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.
- build_backends/__init__.py +0 -0
- build_backends/main.py +313 -0
- build_backends/main_prd.py +202 -0
- flowfile/__init__.py +71 -0
- flowfile/__main__.py +24 -0
- flowfile-0.2.2.dist-info/LICENSE +21 -0
- flowfile-0.2.2.dist-info/METADATA +225 -0
- flowfile-0.2.2.dist-info/RECORD +171 -0
- flowfile-0.2.2.dist-info/WHEEL +4 -0
- flowfile-0.2.2.dist-info/entry_points.txt +9 -0
- flowfile_core/__init__.py +13 -0
- flowfile_core/auth/__init__.py +0 -0
- flowfile_core/auth/jwt.py +140 -0
- flowfile_core/auth/models.py +40 -0
- flowfile_core/auth/secrets.py +178 -0
- flowfile_core/configs/__init__.py +35 -0
- flowfile_core/configs/flow_logger.py +433 -0
- flowfile_core/configs/node_store/__init__.py +0 -0
- flowfile_core/configs/node_store/nodes.py +98 -0
- flowfile_core/configs/settings.py +120 -0
- flowfile_core/database/__init__.py +0 -0
- flowfile_core/database/connection.py +51 -0
- flowfile_core/database/init_db.py +45 -0
- flowfile_core/database/models.py +41 -0
- flowfile_core/fileExplorer/__init__.py +0 -0
- flowfile_core/fileExplorer/funcs.py +259 -0
- flowfile_core/fileExplorer/utils.py +53 -0
- flowfile_core/flowfile/FlowfileFlow.py +1403 -0
- flowfile_core/flowfile/__init__.py +0 -0
- flowfile_core/flowfile/_extensions/__init__.py +0 -0
- flowfile_core/flowfile/_extensions/real_time_interface.py +51 -0
- flowfile_core/flowfile/analytics/__init__.py +0 -0
- flowfile_core/flowfile/analytics/analytics_processor.py +123 -0
- flowfile_core/flowfile/analytics/graphic_walker.py +60 -0
- flowfile_core/flowfile/analytics/schemas/__init__.py +0 -0
- flowfile_core/flowfile/analytics/utils.py +9 -0
- flowfile_core/flowfile/connection_manager/__init__.py +3 -0
- flowfile_core/flowfile/connection_manager/_connection_manager.py +48 -0
- flowfile_core/flowfile/connection_manager/models.py +10 -0
- flowfile_core/flowfile/database_connection_manager/__init__.py +0 -0
- flowfile_core/flowfile/database_connection_manager/db_connections.py +139 -0
- flowfile_core/flowfile/database_connection_manager/models.py +15 -0
- flowfile_core/flowfile/extensions.py +36 -0
- flowfile_core/flowfile/flow_data_engine/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/create/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/create/funcs.py +146 -0
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +1521 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +144 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +24 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +36 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +38 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/settings_validator.py +90 -0
- flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -0
- flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +54 -0
- flowfile_core/flowfile/flow_data_engine/pivot_table.py +20 -0
- flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +249 -0
- flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +143 -0
- flowfile_core/flowfile/flow_data_engine/sample_data.py +120 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +36 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +503 -0
- flowfile_core/flowfile/flow_data_engine/threaded_processes.py +27 -0
- flowfile_core/flowfile/flow_data_engine/types.py +0 -0
- flowfile_core/flowfile/flow_data_engine/utils.py +212 -0
- flowfile_core/flowfile/flow_node/__init__.py +0 -0
- flowfile_core/flowfile/flow_node/flow_node.py +771 -0
- flowfile_core/flowfile/flow_node/models.py +111 -0
- flowfile_core/flowfile/flow_node/schema_callback.py +70 -0
- flowfile_core/flowfile/handler.py +123 -0
- flowfile_core/flowfile/manage/__init__.py +0 -0
- flowfile_core/flowfile/manage/compatibility_enhancements.py +70 -0
- flowfile_core/flowfile/manage/manage_flowfile.py +0 -0
- flowfile_core/flowfile/manage/open_flowfile.py +136 -0
- flowfile_core/flowfile/setting_generator/__init__.py +2 -0
- flowfile_core/flowfile/setting_generator/setting_generator.py +41 -0
- flowfile_core/flowfile/setting_generator/settings.py +176 -0
- flowfile_core/flowfile/sources/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/__init__.py +3 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/airbyte.py +159 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +172 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/settings.py +173 -0
- flowfile_core/flowfile/sources/external_sources/base_class.py +39 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/__init__.py +2 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/exchange_rate.py +0 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +100 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/google_sheet.py +74 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +29 -0
- flowfile_core/flowfile/sources/external_sources/factory.py +22 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/models.py +90 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +328 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +379 -0
- flowfile_core/flowfile/util/__init__.py +0 -0
- flowfile_core/flowfile/util/calculate_layout.py +137 -0
- flowfile_core/flowfile/util/execution_orderer.py +141 -0
- flowfile_core/flowfile/utils.py +106 -0
- flowfile_core/main.py +138 -0
- flowfile_core/routes/__init__.py +0 -0
- flowfile_core/routes/auth.py +34 -0
- flowfile_core/routes/logs.py +163 -0
- flowfile_core/routes/public.py +10 -0
- flowfile_core/routes/routes.py +601 -0
- flowfile_core/routes/secrets.py +85 -0
- flowfile_core/run_lock.py +11 -0
- flowfile_core/schemas/__init__.py +0 -0
- flowfile_core/schemas/analysis_schemas/__init__.py +0 -0
- flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +118 -0
- flowfile_core/schemas/defaults.py +9 -0
- flowfile_core/schemas/external_sources/__init__.py +0 -0
- flowfile_core/schemas/external_sources/airbyte_schemas.py +20 -0
- flowfile_core/schemas/input_schema.py +477 -0
- flowfile_core/schemas/models.py +193 -0
- flowfile_core/schemas/output_model.py +115 -0
- flowfile_core/schemas/schemas.py +106 -0
- flowfile_core/schemas/transform_schema.py +569 -0
- flowfile_core/secrets/__init__.py +0 -0
- flowfile_core/secrets/secrets.py +64 -0
- flowfile_core/utils/__init__.py +0 -0
- flowfile_core/utils/arrow_reader.py +247 -0
- flowfile_core/utils/excel_file_manager.py +18 -0
- flowfile_core/utils/fileManager.py +45 -0
- flowfile_core/utils/fl_executor.py +38 -0
- flowfile_core/utils/utils.py +8 -0
- flowfile_frame/__init__.py +56 -0
- flowfile_frame/__main__.py +12 -0
- flowfile_frame/adapters.py +17 -0
- flowfile_frame/expr.py +1163 -0
- flowfile_frame/flow_frame.py +2093 -0
- flowfile_frame/group_frame.py +199 -0
- flowfile_frame/join.py +75 -0
- flowfile_frame/selectors.py +242 -0
- flowfile_frame/utils.py +184 -0
- flowfile_worker/__init__.py +55 -0
- flowfile_worker/configs.py +95 -0
- flowfile_worker/create/__init__.py +37 -0
- flowfile_worker/create/funcs.py +146 -0
- flowfile_worker/create/models.py +86 -0
- flowfile_worker/create/pl_types.py +35 -0
- flowfile_worker/create/read_excel_tables.py +110 -0
- flowfile_worker/create/utils.py +84 -0
- flowfile_worker/external_sources/__init__.py +0 -0
- flowfile_worker/external_sources/airbyte_sources/__init__.py +0 -0
- flowfile_worker/external_sources/airbyte_sources/cache_manager.py +161 -0
- flowfile_worker/external_sources/airbyte_sources/main.py +89 -0
- flowfile_worker/external_sources/airbyte_sources/models.py +133 -0
- flowfile_worker/external_sources/airbyte_sources/settings.py +0 -0
- flowfile_worker/external_sources/sql_source/__init__.py +0 -0
- flowfile_worker/external_sources/sql_source/main.py +56 -0
- flowfile_worker/external_sources/sql_source/models.py +72 -0
- flowfile_worker/flow_logger.py +58 -0
- flowfile_worker/funcs.py +327 -0
- flowfile_worker/main.py +108 -0
- flowfile_worker/models.py +95 -0
- flowfile_worker/polars_fuzzy_match/__init__.py +0 -0
- flowfile_worker/polars_fuzzy_match/matcher.py +435 -0
- flowfile_worker/polars_fuzzy_match/models.py +36 -0
- flowfile_worker/polars_fuzzy_match/pre_process.py +213 -0
- flowfile_worker/polars_fuzzy_match/process.py +86 -0
- flowfile_worker/polars_fuzzy_match/utils.py +50 -0
- flowfile_worker/process_manager.py +36 -0
- flowfile_worker/routes.py +440 -0
- flowfile_worker/secrets.py +148 -0
- flowfile_worker/spawner.py +187 -0
- flowfile_worker/utils.py +25 -0
- test_utils/__init__.py +3 -0
- test_utils/postgres/__init__.py +1 -0
- test_utils/postgres/commands.py +109 -0
- 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
|