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.
- clear_skies_cortex-2.0.1.dist-info/METADATA +74 -0
- clear_skies_cortex-2.0.1.dist-info/RECORD +27 -0
- clear_skies_cortex-2.0.1.dist-info/WHEEL +4 -0
- clear_skies_cortex-2.0.1.dist-info/licenses/LICENSE +21 -0
- clearskies_cortex/__init__.py +8 -0
- clearskies_cortex/backends/__init__.py +4 -0
- clearskies_cortex/backends/cortex_backend.py +45 -0
- clearskies_cortex/backends/cortex_team_relationship_backend.py +174 -0
- clearskies_cortex/columns/__init__.py +3 -0
- clearskies_cortex/columns/string_list.py +25 -0
- clearskies_cortex/dataclasses.py +73 -0
- clearskies_cortex/defaults/__init__.py +7 -0
- clearskies_cortex/defaults/default_cortex_auth.py +9 -0
- clearskies_cortex/defaults/default_cortex_url.py +7 -0
- clearskies_cortex/models/__init__.py +21 -0
- clearskies_cortex/models/cortex_catalog_entity.py +62 -0
- clearskies_cortex/models/cortex_catalog_entity_domain.py +25 -0
- clearskies_cortex/models/cortex_catalog_entity_group.py +22 -0
- clearskies_cortex/models/cortex_catalog_entity_scorecard.py +33 -0
- clearskies_cortex/models/cortex_catalog_entity_service.py +92 -0
- clearskies_cortex/models/cortex_catalog_entity_types.py +25 -0
- clearskies_cortex/models/cortex_entity_relationships.py +25 -0
- clearskies_cortex/models/cortex_model.py +9 -0
- clearskies_cortex/models/cortex_scorecard.py +27 -0
- clearskies_cortex/models/cortex_team.py +69 -0
- clearskies_cortex/models/cortex_team_category_tree.py +27 -0
- clearskies_cortex/models/cortex_team_department.py +25 -0
|
@@ -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,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,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,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,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,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()
|