cognite-neat 1.0.20__py3-none-any.whl → 1.0.21__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.
@@ -1,4 +1,5 @@
1
1
  from .client import NeatClient
2
2
  from .config import NeatClientConfig
3
+ from .init.main import get_cognite_client
3
4
 
4
- __all__ = ["NeatClient", "NeatClientConfig"]
5
+ __all__ = ["NeatClient", "NeatClientConfig", "get_cognite_client"]
File without changes
@@ -0,0 +1,70 @@
1
+ from collections.abc import Callable
2
+
3
+ from cognite.client.credentials import CredentialProvider, OAuthClientCredentials, OAuthInteractive, Token
4
+
5
+ from cognite.neat._utils.text import humanize_collection
6
+
7
+ from .env_vars import ClientEnvironmentVariables, LoginFlow
8
+
9
+
10
+ def get_credentials(env_vars: ClientEnvironmentVariables) -> CredentialProvider:
11
+ options: dict[LoginFlow, Callable[[ClientEnvironmentVariables], CredentialProvider]] = {
12
+ "client_credentials": create_client_credentials,
13
+ "interactive": create_interactive_credentials,
14
+ "token": create_token_credentials,
15
+ "infer": create_infer_credentials,
16
+ }
17
+ return options[env_vars.LOGIN_FLOW](env_vars)
18
+
19
+
20
+ def create_client_credentials(env_vars: ClientEnvironmentVariables) -> CredentialProvider:
21
+ missing: list[str] = []
22
+ if not env_vars.IDP_CLIENT_ID:
23
+ missing.append("IDP_CLIENT_ID")
24
+ if not env_vars.IDP_CLIENT_SECRET:
25
+ missing.append("IDP_CLIENT_SECRET")
26
+ if env_vars.IDP_CLIENT_ID is None or env_vars.IDP_CLIENT_SECRET is None:
27
+ raise ValueError(
28
+ f"The following environment variables must be set for "
29
+ f"client credentials authentication: {humanize_collection(missing)}"
30
+ )
31
+
32
+ if env_vars.PROVIDER == "cdf":
33
+ return OAuthClientCredentials(
34
+ client_id=env_vars.IDP_CLIENT_ID,
35
+ client_secret=env_vars.IDP_CLIENT_SECRET,
36
+ token_url=env_vars.idp_token_url,
37
+ scopes=None, # type: ignore[arg-type]
38
+ )
39
+ return OAuthClientCredentials(
40
+ client_id=env_vars.IDP_CLIENT_ID,
41
+ client_secret=env_vars.IDP_CLIENT_SECRET,
42
+ token_url=env_vars.idp_token_url,
43
+ audience=env_vars.idp_audience,
44
+ scopes=env_vars.idp_scopes,
45
+ )
46
+
47
+
48
+ def create_interactive_credentials(env_vars: ClientEnvironmentVariables) -> CredentialProvider:
49
+ if not env_vars.IDP_CLIENT_ID:
50
+ raise ValueError("IDP_CLIENT_ID environment variable must be set for interactive authentication.")
51
+ return OAuthInteractive(
52
+ client_id=env_vars.IDP_CLIENT_ID,
53
+ authority_url=env_vars.idp_authority_url,
54
+ scopes=env_vars.idp_scopes,
55
+ )
56
+
57
+
58
+ def create_token_credentials(env_vars: ClientEnvironmentVariables) -> CredentialProvider:
59
+ if not env_vars.CDF_TOKEN:
60
+ raise ValueError("CDF_TOKEN environment variable must be set for token authentication.")
61
+ return Token(env_vars.CDF_TOKEN)
62
+
63
+
64
+ def create_infer_credentials(env_vars: ClientEnvironmentVariables) -> CredentialProvider:
65
+ if env_vars.IDP_CLIENT_SECRET:
66
+ return create_client_credentials(env_vars)
67
+ elif env_vars.CDF_TOKEN:
68
+ return create_token_credentials(env_vars)
69
+ else:
70
+ return create_interactive_credentials(env_vars)
@@ -0,0 +1,131 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import Any, Literal, TypeAlias, get_args
4
+
5
+ from pydantic import BaseModel, ConfigDict, ValidationError
6
+
7
+ from cognite.neat._utils.repo import get_repo_root
8
+ from cognite.neat._utils.validation import humanize_validation_error
9
+
10
+ if sys.version_info >= (3, 11):
11
+ from typing import Self
12
+ else:
13
+ from typing_extensions import Self
14
+
15
+ LoginFlow: TypeAlias = Literal["infer", "client_credentials", "interactive", "token"]
16
+ VALID_LOGIN_FLOWS = get_args(LoginFlow)
17
+ Provider: TypeAlias = Literal["entra_id", "auth0", "cdf", "other"]
18
+
19
+
20
+ class ClientEnvironmentVariables(BaseModel):
21
+ """Configuration for environment variables used by the NEAT client."""
22
+
23
+ model_config = ConfigDict(extra="forbid", frozen=True)
24
+ CDF_CLUSTER: str
25
+ CDF_PROJECT: str
26
+ PROVIDER: Provider = "entra_id"
27
+ LOGIN_FLOW: LoginFlow = "infer"
28
+
29
+ IDP_CLIENT_ID: str | None = None
30
+ IDP_CLIENT_SECRET: str | None = None
31
+ CDF_TOKEN: str | None = None
32
+
33
+ IDP_TENANT_ID: str | None = None
34
+ IDP_TOKEN_URL: str | None = None
35
+
36
+ CDF_URL: str | None = None
37
+ IDP_AUDIENCE: str | None = None
38
+ IDP_SCOPES: str | None = None
39
+ IDP_AUTHORITY_URL: str | None = None
40
+ IDP_DISCOVERY_URL: str | None = None
41
+ CDF_MAX_WORKERS: int | None = None
42
+ CDF_CLIENT_TIMEOUT: int | None = None
43
+ CDF_REDIRECT_PORT: int = 53_000
44
+
45
+ @classmethod
46
+ def create_humanize(cls, values: dict[str, Any]) -> Self:
47
+ try:
48
+ return cls.model_validate(values)
49
+ except ValidationError as e:
50
+ errors = [humanize_validation_error(error) for error in e.errors()]
51
+ raise ValueError("Invalid environment variable configuration:\n" + "\n - ".join(errors)) from e
52
+
53
+ @property
54
+ def idp_tenant_id(self) -> str:
55
+ if self.IDP_TENANT_ID:
56
+ return self.IDP_TENANT_ID
57
+ # This line is technically unreachable due to the checks in idp_token_url and idp_authority_url
58
+ raise RuntimeError("IDP_TENANT_ID is missing")
59
+
60
+ @property
61
+ def idp_token_url(self) -> str:
62
+ if self.PROVIDER == "cdf":
63
+ return "https://auth.cognite.com/oauth2/token"
64
+ if self.IDP_TOKEN_URL:
65
+ return self.IDP_TOKEN_URL
66
+ if self.PROVIDER == "entra_id" and self.IDP_TENANT_ID:
67
+ return f"https://login.microsoftonline.com/{self.IDP_TENANT_ID}/oauth2/v2.0/token"
68
+ alternative = " or provide IDP_TENANT_ID" if self.PROVIDER == "entra_id" else ""
69
+ raise ValueError(
70
+ f"IDP_TOKEN_URL is missing. Please provide it{alternative} in the environment variables.",
71
+ )
72
+
73
+ @property
74
+ def cdf_url(self) -> str:
75
+ return self.CDF_URL or f"https://{self.CDF_CLUSTER}.cognitedata.com"
76
+
77
+ @property
78
+ def idp_audience(self) -> str:
79
+ if self.IDP_AUDIENCE:
80
+ return self.IDP_AUDIENCE
81
+ if self.PROVIDER == "auth0":
82
+ return f"https://{self.CDF_PROJECT}.fusion.cognite.com/{self.CDF_PROJECT}"
83
+ else:
84
+ return f"https://{self.CDF_CLUSTER}.cognitedata.com"
85
+
86
+ @property
87
+ def idp_scopes(self) -> list[str]:
88
+ if self.IDP_SCOPES:
89
+ return self.IDP_SCOPES.split(",")
90
+ if self.PROVIDER == "auth0":
91
+ return ["IDENTITY", "user_impersonation"]
92
+ return [f"https://{self.CDF_CLUSTER}.cognitedata.com/.default"]
93
+
94
+ @property
95
+ def idp_authority_url(self) -> str:
96
+ if self.IDP_AUTHORITY_URL:
97
+ return self.IDP_AUTHORITY_URL
98
+ if self.PROVIDER == "entra_id" and self.IDP_TENANT_ID:
99
+ return f"https://login.microsoftonline.com/{self.IDP_TENANT_ID}"
100
+ alternative = " or provide IDP_TENANT_ID" if self.PROVIDER == "entra_id" else ""
101
+ raise ValueError(
102
+ f"IDP_AUTHORITY_URL is missing. Please provide it{alternative} in the environment variables.",
103
+ )
104
+
105
+
106
+ def get_environment_variables(env_file_name: str) -> ClientEnvironmentVariables:
107
+ to_search: list[tuple[str, Path]] = []
108
+ try:
109
+ repo_root = get_repo_root()
110
+ except RuntimeError:
111
+ ...
112
+ else:
113
+ to_search.append(("repository root", repo_root))
114
+ to_search.append(("current working directory", Path.cwd()))
115
+ for location_desc, path in to_search:
116
+ env_path = path / env_file_name
117
+ if env_path.is_file():
118
+ print(f"Found {env_file_name} in {location_desc}.")
119
+ return _parse_env_file(env_path)
120
+ raise FileNotFoundError(f"Could not find {env_file_name} in the repository root or current working directory.")
121
+
122
+
123
+ def _parse_env_file(env_file_path: Path) -> ClientEnvironmentVariables:
124
+ content = env_file_path.read_text()
125
+ variables: dict[str, Any] = {}
126
+ for line in content.splitlines():
127
+ if line.startswith("#") or "=" not in line:
128
+ continue
129
+ key, value = line.strip().split("=", 1)
130
+ variables[key] = value
131
+ return ClientEnvironmentVariables.create_humanize(variables)
@@ -0,0 +1,51 @@
1
+ from cognite.client import CogniteClient
2
+ from cognite.client.config import ClientConfig, global_config
3
+
4
+ from cognite.neat import _version
5
+
6
+ from .credentials import get_credentials
7
+ from .env_vars import ClientEnvironmentVariables, get_environment_variables
8
+
9
+ CLIENT_NAME = f"CogniteNeat:{_version.__version__}"
10
+
11
+
12
+ def get_cognite_client(env_file_name: str) -> CogniteClient:
13
+ """Get a CogniteClient using environment variables from a .env file."
14
+
15
+ Args:
16
+ env_file_name: The name of the .env file to look for in the repository root / current working directory. If
17
+ the file is found, the variables will be loaded from the file. If the file is not found, the user will
18
+ be prompted to enter the variables and the file will be created.
19
+
20
+ Returns:
21
+ CogniteClient: An instance of CogniteClient configured with the loaded environment variables.
22
+ """
23
+ try:
24
+ return get_cognite_client_internal(env_file_name)
25
+ except Exception as e:
26
+ raise RuntimeError(f"Failed to create client ❌: {e!s}") from None
27
+
28
+
29
+ def get_cognite_client_internal(env_file_name: str) -> CogniteClient:
30
+ # This function raises exceptions on failure
31
+ if not env_file_name.endswith(".env"):
32
+ raise ValueError(f"env_file_name must end with '.env'. Got: {env_file_name!r}")
33
+ global_config.disable_pypi_version_check = True
34
+ global_config.silence_feature_preview_warnings = True
35
+ env_vars = get_environment_variables(env_file_name)
36
+ client_config = create_client_config_from_env_vars(env_vars)
37
+ # Todo validate credentials by making a simple call to CDF
38
+ # Offer to store credentials securely if valid
39
+ #
40
+ return CogniteClient(client_config)
41
+
42
+
43
+ def create_client_config_from_env_vars(env_vars: ClientEnvironmentVariables) -> ClientConfig:
44
+ return ClientConfig(
45
+ client_name=CLIENT_NAME,
46
+ project=env_vars.CDF_PROJECT,
47
+ credentials=get_credentials(env_vars),
48
+ max_workers=env_vars.CDF_MAX_WORKERS,
49
+ timeout=env_vars.CDF_CLIENT_TIMEOUT,
50
+ base_url=env_vars.cdf_url,
51
+ )
@@ -1,6 +1,6 @@
1
1
  from pydantic import HttpUrl, RootModel, ValidationError
