iceaxe 0.7.0.dev2__tar.gz → 0.7.1__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.

Potentially problematic release.


This version of iceaxe might be problematic. Click here for more details.

Files changed (81) hide show
  1. {iceaxe-0.7.0.dev2/iceaxe.egg-info → iceaxe-0.7.1}/PKG-INFO +1 -1
  2. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/conftest.py +97 -28
  3. iceaxe-0.7.1/iceaxe/__tests__/docker_helpers.py +208 -0
  4. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/migrations/test_action_sorter.py +1 -1
  5. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/test_actions.py +5 -5
  6. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/test_db_memory_serializer.py +2 -2
  7. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/test_db_serializer.py +46 -0
  8. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_comparison.py +27 -0
  9. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_session.py +32 -30
  10. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/migration.py +22 -2
  11. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/migrator.py +13 -0
  12. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/actions.py +2 -2
  13. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/db_memory_serializer.py +4 -4
  14. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/db_stubs.py +22 -16
  15. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/session_optimized.c +244 -68
  16. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/sql_types.py +33 -4
  17. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1/iceaxe.egg-info}/PKG-INFO +1 -1
  18. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe.egg-info/SOURCES.txt +1 -0
  19. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/pyproject.toml +2 -2
  20. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/LICENSE +0 -0
  21. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/MANIFEST.in +0 -0
  22. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/README.md +0 -0
  23. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__init__.py +0 -0
  24. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/__init__.py +0 -0
  25. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/benchmarks/__init__.py +0 -0
  26. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/benchmarks/test_bulk_insert.py +0 -0
  27. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/benchmarks/test_select.py +0 -0
  28. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/conf_models.py +0 -0
  29. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/helpers.py +0 -0
  30. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/migrations/__init__.py +0 -0
  31. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/migrations/conftest.py +0 -0
  32. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/migrations/test_generator.py +0 -0
  33. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/migrations/test_generics.py +0 -0
  34. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/mountaineer/__init__.py +0 -0
  35. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
  36. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/mountaineer/dependencies/test_core.py +0 -0
  37. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/__init__.py +0 -0
  38. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/test_cli.py +0 -0
  39. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/schemas/test_db_stubs.py +0 -0
  40. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_alias.py +0 -0
  41. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_base.py +0 -0
  42. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_field.py +0 -0
  43. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_helpers.py +0 -0
  44. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_modifications.py +0 -0
  45. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_queries.py +0 -0
  46. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_queries_str.py +0 -0
  47. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/__tests__/test_text_search.py +0 -0
  48. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/alias_values.py +0 -0
  49. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/base.py +0 -0
  50. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/comparison.py +0 -0
  51. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/field.py +0 -0
  52. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/functions.py +0 -0
  53. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/generics.py +0 -0
  54. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/io.py +0 -0
  55. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/logging.py +0 -0
  56. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/__init__.py +0 -0
  57. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/action_sorter.py +0 -0
  58. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/cli.py +0 -0
  59. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/client_io.py +0 -0
  60. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/migrations/generator.py +0 -0
  61. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/modifications.py +0 -0
  62. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/mountaineer/__init__.py +0 -0
  63. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/mountaineer/cli.py +0 -0
  64. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/mountaineer/config.py +0 -0
  65. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/mountaineer/dependencies/__init__.py +0 -0
  66. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/mountaineer/dependencies/core.py +0 -0
  67. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/postgres.py +0 -0
  68. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/py.typed +0 -0
  69. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/queries.py +0 -0
  70. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/queries_str.py +0 -0
  71. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/__init__.py +0 -0
  72. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/cli.py +0 -0
  73. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/schemas/db_serializer.py +0 -0
  74. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/session.py +0 -0
  75. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/session_optimized.pyx +0 -0
  76. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe/typing.py +0 -0
  77. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe.egg-info/dependency_links.txt +0 -0
  78. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe.egg-info/requires.txt +0 -0
  79. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/iceaxe.egg-info/top_level.txt +0 -0
  80. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/setup.cfg +0 -0
  81. {iceaxe-0.7.0.dev2 → iceaxe-0.7.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iceaxe
3
- Version: 0.7.0.dev2
3
+ Version: 0.7.1
4
4
  Summary: A modern, fast ORM for Python.
5
5
  Author-email: Pierce Freeman <pierce@freeman.vc>
6
6
  Requires-Python: >=3.11
@@ -1,84 +1,151 @@
1
+ import logging
2
+
1
3
  import asyncpg
2
4
  import pytest
3
5
  import pytest_asyncio
4
6
 
7
+ from iceaxe.__tests__ import docker_helpers
5
8
  from iceaxe.base import DBModelMetaclass
6
9
  from iceaxe.session import DBConnection
7
10
 
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @pytest.fixture(scope="session")
17
+ def docker_postgres():
18
+ """
19
+ Fixture that creates a PostgreSQL container using the Python Docker API.
20
+ This allows running individual tests without needing Docker Compose.
21
+ """
22
+ # Create and start a PostgreSQL container
23
+ postgres_container = docker_helpers.PostgresContainer()
24
+
25
+ # Start the container and yield connection details
26
+ connection_info = postgres_container.start()
27
+ yield connection_info
28
+
29
+ # Cleanup: stop the container
30
+ postgres_container.stop()
31
+
8
32
 
9
33
  @pytest_asyncio.fixture
10
- async def db_connection():
34
+ async def db_connection(docker_postgres):
35
+ """
36
+ Create a database connection using the PostgreSQL container.
37
+ """
11
38
  conn = DBConnection(
12
39
  await asyncpg.connect(
13
- host="localhost",
14
- port=5438,
15
- user="iceaxe",
16
- password="mysecretpassword",
17
- database="iceaxe_test_db",
40
+ host=docker_postgres["host"],
41
+ port=docker_postgres["port"],
42
+ user=docker_postgres["user"],
43
+ password=docker_postgres["password"],
44
+ database=docker_postgres["database"],
18
45
  )
19
46
  )
20
47
 
21
48
  # Drop all tables first to ensure clean state
22
- await conn.conn.execute("DROP TABLE IF EXISTS artifactdemo CASCADE")
23
- await conn.conn.execute("DROP TABLE IF EXISTS userdemo CASCADE")
24
- await conn.conn.execute("DROP TABLE IF EXISTS complexdemo CASCADE")
25
- await conn.conn.execute("DROP TABLE IF EXISTS article CASCADE")
49
+ known_tables = [
50
+ "artifactdemo",
51
+ "userdemo",
52
+ "complexdemo",
53
+ "article",
54
+ "employee",
55
+ "department",
56
+ "projectassignment",
57
+ "employeemetadata",
58
+ "functiondemomodel",
59
+ "demomodela",
60
+ "demomodelb",
61
+ "jsondemo",
62
+ "complextypedemo",
63
+ ]
64
+ known_types = ["statusenum", "employeestatus"]
65
+
66
+ for table in known_tables:
67
+ await conn.conn.execute(f"DROP TABLE IF EXISTS {table} CASCADE", timeout=30.0)
68
+
69
+ for known_type in known_types:
70
+ await conn.conn.execute(
71
+ f"DROP TYPE IF EXISTS {known_type} CASCADE", timeout=30.0
72
+ )
26
73
 
27
74
  # Create tables
28
- await conn.conn.execute("""
75
+ await conn.conn.execute(
76
+ """
29
77
  CREATE TABLE IF NOT EXISTS userdemo (
30
78
  id SERIAL PRIMARY KEY,
31
79
  name TEXT,
32
80
  email TEXT
33
81
  )
34
- """)
82
+ """,
83
+ timeout=30.0,
84
+ )
35
85
 
36
- await conn.conn.execute("""
86
+ await conn.conn.execute(
87
+ """
37
88
  CREATE TABLE IF NOT EXISTS artifactdemo (
38
89
  id SERIAL PRIMARY KEY,
39
90
  title TEXT,
40
91
  user_id INT REFERENCES userdemo(id)
41
92
  )
42
- """)
93
+ """,
94
+ timeout=30.0,
95
+ )
43
96
 
44
- await conn.conn.execute("""
97
+ await conn.conn.execute(
98
+ """
45
99
  CREATE TABLE IF NOT EXISTS complexdemo (
46
100
  id SERIAL PRIMARY KEY,
47
101
  string_list TEXT[],
48
102
  json_data JSON
49
103
  )
50
- """)
104
+ """,
105
+ timeout=30.0,
106
+ )
51
107
 
52
- await conn.conn.execute("""
108
+ await conn.conn.execute(
109
+ """
53
110
  CREATE TABLE IF NOT EXISTS article (
54
111
  id SERIAL PRIMARY KEY,
55
112
  title TEXT,
56
113
  content TEXT,
57
114
  summary TEXT
58
115
  )
59
- """)
116
+ """,
117
+ timeout=30.0,
118
+ )
60
119
 
61
120
  # Create each index separately to handle errors better
62
121
  yield conn
63
122
 
64
123
  # Drop all tables after tests
65
- await conn.conn.execute("DROP TABLE IF EXISTS artifactdemo CASCADE")
66
- await conn.conn.execute("DROP TABLE IF EXISTS userdemo CASCADE")
67
- await conn.conn.execute("DROP TABLE IF EXISTS complexdemo CASCADE")
68
- await conn.conn.execute("DROP TABLE IF EXISTS article CASCADE")
124
+ for table in known_tables:
125
+ await conn.conn.execute(f"DROP TABLE IF EXISTS {table} CASCADE", timeout=30.0)
126
+
127
+ # Drop all types after tests
128
+ for known_type in known_types:
129
+ await conn.conn.execute(
130
+ f"DROP TYPE IF EXISTS {known_type} CASCADE", timeout=30.0
131
+ )
132
+
69
133
  await conn.conn.close()
70
134
 
71
135
 
72
136
  @pytest_asyncio.fixture()
73
137
  async def indexed_db_connection(db_connection: DBConnection):
74
138
  await db_connection.conn.execute(
75
- "CREATE INDEX IF NOT EXISTS article_title_tsv_idx ON article USING GIN (to_tsvector('english', title))"
139
+ "CREATE INDEX IF NOT EXISTS article_title_tsv_idx ON article USING GIN (to_tsvector('english', title))",
140
+ timeout=30.0,
76
141
  )
77
142
  await db_connection.conn.execute(
78
- "CREATE INDEX IF NOT EXISTS article_content_tsv_idx ON article USING GIN (to_tsvector('english', content))"
143
+ "CREATE INDEX IF NOT EXISTS article_content_tsv_idx ON article USING GIN (to_tsvector('english', content))",
144
+ timeout=30.0,
79
145
  )
80
146
  await db_connection.conn.execute(
81
- "CREATE INDEX IF NOT EXISTS article_summary_tsv_idx ON article USING GIN (to_tsvector('english', summary))"
147
+ "CREATE INDEX IF NOT EXISTS article_summary_tsv_idx ON article USING GIN (to_tsvector('english', summary))",
148
+ timeout=30.0,
82
149
  )
83
150
 
84
151
  yield db_connection
@@ -88,7 +155,7 @@ async def indexed_db_connection(db_connection: DBConnection):
88
155
  async def clear_table(db_connection):
89
156
  # Clear all tables and reset sequences
90
157
  await db_connection.conn.execute(
91
- "TRUNCATE TABLE userdemo, article RESTART IDENTITY CASCADE"
158
+ "TRUNCATE TABLE userdemo, article RESTART IDENTITY CASCADE", timeout=30.0
92
159
  )
93
160
 
94
161
 
@@ -107,7 +174,8 @@ async def clear_all_database_objects(db_connection: DBConnection):
107
174
  EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
108
175
  END LOOP;
109
176
  END $$;
110
- """
177
+ """,
178
+ timeout=30.0,
111
179
  )
