add: initial POC

This commit is contained in:
jez 2023-10-06 19:16:30 -04:00
commit dcb27eecc1
23 changed files with 5649 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/*
__pycache__
.direnv/*

119
README.md Normal file
View File

@ -0,0 +1,119 @@
# Shell IAM - Identity Access Manager
Shell IAM is a small python utility that helps you to manage differents
identities along your terminal journey.
The first step is to determine one or more identities; using `home` and `work` is usually a good start.
Then you will be able to attach resources to each identities. A resource can be anything like an username, an email, environment variable, an account (Unix, google, github ...), git author name, SSH keys,
SSH certificates, TLS files, secrets, tokens... You can extend resources types with a powerful plugin system. To each of those resources, you can attach variables and dependencies.
When you finished to list all your user resources in a `iam.yml` config file, you can now start to jump from
one identity to others. Be sure you have correctly installed `iam` in your favorite shell config, because
it hooks to your shell like [direnv](https://direnv.net) does.
To enable an identity, simple run `iam enable home` in your shell session. All your resources related to this
identity are now available in your shell session. You can switch back and forth between your identities, without being worried about mixing/leaking your secrets and environments. When your done your work, disable
the current identity with `iam disable` or directly switch to another identity with `iam enable work`.
Iam is extensible via a plugin system that allows you to define Services. Each service may provide custom
enable/disable shell scripts, custom commands and more... That let the user to implement virtually
anything. Usually a service will simply load environment variables, but it can also starts process/daemons like a dedicated `ssh-agent`.
## Quickstart
Install iam python package:
```shell
pipx install python-iam
```
Install in your shell:
```shell
iam shell install --shell $SHELL,bash,zsh
```
### Initial setup
Let's create a basic configuration, with 2 identities and two resources:
```yaml
mkdir -p ~/.config/iam/
cat <<EOF > ~/.config/iam/default.yml
idents:
home:
resources:
# Mendatory resource to declare identity
account:home:
input:
user: jdoe
name: John
surname: Doe
email: johnny.d@gmail.com
uses:
- auth.ssh_key:home
# Let's setup a basic ssh_keys
auth.ssh_key:home:
input:
ssh_key_file: ~/.ssh/home/id_rsa
ssh_pub_file: ~/.ssh/home/id_rsa.pub
work:
resources:
# Mendatory resource to declare identity
account:work:
input:
user: john-doe327
name: John
surname: Doe
email: jdoe327@company.com
uses:
- auth.ssh_key:work
- auth.gpg_key:work
# Let's setup a basic ssh_keys
auth.ssh_key:work:
input:
ssh_key_file: ~/.ssh/work/id_rsa
ssh_pub_file: ~/.ssh/work/id_rsa.pub
auth.gpg_key:work:
input:
gpg_key_file: ~/.gpg/work/gpgkey
gpg_pub_file: ~/.gpg/work/gpgkey.pub
EOF
```
You can add any resources kinds listed here:
```shell
iam kind list
iam kind show auth.ssh_key
```
Just be sure the right part, after the colon is unique. You will be able to make custom resources later via the plugin system.
You can inspect your current configuration:
```shell
iam res list
iam res show account
```
Then you can see whats happen:
```shell
iam shell enable home
iam shell enable work
iam shell disable
```

190
iam/app.py Normal file
View File

@ -0,0 +1,190 @@
import glob
import logging
import os
from collections import namedtuple
from copy import copy
from pprint import pprint
from types import SimpleNamespace
from xdg import BaseDirectory
from . import exceptions as error
from .catalog import Catalog
from .framework import DictItem, scoped_ident
from .idents import Idents
from .lib.utils import import_module, open_yaml
from .meta import NAME, VERSION
from .providers import Providers
logger = logging.getLogger(__name__)
IamException = error.IamException
class App:
name = NAME
def __init__(self, ident="_", config_path=None):
# Load application
db = self.load_config_files(config_path=config_path)
self.load_db(db)
self.init_ident(ident)
def init_ident(self, ident):
"Init application with ident"
self.ident_raw = ident
# # Split ident from scope
# scope = ""
# if "/" in ident:
# parts = ident.split("/", 2)
# ident = parts[0]
# scope = parts[1]
view = scoped_ident(ident)
# Save important vars
self.ident_name = view.ident
self.ident_scope = view.scope
# Fetch ident config
if ident == "_" or not ident:
logger.debug("Enable default ident")
self.ident = self.idents.items_class("default")
else:
self.ident = self.idents.get(self.ident_name)
# Load user and catalog
self.catalog = Catalog(self, self.ident)
def load_config_files(self, config_path=None):
"Fetch config from default place"
config_candidates = []
# Load file or config
if config_path is not None:
path = config_path
else:
path = os.path.join(BaseDirectory.xdg_config_home, self.name)
if os.path.isdir(path):
files = glob.glob("*.yml", root_dir=path)
for file_ in files:
file_ = os.path.join(path, file_)
payloads = open_yaml(file_)
for payload in payloads:
config_candidates.append(
(
file_,
payload,
)
)
if len(config_candidates) == 0:
_msg = f"Can't find any yaml configuration in: {path}"
raise error.MissingConfigFiles(_msg)
config = {}
for cand in config_candidates:
path, conf = cand
config.update(conf)
return config
def load_db(self, db):
"Load database"
# Init Databases
self.db = db
self._load_plugins(db.get("plugins", []))
# Load providers
providers_conf = {}
providers_conf.update(self.plugin_providers)
providers_conf.update(db.get("providers", {}))
# pprint (providers_conf)
self.providers = Providers("ConfigProviders", providers_conf)
# Load idents
idents_conf = {
# "_": {},
}
idents_conf.update(db.get("idents", {}))
self.idents = Idents("ConfigIdents", idents_conf)
def _load_plugins(self, plugins_refs):
"Load plugins"
plugins = {}
for plugin_name in plugins_refs:
if not ":" in plugin_name:
plugin_name = plugin_name + ":all"
load_one = False
if not plugin_name.endswith(":all"):
load_one = plugin_name.split(":", 2)[1]
mod = None
try:
mod = import_module(plugin_name)
except ModuleNotFoundError as err:
msg = f"Impossible to load module: {plugin_name}"
raise IamException(msg)
print(err)
continue
if load_one:
mod = {load_one: mod}
plugins.update(mod)
self.plugin_providers = plugins
def user_dump(self, pattern=None):
"Dump ident config"
self.catalog.dump()
def get_idents(self, extended=False):
"Return list of identities"
ret = list(self.idents.get_idents())
if extended:
out = ["mrjk/backups", "mrjk/pictures"]
ret.extend(out)
return ret
# def get_resource(self, name):
# "Query and resolve a resource"
# return self.catalog.get_resource(name)
# # def get_service(self, name, resolve=False):
# # "Query and resolve a service"
# # return self.catalog.get_service(name, resolve=resolve)
# def list_resources(self):
# "List all resources"
# return self.catalog.resources
def list_services(self):
"List all services"
return self.catalog.services
ret = {svc.name: svc.list_cmds() for svc in self.catalog.services.values()}
return ret
def shell_enable(self):
"Enable shell"
return self.catalog.shell_enable()
def shell_disable(self):
"Disable shell"
return self.catalog.shell_disable()

870
iam/catalog.py Normal file
View File

@ -0,0 +1,870 @@
import logging
from collections import namedtuple
from pprint import pprint
from types import SimpleNamespace
from . import exceptions as error
from .framework import DictCtrl, DictItem, KeyValue, KeyValueExtra
from .idents import Ident
from .lib.utils import format_render, jinja_render, uniq
logger = logging.getLogger(__name__)
IamException = error.IamException
# Config classes: Plugin
# ================
# class Plugin(DictItem):
# "Hold provider entities"
# default_attrs = {
# "desc": "Resource without description",
# "input": {},
# "needs": [],
# }
# class Plugins(DictCtrl):
# "Hold provider entities"
# default_attrs = {
# "desc": "Resource without description",
# "input": {},
# "needs": [],
# }
# Config classes: Services
# ================
class ServiceCommand(DictItem):
"Hold provider services command"
default_attrs = {
"desc": "Service without description",
"cmd": "",
}
# Overrides
# ---------------
def payload_transform(self, name, kwargs=None):
"Transform short form into long form"
payload = self._payload
if not isinstance(payload, dict):
payload = {"cmd": payload}
self._payload = payload
class Service(DictItem):
"Hold provider services"
default_attrs = {
"desc": "Service without description",
"enabled": False,
"input": {},
"resources_lookup": [],
"required_services": [],
"commands": {},
# "resolved_resources": [],
"resources_matches": 1,
}
# Overrides
# ---------------
def prepare(self):
self.catalog = self.parent.catalog
def init(self):
ret = {}
for cmd_name, cmd in self.commands.items():
ret[cmd_name] = ServiceCommand(cmd_name, cmd)
self.commands = ret
# def require_resolved_deps(func):
# "Decorator that ensure resource resolution is called before method call"
# def wrap(self, *args, **kwargs):
# "Wrapper that ensure dependencies are resolved"
# if not self._resolved:
# self.resolve_deps(skip_missing=True)
# return func(self, *args, **kwargs)
# return wrap
# Catalog dependent is built !
# ---------------
def resolve_resources(self, catalog):
"Resolve required resources"
# catalog.res_inst,
# catalog.context
self._catalog = catalog
requests = self.resources_lookup
curr_vars = catalog.context.get_vars()
ret = []
for pattern in requests:
req = pattern.format(**curr_vars)
if req in catalog.res_inst:
resource = catalog.res_inst[req]
tmp = {
"resource": resource,
"vars": resource.resolve_inputs(),
}
ret.append(tmp)
self.resolved_resources_names = [x["resource"].name for x in ret]
self._resolved_resources = ret
def _prepare_runtime(self):
if not hasattr(self, "_resolved_resources"):
assert (
self._resolved_resources
), f"Please run {self}.resolve_resources() first"
print("Process each item")
processed_vars = []
# Deduplicate common resources
for box in self._resolved_resources:
res = box["resource"]
name = res.name
curr_vars = res.resolve_inputs()
# WIPPPP
expand_inputs(curr_vars)
var_matches = [x for x in processed_vars if x.value == curr_vars]
if len(var_matches) > 0:
print(f" Skip duplicate resource vars {res.name}")
continue
# print (f" Process: {res.name}")
processed_vars.append(KeyValueExtra(name, curr_vars, res))
# print("PROCESSEDS")
# pprint(processed_vars)
return processed_vars
# Workflow
# ---------------
def run_cmd(self, command=None):
processed_vars = self._prepare_runtime()
cmd = self.commands.get(command, None)
if not cmd:
choices = (
",".join(self.commands.keys())
or "No available commands for this service"
)
raise IamException(
f"Unknown command: {command}, please choose one of: {choices}"
)
svc_input = self.input
# Run on each requested resources
max_items = self.resources_matches
for res in processed_vars[0:max_items]:
print("process item:", res.key)
# if command:
# pprint (res.value)
# # pprint (self.__dict__)
# # assert False
# pprint (self._catalog.context.get_vars())
env_var = {}
env_var.update(svc_input)
env_var.update(res.value) # == res.resolve_inputs()
env_var.update(self._catalog.context.get_vars())
# pprint (env_var)
try:
out = cmd.format(**env_var)
except KeyError as err:
msg = f"Missing variable: {err} for service: {res.key}"
# print (msg)
out = msg
# raise IamException(msg)
print("\n" + "-- %<--" * 6)
print(out)
print("-- %<--" * 6)
# def _list_cmds_shell(self):
# "Li"
# def _list_cmds(self):
def list_cmds(self):
"List all services commands"
prefix = self.name.split(".")[-1]
shell_prefix = "shell_"
ret2 = {
"shell": {},
"cmds": {},
}
for cmd_name, conf in self.commands.items():
target = ret2["cmds"]
if cmd_name.startswith(shell_prefix):
target = ret2["shell"]
cmd_name = cmd_name[len(shell_prefix) :]
name = f"{prefix} {cmd_name}"
target[name] = conf.desc
return ret2
def get_linked_resource(self):
"Return linked resource or None"
catalog = self.catalog
res_name = f"service.{self.name}:{catalog.ident.name}"
ret = catalog.resources.get(res_name, None)
logger.debug(f"Linked resource for service {self.name}: {ret}")
return ret
def is_active(self):
return True if self.get_linked_resource() is not None else False
class Services(DictCtrl):
"Simple wrapper class to manage services"
items_class = Service
def prepare(self):
self.catalog = self.parent
def get_linked_resources(self):
"""
Like get method, but only grab enabled services
Return a list of an object containing service and its
matched resource
"""
enabled_services = {}
for name, service in self.items():
res = service.get_linked_resource()
if res:
enabled_services[service.name] = SimpleNamespace(
svc=service,
res=res, # order=None
)
return enabled_services
def get_loading_order(self, reverse=False):
"Process resolution order"
# Filter out disabled services
enabled_services = self.get_linked_resources()
# Process resolution order
queue_todo = list(enabled_services.keys())
queue_done = []
loop_index = 0
while len(queue_todo) > 0:
loop_index = loop_index + 1
_queue_todo = list(queue_todo)
for svc_name in queue_todo:
service = enabled_services[svc_name].svc
resource = enabled_services[svc_name].res
# Check services requirements
required_services = list(set(service.required_services))
# if not svc_name.startswith("service.id:"):
if svc_name != "id":
required_services.insert(0, f"id")
missing = False
_missing = []
for req in required_services:
if not req in queue_done:
missing = True
_missing.append(req)
if not missing:
# Save result
queue_todo.remove(svc_name)
queue_done.append(svc_name)
if queue_todo == _queue_todo:
print("Loop Index ERROR", loop_index)
raise IamException(f"Loop detexted ! {loop_index}")
if reverse:
queue_done.reverse()
for index, svc_name in enumerate(queue_done):
enabled_services[svc_name].order = index
# Return list of ordered service names, and other useful data
return queue_done, enabled_services
# Config classes: ResourcesKinds
# ================
ResolvedRes = namedtuple("ResolvedRes", "name inputs")
class ResourceKind(DictItem):
"Hold resource kinds"
default_attrs = {
"desc": "ResourceKind without description",
"input": {},
"needs": [],
"remap": {},
}
# def config_validate(self):
# if ":" in self.name:
# raise IamException(
# "Colons are not accepted for catalog resources", self.__dict__
# )
class ResourcesKinds(DictCtrl):
"Simple wrapper class to manage resources kinds"
items_class = ResourceKind
# def prepare(self):
# self.catalog = self.parent
# Config classes: Resources
# ================
class Resource(DictItem):
"Create a new resource"
default_attrs = {
"desc": "",
"input": {},
"uses": [], # Optional
"needs": [], # Required dependencies AND
"loop_limit": 1,
"loop": [],
"_ctx_vars": {},
}
# Overrides
# ---------------
def prepare(self):
self.catalog = self.parent.catalog
def config_validate(self):
# Fetch info from controller
resources_kind = self.parent.available_resources_kinds
loaded_resources = self.parent.loaded_resources
# Check overrides
if self.name in loaded_resources:
_msg = f"User resource overrides: {self.name}"
logger.debug(_msg)
# Check for the kind
kind = self.get_kind()
if not kind in resources_kind:
choices = ", ".join(resources_kind.keys())
_msg = (
f"Unknown resource kind: {self.name}, please choose one of: {choices}"
)
raise error.UnknownResourceKind(_msg)
def init(self):
"Init each resources"
self.name_raw = self.name
ctx_vars = self.parent.ctx_vars
self.name = format_render(self.name, vars=ctx_vars)
# self.name = format_render(self.name, vars=self.catalog.context.get_vars())
# Append common base dependency on all resources
if len(self.uses) == 0:
self.uses.insert(0, "account:{user}")
# self.uses = list(set(self.uses))
self.uses = uniq(self.uses)
# Resolution management vars
self._resolved = False
self.resources_deps = None
self.resources_missing = None
# Helpers
# ---------------
def _parse_name(self):
ret = self.name.split(":", 2)
if len(ret) == 2:
return ret[0], ret[1]
# Raise error if name is invalid
_msg = f"Invalid resource name: {self.name}, missing :<name>"
raise IamException(_msg)
def get_kind(self):
"Return resource kind"
ret, _ = self._parse_name()
return ret
def get_name(self):
"Return resource instance name"
_, ret = self._parse_name()
return ret
# Methods
# ---------------
def require_resolved_deps(func):
"Decorator that ensure resource resolution is called before method call"
def wrap(self, *args, **kwargs):
"Wrapper that ensure dependencies are resolved"
if not self._resolved:
self.resolve_deps(skip_missing=True)
return func(self, *args, **kwargs)
return wrap
def resolve_deps(self, vars=None, add_self=True, cache=True, skip_missing=False):
"Recursive uses dependency resolver"
if cache and self.resources_deps is not None:
return self.resources_deps
# try:
# ret, _missing = self._resolve_deps(vars=vars, add_self=add_self)
# except error.UnresolvedResourceDependencies:
# return None
dependencies, missings = self._resolve_deps(vars=vars, add_self=add_self)
self.resources_missing = missings
self.resources_deps = dependencies
self._is_active = True if len(missings) == 0 else False
# Raise error on missing deps
if not skip_missing and len(missings) > 0:
pprint(self.catalog.resources.get())
_msg = f"Missing deps for {self.name}: {missings}"
raise error.UnresolvedResourceDependencies(_msg)
if cache:
self._resolved_deps = dependencies
self._resolved = True
return dependencies
def _resolve_deps(self, vars=None, add_self=True, _results=None, _missings=None):
"Recursive dependency helper"
# Recurisive control
_missings = _missings or []
_results = _results or []
if self in _results:
return _results
# Get root context
# ctx_vars = vars or self.catalog.context.get_vars()
catalog = self.catalog
ctx_vars = catalog.context.get_vars()
# Check each dependency
uses = self.uses
for dep_name in uses:
# Parse resource name
if ctx_vars:
dep_name = format_render(dep_name, ctx_vars)
if dep_name == self.name:
continue
if dep_name in _missings:
_missings.delete(dep_name)
# Load child or quit if missing
child = catalog.resources.find_closest(dep_name)
if not child:
_missings.append(dep_name)
# _missings.append((self.name, dep_name, ))
# _msg = f"Undeclared resource: {dep_name}"
# raise error.UnresolvedResourceDependencies(_msg)
continue
# Avoid already processed resources
if child and child in _results:
continue
_results.append(child)
child._resolve_deps(vars=vars, _results=_results)
if add_self:
_results.append(self)
return _results, _missings
# Resolved methods
# ==================
@require_resolved_deps
def is_active(self):
"Return True if this resource is active"
return self._is_active
# return True if len(self.resources_missing) == 0 else False
@require_resolved_deps
def resolve_inputs(self, vars=None):
"Resolve uses dependencies recursively"
# WIPPPP, split this function in 2 !!!
# self.catalog.
# deps = self.resolve_deps()
deps = self.resources_deps
output = {}
for res in deps:
output.update(res.input)
return output
def loop_extend(self):
"Extend loops from catalog"
ctx_vars = self.catalog.context.get_vars()
loops = self.loop
loop_limit = self.loop_limit
matches = []
stop = False
for loop_search in loops:
if len(matches) >= loop_limit:
stop = True
if not stop:
loop_search = loop_search.format(**ctx_vars)
res = self.catalog.resources.get(loop_search, skip_missing=True)
if res:
matches.append(res)
return matches
class Resources(DictCtrl):
"Simple wrapper class to manage resources"
items_class = Resource
# Overrides
# ---------------
def prepare(self):
# Save Catalog
self.catalog = self.parent
self.ctx_vars = self.catalog.context.get_vars()
self.available_resources_kinds = self.catalog.resources_kind
self.loaded_resources = []
# pprint (self.__dict__)
# print ("INIT RES CATALOG")
def find_closest(self, dep_name):
"Find closest match on type"
assert ":" in dep_name, f"We have unresolved query here: {dep_name}"
catalog = self.catalog
# Direct access
child = catalog.resources.get(dep_name, None)
if child:
return child
# Loop over resources
target = dep_name.split(":")[0]
scope = dep_name.split(":")[1]
match = None
while target:
# Query other resources
# query_name = f"{target}:{scope}"
matches = catalog.resources.select("startswith", target)
matches = {
key: val for key, val in matches.items() if key.endswith(f":{scope}")
}
# Check closest matches
if len(matches) == 1:
match = list(matches.values())[0]
# print ("Longer MATCH", match.name)
logger.info(
f"Closest resource of {dep_name} was mapped to: {match.name}"
)
break
elif len(matches) > 1:
logger.debug(
f"More than one closest resources for {dep_name}: {matches}"
)
# raise Exception(f"Bug here, too many matches: {matches}")
# Reduce again the common lookup pattern
if "." in target:
target = ".".join(target.split(".")[:-1])
else:
break
# print ("QUERY", target, scope)
return match
# view = scoped_ident(ident)
# Catalog
# ============================================================
# Context classes
# ================
class Context(DictItem):
"Class that hold a context"
default_attrs = {
"ident": None,
}
# Overrides
# ---------------
def config_validate(self):
assert isinstance(self.ident, Ident), f"Gotr: {self.ident}"
def init(self):
self._vars = {
"ident": self.ident.name,
"user": self.ident.name,
}
def get_vars(self):
"Return all vars context"
return dict(self._vars)
# Catalog classes
# ================
class Catalog:
"Manage catalog resources"
def __init__(self, mgr, ident):
# Prepare catalog
self.mgr = mgr
self.ident = ident
# Prepare context
ctx_conf = {
"ident": ident,
}
self.context = Context("current_context", ctx_conf)
self._prepare_catalog()
# Catalog building classes
# ================
def _merge_inst_configs(self, merge=True):
plugins_confs = self.mgr.providers.get_resource_configs()
user_confs = self.ident.get_resource_configs()
final_config = {}
# Remap config names
for source in [plugins_confs, user_confs]:
# source = source or {}
for name, conf in source.items():
# Check name
if not ":" in name:
ident_name = f"{name}:{self.ident.name}"
# print (f"Renaming resource '{name}' to '{ident_name}'")
name = ident_name
# Check config
existing = final_config.get(name, None)
if existing:
# print (f"Overrides: {name}")
if merge:
new_conf = dict(existing)
new_conf.update(conf)
conf = new_conf
final_config[name] = conf
return final_config
def _prepare_catalog(self):
"Prepare catalog resources configs"
# Get resources from providers and user
providers = self.mgr.providers
ident = self.ident
# Build kind catalog
res_kind_configs = providers.get_resource_kinds()
self.resources_kind = ResourcesKinds(
"CatalogResourcesKinds", payload=res_kind_configs, parent=self
)
# Build resources catalog
res_inst_configs = self._merge_inst_configs()
self.resources = Resources(
"CatalogResources", payload=res_inst_configs, parent=self
)
self._resources_config = res_inst_configs
# Prepare services catalog
services_confs = providers.get_services_configs()
self.services = Services("CatalogServices", payload=services_confs, parent=self)
def dump(self):
print("Catalog settings")
print("=" * 16)
# pprint (self.__dict__)
# return
print("\n=== Catalog resources kinds:")
pprint(self.resources_kind)
print("\n=== Catalog resources configs:")
pprint(self._resources_config)
print("\n=== Catalog resources instances:")
pprint(self.resources)
print("\n=== Catalog services instances:")
# for name, srv in self.services.items():
# pprint(srv.dump_item())
pprint({name: srv.dump_item() for name, srv in self.services.items()})
# Catalog interfaces
# ================
# Catalog helpers
# ================
# def resolve_service_order(self, reverse=False):
# "Resolve service order"
# return self.services.get_loading_order(reverse=reverse)
# Shell helpers
# ================
def shell_enable(self):
"Enable shell"
return self._shell_action()
def shell_disable(self):
"Disable shell"
return self._shell_action(reverse=True)
def _shell_action(self, reverse=False):
"Run command order"
ident = self.ident
vars = self.context.get_vars()
# Fetch execution order
order, services = self.services.get_loading_order(reverse=reverse)
# Prepare context
action_name = "shell_enable"
log_action_name = "Enable"
if reverse:
action_name = "shell_disable"
log_action_name = "Disable"
logger.info(f"{log_action_name} identity: {ident.name}")
# Execute on each plugins commands
output_code = []
for srv_name in order:
service = services[srv_name].svc
res = services[srv_name].res
# Load service command
command = service.commands.get(action_name, None)
if not command:
continue
# Build loop with at least ONE item
cmd = command.cmd
if not cmd:
continue
# Create context var dict
ctx_vars = dict()
ctx_vars.update(service.input)
ctx_vars.update(vars)
ctx_vars.update(res.resolve_inputs(vars=ctx_vars))
loops = res.loop_extend() or [res]
res_vars = [x.resolve_inputs(vars=ctx_vars) for x in loops]
ctx_vars.update(
{
"item": res_vars[0],
"loop": res_vars,
}
)
logger.debug(f"{log_action_name} service: {res.name}")
# pprint (ctx_vars)
cmd = jinja_render(cmd, ctx_vars)
output_code.append(f"# Loading of {res.name} ({service.name})")
output_code.append(f"# =====================")
output_code.append(cmd)
output_code.append("\n")
return "\n".join(output_code)

585
iam/cli.py Normal file
View File

@ -0,0 +1,585 @@
# Better case
import logging
import os
import sys
from enum import Enum
from pprint import pformat, pprint
from types import SimpleNamespace
import coloredlogs
import typer
from typing_extensions import Annotated
from iam.app import App
from iam.framework import (empty, get_app_logger, iterate_any, open_yaml,
prune, to_csv, to_json, to_yaml)
from rich import box
from rich.console import Console
from rich.pretty import pprint
from rich.prompt import Prompt
from rich.table import Table
from . import exceptions as error
# from .cli_views import OutputFormat #, view_list, view_show
from .cli_views import ViewItem, ViewList
IamException = error.IamException
# Global vars
# ======================
# See: https://docs.python.org/3.8/library/logging.html#logging-levels
DEFAULT_LOG_LEVEL = 30 # warning
# Get default ident
DEBUG = os.environ.get("IAM_DEBUG", "False")
DEFAULT_IDENT = os.environ.get("IAM_IDENT", os.environ.get("SHELL_IDENT", ""))
DEFAULT_CONFIG = os.environ.get("IAM_CONFIG", None)
BOX_STYLE = box.MINIMAL
TABLE_MIN_WIDTH = 80
table_box_params = {
"box": box.ASCII2,
"min_width": 60,
}
# Init app
# ======================
logger = logging.getLogger(__name__)
console = Console()
item_view = ViewItem(output=console.print)
list_view = ViewList(output=console.print)
# General CLI
# ======================
cli = typer.Typer(rich_markup_mode="rich")
@cli.callback()
def cli_callback(
ctx: typer.Context,
verbose: Annotated[int, typer.Option("--verbose", "-v", count=True, max=3)] = 0,
config: Annotated[
str, typer.Option("--config", "-C", help="Configuration directory")
] = DEFAULT_CONFIG,
fmt: Annotated[
list_view.formats_enum, typer.Option("--format", "-F", help="Output format")
] = list_view.default_format,
ident: str = DEFAULT_IDENT,
):
# Calculate log level
log_level = DEFAULT_LOG_LEVEL - verbose * 10
log_level = log_level if log_level > 0 else 10
# Define loggers
loggers = {
"iam": {"level": log_level},
"iam.cli": {"level": "INFO"},
}
# Instanciate logger
get_app_logger(loggers=loggers, level=log_level, colors=True)
cli_config = SimpleNamespace(
log_level=log_level,
fmt=fmt,
ident=ident,
)
render_config = {
"fmt": fmt,
"table_settings": table_box_params,
}
render_item = lambda *args, conf={}: item_view.render(
*args, conf={**render_config, **conf}
)
render_list = lambda *args, conf={}: list_view.render(
*args, conf={**render_config, **conf}
)
# Instanciate app
logger.info(f"Current ident: {ident}")
ctx.obj = SimpleNamespace(
app=App(config_path=config, ident=ident),
cli=cli_config,
render_item=render_item,
render_list=render_list,
)
@cli.command("dump")
def main_dump(ctx: typer.Context):
"Dump ident configuration"
ctx.obj.app.user_dump()
@cli.command("help")
def main_dump(ctx: typer.Context):
"Dump ident configuration"
# pprint(ctx.parent.__dict__)
# pprint (cli.__dict__)
# pprint (dir(cli))
# # help(cli.__class__)
print("commands")
for item in cli.registered_commands:
pprint(item.__dict__)
# get_help(ctx)
# print ("OUTTT")
# pprint ((cli.registered_commands[0]))
# pprint ((cli.registered_commands[0].__dict__))
@cli.command("list")
def main_list(ctx: typer.Context):
"List all items"
ctx.invoke(kind_list, ctx)
ctx.invoke(res_list, ctx)
ctx.invoke(svc_list, ctx)
# Resource Kinds management
# ======================
# cli_kind = typer.Typer()
class KindCmd(str, Enum):
"Kinds sub commands"
LS = "list"
SHOW = "show"
@cli.command(
"kind",
hidden=True,
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def cli_kind(ctx: typer.Context, cmd: KindCmd):
"""Command sources"""
prefix = "kind_"
func = globals()[f"{prefix}{cmd.value}"]
return ctx.invoke(func, ctx)
@cli.command("kind list", rich_help_panel="Resources Kind")
def kind_list(
ctx: typer.Context,
name: Annotated[str, typer.Argument()] = "",
):
"List resource kinds"
# Fetch data
filter = name
resources_kind = ctx.obj.app.catalog.resources_kind
data = []
for name, item in sorted(resources_kind.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data.append((name, item.desc))
# Render data
columns = ["name", "desc"]
conf = {
"table_title": "Resources kinds listing",
}
ctx.obj.render_list(data, columns, conf=conf)
@cli.command("kind show", rich_help_panel="Resources Kind")
def kind_show(
ctx: typer.Context,
name: Annotated[str, typer.Argument()] = "",
):
"Show resource kinds"
# Fetch data
columns = ["name", "desc", "input", "needs", "remap"]
filter = name
resources_kind = ctx.obj.app.catalog.resources_kind
for name, item in sorted(resources_kind.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data = [
item.name,
item.desc,
f"{to_yaml(item.input).strip()}",
f"{to_yaml(item.needs)}".strip(),
f"{to_yaml(item.remap)}".strip(),
]
# Render data
conf = {
"table_title": f"Resources kind: {data[0]}",
}
ctx.obj.render_item(data, columns, fmt=fmt, conf=conf)
# Resource management
# ======================
# cli_res = typer.Typer()
class ResCmds(str, Enum):
"Resources sub commands"
LS = "list"
SHOW = "show"
@cli.command(
"res",
hidden=True,
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def cli_res(ctx: typer.Context, cmd: ResCmds):
"""Command sources"""
pprint(ctx.__dict__)
prefix = "res_"
func = globals()[f"{prefix}{cmd.value}"]
ctx.invoke(res_list, ctx)
# ctx.invoke(func, ctx)
@cli.command("res list", rich_help_panel="Resources")
def res_list(
ctx: typer.Context,
name: Annotated[str, typer.Argument()] = "",
):
"List resource kinds"
# Fetch data
filter = name
resources = ctx.obj.app.catalog.resources
data = []
for name, res in sorted(resources.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data.append(
(
res.get_kind(),
res.get_name(),
res.desc,
)
)
# Render data
columns = ["kind", "name", "desc"]
conf = {
"table_title": "Resources listing",
}
ctx.obj.render_list(data, columns, conf=conf)
@cli.command("res show", rich_help_panel="Resources")
def res_show(
ctx: typer.Context,
name: Annotated[str, typer.Argument()] = "",
all: bool = False,
):
"Show resource"
# Fetch data
columns = ["name", "uses", "input"]
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
filter = name
resources = ctx.obj.app.catalog.resources.select("startswith", name)
for name, item in sorted(resources.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
if not all and not item.is_active():
continue
data = [
name,
to_yaml(item.uses).strip(),
to_yaml(item.input).strip(),
]
if all:
data.extend(
[
item.is_active(),
",".join([x.name for x in item.resources_deps]),
",".join(item.resources_missing),
item.loop_limit,
to_yaml(item.loop).strip(),
to_yaml(item.needs).strip(),
]
)
# Render data
ctx.obj.render_item(
data,
columns_all if all else columns,
conf={
"table_title": f"Resources kind: {data[0]}",
},
)
# Services management
# ======================
class ServiceCmds(str, Enum):
"Serviceources sub commands"
LS = "list"
SHOW = "show"
@cli.command(
"svc",
hidden=True,
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def cli_svc(ctx: typer.Context, cmd: ServiceCmds):
"""Command sources"""
prefix = "svc_"
func = globals()[f"{prefix}{cmd.value}"]
return ctx.invoke(func, ctx)
@cli.command("svc list", rich_help_panel="Services")
def svc_list(
ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", all: bool = False
):
# filter: str = ""):
"List services"
columns = ["name", "desc"]
filter = name
services = ctx.obj.app.catalog.services
data = []
for name, res in sorted(services.items(), key=lambda key: key[1].name):
if filter and not name.startswith(filter):
continue
# if not all and not res.is_active():
# continue
data.append(
(
# res.get_kind(),
# res.get_name(),
res.name,
res.desc,
)
)
# Render data
ctx.obj.render_list(
data,
columns,
conf={
"table_title": f"Services listing",
},
)
@cli.command("svc show", rich_help_panel="Services")
def svc_show(
ctx: typer.Context,
name: Annotated[str, typer.Argument()] = "",
all: bool = False,
):
"Show service"
columns = [
"name",
"desc",
"enabled",
"input",
"commands",
"required_svc",
"resource_lookup",
"rest_matches" "dump",
]
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
output = {}
services = ctx.obj.app.catalog.services.select("startswith", name)
for name, svc in services.items():
assert name == svc.name
data = [
svc.name,
svc.desc,
svc.enabled,
svc.input,
to_yaml(svc.list_cmds()).strip(),
svc.required_services,
svc.resources_lookup,
svc.resources_matches,
svc.dump_item(),
]
data = [str(item) for item in data]
ctx.obj.render_item(
data,
columns_all if all else columns,
conf={
"table_title": f"Service show: {data[0]}",
},
)
@cli.command("svc commands", rich_help_panel="Services")
def svc_commands(ctx: typer.Context):
"List services commands"
ret = {
"shell": {},
"cmds": {},
}
for svc_name, svc in ctx.obj.app.list_services().items():
cmds = svc.list_cmds()
for source in ["shell", "cmds"]:
items = cmds[source]
target = ret[source]
for cmd_name, conf in items.items():
target[cmd_name] = conf
pprint(ret, expand_all=True)
@cli.command("svc run", rich_help_panel="Services")
def svc_run(ctx: typer.Context, name: str, command: str):
"Run service command"
ret = ctx.obj.app.services.get(name, resolve=True)
tmp = ret.run_cmd(command)
pprint(tmp)
# Shell management
# ======================
class ShellCmds(str, Enum):
"Shellources sub commands"
ENABLE = "enable"
DISABLE = "disable"
ORDER = "order"
IDENTS = "idents"
@cli.command(
"shell",
hidden=True,
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def cli_shell(ctx: typer.Context, cmd: ShellCmds):
"""Command sources"""
prefix = "shell_"
func = globals()[f"{prefix}{cmd.value}"]
return ctx.forward(func)
# return ctx.invoke(func, ctx)
@cli.command("shell idents", rich_help_panel="Shell")
def shell_idents(ctx: typer.Context, filter: str = ""):
"List idents"
ret = ctx.obj.app.get_idents()
print(" ".join(ret))
# pprint(ret, expand_all=True)
@cli.command("shell order", rich_help_panel="Shell")
def shell_order(ctx: typer.Context, reverse: bool = False):
"List services by loading order"
table = Table(title="Identity services", **table_box_params)
table.add_column("Order", style="magenta")
table.add_column("Service", style="cyan") # , justify="right", , no_wrap=True)
# services , _ = ctx.obj.app.catalog.resolve_service_order(reverse=reverse)
services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse)
for index, name in enumerate(services):
table.add_row(str(index), name)
console.print(table)
@cli.command("shell enable", rich_help_panel="Shell")
def shell_enable(ctx: typer.Context, ident):
"Enable shell"
app = ctx.obj.app
# Disable existing
# logger.info(f"Disabling ident: {app.ident_name}")
ret1 = app.shell_disable()
print("-- %< --" * 8)
print(ret1)
print("-- %< --" * 8)
# Enable new
# logger.info(f"Enabling ident: {ident}")
app.init_ident(ident)
ret2 = app.shell_enable()
# Output
print("-- %< --" * 8)
print(ret2)
print("-- %< --" * 8)
@cli.command("shell disable", rich_help_panel="Shell")
def shell_disable(ctx: typer.Context):
"Disable shell"
ret = ctx.obj.app.shell_disable()
print("-- %< --" * 8)
print(ret)
print("-- %< --" * 8)
# Merge all subcommands
# ======================
# cli.add_typer(cli_shell, name="shell", help="Manage shell")
# cli.add_typer(cli_res, name="res", help="Manage resources")
# cli.add_typer(cli_svc, name="svc", help="Manage services")
# cli.add_typer(cli_kind, name="kind", help="Manage resource kind")
import traceback
def run():
"Run cli"
if DEBUG.lower() in ["true", "y", "1", "t"]:
cli()
else:
try:
cli()
except IamException as err:
_msg = f"Iam exited with {type(err).__name__} error: {err}"
logger.error(_msg)
sys.exit(1)
# traceback.print_stack()
# logger.warning
# Cli bootsrapping
# ======================
if __name__ == "__main__":
run()

827
iam/cli_click.py Executable file
View File

@ -0,0 +1,827 @@
#!/usr/bin/env python3
import io
import logging
import os
import sys
from enum import Enum
from pprint import pprint
from types import SimpleNamespace
from iam.app import App
from iam.framework import get_app_logger
from . import exceptions as error
from .lib.cli_views import VIEW_FORMATS_NAMES, ViewItem, ViewList
from .lib.click_utils import NestedHelpGroup
# from .cli_views import ViewItem, ViewList, VIEW_FORMATS
from .lib.utils import prune, to_yaml
# import rich
# from rich import box
# from rich.console import Console
# from rich.pretty import pprint
# from rich.prompt import Prompt
# from rich.table import Table
# from rich.pretty import pprint
# Rich loader
# ======================
APP_RICH = True
table_box_params = {}
if not APP_RICH:
import click
# Create console
console = SimpleNamespace(print=print)
else:
# Import rich
# Transparent rich proxy for click (help menus)
import rich_click as click
from rich_click import RichGroup
from rich import box, print
from rich.console import Console
# Overrides defaults
from rich.pretty import pprint
# Load rich wrappers
class NestedRichHelpGroup(NestedHelpGroup, RichGroup):
"Add rich class"
NestedHelpGroup = NestedRichHelpGroup
# Create default rich table settings
table_box_params = {
"box": box.ASCII2, # box.MINIMAL
"min_width": 60,
}
# Create a console
console = Console()
logger = logging.getLogger()
# Global vars
# ======================
# Init app
# ======================
logger = logging.getLogger(__name__)
item_view = ViewItem(output=console.print)
list_view = ViewList(output=console.print)
# General App settings
# ===============================
APP_NAME = "iam"
APP_VERSION = "__version__TOFIX"
APP_EXCEPTION = error.IamException
# See: https://docs.python.org/3.8/library/logging.html#logging-levels
DEFAULT_LOG_LEVEL = 30 # warning
# Get default ident
DEBUG = os.environ.get("IAM_DEBUG", "False")
DEFAULT_IDENT = os.environ.get("IAM_IDENT", os.environ.get("SHELL_IDENT", ""))
DEFAULT_FORMAT = os.environ.get("IAM_FORMAT", "table")
# list_view.formats_enum
# Click helpers
# ===============================
_cmd1_options = [click.option("--format")]
_cmd2_options = [click.option("--cmd2-opt")]
CONTEXT_SETTINGS = dict(
show_default=True,
help_option_names=["-h", "--help"],
auto_envvar_prefix=APP_NAME.upper(),
)
def global_options(function):
# function = click.option(
# "debug", "--debug",
# is_flag=True, show_default=False, default=False,
# help="Debug"
# )(function)
# function = click.option(
# "force", "--force", "-f"
# is_flag=True, show_default=False, default=False,
# help="Force"
# )(function)
# function = click.option(
# "interactive", "--interactive", "-i"
# is_flag=True, show_default=False, default=False,
# help="Interactive mode"
# )(function)
# function = click.option(
# "recursive", "--recursive", "-r"
# is_flag=True, show_default=False, default=False,
# help="Recursive mode"
# )(function)
function = click.option(
"quiet",
"--quiet",
"-q",
is_flag=True,
show_default=False,
default=False,
help="Suppress output except warnings and errors.",
)(function)
function = click.option(
"log_trace",
"--trace",
is_flag=True,
show_default=False,
default=False,
help="Show traceback on errors",
)(function)
function = click.option(
"-v",
"--verbose",
count=True,
type=click.IntRange(0, int(DEFAULT_LOG_LEVEL / 10), clamp=True),
help="Increase verbosity of output. Can be repeated.",
)(function)
return function
def format_options(function):
# function = click.option(
# 'dry_run', '-n', '--dry-run',
# is_flag=True, show_default=False, default=False,
# help="Dry run, do not take any actions."
# )(function)
# Duplicates
function = click.option(
"-v",
"--verbose",
count=True,
type=click.IntRange(0, int(DEFAULT_LOG_LEVEL / 10), clamp=True),
help="Increase verbosity of output. Can be repeated.",
)(function)
# Special output options
function = click.option(
"fmt_name",
"-O",
"--output",
type=click.Choice(VIEW_FORMATS_NAMES, case_sensitive=False),
help="Output format",
default=None,
show_default=DEFAULT_FORMAT,
)(function)
function = click.option(
"fmt_fields", "-H", "--headers", help="Show specific headers"
)(function)
function = click.option("fmt_sort", "-S", "--sort", help="Sort columns")(function)
return function
# Base Application example
# ===============================
# DEFAULT_CONFIG = os.environ.get("IAM_CONFIG", "MISSING")
DEFAULT_CONFIG = click.get_app_dir(APP_NAME)
# SHOW_DEFAULT_HELP=True
def get_var(name, default=None):
real_name = f"{APP_NAME.upper()}_{name.upper()}"
# print ("QUERY", real_name)
return os.environ.get(real_name, default)
# @click.group(cls=NestedHelpGroup, context_settings=CONTEXT_SETTINGS)
@click.group(cls=NestedHelpGroup)
@global_options
@format_options
@click.version_option()
@click.option(
"config",
"--config",
"-C",
# envvar='CONFIG', # multiple=True,
type=click.Path(),
default=get_var("CONFIG", DEFAULT_CONFIG),
show_default=get_var("CONFIG", DEFAULT_CONFIG),
help="Configuration directory or file",
)
@click.pass_context
def cli_app(
ctx,
verbose,
config: str = DEFAULT_IDENT,
ident: str = DEFAULT_IDENT,
quiet=False,
log_trace=False,
fmt_fields=None,
fmt_sort=None,
fmt_name=None,
):
"""Naval Fate.
This is the docopt example adopted to Click but with some actual
commands implemented and not just the empty parsing which really
is not all that interesting.
"""
# Calculate log level
log_level = DEFAULT_LOG_LEVEL - verbose * 10
log_level = log_level if log_level > 0 else 10
# Define loggers
loggers = {
"iam": {"level": log_level},
"iam.cli": {"level": "INFO"},
}
# Instanciate logger
get_app_logger(loggers=loggers, level=log_level, colors=True)
cli_config = SimpleNamespace(
log_level=log_level,
fmt_name=fmt_name or DEFAULT_FORMAT,
ident=ident,
)
render_config = {
"fmt_name": cli_config.fmt_name,
"table_settings": table_box_params,
}
pprint(cli_config)
render_item = lambda *args, conf={}, **kwargs: item_view.render(
*args, conf={**render_config, **prune(conf)}, **kwargs
)
render_list = lambda *args, conf={}, **kwargs: list_view.render(
*args, conf={**render_config, **prune(conf)}, **kwargs
)
# Instanciate app
logger.info(f"Current ident: {ident}")
ctx.obj = SimpleNamespace(
app=App(config_path=config, ident=ident),
cli=cli_config,
render_item=render_item,
render_list=render_list,
)
@cli_app.command()
@click.pass_context
def tests(ctx):
print("Hello world")
return
# pprint(command_tree(cli_app))
print("TREEE @")
pprint(command_tree3(cli_app, ctx))
all_help(cli_app, ctx)
# subcommand_obj = cli_app.get_command(ctx, subcommand)
# if subcommand_obj is None:
# click.echo("I don't know that command.")
# else:
# pprint (subcommand_obj)
# pprint (subcommand_obj.__dict__)
# click.echo(subcommand_obj.get_help(ctx))
# Cli Utils
# ===============================
# Dedicated help command
# Source: https://stackoverflow.com/a/53137265
@cli_app.command()
@click.argument("subcommand", nargs=-1)
@click.pass_context
def help(ctx, subcommand):
"Show a command help"
subcommand = " ".join(subcommand)
subcommand_obj = cli_app.get_command(ctx, subcommand)
if subcommand_obj is None:
click.echo("I don't know that command.")
else:
pprint(subcommand_obj)
pprint(subcommand_obj.__dict__)
click.echo(subcommand_obj.get_help(ctx))
# Global commands
# ===============================
@cli_app.command()
@click.pass_context
def dump(ctx):
ctx.obj.app.user_dump()
# Resource kind management
# ===============================
@cli_app.group("kind")
def cli__kind():
"""Manages Resources Kind."""
@cli__kind.command("list")
@click.argument("filter", required=False)
@format_options
@click.pass_context
def kind_list(ctx, filter, fmt_name=None, fmt_sort=None, fmt_fields=None, verbose=None):
"List resource kinds"
# Fetch data
# filter = name
resources_kind = ctx.obj.app.catalog.resources_kind
data = []
for name, item in sorted(resources_kind.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data.append((name, item.desc))
# Render data
columns = ["name", "desc"]
conf = {
"table_title": "Resources kinds listing",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
}
ctx.obj.render_list(data, columns, conf=conf)
@cli__kind.command("show")
@click.argument("name", required=False)
@format_options
@click.pass_context
def kind_show(ctx, name, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"Show resource kind"
# Fetch data
columns = ["name", "desc", "input", "needs", "remap"]
filter = name
resources_kind = ctx.obj.app.catalog.resources_kind
for name, item in sorted(resources_kind.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data = [
item.name,
item.desc,
f"{to_yaml(item.input)}",
f"{to_yaml(item.needs)}",
f"{to_yaml(item.remap)}",
]
# Render data
conf = {
"table_title": f"Resources kind: {data[0]}",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
}
ctx.obj.render_item(data, columns, conf=conf)
# @cli__kind.command("move")
# @click.argument("cli__kind")
# @click.argument("x", type=float)
# @click.argument("y", type=float)
# @click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
# def ship_move(cli__kind, x, y, speed):
# """Moves SHIP to the new location X,Y."""
# click.echo(f"Moving cli__kind {cli__kind} to {x},{y} with speed {speed}")
# @cli__kind.command("shoot")
# @click.argument("cli__kind")
# @click.argument("x", type=float)
# @click.argument("y", type=float)
# def ship_shoot(cli__kind, x, y):
# """Makes SHIP fire to X,Y."""
# click.echo(f"Ship {cli__kind} fires to {x},{y}")
# Resource res management
# ===============================
@cli_app.group("res")
def cli__res():
"""Manages Resources."""
@cli__res.command("list")
@click.argument("filter", required=False)
# @format_options
@click.pass_context
def res_list(ctx, filter, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"List resource ress"
# Fetch data
resources = ctx.obj.app.catalog.resources
data = []
for name, res in sorted(resources.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
data.append(
(
res.get_kind(),
res.get_name(),
res.desc,
)
)
# Render data
columns = ["kind", "name", "desc"]
conf = {
"table_title": "Resources listing",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
}
ctx.obj.render_list(data, columns, conf=conf)
@cli__res.command("show")
@click.argument("name", required=False)
@format_options
@click.pass_context
def res_show(ctx, name, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"Show resource res"
# Fetch data
columns = ["name", "uses", "input"]
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
filter = name
resources = ctx.obj.app.catalog.resources.select("startswith", name)
for name, item in sorted(resources.items(), key=lambda key: key):
if filter and not name.startswith(filter):
continue
if not all and not item.is_active():
continue
data = [
name,
to_yaml(item.uses).strip(),
to_yaml(item.input).strip(),
]
if all:
data.extend(
[
item.is_active(),
",".join([x.name for x in item.resources_deps]),
",".join(item.resources_missing),
item.loop_limit,
to_yaml(item.loop).strip(),
to_yaml(item.needs).strip(),
]
)
# Render data
ctx.obj.render_item(
data,
columns_all if all else columns,
conf={
"table_title": f"Resources kind: {data[0]}",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
# Resource svc management
# ===============================
@cli_app.group("svc")
def cli__svc():
"""Manages Resources Kind."""
@cli__svc.command("list")
@click.argument("filter", required=False)
@format_options
@click.pass_context
def svc_list(ctx, filter, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"List services"
columns = ["name", "desc"]
services = ctx.obj.app.catalog.services
data = []
for name, res in sorted(services.items(), key=lambda key: key[1].name):
if filter and not name.startswith(filter):
continue
# if not all and not res.is_active():
# continue
data.append(
(
# res.get_kind(),
# res.get_name(),
res.name,
res.desc,
)
)
# Render data
ctx.obj.render_list(
data,
columns,
conf={
"table_title": f"Services listing",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
@cli__svc.command("show")
@click.argument("name", required=False)
@format_options
@click.pass_context
def svc_show(ctx, name, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"Show service"
columns = [
"name",
"desc",
"enabled",
"input",
"commands",
"required_svc",
"resource_lookup",
"rest_matches" "dump",
]
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
output = {}
services = ctx.obj.app.catalog.services.select("startswith", name)
for name, svc in services.items():
assert name == svc.name
data = [
svc.name,
svc.desc,
svc.enabled,
svc.input,
to_yaml(svc.list_cmds()).strip(),
svc.required_services,
svc.resources_lookup,
svc.resources_matches,
svc.dump_item(),
]
data = [str(item) for item in data]
ctx.obj.render_item(
data,
columns_all if all else columns,
conf={
"table_title": f"Service show: {data[0]}",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
@cli__svc.command("commands")
@format_options
@click.pass_context
def svc_commands(ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None):
"Show service"
columns = ["name", "type", "desc"]
data = []
for svc_name, svc in ctx.obj.app.list_services().items():
cmds = svc.list_cmds()
for source in ["shell", "cmds"]:
items = cmds[source]
# target = ret[source]
for cmd_name, conf in items.items():
# target[cmd_name] = conf
data.append([cmd_name, source, conf])
# pprint(ret)
# pprint(ret, expand_all=True)
# Render data
ctx.obj.render_list(
data,
columns,
conf={
"table_title": f"Services commands",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
@cli__svc.command("run")
@click.argument("name")
@click.argument("command")
@format_options
@click.pass_context
def svc_run(
ctx, name, command, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None
):
"Show service"
ret = ctx.obj.app.catalog.services.get(name)
tmp = ret.run_cmd(command)
pprint(tmp)
# Shell management
# ======================
@cli_app.group("shell")
def cli__shell():
"""Manages Resources Kind."""
@cli__shell.command("idents")
@format_options
@click.pass_context
def shell_idents(
ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None, reverse=False
):
"Shell identities"
columns = ["name"]
data = [[x] for x in ctx.obj.app.get_idents()]
# print(" ".join(ret))
# return
# columns = ["order", "name"]
# data = []
# services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse)
# for index, name in enumerate(services):
# # table.add_row(str(index), name)
# data.append([str(index), name])
# Render data
ctx.obj.render_list(
data,
columns,
conf={
"table_title": f"Shell identities",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
@cli__shell.command("order")
@click.option(
"--reverse", is_flag=True, show_default=False, default=False, help="Reverse order"
)
@format_options
@click.pass_context
def shell_order(
ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None, reverse=False
):
"Shell loading order"
columns = ["order", "name"]
data = []
services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse)
for index, name in enumerate(services):
# table.add_row(str(index), name)
data.append([str(index), name])
# Render data
ctx.obj.render_list(
data,
columns,
conf={
"table_title": f"Services commands",
"fmt_name": fmt_name,
"fmt_sort": fmt_sort,
"fmt_fields": fmt_fields,
},
)
@cli__shell.command("enable")
@click.argument("ident")
# @format_options
@click.pass_context
def shell_enable(ctx, ident, verbose=None):
"Enable identity in shell"
app = ctx.obj.app
# Disable existing
# logger.info(f"Disabling ident: {app.ident_name}")
ret1 = app.shell_disable()
print("-- %< --" * 8)
print(ret1)
print("-- %< --" * 8)
# Enable new
# logger.info(f"Enabling ident: {ident}")
app.init_ident(ident)
ret2 = app.shell_enable()
# Output
print("-- %< --" * 8)
print(ret2)
print("-- %< --" * 8)
@cli__shell.command("disable")
# @format_options
@click.pass_context
def shell_disable(ctx, verbose=None):
"Disable identity in shell"
ret = ctx.obj.app.shell_disable()
print("-- %< --" * 8)
print(ret)
print("-- %< --" * 8)
# Exception handler
# ===============================
def clean_terminate(err):
"Terminate nicely the program depending the exception"
# Choose dead end way
if APP_EXCEPTION is not None and isinstance(err, APP_EXCEPTION):
err_name = err.__class__.__name__
logger.error(err)
logger.critical("MyApp exited with error: %s", err_name)
sys.exit(1)
# Core application executor
# ===============================
def run():
"Return a MyApp App instance"
try:
cli_app(**CONTEXT_SETTINGS)
# pylint: disable=broad-except
except Exception as err:
clean_terminate(err)
# Developper catchall
raise Exception(err) from err
# logger.error(traceback.format_exc())
# logger.critical("Uncatched error %s; this may be a bug!", err.__class__)
# logger.critical("Exit 1 with bugs")
# sys.exit(1)
if __name__ == "__main__":
run()

192
iam/cli_views.py Normal file
View File

@ -0,0 +1,192 @@
import enum
import logging
from enum import Enum
from pprint import pformat, pprint
from types import SimpleNamespace
from iam.framework import (empty, get_app_logger, iterate_any, open_yaml,
prune, to_csv, to_json, to_yaml)
from rich import box
from rich.console import Console
from rich.pretty import pprint
from rich.prompt import Prompt
from rich.table import Table
from .lib.cli_views import ViewItem, ViewKind, ViewList
# Iam Implementations
# ===============================
MISSING_PKG = []
try:
import hcl2
except ModuleNotFoundError:
MISSING_PKG.append("hcl2")
try:
import yaml
except ModuleNotFoundError:
MISSING_PKG.append("yaml")
# Main views
# ======================
# def view_list(data, columns, title=None, fmt=None, **kwargs):
# "Show list view"
# fmt = fmt or OutputFormat.DEFAULT
# ret = None
# _kwargs = {
# key[len(fmt.value) + 1 :]: val
# for key, val in kwargs.items()
# if key.startswith(fmt.value)
# }
# # print ("YOOO", fmt, OutputFormat.YAML)
# if fmt == OutputFormat.YAML:
# ret = to_yaml(restructure_list_to_dict(data, columns))
# elif fmt == OutputFormat.JSON:
# ret = to_json(restructure_list_to_dict(data, columns), nice=True)
# elif fmt == OutputFormat.PYTHON:
# ret = pformat(restructure_list_to_dict(data, columns), indent=2)
# elif fmt == OutputFormat.CSV:
# ret = to_csv(restructure_list_to_csv(data, columns))
# elif fmt == OutputFormat.ENV:
# ret = to_vars(restructure_list_to_env(data, columns), export=True)
# elif fmt == OutputFormat.VARS:
# ret = to_vars(restructure_list_to_env(data, columns))
# elif fmt == OutputFormat.RICH_TABLE:
# ret = to_rich_table(data, columns=columns, title=title, **_kwargs)
# else:
# raise Exception(f"Unmanagable format: {fmt}")
# console.print(ret)
# def view_show(data, columns=None, title=None):
# "Show single view"
# ret = None
# _kwargs = {
# key[len(fmt.value) + 1 :]: val
# for key, val in kwargs.items()
# if key.startswith(fmt.value)
# }
# # print ("YOOO", fmt, OutputFormat.YAML)
# if fmt == OutputFormat.YAML:
# ret = to_yaml(data)
# elif fmt == OutputFormat.JSON:
# ret = to_json(data, indent=2)
# elif fmt == OutputFormat.PYTHON:
# ret = pformat(data, indent=2)
# elif fmt == OutputFormat.CSV:
# ret = to_csv(data)
# elif fmt == OutputFormat.RICH_TABLE:
# data = list(zip(*data))
# data.insert(0, ["Field", "Value"])
# ret = to_rich_table(data, **_kwargs)
# else:
# raise Exception(f"Unmanagable format: {fmt}")
# return ret
# # DEPRECATED
# # ======================
# def output_list(payload, fmt=None, **kwargs):
# "Render output format"
# assert False, "DEPRECATED"
# fmt = fmt or OutputFormat.YAML
# # Requested format
# # payload = [
# # ["Header1", "Header2"], # First line is always headers
# # [["val1", "val2"]], # First row
# # [["val1", "val2"]], # Second row, etc ...
# # ]
# ret = None
# # _kwargs = {key.lstrip(f"{fmt.value}_"): val for key, val in kwargs.items() if key.startswith(fmt.value)}
# _kwargs = {
# key[len(fmt.value) + 1 :]: val
# for key, val in kwargs.items()
# if key.startswith(fmt.value)
# }
# # print ("YOOO", fmt, OutputFormat.YAML)
# if fmt == OutputFormat.YAML:
# ret = to_yaml(payload)
# elif fmt == OutputFormat.JSON:
# ret = to_json(payload, indent=2)
# elif fmt == OutputFormat.PYTHON:
# ret = pformat(payload, indent=2)
# elif fmt == OutputFormat.CSV:
# ret = to_csv(payload)
# elif fmt == OutputFormat.RICH_TABLE:
# # pprint (kwargs)
# # pprint (_kwargs)
# return to_rich_table(payload, **_kwargs)
# else:
# raise Exception(f"Unmanagable format list: {fmt}")
# assert isinstance(ret, str)
# return ret
# def output_show(payload, fmt=None, **kwargs):
# "Show one item"
# assert False, "DEPRECATED"
# # Requested format
# # payload = [
# # ["Header1", "Header2"], # First line is always headers
# # [["val1", "val2"]], # First row ONLY
# # [["val1", "val2"]], # Second row, etc ...
# # ]
# ret = None
# _kwargs = {
# key[len(fmt.value) + 1 :]: val
# for key, val in kwargs.items()
# if key.startswith(fmt.value)
# }
# # print ("YOOO", fmt, OutputFormat.YAML)
# if fmt == OutputFormat.YAML:
# ret = to_yaml(payload)
# elif fmt == OutputFormat.JSON:
# ret = to_json(payload, indent=2)
# elif fmt == OutputFormat.PYTHON:
# ret = pformat(payload, indent=2)
# elif fmt == OutputFormat.CSV:
# ret = to_csv(payload)
# elif fmt == OutputFormat.RICH_TABLE:
# # Return data
# payload = list(zip(*payload))
# payload.insert(0, ["Field", "Value"])
# ret = to_rich_table(payload, **_kwargs)
# else:
# raise Exception(f"Unmanagable format show: {fmt}")
# return ret

18
iam/exceptions.py Normal file
View File

@ -0,0 +1,18 @@
class IamException(Exception):
"Iam Exception"
class UnresolvedResourceDependencies(IamException):
"Raised when resources dependency is unmet"
class UnknownCatalogItem(IamException):
"Raised when resources dependency is unmet"
class UnknownResourceKind(IamException):
"Raised when resource refers to unexisting kind"
class MissingConfigFiles(IamException):
"Raised when iam can't find any valid configuration file"

355
iam/framework.py Normal file
View File

@ -0,0 +1,355 @@
import importlib
import json
import logging
import os
from collections import namedtuple
from copy import copy
from logging.config import dictConfig
from pprint import pprint
from types import SimpleNamespace
import yaml
from colorama import Back, Fore, Style
from jinja2 import Template
from . import exceptions as error
KeyValue = namedtuple("KeyValue", "key value")
KeyValueExtra = namedtuple("KeyValueExtra", "key value extra")
logger = logging.getLogger(__name__)
# Logging helpers
# ================
def get_app_logger(loggers=None, level="WARNING", colors=False, format="default"):
"Instanciate application logger"
loggers = loggers or {}
# Settings
fclass = "logging.Formatter"
# msconds = ""
if colors:
# Require coloredlogs
fclass = "coloredlogs.ColoredFormatter"
# msconds = "%(msecs)03d"
# Define formatters
formatters = {
"default": {
"()": fclass,
"format": "[%(levelname)s] %(message)s",
# 'datefmt': '%Y-%m-%d %H:%M:%S',
},
"extended": {
"()": fclass,
"format": "[%(levelname)s] %(name)s: %(message)s",
"datefmt": "%H:%M:%S",
},
"audit": {
"()": fclass,
"format": "%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"debug": {
"()": fclass,
"format": "%(msecs)03d [%(levelname)s] %(name)s: %(message)s [%(filename)s/%(funcName)s:%(lineno)d]",
"datefmt": "%H:%M:%S",
},
}
# Assert arguments
if not format in formatters:
choice = ",".join(formatters.keys())
raise Exception(f"Invalid format: '{format}', please choose one of: {choice}")
# Logging config
logging_config = {
"version": 1,
"disable_existing_loggers": True,
# How logs looks like
"formatters": formatters,
# Where goes the logs
"handlers": {
"default": {
"level": level,
"formatter": format,
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr", # Default is stderr
},
},
# Where logs come from
"loggers": {
# Used to catch ALL logs
"": { # root logger
"handlers": ["default"],
"level": "WARNING",
"propagate": False,
},
# # Used to catch all logs of myapp and sublibs
# 'myapp': {
# 'handlers': ['default'],
# 'level': 'INFO',
# 'propagate': False
# },
# # Used to catch cli logs only
# 'myapp.cli': {
# 'handlers': ['default'],
# 'level': 'INFO',
# 'propagate': False
# },
# # Used to catch app components, instanciated loggers
# 'myapp.comp': {
# 'handlers': ['default'],
# 'level': 'DEBUG',
# 'propagate': False
# },
},
}
# Prepare logger components
for name, conf in loggers.items():
logging_config["loggers"][name] = {
"propagate": False,
"handlers": ["default"],
}
logging_config["loggers"][name].update(conf)
# Load logger
logging.config.dictConfig(logging_config)
# Exceptions
# ================
# # DEPRECATED
# class IamException(Exception):
# "Iam Exception"
# Common classes
# ================
def scoped_ident(*args):
"Serialize and deserialize scoped indents"
if len(args) == 1:
ident = args[0]
scope = ""
if "/" in args[0]:
parts = payload.split("/", 2)
ident = parts[0]
scope = parts[1]
return SimpleNamespace(ident=ident, scope=scope)
elif len(args) == 2:
ident = args[0]
scope = args[1]
return f"{ident}/{scope}"
raise Exception(f"Bad arguments for scoped_ident: {args}")
class _DictBase:
default_attrs = {}
payload_type = dict
def __init__(self, name, payload=None, parent=None, **kwargs):
"Init object"
self.name = name
self.parent = parent
self._payload_raw = payload
self._payload = self._payload_raw or {}
self.kwargs = kwargs
# Call internal methods
self.prepare()
self.payload_transform(name, kwargs=kwargs)
self.payload_validate()
self.config_load()
self.config_validate()
self.init()
def __repr__(self):
return f"<{self.__class__.__name__}: {self.name}>"
# Overrides
# ---------------
def prepare(self):
"Prepare object"
def payload_transform(self, name, kwargs=None):
"Transform payload, accessible from self._payload"
def payload_validate(self):
"Validate payload is expected type"
payload = self._payload
payload_type = self.payload_type
if not isinstance(payload, payload_type):
# pprint (payload)
assert False
raise IamException(
f"Invalid configuration for {self}, expected a {payload_type} payload, got {type(payload)} instead: {payload}"
)
def config_load(self):
"Load configuration, raise Exception if not good"
def config_validate(self):
"Validate configuration, raise Exception if not good"
def init(self):
"Init object"
class DictItem(_DictBase):
"Dict Item"
default_attrs = {}
# payload_type = dict
# Methods
# ---------------
def payload_transform(self, name, kwargs=None):
"Transform payload, accessible from self._payload"
self._payload["name"] = name
self._payload.update(kwargs or {})
def config_load(self):
"Load configuration from payload"
# Set default attributes
for key, val in self.default_attrs.items():
setattr(self, key, copy(val))
# Overrides default attributes with payload
payload = self._payload
for key, val in payload.items():
setattr(self, key, val)
def dump_item(self):
ret = {
key: val for key, val in self.__dict__.items() if not key.startswith("_")
}
return ret
class DictCtrl(_DictBase):
"Dict ctrl"
items_class = None
# Overrides
# ---------------
def prepare(self):
"Prepare controller"
# assert False
kwargs = self.kwargs
# Set kwargs
kwargs = kwargs or {}
for key, val in kwargs.items():
# print ("DICT SET:", key, val)
setattr(self, key, val)
def config_load(self):
"Prepare controller"
# Set catalog
self._items = {}
if self.items_class:
# Note: self.items_class is derived from DictItem !!!
for item_name, item in self._payload.items():
# print ("CONFIG_LOAD", item_name)
# pprint (item)
obj = self.items_class(item_name, payload=item, parent=self)
self._items[obj.name] = obj
# Dunders
# ---------------
def __contains__(self, name):
return self.has(name)
# Methods
# ---------------
def items(self):
return self._items.items()
def values(self):
return self._items.values()
def keys(self):
return self._items.keys()
def has(self, name):
"Return true or false if an item exists"
return True if name in self._items else False
def select(self, mode, name):
"Select items on criterion"
name = name or ""
modes = ["startswith", "endswith"]
if name.endswith(":"):
self.get(name[:-1])
assert mode in modes, f"Unsupported mode: {mode}"
ret = {}
for key, value in self._items.items():
validated = False
if mode == "startswith" and key.startswith(name):
validated = True
elif mode == "endswith" and key.endswith(name):
validated = True
if validated:
ret[key] = value
return ret
def get(self, *args):
"Retrieve an item or raise exception if not exists, args: [name [default]]"
# Fetch all items
if len(args) == 0:
return self._items
# Parse argument count
name = None
default = None
use_default = False
if len(args) == 2:
name = args[0]
default = args[1]
use_default = True
elif len(args) == 1:
name = args[0]
# Return result
if not name in self._items:
if use_default:
return default
choices = "\n - ".join(self._items.keys())
msg = f"Missing {self.name} item: {name}, please choose one of:\n - {choices}"
raise error.UnknownCatalogItem(msg)
return self._items[name]
raise IamException(f"Unsupported arguments for .get(), got: {args}")

45
iam/idents.py Normal file
View File

@ -0,0 +1,45 @@
import logging
from pprint import pprint
from . import exceptions as error
from .framework import DictCtrl, DictItem, KeyValue, KeyValueExtra
logger = logging.getLogger(__name__)
class Ident(DictItem):
"Ident instance"
default_attrs = {
"secrets": {},
"resources": {},
"services": {},
}
def init(self):
self.secrets = self._payload.get("secrets", {})
self.resources = self._payload.get("resources", {})
self.services = self._payload.get("services", {})
def get_resource_configs(self):
"Returns resources configurations"
return self.resources or {}
class Idents(DictCtrl):
"Ident controller"
items_class = Ident
def get_services_configs(self):
"Return services configurations"
ret = {}
for prov_name, ident in self.items():
ret.update(ident.services)
return ret
def get_idents(self):
"Return a list of idents"
return list(self._items.keys())

548
iam/lib/cli_views.py Normal file
View File

@ -0,0 +1,548 @@
import enum
import importlib.util
import logging
from pprint import pformat, pprint
from types import SimpleNamespace
from iam.framework import get_app_logger
from .utils import iterate_any, to_csv, to_hcl, to_json, to_toml, to_yaml
# Optional deps
MISSING_PKGS = []
try:
from rich.table import Table
except ModuleNotFoundError:
MISSING_PKGS.append("Table")
# MISSING_PKGS = []
# try:
# from rich.console import Console
# except ModuleNotFoundError:
# MISSING_PKGS.append("Console")
# if not "Console" in MISSING_PKGS:
# console = Console()
# else:
# console
logger = logging.getLogger(__name__)
# Output configuration
# ======================
VIEW_MODULES = {
# "table": "rich",
"yaml": "pyaml",
"toml": "tomllib",
"hcl": ("hcl2", "python-hcl2"),
}
VIEW_FORMATS = {
"DEFAULT": "table",
# CLI
"RICH_TABLE": "table",
# Item
"ENV": "env",
"VARS": "vars",
"CSV": "csv",
"WORDS": "words",
"LINES": "lines",
# Structured
"JSON": "json",
"YAML": "yaml",
"PYTHON": "python",
# "TOML": "toml",
# "HCL": "hcl",
# Shell syntax
# "SHELL": "sh",
# "SHELL_BASH": "bash",
# "SHELL_ZSH": "zsh",
# "SHELL_FISH": "fish",
}
VIEW_FORMATS_NAMES = list(set(VIEW_FORMATS.values()))
# Structurers
# ===============================
def restructure_to_item_view(data, columns):
"Restructure data to fit to item view"
ret = {}
for index, key in enumerate(columns):
value = data[index]
ret[key] = value
return ret
def restructure_to_list_view(data, columns):
"Restructure data to fit to list view"
col_count = len(columns)
ret = []
for row in data:
line = {}
for index in range(col_count):
col = columns[index]
cell = row[index]
line[col] = cell
ret.append(line)
return ret
def restructure_to_csv(data, columns):
"Restructure data to fit to CSV view"
col_count = len(columns)
ret = [columns]
for row in data:
line = []
for key, val in iterate_any(row):
line.append(val)
ret.append(line)
return ret
def restructure_to_env(data, columns, prefix_index=-1, prefix="", count_var="_count"):
"Restructure data to fit to item env/vars view"
# Prepare
col_count = len(columns)
data_count = len(data)
prefix_col = None
if (prefix_index >= 0) and (prefix_index < col_count):
prefix_col = columns[prefix_index]
# Loop over each records
ret = []
for idx, row in enumerate(data):
# Defaults
curr_prefix = ""
curr_suffix = ""
# Rebuild dict config
dict_payload = {}
for index in range(col_count):
key = columns[index]
value = row[index]
dict_payload[key] = value
# Extract named prefix if more than one result
if len(data) > 1:
if prefix_col:
curr_prefix = f"{dict_payload.pop(prefix_col)}__"
else:
curr_suffix = f"_{idx}"
# Build each value settings
payload = []
for column_name in columns:
if column_name in dict_payload:
key = f"{prefix}{curr_prefix}{column_name}{curr_suffix}"
value = dict_payload[column_name]
payload.append([key, value])
ret.extend(payload)
# Add count var if many records
if count_var and len(data) > 1:
ret.insert(0, [count_var, len(data)])
return ret
def restructure_to_toml(data, columns, prefix="", root="items"):
ret = {}
for line in data:
name = line[0]
value = {}
if len(line) > 1:
for idx, val in enumerate(line[1:]):
key = columns[idx + 1]
value[key] = val
ret[prefix + name] = value
if root:
ret = {root: data}
# pprint (ret)
return ret
# Formats
# ===============================
def to_vars(data, export=False, comments=True, prefix=""):
"Render to vars or environment vars, Input ormat: [[key, value,]"
out = []
for line in data:
assert isinstance(line, (list, set)), f"Got: {line}"
# Fetch key/value
key = str(line[0])
value = str(line[1]) if len(line) > 1 else ""
# # Comments
# if comments and len(line) > 2:
# comments = "# " + ', '.join(line[2:])
# else:
# comment = ""
content = f"{prefix}{key}='{value}'\t\t"
# Exports
if export:
content = f"export {prefix.upper()}{key.upper()}='{value}'"
out.append(content)
return "\n".join(out)
def to_rich_table(data, columns=None, title=None, settings=None):
settings = settings or {}
if title:
settings["title"] = title
columns = columns or []
rows = data or []
table = Table(**settings)
for column in columns:
if isinstance(column, dict):
txt, args = column["text"], column.get("args", {})
table.add_column(str(text), **args)
else:
table.add_column(str(column))
# Loop over each lines
for key, cells in iterate_any(rows):
line = []
if isinstance(key, str):
line = [key]
# Loop over each cell
for _, cell in iterate_any(cells):
line.append(str(cell))
table.add_row(*line)
return table
# Helpers
# ===============================
def is_module_present(name):
"Return true if a module is present"
spam_spec = importlib.util.find_spec("spam")
return spam_spec is not None
def view_table_sort(row, indexes):
"Helper to sort tables on columns"
ret = []
for idx in indexes:
ret.append(row[idx])
# ret = list(set(ret))
return ret
# Main Views Class
# ===============================
class _RootView:
name = "UNSET"
formats_dict = {"DEFAULT": "default"}
formats_modules = {"DEFAULT": None}
def __init__(self, output=None):
self._init_enum()
self._init_default()
self.output = output or print
## DEPRECATED !!!
def _init_enum(self):
"Create enum"
self.formats_enum = enum.Enum("OutputFormat", self.formats_dict)
def _init_default(self):
"Init default format"
self.default_format = self.formats_enum.DEFAULT.value
## Rendering
def render(self, data, columns, conf=None):
"Render data"
# assert isinstance(data, dict)
# assert isinstance(columns, dict)
# Prepare vars
conf = conf or {}
# fmt = (
# conf.get("fmt_format", None)
# or conf.get("fmt", None)
# or self.default_format
# )
# Extract Format config
# ---------------------------
fmt_config = dict(name=None, sort=None, fields=None)
fmt_config.update(
{
key.replace("fmt_", ""): val
for key, val in conf.items()
if key.startswith("fmt_") and val is not None
}
)
fmt_ = SimpleNamespace(**fmt_config)
print("FORMAT CONFIG")
pprint(conf)
pprint(fmt_)
assert fmt_.name, f"Format name can't be None: {fmt_.name}"
# check module presence
pprint(self.formats_modules)
mod_spec = self.formats_modules.get(fmt_.name, None)
print("MOD_NAME", fmt_.name, mod_spec)
if mod_spec:
# Extract specs
mod_package = None
mod_name = mod_spec
if isinstance(mod_spec, (list, set, tuple)):
mod_name = mod_spec[0]
mod_package = mod_spec[1]
# Check module presence
print("CHECKING MODE NAME", mod_name)
ret = is_module_present(mod_name)
if not ret:
_msg = f"Missing python module '{mod_name}' to support '{fmt_.name}' output."
if mod_package:
_msg = f"{_msg} Please first install package: {mod_package}"
logger.warning(_msg)
# raise Exception(_msg)
# Fetch renderer method
# ---------------------------
fmt_name = fmt_.name
fn_name = f"to__{fmt_name}"
fn = getattr(self, fn_name, None)
if not callable(fn):
raise Exception(
f"Unsupported format {fmt_.name} for {self.__class__.__name__}: No such {fn_name} method"
)
# Sort data
# ---------------------------
if fmt_.sort:
col_names = fmt_.sort.split(",")
# col_name = fmt_.sort
# Get columns indexes
indexes = []
for col_name in col_names:
try:
_idx = columns.index(col_name)
except ValueError:
choices = ",".join(columns)
raise Exception(
f"No such column '{col_name}', please choose one of: {choices}"
)
indexes.append(_idx)
# Sort data
# print ("SORT COLUMNS", indexes)
data.sort(key=lambda row: view_table_sort(row, indexes))
# Filter out fields
# ---------------------------
if fmt_.fields:
fields_keys = fmt_.fields.split(",")
# Get columns indexes
indexes = []
for col_name in fields_keys:
try:
_idx = columns.index(col_name)
except ValueError:
choices = ",".join(columns)
raise Exception(
f"No such column '{col_name}', please choose one of: {choices}"
)
indexes.append(_idx)
# print ("LIMIT FIELDS !!!", fields_keys, indexes)
rm_cols = [idx for idx, item in enumerate(columns) if not idx in indexes]
# Recreate smaller table
new_columns = []
for idx in indexes:
col_name = columns[idx]
new_columns.append(col_name)
new_data = []
for row in data:
new_row = []
for idx in indexes:
new_row.append(row[idx])
new_data.append(new_row)
data = new_data
columns = new_columns
# Call renderer
# ---------------------------
stripped_conf = {
key.replace(f"{fmt_name}_", ""): val
for key, val in conf.items()
if key.startswith(fmt_name)
}
ret = fn(data, columns, **stripped_conf)
# Display or return
# ---------------------------
if callable(self.output):
self.output(ret)
else:
return ret
# Views Implementations
# ===============================
class SpecificView(_RootView):
"Sprecific views (BETA)"
formats_dict = VIEW_FORMATS
formats_modules = VIEW_MODULES
def to__hcl(self, data, columns):
tmp = restructure_to_list_view(data, columns)
return to_hcl(tmp)
def to__toml(self, data, columns):
return to_toml(restructure_to_toml(data, columns, root="data"))
# def to__toml(self, data, columns):
# return to_toml(data)
# def to__toml(self, data, columns):
# return to_toml(restructure_to_list_view(data, columns))
class ViewList(_RootView):
name = "list"
formats_dict = VIEW_FORMATS
formats_modules = VIEW_MODULES
# Standard formats
def to__yaml(self, data, columns):
return to_yaml(restructure_to_list_view(data, columns))
def to__json(self, data, columns):
return to_json(restructure_to_list_view(data, columns), nice=True)
def to__python(self, data, columns):
return pformat(restructure_to_list_view(data, columns), indent=2)
# Rich table output
def to__table(self, data, columns, **kwargs):
"Return a rich table"
return to_rich_table(data, columns=columns, **kwargs)
# Script helpers
def to__csv(self, data, columns):
return to_csv(restructure_to_csv(data, columns))
def to__vars(self, data, columns):
return to_vars(restructure_to_env(data, columns), export=False)
def to__env(self, data, columns):
return to_vars(restructure_to_env(data, columns), export=True)
def to__words(self, data, columns, **kwargs):
"Return words"
ret = []
for line in data:
for cell in line:
ret.append(cell)
return " ".join(ret)
pprint(data)
return None
return to_rich_table(data, columns=columns, **kwargs)
def to__lines(self, data, columns, **kwargs):
"Return lines"
ret = []
for line in data:
ret.append(" ".join(line))
return "\n".join(ret)
class ViewItem(_RootView):
name = "item"
formats_dict = VIEW_FORMATS
formats_modules = VIEW_MODULES
# Standard formats
def to__yaml(self, data, columns):
return to_yaml(restructure_to_item_view(data, columns))
def to__json(self, data, columns):
return to_json(restructure_to_item_view(data, columns), nice=True)
def to__toml(self, data, columns):
return to_toml(restructure_to_toml(data, columns))
def to__python(self, data, columns):
return pformat(restructure_to_item_view(data, columns), indent=2)
# Rich table output
def to__table(self, data, columns, **kwargs):
"Return a rich table"
# pprint (columns)
# pprint (data)
# Rotate list
_list = []
_list.append(columns)
_list.append(data)
data = list(zip(*_list))
# Render table
columns = ["Field", "Value"]
return to_rich_table(data, columns=columns, **kwargs)
def to__csv(self, data, columns):
return to_csv(restructure_to_csv(data, columns))
def to__vars(self, data, columns):
return to_vars(restructure_to_env(data, columns), export=False)
def to__env(self, data, columns):
return to_vars(restructure_to_env(data, columns), export=True)

62
iam/lib/click_utils.py Normal file
View File

@ -0,0 +1,62 @@
import click
# Click Plugins
# ===============================
class NestedHelpGroup(click.Group):
# class NestedHelpGroup(RichGroup):
"""This class provides a way to show all commands of all children
instead of just the parent. Optionnaly accept to hides groups or not.
"""
def get_command(self, ctx, cmd_name: str):
"""Given a context and a command name, this returns a :class:`Command`
object if it exists or returns ``None``.
"""
# Resolve name part by part
parts = cmd_name.split(" ")
curr = self
for idx, part in enumerate(parts):
curr = curr.commands.get(part)
return curr
def list_commands(self, ctx) -> list[str]:
"List all children commands"
return self._resolve_children(self, ctx=ctx, ignore_groups=True)
@classmethod
def _resolve_children(cls, obj, ctx=None, ignore_groups=False, _parent=None):
"Resolve recursively all children"
# Source: Adapted from https://stackoverflow.com/a/56159096
if isinstance(obj, click.Group):
ret = []
for name, child in obj.commands.items():
# Build full name
full_name = name
if _parent:
full_name = " ".join([_parent, name])
# Save records
record = full_name
if ignore_groups:
if not isinstance(child, click.Group):
ret.append(record)
else:
ret.append(record)
# Recursive loop
ret.extend(
cls._resolve_children(
child, ctx=ctx, ignore_groups=ignore_groups, _parent=full_name
)
)
return ret
return []

276
iam/lib/utils.py Normal file
View File

@ -0,0 +1,276 @@
import importlib
import json
import logging
import os
import tomllib
from pprint import pprint
import yaml
# Dynamic lib loader
# ================
MISSING_PKGS = []
try:
import yaml
except ModuleNotFoundError:
MISSING_PKGS.append("yaml")
try:
# TOFIX: Python 3.11 provides tomllib
import tomlkit
except ModuleNotFoundError:
MISSING_PKGS.append("tomlkit")
try:
import hcl2
except ModuleNotFoundError:
MISSING_PKGS.append("hcl2")
try:
from jinja2 import Template
except ModuleNotFoundError:
MISSING_PKGS.append("jinja2")
# Globals
# ================
logger = logging.getLogger(__name__)
# Helper functions
# ================
def empty(item):
"Check if an item is empty"
if item is None:
return True
elif isinstance(item, str):
if item.strip() == "":
return True
elif isinstance(item, (list, dict)):
if len(item) == 0:
return True
else:
if item == None:
return True
if item == []:
return True
if item == {}:
return True
if item == tuple():
return True
return False
def prune(items):
"Prune empty lists, dicts and None values from list or dict"
if isinstance(items, dict):
ret = {key: val for key, val in items.items() if not empty(val)}
elif isinstance(items, list):
ret = [val for val in items if not empty(val)]
else:
raise Exception(f"Function prune requires a list or a dict, got: {items}")
return ret
def jinja_render(payload, vars):
"Parse a string with jinja"
vars = vars or {}
assert isinstance(payload, str)
return Template(payload).render(**vars)
def format_render(payload, vars):
"Parse a string with python format"
vars = vars or {}
assert isinstance(payload, str)
return payload.format(**vars)
def open_yaml(document):
ret = []
with open(document, "r") as file:
docs = yaml.safe_load_all(file)
for doc in docs:
ret.append(doc)
return ret
def uniq(items):
"Filter out duplicates items of list while preserving order, first stay, others are discarded"
assert isinstance(items, list), f"Expected a list, got {type(items)}: {items}"
return list(dict.fromkeys(items))
# TODO: add tests
def from_yaml(string):
"Transform YAML string to python dict"
return yaml.safe_load(string)
# TODO: add tests
def to_yaml(obj, headers=False):
"Transform obj to YAML"
options = {}
return yaml.safe_dump(obj)
# TODO: add tests
def to_json(obj, nice=True):
"Transform JSON string to python dict"
if nice:
return json.dumps(obj, indent=2)
return json.dumps(obj)
# TODO: add tests
def from_json(string):
"Transform JSON string to python dict"
return json.loads(string)
# TODO: add tests
def to_dict(obj):
"""Transform JSON obj/string to python dict
Useful to transofmr nested dicts as well"""
if not isinstance(obj, str):
obj = json.dumps(obj)
return json.loads(obj)
def to_csv(obj, sep=";"):
"""List or dict in csv"""
out = []
for key, val in iterate_any(obj):
line = []
if isinstance(key, str):
line = [key]
for _, row in iterate_any(val):
line.append(str(row))
out.append(sep.join(line))
out = "\n".join(out)
return out
def to_hcl(data):
"Render to HCL"
pprint(data)
return hcl2.loads(data)
# # Only for python >3.11
# def to_toml(data):
# "Render to toml"
# pprint(data)
# return tomllib.dumps(data)
def to_toml(data):
"Render to toml"
pprint(data)
if isinstance(data, list):
raise Exception("Bug: Toml can't handle list as top level objects")
# if isinstance(data, list):
# tab = tomlkit.table()
# for idx, line in enumerate(data):
# if isinstance(line, list) and len(line) > 1:
# first_item = line[0]
# other = line[1:]
# tab.add(first_item, other)
# else:
# tab.add(f"line.{str(idx)}", line)
# data = {
# "table": tab
# }
return tomlkit.dumps(data)
def iterate_any(payload):
"Try to iterate on anything, always return key and value"
try:
iterator = iter(payload)
except TypeError:
print("NOT ITERABLE", payload)
return {}.items()
# if hasattr(payload, "items"):
try:
return payload.items()
except AttributeError:
pass
# if hasattr(payload, "count"):
# print ("PAYLOAD", type(payload), payload)
return enumerate(payload)
assert False
raise Exception(f"Could not iterate over: {payload}")
# if payload is None:
# return {}.items()
# if hasattr(payload, "items"):
# return payload.items()
# # if hasattr(payload, "append"):
# return enumerate(payload)
# raise Exception(f"Could not iterate over: {payload}")
def get_pkg_dir(name):
"""Return the dir where the actual paasify source code lives"""
# pylint: disable=import-outside-toplevel
mod = import_module_pkg(name)
return os.path.dirname(mod.__file__)
def import_module_pkg(package):
"Simple helper to load dynamically python modules"
return importlib.import_module(package)
def import_module_from(package, *names):
"Allow to import from python packages"
mod = import_module(package)
names_len = len(names)
if names_len == 0:
return mod
if names_len == 1:
return getattr(mod, names[0])
if names_len > 1:
ret = []
for name in names:
ret.append(getattr(mod, name))
return set(ret)
def import_module(name):
"Simple helper to load python modules"
if ":" in name:
package, comp = name.rsplit(":", 1)
return import_module_from(package, comp)
return import_module_pkg(name)

2
iam/meta.py Normal file
View File

@ -0,0 +1,2 @@
NAME = "iam"
VERSION = "0.0.1"

0
iam/plugins/__init__.py Normal file
View File

66
iam/plugins/base.py Normal file
View File

@ -0,0 +1,66 @@
plugin_base = {
"resources_def": {
# Secrets
"secret": {"desc": "Secret", "input": {"secret": None}},
"secret.env": {
"desc": "Environment secret" "vars",
"input": {"secret_env": None},
},
"secret.file": {
"desc": "File secret",
"input": {"secret_file": None},
},
# Auths
"auth": {"desc": "Authentification"},
"auth.password": {"desc": "Password", "input": {"password": None}},
"auth.token": {
"desc": "Token",
"input": {"token": None},
},
"auth.totp": {
"desc": "One time password",
"input": {"token": None},
},
# Accounts
"account": {
"desc": "Account",
"input": {"password": None, "user": None},
},
"account.email": {"desc": "Email account", "input": {"email": None}},
# Services
"service": {"desc": "Session service"},
# ID
"service.id": {"desc": "Default ident service"},
},
"resources": {
"service.id": {
"enabled": True,
},
},
"services": {
"id": {
"desc": "Local id",
"commands": {
"shell_enable": {
"desc": "Enable shell ident",
"cmd": "export SHELL_IDENT={{ident}}",
},
"shell_disable": {
"desc": "Disable shell ident",
"cmd": "unset SHELL_IDENT",
},
"new": {
"desc": "Create shell identy",
"cmd": "add_ident {{ param }}",
},
"delete": {
"desc": "Delete shell identy",
"cmd": "rm_ident {{ param }}",
},
},
},
},
}
all = {"base": plugin_base}

10
iam/plugins/devops.py Normal file
View File

@ -0,0 +1,10 @@
import os
from iam.lib.utils import get_pkg_dir, open_yaml, to_yaml
all = None
yml_dir = get_pkg_dir(__name__)
conf_files = open_yaml(os.path.join(yml_dir, "devops.yml"))
for conf in conf_files:
all = conf.get("providers", {})
break

177
iam/plugins/devops.yml Normal file
View File

@ -0,0 +1,177 @@
providers:
# Provider: Github Config
# ==================
github:
services:
local.github:
desc: Github client
input:
gh_token: "MISSING_TOKEN"
commands:
shell_enable:
desc: Enable git identity
cmd: |
export GH_TOKEN='{{gh_token}}'
export GH_REPO='{{ident}}'
shell_disable:
desc: Disable git identity
cmd: |
unset GH_TOKEN GH_REPO
resources_def:
account.github:
desc: A github account
service.local.github:
desc: Configure gh client
uses:
- account.github:{user}
resources:
# account.github:
# desc: Github account
# input:
# gh_token: TOKEN789456123
service.local.github:
enabled: true
uses:
- account.github:{user}
# Provider: Gitea Config
# ==================
gitea:
services:
local.gitea:
desc: Gitea client
input:
gitea_server_url: "MyGiteaServer.com"
commands:
shell_enable:
desc: Enable git identity
cmd: |
export GITEA_SERVER_URL='{{gitea_server_url}}'
export GITEA_LOGIN='{{email}}'
shell_disable:
desc: Disable git identity
cmd: |
unset GITEA_SERVER_URL GITEA_LOGIN
resources_def:
account.gitea:
desc: A gitea account
service.local.gitea:
desc: Configure gitea client
needs:
- account.gitea:{user}
resources:
# account.gitea:
# desc: Gitea account
# input:
# gh_token: TOKEN789456123
service.local.gitea:
enabled: true
uses:
- account.gitea:{user}
# Provider: Minio Config
# ==================
minio:
services:
local.minio:
desc: Minio client
input:
minio_url: "minio.org"
minio_access: ident
minio_secret: secret
commands:
shell_enable:
desc: Enable minio alias
cmd: |
export MINIO_ACCESS_KEY={{minio_access}}
export MINIO_SECRET_KEY={{minio_secret}}
# cmd_FUTURE: |
# $ export MINIO_ACCESS_KEY=$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
# # this one is actually a secret, so careful
# $ export MINIO_SECRET_KEY=$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
shell_disable:
desc: Disable minio alias
cmd: |
unset MINIO_ACCESS_KEY MINIO_SECRET_KEY
new_alias:
desc: Create alias
cmd: |
mc alias set {{ident}} {{api_url}} {{minio_user}} {{minio_password}}
rm_alias:
desc: Remove alias
cmd: |
mc alias rm {{ident}}
resources_def:
account.minio:
desc: A minio account
service.local.minio:
desc: Configure minio client
uses:
- account.minio:{user}
resources:
# account.minio:
# desc: Minio account
# input:
# gh_token: TOKEN789456123
service.local.minio:
enabled: true
uses:
- account.minio:{user}
# Other tools
# libvirt-url
# terraform_endpoint
# openstack-endpoint
# minio aliases

178
iam/plugins/local.py Normal file
View File

@ -0,0 +1,178 @@
import os
from pprint import pprint
from iam.lib.utils import get_pkg_dir, open_yaml, to_yaml
yml_dir = get_pkg_dir(__name__)
plugin_conf = open_yaml(os.path.join(yml_dir, "local.yml"))[0]
all = plugin_conf.get("providers", {})
# plugin_ssh = {
# "services": {
# "local.ssh_key": {
# "desc": "Local ssh key",
# # "input": {
# # "ssh_agent_socket_dir": "/run/user/ssh-agent",
# # "ssh_agent_tmout": "7d",
# # },
# "commands": {
# "shell_enable": {
# "cmd": """
# export SSH_AUTH_SOCK={{ssh_agent_socket_dir}}/{{user}} && \
# ssh-agent -a $SSH_AUTH_SOCK -t {{ssh_agent_tmout}}
# """,
# },
# "shell_disable": {
# "cmd": "ssh-agent -k && unset SSH_AUTH_SOCK",
# },
# },
# },
# },
# "resources_def": {
# "service.local.ssh_key": {
# "desc": "A local ssh key",
# },
# "auth.ssh_certificate": {
# "desc": "SSH Certificates",
# "input": {"ssh_cert_file": None},
# "needs": ["auth.ssh_key"],
# },
# "auth.ssh_key": {
# "desc": "ssh_key",
# "input": {
# "ssh_key_file": None,
# "ssh_key_secret": None
# },
# "needs": [
# {"kind": "auth.password", "remap": {"ssh_key_secret": "passord"}}
# ],
# },
# "account.ssh": {"desc": "An unix account", "input": {"host": None}},
# },
# "resources": {
# "service.local.ssh_agent": {
# "enabled": True,
# # "contains": [
# # "auth.ssh_key:{ident}/ed25519",
# # "auth.ssh_key:{ident}/rsa4096",
# # "auth.ssh_key:{ident}/rsa2048",
# # "auth.ssh_key:{ident}/rsa1024",
# # "auth.ssh_key:{ident}",
# # ],
# },
# "service.local.ssh_agent_keys": {
# "enabled": True,
# "loop_limit": 3,
# "loop": [
# "auth.ssh_key:{ident}/ed25519",
# "auth.ssh_key:{ident}/rsa4096",
# "auth.ssh_key:{ident}/rsa2048",
# "auth.ssh_key:{ident}/rsa1024",
# "auth.ssh_key:{ident}",
# ],
# },
# },
# }
# plugin_ssh_agent = {
# "services": {
# "local.ssh_agent": {
# "desc": "Local ssh-agent",
# "input": {
# "ssh_agent_socket_dir": "/run/user/ssh-agent",
# "ssh_agent_tmout": "7d",
# },
# "commands": {
# "shell_enable": {
# "cmd": """
# export SSH_AUTH_SOCK={{ssh_agent_socket_dir}}/{{user}} && \
# ssh-agent -a $SSH_AUTH_SOCK -t {{ssh_agent_tmout}}
# """,
# },
# "shell_disable": {
# "cmd": "ssh-agent -k && unset SSH_AUTH_SOCK",
# },
# },
# },
# "local.ssh_agent_keys": {
# "desc": "Local ssh-agent keys",
# "required_services": [
# "local.ssh_agent"
# ],
# "commands": {
# "shell_enable": {
# "cmd": """
# ssh-add {% for item in loop %} {{item.ssh_key_file}} {% endfor %}
# """,
# },
# "shell_disable": {
# "cmd": "ssh-agent -d {ssh_key_file}",
# },
# },
# },
# },
# "resources_def": {
# "service.local.ssh_agent": {
# "desc": "A local ssh_agent service",
# },
# "service.local.ssh_agent_keys": {
# "desc": "A local ssh_agent_keys service",
# },
# },
# "resources": {
# "service.local.ssh_agent": {
# "enabled": True,
# # "contains": [
# # "auth.ssh_key:{ident}/ed25519",
# # "auth.ssh_key:{ident}/rsa4096",
# # "auth.ssh_key:{ident}/rsa2048",
# # "auth.ssh_key:{ident}/rsa1024",
# # "auth.ssh_key:{ident}",
# # ],
# },
# "service.local.ssh_agent_keys": {
# "enabled": True,
# "loop_limit": 3,
# "loop": [
# "auth.ssh_key:{ident}/ed25519",
# "auth.ssh_key:{ident}/rsa4096",
# "auth.ssh_key:{ident}/rsa2048",
# "auth.ssh_key:{ident}/rsa1024",
# "auth.ssh_key:{ident}",
# ],
# },
# },
# }
# all = {
# "ssh": plugin_ssh,
# "ssh_agent": plugin_ssh_agent,
# }
# print (to_yaml(all))

322
iam/plugins/local.yml Normal file
View File

@ -0,0 +1,322 @@
providers:
# Provider: SSH
# ==================
ssh:
services:
local.ssh_key:
desc: Local ssh key
inputs:
ssh_key_secret: ""
ssh_key_alg: "ed25519"
commands:
new:
desc: Create new SSH key
cmd: |
SSH_KEY_ALG={{ssh_key_alg}}
SSH_KEY_VERSION="$(date +'%Y%m%d')"
SSH_KEY_HOST="$(hostname -f)"
SSH_KEY_FILE=$HOME/.ssh/{ident}/{user}_${SSH_KEY_ALG}_${SSH_KEY_VERSION}
SSH_KEY_COMMENT={user}@${SSH_KEY_HOST}:${SSH_KEY_ALG}_${SSH_KEY_VERSION}
ssh-keygen -f "{SSH_KEY_FILE}" \
-t ed25519 -a 100 \
-N "{{ssh_key_secret}}" \
-C "$SSH_KEY_COMMENT"
delete:
desc: Delete existing SSH key
cmd: |
find $HOME/.ssh/{ident}/ -name "{user}_*"
resources_def:
auth.ssh_certificate:
desc: SSH Certificates
input:
ssh_cert_file: null
needs:
- auth.ssh_key
auth.ssh_key:
desc: SSH Keypair
input:
ssh_key_file: null
ssh_key_secret: null
needs:
- kind: auth.password
remap:
ssh_key_secret: passord
account.ssh:
desc: Unix account
input:
host: null
# service.local.ssh_key:
# desc: A local ssh key
# resources:
# service.local.ssh_agent:
# enabled: true
# service.local.ssh_agent_keys:
# enabled: true
# loop:
# - auth.ssh_key:{ident}/ed25519
# - auth.ssh_key:{ident}/rsa4096
# - auth.ssh_key:{ident}/rsa2048
# - auth.ssh_key:{ident}/rsa1024
# - auth.ssh_key:{ident}
# loop_limit: 3
# Provider: GPG Agent
# ==================
gpg_agent:
resources_def:
auth.gpg_key:
desc: GPG keypair
input:
gpg_key_file: null
gpg_key_secret: null
needs:
- kind: auth.password
remap:
gpg_key_secret: passord
# Provider: SSH Agent
# ==================
ssh_agent:
services:
local.ssh_agent:
desc: Local ssh-agent
input:
ssh_agent_socket_dir: /run/user/ssh-agent
ssh_agent_tmout: 7d
commands:
shell_enable:
desc: Enable ssh-agent
cmd: |
export SSH_AUTH_SOCK={{ssh_agent_socket_dir}}/{{user}}
ssh-agent -a $SSH_AUTH_SOCK -t {{ssh_agent_tmout}}
# SSH_AGENT_PID= ???
shell_disable:
desc: Disable ssh-agent
cmd: ssh-agent -k && unset SSH_AUTH_SOCK
local.ssh_agent_keys:
desc: Local ssh-agent keys
commands:
enable:
desc: Unload keys into ssh-agent
cmd: ssh-agent -d {ssh_key_file}
disable:
desc: Load keys into ssh-agent
cmd: |
ssh-add {% for item in loop %} {{item.ssh_key_file}} {% endfor %}
required_services:
- local.ssh_agent
resources_def:
service.local.ssh_agent:
desc: Configure ssh-agent daemon
service.local.ssh_agent_keys:
desc: Configure ssh-agent keys autoloader
resources:
service.local.ssh_agent:
enabled: true
service.local.ssh_agent_keys:
enabled: true
loop:
- auth.ssh_key:{ident}/ed25519
- auth.ssh_key:{ident}/rsa4096
- auth.ssh_key:{ident}/rsa2048
- auth.ssh_key:{ident}/rsa1024
- auth.ssh_key:{ident}
loop_limit: 3
# Provider: Git Config
# ==================
git:
services:
local.git:
desc: Git identity
# input:
# ssh_agent_socket_dir: /run/user/ssh-agent
# ssh_agent_tmout: 7d
commands:
shell_enable:
desc: Enable git identity
cmd: |
export GIT_AUTHOR_NAME='{{ident}}'
export GIT_AUTHOR_EMAIL='{{email}}'
export GIT_COMMITTER_NAME='{{ident}}'
export GIT_COMMITTER_EMAIL='{{email}}'
shell_disable:
desc: Disable git identity
cmd: |
unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL
local.git_home:
desc: Home as git repo
input:
git_dir: "$HOME"
git_work_tree: $HOME/.local/share/home_git
commands:
shell_enable:
desc: Enable git home management
cmd: |
export GIT_DIR="{{git_dir}}"
export GIT_WORK_TREE="{{git_work_tree}}/{{ ident }}"
shell_disable:
desc: Disable git home management
cmd: |
unset GIT_DIR GIT_WORK_TREE
required_services:
- local.git
resources_def:
service.local.git:
desc: Configure git
service.local.git_home:
desc: Configure home as git repo
resources:
service.local.git:
enabled: true
uses:
- account:{user}
# Disabled by default
service.local.git_home:
# Provider: PS1 Config
# ==================
ps1:
services:
local.ps1:
desc: PS1 prompt
input:
enabled: True
commands:
shell_enable:
desc: Enable git identity
cmd: |
OLD_PS1=$PS1
export PS1="\[\033[0;34m\]({{ident}})\[\033[00m\] ${PS1}"
shell_disable:
desc: Disable git identity
cmd: |
export PS1=$OLD_PS1
resources_def:
service.local.ps1:
desc: PS1 prompt
resources:
service.local.ps1:
desc: Custom Ident PS1
# EXISTING
# WARN__: Your workspace is already activated
# NOTICE: Enabling id ...
# export SHELL_ID='mrjk'
# export GIT_AUTHOR_NAME='mrjk'
# export GIT_AUTHOR_EMAIL='mrjk.78@gmail.com'
# export GIT_COMMITTER_NAME='mrjk'
# export GIT_COMMITTER_EMAIL='mrjk.78@gmail.com'
# NOTICE: Enabling gpg ...
# export GNUPGHOME=/home/jez/.config/gpg/mrjk
# export GPG_AGENT_INFO=/run/user/1000/pgp-agent/mrjk/socket
# export GPG_DEFAULT_ID=mrjk
# export GPG_TTY=/dev/pts/48
# export GNUPGHOME=/home/jez/.config/gpg/mrjk
# NOTICE: Enabling ssh ...
# export SSH_AUTH_SOCK=/run/user/1000/ssh-agent/mrjk/socket
# NOTICE: Enabling gh ...
# export GH_TOKEN="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"
# export GH_REPO="mrjk"
# NOTICE: Enabling gitea ...
# export GITEA_SERVER_URL="ad808bc88fa37bce5e3bb963f1420aa575194d30"
# export GITEA_LOGIN="mrjk@git.jeznet.org"
# NOTICE: Enabling ps1 ...
# export PS1="\[\](mrjk)\[\] ${IDM_SHELL_PS1}"
# NOTICE: Identity 'mrjk' is loaded

118
iam/providers.py Normal file
View File

@ -0,0 +1,118 @@
import logging
from pprint import pprint
# from .config import ResourceKind
from . import exceptions as error
from .framework import DictCtrl, DictItem, KeyValue, KeyValueExtra
logger = logging.getLogger(__name__)
# pprint (logger.__dict__)
class Provider(DictItem):
default_attrs = {
"resources_def": {},
"resources": {},
"services": {},
}
def init(self):
payload = self._payload
# Create resources Defs
# res_def = payload.get("resources_def", {}) # {}
# for key, val in payload.get("resources_def", {}).items():
# res_def[key] = ResourceKind(key, val)
# self.resources_def = res_def
self.resources_kind = payload.get("resources_def", {})
# Create resources instances
res_inst_configs = {}
for key, val in payload.get("resources", {}).items():
res_inst_configs[key] = val
self.resources_inst = res_inst_configs
self.resources_conf = res_inst_configs
self.services_confs = payload.get("services", {})
class Providers(DictCtrl):
items_class = Provider
def get_resource_kinds(self):
"Returns resources kinds for all providers"
ret = {}
for provider in self.values():
res = provider.resources_kind
for name, config in res.items():
ret[name] = config or {}
return ret
def get_resource_configs(self):
"Returns resources configurations for all providers"
ret = {}
for provider in self.values():
res = provider.resources_conf
for name, config in res.items():
ret[name] = config or {}
return ret
def get_services_configs(self):
ret = {}
for prov_name, provider in self.items():
ret.update(provider.services_confs)
return ret
# def get_catalog_resources_inst(self):
# ret = {}
# for provider in self.providers.values():
# # res = provider.resources_def
# for name, config in provider.resources_inst.items():
# ret[name] = config
# return ret
######## BETA
def expand_inputs(payload):
"Expand dict for each of its list value"
ret = []
expanded_keys = [key for key, val in payload.items() if isinstance(val, list)]
return
for exp_key in expanded_keys:
items = len(payload[exp_key])
for idx in range(0, items):
tmp = copy(payload)
for subkey in expanded_keys:
tmp[subkey] = payload[subkey][idx]
ret.append(tmp)
# V1
# for idx, exp_key in enumerate(expanded_keys):
# tmp = copy(payload)
# tmp[exp_key] = payload[exp_key][idx]
# ret.append(tmp)
# for key, val in payload.items():
# if isinstance(val, list):
# # Expand
# pass
print("expanded_keys", expanded_keys)
# pprint (ret)
return ret

629
poetry.lock generated Normal file
View File

@ -0,0 +1,629 @@
# This file is automatically @generated by Poetry 1.6.0 and should not be changed by hand.
[[package]]
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "autopage"
version = "0.5.1"
description = "A library to provide automatic paging for console output"
optional = false
python-versions = ">=3.6"
files = [
{file = "autopage-0.5.1-py3-none-any.whl", hash = "sha256:0fbf5efbe78d466a26753e1dee3272423a3adc989d6a778c700e89a3f8ff0d88"},
{file = "autopage-0.5.1.tar.gz", hash = "sha256:01be3ee61bb714e9090fcc5c10f4cf546c396331c620c6ae50a2321b28ed3199"},
]
[[package]]
name = "black"
version = "23.9.1"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"},
{file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"},
{file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"},
{file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"},
{file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"},
{file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"},
{file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"},
{file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"},
{file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"},
{file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"},
{file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"},
{file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"},
{file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"},
{file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"},
{file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"},
{file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"},
{file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"},
{file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"},
{file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"},
{file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"},
{file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"},
{file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "cliff"
version = "4.3.0"
description = "Command Line Interface Formulation Framework"
optional = false
python-versions = ">=3.8"
files = [
{file = "cliff-4.3.0-py3-none-any.whl", hash = "sha256:db3dc8774f47db9aa86796921ff158d0f023630261c2746c4fff12584b75f5b2"},
{file = "cliff-4.3.0.tar.gz", hash = "sha256:fc5b6ebc8fb815332770b2485ee36c09753937c37cce4f3227cdd4e10b33eacc"},
]
[package.dependencies]
autopage = ">=0.4.0"
cmd2 = ">=1.0.0"
importlib-metadata = ">=4.4"
PrettyTable = ">=0.7.2"
PyYAML = ">=3.12"
stevedore = ">=2.0.1"
[[package]]
name = "cmd2"
version = "2.4.3"
description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "cmd2-2.4.3-py3-none-any.whl", hash = "sha256:f1988ff2fff0ed812a2d25218a081b0fa0108d45b48ba2a9272bb98091b654e6"},
{file = "cmd2-2.4.3.tar.gz", hash = "sha256:71873c11f72bd19e2b1db578214716f0d4f7c8fa250093c601265a9a717dee52"},
]
[package.dependencies]
attrs = ">=16.3.0"
pyperclip = ">=1.6"
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\""}
wcwidth = ">=0.1.7"
[package.extras]
dev = ["codecov", "doc8", "flake8", "invoke", "mypy", "nox", "pytest (>=4.6)", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "twine (>=1.11)"]
test = ["codecov", "coverage", "gnureadline", "pytest (>=4.6)", "pytest-cov", "pytest-mock"]
validate = ["flake8", "mypy", "types-pkg-resources"]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coloredlogs"
version = "15.0.1"
description = "Colored terminal output for Python's logging module"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"},
{file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},
]
[package.dependencies]
humanfriendly = ">=9.1"
[package.extras]
cron = ["capturer (>=2.4)"]
[[package]]
name = "humanfriendly"
version = "10.0"
description = "Human friendly output for text interfaces using Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
]
[package.dependencies]
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
[[package]]
name = "importlib-metadata"
version = "6.8.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
{file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]]
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
]
[[package]]
name = "pbr"
version = "5.11.1"
description = "Python Build Reasonableness"
optional = false
python-versions = ">=2.6"
files = [
{file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"},
{file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"},
]
[[package]]
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]]
name = "prettytable"
version = "3.9.0"
description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
optional = false
python-versions = ">=3.8"
files = [
{file = "prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8"},
{file = "prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34"},
]
[package.dependencies]
wcwidth = "*"
[package.extras]
tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"]
[[package]]
name = "pyaml"
version = "23.9.6"
description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyaml-23.9.6-py3-none-any.whl", hash = "sha256:9dcc67922b7278f3680e573324b2e8a8d2f86c5d09bf640cba83735fb1663e97"},
{file = "pyaml-23.9.6.tar.gz", hash = "sha256:2b2c39017b718a127bef9f96bc55f89414d960876668d69880aae66f4ba98957"},
]
[package.dependencies]
PyYAML = "*"
[package.extras]
anchors = ["unidecode"]
[[package]]
name = "pygments"
version = "2.16.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
files = [
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
]
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "pyperclip"
version = "1.8.2"
description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)"
optional = false
python-versions = "*"
files = [
{file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"},
]
[[package]]
name = "pyreadline3"
version = "3.4.1"
description = "A python implementation of GNU readline."
optional = false
python-versions = "*"
files = [
{file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
]
[[package]]
name = "pyxdg"
version = "0.28"
description = "PyXDG contains implementations of freedesktop.org standards in python."
optional = false
python-versions = "*"
files = [
{file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"},
{file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"},
]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "rich"
version = "13.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"},
{file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "rich-click"
version = "1.6.1"
description = "Format click help output nicely with rich"
optional = false
python-versions = ">=3.7"
files = [
{file = "rich-click-1.6.1.tar.gz", hash = "sha256:f8ff96693ec6e261d1544e9f7d9a5811c5ef5d74c8adb4978430fc0dac16777e"},
{file = "rich_click-1.6.1-py3-none-any.whl", hash = "sha256:0fcf4d1a09029d79322dd814ab0b2e66ac183633037561881d45abae8a161d95"},
]
[package.dependencies]
click = ">=7"
rich = ">=10.7.0"
[package.extras]
dev = ["pre-commit"]
[[package]]
name = "stevedore"
version = "5.1.0"
description = "Manage dynamic plugins for Python applications"
optional = false
python-versions = ">=3.8"
files = [
{file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"},
{file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"},
]
[package.dependencies]
pbr = ">=2.0.0,<2.1.0 || >2.1.0"
[[package]]
name = "typer"
version = "0.9.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.6"
files = [
{file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
{file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
]
[package.dependencies]
click = ">=7.1.1,<9.0.0"
typing-extensions = ">=3.7.4.3"
[package.extras]
all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
[[package]]
name = "typing-extensions"
version = "4.8.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]]
name = "wcwidth"
version = "0.2.8"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"},
{file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"},
]
[[package]]
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "6c7761768920ef58522d19560d56ce366cca74dc275b4e019acf10838923d63d"

57
pyproject.toml Normal file
View File

@ -0,0 +1,57 @@
[tool.poetry]
name = "iam"
version = "0.1.0"
description = ""
authors = ["mrjk"]
readme = "README.md"
#packages = [
# { include = "iam" },
## { include = "name_it", from = "src" },
## { from = "src" },
#]
[tool.poetry.dependencies]
python = "^3.11"
typer = "^0.9.0"
pyaml = "^23.9.6"
jinja2 = "^3.1.2"
pyxdg = "^0.28"
coloredlogs = "^15.0.1"
rich = "^13.6.0"
rich-click = "^1.6.1"
[tool.poetry.scripts]
iam2 = "iam.cli:run"
iam = "iam.cli_click:run"
[tool.poetry.group.dev.dependencies]
black = "^23.9.1"
isort = "^5.12.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
### TESTS FOR CLIFF
[tool.poetry.plugins] # Optional super table
# [tool.poetry.plugins."iam.cliff"]
# "my_plugin" = "iam.cli_cliff:SCUrls"
# "simple" = "iam.cli_cliff:SCUrls"
# 'cliff.demo': [
# 'simple = cliffdemo.simple:Simple',
# 'two_part = cliffdemo.simple:Simple',
# 'error = cliffdemo.simple:Error',
# 'list files = cliffdemo.list:Files',
# 'files = cliffdemo.list:Files',
# 'file = cliffdemo.show:File',
# 'show file = cliffdemo.show:File',
# 'unicode = cliffdemo.encoding:Encoding',
# 'hooked = cliffdemo.hook:Hooked',
# ],