tapestry-orm 0.0.2__tar.gz → 0.0.4__tar.gz

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,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tapestry-orm
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: A prototype ORM for SurrealDB
5
5
  Author-email: timothee@obrecht.xyz
6
6
  License-Expression: MIT
7
7
  Requires-Dist: more-itertools>=10.7.0
8
8
  Requires-Dist: pydantic>=2.11.7
9
- Requires-Dist: surrealdb>=1.0.6
9
+ Requires-Dist: surrealdb==2.0.0a1
10
10
  Requires-Dist: sphinx>=7.2.0 ; extra == 'docs'
11
11
  Requires-Dist: sphinx-rtd-theme>=2.0.0 ; extra == 'docs'
12
12
  Requires-Dist: sphinx-autodoc-typehints>=2.0.0 ; extra == 'docs'
@@ -15,7 +15,7 @@ Requires-Dist: myst-parser>=2.0.0 ; extra == 'docs'
15
15
  Requires-Dist: sphinx-autobuild>=2021.3.14 ; extra == 'docs'
16
16
  Requires-Dist: sphinx-autoapi>=3.0.0 ; extra == 'docs'
17
17
  Requires-Python: >=3.12
18
- Project-URL: Documentation, https://readthedocs.org
18
+ Project-URL: Documentation, https://tapestry.readthedocs.io/en/latest/
19
19
  Project-URL: Repository, https://gitlab.com/timete_OS/tapestry-orm
20
20
  Provides-Extra: docs
21
21
  Description-Content-Type: text/markdown
@@ -338,9 +338,6 @@ Works with **mypy** and **pyright** for catching errors at build time!
338
338
  # Run all tests
339
339
  uv run pytest
340
340
 
341
- # Run with coverage
342
- uv run pytest --cov=tapestry
343
-
344
341
  # Run specific test
345
342
  uv run pytest tests/test_complete.py::TestWorkflow::test_select_queries
346
343
  ```
@@ -363,11 +360,7 @@ This gives you an identical testing experience to the actual CI pipeline, ensuri
363
360
  ### Type Checking
364
361
 
365
362
  ```bash
366
- # With mypy
367
- uv run mypy src/tapestry
368
-
369
- # With pyright
370
- uv run pyright src/tapestry
363
+ uv run basedpyright src/tapestry
371
364
  ```
372
365
 
373
366
  ### Building Documentation
@@ -316,9 +316,6 @@ Works with **mypy** and **pyright** for catching errors at build time!
316
316
  # Run all tests
317
317
  uv run pytest
318
318
 
319
- # Run with coverage
320
- uv run pytest --cov=tapestry
321
-
322
319
  # Run specific test
323
320
  uv run pytest tests/test_complete.py::TestWorkflow::test_select_queries
324
321
  ```
@@ -341,11 +338,7 @@ This gives you an identical testing experience to the actual CI pipeline, ensuri
341
338
  ### Type Checking
342
339
 
343
340
  ```bash
344
- # With mypy
345
- uv run mypy src/tapestry
346
-
347
- # With pyright
348
- uv run pyright src/tapestry
341
+ uv run basedpyright src/tapestry
349
342
  ```
350
343
 
351
344
  ### Building Documentation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tapestry-orm"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  description = "A prototype ORM for SurrealDB"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -13,11 +13,11 @@ requires-python = ">=3.12"
13
13
  dependencies = [
14
14
  "more-itertools>=10.7.0",
15
15
  "pydantic>=2.11.7",
16
- "surrealdb>=1.0.6",
16
+ "surrealdb==2.0.0-alpha.1",
17
17
  ]
18
18
  [project.urls]
19
19
  Repository = "https://gitlab.com/timete_OS/tapestry-orm"
20
- Documentation = "https://readthedocs.org"
20
+ Documentation = "https://tapestry.readthedocs.io/en/latest/"
21
21
 
22
22
  [project.optional-dependencies]
