cognite-neat 0.78.3__py3-none-any.whl → 0.78.5__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-neat might be problematic. Click here for more details.

Files changed (38) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/graph/_tracking/__init__.py +4 -0
  3. cognite/neat/graph/_tracking/base.py +30 -0
  4. cognite/neat/graph/_tracking/log.py +27 -0
  5. cognite/neat/graph/extractors/__init__.py +19 -0
  6. cognite/neat/graph/extractors/_classic_cdf/__init__.py +0 -0
  7. cognite/neat/graph/extractors/_classic_cdf/_assets.py +107 -0
  8. cognite/neat/graph/extractors/_classic_cdf/_events.py +117 -0
  9. cognite/neat/graph/extractors/_classic_cdf/_files.py +131 -0
  10. cognite/neat/graph/extractors/_classic_cdf/_labels.py +72 -0
  11. cognite/neat/graph/extractors/_classic_cdf/_relationships.py +153 -0
  12. cognite/neat/graph/extractors/_classic_cdf/_sequences.py +92 -0
  13. cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +118 -0
  14. cognite/neat/graph/issues/__init__.py +0 -0
  15. cognite/neat/graph/issues/loader.py +104 -0
  16. cognite/neat/graph/loaders/__init__.py +4 -0
  17. cognite/neat/graph/loaders/_base.py +109 -0
  18. cognite/neat/graph/loaders/_rdf2dms.py +280 -0
  19. cognite/neat/graph/stores/_base.py +19 -4
  20. cognite/neat/issues.py +150 -0
  21. cognite/neat/rules/exporters/_base.py +2 -3
  22. cognite/neat/rules/exporters/_rules2dms.py +5 -5
  23. cognite/neat/rules/importers/_base.py +1 -1
  24. cognite/neat/rules/issues/__init__.py +2 -3
  25. cognite/neat/rules/issues/base.py +9 -133
  26. cognite/neat/rules/issues/spreadsheet.py +3 -2
  27. cognite/neat/rules/models/_base.py +6 -0
  28. cognite/neat/rules/models/dms/_rules.py +3 -0
  29. cognite/neat/rules/models/dms/_schema.py +133 -3
  30. cognite/neat/rules/models/domain.py +3 -0
  31. cognite/neat/rules/models/information/_rules.py +4 -1
  32. cognite/neat/{rules/exporters/_models.py → utils/upload.py} +26 -6
  33. cognite/neat/utils/utils.py +24 -0
  34. {cognite_neat-0.78.3.dist-info → cognite_neat-0.78.5.dist-info}/METADATA +2 -2
  35. {cognite_neat-0.78.3.dist-info → cognite_neat-0.78.5.dist-info}/RECORD +38 -21
  36. {cognite_neat-0.78.3.dist-info → cognite_neat-0.78.5.dist-info}/LICENSE +0 -0
  37. {cognite_neat-0.78.3.dist-info → cognite_neat-0.78.5.dist-info}/WHEEL +0 -0
  38. {cognite_neat-0.78.3.dist-info → cognite_neat-0.78.5.dist-info}/entry_points.txt +0 -0
@@ -1,87 +1,43 @@
1
- import sys
2
- import warnings
3
- from abc import ABC, abstractmethod
4
- from collections import UserList
5
- from collections.abc import Sequence
1
+ from abc import ABC
6
2
  from dataclasses import dataclass
7
- from functools import total_ordering
8
- from typing import Any, ClassVar
9
- from warnings import WarningMessage
3
+ from typing import Any
10
4
 
11
- import pandas as pd
12
5
  from pydantic_core import ErrorDetails
13
6
 
14
- if sys.version_info < (3, 11):
15
- from exceptiongroup import ExceptionGroup
16
- else:
17
- pass
7
+ from cognite.neat.issues import MultiValueError, NeatError, NeatIssue, NeatIssueList, NeatWarning
18
8
 
19
9
  __all__ = [
20
10
  "ValidationIssue",
21
11
  "NeatValidationError",
22
12
  "DefaultPydanticError",
23
13
  "ValidationWarning",
24
- "DefaultWarning",
25
14
  "IssueList",
26
15
  "MultiValueError",
27
16
  ]
28
17
 
29
18
 
30
- @total_ordering
31
19
  @dataclass(frozen=True)
