clue2app-cli 0.2.7__tar.gz → 0.3.1__tar.gz
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.
- {clue2app_cli-0.2.7/src/clue2app_cli.egg-info → clue2app_cli-0.3.1}/PKG-INFO +27 -26
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/README.md +23 -20
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/pyproject.toml +10 -1
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/creds_module.py +87 -10
- clue2app_cli-0.3.1/src/clue2app_cli/directory_cmd.py +1076 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/main.py +434 -39
- clue2app_cli-0.3.1/src/clue2app_cli/mcp_cmd.py +467 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/sdk.py +968 -23
- clue2app_cli-0.3.1/src/clue2app_cli/tests_cmd.py +845 -0
- clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/__init__.py +15 -0
- clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/c2a-tests.v1.json +255 -0
- clue2app_cli-0.3.1/src/clue2app_cli/tests_schema/validator.py +141 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1/src/clue2app_cli.egg-info}/PKG-INFO +27 -26
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/SOURCES.txt +6 -2
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/requires.txt +5 -0
- clue2app_cli-0.2.7/setup.py +0 -64
- clue2app_cli-0.2.7/src/clue2app_cli.egg-info/not-zip-safe +0 -1
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/LICENSE +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/setup.cfg +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/__init__.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/agents/__init__.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/agents/ecr_healer.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/auth.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/config.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/core_client.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/curses_wizard.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/entrypoint_check.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/__init__.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/curses_app.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/data.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/screens/__init__.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/textual_app.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui/widgets/__init__.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/tui_wizard.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/version.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli/wizard.py +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/dependency_links.txt +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/entry_points.txt +0 -0
- {clue2app_cli-0.2.7 → clue2app_cli-0.3.1}/src/clue2app_cli.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clue2app-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI application for clue2app using the clue2app-python-sdk
|
|
5
|
-
Home-page: https://github.com/clue2solve/clue2app-cli
|
|
6
|
-
Author: Clue2App CLI Team
|
|
7
5
|
Author-email: Clue2App CLI Team <support@clue2app.com>
|
|
8
6
|
License: MIT
|
|
9
7
|
Project-URL: Homepage, https://github.com/clue2solve/clue2app-cli
|
|
@@ -33,6 +31,9 @@ Requires-Dist: pydantic>=1.10.0
|
|
|
33
31
|
Requires-Dist: toml>=0.10.0
|
|
34
32
|
Requires-Dist: questionary>=2.0.0
|
|
35
33
|
Requires-Dist: textual>=0.40.0
|
|
34
|
+
Requires-Dist: jsonschema>=4.18.0
|
|
35
|
+
Requires-Dist: PyYAML>=6.0
|
|
36
|
+
Requires-Dist: clue2app-mcp>=0.1.1; python_version >= "3.10"
|
|
36
37
|
Provides-Extra: dev
|
|
37
38
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
39
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -45,10 +46,7 @@ Provides-Extra: build
|
|
|
45
46
|
Requires-Dist: pyinstaller>=6.0.0; extra == "build"
|
|
46
47
|
Requires-Dist: invoke>=2.0.0; extra == "build"
|
|
47
48
|
Requires-Dist: tox>=4.0.0; extra == "build"
|
|
48
|
-
Dynamic: author
|
|
49
|
-
Dynamic: home-page
|
|
50
49
|
Dynamic: license-file
|
|
51
|
-
Dynamic: requires-python
|
|
52
50
|
|
|
53
51
|
# Clue2App CLI
|
|
54
52
|
|
|
@@ -59,31 +57,13 @@ Dynamic: requires-python
|
|
|
59
57
|
[](https://github.com/clue2solve/clue2app-cli/actions/workflows/tests.yml)
|
|
60
58
|
[](https://github.com/clue2solve/clue2app-cli/releases)
|
|
61
59
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
### Option 1: pip (Recommended)
|
|
60
|
+
## Install
|
|
65
61
|
|
|
66
62
|
```bash
|
|
67
63
|
pip install clue2app-cli
|
|
68
64
|
```
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Download from [Releases](https://github.com/clue2solve/clue2app-cli/releases):
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
# macOS (Apple Silicon)
|
|
76
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-arm64 -o c2a
|
|
77
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
78
|
-
|
|
79
|
-
# macOS (Intel)
|
|
80
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-x64 -o c2a
|
|
81
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
82
|
-
|
|
83
|
-
# Linux
|
|
84
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-linux-x64 -o c2a
|
|
85
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
86
|
-
```
|
|
66
|
+
Requires Python 3.10+. Verify with `c2a --version`.
|
|
87
67
|
|
|
88
68
|
## Quick Start
|
|
89
69
|
|
|
@@ -128,9 +108,30 @@ c2a rebuild my-app
|
|
|
128
108
|
| `c2a rebuild <app>` | Trigger app rebuild |
|
|
129
109
|
| `c2a env set <app> KEY=value` | Set environment variables |
|
|
130
110
|
| `c2a secrets create -n <name> -u <user> -p <token>` | Create a git credential secret |
|
|
111
|
+
| `c2a user invite -e <email> -A <account_uuid> -P <project_uuid> -r <role>` | Invite a project member via the invitations flow (SCK-625) |
|
|
131
112
|
| `c2a llm list` / `c2a llm create` / `c2a llm bind <app> <secret>` | Manage LLM Gateway bindings (see [Daari LLM](docs/daari-llm.md)) |
|
|
132
113
|
| `c2a daari secrets list` / `bind <app> <secret>` | Manage Daari service bindings (see [Daari](docs/daari.md)) |
|
|
133
114
|
|
|
115
|
+
### Inviting a project member (SCK-625)
|
|
116
|
+
|
|
117
|
+
Pass `-A <account_uuid>` to route through the invitations service
|
|
118
|
+
(`POST /api/v1/invitations/project-member`, backed by SCK-622). The
|
|
119
|
+
new endpoint mints an invitation row, sends the redemption email by
|
|
120
|
+
default, and returns a redemption URL the CLI prints:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Server sends the invitation email (default delivery).
|
|
124
|
+
c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r CONTRIBUTOR
|
|
125
|
+
|
|
126
|
+
# --copy-only skips the email; hand-deliver the printed URL yourself.
|
|
127
|
+
c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r VIEWER --copy-only
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`-r` accepts `VIEWER`, `CONTRIBUTOR`, or `PROJECT_ADMIN`. Without
|
|
131
|
+
`-A`, `c2a user invite` keeps its legacy behaviour (invite-with-
|
|
132
|
+
project-grants via `/api/accounts/{id}/members`, or the LOCAL user
|
|
133
|
+
create flow with `-u`/`-p`).
|
|
134
|
+
|
|
134
135
|
## Configuration
|
|
135
136
|
|
|
136
137
|
Config files are stored in `~/.c2a/`:
|
|
@@ -7,31 +7,13 @@
|
|
|
7
7
|
[](https://github.com/clue2solve/clue2app-cli/actions/workflows/tests.yml)
|
|
8
8
|
[](https://github.com/clue2solve/clue2app-cli/releases)
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
### Option 1: pip (Recommended)
|
|
10
|
+
## Install
|
|
13
11
|
|
|
14
12
|
```bash
|
|
15
13
|
pip install clue2app-cli
|
|
16
14
|
```
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Download from [Releases](https://github.com/clue2solve/clue2app-cli/releases):
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
# macOS (Apple Silicon)
|
|
24
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-arm64 -o c2a
|
|
25
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
26
|
-
|
|
27
|
-
# macOS (Intel)
|
|
28
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-macos-x64 -o c2a
|
|
29
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
30
|
-
|
|
31
|
-
# Linux
|
|
32
|
-
curl -L https://github.com/clue2solve/clue2app-cli/releases/latest/download/clue2app-linux-x64 -o c2a
|
|
33
|
-
chmod +x c2a && sudo mv c2a /usr/local/bin/
|
|
34
|
-
```
|
|
16
|
+
Requires Python 3.10+. Verify with `c2a --version`.
|
|
35
17
|
|
|
36
18
|
## Quick Start
|
|
37
19
|
|
|
@@ -76,9 +58,30 @@ c2a rebuild my-app
|
|
|
76
58
|
| `c2a rebuild <app>` | Trigger app rebuild |
|
|
77
59
|
| `c2a env set <app> KEY=value` | Set environment variables |
|
|
78
60
|
| `c2a secrets create -n <name> -u <user> -p <token>` | Create a git credential secret |
|
|
61
|
+
| `c2a user invite -e <email> -A <account_uuid> -P <project_uuid> -r <role>` | Invite a project member via the invitations flow (SCK-625) |
|
|
79
62
|
| `c2a llm list` / `c2a llm create` / `c2a llm bind <app> <secret>` | Manage LLM Gateway bindings (see [Daari LLM](docs/daari-llm.md)) |
|
|
80
63
|
| `c2a daari secrets list` / `bind <app> <secret>` | Manage Daari service bindings (see [Daari](docs/daari.md)) |
|
|
81
64
|
|
|
65
|
+
### Inviting a project member (SCK-625)
|
|
66
|
+
|
|
67
|
+
Pass `-A <account_uuid>` to route through the invitations service
|
|
68
|
+
(`POST /api/v1/invitations/project-member`, backed by SCK-622). The
|
|
69
|
+
new endpoint mints an invitation row, sends the redemption email by
|
|
70
|
+
default, and returns a redemption URL the CLI prints:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Server sends the invitation email (default delivery).
|
|
74
|
+
c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r CONTRIBUTOR
|
|
75
|
+
|
|
76
|
+
# --copy-only skips the email; hand-deliver the printed URL yourself.
|
|
77
|
+
c2a user invite -e alice@corp.com -A <account_uuid> -P <project_uuid> -r VIEWER --copy-only
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`-r` accepts `VIEWER`, `CONTRIBUTOR`, or `PROJECT_ADMIN`. Without
|
|
81
|
+
`-A`, `c2a user invite` keeps its legacy behaviour (invite-with-
|
|
82
|
+
project-grants via `/api/accounts/{id}/members`, or the LOCAL user
|
|
83
|
+
create flow with `-u`/`-p`).
|
|
84
|
+
|
|
82
85
|
## Configuration
|
|
83
86
|
|
|
84
87
|
Config files are stored in `~/.c2a/`:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clue2app-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "CLI application for clue2app using the clue2app-python-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -36,6 +36,9 @@ dependencies = [
|
|
|
36
36
|
"toml>=0.10.0",
|
|
37
37
|
"questionary>=2.0.0", # Interactive prompts for wizard
|
|
38
38
|
"textual>=0.40.0", # Modern TUI framework with CSS animations
|
|
39
|
+
"jsonschema>=4.18.0", # .c2a-tests.yaml validator (SCK-463); needs Draft 2020-12 support
|
|
40
|
+
"PyYAML>=6.0", # Parse .c2a-tests.yaml for `c2a tests validate`
|
|
41
|
+
"clue2app-mcp>=0.1.1; python_version>='3.10'", # Bundle MCP server transitively (v0.3.0); MCP requires Python 3.10+
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
[project.optional-dependencies]
|
|
@@ -70,6 +73,12 @@ where = ["src"]
|
|
|
70
73
|
[tool.setuptools.package-dir]
|
|
71
74
|
"" = "src"
|
|
72
75
|
|
|
76
|
+
# Ship the bundled .c2a-tests.yaml v1 schema (SCK-463). `c2a tests validate`
|
|
77
|
+
# needs it at runtime; without an explicit package-data line, sdists ship it
|
|
78
|
+
# but wheels can drop it depending on setuptools version. Belt-and-braces.
|
|
79
|
+
[tool.setuptools.package-data]
|
|
80
|
+
"clue2app_cli.tests_schema" = ["*.json"]
|
|
81
|
+
|
|
73
82
|
[tool.black]
|
|
74
83
|
line-length = 88
|
|
75
84
|
target-version = ['py38']
|
|
@@ -109,23 +109,51 @@ def trigger_kpack_rebuild(image_name: str, namespace: str) -> tuple[bool, str]:
|
|
|
109
109
|
class CredStatus:
|
|
110
110
|
"""Result of probing one git secret's PAT against GitHub.
|
|
111
111
|
|
|
112
|
-
`alive`:
|
|
113
|
-
`reason`:
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
`alive`: token authenticated successfully against GitHub.
|
|
113
|
+
`reason`: short tag for grouping (e.g. "ok", "401-unauthorized",
|
|
114
|
+
"saml-required", "no-password", "unreachable",
|
|
115
|
+
"expiring-soon").
|
|
116
|
+
`detail`: human-readable extra info (which scope was missing, etc.).
|
|
117
|
+
`expires_at`: ISO-8601 expiry timestamp from GitHub's
|
|
118
|
+
`github-authentication-token-expiration` response header,
|
|
119
|
+
if present. Returned by GitHub for classic + fine-grained
|
|
120
|
+
PATs (`ghp_*` / `github_pat_*`). Absent for `gho_*` OAuth
|
|
121
|
+
tokens (incl. `gh auth token`) and GitHub App installation
|
|
122
|
+
tokens — those return None and the caller can't predict
|
|
123
|
+
expiry.
|
|
124
|
+
`days_to_expiry`: integer days from now until `expires_at`. Negative
|
|
125
|
+
means already expired. None when `expires_at` is None.
|
|
116
126
|
"""
|
|
117
127
|
|
|
118
128
|
alive: bool
|
|
119
129
|
reason: str
|
|
120
130
|
detail: str = ""
|
|
131
|
+
expires_at: Optional[str] = None
|
|
132
|
+
days_to_expiry: Optional[int] = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Tokens within this many days of expiry get the `expiring-soon` reason
|
|
136
|
+
# even though they're still alive. Picked at 14d so a weekly check cycle
|
|
137
|
+
# always catches them at least twice before they break.
|
|
138
|
+
EXPIRY_WARN_DAYS = 14
|
|
121
139
|
|
|
122
140
|
|
|
123
141
|
def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
|
|
124
|
-
"""Hit `GET https://api.github.com/user` with the token.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
"""Hit `GET https://api.github.com/user` with the token. Classifies
|
|
143
|
+
the response:
|
|
144
|
+
|
|
145
|
+
- 200 + no expiry header -> alive, reason="ok"
|
|
146
|
+
- 200 + expiry header > 14d -> alive, reason="ok", expires_at set
|
|
147
|
+
- 200 + expiry header within 14d -> alive, reason="expiring-soon"
|
|
148
|
+
- 401 -> dead, reason="401-unauthorized"
|
|
149
|
+
- 403 + X-GitHub-SSO header -> dead, reason="saml-required"
|
|
150
|
+
- 403 generic -> dead, reason="403-forbidden"
|
|
151
|
+
- URLError / timeout -> unknown, reason="unreachable"
|
|
152
|
+
|
|
153
|
+
Expiry header (`github-authentication-token-expiration`) is only
|
|
154
|
+
populated for classic + fine-grained PATs. `gho_*` OAuth tokens
|
|
155
|
+
don't expose it; we can only detect them after they break.
|
|
156
|
+
"""
|
|
129
157
|
import urllib.error
|
|
130
158
|
import urllib.request
|
|
131
159
|
|
|
@@ -143,7 +171,16 @@ def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
|
|
|
143
171
|
try:
|
|
144
172
|
with urllib.request.urlopen(req, timeout=timeout_s) as r:
|
|
145
173
|
if r.status == 200:
|
|
146
|
-
|
|
174
|
+
exp_raw = r.headers.get("github-authentication-token-expiration")
|
|
175
|
+
expires_at, days = _parse_expiry_header(exp_raw)
|
|
176
|
+
if days is not None and days <= EXPIRY_WARN_DAYS:
|
|
177
|
+
return CredStatus(
|
|
178
|
+
True, "expiring-soon",
|
|
179
|
+
f"expires in {days} day(s)",
|
|
180
|
+
expires_at=expires_at,
|
|
181
|
+
days_to_expiry=days,
|
|
182
|
+
)
|
|
183
|
+
return CredStatus(True, "ok", expires_at=expires_at, days_to_expiry=days)
|
|
147
184
|
return CredStatus(False, f"unexpected-{r.status}", "")
|
|
148
185
|
except urllib.error.HTTPError as e:
|
|
149
186
|
if e.code == 401:
|
|
@@ -160,6 +197,46 @@ def probe_github_pat(token: str, *, timeout_s: float = 10.0) -> CredStatus:
|
|
|
160
197
|
return CredStatus(False, "unreachable", str(e))
|
|
161
198
|
|
|
162
199
|
|
|
200
|
+
def _parse_expiry_header(raw: Optional[str]) -> tuple[Optional[str], Optional[int]]:
|
|
201
|
+
"""Parse GitHub's `github-authentication-token-expiration` header.
|
|
202
|
+
|
|
203
|
+
Format example: `2026-08-15 14:30:00 UTC`. Returns
|
|
204
|
+
(iso_string, days_to_expiry). Both None if header is absent or
|
|
205
|
+
unparseable — we want this to be a soft signal, never a crash."""
|
|
206
|
+
if not raw:
|
|
207
|
+
return None, None
|
|
208
|
+
import datetime
|
|
209
|
+
|
|
210
|
+
raw = raw.strip()
|
|
211
|
+
formats = [
|
|
212
|
+
"%Y-%m-%d %H:%M:%S %Z",
|
|
213
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
214
|
+
"%Y-%m-%d %H:%M:%S UTC",
|
|
215
|
+
]
|
|
216
|
+
parsed: Optional[datetime.datetime] = None
|
|
217
|
+
for fmt in formats:
|
|
218
|
+
try:
|
|
219
|
+
parsed = datetime.datetime.strptime(raw, fmt)
|
|
220
|
+
break
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
if parsed is None:
|
|
224
|
+
# Last resort: try fromisoformat after Z->+00:00
|
|
225
|
+
try:
|
|
226
|
+
parsed = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
227
|
+
except ValueError:
|
|
228
|
+
return None, None
|
|
229
|
+
|
|
230
|
+
# Make tz-aware in UTC if naive (GitHub header is always UTC)
|
|
231
|
+
if parsed.tzinfo is None:
|
|
232
|
+
parsed = parsed.replace(tzinfo=datetime.timezone.utc)
|
|
233
|
+
|
|
234
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
235
|
+
delta = parsed - now
|
|
236
|
+
days = int(delta.total_seconds() // 86400) # truncate to whole days; can be negative
|
|
237
|
+
return parsed.isoformat(), days
|
|
238
|
+
|
|
239
|
+
|
|
163
240
|
# ---------------------------------------------------------------------------
|
|
164
241
|
# Cluster discovery — find all kpack Image -> git secret references
|
|
165
242
|
# ---------------------------------------------------------------------------
|