cognite-neat 1.0.24__py3-none-any.whl → 1.0.26__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,133 @@
1
+ import importlib.util
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from .env_vars import AVAILABLE_LOGIN_FLOWS, AVAILABLE_PROVIDERS, LoginFlow, Provider, create_env_file_content
7
+
8
+
9
+ class InteractiveFlow(ABC):
10
+ def __init__(self, env_path: Path):
11
+ self.env_path = env_path
12
+
13
+ @abstractmethod
14
+ def run(self) -> None: ...
15
+
16
+ def create_env_file(self, provider: Provider, login_flow: LoginFlow) -> None:
17
+ env_content = create_env_file_content(provider, login_flow)
18
+ self.env_path.write_text(env_content, encoding="utf-8", newline="\n")
19
+
20
+
21
+ def get_interactive_flow(env_file_path: Path) -> InteractiveFlow:
22
+ try:
23
+ importlib.util.find_spec("IPython")
24
+ importlib.util.find_spec("ipywidgets")
25
+ if not _is_in_notebook():
26
+ return NoDependencyFlow(env_file_path)
27
+ return NotebookFlow(env_file_path)
28
+ except ImportError:
29
+ return NoDependencyFlow(env_file_path)
30
+
31
+
32
+ def _is_in_notebook() -> bool:
33
+ try:
34
+ from IPython import get_ipython
35
+
36
+ if "IPKernelApp" not in get_ipython().config:
37
+ return False
38
+ except ImportError:
39
+ return False
40
+ except AttributeError:
41
+ return False
42
+ return True
43
+
44
+
45
+ class NoDependencyFlow(InteractiveFlow):
46
+ def run(self) -> None:
47
+ if not self.should_create_env_file():
48
+ return None
49
+ provider = self.provider()
50
+ login_flow: LoginFlow
51
+ if provider != "cdf":
52
+ login_flow = self.login_flow()
53
+ else:
54
+ login_flow = "client_credentials"
55
+ self.create_env_file(provider, login_flow)
56
+ print(f"Created environment file at {self.env_path!r}.")
57
+ return None
58
+
59
+ def should_create_env_file(self) -> bool:
60
+ env_file_name = self.env_path.name
61
+ answer = input(
62
+ f"Would you like to create a new {env_file_name!r} file with the required environment variables? [y/N]: "
63
+ )
64
+ return answer.strip().lower() == "y"
65
+
66
+ @classmethod
67
+ def provider(cls) -> Provider:
68
+ index = cls._prompt_choice(AVAILABLE_PROVIDERS, "Select provider:")
69
+ return AVAILABLE_PROVIDERS[index]
70
+
71
+ @classmethod
72
+ def login_flow(cls) -> LoginFlow:
73
+ index = cls._prompt_choice(AVAILABLE_LOGIN_FLOWS, "Select login flow:")
74
+ return AVAILABLE_LOGIN_FLOWS[index]
75
+
76
+ @classmethod
77
+ def _prompt_choice(cls, options: tuple[str, ...], prompt: str) -> int:
78
+ for i, option in enumerate(options, start=1):
79
+ print(f"{i}. {option}")
80
+ question = f"{prompt} [1-{len(options)}]: "
81
+ while True:
82
+ answer = input(question)
83
+ if answer.isdigit():
84
+ index = int(answer)
85
+ if 1 <= index <= len(options):
86
+ return index - 1
87
+ print(f"Invalid input. Please enter a number between 1 and {len(options)}.")
88
+
89
+
90
+ class NotebookFlow(InteractiveFlow):
91
+ def __init__(self, env_path: Path):
92
+ super().__init__(env_path)
93
+ import ipywidgets as widgets # type: ignore[import-untyped]
94
+ from IPython.display import display
95
+
96
+ self._widgets = widgets
97
+ self._display = display
98
+
99
+ def run(self) -> None:
100
+ dropdown_providers = self._widgets.Dropdown(
101
+ options=list(AVAILABLE_PROVIDERS),
102
+ value=AVAILABLE_PROVIDERS[0],
103
+ description="Provider:",
104
+ )
105
+ dropdown_login_flows = self._widgets.Dropdown(
106
+ options=list(AVAILABLE_LOGIN_FLOWS),
107
+ value=AVAILABLE_LOGIN_FLOWS[0],
108
+ description="Login Flow:",
109
+ )
110
+ confirm_button = self._widgets.Button(description="Create template .env file", button_style="primary")
111
+ dropdowns = self._widgets.HBox([dropdown_providers, dropdown_login_flows])
112
+ output = self._widgets.Output()
113
+ container = self._widgets.VBox([dropdowns, confirm_button, output])
114
+
115
+ self._display(container)
116
+
117
+ def on_confirm_clicked(b: Any) -> None:
118
+ with output:
119
+ provider = dropdown_providers.value
120
+ login_flow = dropdown_login_flows.value
121
+ if provider == "cdf" and login_flow != "client_credentials":
122
+ print(
123
+ "Warning: 'cdf' provider only supports 'client_credentials' login flow. Overriding selection."
124
+ )
125
+ login_flow = "client_credentials"
126
+ is_existing = self.env_path.exists()
127
+ self.create_env_file(provider, login_flow)
128
+ if is_existing:
129
+ print(f"Overwrote existing environment file at {self.env_path!r}.")
130
+ else:
131
+ print(f"Created environment file at {self.env_path!r}.")
132
+
133
+ confirm_button.on_click(on_confirm_clicked)
@@ -1,15 +1,19 @@
1
+ from pathlib import Path
2
+
1
3
  from cognite.client import CogniteClient
