tropek-client 0.1.1a0__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.
Files changed (86) hide show
  1. tropek_client-0.1.1a0/.gitignore +186 -0
  2. tropek_client-0.1.1a0/PKG-INFO +9 -0
  3. tropek_client-0.1.1a0/README.md +350 -0
  4. tropek_client-0.1.1a0/docs/architecture.md +137 -0
  5. tropek_client-0.1.1a0/docs/technical-debt.md +87 -0
  6. tropek_client-0.1.1a0/pyproject.toml +28 -0
  7. tropek_client-0.1.1a0/tests/__init__.py +0 -0
  8. tropek_client-0.1.1a0/tests/conftest.py +44 -0
  9. tropek_client-0.1.1a0/tests/fixtures/api_responses/annotation_list.json +80 -0
  10. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_get.json +20 -0
  11. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_group_get.json +33 -0
  12. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_group_tree.json +364 -0
  13. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_list.json +249 -0
  14. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_tag_keys.json +30 -0
  15. tropek_client-0.1.1a0/tests/fixtures/api_responses/asset_type_list.json +41 -0
  16. tropek_client-0.1.1a0/tests/fixtures/api_responses/configuration_get.json +6 -0
  17. tropek_client-0.1.1a0/tests/fixtures/api_responses/configuration_list.json +44 -0
  18. tropek_client-0.1.1a0/tests/fixtures/api_responses/datasource_get.json +13 -0
  19. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_detail_baseline.json +113 -0
  20. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_detail_fail.json +185 -0
  21. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_detail_invalidated.json +112 -0
  22. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_detail_override.json +185 -0
  23. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_detail_pass.json +185 -0
  24. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_list.json +3455 -0
  25. tropek_client-0.1.1a0/tests/fixtures/api_responses/evaluation_names.json +42 -0
  26. tropek_client-0.1.1a0/tests/fixtures/api_responses/heatmap_grouped.json +16641 -0
  27. tropek_client-0.1.1a0/tests/fixtures/api_responses/note_category_list.json +42 -0
  28. tropek_client-0.1.1a0/tests/fixtures/api_responses/sli_get.json +22 -0
  29. tropek_client-0.1.1a0/tests/fixtures/api_responses/sli_versions.json +24 -0
  30. tropek_client-0.1.1a0/tests/fixtures/api_responses/slo_assignment_list_for_asset.json +26 -0
  31. tropek_client-0.1.1a0/tests/fixtures/api_responses/slo_get.json +79 -0
  32. tropek_client-0.1.1a0/tests/fixtures/api_responses/slo_group_get.json +24 -0
  33. tropek_client-0.1.1a0/tests/fixtures/api_responses/slo_group_list.json +187 -0
  34. tropek_client-0.1.1a0/tests/fixtures/api_responses/slo_versions.json +81 -0
  35. tropek_client-0.1.1a0/tests/fixtures/api_responses/timeline_get.json +261 -0
  36. tropek_client-0.1.1a0/tests/fixtures/manifests/directory/asset.yaml +6 -0
  37. tropek_client-0.1.1a0/tests/fixtures/manifests/directory/asset_type.yaml +6 -0
  38. tropek_client-0.1.1a0/tests/fixtures/manifests/meta_snapshot.yaml +11 -0
  39. tropek_client-0.1.1a0/tests/fixtures/manifests/missing_api_version.yaml +5 -0
  40. tropek_client-0.1.1a0/tests/fixtures/manifests/multi_document.yaml +13 -0
  41. tropek_client-0.1.1a0/tests/fixtures/manifests/single_document.yaml +6 -0
  42. tropek_client-0.1.1a0/tests/fixtures/manifests/unknown_kind.yaml +9 -0
  43. tropek_client-0.1.1a0/tests/fixtures/manifests/unsorted_dependencies.yaml +13 -0
  44. tropek_client-0.1.1a0/tests/test_api_contract_drift.py +165 -0
  45. tropek_client-0.1.1a0/tests/test_asset_meta_snapshots.py +108 -0
  46. tropek_client-0.1.1a0/tests/test_assets.py +210 -0
  47. tropek_client-0.1.1a0/tests/test_cli.py +64 -0
  48. tropek_client-0.1.1a0/tests/test_configuration.py +76 -0
  49. tropek_client-0.1.1a0/tests/test_datasources.py +55 -0
  50. tropek_client-0.1.1a0/tests/test_evaluations.py +311 -0
  51. tropek_client-0.1.1a0/tests/test_exceptions.py +91 -0
  52. tropek_client-0.1.1a0/tests/test_health.py +14 -0
  53. tropek_client-0.1.1a0/tests/test_heatmap.py +37 -0
  54. tropek_client-0.1.1a0/tests/test_http.py +97 -0
  55. tropek_client-0.1.1a0/tests/test_models.py +200 -0
  56. tropek_client-0.1.1a0/tests/test_slis.py +84 -0
  57. tropek_client-0.1.1a0/tests/test_slos.py +148 -0
  58. tropek_client-0.1.1a0/tests/test_timeline.py +38 -0
  59. tropek_client-0.1.1a0/tests/test_yaml_manifest.py +112 -0
  60. tropek_client-0.1.1a0/tropek_client/__init__.py +23 -0
  61. tropek_client-0.1.1a0/tropek_client/_http.py +106 -0
  62. tropek_client-0.1.1a0/tropek_client/cli.py +191 -0
  63. tropek_client-0.1.1a0/tropek_client/client.py +898 -0
  64. tropek_client-0.1.1a0/tropek_client/exceptions.py +131 -0
  65. tropek_client-0.1.1a0/tropek_client/manifest.py +674 -0
  66. tropek_client-0.1.1a0/tropek_client/manifest_meta.py +44 -0
  67. tropek_client-0.1.1a0/tropek_client/models/__init__.py +289 -0
  68. tropek_client-0.1.1a0/tropek_client/models/annotations.py +85 -0
  69. tropek_client-0.1.1a0/tropek_client/models/asset_groups.py +100 -0
  70. tropek_client-0.1.1a0/tropek_client/models/asset_types.py +28 -0
  71. tropek_client-0.1.1a0/tropek_client/models/assets.py +56 -0
  72. tropek_client-0.1.1a0/tropek_client/models/change_points.py +42 -0
  73. tropek_client-0.1.1a0/tropek_client/models/common.py +70 -0
  74. tropek_client-0.1.1a0/tropek_client/models/configuration.py +18 -0
  75. tropek_client-0.1.1a0/tropek_client/models/datasources.py +43 -0
  76. tropek_client-0.1.1a0/tropek_client/models/evaluations.py +285 -0
  77. tropek_client-0.1.1a0/tropek_client/models/heatmap.py +101 -0
  78. tropek_client-0.1.1a0/tropek_client/models/meta.py +69 -0
  79. tropek_client-0.1.1a0/tropek_client/models/pagination.py +10 -0
  80. tropek_client-0.1.1a0/tropek_client/models/slis.py +55 -0
  81. tropek_client-0.1.1a0/tropek_client/models/slo_assignments.py +37 -0
  82. tropek_client-0.1.1a0/tropek_client/models/slo_groups.py +101 -0
  83. tropek_client-0.1.1a0/tropek_client/models/slos.py +220 -0
  84. tropek_client-0.1.1a0/tropek_client/models/timeline.py +44 -0
  85. tropek_client-0.1.1a0/tropek_client/models/trend.py +41 -0
  86. tropek_client-0.1.1a0/tropek_client/py.typed +0 -0
