Flowfile 0.2.2__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.

Potentially problematic release.


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

Files changed (171) hide show
  1. build_backends/__init__.py +0 -0
  2. build_backends/main.py +313 -0
  3. build_backends/main_prd.py +202 -0
  4. flowfile/__init__.py +71 -0
  5. flowfile/__main__.py +24 -0
  6. flowfile-0.2.2.dist-info/LICENSE +21 -0
  7. flowfile-0.2.2.dist-info/METADATA +225 -0
  8. flowfile-0.2.2.dist-info/RECORD +171 -0
  9. flowfile-0.2.2.dist-info/WHEEL +4 -0
  10. flowfile-0.2.2.dist-info/entry_points.txt +9 -0
  11. flowfile_core/__init__.py +13 -0
  12. flowfile_core/auth/__init__.py +0 -0
  13. flowfile_core/auth/jwt.py +140 -0
  14. flowfile_core/auth/models.py +40 -0
  15. flowfile_core/auth/secrets.py +178 -0
  16. flowfile_core/configs/__init__.py +35 -0
  17. flowfile_core/configs/flow_logger.py +433 -0
  18. flowfile_core/configs/node_store/__init__.py +0 -0
  19. flowfile_core/configs/node_store/nodes.py +98 -0
  20. flowfile_core/configs/settings.py +120 -0
  21. flowfile_core/database/__init__.py +0 -0
  22. flowfile_core/database/connection.py +51 -0
  23. flowfile_core/database/init_db.py +45 -0
  24. flowfile_core/database/models.py +41 -0
  25. flowfile_core/fileExplorer/__init__.py +0 -0
  26. flowfile_core/fileExplorer/funcs.py +259 -0
  27. flowfile_core/fileExplorer/utils.py +53 -0
  28. flowfile_core/flowfile/FlowfileFlow.py +1403 -0
  29. flowfile_core/flowfile/__init__.py +0 -0
  30. flowfile_core/flowfile/_extensions/__init__.py +0 -0
  31. flowfile_core/flowfile/_extensions/real_time_interface.py +51 -0
  32. flowfile_core/flowfile/analytics/__init__.py +0 -0
  33. flowfile_core/flowfile/analytics/analytics_processor.py +123 -0
  34. flowfile_core/flowfile/analytics/graphic_walker.py +60 -0
  35. flowfile_core/flowfile/analytics/schemas/__init__.py +0 -0
  36. flowfile_core/flowfile/analytics/utils.py +9 -0
  37. flowfile_core/flowfile/connection_manager/__init__.py +3 -0
  38. flowfile_core/flowfile/connection_manager/_connection_manager.py +48 -0
  39. flowfile_core/flowfile/connection_manager/models.py +10 -0
  40. flowfile_core/flowfile/database_connection_manager/__init__.py +0 -0
  41. flowfile_core/flowfile/database_connection_manager/db_connections.py +139 -0
  42. flowfile_core/flowfile/database_connection_manager/models.py +15 -0
  43. flowfile_core/flowfile/extensions.py +36 -0
  44. flowfile_core/flowfile/flow_data_engine/__init__.py +0 -0
  45. flowfile_core/flowfile/flow_data_engine/create/__init__.py +0 -0
  46. flowfile_core/flowfile/flow_data_engine/create/funcs.py +146 -0
  47. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +1521 -0
  48. flowfile_core/flowfile/flow_data_engine/flow_file_column/__init__.py +0 -0
  49. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +144 -0
  50. flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +24 -0
  51. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +36 -0
  52. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/__init__.py +0 -0
  53. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +38 -0
  54. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/settings_validator.py +90 -0
  55. flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -0
  56. flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +54 -0
  57. flowfile_core/flowfile/flow_data_engine/pivot_table.py +20 -0
  58. flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +249 -0
  59. flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +143 -0
  60. flowfile_core/flowfile/flow_data_engine/sample_data.py +120 -0
  61. flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -0
  62. flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +36 -0
  63. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +503 -0
  64. flowfile_core/flowfile/flow_data_engine/threaded_processes.py +27 -0
  65. flowfile_core/flowfile/flow_data_engine/types.py +0 -0
  66. flowfile_core/flowfile/flow_data_engine/utils.py +212 -0
  67. flowfile_core/flowfile/flow_node/__init__.py +0 -0
  68. flowfile_core/flowfile/flow_node/flow_node.py +771 -0
  69. flowfile_core/flowfile/flow_node/models.py +111 -0
  70. flowfile_core/flowfile/flow_node/schema_callback.py +70 -0
  71. flowfile_core/flowfile/handler.py +123 -0
  72. flowfile_core/flowfile/manage/__init__.py +0 -0
  73. flowfile_core/flowfile/manage/compatibility_enhancements.py +70 -0
  74. flowfile_core/flowfile/manage/manage_flowfile.py +0 -0
  75. flowfile_core/flowfile/manage/open_flowfile.py +136 -0
  76. flowfile_core/flowfile/setting_generator/__init__.py +2 -0
  77. flowfile_core/flowfile/setting_generator/setting_generator.py +41 -0
  78. flowfile_core/flowfile/setting_generator/settings.py +176 -0
  79. flowfile_core/flowfile/sources/__init__.py +0 -0
  80. flowfile_core/flowfile/sources/external_sources/__init__.py +3 -0
  81. flowfile_core/flowfile/sources/external_sources/airbyte_sources/__init__.py +0 -0
  82. flowfile_core/flowfile/sources/external_sources/airbyte_sources/airbyte.py +159 -0
  83. flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +172 -0
  84. flowfile_core/flowfile/sources/external_sources/airbyte_sources/settings.py +173 -0
  85. flowfile_core/flowfile/sources/external_sources/base_class.py +39 -0
  86. flowfile_core/flowfile/sources/external_sources/custom_external_sources/__init__.py +2 -0
  87. flowfile_core/flowfile/sources/external_sources/custom_external_sources/exchange_rate.py +0 -0
  88. flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +100 -0
  89. flowfile_core/flowfile/sources/external_sources/custom_external_sources/google_sheet.py +74 -0
  90. flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +29 -0
  91. flowfile_core/flowfile/sources/external_sources/factory.py +22 -0
  92. flowfile_core/flowfile/sources/external_sources/sql_source/__init__.py +0 -0
  93. flowfile_core/flowfile/sources/external_sources/sql_source/models.py +90 -0
  94. flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +328 -0
  95. flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +379 -0
  96. flowfile_core/flowfile/util/__init__.py +0 -0
  97. flowfile_core/flowfile/util/calculate_layout.py +137 -0
  98. flowfile_core/flowfile/util/execution_orderer.py +141 -0
  99. flowfile_core/flowfile/utils.py +106 -0
  100. flowfile_core/main.py +138 -0
  101. flowfile_core/routes/__init__.py +0 -0
  102. flowfile_core/routes/auth.py +34 -0
  103. flowfile_core/routes/logs.py +163 -0
  104. flowfile_core/routes/public.py +10 -0
  105. flowfile_core/routes/routes.py +601 -0
  106. flowfile_core/routes/secrets.py +85 -0
  107. flowfile_core/run_lock.py +11 -0
  108. flowfile_core/schemas/__init__.py +0 -0
  109. flowfile_core/schemas/analysis_schemas/__init__.py +0 -0
  110. flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +118 -0
  111. flowfile_core/schemas/defaults.py +9 -0
  112. flowfile_core/schemas/external_sources/__init__.py +0 -0
  113. flowfile_core/schemas/external_sources/airbyte_schemas.py +20 -0
  114. flowfile_core/schemas/input_schema.py +477 -0
  115. flowfile_core/schemas/models.py +193 -0
  116. flowfile_core/schemas/output_model.py +115 -0
  117. flowfile_core/schemas/schemas.py +106 -0
  118. flowfile_core/schemas/transform_schema.py +569 -0
  119. flowfile_core/secrets/__init__.py +0 -0
  120. flowfile_core/secrets/secrets.py +64 -0
  121. flowfile_core/utils/__init__.py +0 -0
  122. flowfile_core/utils/arrow_reader.py +247 -0
  123. flowfile_core/utils/excel_file_manager.py +18 -0
  124. flowfile_core/utils/fileManager.py +45 -0
  125. flowfile_core/utils/fl_executor.py +38 -0
  126. flowfile_core/utils/utils.py +8 -0
  127. flowfile_frame/__init__.py +56 -0
  128. flowfile_frame/__main__.py +12 -0
  129. flowfile_frame/adapters.py +17 -0
  130. flowfile_frame/expr.py +1163 -0
  131. flowfile_frame/flow_frame.py +2093 -0
  132. flowfile_frame/group_frame.py +199 -0
  133. flowfile_frame/join.py +75 -0
  134. flowfile_frame/selectors.py +242 -0
  135. flowfile_frame/utils.py +184 -0
  136. flowfile_worker/__init__.py +55 -0
  137. flowfile_worker/configs.py +95 -0
  138. flowfile_worker/create/__init__.py +37 -0
  139. flowfile_worker/create/funcs.py +146 -0
  140. flowfile_worker/create/models.py +86 -0
  141. flowfile_worker/create/pl_types.py +35 -0
  142. flowfile_worker/create/read_excel_tables.py +110 -0
  143. flowfile_worker/create/utils.py +84 -0
  144. flowfile_worker/external_sources/__init__.py +0 -0
  145. flowfile_worker/external_sources/airbyte_sources/__init__.py +0 -0
  146. flowfile_worker/external_sources/airbyte_sources/cache_manager.py +161 -0
  147. flowfile_worker/external_sources/airbyte_sources/main.py +89 -0
  148. flowfile_worker/external_sources/airbyte_sources/models.py +133 -0
  149. flowfile_worker/external_sources/airbyte_sources/settings.py +0 -0
  150. flowfile_worker/external_sources/sql_source/__init__.py +0 -0
  151. flowfile_worker/external_sources/sql_source/main.py +56 -0
  152. flowfile_worker/external_sources/sql_source/models.py +72 -0
  153. flowfile_worker/flow_logger.py +58 -0
  154. flowfile_worker/funcs.py +327 -0
  155. flowfile_worker/main.py +108 -0
  156. flowfile_worker/models.py +95 -0
  157. flowfile_worker/polars_fuzzy_match/__init__.py +0 -0
  158. flowfile_worker/polars_fuzzy_match/matcher.py +435 -0
  159. flowfile_worker/polars_fuzzy_match/models.py +36 -0
  160. flowfile_worker/polars_fuzzy_match/pre_process.py +213 -0
  161. flowfile_worker/polars_fuzzy_match/process.py +86 -0
  162. flowfile_worker/polars_fuzzy_match/utils.py +50 -0
  163. flowfile_worker/process_manager.py +36 -0
  164. flowfile_worker/routes.py +440 -0
  165. flowfile_worker/secrets.py +148 -0
  166. flowfile_worker/spawner.py +187 -0
  167. flowfile_worker/utils.py +25 -0
  168. test_utils/__init__.py +3 -0
  169. test_utils/postgres/__init__.py +1 -0
  170. test_utils/postgres/commands.py +109 -0
  171. test_utils/postgres/fixtures.py +417 -0