2
4
  from cognite.client.config import ClientConfig, global_config
3
5
 
4
6
  from cognite.neat import _version
7
+ from cognite.neat._utils.repo import get_repo_root
5
8
 
6
9
  from .credentials import get_credentials
7
- from .env_vars import ClientEnvironmentVariables, get_environment_variables
10
+ from .env_vars import ClientEnvironmentVariables, parse_env_file
11
+ from .interactive import get_interactive_flow
8
12
 
9
13
  CLIENT_NAME = f"CogniteNeat:{_version.__version__}"
10
14
 
11
15
 
12
- def get_cognite_client(env_file_name: str) -> CogniteClient:
16
+ def get_cognite_client(env_file_name: str) -> CogniteClient | None:
13
17
  """Get a CogniteClient using environment variables from a .env file."
14
18
 
15
19
  Args:
@@ -20,24 +24,30 @@ def get_cognite_client(env_file_name: str) -> CogniteClient:
20
24
  Returns:
21
25
  CogniteClient: An instance of CogniteClient configured with the loaded environment variables.
22
26
  """
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
27
  # This function raises exceptions on failure
31
28
  if not env_file_name.endswith(".env"):
32
29
  raise ValueError(f"env_file_name must end with '.env'. Got: {env_file_name!r}")
33
30
  global_config.disable_pypi_version_check = True
34
31
  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)
32
+
33
+ repo_root = get_repo_root()
34
+ if repo_root and (env_path := repo_root / env_file_name).exists():
35
+ print(f"Found {env_file_name} in repository root.")
36
+ elif (env_path := Path.cwd() / env_file_name).exists():
37
+ print(f"Found {env_file_name} in current working directory.")
38
+
39
+ if env_path.exists():
40
+ env_vars = parse_env_file(env_path)
41
+ client_config = create_client_config_from_env_vars(env_vars)
42
+ return CogniteClient(client_config)
43
+ print(f"Failed to find {env_file_name} in repository root or current working directory.")
44
+
45
+ env_folder = repo_root if repo_root is not None else Path.cwd()
46
+ new_env_path = env_folder / env_file_name
47
+ flow = get_interactive_flow(new_env_path)
48
+ flow.run()
49
+ print("Could not create CogniteClient because no environment file was found.")
50
+ return None
41
51
 
42
52
 
43
53
  def create_client_config_from_env_vars(env_vars: ClientEnvironmentVariables) -> ClientConfig:
