kentaro 0.1.0__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.
- kentaro-0.1.0/LICENSE +21 -0
- kentaro-0.1.0/PKG-INFO +138 -0
- kentaro-0.1.0/README.md +112 -0
- kentaro-0.1.0/kentaro/__init__.py +0 -0
- kentaro-0.1.0/kentaro/__main__.py +3 -0
- kentaro-0.1.0/kentaro/api/__init__.py +0 -0
- kentaro-0.1.0/kentaro/api/demo.py +48 -0
- kentaro-0.1.0/kentaro/api/live.py +214 -0
- kentaro-0.1.0/kentaro/cli.py +264 -0
- kentaro-0.1.0/kentaro/config/__init__.py +0 -0
- kentaro-0.1.0/kentaro/config/loader.py +48 -0
- kentaro-0.1.0/kentaro/dashboard/__init__.py +0 -0
- kentaro-0.1.0/kentaro/dashboard/render.py +146 -0
- kentaro-0.1.0/kentaro/data_products.py +227 -0
- kentaro-0.1.0/kentaro/governance/__init__.py +14 -0
- kentaro-0.1.0/kentaro/governance/evidence.py +143 -0
- kentaro-0.1.0/kentaro/governance/policy.py +103 -0
- kentaro-0.1.0/kentaro/governance/schemas.py +314 -0
- kentaro-0.1.0/kentaro/reports/__init__.py +0 -0
- kentaro-0.1.0/kentaro/reports/brief.py +40 -0
- kentaro-0.1.0/kentaro/resources/__init__.py +1 -0
- kentaro-0.1.0/kentaro/resources/demo-portfolio.json +25 -0
- kentaro-0.1.0/kentaro/workspace.py +82 -0
- kentaro-0.1.0/kentaro.egg-info/PKG-INFO +138 -0
- kentaro-0.1.0/kentaro.egg-info/SOURCES.txt +35 -0
- kentaro-0.1.0/kentaro.egg-info/dependency_links.txt +1 -0
- kentaro-0.1.0/kentaro.egg-info/entry_points.txt +2 -0
- kentaro-0.1.0/kentaro.egg-info/requires.txt +3 -0
- kentaro-0.1.0/kentaro.egg-info/top_level.txt +1 -0
- kentaro-0.1.0/pyproject.toml +42 -0
- kentaro-0.1.0/setup.cfg +4 -0
- kentaro-0.1.0/tests/test_cli_defaults.py +119 -0
- kentaro-0.1.0/tests/test_data_product_cli.py +132 -0
- kentaro-0.1.0/tests/test_distribution_install.py +67 -0
- kentaro-0.1.0/tests/test_governance.py +167 -0
- kentaro-0.1.0/tests/test_live_client.py +98 -0
- kentaro-0.1.0/tests/test_vertical_slice.py +87 -0
kentaro-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maysano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kentaro-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kentaro
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only Agentic Maysano Harness for sourced portfolio context and local governance validation
|
|
5
|
+
Author: Maysano
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://maysano.com/kentaro
|
|
8
|
+
Project-URL: Documentation, https://github.com/Maysano/Kentaro/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/Maysano/Kentaro
|
|
10
|
+
Project-URL: Issues, https://github.com/Maysano/Kentaro/issues
|
|
11
|
+
Project-URL: ODPS Specifications, https://opendataproducts.org/
|
|
12
|
+
Keywords: agents,governance,maysano,odps,portfolio,read-only
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: odps
|
|
24
|
+
Requires-Dist: open-data-products<0.5,>=0.3.4; extra == "odps"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Kentaro
|
|
28
|
+
|
|
29
|
+
Kentaro is the open-source, read-only Agentic Maysano Harness. It gives you a
|
|
30
|
+
working local portfolio briefing and sourced dashboard immediately, then lets you
|
|
31
|
+
opt in to a bounded read-only Maysano source without turning Kentaro into a system
|
|
32
|
+
that acts on your behalf.
|
|
33
|
+
|
|
34
|
+
Learn more at [maysano.com/kentaro](https://maysano.com/kentaro).
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Kentaro requires Python 3.11 or later.
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
python -m pip install kentaro
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Run the demo
|
|
45
|
+
|
|
46
|
+
Run the complete synthetic demonstration with no account, configuration file,
|
|
47
|
+
repository checkout, API key, or network request:
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
kentaro demo
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
It prints a sourced portfolio briefing and writes a dashboard to a new timestamped
|
|
54
|
+
file under `.kentaro/workspace/`. Kentaro creates that directory automatically and
|
|
55
|
+
prints the complete local path.
|
|
56
|
+
|
|
57
|
+
## Use Kentaro locally
|
|
58
|
+
|
|
59
|
+
The normal local commands use the packaged synthetic portfolio context:
|
|
60
|
+
|
|
61
|
+
```shell
|
|
62
|
+
kentaro portfolio brief
|
|
63
|
+
kentaro dashboard
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`kentaro dashboard` always creates a fresh timestamped HTML file in
|
|
67
|
+
`.kentaro/workspace/`; it never overwrites an existing dashboard.
|
|
68
|
+
|
|
69
|
+
## Connect a read-only Maysano source
|
|
70
|
+
|
|
71
|
+
Live access is explicit and opt-in. Configure a Maysano-issued read-only key through
|
|
72
|
+
the guided local setup, then select the source for an individual read:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
kentaro configure maysano
|
|
76
|
+
kentaro portfolio brief --source maysano
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The key is stored only in Kentaro's ignored local workspace and is never printed.
|
|
80
|
+
The pilot source is GET-only. It does not currently provide owners, decisions,
|
|
81
|
+
risks, evidence, approval history, or milestones; Kentaro labels those fields as
|
|
82
|
+
unavailable rather than inventing them.
|
|
83
|
+
|
|
84
|
+
## Safety boundary
|
|
85
|
+
|
|
86
|
+
Kentaro v0.1 retrieves bounded sourced context, validates local versioned governance
|
|
87
|
+
and delivery objects, generates drafts and reports, classifies proposed requests, and
|
|
88
|
+
exports local evidence. It does not create external records, write to Maysano or a
|
|
89
|
+
business-context graph, execute business actions, request or process approvals, or
|
|
90
|
+
claim immutable or compliance-grade audit. The optional local draft commands create
|
|
91
|
+
local artifacts only; a hosted provider requires explicit acknowledgement before it
|
|
92
|
+
receives source content.
|
|
93
|
+
|
|
94
|
+
Maysano remains the system of record for portfolio state, approvals, graph updates,
|
|
95
|
+
tenant controls, authoritative revocation, retention, and platform audit. An
|
|
96
|
+
`allowed` classification means only that a request is structurally in scope; it is
|
|
97
|
+
not permission to execute.
|
|
98
|
+
|
|
99
|
+
## Advanced configuration and governance
|
|
100
|
+
|
|
101
|
+
TOML profiles are an advanced team configuration mechanism for named environments,
|
|
102
|
+
custom API endpoints or workspace paths, model declarations, organisation policies,
|
|
103
|
+
and non-default profiles. They are never needed for the normal local experience.
|
|
104
|
+
|
|
105
|
+
To create proposed local data-product, use-case, or objective drafts from Markdown or
|
|
106
|
+
text, install the optional SDK integration. These local artifacts align with the
|
|
107
|
+
[Open Data Products (ODPS) family of specifications](https://opendataproducts.org/):
|
|
108
|
+
|
|
109
|
+
```shell
|
|
110
|
+
python -m pip install "kentaro[odps]"
|
|
111
|
+
kentaro data-product draft ./business-context.md
|
|
112
|
+
kentaro use-case draft ./business-context.md
|
|
113
|
+
kentaro objective draft ./business-context.md
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Validate a saved local draft without generating anything:
|
|
117
|
+
|
|
118
|
+
```shell
|
|
119
|
+
kentaro data-product validate ./data-product.yaml
|
|
120
|
+
kentaro use-case validate ./use-case.yaml
|
|
121
|
+
kentaro objective validate ./objective.yaml
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The default provider is local Ollama. A hosted provider requires an explicit provider
|
|
125
|
+
configuration and `--allow-hosted-content`, because it may receive the source document.
|
|
126
|
+
Each result remains a local draft; it never creates a Maysano record or grants authority.
|
|
127
|
+
|
|
128
|
+
Detailed material is kept out of onboarding:
|
|
129
|
+
|
|
130
|
+
- [Getting started](https://github.com/Maysano/Kentaro/blob/main/docs/getting-started.md)
|
|
131
|
+
- [Advanced configuration](https://github.com/Maysano/Kentaro/blob/main/docs/configuration.md)
|
|
132
|
+
- [Local draft artifacts](https://github.com/Maysano/Kentaro/blob/main/docs/data-product-drafts.md)
|
|
133
|
+
- [Governance and delivery object model](https://github.com/Maysano/Kentaro/blob/main/docs/governance-and-delivery-object-model.md)
|
|
134
|
+
- [Architecture](https://github.com/Maysano/Kentaro/blob/main/docs/architecture.md)
|
|
135
|
+
- [Security and data boundaries](https://github.com/Maysano/Kentaro/blob/main/docs/security-and-data-boundaries.md)
|
|
136
|
+
- [Developer guide](https://github.com/Maysano/Kentaro/blob/main/docs/developer-guide.md)
|
|
137
|
+
|
|
138
|
+
[MIT License](LICENSE) © 2026 Maysano.
|
kentaro-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Kentaro
|
|
2
|
+
|
|
3
|
+
Kentaro is the open-source, read-only Agentic Maysano Harness. It gives you a
|
|
4
|
+
working local portfolio briefing and sourced dashboard immediately, then lets you
|
|
5
|
+
opt in to a bounded read-only Maysano source without turning Kentaro into a system
|
|
6
|
+
that acts on your behalf.
|
|
7
|
+
|
|
8
|
+
Learn more at [maysano.com/kentaro](https://maysano.com/kentaro).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Kentaro requires Python 3.11 or later.
|
|
13
|
+
|
|
14
|
+
```shell
|
|
15
|
+
python -m pip install kentaro
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Run the demo
|
|
19
|
+
|
|
20
|
+
Run the complete synthetic demonstration with no account, configuration file,
|
|
21
|
+
repository checkout, API key, or network request:
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
kentaro demo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It prints a sourced portfolio briefing and writes a dashboard to a new timestamped
|
|
28
|
+
file under `.kentaro/workspace/`. Kentaro creates that directory automatically and
|
|
29
|
+
prints the complete local path.
|
|
30
|
+
|
|
31
|
+
## Use Kentaro locally
|
|
32
|
+
|
|
33
|
+
The normal local commands use the packaged synthetic portfolio context:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
kentaro portfolio brief
|
|
37
|
+
kentaro dashboard
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`kentaro dashboard` always creates a fresh timestamped HTML file in
|
|
41
|
+
`.kentaro/workspace/`; it never overwrites an existing dashboard.
|
|
42
|
+
|
|
43
|
+
## Connect a read-only Maysano source
|
|
44
|
+
|
|
45
|
+
Live access is explicit and opt-in. Configure a Maysano-issued read-only key through
|
|
46
|
+
the guided local setup, then select the source for an individual read:
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
kentaro configure maysano
|
|
50
|
+
kentaro portfolio brief --source maysano
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The key is stored only in Kentaro's ignored local workspace and is never printed.
|
|
54
|
+
The pilot source is GET-only. It does not currently provide owners, decisions,
|
|
55
|
+
risks, evidence, approval history, or milestones; Kentaro labels those fields as
|
|
56
|
+
unavailable rather than inventing them.
|
|
57
|
+
|
|
58
|
+
## Safety boundary
|
|
59
|
+
|
|
60
|
+
Kentaro v0.1 retrieves bounded sourced context, validates local versioned governance
|
|
61
|
+
and delivery objects, generates drafts and reports, classifies proposed requests, and
|
|
62
|
+
exports local evidence. It does not create external records, write to Maysano or a
|
|
63
|
+
business-context graph, execute business actions, request or process approvals, or
|
|
64
|
+
claim immutable or compliance-grade audit. The optional local draft commands create
|
|
65
|
+
local artifacts only; a hosted provider requires explicit acknowledgement before it
|
|
66
|
+
receives source content.
|
|
67
|
+
|
|
68
|
+
Maysano remains the system of record for portfolio state, approvals, graph updates,
|
|
69
|
+
tenant controls, authoritative revocation, retention, and platform audit. An
|
|
70
|
+
`allowed` classification means only that a request is structurally in scope; it is
|
|
71
|
+
not permission to execute.
|
|
72
|
+
|
|
73
|
+
## Advanced configuration and governance
|
|
74
|
+
|
|
75
|
+
TOML profiles are an advanced team configuration mechanism for named environments,
|
|
76
|
+
custom API endpoints or workspace paths, model declarations, organisation policies,
|
|
77
|
+
and non-default profiles. They are never needed for the normal local experience.
|
|
78
|
+
|
|
79
|
+
To create proposed local data-product, use-case, or objective drafts from Markdown or
|
|
80
|
+
text, install the optional SDK integration. These local artifacts align with the
|
|
81
|
+
[Open Data Products (ODPS) family of specifications](https://opendataproducts.org/):
|
|
82
|
+
|
|
83
|
+
```shell
|
|
84
|
+
python -m pip install "kentaro[odps]"
|
|
85
|
+
kentaro data-product draft ./business-context.md
|
|
86
|
+
kentaro use-case draft ./business-context.md
|
|
87
|
+
kentaro objective draft ./business-context.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Validate a saved local draft without generating anything:
|
|
91
|
+
|
|
92
|
+
```shell
|
|
93
|
+
kentaro data-product validate ./data-product.yaml
|
|
94
|
+
kentaro use-case validate ./use-case.yaml
|
|
95
|
+
kentaro objective validate ./objective.yaml
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The default provider is local Ollama. A hosted provider requires an explicit provider
|
|
99
|
+
configuration and `--allow-hosted-content`, because it may receive the source document.
|
|
100
|
+
Each result remains a local draft; it never creates a Maysano record or grants authority.
|
|
101
|
+
|
|
102
|
+
Detailed material is kept out of onboarding:
|
|
103
|
+
|
|
104
|
+
- [Getting started](https://github.com/Maysano/Kentaro/blob/main/docs/getting-started.md)
|
|
105
|
+
- [Advanced configuration](https://github.com/Maysano/Kentaro/blob/main/docs/configuration.md)
|
|
106
|
+
- [Local draft artifacts](https://github.com/Maysano/Kentaro/blob/main/docs/data-product-drafts.md)
|
|
107
|
+
- [Governance and delivery object model](https://github.com/Maysano/Kentaro/blob/main/docs/governance-and-delivery-object-model.md)
|
|
108
|
+
- [Architecture](https://github.com/Maysano/Kentaro/blob/main/docs/architecture.md)
|
|
109
|
+
- [Security and data boundaries](https://github.com/Maysano/Kentaro/blob/main/docs/security-and-data-boundaries.md)
|
|
110
|
+
- [Developer guide](https://github.com/Maysano/Kentaro/blob/main/docs/developer-guide.md)
|
|
111
|
+
|
|
112
|
+
[MIT License](LICENSE) © 2026 Maysano.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Local fixture contract. This module contains no network transport."""
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from importlib.resources import as_file, files
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
DEMO_SCOPE = "portfolio-demo"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_packaged_demo() -> dict:
|
|
11
|
+
"""Read the synthetic demo packaged with Kentaro; no configuration or network is used."""
|
|
12
|
+
resource = files("kentaro.resources").joinpath("demo-portfolio.json")
|
|
13
|
+
with as_file(resource) as fixture:
|
|
14
|
+
return DemoClient(fixture, DEMO_SCOPE).read_portfolio()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DemoClient:
|
|
18
|
+
def __init__(self, fixture, scope):
|
|
19
|
+
self._fixture = fixture
|
|
20
|
+
self._scope = scope
|
|
21
|
+
|
|
22
|
+
def request(self, method):
|
|
23
|
+
if method != "GET":
|
|
24
|
+
raise ValueError("Only GET is permitted in Kentaro v0.1")
|
|
25
|
+
return self.read_portfolio()
|
|
26
|
+
|
|
27
|
+
def read_portfolio(self):
|
|
28
|
+
with open(self._fixture, encoding="utf-8") as stream:
|
|
29
|
+
data = json.load(stream)
|
|
30
|
+
if data.get("schema_version") != 1 or data.get("synthetic") is not True:
|
|
31
|
+
raise ValueError("Expected a version 1 synthetic fixture")
|
|
32
|
+
records = [r for r in data["records"] if r["scope"] == self._scope]
|
|
33
|
+
if not records:
|
|
34
|
+
raise ValueError("No demo records available in the selected scope")
|
|
35
|
+
ids = {r["id"] for r in records}
|
|
36
|
+
if len(ids) != len(records):
|
|
37
|
+
raise ValueError("Duplicate source IDs")
|
|
38
|
+
for record in records:
|
|
39
|
+
for key in ("id", "kind", "title", "owner", "state", "version", "updated_at", "source_ref"):
|
|
40
|
+
if key not in record:
|
|
41
|
+
raise ValueError("Incomplete source metadata: " + key)
|
|
42
|
+
relationships = [r for r in data["relationships"] if r["scope"] == self._scope
|
|
43
|
+
and r["source"] in ids and r["target"] in ids]
|
|
44
|
+
return {"scope": self._scope, "records": records, "relationships": relationships,
|
|
45
|
+
"retrieved_at": datetime.now(timezone.utc).isoformat(),
|
|
46
|
+
"limitations": ["Synthetic fixture; live Maysano API capabilities unverified.",
|
|
47
|
+
"Fixture load time is not platform freshness; use record timestamps.",
|
|
48
|
+
"Acceptance criteria, delivery milestones and approval history unavailable."]}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Read-only client for the documented Maysano external pilot API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import ssl
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
from urllib.parse import quote, urlencode, urlsplit
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RESOURCE_SPECS = (
|
|
14
|
+
("products", "data_product", "name"),
|
|
15
|
+
("business-objectives", "objective", "title"),
|
|
16
|
+
("business-use-cases", "use_case", "name"),
|
|
17
|
+
)
|
|
18
|
+
LINK_KINDS = {
|
|
19
|
+
"products": "data_product",
|
|
20
|
+
"objectives": "objective",
|
|
21
|
+
"useCases": "use_case",
|
|
22
|
+
"kpis": "kpi",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _trusted_tls_context():
|
|
27
|
+
"""Keep TLS verification enabled, preferring certifi when the runtime supplies it."""
|
|
28
|
+
try:
|
|
29
|
+
import certifi
|
|
30
|
+
except ModuleNotFoundError:
|
|
31
|
+
return ssl.create_default_context()
|
|
32
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RequestBudgetExhausted(Exception):
|
|
36
|
+
"""Raised before exceeding the documented per-key request allowance."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_live_environment(path):
|
|
40
|
+
"""Read only the expected key/value pairs without executing a dotenv file."""
|
|
41
|
+
values = {}
|
|
42
|
+
try:
|
|
43
|
+
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
|
44
|
+
except OSError as error:
|
|
45
|
+
raise ValueError("Unable to read live environment file") from error
|
|
46
|
+
for line in lines:
|
|
47
|
+
stripped = line.strip()
|
|
48
|
+
if not stripped or stripped.startswith("#"):
|
|
49
|
+
continue
|
|
50
|
+
key, marker, value = line.partition("=")
|
|
51
|
+
if not marker or key not in {"API_BASE_URL", "API_KEY"}:
|
|
52
|
+
raise ValueError("Live environment file contains an unsupported setting")
|
|
53
|
+
if key in values:
|
|
54
|
+
raise ValueError("Live environment file contains a duplicate setting")
|
|
55
|
+
values[key] = value.strip()
|
|
56
|
+
if not values.get("API_BASE_URL") or not values.get("API_KEY"):
|
|
57
|
+
raise ValueError("Live environment requires API_BASE_URL and API_KEY")
|
|
58
|
+
parsed = urlsplit(values["API_BASE_URL"])
|
|
59
|
+
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password:
|
|
60
|
+
raise ValueError("API_BASE_URL must be an HTTPS URL without user credentials")
|
|
61
|
+
if parsed.query or parsed.fragment:
|
|
62
|
+
raise ValueError("API_BASE_URL must not contain query or fragment components")
|
|
63
|
+
values["API_BASE_URL"] = values["API_BASE_URL"].rstrip("/")
|
|
64
|
+
return values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LiveClient:
|
|
68
|
+
"""Retrieves read-only portfolio metadata without persisting API responses."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, environment_file, scope, *, timeout=30, request_budget=40):
|
|
71
|
+
self._settings = load_live_environment(environment_file)
|
|
72
|
+
self._scope = scope
|
|
73
|
+
self._timeout = timeout
|
|
74
|
+
self._request_budget = request_budget
|
|
75
|
+
self._attempts = 0
|
|
76
|
+
self._tls_context = _trusted_tls_context()
|
|
77
|
+
|
|
78
|
+
def request(self, method):
|
|
79
|
+
if method != "GET":
|
|
80
|
+
raise ValueError("Only GET is permitted in Kentaro v0.1")
|
|
81
|
+
return self.read_portfolio()
|
|
82
|
+
|
|
83
|
+
def _get_json(self, path, parameters=None):
|
|
84
|
+
if self._attempts >= self._request_budget:
|
|
85
|
+
raise RequestBudgetExhausted
|
|
86
|
+
query = urlencode(parameters or {})
|
|
87
|
+
url = self._settings["API_BASE_URL"] + path + ("?" + query if query else "")
|
|
88
|
+
request = Request(url, headers={"Authorization": "Bearer " + self._settings["API_KEY"]}, method="GET")
|
|
89
|
+
self._attempts += 1
|
|
90
|
+
try:
|
|
91
|
+
with urlopen(request, timeout=self._timeout, context=self._tls_context) as response:
|
|
92
|
+
return json.load(response)
|
|
93
|
+
except HTTPError as error:
|
|
94
|
+
raise ValueError(f"Maysano API request failed with HTTP status {error.code}") from error
|
|
95
|
+
except URLError as error:
|
|
96
|
+
raise ValueError("Maysano API connection failed") from error
|
|
97
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as error:
|
|
98
|
+
raise ValueError("Maysano API returned invalid JSON") from error
|
|
99
|
+
|
|
100
|
+
def _list_resource(self, resource):
|
|
101
|
+
rows = []
|
|
102
|
+
offset = 0
|
|
103
|
+
total = None
|
|
104
|
+
complete = False
|
|
105
|
+
while True:
|
|
106
|
+
try:
|
|
107
|
+
payload = self._get_json("/" + resource, {"limit": 100, "offset": offset})
|
|
108
|
+
except RequestBudgetExhausted:
|
|
109
|
+
return rows, False, total
|
|
110
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
|
|
111
|
+
raise ValueError(f"Maysano API returned an invalid {resource} list response")
|
|
112
|
+
pagination = payload.get("pagination")
|
|
113
|
+
if not isinstance(pagination, dict) or not isinstance(pagination.get("total"), int):
|
|
114
|
+
raise ValueError(f"Maysano API returned invalid {resource} pagination")
|
|
115
|
+
rows.extend(payload["data"])
|
|
116
|
+
total = pagination["total"]
|
|
117
|
+
if not payload["data"] or offset + 100 >= total:
|
|
118
|
+
complete = True
|
|
119
|
+
break
|
|
120
|
+
offset += 100
|
|
121
|
+
return rows, complete, total
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _normalise_record(row, resource, kind, title_key):
|
|
125
|
+
if not isinstance(row, dict) or not isinstance(row.get("id"), str) or not row["id"]:
|
|
126
|
+
raise ValueError(f"Maysano API returned a {resource} record without an ID")
|
|
127
|
+
record_id = row["id"]
|
|
128
|
+
title = row.get(title_key)
|
|
129
|
+
return {
|
|
130
|
+
"id": record_id,
|
|
131
|
+
"kind": kind,
|
|
132
|
+
"title": title if isinstance(title, str) and title else "not supplied",
|
|
133
|
+
"owner": None,
|
|
134
|
+
"state": row.get("status") if isinstance(row.get("status"), str) else "not supplied",
|
|
135
|
+
"version": str(row["version"]) if row.get("version") is not None else "not supplied",
|
|
136
|
+
"updated_at": row.get("updatedAt") if isinstance(row.get("updatedAt"), str) else "not supplied",
|
|
137
|
+
"source_ref": f"maysano:/{resource}/{quote(record_id, safe='')}",
|
|
138
|
+
"resource": resource,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def _links_for_record(self, record, known_ids, limitations):
|
|
142
|
+
try:
|
|
143
|
+
detail = self._get_json("/" + record["resource"] + "/" + quote(record["id"], safe=""))
|
|
144
|
+
except RequestBudgetExhausted:
|
|
145
|
+
limitations.append("Relationship retrieval stopped before the documented request budget was exceeded.")
|
|
146
|
+
return [], False
|
|
147
|
+
if not isinstance(detail, dict) or detail.get("id") != record["id"]:
|
|
148
|
+
raise ValueError(f"Maysano API returned an invalid detail response for {record['id']}")
|
|
149
|
+
links = detail.get("links", {})
|
|
150
|
+
truncated = detail.get("linksTruncated", {})
|
|
151
|
+
if not isinstance(links, dict) or not isinstance(truncated, dict):
|
|
152
|
+
raise ValueError(f"Maysano API returned invalid link metadata for {record['id']}")
|
|
153
|
+
relationships = []
|
|
154
|
+
for collection, target_kind in LINK_KINDS.items():
|
|
155
|
+
values = links.get(collection)
|
|
156
|
+
if values is None:
|
|
157
|
+
continue
|
|
158
|
+
if not isinstance(values, list) or any(not isinstance(value, str) for value in values):
|
|
159
|
+
raise ValueError(f"Maysano API returned invalid {collection} links for {record['id']}")
|
|
160
|
+
if truncated.get(collection) is True:
|
|
161
|
+
limitations.append(f"Links from {record['id']} to {collection} are truncated by the source API.")
|
|
162
|
+
for target in values:
|
|
163
|
+
if target not in known_ids:
|
|
164
|
+
limitations.append(f"A documented link from {record['id']} to {target} could not be represented in the retrieved record set.")
|
|
165
|
+
continue
|
|
166
|
+
relationships.append({
|
|
167
|
+
"id": f"client-link:{record['id']}:{collection}:{target}",
|
|
168
|
+
"source": record["id"],
|
|
169
|
+
"target": target,
|
|
170
|
+
"type": "related_" + target_kind,
|
|
171
|
+
"version": "not supplied",
|
|
172
|
+
"updated_at": "not supplied",
|
|
173
|
+
"source_ref": record["source_ref"] + "#links." + collection,
|
|
174
|
+
})
|
|
175
|
+
return relationships, True
|
|
176
|
+
|
|
177
|
+
def read_portfolio(self):
|
|
178
|
+
records = []
|
|
179
|
+
limitations = [
|
|
180
|
+
"Live Maysano pilot API snapshot; the key is organization-bound and permissions are independent.",
|
|
181
|
+
"The external API does not provide owner fields; ownership is unknown unless another approved source is added.",
|
|
182
|
+
"The external API does not provide relationship IDs, versions, or timestamps; displayed link references are client-derived from detail responses.",
|
|
183
|
+
"List pagination is not a consistent snapshot; records may change while pages are read.",
|
|
184
|
+
"Responses are not cached because the API specifies Cache-Control: no-store.",
|
|
185
|
+
"Decisions, risks, evidence, approval history, and delivery milestones are not available through the documented external endpoints.",
|
|
186
|
+
]
|
|
187
|
+
for resource, kind, title_key in RESOURCE_SPECS:
|
|
188
|
+
rows, complete, total = self._list_resource(resource)
|
|
189
|
+
if not complete:
|
|
190
|
+
limitations.append(f"{resource} retrieval is incomplete after {len(rows)} of {total if total is not None else 'unknown'} listed records.")
|
|
191
|
+
records.extend(self._normalise_record(row, resource, kind, title_key) for row in rows)
|
|
192
|
+
ids = {record["id"] for record in records}
|
|
193
|
+
if len(ids) != len(records):
|
|
194
|
+
raise ValueError("Maysano API returned duplicate record IDs across retrieved resources")
|
|
195
|
+
relationships = []
|
|
196
|
+
links_complete = True
|
|
197
|
+
for record in records:
|
|
198
|
+
record_links, complete = self._links_for_record(record, ids, limitations)
|
|
199
|
+
relationships.extend(record_links)
|
|
200
|
+
links_complete = links_complete and complete
|
|
201
|
+
if not complete:
|
|
202
|
+
break
|
|
203
|
+
if not links_complete:
|
|
204
|
+
limitations.append("Relationship coverage is incomplete because detail retrieval stopped at the client request budget.")
|
|
205
|
+
return {
|
|
206
|
+
"scope": self._scope,
|
|
207
|
+
"records": records,
|
|
208
|
+
"relationships": relationships,
|
|
209
|
+
"retrieved_at": datetime.now(timezone.utc).isoformat(),
|
|
210
|
+
"source_label": "Live Maysano pilot API",
|
|
211
|
+
"cache_label": "Cache disabled (API no-store)",
|
|
212
|
+
"owner_fields_available": False,
|
|
213
|
+
"limitations": list(dict.fromkeys(limitations)),
|
|
214
|
+
}
|