iceaxe 0.8.3__cp313-cp313-macosx_11_0_arm64.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 iceaxe might be problematic. Click here for more details.

Files changed (75) hide show
  1. iceaxe/__init__.py +20 -0
  2. iceaxe/__tests__/__init__.py +0 -0
  3. iceaxe/__tests__/benchmarks/__init__.py +0 -0
  4. iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
  5. iceaxe/__tests__/benchmarks/test_select.py +114 -0
  6. iceaxe/__tests__/conf_models.py +133 -0
  7. iceaxe/__tests__/conftest.py +204 -0
  8. iceaxe/__tests__/docker_helpers.py +208 -0
  9. iceaxe/__tests__/helpers.py +268 -0
  10. iceaxe/__tests__/migrations/__init__.py +0 -0
  11. iceaxe/__tests__/migrations/conftest.py +36 -0
  12. iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
  13. iceaxe/__tests__/migrations/test_generator.py +140 -0
  14. iceaxe/__tests__/migrations/test_generics.py +91 -0
  15. iceaxe/__tests__/mountaineer/__init__.py +0 -0
  16. iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
  17. iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
  18. iceaxe/__tests__/schemas/__init__.py +0 -0
  19. iceaxe/__tests__/schemas/test_actions.py +1265 -0
  20. iceaxe/__tests__/schemas/test_cli.py +25 -0
  21. iceaxe/__tests__/schemas/test_db_memory_serializer.py +1571 -0
  22. iceaxe/__tests__/schemas/test_db_serializer.py +435 -0
  23. iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
  24. iceaxe/__tests__/test_alias.py +83 -0
  25. iceaxe/__tests__/test_base.py +52 -0
  26. iceaxe/__tests__/test_comparison.py +383 -0
  27. iceaxe/__tests__/test_field.py +11 -0
  28. iceaxe/__tests__/test_helpers.py +9 -0
  29. iceaxe/__tests__/test_modifications.py +151 -0
  30. iceaxe/__tests__/test_queries.py +764 -0
  31. iceaxe/__tests__/test_queries_str.py +173 -0
  32. iceaxe/__tests__/test_session.py +1511 -0
  33. iceaxe/__tests__/test_text_search.py +287 -0
  34. iceaxe/alias_values.py +67 -0
  35. iceaxe/base.py +351 -0
  36. iceaxe/comparison.py +560 -0
  37. iceaxe/field.py +263 -0
  38. iceaxe/functions.py +1432 -0
  39. iceaxe/generics.py +140 -0
  40. iceaxe/io.py +107 -0
  41. iceaxe/logging.py +91 -0
  42. iceaxe/migrations/__init__.py +5 -0
  43. iceaxe/migrations/action_sorter.py +98 -0
  44. iceaxe/migrations/cli.py +228 -0
  45. iceaxe/migrations/client_io.py +62 -0
  46. iceaxe/migrations/generator.py +404 -0
  47. iceaxe/migrations/migration.py +86 -0
  48. iceaxe/migrations/migrator.py +101 -0
  49. iceaxe/modifications.py +176 -0
  50. iceaxe/mountaineer/__init__.py +10 -0
  51. iceaxe/mountaineer/cli.py +74 -0
  52. iceaxe/mountaineer/config.py +46 -0
  53. iceaxe/mountaineer/dependencies/__init__.py +6 -0
  54. iceaxe/mountaineer/dependencies/core.py +67 -0
  55. iceaxe/postgres.py +133 -0
  56. iceaxe/py.typed +0 -0
  57. iceaxe/queries.py +1459 -0
  58. iceaxe/queries_str.py +294 -0
  59. iceaxe/schemas/__init__.py +0 -0
  60. iceaxe/schemas/actions.py +864 -0
  61. iceaxe/schemas/cli.py +30 -0
  62. iceaxe/schemas/db_memory_serializer.py +711 -0
  63. iceaxe/schemas/db_serializer.py +347 -0
  64. iceaxe/schemas/db_stubs.py +529 -0
  65. iceaxe/session.py +860 -0
  66. iceaxe/session_optimized.c +12207 -0
  67. iceaxe/session_optimized.cpython-313-darwin.so +0 -0
  68. iceaxe/session_optimized.pyx +212 -0
  69. iceaxe/sql_types.py +149 -0
  70. iceaxe/typing.py +73 -0
  71. iceaxe-0.8.3.dist-info/METADATA +262 -0
  72. iceaxe-0.8.3.dist-info/RECORD +75 -0
  73. iceaxe-0.8.3.dist-info/WHEEL +6 -0
  74. iceaxe-0.8.3.dist-info/licenses/LICENSE +21 -0
  75. iceaxe-0.8.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,176 @@