@@ -1,18 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- from cognite.neat._data_model.models.dms import DataModelBody, SpaceRequest, SpaceResponse
3
+ from collections.abc import Sequence
4
+
5
+ from cognite.neat._data_model.models.dms import SpaceRequest, SpaceResponse
4
6
  from cognite.neat._data_model.models.dms._references import SpaceReference
5
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
6
- from cognite.neat._utils.useful_types import PrimitiveType
7
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
7
8
 
8
- from .api import NeatAPI
9
+ from .api import Endpoint, NeatAPI
10
+ from .config import NeatClientConfig
9
11
  from .data_classes import PagedResponse
12
+ from .filters import DataModelingFilter
10
13
 
11
14
 
12
15
  class SpacesAPI(NeatAPI):
13
- ENDPOINT = "/models/spaces"
16
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
17
+ super().__init__(
18
+ neat_config,
19
+ http_client,
20
+ endpoint_map={
21
+ "apply": Endpoint("POST", "/models/spaces", item_limit=100),
22
+ "retrieve": Endpoint("POST", "/models/spaces/byids", item_limit=100),
23
+ "delete": Endpoint("POST", "/models/spaces/delete", item_limit=100),
24
+ "list": Endpoint("GET", "/models/spaces", item_limit=1000),
25
+ },
26
+ )
27
+
28
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[SpaceResponse]:
29
+ return PagedResponse[SpaceResponse].model_validate_json(response.body)
30
+
31
+ def _validate_id_response(self, response: SuccessResponse) -> list[SpaceReference]:
32
+ return PagedResponse[SpaceReference].model_validate_json(response.body).items
14
33
 
15
- def apply(self, spaces: list[SpaceRequest]) -> list[SpaceResponse]:
34
+ def apply(self, spaces: Sequence[SpaceRequest]) -> list[SpaceResponse]:
16
35
  """Apply (create or update) spaces in CDF.
17
36
 
18
37
  Args:
@@ -20,20 +39,7 @@ class SpacesAPI(NeatAPI):
20
39
  Returns:
21
40
  List of SpaceResponse objects.
22
41
  """
23
- if not spaces:
24
- return []
25
- if len(spaces) > 100:
26
- raise ValueError("Cannot apply more than 100 spaces at once.")
27
- result = self._http_client.request_with_retries(
28
- ItemsRequest(
29
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
30
- method="POST",
31
- body=DataModelBody(items=spaces),
32
- )
33
- )
34
- result.raise_for_status()
35
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
36
- return result.items
42
+ return self._request_item_response(spaces, "apply")
37
43
 
38
44
  def retrieve(self, spaces: list[SpaceReference]) -> list[SpaceResponse]:
39
45
  """Retrieve spaces by their identifiers.
@@ -44,21 +50,7 @@ class SpacesAPI(NeatAPI):
44
50
  Returns:
45
51
  List of SpaceResponse objects.
46
52
  """
47
- if not spaces:
48
- return []
49
- if len(spaces) > 1000:
50
- raise ValueError("Cannot retrieve more than 1000 spaces at once.")
51
-
52
- result = self._http_client.request_with_retries(
53
- ItemsRequest(
54
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
55
- method="POST",
56
- body=ItemIDBody(items=spaces),
57
- )
58
- )
59
- result.raise_for_status()
60
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
61
- return result.items
53
+ return self._request_item_response(spaces, "retrieve")
62
54
 
63
55
  def delete(self, spaces: list[SpaceReference]) -> list[SpaceReference]:
64
56
  """Delete spaces by their identifiers.
@@ -68,48 +60,21 @@ class SpacesAPI(NeatAPI):
68
60
  Returns:
69
61
  List of SpaceReference objects representing the deleted spaces.
70
62
  """
71
- if not spaces:
72
- return []
73
- if len(spaces) > 100:
74
- raise ValueError("Cannot delete more than 100 spaces at once.")
75
- result = self._http_client.request_with_retries(
76
- ItemsRequest(
77
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
78
- method="POST",
79
- body=ItemIDBody(items=spaces),
80
- )
81
- )
82
- result.raise_for_status()
83
- result = PagedResponse[SpaceReference].model_validate_json(result.success_response.body)
84
- return result.items
63
+ return self._request_id_response(spaces, "delete")
85
64
 
