cognite-extractor-utils 7.4.3__py3-none-any.whl → 7.4.4__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.

Potentially problematic release.


This version of cognite-extractor-utils might be problematic. Click here for more details.

@@ -16,5 +16,5 @@
16
16
  Cognite extractor utils is a Python package that simplifies the development of new extractors.
17
17
  """
18
18
 
19
- __version__ = "7.4.3"
19
+ __version__ = "7.4.4"
20
20
  from .base import Extractor
@@ -13,6 +13,9 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
+ from typing import List, Optional
17
+
18
+
16
19
  class InvalidConfigError(Exception):
17
20
  """
18
21
  Exception thrown from ``load_yaml`` and ``load_yaml_dict`` if config file is invalid. This can be due to
@@ -22,9 +25,10 @@ class InvalidConfigError(Exception):
22
25
  * Unkown fields
23
26
  """
24
27
 
25
- def __init__(self, message: str):
28
+ def __init__(self, message: str, details: Optional[List[str]] = None):
26
29
  super(InvalidConfigError, self).__init__()
27
30
  self.message = message
31
+ self.details = details
28
32
 
29
33
  def __str__(self) -> str:
30
34
  return f"Invalid config: {self.message}"
@@ -0,0 +1,111 @@
1
+ import json
2
+ from enum import Enum
3
+ from io import StringIO
4
+ from pathlib import Path
5
+ from typing import Dict, Optional, TextIO, Type, TypeVar, Union
6
+
7
+ from pydantic import ValidationError
8
+
9
+ from cognite.client import CogniteClient
10
+ from cognite.extractorutils.configtools.loaders import _load_yaml_dict_raw
11
+ from cognite.extractorutils.exceptions import InvalidConfigError
12
+ from cognite.extractorutils.unstable.configuration.models import ConfigModel
13
+
14
+ _T = TypeVar("_T", bound=ConfigModel)
15
+
16
+
17
+ class ConfigFormat(Enum):
18
+ JSON = "json"
19
+ YAML = "yaml"
20
+
21
+
22
+ def load_file(path: Path, schema: Type[_T]) -> _T:
23
+ if path.suffix in [".yaml", ".yml"]:
24
+ format = ConfigFormat.YAML
25
+ elif path.suffix == ".json":
26
+ format = ConfigFormat.JSON
27
+ else:
28
+ raise InvalidConfigError(f"Unknown file type {path.suffix}")
29
+
30
+ with open(path, "r") as stream:
31
+ return load_io(stream, format, schema)
32
+
33
+
34
+ def load_from_cdf(
35
+ cognite_client: CogniteClient, external_id: str, schema: Type[_T], revision: Optional[int] = None
36
+ ) -> _T:
37
+ params: Dict[str, Union[str, int]] = {"externalId": external_id}
38
+ if revision:
39
+ params["revision"] = revision
40
+ response = cognite_client.get(
41
+ f"/api/v1/projects/{cognite_client.config.project}/odin/config",
42
+ params=params,
43
+ headers={"cdf-version": "alpha"},
44
+ )
45
+ response.raise_for_status()
46
+ data = response.json()
47
+ return load_io(StringIO(data["config"]), ConfigFormat.YAML, schema)
48
+
49
+
50
+ def load_io(stream: TextIO, format: ConfigFormat, schema: Type[_T]) -> _T:
51
+ if format == ConfigFormat.JSON:
52
+ data = json.load(stream)
53
+
54
+ elif format == ConfigFormat.YAML:
55
+ data = _load_yaml_dict_raw(stream)
56
+
57
+ if "azure-keyvault" in data:
58
+ data.pop("azure-keyvault")
59
+ if "key-vault" in data:
60
+ data.pop("key-vault")
61
+
62
+ return load_dict(data, schema)
63
+
64
+
65
+ def _make_loc_str(loc: tuple) -> str:
66
+ # Remove the body parameter if it is present
67
+ if loc[0] == "body":
68
+ loc = loc[1:]
69
+
70
+ # Create a string from the loc parameter
71
+ loc_str = ""
72
+ needs_sep = False
73
+ for lo in loc:
74
+ if not needs_sep:
75
+ loc_str = f"{loc_str}{lo}"
76
+ needs_sep = True
77
+ else:
78
+ if isinstance(lo, int):
79
+ loc_str = f"{loc_str}[{lo}]"
80
+ else:
81
+ loc_str = f"{loc_str}.{lo}"
82
+
83
+ return loc_str
84
+
85
+
86
+ def load_dict(data: dict, schema: Type[_T]) -> _T:
87
+ try:
88
+ return schema.model_validate(data)
89
+
90
+ except ValidationError as e:
91
+ messages = []
92
+ for err in e.errors():
93
+ loc = err.get("loc")
94
+ if loc is None:
95
+ continue
96
+
97
+ # Create a string from the loc parameter
98
+ loc_str = _make_loc_str(loc)
99
+
100
+ if "ctx" in err and "error" in err["ctx"]:
101
+ exc = err["ctx"]["error"]
102
+ if isinstance(exc, ValueError) or isinstance(exc, AssertionError):
103
+ messages.append(f"{loc_str}: {str(exc)}")
104
+ continue
105
+
106
+ if err.get("type") == "json_invalid":
107
+ messages.append(f"{err.get('msg')}: {loc_str}")
108
+ else:
109
+ messages.append(f"{loc_str}: {err.get('msg')}")
110
+
111
+ raise InvalidConfigError(", ".join(messages), details=messages) from e
@@ -0,0 +1,159 @@
1
+ import re
2
+ from datetime import timedelta
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
6
+
7
+ from humps import kebabize
8
+ from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler
9
+ from pydantic_core import CoreSchema, core_schema
10
+
11
+ from cognite.extractorutils.exceptions import InvalidConfigError
12
+
13
+
14
+ class ConfigModel(BaseModel):
15
+ model_config = ConfigDict(
16
+ alias_generator=kebabize,
17
+ populate_by_name=True,
18
+ extra="forbid",
19
+ # arbitrary_types_allowed=True,
20
+ )
21
+
22
+
23
+ class _ClientCredentialsConfig(ConfigModel):
24
+ type: Literal["client-credentials"]
25
+ client_id: str
26
+ client_secret: str
27
+ token_url: str
28
+ scopes: List[str]
29
+ resource: Optional[str] = None
30
+ audience: Optional[str] = None
31
+
32
+
33
+ class _ClientCertificateConfig(ConfigModel):
34
+ type: Literal["client-certificate"]
35
+ client_id: str
36
+ certificate_path: Path
37
+ scopes: List[str]
38
+
39
+
40
+ AuthenticationConfig = Annotated[Union[_ClientCredentialsConfig, _ClientCertificateConfig], Field(discriminator="type")]
41
+
42
+
43
+ class TimeIntervalConfig:
44
+ """
45
+ Configuration parameter for setting a time interval
46
+ """
47
+
48
+ def __init__(self, expression: str) -> None:
49
+ self._interval, self._expression = TimeIntervalConfig._parse_expression(expression)
50
+
51
+ @classmethod
52
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
53
+ return core_schema.no_info_after_validator_function(cls, handler(Union[str, int]))
54
+
55
+ def __eq__(self, other: object) -> bool:
56
+ if not isinstance(other, TimeIntervalConfig):
57
+ return NotImplemented
58
+ return self._interval == other._interval
59
+
60
+ def __hash__(self) -> int:
61
+ return hash(self._interval)
62
+
63
+ @classmethod
64
+ def _parse_expression(cls, expression: str) -> tuple[int, str]:
65
+ # First, try to parse pure number and assume seconds (for backwards compatibility)
66
+ try:
67
+ return int(expression), f"{expression}s"
68
+ except ValueError:
69
+ pass
70
+
71
+ match = re.match(r"(\d+)[ \t]*(s|m|h|d)", expression)
72
+ if not match:
73
+ raise InvalidConfigError("Invalid interval pattern")
74
+
75
+ number, unit = match.groups()
76
+ numeric_unit = {"s": 1, "m": 60, "h": 60 * 60, "d": 60 * 60 * 24}[unit]
77
+
78
+ return int(number) * numeric_unit, expression
79
+
80
+ @property
81
+ def seconds(self) -> int:
82
+ return self._interval
83
+
84
+ @property
85
+ def minutes(self) -> float:
86
+ return self._interval / 60
87
+
88
+ @property
89
+ def hours(self) -> float:
90
+ return self._interval / (60 * 60)
91
+
92
+ @property
93
+ def days(self) -> float:
94
+ return self._interval / (60 * 60 * 24)
95
+
96
+ @property
97
+ def timedelta(self) -> timedelta:
98
+ days = self._interval // (60 * 60 * 24)
99
+ seconds = self._interval % (60 * 60 * 24)
100
+ return timedelta(days=days, seconds=seconds)
101
+
102
+ def __int__(self) -> int:
103
+ return int(self._interval)
104
+
105
+ def __float__(self) -> float:
106
+ return float(self._interval)
107
+
108
+ def __str__(self) -> str:
109
+ return self._expression
110
+
111
+ def __repr__(self) -> str:
112
+ return self._expression
113
+
114
+
115
+ class _ConnectionParameters(ConfigModel):
116
+ gzip_compression: bool = False
117
+ status_forcelist: List[int] = Field(default_factory=lambda: [429, 502, 503, 504])
118
+ max_retries: int = 10
119
+ max_retries_connect: int = 3
120
+ max_retry_backoff: TimeIntervalConfig = Field(default_factory=lambda: TimeIntervalConfig("30s"))
121
+ max_connection_pool_size: int = 50
122
+ ssl_verify: bool = True
123
+ proxies: Dict[str, str] = Field(default_factory=dict)
124
+
125
+
126
+ class ConnectionConfig(ConfigModel):
127
+ project: str
128
+ base_url: str
129
+
130
+ extraction_pipeline: str
131
+
132
+ authentication: AuthenticationConfig
133
+
134
+ connection: _ConnectionParameters = Field(default_factory=_ConnectionParameters)
135
+
136
+
137
+ class LogLevel(Enum):
138
+ CRITICAL = "CRITICAL"
139
+ ERROR = "ERROR"
140
+ WARNING = "WARNING"
141
+ INFO = "INFO"
142
+ DEBUG = "DEBUG"
143
+
144
+
145
+ class LogFileHandlerConfig(ConfigModel):
146
+ path: Path
147
+ level: LogLevel
148
+ retention: int = 7
149
+
150
+
151
+ class LogConsoleHandlerConfig(ConfigModel):
152
+ level: LogLevel
153
+
154
+
155
+ LogHandlerConfig = Union[LogFileHandlerConfig, LogConsoleHandlerConfig]
156
+
157
+
158
+ class ExtractorConfig(ConfigModel):
159
+ log_handlers: List[LogHandlerConfig] = Field(default_factory=lambda: [LogConsoleHandlerConfig(level=LogLevel.INFO)])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-extractor-utils
3
- Version: 7.4.3
3
+ Version: 7.4.4
4
4
  Summary: Utilities for easier development of extractors for CDF
5
5
  Home-page: https://github.com/cognitedata/python-extractor-utils
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Provides-Extra: experimental
17
17
  Requires-Dist: arrow (>=1.0.0,<2.0.0)
18
18
  Requires-Dist: azure-identity (>=1.14.0,<2.0.0)
19
19
  Requires-Dist: azure-keyvault-secrets (>=4.7.0,<5.0.0)
20
- Requires-Dist: cognite-sdk (>=7.58.4,<8.0.0)
20
+ Requires-Dist: cognite-sdk (>=7.59.0,<8.0.0)
21
21
  Requires-Dist: dacite (>=1.6.0,<2.0.0)
22
22
  Requires-Dist: decorator (>=5.1.1,<6.0.0)
23
23
  Requires-Dist: httpx (>=0.27.0,<0.28.0)
@@ -25,6 +25,8 @@ Requires-Dist: more-itertools (>=10.0.0,<11.0.0)
25
25
  Requires-Dist: orjson (>=3.10.3,<4.0.0)
26
26
  Requires-Dist: prometheus-client (>0.7.0,<=1.0.0)
27
27
  Requires-Dist: psutil (>=6.0.0,<7.0.0)
28
+ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
29
+ Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
28
30
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
29
31
  Requires-Dist: pyyaml (>=5.3.0,<7)
30
32
  Requires-Dist: typing-extensions (>=3.7.4,<5)
@@ -1,11 +1,11 @@
1
- cognite/extractorutils/__init__.py,sha256=piRKiXLS1XurR0Hwyz9mKGaicqhmzSvu3q1yPTE87ro,739
1
+ cognite/extractorutils/__init__.py,sha256=31r2uuSwG5TA05s9mUYwn8tfl1ShWccfkVVf7uVOcYs,739
2
2
  cognite/extractorutils/_inner_util.py,sha256=gmz6aqS7jDNsg8z4RHgJjMFohDLOMiaU4gMWBhg3xcE,1558
3
3
  cognite/extractorutils/base.py,sha256=q6NU2bPec3WOasVnnIFoh-aUJudVZWZ2R6emz3IRj8Q,16391
4
4
  cognite/extractorutils/configtools/__init__.py,sha256=L-daaqInIsmHcjb2forJeY0fW8tz1mlteOUo7IsWnrU,3059
5
5
  cognite/extractorutils/configtools/_util.py,sha256=SZycZm_py9v9WZbDiDQbgS6_PiLtu-TtwuuH7tG2YCI,4739
6
6
  cognite/extractorutils/configtools/elements.py,sha256=0q1DaQFeKpP8tQvgJ7sN4Tx8ryY0eR8kFolKvzoqSoM,23392
7
7
  cognite/extractorutils/configtools/loaders.py,sha256=_bMEb2_WaBL_G9w5IXU5UKwSu1VGge0PPAWgfSribYI,17647
8
- cognite/extractorutils/exceptions.py,sha256=XiwyNPSN0YxFYaPw7tfA63B94PL48xDK3EfdGdhgQgc,1084
8
+ cognite/extractorutils/exceptions.py,sha256=1PgvW1FrgVnuNtkwC0RTvG1-FZp1qmBuYrY1AWW-BJc,1188
9
9
  cognite/extractorutils/metrics.py,sha256=01ZMRbDisXPxrfCSyTSEkXMsslzmZwEqw18fuu9okdc,15509
10
10
  cognite/extractorutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  cognite/extractorutils/statestore/__init__.py,sha256=hV3r11FUXkH6-60Ct6zLSROMNVrEeiE3Shmkf28Q-co,359
@@ -14,6 +14,9 @@ cognite/extractorutils/statestore/hashing.py,sha256=o-efTv21_ATQnyxYmple3MF7r5Af
14
14
  cognite/extractorutils/statestore/watermark.py,sha256=c_lcmJfo8bOvWyCJ9iRbbE4BlqRVulom4TpHb2pOnkE,16755
15
15
  cognite/extractorutils/threading.py,sha256=2Hke5cFvP-wA45Crvh58JahoKXB64P3tr7R4y_BhBqM,3605
16
16
  cognite/extractorutils/unstable/__init__.py,sha256=L6nqJHjylpk67CE-PbXJyb_TBI4yjhEYEz9J9WShDfM,341
17
+ cognite/extractorutils/unstable/configuration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ cognite/extractorutils/unstable/configuration/loaders.py,sha256=LBFgPjJrlz2ZHGKxuIbebOMLx1LU7DsGbyqYqd8Aq4w,3313
19
+ cognite/extractorutils/unstable/configuration/models.py,sha256=HG_7MfCKtMdVpW6VmCqGug4Y8QrEek_ATpRBgVc4_5k,4429
17
20
  cognite/extractorutils/uploader/__init__.py,sha256=W22u6QHA4cR0j78LN5LTL5YGbfC-uTApagTyP5ab7uQ,3110
18
21
  cognite/extractorutils/uploader/_base.py,sha256=wktbV8dpb8zBOsNaECZkBNoJSpOz437NlNMER3-a3xQ,5304
19
22
  cognite/extractorutils/uploader/_metrics.py,sha256=J2LJXb19L_SLSJ_voNIQHYLp0pjxUKevpH1q_xKX6Hk,3247
@@ -26,7 +29,7 @@ cognite/extractorutils/uploader/time_series.py,sha256=HBtQdsQoIOaL-EG5lMsaY-ORwV
26
29
  cognite/extractorutils/uploader_extractor.py,sha256=E-mpVvbPg_Tk90U4S9JybV0duptJ2SXE88HB6npE3zI,7732
27
30
  cognite/extractorutils/uploader_types.py,sha256=wxfrsiKPTzG5lmoYtQsxt8Xyj-s5HnaLl8WDzJNrazg,1020
28
31
  cognite/extractorutils/util.py,sha256=T6ef5b7aYJ8yq9swQwybYaLe3YGr3hElsJQy8E-d5Rs,17469
29
- cognite_extractor_utils-7.4.3.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
30
- cognite_extractor_utils-7.4.3.dist-info/METADATA,sha256=XjJEfPowEj6H8hx77yT9OljKGfREA8ilNPaWVTAAPlI,5476
31
- cognite_extractor_utils-7.4.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- cognite_extractor_utils-7.4.3.dist-info/RECORD,,
32
+ cognite_extractor_utils-7.4.4.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
33
+ cognite_extractor_utils-7.4.4.dist-info/METADATA,sha256=CtumhyB72ieH7_rSizELwMfG7zVFk087hAwfjnN7jVk,5557
34
+ cognite_extractor_utils-7.4.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
35
+ cognite_extractor_utils-7.4.4.dist-info/RECORD,,