1
+ import logging
2
+ import traceback
3
+ from dataclasses import dataclass
4
+ from typing import Literal, Sequence, TypeVar
5
+
6
+ from iceaxe.base import TableBase
7
+ from iceaxe.logging import LOGGER
8
+
9
+ MODIFICATION_TRACKER_VERBOSITY = Literal["ERROR", "WARNING", "INFO"] | None
10
+ T = TypeVar("T", bound=TableBase)
11
+
12
+
13
+ @dataclass
14
+ class Modification:
15
+ """
16
+ Tracks a single modification to a database model instance, including stack trace information.
17
+
18
+ This class stores both the full stack trace and a simplified user-specific stack trace
19
+ that excludes library code. This helps with debugging by showing where in the user's
20
+ code a modification was made.
21
+
22
+ :param instance: The model instance that was modified
23
+ :param stack_trace: The complete stack trace at the time of modification
24
+ :param user_stack_trace: The most relevant user code stack trace, excluding library code
25
+ """
26
+
27
+ instance: TableBase
28
+
29
+ # The full stack trace of the modification.
30
+ stack_trace: str
31
+
32
+ # Most specific line of the stack trace that is part of the user's code.
33
+ user_stack_trace: str
34
+
35
+ @staticmethod
36
+ def get_current_stack_trace(
37
+ package_allow_list: list[str] | None = None,
38
+ package_deny_list: list[str] | None = None,
39
+ ) -> tuple[str, str]:
40
+ """
41
+ Get both the full stack trace and the most specific user code stack trace.
42
+
43
+ The user stack trace filters out library code and frozen code to focus on
44
+ the most relevant user code location where a modification occurred.
45
+
46
+ :return: A tuple containing (full_stack_trace, user_stack_trace)
47
+ :rtype: tuple[str, str]
48
+ """
49
+ stack = traceback.extract_stack()[:-1] # Remove the current frame
50
+ full_trace = "".join(traceback.format_list(stack))
51
+
52
+ # Find the most specific user code stack trace by filtering out library code
53
+ user_traces = [
54
+ frame
55
+ for frame in stack
56
+ if (
57
+ package_allow_list is None
58
+ or any(pkg in frame.filename for pkg in package_allow_list)
59
+ )
60
+ and (
61
+ package_deny_list is None
62
+ or not any(pkg in frame.filename for pkg in package_deny_list)
63
+ )
64
+ ]
65
+
66
+ user_trace = ""
67
+ if user_traces:
68
+ user_trace = "".join(traceback.format_list([user_traces[-1]]))
69
+
70
+ return full_trace, user_trace
71
+
72
+
73
+ class ModificationTracker:
74
+ """
75
+ Tracks modifications to database model instances and manages their lifecycle.
76
+
77
+ This class maintains a record of all modified model instances that haven't been
78
+ committed yet. It provides functionality to track new modifications, handle commits,
79
+ and log any remaining uncommitted modifications.
80
+
81
+ The tracker organizes modifications by model class and prevents duplicate tracking
82
+ of the same instance. It also captures stack traces at the point of modification
83
+ to help with debugging.
84
+ """
85
+
86
+ modified_models: dict[int, Modification]
87
+ """
88
+ Dictionary mapping model classes to lists of their modifications
89
+ """
90
+
91
+ verbosity: MODIFICATION_TRACKER_VERBOSITY | None
92
+ """
93
+ The logging level to use when reporting uncommitted modifications
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ verbosity: MODIFICATION_TRACKER_VERBOSITY | None = None,
99
+ known_first_party: list[str] | None = None,
100
+ ):
101
+ """
102
+ Initialize a new ModificationTracker.
103
+
104
+ Creates an empty modification tracking dictionary and sets the initial
105
+ verbosity level to None.
106
+ """
107
+ self.modified_models = {}
108
+ self.verbosity = verbosity
109
+ self.known_first_party = known_first_party
110
+
111
+ def track_modification(self, instance: TableBase) -> None:
112
+ """
113
+ Track a modification to a model instance along with its stack trace.
114
+
115
+ This method records a modification to a model instance if it hasn't already
116
+ been tracked. It captures both the full stack trace and a user-specific
117
+ stack trace at the point of modification.
118
+
119
+ :param instance: The model instance that was modified
120
+ :type instance: TableBase
121
+ """
122
+ # Get stack traces. By default we filter out all iceaxe code, but allow users to override this behavior
123
+ # if we want to still test this logic under test.
124
+ full_trace, user_trace = Modification.get_current_stack_trace(
125
+ package_allow_list=self.known_first_party,
126
+ package_deny_list=["iceaxe"] if not self.known_first_party else None,
127
+ )
128
+
129
+ # Only track if we haven't already tracked this instance
130
+ instance_id = id(instance)
131
+ if instance_id not in self.modified_models:
132
+ modification = Modification(
133
+ instance=instance, stack_trace=full_trace, user_stack_trace=user_trace
134
+ )
135
+ self.modified_models[instance_id] = modification
136
+
137
+ def clear_status(self, models: Sequence[TableBase]) -> None:
138
+ """
139
+ Remove models that are about to be committed from tracking.
140
+
141
+ This method should be called before committing changes to the database.
142
+ It removes the specified models from tracking since they will no longer
143
+ be in an uncommitted state.
144
+
145
+ :param models: List of model instances that will be committed
146
+ :type models: list[TableBase]
147
+ """
148
+ for instance in models:
149
+ instance_id = id(instance)
150
+ if instance_id in self.modified_models:
151
+ del self.modified_models[instance_id]
152
+
153
+ def log(self) -> None:
154
+ """
155
+ Log all uncommitted modifications with their stack traces.
156
+
157
+ This method logs information about all tracked modifications that haven't
158
+ been committed yet. The logging level is determined by the tracker's
159
+ verbosity setting. At the INFO level, it includes the full stack trace
160
+ in addition to the user stack trace.
161
+
162
+ If verbosity is not set (None), this method does nothing.
163
+ """
164
+ if not self.verbosity:
165
+ return
166
+
167
+ log_level = getattr(logging, self.verbosity)
168
+ LOGGER.setLevel(log_level) # Ensure logger will capture messages at this level
169
+
170
+ for mod in self.modified_models.values():
171
+ LOGGER.log(
172
+ log_level, f"Object modified locally but not committed: {mod.instance}"
173
+ )
174
+ LOGGER.log(log_level, f"Modified at:\n{mod.user_stack_trace}")
175
+ if self.verbosity == "INFO":
176
+ LOGGER.log(log_level, f"Full stack trace:\n{mod.stack_trace}")
@@ -0,0 +1,10 @@
1
+ from iceaxe.mountaineer import (
2
+ dependencies as DatabaseDependencies, # noqa: F401
3
+ )
4
+
5
+ from .cli import (
6
+ apply_migration as apply_migration,
7
+ generate_migration as generate_migration,
8
+ rollback_migration as rollback_migration,
9
+ )
10
+ from .config import DatabaseConfig as DatabaseConfig
@@ -0,0 +1,74 @@
1
+ """
2
+ Alternative entrypoints to migrations/cli that use Mountaineer configurations
3
+ to simplify the setup of the database connection.
4
+
5
+ """
6
+
7
+ from mountaineer import ConfigBase, CoreDependencies, Depends
8
+ from mountaineer.dependencies import get_function_dependencies
9
+
10
+ from iceaxe.migrations.cli import handle_apply, handle_generate, handle_rollback
11
+ from iceaxe.mountaineer.config import DatabaseConfig
12
+ from iceaxe.mountaineer.dependencies.core import get_db_connection
13
+ from iceaxe.session import DBConnection
14
+
15
+
16
+ async def generate_migration(message: str | None = None):
17
+ async def _inner(
18
+ db_config: DatabaseConfig = Depends(
19
+ CoreDependencies.get_config_with_type(DatabaseConfig)
20
+ ),
21
+ core_config: ConfigBase = Depends(
22
+ CoreDependencies.get_config_with_type(ConfigBase)
23
+ ),
24
+ db_connection: DBConnection = Depends(get_db_connection),
25
+ ):
26
+ if not core_config.PACKAGE:
27
+ raise ValueError("No package provided in the configuration")
28
+
29
+ await handle_generate(
30
+ package=core_config.PACKAGE,
31
+ db_connection=db_connection,
32
+ message=message,
33
+ )
34
+
35
+ async with get_function_dependencies(callable=_inner) as values:
36
+ await _inner(**values)
37
+
38
+
39
+ async def apply_migration():
40
+ async def _inner(
41
+ core_config: ConfigBase = Depends(
42
+ CoreDependencies.get_config_with_type(ConfigBase)
43
+ ),
44
+ db_connection: DBConnection = Depends(get_db_connection),
45
+ ):
46
+ if not core_config.PACKAGE:
47
+ raise ValueError("No package provided in the configuration")
48
+
49
+ await handle_apply(
50
+ package=core_config.PACKAGE,
51
+ db_connection=db_connection,
52
+ )
53
+
54
+ async with get_function_dependencies(callable=_inner) as values:
55
+ await _inner(**values)
56
+
57
+
58
+ async def rollback_migration():
59
+ async def _inner(
60
+ core_config: ConfigBase = Depends(
61
+ CoreDependencies.get_config_with_type(ConfigBase)
62
+ ),
63
+ db_connection: DBConnection = Depends(get_db_connection),
64
+ ):
65
+ if not core_config.PACKAGE:
66
+ raise ValueError("No package provided in the configuration")
67
+
68
+ await handle_rollback(
69
+ package=core_config.PACKAGE,
70
+ db_connection=db_connection,
71
+ )
72
+
73
+ async with get_function_dependencies(callable=_inner) as values:
74
+ await _inner(**values)
@@ -0,0 +1,46 @@
1
+ from pydantic_settings import BaseSettings
2
+
3
+ from iceaxe.modifications import MODIFICATION_TRACKER_VERBOSITY
4
+
5
+
6
+ class DatabaseConfig(BaseSettings):
7
+ """
8
+ Configuration settings for PostgreSQL database connection.
9
+ This class uses Pydantic's BaseSettings to manage environment-based configuration.
10
+ """
11
+
12
+ POSTGRES_HOST: str
13
+ """
14
+ The hostname where the PostgreSQL server is running.
15
+ This can be a domain name or IP address (e.g., 'localhost' or '127.0.0.1').
16
+ """
17
+
18
+ POSTGRES_USER: str
19
+ """
20
+ The username to authenticate with the PostgreSQL server.
21
+ This user should have appropriate permissions for the database operations.
22
+ """
23
+
24
+ POSTGRES_PASSWORD: str
25
+ """
26
+ The password for authenticating the PostgreSQL user.
27
+ This should be kept secure and not exposed in code or version control.
28
+ """
29
+
30
+ POSTGRES_DB: str
31
+ """
32
+ The name of the PostgreSQL database to connect to.
33
+ This database should exist on the server before attempting connection.
34
+ """
35
+
36
+ POSTGRES_PORT: int = 5432
37
+ """
38
+ The port number where PostgreSQL server is listening.
39
+ Defaults to the standard PostgreSQL port 5432 if not specified.
40
+ """
41
+
42
+ ICEAXE_UNCOMMITTED_VERBOSITY: MODIFICATION_TRACKER_VERBOSITY | None = None
43
+ """
44
+ The verbosity level for uncommitted modifications.
45
+ If set to None, uncommitted modifications will not be tracked.
46
+ """
@@ -0,0 +1,6 @@
1
+ """
2
+ Database dependencies for use in API endpoint routes.
3
+
4
+ """
5
+
6
+ from .core import get_db_connection as get_db_connection
@@ -0,0 +1,67 @@
1
+ """
2
+ Optional compatibility layer for `mountaineer` dependency access.
3
+
4
+ """
5
+
6
+ from typing import AsyncGenerator
7
+
8
+ import asyncpg
9
+ from mountaineer import CoreDependencies, Depends
10
+
11
+ from iceaxe.mountaineer.config import DatabaseConfig
12
+ from iceaxe.session import DBConnection
13
+
14
+
15
+ async def get_db_connection(
16
+ config: DatabaseConfig = Depends(
17
+ CoreDependencies.get_config_with_type(DatabaseConfig)
18
+ ),
19
+ ) -> AsyncGenerator[DBConnection, None]:
20
+ """
21
+ A dependency that provides a database connection for use in FastAPI endpoints or other
22
+ dependency-injected contexts. The connection is automatically closed when the endpoint
23
+ finishes processing.
24
+
25
+ This dependency:
26
+ - Creates a new PostgreSQL connection using the provided configuration
27
+ - Wraps it in a DBConnection for ORM functionality
28
+ - Initializes the connection's type cache to support enums without per-connection
29
+ type introspection
30
+ - Automatically closes the connection when done
31
+ - Integrates with Mountaineer's dependency injection system
32
+
33
+ :param config: DatabaseConfig instance containing connection parameters.
34
+ Automatically injected by Mountaineer if not provided.
35
+ :return: An async generator yielding a DBConnection instance
36
+
37
+ ```python
38
+ from fastapi import FastAPI, Depends
39
+ from iceaxe.mountaineer.dependencies import get_db_connection
40
+ from iceaxe.session import DBConnection
41
+
42
+ app = FastAPI()
43
+
44
+ # Basic usage in a FastAPI endpoint
45
+ @app.get("/users")
46
+ async def get_users(db: DBConnection = Depends(get_db_connection)):
47
+ users = await db.exec(select(User))
48
+ return users
49
+ ```
50
+ """
51
+ conn = await asyncpg.connect(
52
+ host=config.POSTGRES_HOST,
53
+ port=config.POSTGRES_PORT,
54
+ user=config.POSTGRES_USER,
55
+ password=config.POSTGRES_PASSWORD,
56
+ database=config.POSTGRES_DB,
57
+ )
58
+
59
+ connection = DBConnection(
60
+ conn, uncommitted_verbosity=config.ICEAXE_UNCOMMITTED_VERBOSITY
61
+ )
62
+ await connection.initialize_types()
63
+
64
+ try:
65
+ yield connection
66
+ finally:
67
+ await connection.close()
iceaxe/postgres.py ADDED
@@ -0,0 +1,133 @@
1
+ from enum import StrEnum
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class LexemePriority(StrEnum):
8
+ """Enum representing text search lexeme priority weights in Postgres."""
9
+
10
+ HIGHEST = "A"
11
+ HIGH = "B"
12
+ LOW = "C"
13
+ LOWEST = "D"
14
+
15
+
16
+ class PostgresFieldBase(BaseModel):
17
+ """
18
+ Extensions to python core types that specify addition arguments
19
+ used by Postgres.
20
+
21
+ """
22
+
23
+ pass
24
+
25
+
26
+ class PostgresDateTime(PostgresFieldBase):
27
+ """
28
+ Extension to Python's datetime type that specifies additional Postgres-specific configuration.
29
+ Used to customize the timezone behavior of datetime fields in Postgres.
30
+
31
+ ```python {{sticky: True}}
32
+ from iceaxe import Field, TableBase
33
+ class Event(TableBase):
34
+ id: int = Field(primary_key=True)
35
+ created_at: datetime = Field(postgres_config=PostgresDateTime(timezone=True))
36
+ ```
37
+ """
38
+
39
+ timezone: bool = False
40
+ """
41
+ Whether the datetime field should include timezone information in Postgres.
42
+ If True, maps to TIMESTAMP WITH TIME ZONE.
43
+ If False, maps to TIMESTAMP WITHOUT TIME ZONE.
44
+ Defaults to False.
45
+
46
+ """
47
+
48
+
49
+ class PostgresTime(PostgresFieldBase):
50
+ """
51
+ Extension to Python's time type that specifies additional Postgres-specific configuration.
52
+ Used to customize the timezone behavior of time fields in Postgres.
53
+
54
+ ```python {{sticky: True}}
55
+ from iceaxe import Field, TableBase
56
+ class Schedule(TableBase):
57
+ id: int = Field(primary_key=True)
58
+ start_time: time = Field(postgres_config=PostgresTime(timezone=True))
59
+ ```
60
+ """
61
+
62
+ timezone: bool = False
63
+ """
64
+ Whether the time field should include timezone information in Postgres.
65
+ If True, maps to TIME WITH TIME ZONE.
66
+ If False, maps to TIME WITHOUT TIME ZONE.
67
+ Defaults to False.
68
+
69
+ """
70
+
71
+
72
+ class PostgresFullText(PostgresFieldBase):
73
+ """
74
+ Extension to Python's string type that specifies additional Postgres-specific configuration
75
+ for full-text search. Used to customize the behavior of text search fields in Postgres.
76
+
77
+ ```python {{sticky: True}}
78
+ from iceaxe import TableBase, Field
79
+ from iceaxe.postgres import PostgresFullText, LexemePriority
80
+
81
+ class Article(TableBase):
82
+ id: int = Field(primary_key=True)
83
+ title: str = Field(postgres_config=PostgresFullText(
84
+ language="english",
85
+ weight=LexemePriority.HIGHEST # or "A"
86
+ ))
87
+ content: str = Field(postgres_config=PostgresFullText(
88
+ language="english",
89
+ weight=LexemePriority.HIGH # or "B"
90
+ ))
91
+ ```
92
+ """
93
+
94
+ language: str = "english"
95
+ """
96
+ The language to use for text search operations.
97
+ Defaults to 'english'.
98
+ """
99
+ weight: Literal["A", "B", "C", "D"] | LexemePriority = LexemePriority.HIGHEST
100
+ """
101
+ The weight to assign to matches in this column.
102
+ Can be specified either as a string literal ("A", "B", "C", "D") or using LexemePriority enum.
103
+ A/HIGHEST is highest priority, D/LOWEST is lowest priority.
104
+ Defaults to LexemePriority.HIGHEST (A).
105
+ """
106
+
107
+
108
+ ForeignKeyModifications = Literal[
109
+ "RESTRICT", "NO ACTION", "CASCADE", "SET DEFAULT", "SET NULL"
110
+ ]
111
+
112
+
113
+ class PostgresForeignKey(PostgresFieldBase):
114
+ """
115
+ Extension to Python's ForeignKey type that specifies additional Postgres-specific configuration.
116
+ Used to customize the behavior of foreign key constraints in Postgres.
117
+
118
+ ```python {{sticky: True}}
119
+ from iceaxe import TableBase, Field
120
+
121
+ class Office(TableBase):
122
+ id: int = Field(primary_key=True)
123
+ name: str
124
+
125
+ class Employee(TableBase):
126
+ id: int = Field(primary_key=True)
127
+ name: str
128
+ office_id: int = Field(foreign_key="office.id", postgres_config=PostgresForeignKey(on_delete="CASCADE", on_update="CASCADE"))
129
+ ```
130
+ """
131
+
132
+ on_delete: ForeignKeyModifications = "NO ACTION"
133
+ on_update: ForeignKeyModifications = "NO ACTION"
iceaxe/py.typed ADDED
File without changes