86
65
  def list(
87
66
  self,
88
67
  include_global: bool = False,
89
- limit: int = 10,
68
+ limit: int | None = 10,
90
69
  ) -> list[SpaceResponse]:
91
70
  """List spaces in CDF Project.
92
71
 
93
72
  Args:
94
73
  include_global: If True, include global spaces.
95
- limit: Maximum number of spaces to return. Max is 1000.
74
+ limit: Maximum number of spaces to return. If None, return all spaces.
96
75
 
97
76
  Returns:
98
77
  List of SpaceResponse objects.
99
78
  """
100
- if limit > 1000:
101
- raise ValueError("Pagination is not (yet) supported for listing spaces. The maximum limit is 1000.")
102
- parameters: dict[str, PrimitiveType] = {
103
- "includeGlobal": include_global,
104
- "limit": limit,
105
- }
106
- result = self._http_client.request_with_retries(
107
- ParametersRequest(
108
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
109
- method="GET",
110
- parameters=parameters,
111
- )
112
- )
113
- result.raise_for_status()
114
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
115
- return result.items
79
+ filter = DataModelingFilter(include_global=include_global)
80
+ return self._list(limit=limit, params=filter.dump())
@@ -1,9 +1,14 @@
1
- from cognite.neat._client.api import NeatAPI
2
- from cognite.neat._client.data_classes import StatisticsResponse
3
- from cognite.neat._utils.http_client import ParametersRequest
1
+ from cognite.neat._utils.http_client import HTTPClient, ParametersRequest
4
2
 
3
+ from .config import NeatClientConfig
4
+ from .data_classes import StatisticsResponse
5
+
6
+
7
+ class StatisticsAPI:
8
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
9
+ self._config = neat_config
10
+ self._http_client = http_client
5
11
 
6
- class StatisticsAPI(NeatAPI):
7
12
  def project(self) -> StatisticsResponse:
8
13
  """Retrieve project-wide usage data and limits.
9
14
 
@@ -2,46 +2,45 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
4
 
5
- from cognite.neat._data_model.models.dms import DataModelBody, ViewReference, ViewRequest, ViewResponse
6
- from cognite.neat._utils.collection import chunker_sequence
7
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
8
- from cognite.neat._utils.useful_types import PrimitiveType
5
+ from cognite.neat._data_model.models.dms import ViewReference, ViewRequest, ViewResponse
6
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
9
7
 
10
- from .api import NeatAPI
8
+ from .api import Endpoint, NeatAPI
9
+ from .config import NeatClientConfig
11
10
  from .data_classes import PagedResponse
11
+ from .filters import ViewFilter
12
12
 
13
13
 
14
14
  class ViewsAPI(NeatAPI):
15
- ENDPOINT = "/models/views"
16
- LIST_REQUEST_LIMIT = 1000
15
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
16
+ super().__init__(
17
+ neat_config,
18
+ http_client,
19
+ endpoint_map={
20
+ "apply": Endpoint("POST", "/models/views", item_limit=100),
21
+ "retrieve": Endpoint("POST", "/models/views/byids", item_limit=100),
22
+ "delete": Endpoint("POST", "/models/views/delete", item_limit=100),
23
+ "list": Endpoint("GET", "/models/views", item_limit=1000),
24
+ },
25
+ )
26
+
27
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[ViewResponse]:
28
+ return PagedResponse[ViewResponse].model_validate_json(response.body)
29
+
30
+ def _validate_id_response(self, response: SuccessResponse) -> list[ViewReference]:
31
+ return PagedResponse[ViewReference].model_validate_json(response.body).items
17
32
 
18
33
  def apply(self, items: Sequence[ViewRequest]) -> list[ViewResponse]:
19
34
  """Create or update views in CDF Project.
35
+
20
36
  Args:
21
37
  items: List of ViewRequest objects to create or update.
22
38
  Returns:
23
39
  List of ViewResponse objects.
