add: initial POC
This commit is contained in:
commit
dcb27eecc1
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/*
|
||||
__pycache__
|
||||
.direnv/*
|
||||
119
README.md
Normal file
119
README.md
Normal 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
190
iam/app.py
Normal 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
870
iam/catalog.py
Normal 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
585
iam/cli.py
Normal 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
827
iam/cli_click.py
Executable 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
192
iam/cli_views.py
Normal 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
18
iam/exceptions.py
Normal 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
355
iam/framework.py
Normal 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
45
iam/idents.py
Normal 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
548
iam/lib/cli_views.py
Normal 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
62
iam/lib/click_utils.py
Normal 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
276
iam/lib/utils.py
Normal 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
2
iam/meta.py
Normal file
@ -0,0 +1,2 @@
|
||||
NAME = "iam"
|
||||
VERSION = "0.0.1"
|
||||
0
iam/plugins/__init__.py
Normal file
0
iam/plugins/__init__.py
Normal file
66
iam/plugins/base.py
Normal file
66
iam/plugins/base.py
Normal 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
10
iam/plugins/devops.py
Normal 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
177
iam/plugins/devops.yml
Normal 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
178
iam/plugins/local.py
Normal 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
322
iam/plugins/local.yml
Normal 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
118
iam/providers.py
Normal 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
629
poetry.lock
generated
Normal 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
57
pyproject.toml
Normal 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',
|
||||
# ],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user