23
23
  docs = [
@@ -1,11 +1,9 @@
1
- __all__ = [
2
- "Base", "Text", "create_engine", "create_engine_context", "Engine", "Reference", "Node", "Edge", "Q"
3
- ]
1
+ __all__ = ["Base", "Text", "create_engine", "create_engine_context", "Engine", "Reference", "Indexed", "Node", "Edge", "Q"]
4
2
 
5
3
  from .query import Q
6
4
  from .base import Base
7
5
  from .node import Node
8
6
  from .edge import Edge
9
7
  from .tokenizer import Text
10
- from .table import Reference
8
+ from .table import Reference, Indexed
11
9
  from .engine import create_engine, create_engine_context, Engine
@@ -9,7 +9,7 @@ from copy import deepcopy
9
9
  from itertools import chain
10
10
  from queue import LifoQueue
11
11
  from surrealdb import RecordID
12
- from typing import Any, ClassVar, Unpack, get_origin, get_args
12
+ from typing import Any, ClassVar, Unpack, get_origin, get_args
13
13
  from pydantic_core.core_schema import ValidationInfo
14
14
  from pydantic.fields import FieldInfo, ComputedFieldInfo
15
15
  from pydantic import BaseModel, field_validator, model_serializer, Field, ConfigDict
@@ -57,12 +57,13 @@ class Base(BaseModel):
57
57
  - Handles enum serialization automatically
58
58
  - Provides full-text search capabilities with Text fields
59
59
  """
60
- id: RecordID | None = Field(exclude=False, default = None)
60
+
61
+ id: RecordID | None = Field(exclude=False, default=None)
61
62
 
62
63
  model_config: ClassVar[ConfigDict] = ConfigDict(
63
64
  arbitrary_types_allowed=True,
64
- use_enum_values=False, # would be great to have a 'use_enum_names'
65
- validate_assignment=True
65
+ use_enum_values=False, # would be great to have a 'use_enum_names'
66
+ validate_assignment=True,
66
67
  )
67
68
  _registry: ClassVar[tuple[Table, ...]] = ()
68
69
  _tokenizers: ClassVar[set[str]] = set()
@@ -70,9 +71,8 @@ class Base(BaseModel):
70
71
  _discarded: ClassVar[bool] = False
71
72
  child_classes: ClassVar[dict[str, type[Base]]] = {}
72
73
 
73
-
74
74
  @classmethod
75
- @field_validator('id')
75
+ @field_validator("id")
76
76
  def validate_record_id(cls, v: Any) -> RecordID | None:
77
77
  """
78
78
  Validate and convert record ID values.
@@ -94,7 +94,6 @@ class Base(BaseModel):
94
94
  # Convert string to RecordID if needed
95
95
  return RecordID(cls.__name__.lower(), v)
96
96
 
97
-
98
97
  @model_serializer(mode="wrap")
99
98
  def _serialize(self, serializer, info):
100
99
  """
@@ -125,8 +124,7 @@ class Base(BaseModel):
125
124
  # maybe check here in context if we are inserting in db
126
125
  return self.id
127
126
 
128
-
129
- @field_validator('*', mode='before')
127
+ @field_validator("*", mode="before")
130
128
  @classmethod
131
129
  def validate_enums(cls, v: Any, info: ValidationInfo) -> Any:
132
130
  """
@@ -165,7 +163,6 @@ class Base(BaseModel):
165
163
  # return stub
166
164
  return v
167
165
 
168
-
169
166
  def __init_subclass__(cls, **kwargs: Unpack[ConfigDict]):
170
167
  discarded = kwargs.pop("discarded", False)
171
168
  # Create unique queue for each subclass
@@ -174,7 +171,6 @@ class Base(BaseModel):
174
171
  cls._discarded = bool(discarded)
175
172
  return super().__init_subclass__(**kwargs)
176
173
 
177
-
178
174
  @classmethod
179
175
  def __pydantic_init_subclass__(cls, **kwargs):
180
176
  relation = kwargs.pop("relation", None)
@@ -190,7 +186,9 @@ class Base(BaseModel):
190
186
  for field_name, field_info in model_fields.items():
191
187
  field_type = field_info.annotation if isinstance(field_info, FieldInfo) else field_info.return_type
192
188
  if isinstance(field_info, ComputedFieldInfo):
193
- field_descriptor = ComputedFieldDescriptor[field_type](field_name, cls.__name__.lower(), field_type or Any, getattr(cls, field_name, None))
189
+ field_descriptor = ComputedFieldDescriptor[field_type](
190
+ field_name, cls.__name__.lower(), field_type or Any, getattr(cls, field_name, None)
191
+ )
194
192
  else:
195
193
  field_descriptor = CustomField[field_type](field_name, cls.__name__.lower(), field_type or Any)
196
194
  fields.append(field_descriptor)
@@ -199,15 +197,19 @@ class Base(BaseModel):
199
197
  nested_class = None
200
198
  try:
201
199
  # Check if it's a Base subclass directly
202
- if field_type and hasattr(field_type, '__mro__') and Base in field_type.__mro__:
200
+ if field_type and hasattr(field_type, "__mro__") and Base in field_type.__mro__:
203
201
  nested_class = field_type
204
202
  # Check if it's Optional[Base subclass] (Union with None)
205
- elif get_origin(field_type) is types.UnionType or (get_origin(field_type) is not None and hasattr(get_origin(field_type), '__name__') and get_origin(field_type).__name__ == 'UnionType'):
203
+ elif get_origin(field_type) is types.UnionType or (
204
+ get_origin(field_type) is not None
205
+ and hasattr(get_origin(field_type), "__name__")
206
+ and get_origin(field_type).__name__ == "UnionType"
207
+ ):
206
208
  # Get the args of the Union
207
209
  args = get_args(field_type)
208
210
  # Filter out None and check if any remaining type is a Base subclass
209
211
  for arg in args:
210
- if arg is not type(None) and hasattr(arg, '__mro__') and Base in arg.__mro__:
212
+ if arg is not type(None) and hasattr(arg, "__mro__") and Base in arg.__mro__:
211
213
  nested_class = arg
212
214
  break
213
215
 
@@ -223,8 +225,9 @@ class Base(BaseModel):
223
225
 
224
226
  # replace Base instance with Union[annotation, RecordID]
225
227
  if field_name in cls.model_fields:
226
- cls.model_fields[field_name].annotation = replace_type(cls.model_fields[field_name].annotation, Base, Base | RecordID)
227
-
228
+ cls.model_fields[field_name].annotation = replace_type(
229
+ cls.model_fields[field_name].annotation, Base, Base | RecordID
230
+ )
228
231
 
229
232
  table = Table(name=cls.__name__.lower(), base_class=Base, model_class=cls, fields=tuple(fields), relation=relation)
230
233
  for tokenizer in table.tokenizers:
@@ -235,7 +238,6 @@ class Base(BaseModel):
235
238
  Base.add_table(table, cls)
236
239
  cls.model_rebuild(force=True)
237
240
 
238
-
239
241
  @classmethod
240
242
  def registered_tables(cls) -> set[str]:
241
243
  """
@@ -251,7 +253,6 @@ class Base(BaseModel):
251
253
  """
252
254
  return {f.name for f in cls._registry}
253
255
 
254
-
255
256
  @classmethod
256
257
  def add_table(cls, table: Table, child_class: type[Base]):
257
258
  """
@@ -265,10 +266,9 @@ class Base(BaseModel):
265
266
  This is called automatically when subclasses are defined.
266
267
  Users typically don't need to call this directly.
267
268
  """
268
- cls._registry = tuple(t for t in chain(cls._registry, (table, )))
269
+ cls._registry = tuple(t for t in chain(cls._registry, (table,)))
269
270
  cls.child_classes[table.name] = child_class
270
271
 
271
-
272
272
  @classmethod
273
273
  def registered_models(cls) -> list[Table]:
274
274
  """
@@ -284,7 +284,6 @@ class Base(BaseModel):
284
284
  """
285
285
  return list(cls._registry)
286
286
 
287
-
288
287
  @classmethod
289
288
  def generate_schema(cls) -> str:
290
289
  """
@@ -314,7 +313,6 @@ class Base(BaseModel):
314
313
  blocks = [t.generate_table_sql() for t in cls._registry]
315
314
  return "\n\n".join(chain(cls._tokenizers, blocks))
316
315
 
317
-
318
316
  @classmethod
319
317
  def deserialize_record(cls, data: dict) -> Any:
320
318
  """
@@ -331,7 +329,7 @@ class Base(BaseModel):
331
329
  if no matching model is found
332
330
 
333
331
  Notes:
334
- - Handles edge records by converting 'in' and 'out' to 'in_' and 'out_'
332
+ - Handles edge records by converting ``in`` and ``out`` to ``in_`` and ``out_``
335
333
  - Automatically determines the model class from the record ID
336
334
  - Validates data using Pydantic validation
337
335
  """
@@ -343,7 +341,7 @@ class Base(BaseModel):
343
341
  data["out_"] = data.pop("out", None)
344
342
 
345
343
  # Get the model class from the record ID
346
- record_id = data.get('id')
344
+ record_id = data.get("id")
347
345
  if not record_id or not isinstance(record_id, RecordID):
348
346
  return data
349
347
 
@@ -355,7 +353,6 @@ class Base(BaseModel):
355
353
  # here maybe specify if the users wants related records as ids or full objects
356
354
  return model_class.model_validate(data, context="could this parameter be of any use ?")
357
355
 
358
-
359
356
  @classmethod
360
357
  def deserialize_response(cls, response: Any) -> Any:
361
358
  """
@@ -381,7 +378,6 @@ class Base(BaseModel):
381
378
  return cls.deserialize_record(response)
382
379
  return response
383
380
 
384
-
385
381
  def db_dump(self) -> dict[str, Any]:
386
382
  """
387
383
  Serialize the model instance for database insertion/update.
@@ -400,6 +396,6 @@ class Base(BaseModel):
400
396
  >>> await db.create("person", data)
401
397
  """
402
398
  serialized = self.model_dump(context={"root": True})
403
- serialized.pop('id', None)
399
+ serialized.pop("id", None)
404
400
  # this convert_types fonction is only here
405
401
  return convert_types(serialized)
@@ -1,15 +1,13 @@
1
1
  from abc import ABC
2
- from copy import deepcopy
3
- from typing import Self, Iterable, get_type_hints, Any, ClassVar, TYPE_CHECKING
4
- from pydantic.fields import FieldInfo, ComputedFieldInfo
2
+ from typing import Self, Iterable, get_type_hints, Any
5
3
  from surrealdb import AsyncWsSurrealConnection, AsyncHttpSurrealConnection, RecordID
6
4
 
7
5
 
8
6
  from .base import Base
9
7
  from .node import Node
10
8
  from .utils import replace_type
11
- from .table import Table, Link, flatten_type
12
- from .field import Traversal, Direction, Condition, LogicalCondition, Field
9
+ from .table import Link, flatten_type
10
+ from .field import Traversal, Direction
13
11
 
14
12
 
15
13
  class Edge(Base, ABC, discarded=True):
@@ -18,14 +16,14 @@ class Edge(Base, ABC, discarded=True):
18
16
 
19
17
  Edge represents relationship tables in SurrealDB that connect two nodes.
20
18
  All relationship tables should inherit from this class and define
21
- 'in_' and 'out_' fields to specify the connected node types.
19
+ ``in_`` and ``out_`` fields to specify the connected node types.
22
20
 
23
21
  Class Attributes:
24
22
  _directed (bool): Whether the relationship is directional (default: True)
25
23
 
26
24
  Required Fields:
27
- in_: The source node of the relationship
28
- out_: The target node of the relationship
25
+ ``in_``: The source node of the relationship
26
+ ``out_``: The target node of the relationship
29
27
 
30
28
  Example:
31
29
  >>> from tapestry import Edge, Node
@@ -54,12 +52,13 @@ class Edge(Base, ABC, discarded=True):
54
52
  >>> await belongs.relate(db)
55
53
 
56
54
  Notes:
57
- - Requires both 'in_' and 'out_' fields to be defined
55
+ - Requires both ``in_`` and ``out_`` fields to be defined
58
56
  - Automatically creates SurrealDB RELATION tables
59
57
  - Supports directional and bidirectional relationships
60
- - Can have additional fields beyond in_ and out_
58
+ - Can have additional fields beyond ``in_`` and ``out_``
61
59
  - Use relate() instead of create() for edge records
62
60
  """
61
+
63
62
  # in_: Node
64
63
  # out_: Node
65
64
  #
@@ -76,7 +75,7 @@ class Edge(Base, ABC, discarded=True):
76
75
  **kwargs: Additional configuration passed to parent
77
76
 
78
77
  Raises:
79
- TypeError: If the subclass doesn't define both 'in_' and 'out_' fields
78
+ TypeError: If the subclass doesn't define both ``in_`` and ``out_`` fields
80
79
  """
81
80
  super().__init_subclass__(**kwargs)
82
81
 
@@ -84,20 +83,17 @@ class Edge(Base, ABC, discarded=True):
84
83
  hints = get_type_hints(cls)
85
84
 
86
85
  # Check if required fields are annotated
87
- required_fields = {'in_', 'out_'}
86
+ required_fields = {"in_", "out_"}
88
87
  missing_fields = required_fields - set(hints.keys())
89
88
  if missing_fields:
90
- raise TypeError(
91
- f"Class {cls.__name__} must have field annotations for: {missing_fields}"
92
- )
89
+ raise TypeError(f"Class {cls.__name__} must have field annotations for: {missing_fields}")
93
90
 
94
91
  cls._directed = directed
95
92
 
96
-
97
93
  @classmethod
98
94
  def __pydantic_init_subclass__(cls, **kwargs):
99
95
  relation = None
100
- in_, out_ = cls.model_fields.pop('in_', None), cls.model_fields.pop('out_', None)
96
+ in_, out_ = cls.model_fields.pop("in_", None), cls.model_fields.pop("out_", None)
101
97
  if in_ or out_:
102
98
  if in_ and out_:
103
99
  assert in_.annotation
@@ -119,14 +115,12 @@ class Edge(Base, ABC, discarded=True):
119
115
  cls.model_fields["out_"] = out_
120
116
  cls.model_rebuild(force=True)
121
117
 
122
-
123
-
124
118
  def db_dump(self) -> dict[str, Any]:
125
119
  """
126
120
  Serialize the edge instance for database insertion.
127
121
 
128
122
  Converts Python field names to SurrealDB format by renaming
129
- 'in_' to 'in' and 'out_' to 'out'.
123
+ ``in_`` to ``in`` and ``out_`` to ``out``.
130
124
 
131
125
  Returns:
132
126
  dict[str, Any]: Dictionary ready for SurrealDB relation operations
@@ -136,11 +130,7 @@ class Edge(Base, ABC, discarded=True):
136
130
  dump["out"] = dump.pop("out_")
137
131
  return dump
138
132
 
139
-
140
- async def relate(
141
- self,
142
- db: AsyncWsSurrealConnection | AsyncHttpSurrealConnection
143
- ) -> Self:
133
+ async def relate(self, db: AsyncWsSurrealConnection | AsyncHttpSurrealConnection) -> Self:
144
134
  """
145
135
  Create a relationship record in the database.
146
136
 
@@ -168,29 +158,22 @@ class Edge(Base, ABC, discarded=True):
168
158
  Exception: If called on a non-relation table
169
159
 
170
160
  Notes:
171
- - Both in_ and out_ nodes must exist in the database
161
+ - Both ``in_`` and ``out_`` nodes must exist in the database
172
162
  - Creates a directed or bidirectional edge based on class configuration
173
163
  - The edge record gets a unique ID from SurrealDB
174
164
  """
175
165
  if not self.is_relation:
176
166
  raise Exception("You should use .create() to create a record")
177
167
  thing = self.__class__.__name__.lower()
178
- relation = await db.insert_relation(
179
- thing,
180
- self.db_dump()
181
- )
168
+ relation = await db.insert_relation(thing, self.db_dump())
182
169
  if isinstance(relation, list):
183
170
  self.id = relation[0]["id"]
184
171
  else:
185
172
  self.id = relation["id"]
186
173
  return self
187
174
 
188
-
189
175
  @classmethod
190
- async def insert(
191
- cls,
192
- db: AsyncWsSurrealConnection | AsyncHttpSurrealConnection, others: Iterable[Self]
193
- ) -> list[Self]:
176
+ async def insert(cls, db: AsyncWsSurrealConnection | AsyncHttpSurrealConnection, others: Iterable[Self]) -> list[Self]:
194
177
  """
195
178
  Batch insert multiple edge records into the database.
196
179
 
@@ -218,10 +201,7 @@ class Edge(Base, ABC, discarded=True):
218
201
  - Original instances are modified with IDs
219
202
  """
220
203
  others = list(others)
221
- inserted = await db.insert_relation(
222
- cls.__name__.lower(),
223
- [other.db_dump() for other in others]
224
- )
204
+ inserted = await db.insert_relation(cls.__name__.lower(), [other.db_dump() for other in others])
225
205
  for insert, original in zip(inserted, others):
226
206
  original.id = insert["id"]
227
207
  return others