24
40
  """
25
- if not items:
26
- return []
27
- if len(items) > 100:
28
- raise ValueError("Cannot apply more than 100 views at once.")
29
- result = self._http_client.request_with_retries(
30
- ItemsRequest(
31
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
32
- method="POST",
33
- body=DataModelBody(items=items),
34
- )
35
- )
36
- result.raise_for_status()
37
- result = PagedResponse[ViewResponse].model_validate_json(result.success_response.body)
38
- return result.items
41
+ return self._request_item_response(items, "apply")
39
42
 
40
- def retrieve(
41
- self,
42
- items: list[ViewReference],
43
- include_inherited_properties: bool = True,
44
- ) -> list[ViewResponse]:
43
+ def retrieve(self, items: list[ViewReference], include_inherited_properties: bool = True) -> list[ViewResponse]:
45
44
  """Retrieve views by their identifiers.
46
45
 
47
46
  Args:
@@ -51,42 +50,20 @@ class ViewsAPI(NeatAPI):
51
50
  Returns:
52
51
  List of ViewResponse objects.
53
52
  """
54
- results: list[ViewResponse] = []
55
- for chunk in chunker_sequence(items, 100):
56
- batch = self._http_client.request_with_retries(
57
- ItemsRequest(
58
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
59
- method="POST",
60
- body=ItemIDBody(items=chunk),
61
- parameters={"includeInheritedProperties": include_inherited_properties},
62
- )
63
- )
64
- batch.raise_for_status()
65
- result = PagedResponse[ViewResponse].model_validate_json(batch.success_response.body)
66
- results.extend(result.items)
67
- return results
53
+ return self._request_item_response(
54
+ items, "retrieve", extra_body={"includeInheritedProperties": include_inherited_properties}
55
+ )
68
56
 
69
57
  def delete(self, items: list[ViewReference]) -> list[ViewReference]:
70
58
  """Delete views by their identifiers.
71
59
 
72
60
  Args:
73
61
  items: List of (space, external_id, version) tuples identifying the views to delete.
62
+
63
+ Returns:
64
+ List of ViewReference objects representing the deleted views.
74
65
  """
75
- if not items:
76
- return []
77
- if len(items) > 100:
78
- raise ValueError("Cannot delete more than 100 views at once.")
79
-
80
- result = self._http_client.request_with_retries(
81
- ItemsRequest(
82
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
83
- method="POST",
84
- body=ItemIDBody(items=items),
85
- )
86
- )
87
- result.raise_for_status()
88
- result = PagedResponse[ViewReference].model_validate_json(result.success_response.body)
89
- return result.items
66
+ return self._request_id_response(items, "delete")
90
67
 
91
68
  def list(
92
69
  self,
@@ -108,37 +85,10 @@ class ViewsAPI(NeatAPI):
108
85
  Returns:
109
86
  List of ViewResponse objects.
110
87
  """
111
- if limit is not None and limit < 0:
112
- raise ValueError("Limit must be non-negative.")
113
- elif limit is not None and limit == 0:
114
- return []
115
- parameters: dict[str, PrimitiveType] = {
116
- "allVersions": all_versions,
117
- "includeInheritedProperties": include_inherited_properties,
118
- "includeGlobal": include_global,
119
- }
120
- if space is not None:
121
- parameters["space"] = space
122
- cursor: str | None = None
123
- view_responses: list[ViewResponse] = []
124
- while True:
125
- if cursor is not None:
126
- parameters["cursor"] = cursor
127
- if limit is None:
128
- parameters["limit"] = self.LIST_REQUEST_LIMIT
129
- else:
130
- parameters["limit"] = min(self.LIST_REQUEST_LIMIT, limit - len(view_responses))
131
- result = self._http_client.request_with_retries(
132
- ParametersRequest(
133
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
134
- method="GET",
135
- parameters=parameters,
136
- )
137
- )
138
- result.raise_for_status()
139
- result = PagedResponse[ViewResponse].model_validate_json(result.success_response.body)
140
- view_responses.extend(result.items)
141
- cursor = result.next_cursor
142
- if cursor is None or (limit is not None and len(view_responses) >= limit):
143
- break
144
- return view_responses
88
+ filter = ViewFilter(
89
+ space=space,
90
+ all_versions=all_versions,
91
+ include_inherited_properties=include_inherited_properties,
92
+ include_global=include_global,
93
+ )
94
+ return self._list(limit=limit, params=filter.dump())
@@ -22,6 +22,9 @@ class WriteableResource(Resource, Generic[T_Resource], ABC):
22
22
  raise NotImplementedError()
