clear-skies-cortex 2.0.2__tar.gz → 2.0.4__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 (60) hide show
  1. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.copier-answers.yml +1 -1
  2. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.github/workflows/create-version.yaml +1 -1
  3. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.github/workflows/docs.yaml +2 -0
  4. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.github/workflows/run-tests.yml +2 -1
  5. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.github/workflows/tests-matrix.yaml +6 -19
  6. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.pre-commit-config.yaml +1 -7
  7. clear_skies_cortex-2.0.4/.vscode/settings.json +21 -0
  8. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/CHANGELOG.md +19 -0
  9. clear_skies_cortex-2.0.4/LATEST_CHANGELOG.md +9 -0
  10. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/PKG-INFO +2 -2
  11. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/pyproject.toml +5 -16
  12. clear_skies_cortex-2.0.4/src/clearskies_cortex/backends/cortex_backend.py +236 -0
  13. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/backends/cortex_team_relationship_backend.py +20 -13
  14. clear_skies_cortex-2.0.4/src/clearskies_cortex/columns/string_list.py +69 -0
  15. clear_skies_cortex-2.0.4/src/clearskies_cortex/dataclasses.py +243 -0
  16. clear_skies_cortex-2.0.4/src/clearskies_cortex/defaults/default_cortex_auth.py +55 -0
  17. clear_skies_cortex-2.0.4/src/clearskies_cortex/defaults/default_cortex_url.py +45 -0
  18. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_catalog_entity.py +247 -0
  19. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/models/cortex_catalog_entity_domain.py +29 -1
  20. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_catalog_entity_group.py +47 -0
  21. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_catalog_entity_scorecard.py +82 -0
  22. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/models/cortex_catalog_entity_service.py +35 -1
  23. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/models/cortex_catalog_entity_team.py +30 -1
  24. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_catalog_entity_types.py +62 -0
  25. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_entity_relationships.py +64 -0
  26. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_model.py +32 -0
  27. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_scorecard.py +68 -0
  28. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/models/cortex_team.py +98 -1
  29. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_team_category_tree.py +67 -0
  30. clear_skies_cortex-2.0.4/src/clearskies_cortex/models/cortex_team_department.py +61 -0
  31. clear_skies_cortex-2.0.4/uv.lock +999 -0
  32. clear_skies_cortex-2.0.2/LATEST_CHANGELOG.md +0 -11
  33. clear_skies_cortex-2.0.2/src/clearskies_cortex/backends/cortex_backend.py +0 -100
  34. clear_skies_cortex-2.0.2/src/clearskies_cortex/columns/string_list.py +0 -25
  35. clear_skies_cortex-2.0.2/src/clearskies_cortex/dataclasses.py +0 -73
  36. clear_skies_cortex-2.0.2/src/clearskies_cortex/defaults/default_cortex_auth.py +0 -9
  37. clear_skies_cortex-2.0.2/src/clearskies_cortex/defaults/default_cortex_url.py +0 -7
  38. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_catalog_entity.py +0 -90
  39. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_catalog_entity_group.py +0 -22
  40. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_catalog_entity_scorecard.py +0 -33
  41. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_catalog_entity_types.py +0 -25
  42. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_entity_relationships.py +0 -25
  43. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_model.py +0 -9
  44. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_scorecard.py +0 -27
  45. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_team_category_tree.py +0 -27
  46. clear_skies_cortex-2.0.2/src/clearskies_cortex/models/cortex_team_department.py +0 -25
  47. clear_skies_cortex-2.0.2/uv.lock +0 -1024
  48. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.editorconfig +0 -0
  49. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.github/workflows/tests.yaml +0 -0
  50. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.gitignore +0 -0
  51. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/.python-version +0 -0
  52. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/LICENSE +0 -0
  53. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/README.md +0 -0
  54. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/cliff.toml +0 -0
  55. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/ruff.toml +0 -0
  56. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/__init__.py +0 -0
  57. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/backends/__init__.py +0 -0
  58. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/columns/__init__.py +0 -0
  59. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/defaults/__init__.py +0 -0
  60. {clear_skies_cortex-2.0.2 → clear_skies_cortex-2.0.4}/src/clearskies_cortex/models/__init__.py +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2
