lnp-devopscli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dc_cli/__init__.py +3 -0
- dc_cli/groups/__init__.py +0 -0
- dc_cli/groups/bw.py +430 -0
- dc_cli/groups/gl.py +300 -0
- dc_cli/groups/ws.py +1249 -0
- dc_cli/main.py +54 -0
- dc_cli/utils/__init__.py +0 -0
- dc_cli/utils/config.py +83 -0
- dc_cli/utils/gitlab.py +165 -0
- lnp_devopscli-1.0.0.dist-info/METADATA +136 -0
- lnp_devopscli-1.0.0.dist-info/RECORD +14 -0
- lnp_devopscli-1.0.0.dist-info/WHEEL +5 -0
- lnp_devopscli-1.0.0.dist-info/entry_points.txt +3 -0
- lnp_devopscli-1.0.0.dist-info/top_level.txt +1 -0
dc_cli/main.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Entry point do CLI dc - DevOps CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
@click.version_option(version=__version__, prog_name="devopscli")
|
|
15
|
+
def cli():
|
|
16
|
+
"""DevOps CLI — ferramentas DevOps para LNP Consulting TI."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _auto_register_groups():
|
|
20
|
+
"""Auto-discover e registra command groups de groups/.
|
|
21
|
+
|
|
22
|
+
Convencao: cada arquivo groups/<nome>.py deve exportar <nome>_group.
|
|
23
|
+
Exemplo: groups/gl.py exporta gl_group.
|
|
24
|
+
"""
|
|
25
|
+
groups_dir = Path(__file__).parent / "groups"
|
|
26
|
+
|
|
27
|
+
for py_file in sorted(groups_dir.glob("*.py")):
|
|
28
|
+
if py_file.name.startswith("_"):
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
module_name = py_file.stem
|
|
32
|
+
expected_name = f"{module_name}_group"
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
module = importlib.import_module(
|
|
36
|
+
f".groups.{module_name}", package=__package__
|
|
37
|
+
)
|
|
38
|
+
group = getattr(module, expected_name, None)
|
|
39
|
+
|
|
40
|
+
if group and isinstance(group, (click.Group, click.Command)):
|
|
41
|
+
cli.add_command(group)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
click.echo(f"[ERRO] Falha ao carregar grupo '{module_name}': {e}", err=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_auto_register_groups()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
cli(prog_name="devopscli")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
dc_cli/utils/__init__.py
ADDED
|
File without changes
|
dc_cli/utils/config.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Utilitarios de configuracao: .env, config.yaml, workspace root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_workspace_root() -> Path:
|
|
13
|
+
"""Retorna raiz do projeto (3 niveis acima de utils/)."""
|
|
14
|
+
return Path(__file__).resolve().parent.parent.parent.parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_global_config_path() -> Path:
|
|
18
|
+
"""Retorna path do config global (~/bin/config.yaml)."""
|
|
19
|
+
return Path.home() / "bin" / "config.yaml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _strip_quotes(value: str) -> str:
|
|
23
|
+
"""Remove aspas simples ou duplas que envolvem o valor."""
|
|
24
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
25
|
+
return value[1:-1]
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_env_line(line: str) -> tuple[str, str] | None:
|
|
30
|
+
"""Parseia uma linha de .env e retorna (key, value) ou None."""
|
|
31
|
+
line = line.strip()
|
|
32
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
33
|
+
return None
|
|
34
|
+
key, _, value = line.partition("=")
|
|
35
|
+
key = key.strip()
|
|
36
|
+
value = _strip_quotes(value.strip())
|
|
37
|
+
if not key:
|
|
38
|
+
return None
|
|
39
|
+
return key, value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_dotenv() -> None:
|
|
43
|
+
"""Carrega variaveis do .env do workspace-personal (se existir)."""
|
|
44
|
+
env_file = Path.home() / "workspace-personal" / ".env"
|
|
45
|
+
if not env_file.exists():
|
|
46
|
+
env_file = get_workspace_root() / ".env"
|
|
47
|
+
if not env_file.exists():
|
|
48
|
+
return
|
|
49
|
+
with open(env_file) as f:
|
|
50
|
+
for line in f:
|
|
51
|
+
parsed = parse_env_line(line)
|
|
52
|
+
if not parsed:
|
|
53
|
+
continue
|
|
54
|
+
key, value = parsed
|
|
55
|
+
if key not in os.environ:
|
|
56
|
+
os.environ[key] = value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config() -> dict:
|
|
60
|
+
"""Carrega config.yaml completo, retorna dict."""
|
|
61
|
+
import click
|
|
62
|
+
|
|
63
|
+
config_path = get_global_config_path()
|
|
64
|
+
if not config_path.exists():
|
|
65
|
+
config_path = get_workspace_root() / "config.yaml"
|
|
66
|
+
if not config_path.exists():
|
|
67
|
+
click.echo("[ERRO] config.yaml nao encontrado.", err=True)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
with open(config_path) as f:
|
|
71
|
+
return yaml.safe_load(f) or {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_section(name: str) -> dict:
|
|
75
|
+
"""Retorna secao do config (ex: 'gl' -> config['gl'])."""
|
|
76
|
+
import click
|
|
77
|
+
|
|
78
|
+
config = load_config()
|
|
79
|
+
section = config.get(name)
|
|
80
|
+
if not section:
|
|
81
|
+
click.echo(f"[ERRO] Secao '{name}' nao encontrada no config.yaml.", err=True)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
return section
|
dc_cli/utils/gitlab.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""GitLab API helper — wrapper fino para requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitLabAPI:
|
|
12
|
+
"""Cliente para a API do GitLab."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, host: str, token: str):
|
|
15
|
+
self.host = host.rstrip("/")
|
|
16
|
+
self.token = token
|
|
17
|
+
self.session = requests.Session()
|
|
18
|
+
self.session.headers.update({"PRIVATE-TOKEN": token})
|
|
19
|
+
|
|
20
|
+
def _url(self, path: str) -> str:
|
|
21
|
+
return f"{self.host}/api/v4{path}"
|
|
22
|
+
|
|
23
|
+
def get(self, path: str, **params) -> Any:
|
|
24
|
+
"""GET request para a API do GitLab."""
|
|
25
|
+
resp = self.session.get(self._url(path), params=params)
|
|
26
|
+
resp.raise_for_status()
|
|
27
|
+
return resp.json()
|
|
28
|
+
|
|
29
|
+
def post(self, path: str, **data) -> dict[str, Any]:
|
|
30
|
+
"""POST request para a API do GitLab."""
|
|
31
|
+
resp = self.session.post(self._url(path), json=data)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
return resp.json()
|
|
34
|
+
|
|
35
|
+
def delete(self, path: str) -> None:
|
|
36
|
+
"""DELETE request para a API do GitLab."""
|
|
37
|
+
resp = self.session.delete(self._url(path))
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
|
|
40
|
+
def search_groups(self, search: str) -> list[dict[str, Any]]:
|
|
41
|
+
"""Busca grupos pelo nome/path."""
|
|
42
|
+
return cast(
|
|
43
|
+
list[dict[str, Any]], self.get("/groups", search=search, per_page=20)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def search_projects(self, search: str) -> list[dict[str, Any]]:
|
|
47
|
+
"""Busca projetos pelo nome/path."""
|
|
48
|
+
return cast(
|
|
49
|
+
list[dict[str, Any]],
|
|
50
|
+
self.get("/projects", search=search, simple=True, per_page=20),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def resolve_group_id(self, group: str) -> int:
|
|
54
|
+
"""Resolve group_id a partir de ID numerico ou path.
|
|
55
|
+
|
|
56
|
+
Aceita:
|
|
57
|
+
- "12345" -> retorna 12345
|
|
58
|
+
- "lnp-consulting-ti" -> busca e retorna o ID
|
|
59
|
+
- "lnp-consulting-ti/saas" -> busca e retorna o ID
|
|
60
|
+
"""
|
|
61
|
+
if group.isdigit():
|
|
62
|
+
return int(group)
|
|
63
|
+
|
|
64
|
+
# Tentar buscar pelo path exato via URL-encoded path
|
|
65
|
+
try:
|
|
66
|
+
encoded = group.replace("/", "%2F")
|
|
67
|
+
result = cast(dict[str, Any], self.get(f"/groups/{encoded}"))
|
|
68
|
+
return result["id"]
|
|
69
|
+
except requests.HTTPError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Fallback: buscar pelo nome
|
|
73
|
+
groups = self.search_groups(group)
|
|
74
|
+
for g in groups:
|
|
75
|
+
if g["full_path"] == group or g["path"] == group:
|
|
76
|
+
return g["id"]
|
|
77
|
+
|
|
78
|
+
if groups:
|
|
79
|
+
click.echo(
|
|
80
|
+
f"[!] Grupo exato '{group}' nao encontrado. Resultados parciais:"
|
|
81
|
+
)
|
|
82
|
+
for g in groups[:5]:
|
|
83
|
+
click.echo(f" {g['full_path']} (id: {g['id']})")
|
|
84
|
+
|
|
85
|
+
raise click.ClickException(f"Grupo '{group}' nao encontrado.")
|
|
86
|
+
|
|
87
|
+
def create_deploy_token(
|
|
88
|
+
self, group_id: int, name: str, scopes: list[str]
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Cria deploy token no grupo."""
|
|
91
|
+
return self.post(
|
|
92
|
+
f"/groups/{group_id}/deploy_tokens",
|
|
93
|
+
name=name,
|
|
94
|
+
scopes=scopes,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def list_deploy_tokens(self, group_id: int) -> list[dict[str, Any]]:
|
|
98
|
+
"""Lista deploy tokens do grupo."""
|
|
99
|
+
return cast(
|
|
100
|
+
list[dict[str, Any]],
|
|
101
|
+
self.get(f"/groups/{group_id}/deploy_tokens", per_page=100),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def revoke_deploy_token(self, group_id: int, token_id: int) -> None:
|
|
105
|
+
"""Revoga deploy token do grupo."""
|
|
106
|
+
self.delete(f"/groups/{group_id}/deploy_tokens/{token_id}")
|
|
107
|
+
|
|
108
|
+
def resolve_project_id(self, project: str) -> int:
|
|
109
|
+
"""Resolve project_id a partir de ID numerico ou path."""
|
|
110
|
+
if project.isdigit():
|
|
111
|
+
return int(project)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
encoded = project.replace("/", "%2F")
|
|
115
|
+
result = cast(dict[str, Any], self.get(f"/projects/{encoded}"))
|
|
116
|
+
return result["id"]
|
|
117
|
+
except requests.HTTPError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
projects = self.search_projects(project)
|
|
121
|
+
for p in projects:
|
|
122
|
+
if p.get("path_with_namespace") == project or p.get("path") == project:
|
|
123
|
+
return p["id"]
|
|
124
|
+
|
|
125
|
+
if projects:
|
|
126
|
+
click.echo(
|
|
127
|
+
f"[!] Projeto exato '{project}' nao encontrado. Resultados parciais:"
|
|
128
|
+
)
|
|
129
|
+
for p in projects[:5]:
|
|
130
|
+
click.echo(
|
|
131
|
+
f" {p.get('path_with_namespace', '?')} (id: {p.get('id', '?')})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
raise click.ClickException(f"Projeto '{project}' nao encontrado.")
|
|
135
|
+
|
|
136
|
+
def create_project_access_token(
|
|
137
|
+
self,
|
|
138
|
+
project_id: int,
|
|
139
|
+
name: str,
|
|
140
|
+
scopes: list[str],
|
|
141
|
+
access_level: int | None = None,
|
|
142
|
+
expires_at: str | None = None,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Cria project access token."""
|
|
145
|
+
payload: dict[str, Any] = {
|
|
146
|
+
"name": name,
|
|
147
|
+
"scopes": scopes,
|
|
148
|
+
}
|
|
149
|
+
if access_level is not None:
|
|
150
|
+
payload["access_level"] = access_level
|
|
151
|
+
if expires_at:
|
|
152
|
+
payload["expires_at"] = expires_at
|
|
153
|
+
|
|
154
|
+
return self.post(f"/projects/{project_id}/access_tokens", **payload)
|
|
155
|
+
|
|
156
|
+
def list_project_access_tokens(self, project_id: int) -> list[dict[str, Any]]:
|
|
157
|
+
"""Lista project access tokens do projeto."""
|
|
158
|
+
return cast(
|
|
159
|
+
list[dict[str, Any]],
|
|
160
|
+
self.get(f"/projects/{project_id}/access_tokens", per_page=100),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def revoke_project_access_token(self, project_id: int, token_id: int) -> None:
|
|
164
|
+
"""Revoga project access token do projeto."""
|
|
165
|
+
self.delete(f"/projects/{project_id}/access_tokens/{token_id}")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lnp-devopscli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: DevOps CLI — workspace sync (GDrive, git, GPG, systemd timers) + GitLab/Bitwarden tooling
|
|
5
|
+
Author-email: Lucas Neves Pires <npires.lucas@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/lnp-consulting-ti/devops/devops-cli
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/lnp-consulting-ti/devops/devops-cli
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/lnp-consulting-ti/devops/devops-cli/-/issues
|
|
10
|
+
Keywords: devops,workspace-sync,rclone,gdrive,gitlab,bitwarden,systemd
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: requests>=2.28
|
|
27
|
+
|
|
28
|
+
# lnp-devopscli
|
|
29
|
+
|
|
30
|
+
DevOps CLI for workspace sync (Google Drive + git + GPG + systemd timers) and
|
|
31
|
+
GitLab/Bitwarden tooling.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
### Via PyPI (recommended)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install lnp-devopscli
|
|
39
|
+
# or
|
|
40
|
+
pip install --user lnp-devopscli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Provides two equivalent commands: `devopscli` and `dc`.
|
|
44
|
+
|
|
45
|
+
### From source
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://gitlab.com/lnp-consulting-ti/devops/devops-cli.git ~/bin/devops-cli
|
|
49
|
+
cd ~/bin/devops-cli && make setup
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Bootstrap on a new PC
|
|
53
|
+
|
|
54
|
+
For a full workspace bootstrap (rclone + GPG + git-crypt + 10 repos + systemd
|
|
55
|
+
timers), use the public bootstrap launcher:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bash <(curl -fsSL https://gitlab.com/-/snippets/6003334/raw/main/bootstrap.sh)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [MIGRATION-RUNBOOK.md](https://gitlab.com/lnpires/ai/workspace-personal/-/blob/main/MIGRATION-RUNBOOK.md)
|
|
62
|
+
for the complete migration procedure.
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
devopscli ws sync # full bidirectional sync (pull → git → push)
|
|
68
|
+
devopscli ws push # sync local → GDrive (13 categories)
|
|
69
|
+
devopscli ws pull # sync GDrive → local
|
|
70
|
+
devopscli ws doctor # health check (deps, configs, timers, repos)
|
|
71
|
+
devopscli ws ai-sync # git pull/commit/push for ai_workspaces
|
|
72
|
+
devopscli ws repos-sync # git pull/commit/push for repos_sync
|
|
73
|
+
devopscli ws gpg-backup # export GPG key + encrypt + send to GDrive
|
|
74
|
+
devopscli ws gpg-restore # download GPG key from GDrive + decrypt + import
|
|
75
|
+
devopscli ws install-timers # install systemd user timers (replaces cron)
|
|
76
|
+
devopscli ws uninstall-timers # remove systemd timers
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See `devopscli ws --help` for the full list.
|
|
80
|
+
|
|
81
|
+
## Security
|
|
82
|
+
|
|
83
|
+
The CLI is open source but never accesses your data without your credentials:
|
|
84
|
+
|
|
85
|
+
- **Google OAuth** protects access to your GDrive (rclone config)
|
|
86
|
+
- **GPG passphrase** protects the GPG key backup
|
|
87
|
+
- **git-crypt** protects `.env` files inside synced repos
|
|
88
|
+
- **GitLab/SSH keys** protect access to private repositories
|
|
89
|
+
|
|
90
|
+
Anyone can `pip install` the CLI — but without your credentials, they cannot
|
|
91
|
+
access your data.
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Edit `~/bin/config.yaml` (synced via `ws push` step 7):
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
ai_workspaces:
|
|
99
|
+
- name: "workspace-personal"
|
|
100
|
+
path: "~/workspace-personal"
|
|
101
|
+
clone_url: "git@gitlab.com:user/repo.git"
|
|
102
|
+
remote: "origin"
|
|
103
|
+
branch: "main"
|
|
104
|
+
auto_commit: true
|
|
105
|
+
commit_message: "chore(auto-sync): snapshot via ws ai-sync"
|
|
106
|
+
|
|
107
|
+
repos_sync:
|
|
108
|
+
- name: "devops-cli"
|
|
109
|
+
path: "~/bin/devops-cli"
|
|
110
|
+
clone_url: "git@gitlab.com:lnp-consulting-ti/devops/devops-cli.git"
|
|
111
|
+
remote: "origin"
|
|
112
|
+
branch: "main"
|
|
113
|
+
auto_commit: true
|
|
114
|
+
commit_message: "chore(auto-sync): snapshot via ws repos-sync"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Release
|
|
118
|
+
|
|
119
|
+
To publish a new version to PyPI:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 1. Bump version in pyproject.toml
|
|
123
|
+
git commit -am "chore(release): v1.0.1"
|
|
124
|
+
|
|
125
|
+
# 2. Tag and push
|
|
126
|
+
git tag v1.0.1
|
|
127
|
+
git push origin main v1.0.1
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The GitLab CI pipeline auto-publishes when a tag matches `v*.*.*`.
|
|
131
|
+
|
|
132
|
+
Setup once: add `PYPI_TOKEN` to GitLab CI/CD Variables (Settings > CI/CD).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dc_cli/__init__.py,sha256=V8NQxHA6heIkRMo_WVrkPrlazyK21jUDcGWwRuNgQzs,61
|
|
2
|
+
dc_cli/main.py,sha256=h30wx8jGTZ_qOpYtMzfTUB2nW3VKePcXRSoLYUOMWZ0,1326
|
|
3
|
+
dc_cli/groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
dc_cli/groups/bw.py,sha256=AACX6MSslPjlU2tHTX5FL8tDlHf4IzZtRTlxcnfvd5Q,12997
|
|
5
|
+
dc_cli/groups/gl.py,sha256=d_OVtQOGRcPTQ2yJOwUiVbrcuBqiEOahrZ1i1_Jr4X0,9632
|
|
6
|
+
dc_cli/groups/ws.py,sha256=bRhOz6ar0DgZX8pd-hgwfML_r3S101j_RnUAvpYlaWM,47147
|
|
7
|
+
dc_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
dc_cli/utils/config.py,sha256=rnZmJqfd3vq0MoD5UFwzVVtJjQLw28aWS22RqxaxAag,2383
|
|
9
|
+
dc_cli/utils/gitlab.py,sha256=yK0UuvYfwxgV1T6nJxo0iOm-pYQWZJ9sWkjl01Ko1sw,5577
|
|
10
|
+
lnp_devopscli-1.0.0.dist-info/METADATA,sha256=Neg9EcLmTYgNbjRRP0w7vMNGn4AZpyiSD9-JSsvdoZI,4208
|
|
11
|
+
lnp_devopscli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
lnp_devopscli-1.0.0.dist-info/entry_points.txt,sha256=yvFR7YX57Mr_KtRiCA2muf61eVGV2ThFmXOu6O2jy0U,69
|
|
13
|
+
lnp_devopscli-1.0.0.dist-info/top_level.txt,sha256=lddxuZKQNgGZHWu5I8IQX_PYOOYHfVQZpiBIeU2DYfc,7
|
|
14
|
+
lnp_devopscli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dc_cli
|