2
2
 
3
- from cognite.neat._v0.core._utils.auxiliary import local_import
3
+ from cognite.neat._utils.auxiliary import local_import
4
4
 
5
5
 
6
6
  class URI(RootModel[str]):
@@ -3,6 +3,8 @@ from collections import defaultdict
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Any, Literal
5
5
 
6
+ from pydantic import ValidationError
7
+
6
8
  from cognite.neat._data_model._constants import DEFAULT_MAX_LIST_SIZE, DEFAULT_MAX_LIST_SIZE_DIRECT_RELATIONS
7
9
  from cognite.neat._data_model.importers._table_importer.data_classes import (
8
10
  CREATOR_KEY,
@@ -12,8 +14,11 @@ from cognite.neat._data_model.importers._table_importer.data_classes import (
12
14
  DMSNode,
13
15
  DMSProperty,
14
16
  DMSView,
17
+ EntityTableFilter,
15
18
  MetadataValue,
19
+ RAWFilterTableFilter,
16
20
  TableDMS,
21
+ TableViewFilter,
17
22
  )
18
23
  from cognite.neat._data_model.models.dms import (
19
24
  ContainerPropertyDefinition,
@@ -23,7 +28,11 @@ from cognite.neat._data_model.models.dms import (
23
28
  DataType,
24
29
  DirectNodeRelation,
25
30
  EnumProperty,
31
+ EqualsFilterData,
32
+ Filter,
26
33
  FilterAdapter,
34
+ HasDataFilter,
35
+ InFilterData,
27
36
  ListablePropertyTypeDefinition,
28
37
  NodeReference,
29
38
  RequestSchema,
@@ -299,13 +308,60 @@ class DMSTableWriter:
299
308
  implements=[self._create_view_entity(parent) for parent in view.implements]
300
309
  if view.implements
301
310
  else None,
302
- filter=FilterAdapter.dump_json(view.filter, by_alias=True).decode(encoding="utf-8")
303
- if view.filter
304
- else None,
311
+ filter=self.write_view_filter(view.filter),
305
312
  )
306
313
  for view in views
307
314
  ]
308
315
 
316
+ def write_view_filter(self, filter: Filter | None) -> TableViewFilter | None:
317
+ if filter is None:
318
+ return None
319
+ filter_type, entities = self._get_entity_filter(filter)
320
+ if filter_type is not None and entities:
321
+ return EntityTableFilter(type=filter_type, entities=entities)
322
+ else:
323
+ return RAWFilterTableFilter(filter=FilterAdapter.dump_json(filter, by_alias=True).decode(encoding="utf-8"))
324
+
325
+ def _get_entity_filter(self, filter: Filter) -> tuple[Literal["nodeType", "hasData"] | None, list[ParsedEntity]]:
326
+ """If the filter is an entity-based filter (Equals or In on nodes), return the type and entities.
327
+ Otherwise, return (None, [])."""
328
+ if filter is None or len(filter) != 1:
329
+ return None, []
330
+ filter_name, body = next(iter(filter.items()))
331
+ if (
332
+ isinstance(body, EqualsFilterData)
333
+ and body.property == ["node", "type"]
334
+ and isinstance(body.value, dict)
335
+ and (node_reference := self._try_get_node_reference(body.value))
336
+ ):
337
+ return "nodeType", [self._create_node_entity(node_reference)]
338
+ elif (
339
+ isinstance(body, InFilterData)
340
+ and body.property == ["node", "type"]
341
+ and isinstance(body.values, list)
342
+ # All values must be node references
343
+ and len(node_references := [ref for value in body.values if (ref := self._try_get_node_reference(value))])
344
+ == len(body.values)
345
+ ):
346
+ return "nodeType", [self._create_node_entity(node) for node in node_references]
347
+ elif (
348
+ isinstance(body, HasDataFilter)
349
+ and
350
+ # All data must be container references, a single view reference makes it a raw filter
351
+ len(container_refs := [item for item in body.data if isinstance(item, ContainerReference)])
352
+ == len(body.data)
353
+ ):
354
+ return "hasData", [self._create_container_entity(item) for item in container_refs]
355
+ else:
356
+ return None, []
357
+
358
+ @staticmethod
359
+ def _try_get_node_reference(value: Any) -> NodeReference | None:
360
+ try:
361
+ return NodeReference.model_validate(value)
362
+ except ValidationError:
363
+ return None
364
+
309
365
  def write_view_properties(self, views: list[ViewRequest], container: ContainerProperties) -> ViewProperties:
310
366
  output = ViewProperties()
311
367
  for view in views:
@@ -1,4 +1,3 @@
1
- import json
2
1
  from collections.abc import Mapping
3
2
  from typing import Annotated, Literal, cast, get_args
4
3
 
@@ -18,11 +17,6 @@ from traitlets import Any
18
17
  from cognite.neat._data_model.models.entities import ParsedEntity, parse_entities, parse_entity
19
18
  from cognite.neat._utils.text import title_case
20
19
  from cognite.neat._utils.useful_types import CellValueType
21
- from cognite.neat._v0.core._data_model.models.entities import (
22
- HasDataFilter,
23
- NodeTypeFilter,
24
- RawFilter,
25
- )
26
20
 
27
21
  # This marker is used to identify creator in the description field.
28
22
  CREATOR_MARKER = "Creator: "
@@ -146,30 +140,59 @@ class DMSProperty(TableObj):
146
140
  return self
147
141
 
148
142
 
143
+ class EntityTableFilter(BaseModel):
144
+ """These are special formats that Neat Table format supports for filters."""
145
+
146
+ type: Literal["hasData", "nodeType"]
147
+ entities: EntityList
148
+
149
+ def __str__(self) -> str:
150
+ entities_str = ",".join(str(entity) for entity in self.entities)
151
+ return f"{self.type}({entities_str})"
152
+
153
+
154
+ class RAWFilterTableFilter(BaseModel):
155
+ """This is a generic filter that holds raw JSON filter."""
156
+
157
+ type: Literal["rawFilter"] = "rawFilter"
158
+ filter: str
159
+
160
+ def __str__(self) -> str:
161
+ return f"rawFilter({self.filter})"
162
+
163
+
164
+ def _parse_table_filter(v: str) -> dict[str, str] | EntityTableFilter | RAWFilterTableFilter:
165
+ if isinstance(v, EntityTableFilter | RAWFilterTableFilter):
166
+ return v
167
+ filter_configs = {
168
+ "hasdata(": ("hasData", "entities"),
169
+ "nodetype(": ("nodeType", "entities"),
170
+ "rawfilter(": ("rawFilter", "filter"),
171
+ }
172
+ v_lowered = v.casefold()
173
+ for prefix, (filter_type, field_name) in filter_configs.items():
174
+ if v_lowered.startswith(prefix) and v_lowered.endswith(")"):
175
+ return {"type": filter_type, field_name: v[len(prefix) : -1]}
176
+ # Fallback to raw filter with the whole string
177
+ return {"type": "rawFilter", "filter": v}
178
+
179
+
180
+ TableViewFilter = Annotated[
181
+ EntityTableFilter | RAWFilterTableFilter,
182
+ Field(discriminator="type"),
183
+ BeforeValidator(_parse_table_filter, str),
184
+ PlainSerializer(func=str),
185
+ ]
186
+
187
+
149
188
  class DMSView(TableObj):
150
189
  view: Entity
151
190
  name: str | None = None
152
191
  description: str | None = None
153
192
  implements: EntityList | None = None
154
- filter: str | None = None
193
+ filter: TableViewFilter | None = None
155
194
  in_model: bool | None = Field(None, exclude=True, description="Legacy column")
156
195
 
157
- @field_validator("filter", mode="after")
158
- def _legacy_filter(cls, value: str | None) -> str | None:
159
- if value is None:
160
- return value
161
-
162
- value_lower = value.lower()
163
-
164
- if value_lower.startswith("hasdata("):
165
- return json.dumps(HasDataFilter.load(value).as_dms_filter().dump())
166
- elif value_lower.startswith("nodetype("):
167
- return json.dumps(NodeTypeFilter.load(value).as_dms_filter().dump())
168
- elif value_lower.startswith("rawfilter("):
169
- return json.dumps(RawFilter.load(value).as_dms_filter().dump())
170
-
171
- return value
172
-
173
196
 
174
197
  class DMSContainer(TableObj):
175
198
  container: Entity
@@ -30,7 +30,19 @@ from cognite.neat._issues import ModelSyntaxError
30
30
  from cognite.neat._utils.text import humanize_collection
31
31
  from cognite.neat._utils.validation import ValidationContext, humanize_validation_error
32
32
 
33
- from .data_classes import CREATOR_KEY, CREATOR_MARKER, DMSContainer, DMSEnum, DMSNode, DMSProperty, DMSView, TableDMS
33
+ from .data_classes import (
34
+ CREATOR_KEY,
35
+ CREATOR_MARKER,
36
+ DMSContainer,
37
+ DMSEnum,
38
+ DMSNode,
39
+ DMSProperty,
40
+ DMSView,
41
+ EntityTableFilter,
42
+ RAWFilterTableFilter,
43
+ TableDMS,
44
+ TableViewFilter,
45
+ )
34
46
  from .source import TableSource
35
47
 
36
48
  T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
@@ -845,19 +857,6 @@ class DMSTableReader:
845
857
  views_requests: list[ViewRequest] = []
846
858
  rows_by_seen: dict[ParsedEntity, list[int]] = defaultdict(list)
847
859
  for row_no, view in enumerate(views):
848
- filter_dict: dict[str, Any] | None = None
849
- if view.filter is not None:
850
- try:
851
- filter_dict = json.loads(view.filter)
852
- except ValueError as e:
853
- self.errors.append(
854
- ModelSyntaxError(
855
- message=(
856
- f"In {self.source.location((self.Sheets.views, row_no, self.ViewColumns.filter))} "
857
- f"must be valid json. Got error {e!s}"
858
- )
859
- )
860
- )
861
860
  view_request = self._validate_obj(
862
861
  ViewRequest,
863
862
  dict(
@@ -865,7 +864,7 @@ class DMSTableReader:
865
864
  name=view.name,
866
865
  description=view.description,
867
866
  implements=[self._create_view_ref(impl) for impl in view.implements] if view.implements else None,
868
- filter=filter_dict,
867
+ filter=self._create_filter_dict(view.filter, row_no) if view.filter else None,
869
868
  properties=properties.get(view.view, {}),
870
869
  ),
871
870
  (self.Sheets.views, row_no),
@@ -891,6 +890,46 @@ class DMSTableReader:
891
890
  )
892
891
  return views_requests, set(rows_by_seen.keys())
893
892
 
893
+ def _create_filter_dict(self, filter: TableViewFilter, row_no: int) -> dict[str, Any] | None:
894
+ if isinstance(filter, RAWFilterTableFilter):
895
+ try:
896
+ return json.loads(filter.filter)
897
+ except ValueError as e:
898
+ self.errors.append(
899
+ ModelSyntaxError(
900
+ message=(
901
+ f"In {self.source.location((self.Sheets.views, row_no, self.ViewColumns.filter))} "
902
+ f"must be valid json. Got error {e!s}"
903
+ )
904
+ )
905
+ )
906
+ return None
907
+ elif isinstance(filter, EntityTableFilter):
908
+ return self._create_entity_filter_dict(filter)
909
+ else:
910
+ # This is unreachable due to validation of the TableViewFilter model.
911
+ raise RuntimeError(f"Unknown filter type {filter.__class__.__name__}")
912
+
913
+ def _create_entity_filter_dict(self, filter: EntityTableFilter) -> dict[str, Any]:
914
+ """Creates the filter dictionary from an EntityTableFilter."""
915
+ if filter.type == "hasData":
916
+ return {
917
+ "hasData": [{**self._create_container_ref(entity), "type": "container"} for entity in filter.entities]
918
+ }
919
+ elif filter.type == "nodeType":
920
+ if len(filter.entities) == 1:
921
+ return {"equals": {"property": ["node", "type"], "value": self._create_node_ref(filter.entities[0])}}
922
+ else:
923
+ return {
924
+ "in": {
925
+ "property": ["node", "type"],
926
+ "values": [self._create_node_ref(entity) for entity in filter.entities],
927
+ }
928
+ }
929
+ else:
930
+ # This is unreachable due to validation of the EntityTableFilter model.
931
+ raise RuntimeError(f"Unknown filter type {filter.__class__.__name__}")
932
+
894
933
  def read_data_model(self, tables: TableDMS, valid_view_entities: set[ParsedEntity]) -> DataModelRequest:
895
934
  data: dict[str, Any] = {
896
935
  **{meta.key: meta.value for meta in tables.metadata},
@@ -2,9 +2,9 @@ from collections import Counter
2
2
 
3
3
  from pydantic import Field, ValidationInfo, field_validator
4
4
 
5
+ from cognite.neat._data_model.models.entities import ConceptEntity
5
6
  from cognite.neat._data_model.models.entities._constants import PREFIX_PATTERN, SUFFIX_PATTERN, VERSION_PATTERN
6
7
  from cognite.neat._utils.text import humanize_collection
7
- from cognite.neat._v0.core._data_model.models.entities._single_value import ConceptEntity
8
8
 
9
9
  from ._base import ResourceMetadata
10
10
  from ._concept import Concept
@@ -3,7 +3,7 @@ from abc import ABC
3
3
  from pydantic import Field, field_validator
4
4
 
5
5
  from cognite.neat._data_model.models.dms._references import SpaceReference
6
- from cognite.neat._v0.core._utils.text import humanize_collection
6
+ from cognite.neat._utils.text import humanize_collection
7
7
 
8
8
  from ._base import APIResource, Resource, WriteableResource
9
9
  from ._constants import FORBIDDEN_SPACES, SPACE_FORMAT_PATTERN
@@ -1,6 +1,6 @@
1
1
  from pydantic import HttpUrl, RootModel, ValidationError
2
2
 
3
- from cognite.neat._v0.core._utils.auxiliary import local_import
3
+ from cognite.neat._utils.auxiliary import local_import
4
4
 
5
5
 
6
6
  class URI(RootModel[str]):
@@ -54,3 +54,15 @@ class UserInputError(NeatException):
54
54
 
55
55
  def __str__(self) -> str:
56
56
  return f"User input error: {self.message}"
57
+
58
+
59
+ class NeatImportError(NeatException, ImportError):
60
+ def __init__(self, module: str, neat_extra: str, functionality: str = "functionality") -> None:
61
+ message = (
62
+ f"The {functionality} requires {module}. You can include it in your neat "
63
+ f'installation with `pip install "cognite-neat[{neat_extra}]"`.'
64
+ )
65
+ super().__init__(message)
66
+
67
+ def __str__(self) -> str:
68
+ return self.args[0]
@@ -13,7 +13,6 @@ from cognite.neat._data_model.exporters import DMSExporter, DMSFileExporter
13
13
  from cognite.neat._data_model.exporters._api_exporter import DMSAPIExporter
14
14
  from cognite.neat._data_model.exporters._table_exporter.exporter import DMSTableExporter
15
15
  from cognite.neat._data_model.importers import DMSImporter, DMSTableImporter
16
- from cognite.neat._data_model.importers._api_importer import DMSAPIImporter
17
16
  from cognite.neat._data_model.models.dms import RequestSchema as PhysicalDataModel
18
17
  from cognite.neat._data_model.models.dms._limits import SchemaLimits
19
18
  from cognite.neat._exceptions import DataModelImportException
@@ -108,7 +107,7 @@ class NeatStore:
108
107
  raise RuntimeError("No successful physical data model read found in provenance.")
109
108
 
110
109
  # We do not want to modify the data model for API representations
111
- if not (change.agent == DMSAPIImporter.__name__ and isinstance(writer, DMSTableExporter)):
110
+ if not isinstance(writer, DMSTableExporter):
112
111
  return self.physical_data_model[-1]
113
112
 
114
113
  # This will handle data model that are partially and require to be converted to
@@ -1,8 +1,11 @@
1
+ import importlib
1
2
  import inspect
2
3
  from abc import ABC
4
+ from types import ModuleType
3
5
  from typing import TypeVar
4
6
 
5
7
  from cognite.neat import _version
8
+ from cognite.neat._exceptions import NeatImportError
6
9
 
7
10
 
8
11
  def get_current_neat_version() -> str:
@@ -37,3 +40,10 @@ def get_concrete_subclasses(base_cls: type[T_Cls], exclude_direct_abc_inheritanc
37
40
  seen.add(subclass)
38
41
  to_check.append(subclass)
39
42
  return subclasses
43
+
44
+
45
+ def local_import(module: str, extra: str) -> ModuleType:
46
+ try:
47
+ return importlib.import_module(module)
48
+ except ImportError as e:
49
+ raise NeatImportError(module.split(".")[0], extra) from e
@@ -0,0 +1,19 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def get_repo_root() -> Path:
6
+ """Get the root path of the git repository.
7
+
8
+ Raises:
9
+ RuntimeError: If git is not installed or the current directory is not in a git repository
10
+
11
+ """
12
+ try:
13
+ result = subprocess.run("git rev-parse --show-toplevel".split(), stdout=subprocess.PIPE)
14
+ except FileNotFoundError as e:
15
+ raise RuntimeError("Git is not installed or not found in PATH") from e
16
+ output = result.stdout.decode().strip()
17
+ if not output:
18
+ raise RuntimeError("Not in a git repository")
19
+ return Path(output)
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.0.20"
1
+ __version__ = "1.0.21"
2
2
  __engine__ = "^2.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 1.0.20
3
+ Version: 1.0.21
4
4
  Summary: Knowledge graph transformation
5
5
  Author: Nikola Vasiljevic, Anders Albert
6
6
  Author-email: Nikola Vasiljevic <nikola.vasiljevic@cognite.com>, Anders Albert <anders.albert@cognite.com>
@@ -1,11 +1,15 @@
1
1
  cognite/neat/__init__.py,sha256=4RJ6p29cDU_VwQ3sVsgiFdb817BKqKASp-lIgQeD6BI,300
2
- cognite/neat/_client/__init__.py,sha256=75Bh7eGhaN4sOt3ZcRzHl7pXaheu1z27kmTHeaI05vo,114
2
+ cognite/neat/_client/__init__.py,sha256=DIHMeggZ81rVJwO97E5PVP8AwVAhhLpjBQu7Ns3_xdc,178
3
3
  cognite/neat/_client/api.py,sha256=nbxCdWBXcTVM6MrQeT_VpB6ehfoI544JHPFq-ejQKCY,292
4
4
  cognite/neat/_client/client.py,sha256=h0HELAHiBFxMNInkDu4AzbgfEIXqeM0BqqnMBmXjgi0,903
5
5
  cognite/neat/_client/config.py,sha256=eIIdWaA13yncRP6X7vTYsTpmXmVcmkhZPv5oPnLUEVc,1484
6
6
  cognite/neat/_client/containers_api.py,sha256=7bVIlL5PwoAG5Bks1ortW_bCG8iTkFqFVyL05pdJ3Pw,5176
7
7
  cognite/neat/_client/data_classes.py,sha256=HYPsrAJGVCUmlWTSIxJgAnIHAOzcyDveMM6Z-cuA92M,1404
8
8
  cognite/neat/_client/data_model_api.py,sha256=ogVHOabQ3HTqWaaoiGClmbtYdP-pl6DPN2zmPdH5LWY,4253
9
+ cognite/neat/_client/init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ cognite/neat/_client/init/credentials.py,sha256=95JZFo-LoZO5CAWS18FplrpDvd6TRgG2SvBH0cAEug4,2751
11
+ cognite/neat/_client/init/env_vars.py,sha256=t0zf63TYA3jP_isFiV3rPG81AJ2v_1ULyRthNMKc15Q,4916
12
+ cognite/neat/_client/init/main.py,sha256=q9NA5WCXrtxF2HLAfXOu5UrTH9Ou6qlRoVI9XDtTbZQ,2067
9
13
  cognite/neat/_client/spaces_api.py,sha256=xHtSMt_2k2YwZ5_8kH2dfa7fWxQQrky7wra4Ar2jwqs,4111
10
14
  cognite/neat/_client/statistics_api.py,sha256=HcYb2nNC9M_iaI1xyjjLn2Cz1tcyu7BJeaqVps79tg4,773
11
15
  cognite/neat/_client/views_api.py,sha256=Qzk_wiLtaWszxCQFDBoWCH1yDc4GOEJsVOcL061rcK0,5639
@@ -13,7 +17,7 @@ cognite/neat/_config.py,sha256=ZvCkcaRVAvH4-ClvinoWaLWhRJpRByqdvncGFsf5gLk,9886
13
17
  cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
18
  cognite/neat/_data_model/_analysis.py,sha256=lPiXF0td4y5TudrR1KPH5OUZ_YNBz8tO6cLg3RnJwEg,22413
15
19
  cognite/neat/_data_model/_constants.py,sha256=E2axzdYjsIy7lTHsjW91wsv6r-pUwko8g6K8C_oRnxk,1707
16
- cognite/neat/_data_model/_identifiers.py,sha256=2l_bCtuE6TVZLCnzV7hhAUTP0kU6ji4QlIK-JhRK1fM,1922
20
+ cognite/neat/_data_model/_identifiers.py,sha256=lDLvMvYDgRNFgk5GmxWzOUunG7M3synAciNjzJI0m_o,1913
17
21
  cognite/neat/_data_model/_shared.py,sha256=H0gFqa8tKFNWuvdat5jL6OwySjCw3aQkLPY3wtb9Wrw,1302
18
22
  cognite/neat/_data_model/_snapshot.py,sha256=JBaKmL0Tmprz59SZ1JeB49BPMB8Hqa-OAOt0Bai8cw4,6305
19
23
  cognite/neat/_data_model/deployer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,20 +34,20 @@ cognite/neat/_data_model/exporters/_base.py,sha256=rG_qAU5i5Hh5hUMep2UmDFFZID4x3
30
34
  cognite/neat/_data_model/exporters/_table_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
35
  cognite/neat/_data_model/exporters/_table_exporter/exporter.py,sha256=4BPu_Chtjh1EyOaKbThXYohsqllVOkCbSoNekNZuBXc,5159
32
36
  cognite/neat/_data_model/exporters/_table_exporter/workbook.py,sha256=1Afk1WqeNe9tiNeSAm0HrF8jTQ1kTbIv1D9hMztKwO8,18482
33
- cognite/neat/_data_model/exporters/_table_exporter/writer.py,sha256=WAXPXjbIXrKtAKebXQ3npShbgWs_rUwdyQsCsm_jt3I,19374
37
+ cognite/neat/_data_model/exporters/_table_exporter/writer.py,sha256=k1gPrI8OdS75x3ncLn27Z-wnWgkOJ_eiCFnfp-rgdyY,21748
34
38
  cognite/neat/_data_model/importers/__init__.py,sha256=dHnKnC_AXk42z6wzEHK15dxIOh8xSEkuUf_AFRZls0E,193
35
39
  cognite/neat/_data_model/importers/_api_importer.py,sha256=H8Ow3Tt7utuAuBhC6s7yWvhGqunHAtE0r0XRsVAr6IE,7280
36
40
  cognite/neat/_data_model/importers/_base.py,sha256=NRB0FcEBj4GaethU68nRffBfTedBBA866A3zfJNfmiQ,433
37
41
  cognite/neat/_data_model/importers/_table_importer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=MU3gZg9w3OO0FCJfYdbfa4e8tSNPVhkPOzpJNab4jG4,10762
42
+ cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=wnFMrzLRWAsPyMBMZ7W02JLMtjMJRG0VGsXDrj_Ww5Y,11487
39
43
  cognite/neat/_data_model/importers/_table_importer/importer.py,sha256=lQ4_Gpv0haEwQEDYZJaxtR9dL6Y0ys9jbjFfWxH6s2o,8870
40
- cognite/neat/_data_model/importers/_table_importer/reader.py,sha256=I9-zHCpJLo7bj4BabAzSgNBDVUAocdhlvBfy95JkWRw,49451
44
+ cognite/neat/_data_model/importers/_table_importer/reader.py,sha256=5NkUvem9sR3WUM1vtfm6duu76oD3vQ0gi6UuBOcr3bo,50933
41
45
  cognite/neat/_data_model/importers/_table_importer/source.py,sha256=h7u5ur5oetmvBs3wgj7Ody5uPF21QwxeAceoIhJ5qzo,3300
42
46
  cognite/neat/_data_model/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
47
  cognite/neat/_data_model/models/conceptual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
48
  cognite/neat/_data_model/models/conceptual/_base.py,sha256=SFkoBJDM51pqew_isHFJoB20OgfofpwVRnTrg-rKkNY,710
45
49
  cognite/neat/_data_model/models/conceptual/_concept.py,sha256=0Pk4W2TJ_Y0Z7oPHpzely1kPXrAkmkyqw6a0n3il6LY,2248
46
- cognite/neat/_data_model/models/conceptual/_data_model.py,sha256=xRnKD1JFXscXAV_joitRdbjh9BTbME6z6IxS3fZNMZM,1696
50
+ cognite/neat/_data_model/models/conceptual/_data_model.py,sha256=zvJZi0OqOFMviwY84Am3Oz_RmF1g3tOzANFHlrldBuU,1673
47
51
  cognite/neat/_data_model/models/conceptual/_properties.py,sha256=CpF37vJYBTLT4DH4ZOu2U-JyWtkb_27V8fw52qiaE_k,4007
48
52
  cognite/neat/_data_model/models/conceptual/_property.py,sha256=blSZQxX52zaILAtjUkldPzPeysz7wnG-UGSNU5tacI8,4138
49
53
  cognite/neat/_data_model/models/dms/__init__.py,sha256=CW5NPMRrMyY4iyZgqYb8eZkRuwbbXUDSVNMWep3zEPI,5326
@@ -58,7 +62,7 @@ cognite/neat/_data_model/models/dms/_indexes.py,sha256=ZtXe8ABuRcsAwRIZ9FCanS3uw
58
62
  cognite/neat/_data_model/models/dms/_limits.py,sha256=x_X7T50SkwPNo_aHTGCr35hDXI8FRdZLYIB9HpFqnIk,3520
59
63
  cognite/neat/_data_model/models/dms/_references.py,sha256=Mx_nxfvOrvAx7nvebhhbFw6eRm3nHqeFW5P5AqADUlM,3890
60
64
  cognite/neat/_data_model/models/dms/_schema.py,sha256=2JFLcm52smzPdtZ69Lf02UbYAD8I_hpRbI7ZAzdxJJs,641
61
- cognite/neat/_data_model/models/dms/_space.py,sha256=3KvWg0bVuLpgQwhkDbsJ53ZMMmK0cKUgfyDRrSrERko,1904
65
+ cognite/neat/_data_model/models/dms/_space.py,sha256=mj6gID4vcAGsHNtgfXm4_4FMOQbUOkMd3HaYEdy07XM,1895
62
66
  cognite/neat/_data_model/models/dms/_types.py,sha256=5-cgC53AG186OZUqkltv7pMjcGNLuH7Etbn8IUcgk1c,447
63
67
  cognite/neat/_data_model/models/dms/_view_filter.py,sha256=XxMffUH5kYtcg0xHgyUsY4nueWRoJu2CoJtOU7wbH4Y,11274
64
68
  cognite/neat/_data_model/models/dms/_view_property.py,sha256=nJBPmw4KzJOdaQmvRfCE3A4FL-E13OsNUEufI64vLKo,9271
@@ -67,7 +71,7 @@ cognite/neat/_data_model/models/entities/__init__.py,sha256=7dDyES7fYl9LEREal59F
67
71
  cognite/neat/_data_model/models/entities/_base.py,sha256=PaNrD29iwxuqTpRWbmESMTxRhhKXmRyDF_cLZEC69dg,3927
68
72
  cognite/neat/_data_model/models/entities/_constants.py,sha256=EK9Bus8UgFgxK5cVFMTAqWSl6aWkDe7d59hpUmlHlBs,517
69
73
  cognite/neat/_data_model/models/entities/_data_types.py,sha256=DfdEWGek7gODro-_0SiiInhPGwul4zn-ASACQfn8HUY,2838
70
- cognite/neat/_data_model/models/entities/_identifiers.py,sha256=Ab_cMPbk5b0tKnivkm5pHKLv3cLb5KrwhxDu2elStvQ,1924
74
+ cognite/neat/_data_model/models/entities/_identifiers.py,sha256=a7ojJKY1ErZgUANHscEwkctX4RJ7bWEEWOQt5g5Tsdk,1915
71
75
  cognite/neat/_data_model/models/entities/_parser.py,sha256=zef_pSDZYMZrJl4IKreFDR577KutfhtN1xpH3Ayjt2o,7669
72
76
  cognite/neat/_data_model/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
77
  cognite/neat/_data_model/validation/dms/__init__.py,sha256=kKD18-Bg_G-w11Cs7Wv_TKV0C_q62Pm2RKLpOz27ar4,2642
@@ -79,7 +83,7 @@ cognite/neat/_data_model/validation/dms/_containers.py,sha256=5Lka1Cg-SaP9Ued0ck
79
83
  cognite/neat/_data_model/validation/dms/_limits.py,sha256=U7z8sN-kAyJsF5hYHPNBBg25Fvz1F8njhzYVSQOIiOU,14779
80
84
  cognite/neat/_data_model/validation/dms/_orchestrator.py,sha256=qiuUSUmNhekFyBARUUO2yhG-X9AeU_LL49UrJ65JXFA,2964
81
85
  cognite/neat/_data_model/validation/dms/_views.py,sha256=Q0x7jdG69-AVc93VrwdZ1_rFHpq-I-OG98puM4lcweE,5068
82
- cognite/neat/_exceptions.py,sha256=ox-5hXpee4UJlPE7HpuEHV2C96aLbLKo-BhPDoOAzhA,1650
86
+ cognite/neat/_exceptions.py,sha256=mO19TEecZYDNqSvzuc6JmCLFQ70eniT1-Gb0AEbgbzE,2090
83
87
  cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
84
88
  cognite/neat/_session/__init__.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
85
89
  cognite/neat/_session/_html/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -113,16 +117,17 @@ cognite/neat/_state_machine/_base.py,sha256=-ZpeAhM6l6N6W70dET25tAzOxaaK5aa474ea
113
117
  cognite/neat/_state_machine/_states.py,sha256=nmj4SmunpDYcBsNx8A284xnXGS43wuUuWpMMORha2DE,1170
114
118
  cognite/neat/_store/__init__.py,sha256=TvM9CcFbtOSrxydPAuJi6Bv_iiGard1Mxfx42ZFoTl0,55
115
119
  cognite/neat/_store/_provenance.py,sha256=1zzRDWjR9twZu2jVyIG3UdYdIXtQKJ7uF8a0hV7LEuA,3368
116
- cognite/neat/_store/_store.py,sha256=PwW4hYQ-ENbCtKpGdGmsJjk0qGU4PPg0LIfhn5nNGSI,9751
120
+ cognite/neat/_store/_store.py,sha256=jtJPBQ8PBG0UlgzSJJpzOIRkC2Np3fHNtV4omRC6H5A,9629
117
121
  cognite/neat/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
122
  cognite/neat/_utils/_reader.py,sha256=9dXrODNNqWU0Gx1zXjRTOiiByFuDZlpQkQEzx3HAxYQ,5390
119
- cognite/neat/_utils/auxiliary.py,sha256=Cx-LP8dfN782R3iUcm--q26zdzQ0k_RFnVbJ0bwVZMI,1345
123
+ cognite/neat/_utils/auxiliary.py,sha256=YQMpqCxccex_slmLYrR5icVX9aeLbD793ou7IrbNTFs,1654
120
124
  cognite/neat/_utils/collection.py,sha256=BIwRrFbUXNPvHhEVujLHgVoDJXzdPEMScrbSBhyCibk,446
121
125
  cognite/neat/_utils/http_client/__init__.py,sha256=qaCLLLhi7H3b_cmbknX0S66KILT7JSKX1YSgZjNdd1U,786
122
126
  cognite/neat/_utils/http_client/_client.py,sha256=TO9C77LcsqX0R3Fu-mP560nnV6rP5oRXki9kxRYtBlg,9658
123
127
  cognite/neat/_utils/http_client/_config.py,sha256=C8IF1JoijmVMjA_FEMgAkiD1buEV1cY5Og3t-Ecyfmk,756
124
128
  cognite/neat/_utils/http_client/_data_classes.py,sha256=McyCQZUcwkbOXdBmc99DdNsO6mQz8dNVsepDvXmaINA,10138
125
129
  cognite/neat/_utils/http_client/_tracker.py,sha256=EBBnd-JZ7nc_jYNFJokCHN2UZ9sx0McFLZvlceUYYic,1215
130
+ cognite/neat/_utils/repo.py,sha256=nSpT9_XNbbExMip4LyNL0b89aAurHVx003q_Wky-qww,603
126
131
  cognite/neat/_utils/text.py,sha256=-ujNaG_hLkdurKsUmZB9ZI_kJkddlCKEf8g-g_XCk10,2010
127
132
  cognite/neat/_utils/useful_types.py,sha256=BwTjcWnpxnxN8rWXuYXMgU55O_YjVteMtYK0y25OmH0,1260
128
133
  cognite/neat/_utils/validation.py,sha256=U422V0TY5KujFJFyfhRLONVj5A4AcCWgqIKVK6BUm7M,6938
@@ -316,9 +321,9 @@ cognite/neat/_v0/session/_template.py,sha256=BNcvrW5y7LWzRM1XFxZkfR1Nc7e8UgjBClH
316
321
  cognite/neat/_v0/session/_to.py,sha256=AnsRSDDdfFyYwSgi0Z-904X7WdLtPfLlR0x1xsu_jAo,19447
317
322
  cognite/neat/_v0/session/_wizard.py,sha256=baPJgXAAF3d1bn4nbIzon1gWfJOeS5T43UXRDJEnD3c,1490
318
323
  cognite/neat/_v0/session/exceptions.py,sha256=jv52D-SjxGfgqaHR8vnpzo0SOJETIuwbyffSWAxSDJw,3495
319
- cognite/neat/_version.py,sha256=sA2ZsS8D9yHBhDKFo6czh0Y817b0hccxIxB45Dzk75o,45
324
+ cognite/neat/_version.py,sha256=0Bv-euqjHzX0kdwCv6YRDD5BAUEFBhkf7f6PaEKGdd0,45
320
325
  cognite/neat/legacy.py,sha256=eI2ecxOV8ilGHyLZlN54ve_abtoK34oXognkFv3yvF0,219
321
326
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
322
- cognite_neat-1.0.20.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
323
- cognite_neat-1.0.20.dist-info/METADATA,sha256=-nVza3PgQsgHSNGru9JTxml_7T4k4UqxhQo4hrbMMG0,6689
324
- cognite_neat-1.0.20.dist-info/RECORD,,
327
+ cognite_neat-1.0.21.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
328
+ cognite_neat-1.0.21.dist-info/METADATA,sha256=QpA63SYiPB_pttmYhdDpmx7AbJl98Sd8V-wIbjoWCwI,6689
329
+ cognite_neat-1.0.21.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.24
2
+ Generator: uv 0.9.25
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any