- _commit: v0.0.35
2
+ _commit: v0.0.46
3
3
  _src_path: gh:clearskies-py/clearskies-module-template
4
4
  author_email: tom.nijboer@cimpress.com
5
5
  author_name: Tom Nijboer
@@ -79,7 +79,7 @@ jobs:
79
79
  # STEP 2: Commit all file changes together
80
80
  - name: Commit All Changes
81
81
  run: |
82
- git add pyproject.toml CHANGELOG.md
82
+ git add pyproject.toml CHANGELOG.md uv.lock
83
83
  git commit -m "chore(release): bump version to ${{ steps.bump.outputs.tag }}"
84
84
 
85
85
  # STEP 3: Create the tag for the final commit
@@ -33,6 +33,8 @@ jobs:
33
33
  enable-cache: true
34
34
  - name: Publish
35
35
  run: |
36
+ uv sync --all-groups --all-extras
37
+ uv add clear-skies-doc-builder --group doc
36
38
  cd docs/python
37
39
  uv run build.py
38
40
  sudo apt update
@@ -67,7 +67,7 @@ jobs:
67
67
  python-version: ${{ matrix.python-version }}
68
68
  run-mypy: ${{ needs.changes.outputs.tests == 'true' }}
69
69
  run-pytest: ${{ needs.changes.outputs.tests == 'true' }}
70
- run-black: ${{ needs.changes.outputs.tests == 'true' }}
70
+ run-ruff-format: ${{ needs.changes.outputs.tests == 'true' }}
71
71
  run-ruff-check: ${{ needs.changes.outputs.tests == 'true' }}
72
72
  secrets: inherit
73
73
  strategy:
@@ -76,6 +76,7 @@ jobs:
76
76
  - '3.11'
77
77
  - '3.12'
78
78
  - '3.13'
79
+ - '3.14'
79
80
  fail-fast: false
80
81
 
81
82
  status:
@@ -14,7 +14,7 @@ on:
14
14
  run-pytest:
15
15
  required: true
16
16
  type: boolean
17
- run-black:
17
+ run-ruff-format:
18
18
  required: true
19
19
  type: boolean
20
20
  run-ruff-check:
@@ -66,27 +66,14 @@ jobs:
66
66
  - run: uv run pytest -v
67
67
  - run: git diff --exit-code --stat HEAD
68
68
 
69
- black:
70
- name: black
69
+ ruff-format:
70
+ name: ruff-format
71
71
  runs-on: ${{ inputs.runner }}
72
- if: inputs.run-black
72
+ if: inputs.run-ruff-format
73
73
  steps:
74
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
75
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
76
- with:
77
- persist-credentials: false
78
-
79
- - name: Install uv and set the python version
80
- uses: astral-sh/setup-uv@v6
81
- with:
82
- python-version: ${{ inputs.python-version }}
83
- enable-cache: true
84
- - name: Install dependencies
85
- run: uv sync --locked --all-extras --dev
86
- - uses: psf/black@stable
74
+ - uses: astral-sh/ruff-action@v3
87
75
  with:
88
- options: "--check --verbose"
89
- src: "./src"
76
+ args: "format --diff"
90
77
 
91
78
  ruff-check:
92
79
  name: ruff-check
@@ -38,12 +38,6 @@ repos:
38
38
  hooks:
39
39
  - id: uv-lock
40
40
 
41
- - repo: https://github.com/psf/black
42
- rev: 25.1.0
43
- hooks:
44
- - id: black
45
- files: \.py$
46
-
47
41
  - repo: https://github.com/astral-sh/ruff-pre-commit