112
180
 
113
181
  # Step 2: Drop all custom types in the public schema
@@ -120,7 +188,8 @@ async def clear_all_database_objects(db_connection: DBConnection):
120
188
  EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
121
189
  END LOOP;
122
190
  END $$;
123
- """
191
+ """,
192
+ timeout=30.0,
124
193
  )
125
194
 
126
195
 
@@ -0,0 +1,208 @@
1
+ """
2
+ Docker helper utilities for testing.
3
+
4
+ This module provides classes and functions to manage Docker containers for testing,
5
+ particularly focusing on PostgreSQL database containers.
6
+ """
7
+
8
+ import logging
9
+ import socket
10
+ import time
11
+ import uuid
12
+ from typing import Any, Dict, Optional, cast
13
+
14
+ import docker
15
+ from docker.errors import APIError
16
+
17
+ # Configure logging
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def get_free_port() -> int:
22
+ """Find a free port on the host machine."""
23
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24
+ s.bind(("", 0))
25
+ return s.getsockname()[1]
26
+
27
+
28
+ class PostgresContainer:
29
+ """
30
+ A class that manages a PostgreSQL Docker container for testing.
31
+
32
+ This class handles the lifecycle of a PostgreSQL container, including:
33
+ - Starting the container with appropriate configuration
34
+ - Finding available ports
35
+ - Waiting for the container to be ready
36
+ - Providing connection information
37
+ - Cleaning up after tests
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ pg_user: str = "iceaxe",
43
+ pg_password: str = "mysecretpassword",
44
+ pg_db: str = "iceaxe_test_db",
45
+ postgres_version: str = "16",
46
+ ):
47
+ self.pg_user = pg_user
48
+ self.pg_password = pg_password
49
+ self.pg_db = pg_db
50
+ self.postgres_version = postgres_version
51
+ self.port = get_free_port()
52
+ self.container: Optional[Any] = None
53
+ self.client = docker.from_env()
54
+ self.container_name = f"iceaxe-postgres-test-{uuid.uuid4().hex[:8]}"
55
+
56
+ def start(self) -> Dict[str, Any]:
57
+ """
58
+ Start the PostgreSQL container.
59
+
60
+ Returns:
61
+ Dict[str, Any]: Connection information for the PostgreSQL container
62
+
63
+ Raises:
64
+ RuntimeError: If the container fails to start or become ready
65
+ """
66
+ logger.info(f"Starting PostgreSQL container on port {self.port}")
67
+
68
+ max_attempts = 3
69
+ attempt = 0
70
+
71
+ while attempt < max_attempts:
72
+ attempt += 1
73
+ try:
74
+ self.container = self._run_container(self.port)
75
+ break
76
+ except APIError as e:
77
+ if "port is already allocated" in str(e) and attempt < max_attempts:
78
+ logger.warning(
79
+ f"Port {self.port} is still in use. Trying with a new port (attempt {attempt}/{max_attempts})."
80
+ )
81
+ self.port = get_free_port()
82
+ else:
83
+ raise RuntimeError(f"Failed to start PostgreSQL container: {e}")
84
+
85
+ # Wait for PostgreSQL to be ready
86
+ if not self._wait_for_container_ready():
87
+ self.stop()
88
+ raise RuntimeError("Failed to connect to PostgreSQL container")
89
+
90
+ return self.get_connection_info()
91
+
92
+ def _run_container(
93
+ self, port: int
94
+ ) -> Any: # Type as Any since docker.models.containers.Container isn't imported
95
+ """
96
+ Run the Docker container with the specified port.
97
+
98
+ Args:
99
+ port: The port to map PostgreSQL to on the host
100
+
101
+ Returns:
102
+ The Docker container object
103
+ """
104
+ return self.client.containers.run(
105
+ f"postgres:{self.postgres_version}",
106
+ name=self.container_name,
107
+ detach=True,
108
+ environment={
109
+ "POSTGRES_USER": self.pg_user,
110
+ "POSTGRES_PASSWORD": self.pg_password,
111
+ "POSTGRES_DB": self.pg_db,
112
+ # Additional settings for faster startup in testing
113
+ "POSTGRES_HOST_AUTH_METHOD": "trust",
114
+ },
115
+ ports={"5432/tcp": port},
116
+ remove=True, # Auto-remove container when stopped
117
+ )
118
+
119
+ def _wait_for_container_ready(self) -> bool:
120
+ """
121
+ Wait for the PostgreSQL container to be ready.
122
+
123
+ Returns:
124
+ bool: True if the container is ready, False otherwise
125
+ """
126
+ max_retries = 30
127
+ retry_interval = 1
128
+
129
+ for i in range(max_retries):
130
+ try:
131
+ if self.container is None:
132
+ logger.warning("Container is None, cannot proceed")
133
+ return False
134
+
135
+ # We've already checked that self.container is not None
136
+ container = cast(Any, self.container)
137
+ container.reload() # Refresh container status
138
+ if container.status != "running":
139
+ logger.warning(f"Container status: {container.status}")
140
+ return False
141
+
142
+ # Try to connect to PostgreSQL
143
+ conn = socket.create_connection(("localhost", self.port), timeout=1)
144
+ conn.close()
145
+ # Wait a bit more to ensure PostgreSQL is fully initialized
146
+ time.sleep(2)
147
+ logger.info(f"PostgreSQL container is ready after {i + 1} attempt(s)")
148
+ return True
149
+ except (socket.error, ConnectionRefusedError) as e:
150
+ if i == max_retries - 1:
151
+ logger.warning(
152
+ f"Failed to connect after {max_retries} attempts: {e}"
153
+ )
154
+ return False
155
+ time.sleep(retry_interval)
156
+ except Exception as e:
157
+ logger.warning(f"Unexpected error checking container readiness: {e}")
158
+ if i == max_retries - 1:
159
+ return False
160
+ time.sleep(retry_interval)
161
+
162
+ return False
163
+
164
+ def stop(self) -> None:
165
+ """
166
+ Stop the PostgreSQL container.
167
+
168
+ This method ensures the container is properly stopped and removed.
169
+ """
170
+ if self.container is not None:
171
+ try:
172
+ logger.info(f"Stopping PostgreSQL container {self.container_name}")
173
+ # We've already checked that self.container is not None
174
+ container = cast(Any, self.container)
175
+ container.stop(timeout=10) # Allow 10 seconds for graceful shutdown
176
+ except Exception as e:
177
+ logger.warning(f"Failed to stop container: {e}")
178
+ try:
179
+ # Force remove as a fallback
180
+ if self.container is not None:
181
+ self.container.remove(force=True)
182
+ logger.info("Forced container removal")
183
+ except Exception as e2:
184
+ logger.warning(f"Failed to force remove container: {e2}")
185
+
186
+ def get_connection_info(self) -> Dict[str, Any]:
187
+ """
188
+ Get the connection information for the PostgreSQL container.
189
+
190
+ Returns:
191
+ Dict[str, Any]: A dictionary containing connection parameters
192
+ """
193
+ return {
194
+ "host": "localhost",
195
+ "port": self.port,
196
+ "user": self.pg_user,
197
+ "password": self.pg_password,
198
+ "database": self.pg_db,
199
+ }
200
+
201
+ def get_connection_string(self) -> str:
202
+ """
203
+ Get a PostgreSQL connection string.
204
+
205
+ Returns:
206
+ str: A connection string in the format 'postgresql://user:password@host:port/database'
207
+ """
208
+ return f"postgresql://{self.pg_user}:{self.pg_password}@localhost:{self.port}/{self.pg_db}"
@@ -19,7 +19,7 @@ class MockNode(DBObject):
19
19
  async def create(self, actor: DatabaseActions):
20
20
  pass
21
21
 
22
- async def migrate(self, previous: "MockNode", actor: DatabaseActions):
22
+ async def migrate(self, previous: DBObject, actor: DatabaseActions):
23
23
  pass
24
24
 
25
25
  async def destroy(self, actor: DatabaseActions):
@@ -263,8 +263,8 @@ async def test_add_column_any_type(
263
263
  (ColumnType.SERIAL, ColumnType.INTEGER),
264
264
  (ColumnType.BIGSERIAL, ColumnType.BIGINT),
265
265
  (ColumnType.CHAR, "character"),
266
- (ColumnType.TIME, "time without time zone"),
267
- (ColumnType.TIMESTAMP, "timestamp without time zone"),
266
+ (ColumnType.TIME_WITHOUT_TIME_ZONE, "time without time zone"),
267
+ (ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE, "timestamp without time zone"),
268
268
  )
269
269
 
270
270
  allowed_values = {enum_value.value}
@@ -365,7 +365,7 @@ async def test_modify_column_type(
365
365
  (ColumnType.VARCHAR, ColumnType.DATE, "2023-01-01", "2023-01-01", True),
366
366
  (
367
367
  ColumnType.TEXT,
368
- ColumnType.TIMESTAMP,
368
+ ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE,
369
369
  "2023-01-01 12:00:00",
370
370
  "2023-01-01 12:00:00",
371
371
  True,
@@ -443,7 +443,7 @@ async def test_modify_column_type_with_autocast(
443
443
  actual_value = row[column_name]
444
444
  if isinstance(expected_value, str) and to_type in [
445
445
  ColumnType.DATE,
446
- ColumnType.TIMESTAMP,
446
+ ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE,
447
447
  ]:
448
448
  # For date/timestamp, convert to string for comparison
449
449
  actual_value = str(actual_value)
@@ -1158,7 +1158,7 @@ async def test_modify_column_type_date_to_timestamp(
1158
1158
  await db_backed_actions.modify_column_type(
1159
1159
  table_name,
1160
1160
  column_name,
1161
- explicit_data_type=ColumnType.TIMESTAMP,
1161
+ explicit_data_type=ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE,
1162
1162
  autocast=False,
1163
1163
  )
1164
1164
 
@@ -750,7 +750,7 @@ def test_enum_column_assignment(clear_all_database_objects):
750
750
  DBColumn(
751
751
  table_name="exampledbmodel",
752
752
  column_name="standard_datetime",
753
- column_type=ColumnType.TIMESTAMP,
753
+ column_type=ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE,
754
754
  column_is_list=False,
755
755
  nullable=False,
756
756
  ),
@@ -810,7 +810,7 @@ def test_enum_column_assignment(clear_all_database_objects):
810
810
  DBColumn(
811
811
  table_name="exampledbmodel",
812
812
  column_name="standard_time",
813
- column_type=ColumnType.TIME,
813
+ column_type=ColumnType.TIME_WITHOUT_TIME_ZONE,
814
814
  column_is_list=False,
815
815
  nullable=False,
816
816
  ),
@@ -143,6 +143,52 @@ class ValueEnumInt(IntEnum):
143
143
  )
144
144
  ],
145
145
  ),
146
+ # Test PostgreSQL's storage format for timestamp without timezone
147
+ (
148
+ """
149
+ CREATE TABLE exampledbmodel (
150
+ id SERIAL PRIMARY KEY,
151
+ created_at TIMESTAMP NOT NULL
152
+ );
153
+ """,
154
+ [
155
+ (
156
+ DBColumn(
157
+ table_name="exampledbmodel",
158
+ column_name="created_at",
159
+ column_type=ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE,
160
+ column_is_list=False,
161
+ nullable=False,
162
+ ),
163
+ [
164
+ DBTable(table_name="exampledbmodel"),
165
+ ],
166
+ )
167
+ ],
168
+ ),
169
+ # Test PostgreSQL's storage format for timestamp with timezone
170
+ (
171
+ """
172
+ CREATE TABLE exampledbmodel (
173
+ id SERIAL PRIMARY KEY,
174
+ created_at TIMESTAMPTZ NOT NULL
175
+ );
176
+ """,
177
+ [
178
+ (
179
+ DBColumn(
180
+ table_name="exampledbmodel",
181
+ column_name="created_at",
182
+ column_type=ColumnType.TIMESTAMP_WITH_TIME_ZONE,
183
+ column_is_list=False,
184
+ nullable=False,
185
+ ),
186
+ [
187
+ DBTable(table_name="exampledbmodel"),
188
+ ],
189
+ )
190
+ ],
191
+ ),
146
192
  ],
147
193
  )
148
194
  async def test_simple_db_serializer(
@@ -10,6 +10,7 @@ from iceaxe.base import TableBase
10
10
  from iceaxe.comparison import ComparisonType, FieldComparison
11
11
  from iceaxe.field import DBFieldClassDefinition, DBFieldInfo
12
12
  from iceaxe.queries_str import QueryLiteral
13
+ from iceaxe.sql_types import ColumnType
13
14
  from iceaxe.typing import column
14
15
 
15
16
 
@@ -354,3 +355,29 @@ def test_force_join_constraints(
354
355
  )
355
356
  forced = comparison.force_join_constraints()
356
357
  assert forced.comparison == expected_comparison
358
+
359
+
360
+ @pytest.mark.parametrize(
361
+ "sql_type_string, expected_column_type",
362
+ [
363
+ ("timestamp", ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE), # Tests aliasing
364
+ ("timestamp without time zone", ColumnType.TIMESTAMP_WITHOUT_TIME_ZONE),
365
+ ("timestamp with time zone", ColumnType.TIMESTAMP_WITH_TIME_ZONE),
366
+ ("time", ColumnType.TIME_WITHOUT_TIME_ZONE), # Tests aliasing
367
+ ("time without time zone", ColumnType.TIME_WITHOUT_TIME_ZONE),
368
+ ("time with time zone", ColumnType.TIME_WITH_TIME_ZONE),
369
+ ],
370
+ )
371
+ def test_postgres_datetime_timezone_casting(
372
+ sql_type_string: str, expected_column_type: ColumnType
373
+ ):
374
+ """
375
+ Test that PostgresDateTime fields with different timezone configurations
376
+ are properly handled by the ColumnType enum, specifically testing that
377
+ PostgreSQL's storage format ('timestamp without time zone') can be parsed.
378
+ This also tests that SQL standard aliases like "timestamp" correctly map
379
+ to "timestamp without time zone".
380
+ """
381
+
382
+ # Test that ColumnType enum can handle PostgreSQL's storage formats and aliases
383
+ assert ColumnType(sql_type_string) == expected_column_type