iceaxe 0.2.1__tar.gz → 0.2.3.dev1__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.
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/PKG-INFO +1 -1
- iceaxe-0.2.3.dev1/iceaxe/__tests__/conf_models.py +81 -0
- iceaxe-0.2.3.dev1/iceaxe/__tests__/mountaineer/dependencies/test_core.py +73 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/test_comparison.py +3 -3
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/test_field.py +5 -5
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/test_session.py +179 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/field.py +8 -2
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/mountaineer/dependencies/core.py +7 -2
- iceaxe-0.2.3.dev1/iceaxe/py.typed +0 -0
- iceaxe-0.2.3.dev1/iceaxe/schemas/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/session.py +120 -20
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/pyproject.toml +1 -1
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/setup.py +3 -1
- iceaxe-0.2.1/iceaxe/__tests__/conf_models.py +0 -41
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/LICENSE +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/README.md +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/build.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/.DS_Store +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/benchmarks/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/benchmarks/test_select.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/conftest.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/migrations/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/migrations/conftest.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/migrations/test_action_sorter.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/migrations/test_generator.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/migrations/test_generics.py +0 -0
- {iceaxe-0.2.1/iceaxe/__tests__/schemas → iceaxe-0.2.3.dev1/iceaxe/__tests__/mountaineer}/__init__.py +0 -0
- {iceaxe-0.2.1/iceaxe/schemas → iceaxe-0.2.3.dev1/iceaxe/__tests__/mountaineer/dependencies}/__init__.py +0 -0
- /iceaxe-0.2.1/iceaxe/py.typed → /iceaxe-0.2.3.dev1/iceaxe/__tests__/schemas/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/schemas/test_actions.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/schemas/test_cli.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/schemas/test_db_memory_serializer.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/schemas/test_db_serializer.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/schemas/test_db_stubs.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/test_base.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/__tests__/test_queries.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/base.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/comparison.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/functions.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/generics.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/io.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/logging.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/action_sorter.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/cli.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/client_io.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/generator.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/migration.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/migrations/migrator.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/mountaineer/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/mountaineer/cli.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/mountaineer/config.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/mountaineer/dependencies/__init__.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/postgres.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/queries.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/queries_str.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/schemas/actions.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/schemas/cli.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/schemas/db_memory_serializer.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/schemas/db_serializer.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/schemas/db_stubs.py +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/session_optimized.pyx +0 -0
- {iceaxe-0.2.1 → iceaxe-0.2.3.dev1}/iceaxe/typing.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyinstrument import Profiler
|
|
7
|
+
|
|
8
|
+
from iceaxe.base import Field, TableBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserDemo(TableBase):
|
|
12
|
+
id: int = Field(primary_key=True, default=None)
|
|
13
|
+
name: str
|
|
14
|
+
email: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ArtifactDemo(TableBase):
|
|
18
|
+
id: int = Field(primary_key=True, default=None)
|
|
19
|
+
title: str
|
|
20
|
+
user_id: int = Field(foreign_key="userdemo.id")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComplexDemo(TableBase):
|
|
24
|
+
id: int = Field(primary_key=True, default=None)
|
|
25
|
+
string_list: list[str]
|
|
26
|
+
json_data: dict[str, str] = Field(is_json=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Employee(TableBase):
|
|
30
|
+
id: int = Field(primary_key=True, default=None)
|
|
31
|
+
email: str = Field(unique=True)
|
|
32
|
+
first_name: str
|
|
33
|
+
last_name: str
|
|
34
|
+
department: str
|
|
35
|
+
salary: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Department(TableBase):
|
|
39
|
+
id: int = Field(primary_key=True, default=None)
|
|
40
|
+
name: str = Field(unique=True)
|
|
41
|
+
budget: float
|
|
42
|
+
location: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProjectAssignment(TableBase):
|
|
46
|
+
id: int = Field(primary_key=True, default=None)
|
|
47
|
+
employee_id: int = Field(foreign_key="employee.id")
|
|
48
|
+
project_name: str
|
|
49
|
+
role: str
|
|
50
|
+
start_date: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EmployeeStatus(StrEnum):
|
|
54
|
+
ACTIVE = "active"
|
|
55
|
+
INACTIVE = "inactive"
|
|
56
|
+
ON_LEAVE = "on_leave"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EmployeeMetadata(TableBase):
|
|
60
|
+
id: int = Field(primary_key=True, default=None)
|
|
61
|
+
employee_id: int = Field(foreign_key="employee.id")
|
|
62
|
+
status: EmployeeStatus
|
|
63
|
+
tags: list[str] = Field(is_json=True)
|
|
64
|
+
additional_info: dict[str, Any] = Field(is_json=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@contextmanager
|
|
68
|
+
def run_profile(request):
|
|
69
|
+
TESTS_ROOT = Path.cwd()
|
|
70
|
+
PROFILE_ROOT = TESTS_ROOT / ".profiles"
|
|
71
|
+
|
|
72
|
+
# Turn profiling on
|
|
73
|
+
profiler = Profiler()
|
|
74
|
+
profiler.start()
|
|
75
|
+
|
|
76
|
+
yield # Run test
|
|
77
|
+
|
|
78
|
+
profiler.stop()
|
|
79
|
+
PROFILE_ROOT.mkdir(exist_ok=True)
|
|
80
|
+
results_file = PROFILE_ROOT / f"{request.node.name}.html"
|
|
81
|
+
profiler.write_html(results_file)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import asyncpg
|
|
4
|
+
import pytest
|
|
5
|
+
from mountaineer import CoreDependencies
|
|
6
|
+
|
|
7
|
+
from iceaxe.mountaineer.config import DatabaseConfig
|
|
8
|
+
from iceaxe.mountaineer.dependencies.core import get_db_connection
|
|
9
|
+
from iceaxe.session import DBConnection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def mock_db_connect():
|
|
14
|
+
conn = AsyncMock(spec=asyncpg.Connection)
|
|
15
|
+
conn.close = AsyncMock()
|
|
16
|
+
|
|
17
|
+
with patch("asyncpg.connect", new_callable=AsyncMock) as mock:
|
|
18
|
+
mock.return_value = conn
|
|
19
|
+
yield mock
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_config():
|
|
24
|
+
return DatabaseConfig(
|
|
25
|
+
POSTGRES_HOST="test-host",
|
|
26
|
+
POSTGRES_PORT=5432,
|
|
27
|
+
POSTGRES_USER="test-user",
|
|
28
|
+
POSTGRES_PASSWORD="test-pass",
|
|
29
|
+
POSTGRES_DB="test-db",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_connection():
|
|
35
|
+
conn = AsyncMock(spec=asyncpg.Connection)
|
|
36
|
+
conn.close = AsyncMock()
|
|
37
|
+
return conn
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_get_db_connection_closes_after_yield(
|
|
42
|
+
mock_config: DatabaseConfig,
|
|
43
|
+
mock_connection: AsyncMock,
|
|
44
|
+
mock_db_connect: AsyncMock,
|
|
45
|
+
):
|
|
46
|
+
mock_get_config = MagicMock(return_value=mock_config)
|
|
47
|
+
CoreDependencies.get_config_with_type = mock_get_config
|
|
48
|
+
|
|
49
|
+
mock_db_connect.return_value = mock_connection
|
|
50
|
+
|
|
51
|
+
# Get the generator
|
|
52
|
+
db_gen = get_db_connection(mock_config)
|
|
53
|
+
|
|
54
|
+
# Get the connection
|
|
55
|
+
connection = await anext(db_gen) # noqa: F821
|
|
56
|
+
|
|
57
|
+
assert isinstance(connection, DBConnection)
|
|
58
|
+
assert connection.conn == mock_connection
|
|
59
|
+
mock_db_connect.assert_called_once_with(
|
|
60
|
+
host=mock_config.POSTGRES_HOST,
|
|
61
|
+
port=mock_config.POSTGRES_PORT,
|
|
62
|
+
user=mock_config.POSTGRES_USER,
|
|
63
|
+
password=mock_config.POSTGRES_PASSWORD,
|
|
64
|
+
database=mock_config.POSTGRES_DB,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Simulate the end of the generator's scope
|
|
68
|
+
try:
|
|
69
|
+
await db_gen.aclose()
|
|
70
|
+
except StopAsyncIteration:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
mock_connection.close.assert_called_once()
|
|
@@ -4,7 +4,7 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from iceaxe.base import TableBase
|
|
6
6
|
from iceaxe.comparison import ComparisonType, FieldComparison
|
|
7
|
-
from iceaxe.field import DBFieldClassDefinition,
|
|
7
|
+
from iceaxe.field import DBFieldClassDefinition, DBFieldInfo
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_comparison_type_enum():
|
|
@@ -24,7 +24,7 @@ def test_comparison_type_enum():
|
|
|
24
24
|
@pytest.fixture
|
|
25
25
|
def db_field():
|
|
26
26
|
return DBFieldClassDefinition(
|
|
27
|
-
root_model=TableBase, key="test_key", field_definition=
|
|
27
|
+
root_model=TableBase, key="test_key", field_definition=DBFieldInfo()
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
|
|
@@ -137,7 +137,7 @@ def test_compare(db_field: DBFieldClassDefinition):
|
|
|
137
137
|
3.14,
|
|
138
138
|
complex(1, 2),
|
|
139
139
|
DBFieldClassDefinition(
|
|
140
|
-
root_model=TableBase, key="other_key", field_definition=
|
|
140
|
+
root_model=TableBase, key="other_key", field_definition=DBFieldInfo()
|
|
141
141
|
),
|
|
142
142
|
],
|
|
143
143
|
)
|
|
@@ -4,13 +4,13 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from iceaxe.base import TableBase
|
|
6
6
|
from iceaxe.comparison import ComparisonType, FieldComparison
|
|
7
|
-
from iceaxe.field import DBFieldClassDefinition,
|
|
7
|
+
from iceaxe.field import DBFieldClassDefinition, DBFieldInfo
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.fixture
|
|
11
11
|
def db_field():
|
|
12
12
|
return DBFieldClassDefinition(
|
|
13
|
-
root_model=TableBase, key="test_key", field_definition=
|
|
13
|
+
root_model=TableBase, key="test_key", field_definition=DBFieldInfo()
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
@@ -107,7 +107,7 @@ def test_compare(db_field: DBFieldClassDefinition):
|
|
|
107
107
|
3.14,
|
|
108
108
|
complex(1, 2),
|
|
109
109
|
DBFieldClassDefinition(
|
|
110
|
-
root_model=TableBase, key="other_key", field_definition=
|
|
110
|
+
root_model=TableBase, key="other_key", field_definition=DBFieldInfo()
|
|
111
111
|
),
|
|
112
112
|
],
|
|
113
113
|
)
|
|
@@ -132,8 +132,8 @@ def test_comparison_with_different_types(db_field: DBFieldClassDefinition, value
|
|
|
132
132
|
|
|
133
133
|
def test_db_field_class_definition_instantiation():
|
|
134
134
|
field_def = DBFieldClassDefinition(
|
|
135
|
-
root_model=TableBase, key="test_key", field_definition=
|
|
135
|
+
root_model=TableBase, key="test_key", field_definition=DBFieldInfo()
|
|
136
136
|
)
|
|
137
137
|
assert field_def.root_model == TableBase
|
|
138
138
|
assert field_def.key == "test_key"
|
|
139
|
-
assert isinstance(field_def.field_definition,
|
|
139
|
+
assert isinstance(field_def.field_definition, DBFieldInfo)
|
|
@@ -512,3 +512,182 @@ async def test_db_connection_insert_update_enum(db_connection: DBConnection):
|
|
|
512
512
|
result = await db_connection.conn.fetch("SELECT * FROM enumdemo")
|
|
513
513
|
assert len(result) == 1
|
|
514
514
|
assert result[0]["value"] == "b"
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
#
|
|
518
|
+
# Upsert
|
|
519
|
+
#
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@pytest.mark.asyncio
|
|
523
|
+
async def test_upsert_basic_insert(db_connection: DBConnection):
|
|
524
|
+
"""
|
|
525
|
+
Test basic insert when no conflict exists
|
|
526
|
+
|
|
527
|
+
"""
|
|
528
|
+
await db_connection.conn.execute(
|
|
529
|
+
"""
|
|
530
|
+
ALTER TABLE userdemo
|
|
531
|
+
ADD CONSTRAINT email_unique UNIQUE (email)
|
|
532
|
+
"""
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
user = UserDemo(name="John Doe", email="john@example.com")
|
|
536
|
+
result = await db_connection.upsert(
|
|
537
|
+
[user],
|
|
538
|
+
conflict_fields=(UserDemo.email,),
|
|
539
|
+
returning_fields=(UserDemo.id, UserDemo.name, UserDemo.email),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
assert result is not None
|
|
543
|
+
assert len(result) == 1
|
|
544
|
+
assert result[0][1] == "John Doe"
|
|
545
|
+
assert result[0][2] == "john@example.com"
|
|
546
|
+
|
|
547
|
+
# Verify in database
|
|
548
|
+
db_result = await db_connection.conn.fetch("SELECT * FROM userdemo")
|
|
549
|
+
assert len(db_result) == 1
|
|
550
|
+
assert db_result[0][1] == "John Doe"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@pytest.mark.asyncio
|
|
554
|
+
async def test_upsert_update_on_conflict(db_connection: DBConnection):
|
|
555
|
+
"""
|
|
556
|
+
Test update when conflict exists
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
await db_connection.conn.execute(
|
|
560
|
+
"""
|
|
561
|
+
ALTER TABLE userdemo
|
|
562
|
+
ADD CONSTRAINT email_unique UNIQUE (email)
|
|
563
|
+
"""
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# First insert
|
|
567
|
+
user = UserDemo(name="John Doe", email="john@example.com")
|
|
568
|
+
await db_connection.insert([user])
|
|
569
|
+
|
|
570
|
+
# Attempt upsert with same email but different name
|
|
571
|
+
new_user = UserDemo(name="Johnny Doe", email="john@example.com")
|
|
572
|
+
result = await db_connection.upsert(
|
|
573
|
+
[new_user],
|
|
574
|
+
conflict_fields=(UserDemo.email,),
|
|
575
|
+
update_fields=(UserDemo.name,),
|
|
576
|
+
returning_fields=(UserDemo.id, UserDemo.name, UserDemo.email),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
assert result is not None
|
|
580
|
+
assert len(result) == 1
|
|
581
|
+
assert result[0][1] == "Johnny Doe"
|
|
582
|
+
|
|
583
|
+
# Verify only one record exists
|
|
584
|
+
db_result = await db_connection.conn.fetch("SELECT * FROM userdemo")
|
|
585
|
+
assert len(db_result) == 1
|
|
586
|
+
assert db_result[0]["name"] == "Johnny Doe"
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@pytest.mark.asyncio
|
|
590
|
+
async def test_upsert_do_nothing_on_conflict(db_connection: DBConnection):
|
|
591
|
+
"""
|
|
592
|
+
Test DO NOTHING behavior when no update_fields specified
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
await db_connection.conn.execute(
|
|
596
|
+
"""
|
|
597
|
+
ALTER TABLE userdemo
|
|
598
|
+
ADD CONSTRAINT email_unique UNIQUE (email)
|
|
599
|
+
"""
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# First insert
|
|
603
|
+
user = UserDemo(name="John Doe", email="john@example.com")
|
|
604
|
+
await db_connection.insert([user])
|
|
605
|
+
|
|
606
|
+
# Attempt upsert with same email but different name
|
|
607
|
+
new_user = UserDemo(name="Johnny Doe", email="john@example.com")
|
|
608
|
+
result = await db_connection.upsert(
|
|
609
|
+
[new_user],
|
|
610
|
+
conflict_fields=(UserDemo.email,),
|
|
611
|
+
returning_fields=(UserDemo.id, UserDemo.name, UserDemo.email),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Should return empty list as no update was performed
|
|
615
|
+
assert result == []
|
|
616
|
+
|
|
617
|
+
# Verify original record unchanged
|
|
618
|
+
db_result = await db_connection.conn.fetch("SELECT * FROM userdemo")
|
|
619
|
+
assert len(db_result) == 1
|
|
620
|
+
assert db_result[0][1] == "John Doe"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@pytest.mark.asyncio
|
|
624
|
+
async def test_upsert_multiple_objects(db_connection: DBConnection):
|
|
625
|
+
"""
|
|
626
|
+
Test upserting multiple objects at once
|
|
627
|
+
|
|
628
|
+
"""
|
|
629
|
+
await db_connection.conn.execute(
|
|
630
|
+
"""
|
|
631
|
+
ALTER TABLE userdemo
|
|
632
|
+
ADD CONSTRAINT email_unique UNIQUE (email)
|
|
633
|
+
"""
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
users = [
|
|
637
|
+
UserDemo(name="John Doe", email="john@example.com"),
|
|
638
|
+
UserDemo(name="Jane Doe", email="jane@example.com"),
|
|
639
|
+
]
|
|
640
|
+
result = await db_connection.upsert(
|
|
641
|
+
users,
|
|
642
|
+
conflict_fields=(UserDemo.email,),
|
|
643
|
+
returning_fields=(UserDemo.name, UserDemo.email),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
assert result is not None
|
|
647
|
+
assert len(result) == 2
|
|
648
|
+
assert {r[1] for r in result} == {"john@example.com", "jane@example.com"}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@pytest.mark.asyncio
|
|
652
|
+
async def test_upsert_empty_list(db_connection: DBConnection):
|
|
653
|
+
await db_connection.conn.execute(
|
|
654
|
+
"""
|
|
655
|
+
ALTER TABLE userdemo
|
|
656
|
+
ADD CONSTRAINT email_unique UNIQUE (email)
|
|
657
|
+
"""
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
"""Test upserting an empty list"""
|
|
661
|
+
result = await db_connection.upsert(
|
|
662
|
+
[], conflict_fields=(UserDemo.email,), returning_fields=(UserDemo.id,)
|
|
663
|
+
)
|
|
664
|
+
assert result is None
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@pytest.mark.asyncio
|
|
668
|
+
async def test_upsert_multiple_conflict_fields(db_connection: DBConnection):
|
|
669
|
+
"""
|
|
670
|
+
Test upserting with multiple conflict fields
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
await db_connection.conn.execute(
|
|
674
|
+
"""
|
|
675
|
+
ALTER TABLE userdemo
|
|
676
|
+
ADD CONSTRAINT email_unique UNIQUE (name, email)
|
|
677
|
+
"""
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
users = [
|
|
681
|
+
UserDemo(name="John Doe", email="john@example.com"),
|
|
682
|
+
UserDemo(name="John Doe", email="john@example.com"),
|
|
683
|
+
UserDemo(name="Jane Doe", email="jane@example.com"),
|
|
684
|
+
]
|
|
685
|
+
result = await db_connection.upsert(
|
|
686
|
+
users,
|
|
687
|
+
conflict_fields=(UserDemo.name, UserDemo.email),
|
|
688
|
+
returning_fields=(UserDemo.name, UserDemo.email),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
assert result is not None
|
|
692
|
+
assert len(result) == 2
|
|
693
|
+
assert {r[1] for r in result} == {"john@example.com", "jane@example.com"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from json import dumps as json_dumps
|
|
1
2
|
from typing import (
|
|
2
3
|
TYPE_CHECKING,
|
|
3
4
|
Any,
|
|
@@ -92,6 +93,11 @@ class DBFieldInfo(FieldInfo):
|
|
|
92
93
|
**field._attributes_set, # type: ignore
|
|
93
94
|
)
|
|
94
95
|
|
|
96
|
+
def to_db_value(self, value: Any):
|
|
97
|
+
if self.is_json:
|
|
98
|
+
return json_dumps(value)
|
|
99
|
+
return value
|
|
100
|
+
|
|
95
101
|
|
|
96
102
|
def __get_db_field(_: Callable[Concatenate[Any, P], Any] = PydanticField): # type: ignore
|
|
97
103
|
"""
|
|
@@ -136,13 +142,13 @@ def __get_db_field(_: Callable[Concatenate[Any, P], Any] = PydanticField): # ty
|
|
|
136
142
|
class DBFieldClassDefinition(ComparisonBase):
|
|
137
143
|
root_model: Type["TableBase"]
|
|
138
144
|
key: str
|
|
139
|
-
field_definition:
|
|
145
|
+
field_definition: DBFieldInfo
|
|
140
146
|
|
|
141
147
|
def __init__(
|
|
142
148
|
self,
|
|
143
149
|
root_model: Type["TableBase"],
|
|
144
150
|
key: str,
|
|
145
|
-
field_definition:
|
|
151
|
+
field_definition: DBFieldInfo,
|
|
146
152
|
):
|
|
147
153
|
self.root_model = root_model
|
|
148
154
|
self.key = key
|
|
@@ -3,6 +3,8 @@ Optional compatibility layer for `mountaineer` dependency access.
|
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from typing import AsyncGenerator
|
|
7
|
+
|
|
6
8
|
import asyncpg
|
|
7
9
|
from mountaineer import CoreDependencies, Depends
|
|
8
10
|
|
|
@@ -14,7 +16,7 @@ async def get_db_connection(
|
|
|
14
16
|
config: DatabaseConfig = Depends(
|
|
15
17
|
CoreDependencies.get_config_with_type(DatabaseConfig)
|
|
16
18
|
),
|
|
17
|
-
) -> DBConnection:
|
|
19
|
+
) -> AsyncGenerator[DBConnection, None]:
|
|
18
20
|
conn = await asyncpg.connect(
|
|
19
21
|
host=config.POSTGRES_HOST,
|
|
20
22
|
port=config.POSTGRES_PORT,
|
|
@@ -22,4 +24,7 @@ async def get_db_connection(
|
|
|
22
24
|
password=config.POSTGRES_PASSWORD,
|
|
23
25
|
database=config.POSTGRES_DB,
|
|
24
26
|
)
|
|
25
|
-
|
|
27
|
+
try:
|
|
28
|
+
yield DBConnection(conn)
|
|
29
|
+
finally:
|
|
30
|
+
await conn.close()
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from contextlib import asynccontextmanager
|
|
3
|
-
from json import dumps as json_dumps
|
|
4
3
|
from typing import (
|
|
5
4
|
Any,
|
|
6
5
|
Literal,
|
|
@@ -13,6 +12,7 @@ from typing import (
|
|
|
13
12
|
)
|
|
14
13
|
|
|
15
14
|
import asyncpg
|
|
15
|
+
from typing_extensions import TypeVarTuple
|
|
16
16
|
|
|
17
17
|
from iceaxe.base import TableBase
|
|
18
18
|
from iceaxe.logging import LOGGER
|
|
@@ -27,6 +27,7 @@ from iceaxe.session_optimized import optimize_exec_casting
|
|
|
27
27
|
|
|
28
28
|
P = ParamSpec("P")
|
|
29
29
|
T = TypeVar("T")
|
|
30
|
+
Ts = TypeVarTuple("Ts")
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class DBConnection:
|
|
@@ -94,23 +95,11 @@ class DBConnection:
|
|
|
94
95
|
return
|
|
95
96
|
|
|
96
97
|
for model, model_objects in self._aggregate_models_by_table(objects):
|
|
97
|
-
# We let the DB handle autoincrement keys
|
|
98
|
-
auto_increment_keys = [
|
|
99
|
-
field
|
|
100
|
-
for field, info in model.model_fields.items()
|
|
101
|
-
if info.autoincrement
|
|
102
|
-
]
|
|
103
|
-
|
|
104
98
|
table_name = QueryIdentifier(model.get_table_name())
|
|
105
|
-
fields =
|
|
106
|
-
field
|
|
99
|
+
fields = {
|
|
100
|
+
field: info
|
|
107
101
|
for field, info in model.model_fields.items()
|
|
108
|
-
if not info.exclude
|
|
109
|
-
and not info.autoincrement
|
|
110
|
-
and field not in auto_increment_keys
|
|
111
|
-
]
|
|
112
|
-
json_fields = {
|
|
113
|
-
field for field, info in model.model_fields.items() if info.is_json
|
|
102
|
+
if (not info.exclude and not info.autoincrement)
|
|
114
103
|
}
|
|
115
104
|
field_string = ", ".join(f'"{field}"' for field in fields)
|
|
116
105
|
primary_key = self._get_primary_key(model)
|
|
@@ -124,10 +113,8 @@ class DBConnection:
|
|
|
124
113
|
for obj in model_objects:
|
|
125
114
|
obj_values = obj.model_dump()
|
|
126
115
|
values = [
|
|
127
|
-
obj_values[field]
|
|
128
|
-
|
|
129
|
-
else json_dumps(obj_values[field])
|
|
130
|
-
for field in fields
|
|
116
|
+
info.to_db_value(obj_values[field])
|
|
117
|
+
for field, info in fields.items()
|
|
131
118
|
]
|
|
132
119
|
result = await self.conn.fetchrow(query, *values)
|
|
133
120
|
|
|
@@ -135,6 +122,119 @@ class DBConnection:
|
|
|
135
122
|
setattr(obj, primary_key, result[primary_key])
|
|
136
123
|
obj.clear_modified_attributes()
|
|
137
124
|
|
|
125
|
+
@overload
|
|
126
|
+
async def upsert(
|
|
127
|
+
self,
|
|
128
|
+
objects: Sequence[TableBase],
|
|
129
|
+
*,
|
|
130
|
+
conflict_fields: tuple[Any, ...],
|
|
131
|
+
update_fields: tuple[Any, ...] | None = None,
|
|
132
|
+
returning_fields: tuple[T, *Ts],
|
|
133
|
+
) -> list[tuple[T, *Ts]]: ...
|
|
134
|
+
|
|
135
|
+
@overload
|
|
136
|
+
async def upsert(
|
|
137
|
+
self,
|
|
138
|
+
objects: Sequence[TableBase],
|
|
139
|
+
*,
|
|
140
|
+
conflict_fields: tuple[Any, ...],
|
|
141
|
+
update_fields: tuple[Any, ...] | None = None,
|
|
142
|
+
returning_fields: None,
|
|
143
|
+
) -> None: ...
|
|
144
|
+
|
|
145
|
+
async def upsert(
|
|
146
|
+
self,
|
|
147
|
+
objects: Sequence[TableBase],
|
|
148
|
+
*,
|
|
149
|
+
conflict_fields: tuple[Any, ...],
|
|
150
|
+
update_fields: tuple[Any, ...] | None = None,
|
|
151
|
+
returning_fields: tuple[T, *Ts] | None = None,
|
|
152
|
+
) -> list[tuple[T, *Ts]] | None:
|
|
153
|
+
"""
|
|
154
|
+
Performs an upsert (INSERT ... ON CONFLICT DO UPDATE) operation for the given objects.
|
|
155
|
+
|
|
156
|
+
:param objects: Sequence of TableBase objects to upsert
|
|
157
|
+
:param conflict_fields: Fields to check for conflicts (ON CONFLICT)
|
|
158
|
+
:param update_fields: Fields to update on conflict. If None, updates all non-excluded fields
|
|
159
|
+
:param returning_fields: Fields to return after the operation. If None, returns nothing
|
|
160
|
+
|
|
161
|
+
:return List of dictionaries containing the returned fields if returning_fields is specified
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
if not objects:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# Evaluate column types
|
|
168
|
+
conflict_fields_cols = [field for field in conflict_fields if is_column(field)]
|
|
169
|
+
update_fields_cols = [
|
|
170
|
+
field for field in update_fields or [] if is_column(field)
|
|
171
|
+
]
|
|
172
|
+
returning_fields_cols = [
|
|
173
|
+
field for field in returning_fields or [] if is_column(field)
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
results: list[tuple[T, *Ts]] = []
|
|
177
|
+
async with self._ensure_transaction():
|
|
178
|
+
for model, model_objects in self._aggregate_models_by_table(objects):
|
|
179
|
+
table_name = QueryIdentifier(model.get_table_name())
|
|
180
|
+
fields = {
|
|
181
|
+
field: info
|
|
182
|
+
for field, info in model.model_fields.items()
|
|
183
|
+
if (not info.exclude and not info.autoincrement)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
field_string = ", ".join(f'"{field}"' for field in fields)
|
|
187
|
+
placeholders = ", ".join(f"${i}" for i in range(1, len(fields) + 1))
|
|
188
|
+
query = (
|
|
189
|
+
f"INSERT INTO {table_name} ({field_string}) VALUES ({placeholders})"
|
|
190
|
+
)
|
|
191
|
+
if conflict_fields_cols:
|
|
192
|
+
conflict_field_string = ", ".join(
|
|
193
|
+
f'"{field.key}"' for field in conflict_fields_cols
|
|
194
|
+
)
|
|
195
|
+
query += f" ON CONFLICT ({conflict_field_string})"
|
|
196
|
+
|
|
197
|
+
if update_fields_cols:
|
|
198
|
+
set_values = ", ".join(
|
|
199
|
+
f'"{field.key}" = EXCLUDED."{field.key}"'
|
|
200
|
+
for field in update_fields_cols
|
|
201
|
+
)
|
|
202
|
+
query += f" DO UPDATE SET {set_values}"
|
|
203
|
+
else:
|
|
204
|
+
query += " DO NOTHING"
|
|
205
|
+
|
|
206
|
+
if returning_fields_cols:
|
|
207
|
+
returning_string = ", ".join(
|
|
208
|
+
f'"{field.key}"' for field in returning_fields_cols
|
|
209
|
+
)
|
|
210
|
+
query += f" RETURNING {returning_string}"
|
|
211
|
+
|
|
212
|
+
# Execute for each object
|
|
213
|
+
for obj in model_objects:
|
|
214
|
+
obj_values = obj.model_dump()
|
|
215
|
+
values = [
|
|
216
|
+
info.to_db_value(obj_values[field])
|
|
217
|
+
for field, info in fields.items()
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
if returning_fields_cols:
|
|
221
|
+
result = await self.conn.fetchrow(query, *values)
|
|
222
|
+
if result:
|
|
223
|
+
results.append(
|
|
224
|
+
tuple(
|
|
225
|
+
[
|
|
226
|
+
result[field.key]
|
|
227
|
+
for field in returning_fields_cols
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
await self.conn.execute(query, *values)
|
|
233
|
+
|
|
234
|
+
obj.clear_modified_attributes()
|
|
235
|
+
|
|
236
|
+
return results if returning_fields_cols else None
|
|
237
|
+
|
|
138
238
|
async def update(self, objects: Sequence[TableBase]):
|
|
139
239
|
if not objects:
|
|
140
240
|
return
|
|
@@ -6,6 +6,8 @@ packages = \
|
|
|
6
6
|
'iceaxe.__tests__',
|
|
7
7
|
'iceaxe.__tests__.benchmarks',
|
|
8
8
|
'iceaxe.__tests__.migrations',
|
|
9
|
+
'iceaxe.__tests__.mountaineer',
|
|
10
|
+
'iceaxe.__tests__.mountaineer.dependencies',
|
|
9
11
|
'iceaxe.__tests__.schemas',
|
|
10
12
|
'iceaxe.migrations',
|
|
11
13
|
'iceaxe.mountaineer',
|
|
@@ -20,7 +22,7 @@ install_requires = \
|
|
|
20
22
|
|
|
21
23
|
setup_kwargs = {
|
|
22
24
|
'name': 'iceaxe',
|
|
23
|
-
'version': '0.2.
|
|
25
|
+
'version': '0.2.3.dev1',
|
|
24
26
|
'description': 'A modern, fast ORM for Python.',
|
|
25
27
|
'long_description': '# iceaxe\n\nA modern, fast ORM for Python. We have the following goals:\n\n- 🏎️ **Performance**: We want to exceed or match the fastest ORMs in Python. We want our ORM\nto be as close as possible to raw-[asyncpg](https://github.com/MagicStack/asyncpg) speeds. See the "Benchmarks" section for more.\n- 📝 **Typehinting**: Everything should be typehinted with expected types. Declare your data as you\nexpect in Python and it should bidirectionally sync to the database.\n- 🐘 **Postgres only**: Leverage native Postgres features and simplify the implementation.\n- ⚡ **Common things are easy, rare things are possible**: 99% of the SQL queries we write are\nvanilla SELECT/INSERT/UPDATEs. These should be natively supported by your ORM. If you\'re writing _really_\ncomplex queries, these are better done by hand so you can see exactly what SQL will be run.\n\nIceaxe is in early alpha. It\'s also an independent project. It\'s compatible with the [Mountaineer](https://github.com/piercefreeman/mountaineer) ecosystem, but you can use it in whatever\nproject and web framework you\'re using.\n\n## Installation\n\nIf you\'re using poetry to manage your dependencies:\n\n```bash\npoetry add iceaxe\n```\n\nOtherwise install with pip:\n\n```bash\npip install iceaxe\n```\n\n## Usage\n\nDefine your models as a `TableBase` subclass:\n\n```python\nfrom iceaxe import TableBase\n\nclass Person(TableBase):\n id: int\n name: str\n age: int\n```\n\nTableBase is a subclass of Pydantic\'s `BaseModel`, so you get all of the validation and Field customization\nout of the box. We provide our own `Field` constructor that adds database-specific configuration. For instance, to make the\n`id` field a primary key / auto-incrementing you can do:\n\n```python\nfrom iceaxe import Field\n\nclass Person(TableBase):\n id: int = Field(primary_key=True)\n name: str\n age: int\n```\n\nOkay now you have a model. How do you interact with it?\n\nDatabases are based on a few core primitives to insert data, update it, and fetch it out again.\nTo do so you\'ll need a _database connection_, which is a connection over the network from your code\nto your Postgres database. The `DBConnection` is the core class for all ORM actions against the database.\n\n```python\nfrom iceaxe import DBConnection\nimport asyncpg\n\nconn = DBConnection(\n await asyncpg.connect(\n host="localhost",\n port=5432,\n user="db_user",\n password="yoursecretpassword",\n database="your_db",\n )\n)\n```\n\nThe Person class currently just lives in memory. To back it with a full\ndatabase table, we can run raw SQL or run a migration to add it:\n\n```python\nawait conn.conn.execute(\n """\n CREATE TABLE IF NOT EXISTS person (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n age INT NOT NULL\n )\n """\n)\n```\n\n### Inserting Data\n\nInstantiate object classes as you normally do:\n\n```python\npeople = [\n Person(name="Alice", age=30),\n Person(name="Bob", age=40),\n Person(name="Charlie", age=50),\n]\nawait conn.insert(people)\n\nprint(people[0].id) # 1\nprint(people[1].id) # 2\n```\n\nBecause we\'re using an auto-incrementing primary key, the `id` field will be populated after the insert.\nIceaxe will automatically update the object in place with the newly assigned value.\n\n### Updating data\n\nNow that we have these lovely people, let\'s modify them.\n\n```python\nperson = people[0]\nperson.name = "Blice"\n```\n\nRight now, we have a Python object that\'s out of state with the database. But that\'s often okay. We can inspect it\nand further write logic - it\'s fully decoupled from the database.\n\n```python\ndef ensure_b_letter(person: Person):\n if person.name[0].lower() != "b":\n raise ValueError("Name must start with \'B\'")\n\nensure_b_letter(person)\n```\n\nTo sync the values back to the database, we can call `update`:\n\n```python\nawait conn.update([person])\n```\n\nIf we were to query the database directly, we see that the name has been updated:\n\n```\nid | name | age\n----+-------+-----\n 1 | Blice | 31\n 2 | Bob | 40\n 3 | Charlie | 50\n```\n\nBut no other fields have been touched. This lets a potentially concurrent process\nmodify `Alice`\'s record - say, updating the age to 31. By the time we update the data, we\'ll\nchange the name but nothing else. Under the hood we do this by tracking the fields that\nhave been modified in-memory and creating a targeted UPDATE to modify only those values.\n\n### Selecting data\n\nTo select data, we can use a `QueryBuilder`. For a shortcut to `select` query functions,\nyou can also just import select directly. This method takes the desired value parameters\nand returns a list of the desired objects.\n\n```python\nfrom iceaxe import select\n\nquery = select(Person).where(Person.name == "Blice", Person.age > 25)\nresults = await conn.exec(query)\n```\n\nIf we inspect the typing of `results`, we see that it\'s a `list[Person]` objects. This matches\nthe typehint of the `select` function. You can also target columns directly:\n\n```python\nquery = select((Person.id, Person.name)).where(Person.age > 25)\nresults = await conn.exec(query)\n```\n\nThis will return a list of tuples, where each tuple is the id and name of the person: `list[tuple[int, str]]`.\n\nWe support most of the common SQL operations. Just like the results, these are typehinted\nto their proper types as well. Static typecheckers and your IDE will throw an error if you try to compare\na string column to an integer, for instance. A more complex example of a query:\n\n```python\nquery = select((\n Person.id,\n FavoriteColor,\n)).join(\n FavoriteColor,\n Person.id == FavoriteColor.person_id,\n).where(\n Person.age > 25,\n Person.name == "Blice",\n).order_by(\n Person.age.desc(),\n).limit(10)\nresults = await conn.exec(query)\n```\n\nAs expected this will deliver results - and typehint - as a `list[tuple[int, FavoriteColor]]`\n\n## Production\n\n> [!IMPORTANT]\n> Iceaxe is in early alpha. We\'re using it internally and showly rolling out to our production\napplications, but we\'re not yet ready to recommend it for general use. The API and larger\nstability is subject to change.\n\nNote that underlying Postgres connection wrapped by `conn` will be alive for as long as your object is in memory. This uses up one\nof the allowable connections to your database. Your overall limit depends on your Postgres configuration\nor hosting provider, but most managed solutions top out around 150-300. If you need more concurrent clients\nconnected (and even if you don\'t - connection creation at the Postgres level is expensive), you can adopt\na load balancer like `pgbouncer` to better scale to traffic. More deployment notes to come.\n\nIt\'s also worth noting the absence of request pooling in this initialization. This is a feature of many ORMs that lets you limit\nthe overall connections you make to Postgres, and re-use these over time. We specifically don\'t offer request\npooling as part of Iceaxe, despite being supported by our underlying engine `asyncpg`. This is a bit more\naligned to how things should be structured in production. Python apps are always bound to one process thanks to\nthe GIL. So no matter what your connection pool will always be tied to the current Python process / runtime. When you\'re deploying onto a server with multiple cores, the pool will be duplicated across CPUs and largely defeats the purpose of capping\nnetwork connections in the first place.\n\n## Benchmarking\n\nWe have basic benchmarking tests in the `__tests__/benchmarks` directory. To run them, you\'ll need to execute the pytest suite:\n\n```bash\npoetry run pytest -m integration_tests\n```\n\nCurrent benchmarking as of October 11 2024 is:\n\n| | raw asyncpg | iceaxe | external overhead | |\n|-------------------|-------------|--------|-----------------------------------------------|---|\n| TableBase columns | 0.098s | 0.093s | | |\n| TableBase full | 0.164s | 1.345s | 10%: dict construction | 90%: pydantic overhead | |\n\n## Development\n\nIf you update your Cython implementation during development, you\'ll need to re-compile the Cython code. This can be done with\na simple poetry install. Poetry is set up to create a dynamic `setup.py` based on our `build.py` definition.\n\n```bash\npoetry install\n```\n\n## TODOs\n\n- [ ] Additional documentation with usage examples.\n',
|
|
26
28
|
'author': 'Pierce Freeman',
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from contextlib import contextmanager
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from pyinstrument import Profiler
|
|
5
|
-
|
|
6
|
-
from iceaxe.base import Field, TableBase
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class UserDemo(TableBase):
|
|
10
|
-
id: int = Field(primary_key=True, default=None)
|
|
11
|
-
name: str
|
|
12
|
-
email: str
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class ArtifactDemo(TableBase):
|
|
16
|
-
id: int = Field(primary_key=True, default=None)
|
|
17
|
-
title: str
|
|
18
|
-
user_id: int = Field(foreign_key="userdemo.id")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ComplexDemo(TableBase):
|
|
22
|
-
id: int = Field(primary_key=True, default=None)
|
|
23
|
-
string_list: list[str]
|
|
24
|
-
json_data: dict[str, str] = Field(is_json=True)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@contextmanager
|
|
28
|
-
def run_profile(request):
|
|
29
|
-
TESTS_ROOT = Path.cwd()
|
|
30
|
-
PROFILE_ROOT = TESTS_ROOT / ".profiles"
|
|
31
|
-
|
|
32
|
-
# Turn profiling on
|
|
33
|
-
profiler = Profiler()
|
|
34
|
-
profiler.start()
|
|
35
|
-
|
|
36
|
-
yield # Run test
|
|
37
|
-
|
|
38
|
-
profiler.stop()
|
|
39
|
-
PROFILE_ROOT.mkdir(exist_ok=True)
|
|
40
|
-
results_file = PROFILE_ROOT / f"{request.node.name}.html"
|
|
41
|
-
profiler.write_html(results_file)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iceaxe-0.2.1/iceaxe/__tests__/schemas → iceaxe-0.2.3.dev1/iceaxe/__tests__/mountaineer}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|