48
42
  rev: v0.12.1
49
43
  hooks:
@@ -55,7 +49,7 @@ repos:
55
49
  rev: v1.16.1
56
50
  hooks:
57
51
  - id: mypy
58
- additional_dependencies: [types-requests, clear-skies]
52
+ additional_dependencies: [types-requests]
59
53
  files: \.py$
60
54
 
61
55
  - repo: https://github.com/adrienverge/yamllint
@@ -0,0 +1,21 @@
1
+ {
2
+ "python.testing.pytestArgs": ["tests"],
3
+ "python.testing.unittestEnabled": false,
4
+ "python.testing.pytestEnabled": true,
5
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
6
+ "ruff.interpreter": ["${workspaceFolder}/.venv/bin/python"],
7
+ "ruff.path": ["${workspaceFolder}/.venv/bin/ruff"],
8
+ "ruff.configuration": "${workspaceFolder}/ruff.toml",
9
+ "ruff.nativeServer": "on",
10
+ "[python]": {
11
+ "editor.formatOnSave": true,
12
+ "editor.codeActionsOnSave": {
13
+ "source.fixAll": "explicit",
14
+ "source.organizeImports": "explicit"
15
+ },
16
+ "editor.defaultFormatter": "charliermarsh.ruff"
17
+ },
18
+ "mypy-type-checker.args": [
19
+ "--config-file=${workspaceFolder}/pyproject.toml"
20
+ ]
21
+ }
@@ -5,12 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.4] - 2026-01-28
9
+
10
+ ### Changed
11
+ - Update to v0.0.46
12
+ - Document all the files
13
+
14
+ ### Fixed
15
+ - Set the count data correctly
16
+
17
+ ## [2.0.3] - 2026-01-27
18
+
19
+ ### Changed
20
+ - Bump version to v2.0.3 by @github-actions[bot]
21
+ - Udpate to QueryResult
22
+ - Update to latest copier version
23
+
8
24
  ## [2.0.2] - 2026-01-06
9
25
 
10
26
  ### Added
11
27
  - Add teams
12
28
 
13
29
  ### Changed
30
+ - Bump version to v2.0.2 by @github-actions[bot]
14
31
  - Make it all work with the api
15
32
 
16
33
  ### Fixed
