surrealdb-orm 0.1.4__py3-none-any.whl → 0.5.0__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.
- surreal_orm/__init__.py +72 -3
- surreal_orm/aggregations.py +164 -0
- surreal_orm/auth/__init__.py +15 -0
- surreal_orm/auth/access.py +167 -0
- surreal_orm/auth/mixins.py +302 -0
- surreal_orm/cli/__init__.py +15 -0
- surreal_orm/cli/commands.py +369 -0
- surreal_orm/connection_manager.py +58 -18
- surreal_orm/fields/__init__.py +36 -0
- surreal_orm/fields/encrypted.py +166 -0
- surreal_orm/fields/relation.py +465 -0
- surreal_orm/migrations/__init__.py +51 -0
- surreal_orm/migrations/executor.py +380 -0
- surreal_orm/migrations/generator.py +272 -0
- surreal_orm/migrations/introspector.py +305 -0
- surreal_orm/migrations/migration.py +188 -0
- surreal_orm/migrations/operations.py +531 -0
- surreal_orm/migrations/state.py +406 -0
- surreal_orm/model_base.py +530 -44
- surreal_orm/query_set.py +609 -33
- surreal_orm/relations.py +645 -0
- surreal_orm/surreal_function.py +95 -0
- surreal_orm/surreal_ql.py +113 -0
- surreal_orm/types.py +86 -0
- surreal_sdk/README.md +79 -0
- surreal_sdk/__init__.py +151 -0
- surreal_sdk/connection/__init__.py +17 -0
- surreal_sdk/connection/base.py +516 -0
- surreal_sdk/connection/http.py +421 -0
- surreal_sdk/connection/pool.py +244 -0
- surreal_sdk/connection/websocket.py +519 -0
- surreal_sdk/exceptions.py +71 -0
- surreal_sdk/functions.py +607 -0
- surreal_sdk/protocol/__init__.py +13 -0
- surreal_sdk/protocol/rpc.py +218 -0
- surreal_sdk/py.typed +0 -0
- surreal_sdk/pyproject.toml +49 -0
- surreal_sdk/streaming/__init__.py +31 -0
- surreal_sdk/streaming/change_feed.py +278 -0
- surreal_sdk/streaming/live_query.py +265 -0
- surreal_sdk/streaming/live_select.py +369 -0
- surreal_sdk/transaction.py +386 -0
- surreal_sdk/types.py +346 -0
- surrealdb_orm-0.5.0.dist-info/METADATA +465 -0
- surrealdb_orm-0.5.0.dist-info/RECORD +52 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.0.dist-info}/WHEEL +1 -1
- surrealdb_orm-0.5.0.dist-info/entry_points.txt +2 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.0.dist-info}/licenses/LICENSE +1 -1
- surrealdb_orm-0.1.4.dist-info/METADATA +0 -184
- surrealdb_orm-0.1.4.dist-info/RECORD +0 -12
surreal_orm/__init__.py
CHANGED
|
@@ -1,12 +1,81 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
SurrealDB ORM - A Django-style ORM for SurrealDB.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- Model definitions with Pydantic validation
|
|
6
|
+
- Query building with fluent interface
|
|
7
|
+
- Migration system with version control
|
|
8
|
+
- JWT authentication support
|
|
9
|
+
- CLI tools for schema management
|
|
10
|
+
"""
|
|
11
|
+
|
|
2
12
|
from .connection_manager import SurrealDBConnectionManager
|
|
3
|
-
from .query_set import QuerySet
|
|
4
13
|
from .enum import OrderBy
|
|
14
|
+
from .model_base import (
|
|
15
|
+
BaseSurrealModel,
|
|
16
|
+
SurrealConfigDict,
|
|
17
|
+
SurrealDbError,
|
|
18
|
+
get_registered_models,
|
|
19
|
+
)
|
|
20
|
+
from .query_set import QuerySet
|
|
21
|
+
from .types import (
|
|
22
|
+
EncryptionAlgorithm,
|
|
23
|
+
FieldType,
|
|
24
|
+
SchemaMode,
|
|
25
|
+
TableType,
|
|
26
|
+
)
|
|
27
|
+
from .fields import Encrypted
|
|
28
|
+
from .fields import (
|
|
29
|
+
ForeignKey,
|
|
30
|
+
ManyToMany,
|
|
31
|
+
Relation,
|
|
32
|
+
RelationInfo,
|
|
33
|
+
get_relation_info,
|
|
34
|
+
is_foreign_key,
|
|
35
|
+
is_graph_relation,
|
|
36
|
+
is_many_to_many,
|
|
37
|
+
is_relation_field,
|
|
38
|
+
)
|
|
39
|
+
from .auth import AuthenticatedUserMixin
|
|
40
|
+
from .aggregations import Aggregation, Count, Sum, Avg, Min, Max
|
|
5
41
|
|
|
6
42
|
__all__ = [
|
|
43
|
+
# Connection
|
|
7
44
|
"SurrealDBConnectionManager",
|
|
45
|
+
# Models
|
|
8
46
|
"BaseSurrealModel",
|
|
47
|
+
"SurrealConfigDict",
|
|
48
|
+
"SurrealDbError",
|
|
49
|
+
"get_registered_models",
|
|
50
|
+
# Query
|
|
9
51
|
"QuerySet",
|
|
10
52
|
"OrderBy",
|
|
11
|
-
|
|
53
|
+
# Aggregations
|
|
54
|
+
"Aggregation",
|
|
55
|
+
"Count",
|
|
56
|
+
"Sum",
|
|
57
|
+
"Avg",
|
|
58
|
+
"Min",
|
|
59
|
+
"Max",
|
|
60
|
+
# Types
|
|
61
|
+
"TableType",
|
|
62
|
+
"SchemaMode",
|
|
63
|
+
"FieldType",
|
|
64
|
+
"EncryptionAlgorithm",
|
|
65
|
+
# Fields
|
|
66
|
+
"Encrypted",
|
|
67
|
+
# Relations
|
|
68
|
+
"ForeignKey",
|
|
69
|
+
"ManyToMany",
|
|
70
|
+
"Relation",
|
|
71
|
+
"RelationInfo",
|
|
72
|
+
"get_relation_info",
|
|
73
|
+
"is_foreign_key",
|
|
74
|
+
"is_graph_relation",
|
|
75
|
+
"is_many_to_many",
|
|
76
|
+
"is_relation_field",
|
|
77
|
+
# Auth
|
|
78
|
+
"AuthenticatedUserMixin",
|
|
12
79
|
]
|
|
80
|
+
|
|
81
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aggregation classes for QuerySet GROUP BY operations.
|
|
3
|
+
|
|
4
|
+
These classes are used with QuerySet.annotate() to compute aggregated values.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from surreal_orm import QuerySet
|
|
8
|
+
from surreal_orm.aggregations import Count, Sum, Avg
|
|
9
|
+
|
|
10
|
+
stats = await Order.objects().values("status").annotate(
|
|
11
|
+
count=Count(),
|
|
12
|
+
total=Sum("amount"),
|
|
13
|
+
avg_amount=Avg("amount"),
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Aggregation(ABC):
|
|
21
|
+
"""Base class for aggregation functions."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def to_surql(self, alias: str) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Convert the aggregation to SurrealQL syntax.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
alias: The alias for the result field.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: SurrealQL expression for the aggregation.
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def function_name(self) -> str:
|
|
39
|
+
"""Return the SurrealQL function name."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Count(Aggregation):
|
|
44
|
+
"""
|
|
45
|
+
Count aggregation.
|
|
46
|
+
|
|
47
|
+
Counts the number of records in each group.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
Count() # Counts all records
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def to_surql(self, alias: str) -> str:
|
|
54
|
+
return f"count() AS {alias}"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def function_name(self) -> str:
|
|
58
|
+
return "count"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Sum(Aggregation):
|
|
62
|
+
"""
|
|
63
|
+
Sum aggregation.
|
|
64
|
+
|
|
65
|
+
Calculates the sum of a numeric field.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
field: The field name to sum.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
Sum("amount") # Sums the "amount" field
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, field: str):
|
|
75
|
+
self.field = field
|
|
76
|
+
|
|
77
|
+
def to_surql(self, alias: str) -> str:
|
|
78
|
+
return f"math::sum({self.field}) AS {alias}"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def function_name(self) -> str:
|
|
82
|
+
return "math::sum"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Avg(Aggregation):
|
|
86
|
+
"""
|
|
87
|
+
Average aggregation.
|
|
88
|
+
|
|
89
|
+
Calculates the average of a numeric field.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
field: The field name to average.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
Avg("age") # Averages the "age" field
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, field: str):
|
|
99
|
+
self.field = field
|
|
100
|
+
|
|
101
|
+
def to_surql(self, alias: str) -> str:
|
|
102
|
+
return f"math::mean({self.field}) AS {alias}"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def function_name(self) -> str:
|
|
106
|
+
return "math::mean"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Min(Aggregation):
|
|
110
|
+
"""
|
|
111
|
+
Minimum aggregation.
|
|
112
|
+
|
|
113
|
+
Finds the minimum value of a field.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
field: The field name to find minimum of.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
Min("price") # Finds minimum "price"
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, field: str):
|
|
123
|
+
self.field = field
|
|
124
|
+
|
|
125
|
+
def to_surql(self, alias: str) -> str:
|
|
126
|
+
return f"math::min({self.field}) AS {alias}"
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def function_name(self) -> str:
|
|
130
|
+
return "math::min"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Max(Aggregation):
|
|
134
|
+
"""
|
|
135
|
+
Maximum aggregation.
|
|
136
|
+
|
|
137
|
+
Finds the maximum value of a field.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
field: The field name to find maximum of.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
Max("price") # Finds maximum "price"
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, field: str):
|
|
147
|
+
self.field = field
|
|
148
|
+
|
|
149
|
+
def to_surql(self, alias: str) -> str:
|
|
150
|
+
return f"math::max({self.field}) AS {alias}"
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def function_name(self) -> str:
|
|
154
|
+
return "math::max"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"Aggregation",
|
|
159
|
+
"Count",
|
|
160
|
+
"Sum",
|
|
161
|
+
"Avg",
|
|
162
|
+
"Min",
|
|
163
|
+
"Max",
|
|
164
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication module for SurrealDB ORM.
|
|
3
|
+
|
|
4
|
+
Provides JWT authentication support using SurrealDB's native
|
|
5
|
+
DEFINE ACCESS ... TYPE RECORD feature.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .access import AccessDefinition, AccessGenerator
|
|
9
|
+
from .mixins import AuthenticatedUserMixin
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AccessDefinition",
|
|
13
|
+
"AccessGenerator",
|
|
14
|
+
"AuthenticatedUserMixin",
|
|
15
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Access definition generator for SurrealDB authentication.
|
|
3
|
+
|
|
4
|
+
Generates DEFINE ACCESS ... TYPE RECORD statements for user authentication
|
|
5
|
+
using SurrealDB's built-in JWT support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ..types import EncryptionAlgorithm, TableType
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..model_base import BaseSurrealModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AccessDefinition:
|
|
19
|
+
"""
|
|
20
|
+
Configuration for generating DEFINE ACCESS statements.
|
|
21
|
+
|
|
22
|
+
This class represents the authentication configuration for a USER table,
|
|
23
|
+
including signup/signin logic and token durations.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
name: Access definition name (e.g., "user_auth")
|
|
27
|
+
table: Associated table name
|
|
28
|
+
identifier_field: Field used for signin (e.g., "email")
|
|
29
|
+
password_field: Field containing password
|
|
30
|
+
signup_fields: Dict of field -> expression for signup
|
|
31
|
+
signin_where: WHERE clause for signin validation
|
|
32
|
+
duration_token: JWT token lifetime (e.g., "15m")
|
|
33
|
+
duration_session: Session lifetime (e.g., "12h")
|
|
34
|
+
algorithm: Password hashing algorithm
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
table: str
|
|
39
|
+
identifier_field: str = "email"
|
|
40
|
+
password_field: str = "password"
|
|
41
|
+
signup_fields: dict[str, str] = field(default_factory=dict)
|
|
42
|
+
signin_where: str | None = None
|
|
43
|
+
duration_token: str = "15m"
|
|
44
|
+
duration_session: str = "12h"
|
|
45
|
+
algorithm: EncryptionAlgorithm = EncryptionAlgorithm.ARGON2
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
"""Build default signin_where if not provided."""
|
|
49
|
+
if not self.signin_where:
|
|
50
|
+
self.signin_where = (
|
|
51
|
+
f"{self.identifier_field} = ${self.identifier_field} AND "
|
|
52
|
+
f"crypto::{self.algorithm}::compare({self.password_field}, ${self.password_field})"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def to_surreal_ql(self) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Generate the DEFINE ACCESS SurrealQL statement.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Complete DEFINE ACCESS statement
|
|
61
|
+
"""
|
|
62
|
+
# Build signup SET clause
|
|
63
|
+
signup_sets = ", ".join(f"{field_name} = {expr}" for field_name, expr in self.signup_fields.items())
|
|
64
|
+
|
|
65
|
+
return f"""DEFINE ACCESS {self.name} ON DATABASE TYPE RECORD
|
|
66
|
+
SIGNUP (CREATE {self.table} SET {signup_sets})
|
|
67
|
+
SIGNIN (SELECT * FROM {self.table} WHERE {self.signin_where})
|
|
68
|
+
DURATION FOR TOKEN {self.duration_token}, FOR SESSION {self.duration_session};"""
|
|
69
|
+
|
|
70
|
+
def to_remove_ql(self) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Generate the REMOVE ACCESS SurrealQL statement.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
REMOVE ACCESS statement
|
|
76
|
+
"""
|
|
77
|
+
return f"REMOVE ACCESS {self.name} ON DATABASE;"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AccessGenerator:
|
|
81
|
+
"""
|
|
82
|
+
Generates AccessDefinition objects from User models.
|
|
83
|
+
|
|
84
|
+
This class inspects USER type models and generates the appropriate
|
|
85
|
+
DEFINE ACCESS configuration for authentication.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def from_model(model: type["BaseSurrealModel"]) -> AccessDefinition | None:
|
|
90
|
+
"""
|
|
91
|
+
Generate AccessDefinition from a model.
|
|
92
|
+
|
|
93
|
+
Only generates for USER type tables.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
model: The model class to generate access for
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
AccessDefinition if model is USER type, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
# Check if this is a USER table
|
|
102
|
+
table_type = model.get_table_type()
|
|
103
|
+
if table_type != TableType.USER:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
table_name = model.get_table_name()
|
|
107
|
+
identifier_field = model.get_identifier_field()
|
|
108
|
+
password_field = model.get_password_field()
|
|
109
|
+
|
|
110
|
+
# Get algorithm from config or default
|
|
111
|
+
config = getattr(model, "model_config", {})
|
|
112
|
+
algorithm_str = config.get("encryption_algorithm", "argon2")
|
|
113
|
+
try:
|
|
114
|
+
algorithm = EncryptionAlgorithm(algorithm_str)
|
|
115
|
+
except ValueError:
|
|
116
|
+
algorithm = EncryptionAlgorithm.ARGON2
|
|
117
|
+
|
|
118
|
+
# Build signup fields from model fields
|
|
119
|
+
signup_fields: dict[str, str] = {}
|
|
120
|
+
|
|
121
|
+
for field_name in model.model_fields:
|
|
122
|
+
if field_name == "id":
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if field_name == password_field:
|
|
126
|
+
# Password gets encrypted
|
|
127
|
+
signup_fields[field_name] = f"crypto::{algorithm}::generate(${field_name})"
|
|
128
|
+
else:
|
|
129
|
+
# Regular fields are passed through
|
|
130
|
+
signup_fields[field_name] = f"${field_name}"
|
|
131
|
+
|
|
132
|
+
# Add created_at if not in model
|
|
133
|
+
if "created_at" not in signup_fields:
|
|
134
|
+
signup_fields["created_at"] = "time::now()"
|
|
135
|
+
|
|
136
|
+
# Get durations from config
|
|
137
|
+
token_duration = config.get("token_duration", "15m")
|
|
138
|
+
session_duration = config.get("session_duration", "12h")
|
|
139
|
+
|
|
140
|
+
return AccessDefinition(
|
|
141
|
+
name=f"{table_name.lower()}_auth",
|
|
142
|
+
table=table_name,
|
|
143
|
+
identifier_field=identifier_field,
|
|
144
|
+
password_field=password_field,
|
|
145
|
+
signup_fields=signup_fields,
|
|
146
|
+
duration_token=token_duration,
|
|
147
|
+
duration_session=session_duration,
|
|
148
|
+
algorithm=algorithm,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def generate_all(models: list[type["BaseSurrealModel"]]) -> list[AccessDefinition]:
|
|
153
|
+
"""
|
|
154
|
+
Generate AccessDefinitions for all USER type models.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
models: List of model classes
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of AccessDefinition objects for USER tables
|
|
161
|
+
"""
|
|
162
|
+
definitions = []
|
|
163
|
+
for model in models:
|
|
164
|
+
definition = AccessGenerator.from_model(model)
|
|
165
|
+
if definition:
|
|
166
|
+
definitions.append(definition)
|
|
167
|
+
return definitions
|