32
- class ValidationIssue(ABC):
33
- description: ClassVar[str]
34
- fix: ClassVar[str]
35
-
36
- def message(self) -> str:
37
- """Return a human-readable message for the issue.
38
-
39
- This is the default implementation, which returns the description.
40
- It is recommended to override this method in subclasses with a more
41
- specific message.
42
- """
43
- return self.description
44
-
45
- @abstractmethod
46
- def dump(self) -> dict[str, Any]:
47
- """Return a dictionary representation of the issue."""
48
- raise NotImplementedError()
49
-
50
- def __lt__(self, other: "ValidationIssue") -> bool:
51
- if not isinstance(other, ValidationIssue):
52
- return NotImplemented
53
- return (type(self).__name__, self.message()) < (type(other).__name__, other.message())
54
-
55
- def __eq__(self, other: object) -> bool:
56
- if not isinstance(other, ValidationIssue):
57
- return NotImplemented
58
- return (type(self).__name__, self.message()) == (type(other).__name__, other.message())
20
+ class ValidationIssue(NeatIssue, ABC): ...
59
21
 
60
22
 
61
23
  @dataclass(frozen=True)
62
- class NeatValidationError(ValidationIssue, ABC):
63
- def dump(self) -> dict[str, Any]:
64
- return {"error": type(self).__name__}
65
-
24
+ class NeatValidationError(NeatError, ValidationIssue, ABC):
66
25
  @classmethod
67
26
  def from_pydantic_errors(cls, errors: list[ErrorDetails], **kwargs) -> "list[NeatValidationError]":
68
27
  """Convert a list of pydantic errors to a list of Error instances.
69
28
 
70
29
  This is intended to be overridden in subclasses to handle specific error types.
71
30
  """
72
- all_errors = []
31
+ all_errors: list[NeatValidationError] = []
73
32
  for error in errors:
74
33
  if isinstance(ctx := error.get("ctx"), dict) and isinstance(
75
34
  multi_error := ctx.get("error"), MultiValueError
76
35
  ):
77
- all_errors.extend(multi_error.errors)
36
+ all_errors.extend(multi_error.errors) # type: ignore[arg-type]
78
37
  else:
79
38
  all_errors.append(DefaultPydanticError.from_pydantic_error(error))
80
39
  return all_errors
81
40
 
82
- def as_exception(self) -> Exception:
83
- return ValueError(self.message())
84
-
85
41
 
86
42
  @dataclass(frozen=True)
87
43
  class DefaultPydanticError(NeatValidationError):
@@ -120,87 +76,7 @@ class DefaultPydanticError(NeatValidationError):
120
76
 
121
77
 
122
78
  @dataclass(frozen=True)
123
- class ValidationWarning(ValidationIssue, ABC, UserWarning):
124
- def dump(self) -> dict[str, Any]:
125
- return {"warning": type(self).__name__}
126
-
127
- @classmethod
128
- def from_warning(cls, warning: WarningMessage) -> "ValidationWarning":
129
- return DefaultWarning.from_warning_message(warning)
130
-
131
-
132
- @dataclass(frozen=True)
133
- class DefaultWarning(ValidationWarning):
134
- description = "A warning was raised during validation."
135
- fix = "No fix is available."
136
-
137
- warning: str | Warning
138
- category: type[Warning]
139
- source: str | None = None
140
-
141
- def dump(self) -> dict[str, Any]:
142
- output = super().dump()
143
- output["msg"] = str(self.warning)
144
- output["category"] = self.category.__name__
145
- output["source"] = self.source
146
- return output
147
-
148
- @classmethod
149
- def from_warning_message(cls, warning: WarningMessage) -> "ValidationWarning":
150
- if isinstance(warning.message, ValidationWarning):
151
- return warning.message
152
-
153
- return cls(
154
- warning=warning.message,
155
- category=warning.category,
156
- source=warning.source,
157
- )
158
-
159
- def message(self) -> str:
160
- return str(self.warning)
161
-
162
-
163
- class IssueList(UserList[ValidationIssue]):
164
- def __init__(self, issues: Sequence[ValidationIssue] | None = None, title: str | None = None):
165
- super().__init__(issues or [])
166
- self.title = title
167
-
168
- @property
169
- def errors(self) -> "IssueList":
170
- return IssueList([issue for issue in self if isinstance(issue, NeatValidationError)])
171
-
172
- @property
173
- def has_errors(self) -> bool:
174
- return any(isinstance(issue, NeatValidationError) for issue in self)
175
-
176
- @property
177
- def warnings(self) -> "IssueList":
178
- return IssueList([issue for issue in self if isinstance(issue, ValidationWarning)])
179
-
180
- def as_errors(self) -> ExceptionGroup:
181
- return ExceptionGroup(
182
- "Validation failed",
183
- [ValueError(issue.message()) for issue in self if isinstance(issue, NeatValidationError)],
184
- )
185
-
186
- def trigger_warnings(self) -> None:
187
- for warning in [issue for issue in self if isinstance(issue, ValidationWarning)]:
188
- warnings.warn(warning, stacklevel=2)
189
-
190
- def to_pandas(self) -> pd.DataFrame:
191
- return pd.DataFrame([issue.dump() for issue in self])
192
-
193
- def _repr_html_(self) -> str | None:
194
- return self.to_pandas()._repr_html_() # type: ignore[operator]
195
-
196
-
197
- class MultiValueError(ValueError):
198
- """This is a container for multiple errors.
199
-
200
- It is used in the pydantic field_validator/model_validator to collect multiple errors, which
201
- can then be caught in a try-except block and returned as an IssueList.
79
+ class ValidationWarning(NeatWarning, ValidationIssue, ABC): ...
202
80
 
203
- """
204
81
 