@@ -0,0 +1,417 @@
1
+ """
2
+ PostgreSQL fixtures for tests.
3
+
4
+ This module provides utilities to set up, manage, and tear down PostgreSQL
5
+ containers with sample data for testing.
6
+ """
7
+
8
+ import os
9
+ import time
10
+ import logging
11
+ import subprocess
12
+ import shutil
13
+ from contextlib import contextmanager
14
+ from typing import Dict, Generator, Optional, Tuple
15
+
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(levelname)s - %(message)s',
20
+ datefmt='%Y-%m-%d %H:%M:%S'
21
+ )
22
+
23
+ logger = logging.getLogger("postgres_fixture")
24
+
25
+ # Configuration constants
26
+ POSTGRES_HOST = os.environ.get("TEST_POSTGRES_HOST", "localhost")
27
+ POSTGRES_PORT = int(os.environ.get("TEST_POSTGRES_PORT", 5433))
28
+ POSTGRES_USER = os.environ.get("TEST_POSTGRES_USER", "testuser")
29
+ POSTGRES_PASSWORD = os.environ.get("TEST_POSTGRES_PASSWORD", "testpass")
30
+ POSTGRES_DB = os.environ.get("TEST_POSTGRES_DB", "testdb")
31
+ POSTGRES_SCHEMA = os.environ.get("TEST_POSTGRES_SCHEMA", "movies") # or "stocks"
32
+ POSTGRES_CONTAINER_NAME = os.environ.get("TEST_POSTGRES_CONTAINER", "test-postgres-sample")
33
+ POSTGRES_IMAGE_TAG = os.environ.get("TEST_POSTGRES_IMAGE", "test-sample-db")
34
+ STARTUP_TIMEOUT = int(os.environ.get("TEST_POSTGRES_STARTUP_TIMEOUT", 30)) # seconds
35
+ SHUTDOWN_TIMEOUT = int(os.environ.get("TEST_POSTGRES_SHUTDOWN_TIMEOUT", 15)) # seconds
36
+ STARTUP_CHECK_INTERVAL = 2 # seconds
37
+
38
+ # Path to postgres-docker-samples repo
39
+ SAMPLES_REPO_URL = "https://github.com/zseta/postgres-docker-samples.git"
40
+ SAMPLES_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "postgres-docker-samples")
41
+
42
+ # Operating system detection
43
+ IS_MACOS = os.uname().sysname == 'Darwin' if hasattr(os, 'uname') else False
44
+ IS_WINDOWS = os.name == 'nt'
45
+
46
+
47
+ def is_docker_available() -> bool:
48
+ """
49
+ Check if Docker is available on the system.
50
+
51
+ Returns:
52
+ bool: True if Docker is available and working, False otherwise
53
+ """
54
+ # Skip Docker on macOS and Windows in CI
55
+ if (IS_MACOS or IS_WINDOWS) and os.environ.get('CI', '').lower() in ('true', '1', 'yes'):
56
+ logger.info("Skipping Docker on macOS/Windows in CI environment")
57
+ return False
58
+
59
+ # If docker executable is not in PATH
60
+ if shutil.which("docker") is None:
61
+ logger.warning("Docker executable not found in PATH")
62
+ return False
63
+
64
+ # Try a simple docker command
65
+ try:
66
+ result = subprocess.run(
67
+ ["docker", "info"],
68
+ stdout=subprocess.PIPE,
69
+ stderr=subprocess.PIPE,
70
+ timeout=5,
71
+ check=False # Don't raise exception on non-zero return code
72
+ )
73
+
74
+ if result.returncode != 0:
75
+ logger.warning("Docker is not operational")
76
+ return False
77
+
78
+ return True
79
+ except (subprocess.SubprocessError, OSError):
80
+ logger.warning("Error running Docker command")
81
+ return False
82
+
83
+
84
+ def is_container_running(container_name: str) -> bool:
85
+ """Check if the postgres container is already running."""
86
+ if not is_docker_available():
87
+ return False
88
+
89
+ try:
90
+ result = subprocess.run(
91
+ ["docker", "ps", "--filter", f"name={container_name}", "--format", "{{.Names}}"],
92
+ capture_output=True,
93
+ text=True,
94
+ check=True
95
+ )
96
+ return container_name in result.stdout.strip()
97
+ except subprocess.CalledProcessError:
98
+ logger.error("Failed to check if container is running.")
99
+ return False
100
+
101
+
102
+ def can_connect_to_db() -> bool:
103
+ """Check if we can connect to the PostgreSQL database."""
104
+ try:
105
+ # Importing here to avoid requiring psycopg2 at module level
106
+ import psycopg2
107
+
108
+ conn = psycopg2.connect(
109
+ dbname=POSTGRES_DB,
110
+ user=POSTGRES_USER,
111
+ password=POSTGRES_PASSWORD,
112
+ host=POSTGRES_HOST,
113
+ port=POSTGRES_PORT,
114
+ connect_timeout=5
115
+ )
116
+ conn.close()
117
+ return True
118
+ except ImportError:
119
+ logger.error("psycopg2 not installed. Run: pip install psycopg2-binary")
120
+ return False
121
+ except Exception as e:
122
+ logger.debug(f"Could not connect to database: {e}")
123
+ return False
124
+
125
+
126
+ def setup_postgres_samples(
127
+ schema: str = POSTGRES_SCHEMA,
128
+ port: int = POSTGRES_PORT,
129
+ user: str = POSTGRES_USER,
130
+ password: str = POSTGRES_PASSWORD,
131
+ db: str = POSTGRES_DB,
132
+ image_tag: str = POSTGRES_IMAGE_TAG
133
+ ) -> bool:
134
+ """
135
+ Clone the postgres-docker-samples repository if it doesn't exist
136
+ and prepare the environment.
137
+
138
+ Args:
139
+ schema: Sample schema to use (movies or stocks)
140
+ port: Port to expose PostgreSQL on
141
+ user: PostgreSQL username
142
+ password: PostgreSQL password
143
+ db: PostgreSQL database name
144
+ image_tag: Docker image tag
145
+
146
+ Returns:
147
+ True if setup succeeds, False otherwise
148
+ """
149
+ # Check Docker availability
150
+ if not is_docker_available():
151
+ logger.warning("Docker not available, skipping Postgres sample setup")
152
+ return False
153
+
154
+ # Clone the repository if it doesn't exist
155
+ if not os.path.exists(SAMPLES_REPO_DIR):
156
+ logger.info(f"Cloning postgres-docker-samples repository to {SAMPLES_REPO_DIR}")
157
+ try:
158
+ subprocess.run(
159
+ ["git", "clone", SAMPLES_REPO_URL, SAMPLES_REPO_DIR],
160
+ check=True
161
+ )
162
+ except subprocess.CalledProcessError as e:
163
+ logger.error(f"Failed to clone repository: {e}")
164
+ return False
165
+
166
+ # Change to the repository directory
167
+ original_dir = os.getcwd()
168
+ os.chdir(SAMPLES_REPO_DIR)
169
+ print(os.getcwd())
170
+
171
+ try:
172
+ # Create a custom .env file for our test configuration
173
+ logger.info(f"Configuring .env file with schema={schema}")
174
+ with open('.env', 'w') as f:
175
+ f.write(f"""
176
+ # choose a schema (must be the name of a folder)
177
+ SAMPLE_SCHEMA={schema}
178
+
179
+ # add database credentials
180
+ POSTGRES_PORT={port}
181
+ POSTGRES_USER={user}
182
+ POSTGRES_PASSWORD={password}
183
+ POSTGRES_DB={db}
184
+
185
+ # image tag
186
+ DOCKER_IMAGE_TAG={image_tag}
187
+ """)
188
+
189
+ # Make scripts executable
190
+ subprocess.run(["chmod", "+x", "build.sh"], check=True)
191
+ subprocess.run(["chmod", "+x", "run.sh"], check=True)
192
+
193
+ # Build the Docker image - FIXED: Use bash to execute the script
194
+ logger.info(f"Building Docker image {image_tag}")
195
+ subprocess.run(["bash", "build.sh"], check=True)
196
+
197
+ # Return to original directory
198
+ os.chdir(original_dir)
199
+ return True
200
+
201
+ except Exception as e:
202
+ logger.error(f"Error setting up postgres samples: {e}")
203
+ os.chdir(original_dir)
204
+ return False
205
+
206
+
207
+ def start_postgres_container(
208
+ container_name: str = POSTGRES_CONTAINER_NAME,
209
+ port: int = POSTGRES_PORT,
210
+ image_tag: str = POSTGRES_IMAGE_TAG
211
+ ) -> Tuple[Optional[subprocess.Popen], bool]:
212
+ """
213
+ Start the PostgreSQL container with sample data.
214
+
215
+ Args:
216
+ container_name: Name to give the Docker container
217
+ port: Port to expose PostgreSQL on
218
+ image_tag: Docker image tag to use
219
+
220
+ Returns:
221
+ Tuple containing the process object (or None) and a success flag
222
+ """
223
+ # Check Docker availability
224
+ if not is_docker_available():
225
+ logger.warning("Docker not available, skipping PostgreSQL container start")
226
+ return None, False
227
+
228
+ logger.info(f"Starting PostgreSQL container with sample data...")
229
+
230
+ # Check if container is already running
231
+ if is_container_running(container_name):
232
+ logger.info(f"Container {container_name} is already running")
233
+ return None, True
234
+
235
+ # Run the container in the background
236
+ try:
237
+ proc = subprocess.Popen([
238
+ "docker", "run", "--name", container_name,
239
+ "-p", f"{port}:5432", "--rm", "-d", image_tag
240
+ ])
241
+
242
+ # Wait for the process to complete (should be quick as it just starts the container)
243
+ proc.wait(timeout=5)
244
+
245
+ if proc.returncode != 0:
246
+ logger.error(f"Failed to start container with return code {proc.returncode}")
247
+ return proc, False
248
+ except Exception as e:
249
+ logger.error(f"Error starting container: {e}")
250
+ return None, False
251
+
252
+ # Wait for the database to be ready
253
+ start_time = time.time()
254
+ max_retries = STARTUP_TIMEOUT // STARTUP_CHECK_INTERVAL
255
+
256
+ for i in range(max_retries):
257
+ if can_connect_to_db():
258
+ elapsed = time.time() - start_time
259
+ logger.info(f"PostgreSQL container started successfully in {elapsed:.2f} seconds")
260
+ return None, True
261
+
262
+ # Log progress
263
+ elapsed = time.time() - start_time
264
+ logger.info(f"Waiting for PostgreSQL to start... ({elapsed:.1f}s / {STARTUP_TIMEOUT}s)")
265
+ time.sleep(STARTUP_CHECK_INTERVAL)
266
+
267
+ # Timeout reached
268
+ logger.error(f"PostgreSQL failed to start within {STARTUP_TIMEOUT} seconds")
269
+ return None, False
270
+
271
+
272
+ def stop_postgres_container(container_name: str = POSTGRES_CONTAINER_NAME, timeout: int = SHUTDOWN_TIMEOUT) -> bool:
273
+ """
274
+ Stop the PostgreSQL container gracefully.
275
+
276
+ Args:
277
+ container_name: Name of the Docker container to stop
278
+ timeout: Timeout for graceful container stop in seconds
279
+
280
+ Returns:
281
+ True if stop succeeds or container not running, False otherwise
282
+ """
283
+ # Check Docker availability
284
+ if not is_docker_available():
285
+ logger.warning("Docker not available, skipping PostgreSQL container stop")
286
+ return True
287
+
288
+ logger.info(f"Stopping PostgreSQL container {container_name}...")
289
+
290
+ if not is_container_running(container_name):
291
+ logger.info("Container is not running")
292
+ return True
293
+
294
+ try:
295
+ subprocess.run(
296
+ ["docker", "stop", container_name],
297
+ check=True,
298
+ timeout=timeout
299
+ )
300
+ logger.info("Container stopped gracefully")
301
+ return True
302
+ except subprocess.TimeoutExpired:
303
+ logger.warning(f"Container did not stop within {timeout} seconds, forcing removal")
304
+ subprocess.run(["docker", "rm", "-f", container_name], check=False)
305
+ return True
306
+ except Exception as e:
307
+ logger.warning(f"Error while stopping container: {e}")
308
+ return False
309
+
310
+
311
+ def print_connection_info(
312
+ host: str = POSTGRES_HOST,
313
+ port: int = POSTGRES_PORT,
314
+ db: str = POSTGRES_DB,
315
+ user: str = POSTGRES_USER,
316
+ password: str = POSTGRES_PASSWORD,
317
+ container_name: str = POSTGRES_CONTAINER_NAME
318
+ ) -> None:
319
+ """
320
+ Print connection information for easy reference.
321
+
322
+ Args:
323
+ host: PostgreSQL host
324
+ port: PostgreSQL port
325
+ db: PostgreSQL database name
326
+ user: PostgreSQL username
327
+ password: PostgreSQL password
328
+ container_name: Docker container name
329
+ """
330
+ if not is_docker_available():
331
+ print("\n" + "=" * 50)
332
+ print("PostgreSQL with Docker not available on this system")
333
+ print("Tests requiring Docker will be skipped")
334
+ print("=" * 50 + "\n")
335
+ return
336
+
337
+ print("\n" + "=" * 50)
338
+ print("PostgreSQL Connection Information:")
339
+ print("=" * 50)
340
+ print(f"Host: {host}")
341
+ print(f"Port: {port}")
342
+ print(f"Database: {db}")
343
+ print(f"User: {user}")
344
+ print(f"Password: {password}")
345
+ print(f"Connection string: postgresql://{user}:{password}@{host}:{port}/{db}")
346
+ print("=" * 50)
347
+ print("\nTo stop the container, run:")
348
+ print(f"poetry run stop_postgres")
349
+ print("=" * 50 + "\n")
350
+
351
+
352
+ @contextmanager
353
+ def managed_postgres() -> Generator[Dict[str, any], None, None]:
354
+ """
355
+ Context manager for PostgreSQL container management.
356
+ Ensures proper cleanup even when tests fail.
357
+
358
+ Yields:
359
+ Dictionary with database connection information or empty dict if Docker isn't available
360
+ """
361
+ # Check Docker availability
362
+ if not is_docker_available():
363
+ logger.warning("Docker not available, skipping managed_postgres context")
364
+ yield {}
365
+ return
366
+
367
+ # Setup
368
+ if not setup_postgres_samples():
369
+ logger.error("Failed to set up postgres samples")
370
+ yield {}
371
+ return
372
+
373
+ # Start container
374
+ _, success = start_postgres_container()
375
+ if not success:
376
+ logger.error("Failed to start PostgreSQL container")
377
+ yield {}
378
+ return
379
+
380
+ try:
381
+ # Create connection details
382
+ connection_info = {
383
+ "host": POSTGRES_HOST,
384
+ "port": POSTGRES_PORT,
385
+ "dbname": POSTGRES_DB,
386
+ "user": POSTGRES_USER,
387
+ "password": POSTGRES_PASSWORD,
388
+ "connection_string": f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
389
+ }
390
+
391
+ yield connection_info
392
+ finally:
393
+ # Always try to stop the container
394
+ stop_postgres_container()
395
+
396
+
397
+ def get_db_engine():
398
+ """
399
+ Create a SQLAlchemy engine connected to the test database.
400
+
401
+ Returns:
402
+ SQLAlchemy engine object or None if Docker isn't available
403
+ """
404
+ # Check Docker availability
405
+ if not is_docker_available():
406
+ logger.warning("Docker not available, skipping get_db_engine")
407
+ return None
408
+
409
+ try:
410
+ from sqlalchemy import create_engine
411
+
412
+ connection_string = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
413
+ engine = create_engine(connection_string)
414
+ return engine
415
+ except ImportError:
416
+ logger.error("SQLAlchemy not installed. Run: pip install sqlalchemy")
417
+ raise