23
23
 
24
24
 
25
+ T_Response = TypeVar("T_Response", bound=WriteableResource)
26
+
27
+
25
28
  class APIResource(Generic[T_Reference], ABC):
26
29
  """Base class for all API data modeling resources."""
27
30
 
@@ -15,11 +15,19 @@ from ._physical import PhysicalDataModel
15
15
  from ._result import Result
16
16
 
17
17
 
18
+ def _is_in_browser() -> bool:
19
+ try:
20
+ from pyodide.ffi import IN_BROWSER # type: ignore [import-not-found]
21
+ except ModuleNotFoundError:
22
+ return False
23
+ return IN_BROWSER
24
+
25
+
18
26
  class NeatSession:
19
27
  """A session is an interface for neat operations."""
20
28
 
21
29
  def __init__(
22
- self, client: CogniteClient | ClientConfig, config: PredefinedProfile | NeatConfig = "legacy-additive"
30
+ self, client: CogniteClient | ClientConfig | None, config: PredefinedProfile | NeatConfig = "legacy-additive"
23
31
  ) -> None:
24
32
  """Initialize a Neat session.
25
33
 
@@ -30,6 +38,10 @@ class NeatSession:
30
38
  Defaults to "legacy-additive". This means Neat will perform additive modeling
31
39
  and apply only validations that were part of the legacy Neat version.
32
40
  """
41
+ if client is None and _is_in_browser():
42
+ client = CogniteClient()
43
+ elif client is None:
44
+ raise ValueError("A CogniteClient or ClientConfig must be provided to initialize a NeatSession.")
33
45
  self._config = NeatConfig.create_predefined(config) if isinstance(config, str) else config
34
46
 
35
47
  # Use configuration for physical data model
@@ -167,7 +167,9 @@ class HTTPClient:
167
167
  if isinstance(item, BodyRequest):
168
168
  data = item.data()
169
169
  if not global_config.disable_gzip:
170
- data = gzip.compress(data.encode("utf-8"))
170
+ if isinstance(data, str):
171
+ data = data.encode("utf-8")
172
+ data = gzip.compress(data)
171
173
  return self.session.request(
172
174
  method=item.method,
173
175
  url=item.endpoint_url,
@@ -118,14 +118,14 @@ class BodyRequest(ParametersRequest, ABC):
118
118
  """Base class for HTTP request messages with a body"""
119
119
 
120
120
  @abstractmethod
121
- def data(self) -> str:
121
+ def data(self) -> str | bytes:
122
122
  raise NotImplementedError()
123
123
 
124
124
 
125
125
  class SimpleBodyRequest(BodyRequest):
126
- body: str
126
+ body: str | bytes
127
127
 
128
- def data(self) -> str:
128
+ def data(self) -> str | bytes:
129
129
  return self.body
130
130
 
131
131
 
@@ -2,7 +2,7 @@ import subprocess
2
2
  from pathlib import Path
3
3
 
4
4
 
5
- def get_repo_root() -> Path:
5
+ def get_repo_root() -> Path | None:
6
6
  """Get the root path of the git repository.
7
7
 
8
8
  Raises:
@@ -11,9 +11,11 @@ def get_repo_root() -> Path:
11
11
  """
12
12
  try:
13
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
14
+ except FileNotFoundError:
15
+ # Git is not installed or not found in PATH
16
+ return None
16
17
  output = result.stdout.decode().strip()
17
18
  if not output:
18
- raise RuntimeError("Not in a git repository")
19
+ # Not in a git repository
20
+ return None
19
21
  return Path(output)
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.0.24"
1
+ __version__ = "1.0.26"
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.24
3
+ Version: 1.0.26
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>