205
- def __init__(self, errors: Sequence[NeatValidationError]):
206
- self.errors = list(errors)
82
+ class IssueList(NeatIssueList[ValidationIssue]): ...
@@ -8,9 +8,10 @@ from cognite.client.data_classes import data_modeling as dm
8
8
  from cognite.client.data_classes.data_modeling import ContainerId, ViewId
9
9
  from pydantic_core import ErrorDetails
10
10
 
11
+ from cognite.neat.issues import MultiValueError
11
12
  from cognite.neat.utils.spreadsheet import SpreadsheetRead
12
13
 
13
- from .base import DefaultPydanticError, MultiValueError, NeatValidationError
14
+ from .base import DefaultPydanticError, NeatValidationError
14
15
 
15
16
  if sys.version_info >= (3, 11):
16
17
  from typing import Self
@@ -69,7 +70,7 @@ class InvalidSheetError(NeatValidationError, ABC):
69
70
  new_row = reader.adjusted_row_number(caught_error.row)
70
71
  # The error is frozen, so we have to use __setattr__ to change the row number
71
72
  object.__setattr__(caught_error, "row", new_row)
72
- output.append(caught_error)
73
+ output.append(caught_error) # type: ignore[arg-type]
73
74
  continue
74
75
 
75
76
  if len(error["loc"]) >= 4:
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  import math
8
8
  import sys
9
9
  import types
10
+ from abc import abstractmethod
10
11
  from collections.abc import Callable, Iterator
11
12
  from functools import wraps
12
13
  from typing import Annotated, Any, ClassVar, Generic, Literal, TypeAlias, TypeVar
@@ -245,6 +246,11 @@ class BaseMetadata(RuleModel):
245
246
  def include_role(self, serializer: Callable) -> dict:
246
247
  return {"role": self.role.value, **serializer(self)}
247
248
 
249
+ @abstractmethod
250
+ def as_identifier(self) -> str:
251
+ """Returns a unique identifier for the metadata."""
252
+ raise NotImplementedError()
253
+
248
254
 
249
255
  class BaseRules(RuleModel):
