clear-skies-cortex 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: clear-skies-cortex
3
+ Version: 2.0.1
4
+ Summary: Cortex module for Clearskies
5
+ Project-URL: Docs, https://https://clearskies.info/modules/clear-skies-cortex
6
+ Project-URL: Repository, https://github.com/clearskies-py/cortex
7
+ Project-URL: Issues, https://github.com/clearskies-py/cortex/issues
8
+ Project-URL: Changelog, https://github.com/clearskies-py/cortex/blob/main/CHANGELOG.md
9
+ Author-email: Tom Nijboer <tom.nijboer@cimpress.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: <4.0,>=3.11
17
+ Requires-Dist: clear-skies<3.0.0,>=2.0.0
18
+ Requires-Dist: dacite>=1.9.2
19
+ Provides-Extra: dev
20
+ Requires-Dist: types-requests>=2.32.4; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+
24
+ # cortex
25
+
26
+
27
+ Cortex module for Clearskies
28
+
29
+ This template scaffolds a dynamic Clearskies module for any kind of logic, integration, or API. You can use it to build modules for data processing, service integration, automation, or any custom business logic.
30
+
31
+ Your module can implement any logic you need: fetch data, process input, interact with external services, or perform custom actions. The endpoints and payloads are up to you.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ # Install uv if not already installed
37
+ pip install uv
38
+
39
+ # Create a virtual environment and install dependencies
40
+ uv sync
41
+ ```
42
+
43
+ ## Development
44
+
45
+ To set up your development environment with pre-commit hooks:
46
+
47
+ ```bash
48
+ # Install uv if not already installed
49
+ pip install uv
50
+
51
+ # Create a virtual environment and install all dependencies (including dev)
52
+ uv sync
53
+
54
+ # Install dev dependencies (including ruff, black, mypy) in the project environment
55
+ uv pip install .[dev]
56
+
57
+ # Install pre-commit hooks
58
+ uv run pre-commit install
59
+
60
+ # Optionally, run pre-commit on all files
61
+ uv run pre-commit run --all-files
62
+ ```
63
+
64
+ ## Usage Example
65
+
66
+ ```python
67
+ import clearskies
68
+ import clearskies_cortex
69
+
70
+ wsgi = clearskies.contexts.WsgiRef(
71
+ clearskies_cortex.build__module()
72
+ )
73
+ wsgi()
74
+ ```
@@ -0,0 +1,27 @@
1
+ clearskies_cortex/__init__.py,sha256=SwFkNfKLJgcq2qE6YJ_78_JfeoRGojlKuR_3DymZZSQ,142
2
+ clearskies_cortex/dataclasses.py,sha256=yZhRm2dnGCVGaZUEuh8n86kqqxFTuKU_jljkh0gAW-4,1432
3
+ clearskies_cortex/backends/__init__.py,sha256=Ek74kpJLE7ERY-6yhH2KwFO25gm3Yjz51laUApnhgnU,179
4
+ clearskies_cortex/backends/cortex_backend.py,sha256=OnbrHXjojBWoOBYRjfKD26FO7XzzYsVrpWJAfQ95wn0,1650
5
+ clearskies_cortex/backends/cortex_team_relationship_backend.py,sha256=IHvYzDMcmUqJHjdWzm1QtNbwWD9LbUNMRQVUC2Afx-U,7795
6
+ clearskies_cortex/columns/__init__.py,sha256=BpoVCEVXRtKcsyRFnAYLtlwYtHBjciUbzuzaBxmfH00,87
7
+ clearskies_cortex/columns/string_list.py,sha256=khsJS_0T4XZvTeXFpRvZsFX8iNTWbx_urCEa9Tv0Bmo,754
8
+ clearskies_cortex/defaults/__init__.py,sha256=tulZSvFgp4YUKj_bnArIevmlpC8z_AQKO0okYs4hCDo,216
9
+ clearskies_cortex/defaults/default_cortex_auth.py,sha256=yA5kCwGxPEdV2t-28UArkKXgI3qbxt-7P6az4cLI1Rg,508
10
+ clearskies_cortex/defaults/default_cortex_url.py,sha256=SaR6dQW3ELtuy4Woap8ufYL_zGQ0K0XwHUv0CYi_p78,305
11
+ clearskies_cortex/models/__init__.py,sha256=aJXrP5UwHCoKe833Mpg4hbTEftXSgAdRFXrUYZ0KGOA,1026
12
+ clearskies_cortex/models/cortex_catalog_entity.py,sha256=-7lo67x09N-iNWRGKpdDrn1raGyhH1oL8UpscUVxb9I,1766
13
+ clearskies_cortex/models/cortex_catalog_entity_domain.py,sha256=UTa2vBGt8DuTFrO_Prcckb0NXHB0YsppZv47D3NmpFs,735
14
+ clearskies_cortex/models/cortex_catalog_entity_group.py,sha256=dvuwwuj8Mwm0HgqVjkz1MpUycBAl9WUrDNC0XNGeWGk,514
15
+ clearskies_cortex/models/cortex_catalog_entity_scorecard.py,sha256=sWp_v7O9uY3VbJjdb_VWUPHgBIn-rRj6kZDHqZq4lXI,908
16
+ clearskies_cortex/models/cortex_catalog_entity_service.py,sha256=4Ibodm7orM9O_HZB1BkAVgYkM2st8Cm9iBO-qdyffhM,3344
17
+ clearskies_cortex/models/cortex_catalog_entity_types.py,sha256=EJBRv0ZMHwZ6HpaXxy0A_EBGqmu9EeVAZiX4xPIljz4,568
18
+ clearskies_cortex/models/cortex_entity_relationships.py,sha256=bB46YFK8Vc2SZsvobvFeyTfEj8YfcaXFaPnkW0o_nG4,607
19
+ clearskies_cortex/models/cortex_model.py,sha256=QmwQ45vuK4iUKCJsOk1pwu1at8IYKg39pYO9TtlYrEA,175
20
+ clearskies_cortex/models/cortex_scorecard.py,sha256=YlINfUGudmqQHe-0ZpD2qOzAUKWshUycwgXYEcxqsUk,663
21
+ clearskies_cortex/models/cortex_team.py,sha256=wPJxkNV1qQPq4WwMhBdKBnotQAB9KbmdVpowWCmfqp0,2042
22
+ clearskies_cortex/models/cortex_team_category_tree.py,sha256=PLC-9E5kuMeItow7RKFX7jKUiAfJ-7ehXIy3eeaFzr8,721
23
+ clearskies_cortex/models/cortex_team_department.py,sha256=DL3k91462-R0VVqbUQgBfYM_TndE5MKmDPBSF448XYw,639
24
+ clear_skies_cortex-2.0.1.dist-info/METADATA,sha256=LM3SjjrLaNTpz66dKoRs1p6hbOoalJpVN46uWAMg3gc,2116
25
+ clear_skies_cortex-2.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ clear_skies_cortex-2.0.1.dist-info/licenses/LICENSE,sha256=MkEX8JF8kZxdyBpTTcB0YTd-xZpWnHvbRlw-pQh8u58,1069
27
+ clear_skies_cortex-2.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Tom Nijboer
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.
@@ -0,0 +1,8 @@
1
+ from clearskies_cortex import backends, columns, defaults, models
2
+
3
+ __all__ = [
4
+ "backends",
5
+ "columns",
6
+ "defaults",
7
+ "models",
8
+ ]
@@ -0,0 +1,4 @@
1
+ from clearskies_cortex.backends.cortex_backend import CortexBackend
2
+ from clearskies_cortex.backends.cortex_team_relationship_backend import (
3
+ CortexTeamRelationshipBackend,
4
+ )
@@ -0,0 +1,45 @@
1
+ import clearskies
2
+ from clearskies import configs
3
+ from clearskies.authentication import Authentication
4
+ from clearskies.decorators import parameters_to_properties
5
+ from clearskies.di import inject
6
+ from clearskies.query import Query
7
+
8
+
9
+ class CortexBackend(clearskies.backends.ApiBackend):
10
+ """Backend for Cortex.io."""
11
+
12
+ base_url = configs.String(default="https://api.getcortexapp.com/api/v1/")
13
+ authentication = inject.ByName("cortex_auth") # type: ignore[assignment]
14
+ requests = inject.Requests()
15
+ _auth_headers: dict[str, str] = {}
16
+
17
+ api_to_model_map = configs.AnyDict(default={})
18
+ pagination_parameter_name = configs.String(default="page")
19
+
20
+ can_count = True
21
+
22
+ @parameters_to_properties
23
+ def __init__(
24
+ self,
25
+ base_url: str | None = "https://api.getcortexapp.com/api/v1/",
26
+ authentication: Authentication | None = None,
27
+ model_casing: str = "snake_case",
28
+ api_casing: str = "camelCase",
29
+ api_to_model_map: dict[str, str | list[str]] = {},
30
+ pagination_parameter_name: str = "page",
31
+ pagination_parameter_type: str = "int",
32
+ limit_parameter_name: str = "pageSize",
33
+ ):
34
+ self.finalize_and_validate_configuration()
35
+
36
+ def count(self, query: Query) -> int:
37
+ """Return count of records matching query."""
38
+ self.check_query(query)
39
+ (url, method, body, headers) = self.build_records_request(query)
40
+ response = self.execute_request(url, method, json=body, headers=headers)
41
+ response.raise_for_status()
42
+ data = response.json()
43
+ if "total" in data:
44
+ return data["total"]
45
+ return len(data)
@@ -0,0 +1,174 @@
1
+ import json
2
+ import logging
3
+ import uuid
4
+ from types import SimpleNamespace
5
+ from typing import Any
6
+
7
+ from clearskies import Configurable, Model, configs
8
+ from clearskies.backends.memory_backend import MemoryBackend, MemoryTable
9
+ from clearskies.columns import String, Uuid
10
+ from clearskies.di import inject
11
+ from clearskies.query.query import Query
12
+
13
+ from clearskies_cortex.backends import cortex_backend as rest_backend
14
+
15
+
16
+ class CortexTeamRelationshipBackend(MemoryBackend, Configurable):
17
+ """Backend for Cortex.io."""
18
+
19
+ logger = logging.getLogger(__name__)
20
+ di = inject.Di()
21
+
22
+ cortex_backend = configs.Any(default=None)
23
+ _cached_teams: dict[str, dict[str, Any]]
24
+
25
+ def __init__(
26
+ self,
27
+ cortex_backend,
28
+ silent_on_missing_tables=True,
29
+ ):
30
+ super().__init__(silent_on_missing_tables)
31
+ # This backend has its dependencies injected automatically because it is attachd to the model,
32
+ # but when you directly instantiate the CortexBackend and pass it in, the di system never has a chance
33
+ # to provide IT with the necessary deendencies. Therefore, we just have to explicitly do it,
34
+ # or we need to let the di system build the CortexBackend. This change does both:
35
+ self.cortex_backend = cortex_backend
36
+
37
+ def records(self, query: Query, next_page_data: dict[str, str | int] | None = None) -> list[dict[str, Any]]:
38
+ """Accept either a model or a model class and creates a "table" for it."""
39
+ table_name = query.model_class.destination_name()
40
+ if table_name not in self._tables:
41
+ self._tables[table_name] = MemoryTable(query.model_class)
42
+ [records, id_index] = self._fetch_and_map_relationship_data(table_name)
43
+ # directly setting internal things is bad, but the create function on the MemoryTable
44
+ # (which is how we _should_ feed data into it) does a lot of extra data validation that
45
+ # we don't need since we built the data ourselves. In short, it will be a lot slower, so I cheat.
46
+ self._tables[table_name]._rows = records # type: ignore[assignment]
47
+ self._tables[table_name]._id_index = id_index # type: ignore[assignment]
48
+ return super().records(query, next_page_data)
49
+
50
+ def _fetch_and_map_relationship_data(self, table_name: str) -> tuple[list[dict[str, str | int]], dict[str, int]]:
51
+ class RelationshipModel(Model):
52
+ id_column_name: str = "id"
53
+ backend = rest_backend.CortexBackend()
54
+
55
+ id = String()
56
+ child_team_tag = String()
57
+ parent_team_tag = String()
58
+ provider = String()
59
+
60
+ @classmethod
61
+ def destination_name(cls) -> str:
62
+ return "teams/relationships"
63
+
64
+ try:
65
+ relationship_data = self._get_cortex_backend().records(
66
+ Query(
67
+ model_class=RelationshipModel,
68
+ ),
69
+ {},
70
+ )[0]
71
+
72
+ except IndexError:
73
+ relationship_data = {"edges": []}
74
+
75
+ # this should match up to exactly what backend.records() will return
76
+ # relationship_data = example_data["edges"]
77
+
78
+ # we need to map this to the kind of row structure expected by the category_tree column
79
+ # (see https://github.com/clearskies-py/clearskies/blob/main/src/clearskies/columns/category_tree.py)
80
+ # This takes slightly more time up front but makes for quick lookups in both directions (and we'll
81
+ # cache the result so it only has to happen once). The trouble is that we need to know the tree before
82
+ # we can get started. We want to start at the top or the bottom, but Cortex gives us neither.
83
+ # therefore, we'll search for the root categories and then start over. While we find those, we'll
84
+ # convert from a list of edges to a dictionary of parent/children
85
+
86
+ # Fetch all teams and filter out archived ones
87
+ from clearskies_cortex.models.cortex_team import CortexTeam
88
+
89
+ root_categories: dict[str, str] = {}
90
+ known_children: dict[str, str] = {}
91
+ relationships: dict[str, set[str]] = {}
92
+ for relationship in relationship_data["edges"]:
93
+ child_category = relationship["childTeamTag"]
94
+ parent_category = relationship["parentTeamTag"]
95
+ # Skip if either parent or child is archived
96
+ if parent_category not in relationships:
97
+ relationships[parent_category] = set()
98
+ relationships[parent_category].add(child_category)
99
+ known_children[child_category] = child_category
100
+ if parent_category not in known_children:
101
+ root_categories[parent_category] = parent_category
102
+ if child_category in root_categories:
103
+ del root_categories[child_category]
104
+
105
+ mapped_records: list[dict[str, str | int]] = []
106
+ id_index: dict[str, int] = {}
107
+ # now we can work our way down the tree, starting at the root categories
108
+
109
+ nested_tree = self._build_nested_tree(relationships, root_categories)
110
+
111
+ def traverse_all_paths(node, ancestors):
112
+ mapped = []
113
+ node_name = node["name"]
114
+ # For every ancestor path, emit a record for each ancestor-child pair
115
+ for idx, ancestor in enumerate(ancestors):
116
+ if (
117
+ not self.all_teams().get(node_name)
118
+ or self.all_teams().get(node_name, {}).get("isArchived")
119
+ or self.all_teams().get(ancestor, {}).get("isArchived")
120
+ ):
121
+ continue
122
+ mapped.append(
123
+ {
124
+ "id": str(uuid.uuid4()),
125
+ "parent_team_tag": ancestor,
126
+ "child_team_tag": node_name,
127
+ "is_parent": 1 if idx == len(ancestors) - 1 and len(ancestors) > 0 else 0,
128
+ "level": idx + 1,
129
+ }
130
+ )
131
+ # Recurse for each child, passing a *copy* of ancestors + this node
132
+ for child in node.get("children", []):
133
+ mapped.extend(traverse_all_paths(child, ancestors + [node_name]))
134
+ return mapped
135
+
136
+ for root in nested_tree.values():
137
+ mapped_records.extend(traverse_all_paths(root, []))
138
+ # now build our id index
139
+ id_index = {str(record["id"]): index for (index, record) in enumerate(mapped_records)}
140
+
141
+ return (mapped_records, id_index)
142
+
143
+ def _build_nested_tree(self, relationships: dict[str, set[str]], root_categories: dict[str, str]) -> dict:
144
+ def build_subtree(node):
145
+ return {"name": node, "children": [build_subtree(child) for child in relationships.get(node, [])]}
146
+
147
+ return {root: build_subtree(root) for root in root_categories}
148
+
149
+ def _get_cortex_backend(self) -> rest_backend.CortexBackend:
150
+ """Return the cortex backend."""
151
+ if self.cortex_backend is not None:
152
+ self.di.inject_properties(self.cortex_backend.__class__)
153
+ else:
154
+ self.cortex_backend = self.di.build_class(rest_backend.CortexBackend)
155
+ return self.cortex_backend
156
+
157
+ def all_teams(self) -> dict[str, dict[str, Any]]:
158
+ """Return all teams from cortex."""
159
+ if hasattr(self, "_cached_teams"):
160
+ return self._cached_teams
161
+
162
+ from clearskies_cortex.models.cortex_team import CortexTeam
163
+
164
+ teams: dict[str, dict[str, Any]] = {}
165
+ team_result = self._get_cortex_backend().records(
166
+ Query(
167
+ model_class=CortexTeam,
168
+ ),
169
+ {},
170
+ )[0]
171
+ for team in team_result["teams"]:
172
+ teams[team["teamTag"]] = team
173
+ self._cached_teams = teams
174
+ return teams
@@ -0,0 +1,3 @@
1
+ from clearskies_cortex.columns.string_list import StringList
2
+
3
+ __all__ = ["StringList"]
@@ -0,0 +1,25 @@
1
+ from typing import Any
2
+
3
+ from clearskies.columns import String
4
+
5
+
6
+ class StringList(String):
7
+ """Column type for comma delimited string."""
8
+
9
+ def from_backend(self, value: str | list[str]) -> list[str]:
10
+ """Return comma delimited string to list."""
11
+ if isinstance(value, list):
12
+ return value
13
+ return value.split(",")
14
+
15
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
16
+ """
17
+ Make any changes needed to save the data to the backend.
18
+
19
+ This typically means formatting changes - converting DateTime objects to database
20
+ date strings, etc...
21
+ """
22
+ if self.name not in data:
23
+ return data
24
+
25
+ return {**data, self.name: str(",".join(data[self.name]))}
@@ -0,0 +1,73 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+
5
+ @dataclass
6
+ class ServiceEntityHierarchy:
7
+ """Dataclass for parent in service hierarchy."""
8
+
9
+ parents: list["ServiceEntityHierarchyParent"]
10
+ children: list["ServiceEntityHierarchyChild"]
11
+
12
+
13
+ @dataclass
14
+ class ServiceEntityHierarchyParent:
15
+ """Dataclass for parent in hierarchy."""
16
+
17
+ tag: str
18
+ type: str
19
+ name: str
20
+ description: str | None
21
+ definition: None | dict[str, Any]
22
+ parents: list["ServiceEntityHierarchyParent"]
23
+ groups: list[str]
24
+
25
+
26
+ @dataclass
27
+ class ServiceEntityHierarchyChild:
28
+ """Dataclass for child in hierarchy."""
29
+
30
+ tag: str
31
+ type: str
32
+ name: str
33
+ description: str | None
34
+ definition: None | dict[str, Any]
35
+ children: list["ServiceEntityHierarchyChild"]
36
+ groups: list[str]
37
+
38
+
39
+ @dataclass
40
+ class TeamCategory:
41
+ """Dataclass for Team Category Tree."""
42
+
43
+ name: str
44
+ level: int
45
+ parent_name: str
46
+
47
+
48
+ @dataclass
49
+ class EntityTeam:
50
+ """Dataclass for team in Catalog Entity."""
51
+
52
+ description: str | None
53
+ inheritance: str
54
+ isArchived: bool # noqa: N815
55
+ name: str
56
+ provider: str
57
+ tag: str
58
+
59
+
60
+ @dataclass
61
+ class EntityIndividual:
62
+ """Dataclass for individual in Catalog Entity."""
63
+
64
+ description: str | None
65
+ email: str
66
+
67
+
68
+ @dataclass
69
+ class EntityTeamOwner:
70
+ """Dataclass for team owners in Catalog Entity."""
71
+
72
+ teams: list[EntityTeam]
73
+ individuals: list[EntityIndividual]
@@ -0,0 +1,7 @@
1
+ from clearskies_cortex.defaults.default_cortex_auth import DefaultCortexAuth
2
+ from clearskies_cortex.defaults.default_cortex_url import DefaultCortexUrl
3
+
4
+ __all__ = [
5
+ "DefaultCortexUrl",
6
+ "DefaultCortexAuth",
7
+ ]
@@ -0,0 +1,9 @@
1
+ import clearskies
2
+
3
+
4
+ class DefaultCortexAuth(clearskies.di.AdditionalConfigAutoImport):
5
+ def provide_cortex_auth(self, environment: clearskies.Environment):
6
+ if environment.get("CORTEX_AUTH_SECRET_PATH", True):
7
+ secret_key = environment.get("CORTEX_AUTH_SECRET_PATH")
8
+ return clearskies.authentication.SecretBearer(secret_key=secret_key, header_prefix="Bearer ")
9
+ return clearskies.authentication.SecretBearer(environment_key="CORTEX_AUTH_KEY", header_prefix="Bearer ")
@@ -0,0 +1,7 @@
1
+ import clearskies
2
+
3
+
4
+ class DefaultCortexUrl(clearskies.di.AdditionalConfigAutoImport):
5
+ def provide_cortex_url(self, environment: clearskies.Environment) -> str:
6
+ cortex_url = environment.get("CORTEX_URL", True)
7
+ return cortex_url if cortex_url else "https://api.getcortexapp.com/api/v1/"
@@ -0,0 +1,21 @@
1
+ from clearskies_cortex.models.cortex_catalog_entity import CortexCatalogEntity
2
+ from clearskies_cortex.models.cortex_catalog_entity_domain import CortexCatalogEntityDomain
3
+ from clearskies_cortex.models.cortex_catalog_entity_group import CortexCatalogEntityGroup
4
+ from clearskies_cortex.models.cortex_catalog_entity_scorecard import CortexCatalogEntityScorecard
5
+ from clearskies_cortex.models.cortex_catalog_entity_service import CortexCatalogEntityService
6
+ from clearskies_cortex.models.cortex_scorecard import CortexScorecard
7
+ from clearskies_cortex.models.cortex_team import CortexTeam
8
+ from clearskies_cortex.models.cortex_team_category_tree import CortexTeamCategoryTree
9
+ from clearskies_cortex.models.cortex_team_department import CortexTeamDepartment
10
+
11
+ __all__ = [
12
+ "CortexCatalogEntity",
13
+ "CortexCatalogEntityDomain",
14
+ "CortexCatalogEntityGroup",
15
+ "CortexCatalogEntityScorecard",
16
+ "CortexCatalogEntityService",
17
+ "CortexScorecard",
18
+ "CortexTeam",
19
+ "CortexTeamCategoryTree",
20
+ "CortexTeamDepartment",
21
+ ]
@@ -0,0 +1,62 @@
1
+ from typing import Self
2
+
3
+ from clearskies import Model
4
+ from clearskies.columns import Boolean, Datetime, HasMany, Json, String
5
+
6
+ from clearskies_cortex.backends import CortexBackend
7
+ from clearskies_cortex.columns import StringList
8
+ from clearskies_cortex.models import cortex_catalog_entity_group, cortex_catalog_entity_scorecard
9
+
10
+
11
+ class CortexCatalogEntity(Model):
12
+ """Model for entities."""
13
+
14
+ id_column_name: str = "tag"
15
+
16
+ backend = CortexBackend()
17
+
18
+ @classmethod
19
+ def destination_name(cls: type[Self]) -> str:
20
+ """Return the slug of the api endpoint for this model."""
21
+ return "catalog"
22
+
23
+ id = String()
24
+ tag = String()
25
+ groups = StringList("groups")
26
+ owners = Json()
27
+ ownership = Json()
28
+ ownersV2 = Json()
29
+ description = String()
30
+ git = Json()
31
+ hierarchy = Json()
32
+ last_updated = Datetime()
33
+ is_archived = Boolean()
34
+ links = Json()
35
+ members = Json()
36
+ metadata = Json()
37
+ slack_channels = Json()
38
+ name = String()
39
+ type = String()
40
+ scorecards = HasMany(
41
+ cortex_catalog_entity_scorecard.CortexCatalogEntityScorecard,
42
+ foreign_column_name="entity_tag",
43
+ )
44
+ group_models = HasMany(
45
+ cortex_catalog_entity_group.CortexCatalogEntityGroup,
46
+ foreign_column_name="entity_tag",
47
+ )
48
+
49
+ def parse_groups(self) -> dict[str, str]:
50
+ """
51
+ Parse the strings of groups.
52
+
53
+ The groups is a list of string with key,value splitted by ':'.
54
+ Return a dict with key value.
55
+ """
56
+ parsed: dict[str, str] = {}
57
+ if self.groups:
58
+ for entity in self.groups:
59
+ splitted = entity.split(":")
60
+ if len(splitted) > 1:
61
+ parsed[splitted[0]] = splitted[1]
62
+ return parsed
@@ -0,0 +1,25 @@
1
+ from typing import Any, Self
2
+
3
+ from clearskies import Column
4
+
5
+ from clearskies_cortex.models import cortex_catalog_entity
6
+
7
+
8
+ class CortexCatalogEntityDomain(cortex_catalog_entity.CortexCatalogEntity):
9
+ """Model for domain entities."""
10
+
11
+ def where_for_request(
12
+ self: Self,
13
+ model: Self,
14
+ input_output: Any,
15
+ routing_data: dict[str, str],
16
+ authorization_data: dict[str, Any],
17
+ overrides: dict[str, Column] = {},
18
+ ) -> Self:
19
+ return (
20
+ model.where("types=domain")
21
+ .where("include_nested_fields=team:members")
22
+ .where("include_owners=true")
23
+ .where("include_metadata=true")
24
+ .where("include_hierarchy_fields=groups")
25
+ )
@@ -0,0 +1,22 @@
1
+ from typing import Self
2
+
3
+ from clearskies import Model
4
+ from clearskies.columns import Json, String
5
+
6
+ from clearskies_cortex.backends import CortexBackend
7
+
8
+
9
+ class CortexCatalogEntityGroup(Model):
10
+ """Model for teams."""
11
+
12
+ id_column_name: str = "entity_tag"
13
+
14
+ backend = CortexBackend()
15
+
16
+ @classmethod
17
+ def destination_name(cls: type[Self]) -> str:
18
+ """Return the slug of the api endpoint for this model."""
19
+ return "catalog/{entity_tag}/groups"
20
+
21
+ entity_tag = String()
22
+ tag = Json()
@@ -0,0 +1,33 @@
1
+ from collections import OrderedDict
2
+ from typing import Any, Self
3
+
4
+ from clearskies import Model
5
+ from clearskies.columns import Float, Integer, Json, String
6
+
7
+ from clearskies_cortex.backends import CortexBackend
8
+
9
+
10
+ class CortexCatalogEntityScorecard(Model):
11
+ """Model for teams."""
12
+
13
+ id_column_name: str = "scorecard_id"
14
+
15
+ backend = CortexBackend()
16
+
17
+ @classmethod
18
+ def destination_name(cls: type[Self]) -> str:
19
+ """Return the slug of the api endpoint for this model."""
20
+ return "catalog/:entity_tag/scorecards"
21
+
22
+ scorecard_id = Integer()
23
+ entity_tag = String()
24
+ ladder_levels = Json()
25
+ score = Integer()
26
+ score_percentage = Float()
27
+ score_card_name = String()
28
+ total_possible_score = Integer()
29
+
30
+ def get_score_card_tag_name(self) -> str:
31
+ """Transform the scorecardName to scorecard tag."""
32
+ name: str = self.score_card_name
33
+ return name
@@ -0,0 +1,92 @@
1
+ import logging
2
+ from typing import Any, Self, cast
3
+
4
+ from clearskies import Column
5
+ from clearskies.di import inject
6
+ from dacite import from_dict
7
+
8
+ from clearskies_cortex import dataclasses
9
+ from clearskies_cortex.backends import CortexBackend
10
+ from clearskies_cortex.models import (
11
+ cortex_catalog_entity,
12
+ cortex_catalog_entity_domain,
13
+ cortex_team,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CortexCatalogEntityService(cortex_catalog_entity.CortexCatalogEntity):
20
+ """Model for domain entities."""
21
+
22
+ backend = CortexBackend()
23
+
24
+ teams = inject.ByClass(cortex_team.CortexTeam)
25
+ entity_domains = inject.ByClass(cortex_catalog_entity_domain.CortexCatalogEntityDomain)
26
+
27
+ def where_for_request(
28
+ self: Self,
29
+ model: Self,
30
+ input_output: Any,
31
+ routing_data: dict[str, str],
32
+ authorization_data: dict[str, Any],
33
+ overrides: dict[str, Column] = {},
34
+ ) -> Self:
35
+ """Return iterable models."""
36
+ return (
37
+ model.where("types=service")
38
+ .where("include_nested_fields=team:members")
39
+ .where("include_owners=true")
40
+ .where("include_metadata=true")
41
+ .where("include_hierarchy_fields=groups")
42
+ )
43
+
44
+ def get_software_domain(self: Self) -> cortex_catalog_entity_domain.CortexCatalogEntityDomain:
45
+ """Get the upper domain of this service if set."""
46
+ hierarchy = from_dict(dataclasses.ServiceEntityHierarchy, data=self.hierarchy)
47
+ if hierarchy.parents:
48
+ parent = hierarchy.parents[0]
49
+ while parent.parents:
50
+ parent = parent.parents[0]
51
+ return cast(
52
+ cortex_catalog_entity_domain.CortexCatalogEntityDomain,
53
+ self.entity_domains.find(f"tag={parent.tag}"),
54
+ )
55
+ return cast(cortex_catalog_entity_domain.CortexCatalogEntityDomain, self.entity_domains.empty())
56
+
57
+ def get_software_container(self: Self) -> cortex_catalog_entity_domain.CortexCatalogEntityDomain:
58
+ """Get the first domain of this service if set."""
59
+ hierarchy = from_dict(dataclasses.ServiceEntityHierarchy, data=self.hierarchy)
60
+ if hierarchy.parents:
61
+ container = hierarchy.parents[0]
62
+ return cast(
63
+ cortex_catalog_entity_domain.CortexCatalogEntityDomain,
64
+ self.entity_domains.find(f"tag={container.tag}"),
65
+ )
66
+ return cast(cortex_catalog_entity_domain.CortexCatalogEntityDomain, self.entity_domains.empty())
67
+
68
+ def get_top_level_team(self: Self) -> cortex_team.CortexTeam:
69
+ """Find the top level team based on the team ownership."""
70
+ team = self.get_team()
71
+
72
+ if team:
73
+ return team.find_top_level_team()
74
+
75
+ return team
76
+
77
+ def get_team(self: Self) -> cortex_team.CortexTeam:
78
+ """Find the team based on the team ownership."""
79
+ team = cast(cortex_team.CortexTeam, self.teams.empty())
80
+ if not self.ownersV2:
81
+ return team
82
+
83
+ logger.debug(f"EntityService: ownersV2 {self.ownersV2}")
84
+ owners = from_dict(dataclasses.EntityTeamOwner, data=self.ownersV2)
85
+
86
+ if not owners.teams:
87
+ return team
88
+
89
+ entity_team = owners.teams[0]
90
+ logger.debug(f"Found entity team: {entity_team}")
91
+
92
+ return self.teams.find(f"team_tag={entity_team.tag}")
@@ -0,0 +1,25 @@
1
+ from typing import Self
2
+
3
+ from clearskies import Model
4
+ from clearskies.columns import Json, String
5
+
6
+ from clearskies_cortex.backends import CortexBackend
7
+
8
+
9
+ class CortexCatalogEntityType(Model):
10
+ """Model for entities."""
11
+
12
+ id_column_name: str = "type"
13
+
14
+ backend = CortexBackend()
15
+
16
+ @classmethod
17
+ def destination_name(cls: type[Self]) -> str:
18
+ """Return the slug of the api endpoint for this model."""
19
+ return "catalog/definitions"
20
+
21
+ name = String()
22
+ description = String()
23
+ schema = Json()
24
+ source = String()
25
+ type = String()
@@ -0,0 +1,25 @@
1
+ from typing import Self
2
+
3
+ from clearskies import Model
4
+ from clearskies.columns import String
5
+
6
+ from clearskies_cortex.backends import CortexBackend
7
+
8
+
9
+ class CortexCatalogEntityRelationship(Model):
10
+ """Model for entities."""
11
+
12
+ id_column_name: str = "tag"
13
+
14
+ backend = CortexBackend()
15
+
16
+ @classmethod
17
+ def destination_name(cls: type[Self]) -> str:
18
+ """Return the slug of the api endpoint for this model."""
19
+ return "catalog/:tag/relationships/:relationship_type_tag/destinations"
20
+
21
+ id = String()
22
+ tag = String()
23
+ description = String()
24
+ name = String()
25
+ type = String()
@@ -0,0 +1,9 @@
1
+ from clearskies import Model
2
+
3
+ from clearskies_cortex.backends import CortexBackend
4
+
5
+
6
+ class CortexModel(Model):
7
+ """Base model for cortex."""
8
+
9
+ backend = CortexBackend()
@@ -0,0 +1,27 @@
1
+ from collections import OrderedDict
2
+ from typing import Any, Self
3
+
4
+ from clearskies import Model
5
+ from clearskies.columns import Boolean, Json, String
6
+
7
+ from clearskies_cortex.backends import CortexBackend
8
+
9
+
10
+ class CortexScorecard(Model):
11
+ """Model for scorecards."""
12
+
13
+ id_column_name: str = "scorecard_tag"
14
+
15
+ backend = CortexBackend()
16
+
17
+ @classmethod
18
+ def destination_name(cls: type[Self]) -> str:
19
+ """Return the slug of the api endpoint for this model."""
20
+ return "scorecards"
21
+
22
+ scorecard_tag = String()
23
+ catalog_entity_tag = String()
24
+ is_archived = Boolean()
25
+ links = Json()
26
+ metadata = Json()
27
+ slack_channels = Json()
@@ -0,0 +1,69 @@
1
+ from typing import Self, cast
2
+
3
+ from clearskies import Model
4
+ from clearskies.columns import (
5
+ BelongsToModel,
6
+ Boolean,
7
+ CategoryTree,
8
+ CategoryTreeAncestors,
9
+ CategoryTreeChildren,
10
+ CategoryTreeDescendants,
11
+ Json,
12
+ String,
13
+ )
14
+
15
+ from clearskies_cortex.backends import CortexBackend
16
+ from clearskies_cortex.models import cortex_team_category_tree
17
+
18
+
19
+ class CortexTeam(Model):
20
+ """Model for teams."""
21
+
22
+ id_column_name: str = "team_tag"
23
+
24
+ backend = CortexBackend()
25
+
26
+ @classmethod
27
+ def destination_name(cls: type[Self]) -> str:
28
+ """Return the slug of the api endpoint for this model."""
29
+ return "teams"
30
+
31
+ team_tag = String()
32
+ catalog_entity_tag = String()
33
+ is_archived = Boolean()
34
+ parent_team_tag = CategoryTree(
35
+ cortex_team_category_tree.CortexTeamCategoryTree,
36
+ load_relatives_strategy="individual",
37
+ tree_child_id_column_name="child_team_tag",
38
+ tree_parent_id_column_name="parent_team_tag",
39
+ )
40
+ parent = BelongsToModel("parent_team_tag")
41
+ children = CategoryTreeChildren("parent_team_tag")
42
+ ancestors = CategoryTreeAncestors("parent_team_tag")
43
+ descendants = CategoryTreeDescendants("parent_team_tag")
44
+ links = Json()
45
+ metadata = Json()
46
+ slack_channels = Json()
47
+ type = String()
48
+ cortex_team = Json()
49
+ id = String()
50
+
51
+ def get_name(self) -> str:
52
+ """Retrieve name from metadata."""
53
+ return str(self.metadata.get("name", "")) if self.metadata else ""
54
+
55
+ def has_parents(self) -> bool:
56
+ """Check if team has parents. If not it's a top-level team."""
57
+ return len(self.ancestors) > 0
58
+
59
+ def has_childeren(self) -> bool:
60
+ """Check if team has child. If not it's a bottom-level team."""
61
+ return len(self.children) > 0
62
+
63
+ def find_top_level_team(self: Self) -> Self:
64
+ """
65
+ Find the top-level team of the team.
66
+
67
+ If team not has parents, return itself.
68
+ """
69
+ return self if not self.has_parents() else self.ancestors[0] # type: ignore[index]
@@ -0,0 +1,27 @@
1
+ import uuid
2
+ from collections import OrderedDict
3
+ from typing import Any, Iterator, Self
4
+
5
+ from clearskies import Model
6
+ from clearskies.columns import Boolean, Integer, String, Uuid
7
+
8
+ from clearskies_cortex.backends import CortexBackend, CortexTeamRelationshipBackend
9
+
10
+
11
+ class CortexTeamCategoryTree(Model):
12
+ """Model for teams."""
13
+
14
+ id_column_name: str = "id"
15
+
16
+ backend = CortexTeamRelationshipBackend(CortexBackend())
17
+
18
+ @classmethod
19
+ def destination_name(cls: type[Self]) -> str:
20
+ """Return the slug of the api endpoint for this model."""
21
+ return "teams/relationships"
22
+
23
+ id = Uuid()
24
+ parent_team_tag = String()
25
+ child_team_tag = String()
26
+ is_parent = Boolean()
27
+ level = Integer()
@@ -0,0 +1,25 @@
1
+ from collections import OrderedDict
2
+ from typing import Any, Self
3
+
4
+ from clearskies import Model
5
+ from clearskies.columns import Json, String
6
+
7
+ from clearskies_cortex.backends import CortexBackend
8
+
9
+
10
+ class CortexTeamDepartment(Model):
11
+ """Model for departments."""
12
+
13
+ backend = CortexBackend()
14
+ id_column_name: str = "department_tag"
15
+
16
+ @classmethod
17
+ def destination_name(cls: type[Self]) -> str:
18
+ """Return the slug of the api endpoint for this model."""
19
+ return "teams/departments"
20
+
21
+ department_tag = String()
22
+ catalog_entity_tag = String()
23
+ description = String()
24
+ name = String()
25
+ members = Json()