@@ -0,0 +1,186 @@
1
+ # Claude Code local settings (personal paths, hooks)
2
+ .claude/settings.local.json
3
+
4
+ # Superpowers brainstorming sessions
5
+ .superpowers/
6
+
7
+ # Personal docs (research, design inspiration, old specs)
8
+ docs/personal/
9
+
10
+ # Git worktrees
11
+ .worktrees/
12
+
13
+ # Dev output (logs, artifacts)
14
+ out/
15
+
16
+ # Byte-compiled / optimized / DLL files
17
+ __pycache__/
18
+ *.py[codz]
19
+ *$py.class
20
+
21
+ # C extensions
22
+ *.so
23
+
24
+ # Distribution / packaging
25
+ .Python
26
+ build/
27
+ develop-eggs/
28
+ dist/
29
+ downloads/
30
+ eggs/
31
+ .eggs/
32
+ lib/
33
+ lib64/
34
+ !ui/src/lib/
35
+ parts/
36
+ sdist/
37
+ var/
38
+ wheels/
39
+ share/python-wheels/
40
+ *.egg-info/
41
+ .installed.cfg
42
+ *.egg
43
+ MANIFEST
44
+
45
+ # PyInstaller
46
+ *.manifest
47
+ *.spec
48
+
49
+ # Installer logs
50
+ pip-log.txt
51
+ pip-delete-this-directory.txt
52
+
53
+ # Unit test / coverage reports
54
+ htmlcov/
55
+ .tox/
56
+ .nox/
57
+ .coverage
58
+ .coverage.*
59
+ .cache
60
+ nosetests.xml
61
+ coverage.xml
62
+ *.cover
63
+ *.py.cover
64
+ .hypothesis/
65
+ .pytest_cache/
66
+ cover/
67
+
68
+ # Translations
69
+ *.mo
70
+ *.pot
71
+
72
+ # Django stuff:
73
+ *.log
74
+ local_settings.py
75
+ db.sqlite3
76
+ db.sqlite3-journal
77
+
78
+ # Flask stuff:
79
+ instance/
80
+ .webassets-cache
81
+
82
+ # Scrapy stuff:
83
+ .scrapy
84
+
85
+ # Sphinx documentation
86
+ docs/_build/
87
+
88
+ # PyBuilder
89
+ .pybuilder/
90
+ target/
91
+
92
+ # Jupyter Notebook
93
+ .ipynb_checkpoints
94
+
95
+ # IPython
96
+ profile_default/
97
+ ipython_config.py
98
+
99
+ # pyenv
100
+ # .python-version
101
+
102
+ # UV
103
+ #uv.lock
104
+
105
+ # poetry
106
+ #poetry.lock
107
+ #poetry.toml
108
+
109
+ # pdm
110
+ #pdm.lock
111
+ #pdm.toml
112
+ .pdm-python
113
+ .pdm-build/
114
+
115
+ # pixi
116
+ #pixi.lock
117
+ .pixi
118
+
119
+ # PEP 582
120
+ __pypackages__/
121
+
122
+ # Celery stuff
123
+ celerybeat-schedule
124
+ celerybeat.pid
125
+
126
+ # SageMath parsed files
127
+ *.sage.py
128
+
129
+ # Environments
130
+ .env
131
+ .envrc
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # Ruff stuff:
164
+ .ruff_cache/
165
+
166
+ # PyPI configuration file
167
+ .pypirc
168
+
169
+ # IDEs
170
+ .idea/
171
+ .vscode/
172
+ *.iml
173
+
174
+ # Cursor
175
+ .cursorignore
176
+ .cursorindexingignore
177
+
178
+ # Generated mock adapter data
179
+ adapters/mock/data/*/metrics.csv
180
+
181
+ # Node / React UI
182
+ node_modules/
183
+ ui/dist/
184
+ ui/.vite/
185
+
186
+ .tmp
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: tropek-client
3
+ Version: 0.1.1a0
4
+ Summary: Typed Python client for the TROPEK quality gate API
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: click>=8.1
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.7
9
+ Requires-Dist: pyyaml>=6.0
@@ -0,0 +1,350 @@
1
+ # TROPEK Python Client
2
+
3
+ Typed Python client and CLI for the TROPEK quality gate API. Provides a programmatic
4
+ interface to all TROPEK resources and a declarative YAML manifest system for
5
+ infrastructure-as-code workflows.
6
+
7
+ ## Installation
8
+
9
+ Requires Python 3.13+. Install from the local package:
10
+
11
+ ```bash
12
+ cd clients/python
13
+ uv pip install -e .
14
+ ```
15
+
16
+ Dependencies: `httpx`, `pydantic`, `pyyaml`, `click`.
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ from tropek_client import TropekClient
22
+
23
+ with TropekClient('http://localhost:8080', api_key='my-key') as client:
24
+ # Check connectivity
25
+ client.health()
26
+
27
+ # Create an asset and trigger an evaluation
28
+ client.assets.create('web-api', type_name='service', tags={'env': 'prod'})
29
+ result = client.evaluations.evaluate(
30
+ asset_name='web-api',
31
+ eval_name='release-42',
32
+ period_start='2024-01-01T00:00:00Z',
33
+ period_end='2024-01-01T01:00:00Z',
34
+ )
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ | Parameter | Default | Description |
40
+ |---|---|---|
41
+ | `base_url` | (required) | TROPEK API base URL |
42
+ | `api_key` | `None` | Bearer token for authentication |
43
+
44
+ The client uses a 30-second timeout. The underlying `httpx.Client` is closed
45
+ automatically when using the context manager.
46
+
47
+ ## Client API Reference
48
+
49
+ The client exposes resource-specific namespaces as attributes on `TropekClient`.
50
+
51
+ ### `client.health() -> dict`
52
+
53
+ Returns API health status.
54
+
55
+ ### Asset Types — `client.asset_types`
56
+
57
+ | Method | Returns | Description |
58
+ |---|---|---|
59
+ | `list()` | `list[AssetType]` | List all asset types |
60
+ | `create(name, *, is_default=False)` | `AssetType` | Create an asset type |
61
+ | `set_default(name)` | `AssetType` | Set a type as default |
62
+ | `rename(name, new_name)` | `AssetType` | Rename a type |
63
+ | `delete(name)` | `None` | Delete a type |
64
+
65
+ ### Assets — `client.assets`
66
+
67
+ | Method | Returns | Description |
68
+ |---|---|---|
69
+ | `list(*, type_name=, tag_key=, tag_val=)` | `PagedResponse[Asset]` | List with optional filters |
70
+ | `create(name, type_name='vm', *, display_name=, tags=, variables=)` | `Asset` | Create an asset |
71
+ | `get(name)` | `Asset` | Get by name |
72
+ | `update(name, **kwargs)` | `Asset` | Partial update |
73
+ | `delete(name)` | `None` | Delete an asset |
74
+ | `tag_keys()` | `list[dict]` | Distinct tag keys with counts |
75
+ | `tag_values(key)` | `list[dict]` | Values for a tag key |
76
+
77
+ ### Asset Groups — `client.asset_groups`
78
+
79
+ | Method | Returns | Description |
80
+ |---|---|---|
81
+ | `list()` | `PagedResponse[AssetGroup]` | List all groups |
82
+ | `tree()` | `AssetGroupTree` | Get hierarchical tree |
83
+ | `create(name, *, display_name=, members=, subgroups=)` | `AssetGroup` | Create a group |
84
+ | `get(name)` | `AssetGroup` | Get by name |
85
+ | `add_member(group_name, asset_id, weight=1.0)` | `AssetGroup` | Add asset to group |
86
+ | `remove_member(group_name, asset_id)` | `None` | Remove asset from group |
87
+ | `add_subgroup(group_name, child_group_id, weight=1.0)` | `AssetGroup` | Nest a group |
88
+ | `remove_subgroup(group_name, child_group_id)` | `None` | Remove nested group |
89
+
90
+ ### Data Sources — `client.datasources`
91
+
92
+ | Method | Returns | Description |
93
+ |---|---|---|
94
+ | `list(*, adapter_type=)` | `PagedResponse[DataSource]` | List with optional filter |
95
+ | `create(name, adapter_type, adapter_url, **kwargs)` | `DataSource` | Register a datasource |
96
+ | `get(name)` | `DataSource` | Get by name |
97
+ | `update(name, **kwargs)` | `DataSource` | Partial update |
98
+ | `delete(name)` | `None` | Delete a datasource |
99
+ | `tag_keys()` | `list[dict]` | Distinct tag keys |
100
+ | `tag_values(key)` | `list[dict]` | Values for a tag key |
101
+
102
+ ### SLI Definitions — `client.slis`
103
+
104
+ | Method | Returns | Description |
105
+ |---|---|---|
106
+ | `list()` | `PagedResponse[SLIDefinition]` | List all SLIs |
107
+ | `create(body)` | `SLIDefinition` | Create (or new version) |
108
+ | `get(name)` | `SLIDefinition` | Get latest version |
109
+ | `versions(name)` | `list[SLIDefinition]` | All versions |
110
+ | `delete(name)` | `None` | Delete an SLI |
111
+ | `new_version(name, **overrides)` | `SLIDefinition` | Create new version with overrides |
112
+ | `tag_keys()` | `list[dict]` | Distinct tag keys |
113
+ | `tag_values(key)` | `list[dict]` | Values for a tag key |
114
+
115
+ ### SLO Definitions — `client.slos`
116
+
117
+ | Method | Returns | Description |
118
+ |---|---|---|
119
+ | `list()` | `PagedResponse[SLODefinition]` | List all SLOs |
120
+ | `create(body)` | `SLODefinition` | Create (or new version) |
121
+ | `get(name)` | `SLODefinition` | Get latest version |
122
+ | `versions(name)` | `list[SLODefinition]` | All versions |
123
+ | `delete(name)` | `None` | Delete an SLO |
124
+ | `new_version(name, **overrides)` | `SLODefinition` | Create new version with overrides |
125
+ | `validate(body)` | `SLOValidationResult` | Validate without saving |
126
+ | `test(body)` | `SLOTestResult` | Dry-run evaluation |
127
+ | `tag_keys()` | `list[dict]` | Distinct tag keys |
128
+ | `tag_values(key)` | `list[dict]` | Values for a tag key |
129
+
130
+ ### SLO Assignments — `client.slo_assignments`
131
+
132
+ Pins an asset or group to a specific SLO definition version + datasource.
133
+
134
+ | Method | Returns | Description |
135
+ |---|---|---|
136
+ | `create_for_asset(asset_name, slo_definition_id, data_source_name, *, comparison_rules=)` | `SLOAssignment` | Upsert for asset |
137
+ | `create_for_group(group_name, slo_definition_id, data_source_name, *, comparison_rules=)` | `SLOAssignment` | Upsert for group |
138
+ | `list_for_asset(asset_name)` | `list[SLOAssignment]` | List for asset |
139
+ | `list_for_group(group_name)` | `list[SLOAssignment]` | List for group |
140
+ | `delete_for_asset(asset_name, slo_definition_id)` | `None` | Delete for asset |
141
+ | `delete_for_group(group_name, slo_definition_id)` | `None` | Delete for group |
142
+
143
+ ### SLO Groups — `client.slo_groups`
144
+
145
+ Template-based SLO generation with variable expansion.
146
+
147
+ | Method | Returns | Description |
148
+ |---|---|---|
149
+ | `create(name, template_slo_name, template_slo_version, gen_variables, *, display_name=, tags=, author=)` | `SLOGroup` | Create a group |
150
+ | `get(name)` | `SLOGroup` | Get by name |
151
+ | `list(*, tag_key=, tag_val=)` | `list[SLOGroup]` | List with optional filters |
152
+ | `update(name, *, template_slo_name=, template_slo_version=, gen_variables=, ...)` | `SLOGroup` | Update (triggers regeneration) |
153
+ | `delete(name)` | `None` | Deactivate a group |
154
+ | `extract(group_name, slo_name, new_name)` | `None` | Extract generated SLO to standalone |
155
+
156
+ ### SLO Group Assignments — `client.slo_group_assignments`
157
+
158
+ Asset/group to SLO group with always-latest semantics.
159
+
160
+ | Method | Returns | Description |
161
+ |---|---|---|
162
+ | `create_for_asset(asset_name, slo_group_name, data_source_name)` | `SLOGroupAssignment` | Upsert for asset |
163
+ | `create_for_group(group_name, slo_group_name, data_source_name)` | `SLOGroupAssignment` | Upsert for group |
164
+ | `list_for_asset(asset_name)` | `list[SLOGroupAssignment]` | List for asset |
165
+ | `list_for_group(group_name)` | `list[SLOGroupAssignment]` | List for group |
166
+ | `delete_for_asset(asset_name, slo_group_name)` | `None` | Delete for asset |
167
+ | `delete_for_group(group_name, slo_group_name)` | `None` | Delete for group |
168
+
169
+ ### Evaluations — `client.evaluations`
170
+
171
+ | Method | Returns | Description |
172
+ |---|---|---|
173
+ | `list(*, asset_name=, slo_name=, result=, date=, group_name=, from_=, to=, limit=50, offset=0)` | `PagedResponse[EvaluationSummary]` | List with filters |
174
+ | `get(eval_id)` | `EvaluationDetail` | Full evaluation detail |
175
+ | `evaluate(asset_name, eval_name, period_start, period_end, *, variables=)` | `dict` | Trigger evaluation |
176
+ | `evaluate_batch(mode, eval_name, *, asset_name=, periods=, asset_names=, period_start=, period_end=, variables=)` | `dict` | Batch trigger (`by_date` or `by_asset`) |
177
+ | `invalidate(eval_id, note)` | `EvaluationSummary` | Invalidate an evaluation |
178
+ | `restore(eval_id)` | `EvaluationSummary` | Restore invalidated eval |
179
+ | `pin_baseline(eval_id, reason, author)` | `EvaluationDetail` | Pin as baseline |
180
+ | `unpin_baseline(eval_id)` | `EvaluationDetail` | Remove baseline pin |
181
+ | `override_status(eval_id, new_result, reason, author)` | `EvaluationDetail` | Override result |
182
+ | `restore_override(eval_id)` | `EvaluationDetail` | Restore original result |
183
+ | `re_evaluate_from_date(scope, from_date, *, selector=, slo_version=, dry_run=, pin_strategy=)` | `dict` | Re-evaluate from date |
184
+ | `re_evaluate_from_baseline(scope, *, selector=, slo_version=, dry_run=, pin_strategy=)` | `dict` | Re-evaluate from pinned baseline |
185
+ | `re_evaluate_from_evaluation(evaluation_id, scope, *, selector=, slo_version=, dry_run=, pin_strategy=)` | `dict` | Re-evaluate from reference eval |
186
+
187
+ Re-evaluation `scope` is `{'kind': 'asset', 'asset_name': '...'}` or
188
+ `{'kind': 'group', 'group_name': '...'}`. `pin_strategy` can be `'skip_to_pin'` or
189
+ `'ignore_pin'` to resolve baseline-pin conflicts.
190
+
191
+ ### Annotations — `client.annotations`
192
+
193
+ | Method | Returns | Description |
194
+ |---|---|---|
195
+ | `list(eval_id)` | `list[Annotation]` | List for an evaluation |
196
+ | `create(eval_id, content, **kwargs)` | `Annotation` | Create SLO-level annotation |
197
+ | `create_for_run(run_id, content, **kwargs)` | `Annotation` | Create run-level annotation |
198
+ | `update(eval_id, ann_id, **kwargs)` | `Annotation` | Update an annotation |
199
+ | `hide(eval_id, ann_id, reason, author=None)` | `Annotation` | Soft-delete |
200
+
201
+ ### Trend — `client.trend`
202
+
203
+ | Method | Returns | Description |
204
+ |---|---|---|
205
+ | `by_eval(eval_id, metric, from_, *, to=)` | `list[TrendPoint]` | Trend by evaluation ID |
206
+ | `by_asset(asset_name, slo_name, metric, from_, *, to=)` | `list[TrendPoint]` | Trend by asset + SLO |
207
+
208
+ ## Manifest System
209
+
210
+ The manifest system enables declarative, infrastructure-as-code management of TROPEK
211
+ resources using YAML files.
212
+
213
+ ### Manifest Format
214
+
215
+ Each manifest document has four fields:
216
+
217
+ ```yaml
218
+ api_version: tropek/v1
219
+ kind: Asset
220
+ metadata:
221
+ name: web-api
222
+ display_name: Web API
223
+ tags:
224
+ env: prod
225
+ spec:
226
+ type_name: service
227
+ ```
228
+
229
+ Supported kinds (processed in dependency order):
230
+ `AssetType`, `DataSource`, `Asset`, `SLI`, `SLO`, `AssetGroup`, `SLOGroup`,
231
+ `SLOAssignment`, `SLOGroupAssignment`.
232
+
233
+ A single file can contain multiple documents separated by `---`. You can also point
234
+ at a directory — all `*.yaml` and `*.yml` files are loaded.
235
+
236
+ ### SLO Assignment Manifest
237
+
238
+ ```yaml
239
+ api_version: tropek/v1
240
+ kind: SLOAssignment
241
+ metadata:
242
+ name: web-api-latency-assignment
243
+ spec:
244
+ target_type: asset # or asset_group
245
+ target_name: web-api
246
+ slo_name: latency-slo
247
+ data_source_name: prometheus
248
+ ```
249
+
250
+ ### Reconciliation
251
+
252
+ The manifest system uses desired-state reconciliation:
253
+ 1. Load and parse all YAML documents
254
+ 2. Topologically sort by kind dependency
255
+ 3. For each document, look up the existing resource via the API
256
+ 4. Compare fields to detect drift (CREATE / UPDATE / SKIP)
257
+ 5. Apply changes, blocking dependent kinds on failure
258
+
259
+ ### Programmatic Usage
260
+
261
+ ```python
262
+ from tropek_client import TropekClient
263
+ from tropek_client.manifest import load_manifests, apply, dry_run
264
+
265
+ docs = load_manifests('manifests/')
266
+
267
+ with TropekClient('http://localhost:8080') as client:
268
+ # Preview changes
269
+ plan = dry_run(client, docs)
270
+ for action in plan.actions:
271
+ print(f'{action.operation} {action.kind}/{action.name}')
272
+
273
+ # Apply
274
+ result = apply(client, docs)
275
+ print(f'{result.created} created, {result.updated} updated')
276
+ ```
277
+
278
+ ## CLI Usage
279
+
280
+ The CLI is installed as `tropek` (entry point defined in pyproject.toml).
281
+
282
+ ### Validate manifests
283
+
284
+ ```bash
285
+ tropek validate -f manifests/
286
+ tropek validate -f my-slo.yaml
287
+ ```
288
+
289
+ Checks syntax and cross-references without contacting the API.
290
+
291
+ ### Apply manifests
292
+
293
+ ```bash
294
+ # Dry run — show what would change
295
+ tropek apply --dry-run -f manifests/ --base-url http://localhost:8080
296
+
297
+ # Apply changes
298
+ tropek apply -f manifests/ --base-url http://localhost:8080 --api-key my-key
299
+ ```
300
+
301
+ ### Export current state
302
+
303
+ ```bash
304
+ # To stdout
305
+ tropek export --base-url http://localhost:8080
306
+
307
+ # To file
308
+ tropek export -f backup.yaml --base-url http://localhost:8080 --api-key my-key
309
+ ```
310
+
311
+ Exports all resources (asset types, datasources, assets, SLIs, SLOs, groups,
312
+ assignments) as a multi-document YAML manifest.
313
+
314
+ ## Known Limitations
315
+
316
+ - **Asset names are globally unique.** There is no per-group scoping — two groups
317
+ cannot each contain an asset named `load_test`. Assets are identified by name
318
+ across the entire TROPEK instance.
319
+
320
+ - **Display groups (`client.display_groups`) are half-baked.** The API exposes
321
+ `/slo-display-groups` as a standalone CRUD resource for organizing generated SLOs
322
+ into named visual buckets, but the feature is incomplete: no FK linking display
323
+ groups to the SLO group they belong to, no validation that member SLO names
324
+ exist, and no UI consumer. The client wraps the routes but they are untested
325
+ against real workflows. This will likely be redesigned when SLO group navigation
326
+ is revisited.
327
+
328
+ ## Error Handling
329
+
330
+ All API errors raise typed exceptions:
331
+
332
+ | Exception | HTTP Status | When |
333
+ |---|---|---|
334
+ | `TropekNotFoundError` | 404 | Resource not found |
335
+ | `TropekConflictError` | 409 | Resource conflict (duplicate name) |
336
+ | `TropekValidationError` | 422 | Request validation failed |
337
+ | `TropekAPIError` | any non-2xx | Base class for all errors |
338
+
339
+ Each exception has `status_code` and `detail` attributes:
340
+
341
+ ```python
342
+ from tropek_client import TropekClient, TropekNotFoundError
343
+
344
+ with TropekClient('http://localhost:8080') as client:
345
+ try:
346
+ client.assets.get('nonexistent')
347
+ except TropekNotFoundError as e:
348
+ print(e.status_code) # 404
349
+ print(e.detail) # "asset 'nonexistent' not found"
350
+ ```
@@ -0,0 +1,137 @@
1
+ # Python Client — Architecture
2
+
3
+ Contributor guide for `clients/python/tropek_client/`.
4
+
5
+ ## Module Structure
6
+
7
+ ```
8
+ tropek_client/
9
+ ├── __init__.py # Public API: TropekClient + exception classes
10
+ ├── client.py # HTTP client with resource-namespaced methods
11
+ ├── models.py # Pydantic response models
12
+ ├── manifest.py # YAML manifest loader and reconciler
13
+ ├── cli.py # Click CLI interface
14
+ └── exceptions.py # Typed exception hierarchy
15
+ ```
16
+
17
+ ## Client Design (`client.py`)
18
+
19
+ The client uses **namespace classes** to group API methods by resource type. Each
20
+ namespace (e.g., `_Assets`, `_SLODefinitions`) is a private class that receives a
21
+ shared `httpx.Client` instance. `TropekClient.__init__` instantiates all namespaces
22
+ and exposes them as public attributes.
23
+
24
+ HTTP pattern for every method:
25
+ 1. Build params/body dict, omitting `None` values
26
+ 2. Call `self._http.{verb}(path, ...)`
27
+ 3. Pass response through `_raise_for_status()` (maps status codes to typed exceptions)
28
+ 4. Deserialize via `Model.model_validate(resp.json())`
29
+
30
+ Paginated endpoints return `PagedResponse[T]` (items + total). Non-paginated
31
+ endpoints return the model directly or a raw `dict` for polymorphic responses
32
+ (evaluations trigger, batch, re-evaluate).
33
+
34
+ `TropekClient` implements the context manager protocol for automatic cleanup.
35
+
36
+ ## Error Handling (`exceptions.py`)
37
+
38
+ Four-class hierarchy:
39
+ - `TropekAPIError(status_code, detail)` — base, catches any non-2xx
40
+ - `TropekNotFoundError(detail)` — 404
41
+ - `TropekConflictError(detail)` — 409
42
+ - `TropekValidationError(detail)` — 422
43
+
44
+ `_raise_for_status()` uses structural pattern matching on `resp.status_code`.
45
+ The response body's `detail` field is extracted when available; raw text is the fallback.
46
+
47
+ ## Models (`models.py`)
48
+
49
+ Pydantic v2 models mirroring API response schemas. Key patterns:
50
+ - `PagedResponse[T]` — generic paginated wrapper using Python 3.12+ type parameter syntax
51
+ - `ConfigDict(from_attributes=True)` on ORM-mapped models for compatibility
52
+ - `EvaluationDetail` extends `EvaluationSummary` (adds annotations, indicator results)
53
+ - Request models (`SLOTestRequest`, `BaselineConfig`) for structured input
54
+ - String IDs on assignment models (`SLOAssignment`, `SLOGroupAssignment`) — the API
55
+ returns UUIDs as strings in some endpoints
56
+
57
+ ## Manifest System (`manifest.py`)
58
+
59
+ ### Processing Pipeline
60
+
61
+ ```
62
+ load_manifests(path)
63
+ ├── _load_file() # YAML multi-doc parsing
64
+ ├── _parse_document() # Validate required fields, known kinds
65
+ └── _topological_sort() # Sort by _KIND_ORDER dependency
66
+
67
+ validate_manifests(path)
68
+ ├── load_manifests()
69
+ └── _validate_doc_refs() # Cross-reference warnings
70
+
71
+ dry_run(client, manifests)
72
+ └── for each doc:
73
+ ├── _lookup() # Fetch existing resource from API
74
+ ├── _has_diff() # Field-level comparison
75
+ └── → PlanAction(CREATE | UPDATE | SKIP)
76
+
77
+ apply(client, manifests)
78
+ ├── dry_run() # Build plan first
79
+ └── for each action:
80
+ ├── _create() # Kind-specific create dispatch
81
+ ├── _update() # Kind-specific update dispatch
82
+ └── _dependents_of() # Block downstream kinds on failure
83
+ ```
84
+
85
+ ### Dependency Graph
86
+
87
+ `_KIND_ORDER` defines processing order: `AssetType → DataSource → Asset → SLI → SLO
88
+ → AssetGroup → SLOGroup → SLOAssignment → SLOGroupAssignment`.
89
+
90
+ `_KIND_DEPS` maps each kind to its dependents for error propagation — if an `Asset`
91
+ creation fails, `AssetGroup`, `SLOAssignment`, and `SLOGroupAssignment` are blocked.
92
+
93
+ ### Update Semantics
94
+
95
+ - **Versioned resources** (SLI, SLO): updates create a new version via the same
96
+ `create` endpoint rather than patching in place.
97
+ - **Immutable resources** (SLOAssignment, SLOGroupAssignment): `_has_diff` always
98
+ returns `False` — requires manual delete + recreate.
99
+ - **AssetGroup**: member/subgroup sync not yet implemented; updates are skipped.
100
+
101
+ ## CLI (`cli.py`)
102
+
103
+ Click command tree with three commands:
104
+
105
+ - `tropek validate -f <path>` — offline YAML validation
106
+ - `tropek apply -f <path> [--dry-run] [--base-url] [--api-key]` — reconcile manifests
107
+ - `tropek export [-f <path>] [--base-url] [--api-key]` — dump current state as YAML
108
+
109
+ `export` uses `_collect_documents()` which iterates all resource types and builds
110
+ manifest dicts. It also walks per-asset and per-group SLO assignments.
111
+
112
+ ## Test Structure
113
+
114
+ ```
115
+ tests/
116
+ ├── test_client.py # HTTP mocking via pytest-httpx; covers CRUD + error mapping
117
+ ├── test_manifest.py # Manifest loading, sorting, dry_run, apply with MagicMock client
118
+ ├── test_cli.py # Click CliRunner tests for validate + apply --dry-run
119
+ └── test_models.py # Pydantic model_validate round-trips
120
+ ```
121
+
122
+ Dev dependencies: `pytest`, `pytest-httpx` (for `HTTPXMock` fixture).
123
+
124
+ Tests use `MagicMock` for the client in manifest/CLI tests and `HTTPXMock` for
125
+ HTTP-level client tests. No real API calls in any test.
126
+
127
+ ## Known Limitations
128
+
129
+ See [`technical-debt.md`](technical-debt.md) for detailed analysis and recommended
130
+ fixes. Summary:
131
+
132
+ - No contract validation against the backend OpenAPI spec — client can silently drift
133
+ - `AssetGroup` update via manifests is a no-op (member sync not implemented)
134
+ - Assignment updates require manual delete + recreate
135
+ - No async client variant
136
+ - `export` does not include SLO group assignments
137
+ - Some endpoints return raw `dict` instead of typed models (evaluation triggers)