250
256
  """
@@ -132,6 +132,9 @@ class DMSMetadata(BaseMetadata):
132
132
  views=[],
133
133
  )
134
134
 
135
+ def as_identifier(self) -> str:
136
+ return repr(self.as_data_model_id())
137
+
135
138
  @classmethod
136
139
  def _get_description_and_creator(cls, description_raw: str | None) -> tuple[str | None, list[str]]:
137
140
  if description_raw and (description_match := re.search(r"Creator: (.+)", description_raw)):
@@ -2,17 +2,27 @@ import json
2
2
  import sys
3
3
  import warnings
4
4
  import zipfile
5
- from collections import Counter, defaultdict
5
+ from collections import ChainMap, Counter, defaultdict
6
+ from collections.abc import Iterable, MutableMapping
6
7
  from dataclasses import Field, dataclass, field, fields
7
8
  from pathlib import Path
8
- from typing import Any, ClassVar, cast
9
+ from typing import Any, ClassVar, Literal, cast
9
10
 
10
11
  import yaml
11
12
  from cognite.client import CogniteClient
12
13
  from cognite.client import data_modeling as dm
13
14
  from cognite.client.data_classes import DatabaseWrite, DatabaseWriteList, TransformationWrite, TransformationWriteList
14
15
  from cognite.client.data_classes.data_modeling import ViewApply
15
- from cognite.client.data_classes.data_modeling.views import ReverseDirectRelation
16
+ from cognite.client.data_classes.data_modeling.views import (
17
+ ReverseDirectRelation,
18
+ ReverseDirectRelationApply,
19
+ SingleEdgeConnection,
20
+ SingleEdgeConnectionApply,
21
+ SingleReverseDirectRelation,
22
+ SingleReverseDirectRelationApply,
23
+ ViewProperty,
24
+ ViewPropertyApply,
25
+ )
16
26
  from cognite.client.data_classes.transformations.common import Edges, EdgeType, Nodes, ViewInfo
17
27
 
18
28
  from cognite.neat.rules import issues
@@ -668,6 +678,126 @@ class DMSSchema:
668
678
  referenced_spaces |= {s.space for s in self.spaces.values()}
669
679
  return referenced_spaces
670
680
 
681
+ def as_read_model(self) -> dm.DataModel[dm.View]:
682
+ if self.data_model is None:
683
+ raise ValueError("Data model is not defined")
684
+ all_containers = self.containers.copy()
685
+ all_views = self.views.copy()
686
+ for other_schema in [self.reference, self.last]:
687
+ if other_schema:
688
+ all_containers |= other_schema.containers
689
+ all_views |= other_schema.views
690
+
691
+ views: list[dm.View] = []
692
+ for view in self.views.values():
693
+ referenced_containers = ContainerApplyDict()
694
+ properties: dict[str, ViewProperty] = {}
695
+ # ChainMap is used to merge properties from the view and its parents
696
+ # Note that the order of the ChainMap is important, as the first dictionary has the highest priority
697
+ # So if a child and parent have the same property, the child property will be used.
698
+ write_properties = ChainMap(view.properties, *(all_views[v].properties for v in view.implements or [])) # type: ignore[arg-type]
699
+ for prop_name, prop in write_properties.items():
700
+ read_prop = self._as_read_properties(prop, all_containers)
701
+ if isinstance(read_prop, dm.MappedProperty) and read_prop.container not in referenced_containers:
702
+ referenced_containers[read_prop.container] = all_containers[read_prop.container]
703
+ properties[prop_name] = read_prop
704
+
705
+ read_view = dm.View(
706
+ space=view.space,
707
+ external_id=view.external_id,
708
+ version=view.version,
709
+ description=view.description,
710
+ name=view.name,
711
+ filter=view.filter,
712
+ implements=view.implements.copy(),
713
+ used_for=self._used_for(referenced_containers.values()),
714
+ writable=self._writable(properties.values(), referenced_containers.values()),
715
+ properties=properties,
716
+ is_global=False,
717
+ last_updated_time=0,
718
+ created_time=0,
719
+ )
720
+ views.append(read_view)
721
+
722
+ return dm.DataModel(
723
+ space=self.data_model.space,
724
+ external_id=self.data_model.external_id,
725
+ version=self.data_model.version,
726
+ name=self.data_model.name,
727
+ description=self.data_model.description,
728
+ views=views,
729
+ is_global=False,
730
+ last_updated_time=0,
731
+ created_time=0,
732
+ )
733
+
734
+ @staticmethod
735
+ def _as_read_properties(
736
+ write: ViewPropertyApply, all_containers: MutableMapping[dm.ContainerId, dm.ContainerApply]
737
+ ) -> ViewProperty:
738
+ if isinstance(write, dm.MappedPropertyApply):
739
+ container_prop = all_containers[write.container].properties[write.container_property_identifier]
740
+ return dm.MappedProperty(
741
+ container=write.container,
742
+ container_property_identifier=write.container_property_identifier,
743
+ name=write.name,
744
+ description=write.description,
745
+ source=write.source,
746
+ type=container_prop.type,
747
+ nullable=container_prop.nullable,
748
+ auto_increment=container_prop.auto_increment,
749
+ # Likely bug in SDK.
750
+ default_value=container_prop.default_value, # type: ignore[arg-type]
751
+ )
752
+ if isinstance(write, dm.EdgeConnectionApply):
753
+ edge_cls = SingleEdgeConnection if isinstance(write, SingleEdgeConnectionApply) else dm.MultiEdgeConnection
754
+ return edge_cls(
755
+ type=write.type,
756
+ source=write.source,
757
+ name=write.name,
758
+ description=write.description,
759
+ edge_source=write.edge_source,
760
+ direction=write.direction,
761
+ )
762
+ if isinstance(write, ReverseDirectRelationApply):
763
+ relation_cls = (
764
+ SingleReverseDirectRelation
765
+ if isinstance(write, SingleReverseDirectRelationApply)
766
+ else dm.MultiReverseDirectRelation
767
+ )
768
+ return relation_cls(
769
+ source=write.source,
770
+ through=write.through,
771
+ name=write.name,
772
+ description=write.description,
773
+ )
774
+ raise ValueError(f"Cannot convert {write} to read format")
775
+
776
+ @staticmethod
777
+ def _used_for(containers: Iterable[dm.ContainerApply]) -> Literal["node", "edge", "all"]:
778
+ used_for = {container.used_for for container in containers}
779
+ if used_for == {"node"}:
780
+ return "node"
781
+ if used_for == {"edge"}:
782
+ return "edge"
783
+ return "all"
784
+
785
+ @staticmethod
786
+ def _writable(properties: Iterable[ViewProperty], containers: Iterable[dm.ContainerApply]) -> bool:
787
+ used_properties = {
788
+ (prop.container, prop.container_property_identifier)
789
+ for prop in properties
790
+ if isinstance(prop, dm.MappedProperty)
791
+ }
792
+ required_properties = {
793
+ (container.as_id(), prop_id)
794
+ for container in containers
795
+ for prop_id, prop in container.properties.items()
796
+ if not prop.nullable
797
+ }
798
+ # If a container has a required property that is not used by the view, the view is not writable
799
+ return not bool(required_properties - used_properties)
800
+
671
801
 
672
802
  @dataclass
673
803
  class PipelineSchema(DMSSchema):
@@ -21,6 +21,9 @@ class DomainMetadata(BaseMetadata):
21
21
  role: ClassVar[RoleTypes] = RoleTypes.domain_expert
22
22
  creator: StrOrListType
23
23
 
24
+ def as_identifier(self) -> str:
25
+ return "DomainRules"
26
+
24
27
 
25
28
  class DomainProperty(SheetEntity):
26
29
  class_: ClassEntity = Field(alias="Class")
@@ -8,8 +8,8 @@ from pydantic.main import IncEx
8
8
  from rdflib import Namespace
9
9
 
10
10
  from cognite.neat.constants import PREFIXES
11
+ from cognite.neat.issues import MultiValueError
11
12
  from cognite.neat.rules import exceptions, issues
12
- from cognite.neat.rules.issues.base import MultiValueError
13
13
  from cognite.neat.rules.models._base import (
14
14
  BaseMetadata,
15
15
  BaseRules,
@@ -116,6 +116,9 @@ class InformationMetadata(BaseMetadata):
116
116
  def as_enum_model_type(cls, value: str) -> DataModelType:
117
117
  return DataModelType(value)
118
118
 
119
+ def as_identifier(self) -> str:
120
+ return f"{self.prefix}:{self.name}"
121
+
119
122
 
120
123
  class InformationClass(SheetEntity):
121
124
  """