@@ -34,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
51
  * @tnijboer made their first contribution in [#1](https://github.com/clearskies-py/cortex/pull/1)
35
52
  * @cmancone made their first contribution
36
53
  * @ made their first contribution
54
+ [2.0.4]: https://github.com/clearskies-py/cortex/compare/v2.0.3..v2.0.4
55
+ [2.0.3]: https://github.com/clearskies-py/cortex/compare/v2.0.2..v2.0.3
37
56
  [2.0.2]: https://github.com/clearskies-py/cortex/compare/v2.0.1..v2.0.2
38
57
 
39
58
  <!-- generated by git-cliff -->
@@ -0,0 +1,9 @@
1
+ ## [2.0.4] - 2026-01-28
2
+
3
+ ### Changed
4
+ - Update to v0.0.46
5
+ - Document all the files
6
+
7
+ ### Fixed
8
+ - Set the count data correctly
9
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear-skies-cortex
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Cortex module for Clearskies
5
5
  Project-URL: Docs, https://https://clearskies.info/modules/clear-skies-cortex
6
6
  Project-URL: Repository, https://github.com/clearskies-py/cortex
@@ -14,7 +14,7 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: <4.0,>=3.11
17
- Requires-Dist: clear-skies<3.0.0,>=2.0.0
17
+ Requires-Dist: clear-skies<3.0.0,>=2.0.37
18
18
  Requires-Dist: dacite>=1.9.2
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: types-requests>=2.32.4; extra == 'dev'
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "clear-skies-cortex"
3
3
  description = "Cortex module for Clearskies"
4
- version = "2.0.2"
4
+ version = "2.0.4"
5
5
  license = "MIT"
6
6
  readme = "./README.md"
7
7
  authors = [{name = "Tom Nijboer", email = "tom.nijboer@cimpress.com"}]
8
8
  requires-python = ">=3.11,<4.0"
9
9
  dependencies = [
10
- "clear-skies>=2.0.0,<3.0.0",
10
+ "clear-skies>=2.0.37,<3.0.0",
11
11
  "dacite>=1.9.2",
12
12
  ]
13
13
  classifiers = [
@@ -44,19 +44,6 @@ exclude = [
44
44
  "tests/**"
45
45
  ]
46
46
 
47
- [tool.black]
48
- line-length = 120
49
- skip-magic-trailing-comma = false
50
- preview = false
51
-
52
- [tool.mypy]
53
- python_version = "3.11"
54
-
55
- exclude = [
56
- ".*_test\\.py$",
57
- "docs/.*"
58
- ]
59
-
60
47
  [tool.pytest.ini_options]
61
48
  minversion = "6.0"
62
49
  addopts = "-ra -q"
@@ -72,10 +59,12 @@ pythonpath = [
72
59
  [dependency-groups]
73
60
  dev = [
74
61
  "akeyless>=5.0.8",
75
- "black>=25.1.0",
76
62
  "mypy>=1.18.2",
77
63
  "pre-commit>=4.3.0",
78
64
  "pytest>=8.4.1",
79
65
  "pytest-cov>=6.2.1",
80
66
  "ruff>=0.12.10",
81
67
  ]
68
+ doc = [
69
+ "clear-skies-doc-builder>=2.0.11",
70
+ ]
@@ -0,0 +1,236 @@
1
+ from typing import Any
2
+
3
+ import clearskies
4
+ import requests
5
+ from clearskies import configs
6
+ from clearskies.authentication import Authentication
7
+ from clearskies.decorators import parameters_to_properties
8
+ from clearskies.di import inject
9
+ from clearskies.query import Query
10
+
11
+
12
+ class CortexBackend(clearskies.backends.ApiBackend):
13
+ """
14
+ Backend for interacting with the Cortex.io API.
15
+
16
+ This backend extends the ApiBackend to provide seamless integration with the Cortex.io platform.
17
+ It handles the specific pagination and response format used by Cortex APIs, where pagination
18
+ information (`page`, `totalPages`, `total`) is returned in the response body rather than headers.
19
+
20
+ ## Usage
21
+
22
+ The CortexBackend is typically used with models that represent Cortex entities:
23
+
24
+ ```python
25
+ import clearskies
26
+ from clearskies_cortex.backends import CortexBackend
27
+
28
+
29
+ class CortexService(clearskies.Model):
30
+ backend = CortexBackend()
31
+
32
+ @classmethod
33
+ def destination_name(cls) -> str:
34
+ return "catalog/services"
35
+
36
+ tag = clearskies.columns.String()
37
+ name = clearskies.columns.String()
38
+ description = clearskies.columns.String()
39
+ ```
40
+
41
+ ## Authentication
42
+
43
+ By default, the backend uses the `cortex_auth` binding for authentication, which should be
44
+ configured in your application's dependency injection container. You can also provide a custom
45
+ authentication instance:
46
+
47
+ ```python
48
+ backend = CortexBackend(
49
+ authentication=clearskies.authentication.SecretBearer(
50
+ environment_key="CORTEX_API_KEY",
51
+ )
52
+ )
53
+ ```
54
+
55
+ ## Pagination
56
+
57
+ The Cortex API uses page-based pagination with the following response format:
58
+
59
+ ```json
60
+ {
61
+ "entities": [...],
62
+ "page": 1,
63
+ "totalPages": 5,
64
+ "total": 100
65
+ }
66
+ ```
67
+
68
+ The backend automatically handles extracting pagination data and provides the next page
69
+ information to clearskies for seamless iteration through results.
70
+ """
71
+
72
+ """
73
+ The base URL for the Cortex API.
74
+ """
75
+ base_url = configs.String(default="https://api.getcortexapp.com/api/v1/")
76
+
77
+ """
78
+ The authentication instance to use for API requests.
79
+
80
+ By default, this uses the `cortex_auth` binding from the dependency injection container.
81
+ """
82
+ authentication = inject.ByName("cortex_auth") # type: ignore[assignment]
83
+
84
+ """
85
+ The requests instance for making HTTP calls.
86
+ """
87
+ requests = inject.Requests()
88
+
89
+ """
90
+ The casing style used by the Cortex API (camelCase by default).
91
+ """
92
+ api_casing = configs.Select(["snake_case", "camelCase", "TitleCase"], default="camelCase")
93
+
94
+ _auth_headers: dict[str, str] = {}
95
+
96
+ """
97
+ A mapping from API response keys to model column names.
98
+ """
99
+ api_to_model_map = configs.AnyDict(default={})
100
+
101
+ """
102
+ The name of the pagination parameter used in requests.
103
+ """
104
+ pagination_parameter_name = configs.String(default="page")
105
+
106
+ """
107
+ The name of the limit parameter used in requests.
108
+ """
109
+ limit_parameter_name = configs.String(default="pageSize")
110
+
111
+ can_count = False
112
+
113
+ @parameters_to_properties
114
+ def __init__(
115
+ self,
116
+ base_url: str | None = "https://api.getcortexapp.com/api/v1/",
117
+ authentication: Authentication | None = None,
118
+ model_casing: str = "snake_case",
119
+ api_casing: str = "camelCase",
120
+ api_to_model_map: dict[str, str | list[str]] = {},
121
+ pagination_parameter_name: str = "page",
122
+ pagination_parameter_type: str = "int",
123
+ limit_parameter_name: str = "pageSize",
124
+ ):
125
+ self.finalize_and_validate_configuration()
126
+
127
+ def map_records_response(
128
+ self, response_data: Any, query: Query, query_data: dict[str, Any] | None = None
129
+ ) -> list[dict[str, Any]]:
130
+ """
131
+ Map the Cortex API response to model fields.
132
+
133
+ The Cortex API returns responses in a specific format where the actual records are nested
134
+ within a dictionary alongside pagination metadata. This method extracts the records and
135
+ removes the pagination fields before passing to the parent implementation.
136
+
137
+ Example Cortex API response:
138
+
139
+ ```json
140
+ {
141
+ "entities": [{"tag": "service-1", "name": "My Service"}, ...],
142
+ "page": 1,
143
+ "totalPages": 5,
144
+ "total": 100
145
+ }
146
+ ```
147
+
148
+ This method will extract the `entities` list and pass it to the parent for further processing.
149
+ """
150
+ if isinstance(response_data, dict):
151
+ if "page" in response_data:
152
+ del response_data["page"]
153
+ del response_data["totalPages"]
154
+ del response_data["total"]
155
+ first_item = next(iter(response_data))
156
+ if isinstance(response_data[first_item], list) and all(
157
+ isinstance(item, dict) for item in response_data[first_item]
158
+ ):
159
+ return super().map_records_response(response_data[first_item], query, query_data)
160
+ return super().map_records_response(response_data, query, query_data)
161
+
162
+ def get_next_page_data_from_response(
163
+ self,
164
+ query: Query,
165
+ response: "requests.Response", # type: ignore
166
+ ) -> dict[str, Any]:
167
+ """
168
+ Extract pagination data from the Cortex API response.
169
+
170
+ The Cortex API includes pagination information in the response body:
171
+
172
+ - `page`: The current page number
173
+ - `totalPages`: The total number of pages available
174
+ - `total`: The total number of records
175
+
176
+ This method checks if there are more pages available and returns the next page number
177
+ if so. It also extracts total count information for use in RecordsQueryResult.
178
+ The returned dictionary is used by clearskies to fetch subsequent pages and
179
+ populate count metadata.
180
+
181
+ Returns:
182
+ A dictionary containing:
183
+ - The next page number if more pages exist
184
+ - total_count: The total number of records (if available)
185
+ - total_pages: The total number of pages (if available)
186
+ """
187
+ next_page_data: dict[str, Any] = {}
188
+
189
+ response_data = response.json() if response.content else {}
190
+
191
+ if isinstance(response_data, dict):
192
+ # Extract count information from response body
193
+ count_info = self.extract_count_from_response(None, response_data)
194
+ if count_info:
195
+ total_count, total_pages = count_info
196
+ if total_count is not None:
197
+ next_page_data["total_count"] = total_count
198
+ if total_pages is not None:
199
+ next_page_data["total_pages"] = total_pages
200
+
201
+ # Check if there are more pages
202
+ page = response_data.get("page", None)
203
+ total_pages_from_response = response_data.get("totalPages", None)
204
+ if page is not None and total_pages_from_response is not None and page < total_pages_from_response:
205
+ next_page_data[self.pagination_parameter_name] = page + 1
206
+
207
+ return next_page_data
208
+
209
+ def extract_count_from_response(
210
+ self,
211
+ response_headers: dict[str, str] | None = None,
212
+ response_data: Any = None,
213
+ ) -> tuple[int | None, int | None]:
214
+ """
215
+ Extract count information from the Cortex API response body.
216
+
217
+ Unlike many APIs that return count information in headers, the Cortex API includes
218
+ this data in the response body:
219
+
220
+ - `total`: The total number of records matching the query
221
+ - `totalPages`: The total number of pages available
222
+
223
+ This method extracts these values and returns them as a tuple for use in
224
+ `RecordsQueryResult`.
225
+
226
+ Returns:
227
+ A tuple of (total_count, total_pages) where either value may be None
228
+ if not present in the response.
229
+ """
230
+ if not isinstance(response_data, dict):
231
+ return (None, None)
232
+
233
+ total_count = response_data.get("total", None)
234
+ total_pages = response_data.get("totalPages", None)
235
+
236
+ return (total_count, total_pages)
@@ -9,6 +9,7 @@ from clearskies.backends.memory_backend import MemoryBackend, MemoryTable
9
9
  from clearskies.columns import String, Uuid
10
10
  from clearskies.di import inject
11
11
  from clearskies.query import Condition, Query
12
+ from clearskies.query.result import RecordsQueryResult
12
13
 
13
14
  from clearskies_cortex.backends import cortex_backend as rest_backend
14
15
 
@@ -34,7 +35,7 @@ class CortexTeamRelationshipBackend(MemoryBackend, Configurable):
34
35
  # or we need to let the di system build the CortexBackend. This change does both:
35
36
  self.cortex_backend = cortex_backend
36
37
 
37
- def records(self, query: Query, next_page_data: dict[str, str | int] | None = None) -> list[dict[str, Any]]:
38
+ def records(self, query: Query) -> RecordsQueryResult:
38
39
  """Accept either a model or a model class and creates a "table" for it."""
39
40
  table_name = query.model_class.destination_name()
40
41
  if table_name not in self._tables:
@@ -45,7 +46,7 @@ class CortexTeamRelationshipBackend(MemoryBackend, Configurable):
45
46
  # we don't need since we built the data ourselves. In short, it will be a lot slower, so I cheat.
46
47
  self._tables[table_name]._rows = records # type: ignore[assignment]
47
48
  self._tables[table_name]._id_index = id_index # type: ignore[assignment]
48
- return super().records(query, next_page_data)
49
+ return super().records(query)
49
50
 
50
51
  def _fetch_and_map_relationship_data(self, table_name: str) -> tuple[list[dict[str, str | int]], dict[str, int]]:
51
52
  class RelationshipModel(Model):
@@ -75,11 +76,14 @@ class CortexTeamRelationshipBackend(MemoryBackend, Configurable):
75
76
  root_categories: dict[str, str] = {}
76
77
  known_children: dict[str, str] = {}
77
78
  relationships: dict[str, set[str]] = {}
78
- for relationship in self._get_cortex_backend().records(
79
- Query(
80
- model_class=RelationshipModel,
81
- ),
82
- {},
79
+ for relationship in (
80
+ self._get_cortex_backend()
81
+ .records(
82
+ Query(
83
+ model_class=RelationshipModel,
84
+ ),
85
+ )
86
+ .records
83
87
  ):
84
88
  child_category = relationship["child_team_tag"]
85
89
  parent_category = relationship["parent_team_tag"]
@@ -153,12 +157,15 @@ class CortexTeamRelationshipBackend(MemoryBackend, Configurable):
153
157
  from clearskies_cortex.models.cortex_team import CortexTeam
154
158
 
155
159
  teams: dict[str, dict[str, Any]] = {}
156
- for team in self._get_cortex_backend().records(
157
- Query(
158
- model_class=CortexTeam,
159
- conditions=[Condition("include_teams_without_members=true")],
160
- ),
161
- {},
160
+ for team in (
161
+ self._get_cortex_backend()
162
+ .records(
163
+ Query(
164
+ model_class=CortexTeam,
165
+ conditions=[Condition("include_teams_without_members=true")],
166
+ ),
167
+ )
168
+ .records
162
169
  ):
163
170
  teams[team["team_tag"]] = team
164
171
  self._cached_teams = teams
@@ -0,0 +1,69 @@
1
+ from typing import Any
2
+
3
+ from clearskies.columns import String
4
+
5
+
6
+ class StringList(String):
7
+ """
8
+ Column type for comma-delimited string lists.
9
+
10
+ This column type extends the String column to handle values that are stored as
11
+ comma-delimited strings but should be represented as Python lists. It automatically
12
+ converts between the two formats when reading from and writing to the backend.
13
+
14
+ Use this column type when the API returns or expects comma-separated values that
15
+ you want to work with as lists in your application code.
16
+
17
+ ```python
18
+ from clearskies import Model
19
+ from clearskies_cortex.columns import StringList
20
+
21
+
22
+ class MyModel(Model):
23
+ # Define a column that stores ["tag1", "tag2"] as "tag1,tag2"
24
+ tags = StringList()
25
+
26
+
27
+ # When reading from the backend:
28
+ # API returns: {"tags": "tag1,tag2,tag3"}
29
+ # Model provides: model.tags = ["tag1", "tag2", "tag3"]
30
+
31
+ # When writing to the backend:
32
+ # Model has: model.tags = ["tag1", "tag2", "tag3"]
33
+ # API receives: {"tags": "tag1,tag2,tag3"}
34
+ ```
35
+ """
36
+
37
+ def from_backend(self, value: str | list[str]) -> list[str]:
38
+ """
39
+ Convert backend value to a Python list.
40
+
41
+ Handles both string (comma-delimited) and list inputs for flexibility.
42
+
43
+ Args:
44
+ value: Either a comma-delimited string or a list of strings.
45
+
46
+ Returns:
47
+ A list of strings.
48
+ """
49
+ if isinstance(value, list):
50
+ return value
51
+ return value.split(",")
52
+
53
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
54
+ """
55
+ Convert Python list to comma-delimited string for backend storage.
56
+
57
+ Transforms the list value back into a comma-separated string format
58
+ expected by the backend API.
59
+
60
+ Args:
61
+ data: Dictionary containing the column data.
62
+
63
+ Returns:
64
+ Dictionary with the column value converted to a comma-delimited string.
65
+ """
66
+ if self.name not in data:
67
+ return data
68
+
69
+ return {**data, self.name: str(",".join(data[self.name]))}