@@ -2,29 +2,31 @@ from abc import ABC
2
2
  from dataclasses import dataclass, field
3
3
  from functools import total_ordering
4
4
 
5
- from cognite.neat.rules.issues import IssueList
5
+ from cognite.neat.issues import NeatIssueList
6
6
 
7
7
 
8
8
  @total_ordering
9
9
  @dataclass
10
10
  class UploadResultCore(ABC):
11
11
  name: str
12
+ error_messages: list[str] = field(default_factory=list)
13
+ issues: NeatIssueList = field(default_factory=NeatIssueList)
12
14
 
13
15
  def __lt__(self, other: object) -> bool:
14
- if isinstance(other, UploadResult):
16
+ if isinstance(other, UploadDiffsCount):
15
17
  return self.name < other.name
16
18
  else:
17
19
  return NotImplemented
18
20
 
19
21
  def __eq__(self, other: object) -> bool:
20
- if isinstance(other, UploadResult):
22
+ if isinstance(other, UploadDiffsCount):
21
23
  return self.name == other.name
22
24
  else:
23
25
  return NotImplemented
24
26
 
25
27
 
26
28
  @dataclass
27
- class UploadResult(UploadResultCore):
29
+ class UploadDiffsCount(UploadResultCore):
28
30
  created: int = 0
29
31
  deleted: int = 0
30
32
  changed: int = 0
@@ -33,8 +35,6 @@ class UploadResult(UploadResultCore):
33
35
  failed_created: int = 0
34
36
  failed_changed: int = 0
35
37
  failed_deleted: int = 0
36
- error_messages: list[str] = field(default_factory=list)
37
- issues: IssueList = field(default_factory=IssueList)
38
38
 
39
39
  @property
40
40
  def total(self) -> int:
@@ -64,3 +64,23 @@ class UploadResult(UploadResultCore):
64
64
  line.append(f"failed to delete {self.failed_deleted}")
65
65
 
66
66
  return f"{self.name.title()}: {', '.join(line)}"
67
+
68
+
69
+ @dataclass
70
+ class UploadResultIDs(UploadResultCore):
71
+ success: list[str] = field(default_factory=list)
72
+ failed: list[str] = field(default_factory=list)
73
+
74
+
75
+ @dataclass
76
+ class UploadDiffsID(UploadResultCore):
77
+ created: list[str] = field(default_factory=list)
78
+ changed: list[str] = field(default_factory=list)
79
+ unchanged: list[str] = field(default_factory=list)
80
+ failed: list[str] = field(default_factory=list)
81
+
82
+ def as_upload_result_ids(self) -> UploadResultIDs:
83
+ result = UploadResultIDs(name=self.name, error_messages=self.error_messages, issues=self.issues)
84
+ result.success = self.created + self.changed + self.unchanged
85
+ result.failed = self.failed
86
+ return result
@@ -355,3 +355,27 @@ def get_inheritance_path(child: Any, child_parent: dict[Any, list[Any]]) -> list
355
355
 
356
356
  def replace_non_alphanumeric_with_underscore(text):
357
357
  return re.sub(r"\W+", "_", text)
358
+
359
+
360
+ def string_to_ideal_type(input_string: str) -> int | bool | float | datetime | str:
361
+ try:
362
+ # Try converting to int
363
+ return int(input_string)
364
+ except ValueError:
365
+ try:
366
+ # Try converting to float
367
+ return float(input_string) # type: ignore
368
+ except ValueError:
369
+ if input_string.lower() == "true":
370
+ # Return True if input is 'true'
371
+ return True
372
+ elif input_string.lower() == "false":
373
+ # Return False if input is 'false'
374
+ return False
375
+ else:
376
+ try:
377
+ # Try converting to datetime
378
+ return datetime.fromisoformat(input_string) # type: ignore
379
+ except ValueError:
380
+ # Return the input string if no conversion is possible
381
+ return input_string
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-neat
3
- Version: 0.78.3
3
+ Version: 0.78.5
4
4
  Summary: Knowledge graph transformation
5
5
  Home-page: https://cognite-neat.readthedocs-hosted.com/
6
6
  License: Apache-2.0
@@ -22,7 +22,7 @@ Requires-Dist: backports.strenum (>=1.2,<2.0) ; python_version < "3.11"
22
22
  Requires-Dist: cognite-sdk (>=7.37.0,<8.0.0)
23
23
  Requires-Dist: deepdiff
24
24
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0) ; python_version < "3.11"
25
- Requires-Dist: fastapi (>=0.110,<0.111)
25
+ Requires-Dist: fastapi (>=0,<1)
26
26
  Requires-Dist: google-api-python-client ; extra == "google" or extra == "all"
27
27
  Requires-Dist: google-auth-oauthlib ; extra == "google" or extra == "all"
28
28
  Requires-Dist: gspread ; extra == "google" or extra == "all"