remdb 0.2.6__py3-none-any.whl → 0.3.118__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 remdb might be problematic. Click here for more details.
- rem/__init__.py +129 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +500 -0
- rem/agentic/context.py +28 -22
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +29 -3
- rem/agentic/otel/setup.py +92 -4
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +168 -24
- rem/agentic/schema.py +358 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +154 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +454 -7
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +152 -16
- rem/api/routers/chat/models.py +7 -3
- rem/api/routers/chat/sse_events.py +526 -0
- rem/api/routers/chat/streaming.py +608 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +148 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/ask.py +15 -11
- rem/cli/commands/cluster.py +1300 -0
- rem/cli/commands/configure.py +170 -97
- rem/cli/commands/db.py +396 -139
- rem/cli/commands/experiments.py +278 -96
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +37 -6
- rem/config.py +2 -2
- rem/models/core/core_model.py +7 -1
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +373 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +94 -140
- rem/services/content/service.py +115 -24
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +24 -17
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +252 -19
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +291 -9
- rem/services/postgres/service.py +6 -6
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +17 -1
- rem/services/session/reload.py +1 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +169 -22
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +284 -21
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/__init__.py +2 -1
- rem/workers/db_maintainer.py +74 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/METADATA +598 -171
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/RECORD +102 -73
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
rem/utils/sql_paths.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Utilities for resolving SQL file paths.
|
|
2
|
+
|
|
3
|
+
Handles package SQL directory resolution and user migrations.
|
|
4
|
+
|
|
5
|
+
Convention for user migrations:
|
|
6
|
+
Place custom SQL files in `./sql/migrations/` relative to your project root.
|
|
7
|
+
Files should be numbered (e.g., `100_custom_table.sql`) to control execution order.
|
|
8
|
+
Package migrations (001-099) run first, then user migrations (100+).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
import importlib.resources
|
|
14
|
+
|
|
15
|
+
# Convention: Default location for user-maintained migrations
|
|
16
|
+
USER_SQL_DIR_CONVENTION = "sql"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_package_sql_dir() -> Path:
|
|
20
|
+
"""Get the SQL directory from the installed rem package.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to the package's sql directory
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
FileNotFoundError: If the SQL directory cannot be found
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Use importlib.resources for Python 3.9+
|
|
30
|
+
sql_ref = importlib.resources.files("rem") / "sql"
|
|
31
|
+
package_sql = Path(str(sql_ref))
|
|
32
|
+
if package_sql.exists():
|
|
33
|
+
return package_sql
|
|
34
|
+
except (AttributeError, TypeError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
# Fallback: use __file__ to find package location
|
|
38
|
+
try:
|
|
39
|
+
import rem
|
|
40
|
+
package_sql = Path(rem.__file__).parent / "sql"
|
|
41
|
+
if package_sql.exists():
|
|
42
|
+
return package_sql
|
|
43
|
+
except (ImportError, AttributeError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# Development fallback: check relative to cwd
|
|
47
|
+
dev_sql = Path("src/rem/sql")
|
|
48
|
+
if dev_sql.exists():
|
|
49
|
+
return dev_sql
|
|
50
|
+
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
"Could not locate rem SQL directory. "
|
|
53
|
+
"Ensure remdb is properly installed or run from the source directory."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_package_migrations_dir() -> Path:
|
|
58
|
+
"""Get the migrations directory from the installed rem package.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Path to the package's migrations directory
|
|
62
|
+
"""
|
|
63
|
+
return get_package_sql_dir() / "migrations"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_user_sql_dir() -> Optional[Path]:
|
|
67
|
+
"""Get the conventional user SQL directory if it exists.
|
|
68
|
+
|
|
69
|
+
Looks for `./sql/` relative to the current working directory.
|
|
70
|
+
This follows the convention for user-maintained migrations.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Path to user sql directory if it exists, None otherwise
|
|
74
|
+
"""
|
|
75
|
+
user_sql = Path.cwd() / USER_SQL_DIR_CONVENTION
|
|
76
|
+
if user_sql.exists() and user_sql.is_dir():
|
|
77
|
+
return user_sql
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_package_migrations() -> List[Path]:
|
|
82
|
+
"""List all migration files in the package.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Sorted list of migration file paths
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
migrations_dir = get_package_migrations_dir()
|
|
89
|
+
if migrations_dir.exists():
|
|
90
|
+
return sorted(
|
|
91
|
+
f for f in migrations_dir.glob("*.sql")
|
|
92
|
+
if f.name[0].isdigit() # Only numbered migrations
|
|
93
|
+
)
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def list_user_migrations() -> List[Path]:
|
|
101
|
+
"""List all migration files in the user's sql/migrations directory.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Sorted list of user migration file paths
|
|
105
|
+
"""
|
|
106
|
+
user_sql = get_user_sql_dir()
|
|
107
|
+
if user_sql:
|
|
108
|
+
migrations_dir = user_sql / "migrations"
|
|
109
|
+
if migrations_dir.exists():
|
|
110
|
+
return sorted(
|
|
111
|
+
f for f in migrations_dir.glob("*.sql")
|
|
112
|
+
if f.name[0].isdigit() # Only numbered migrations
|
|
113
|
+
)
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def list_all_migrations() -> List[Path]:
|
|
118
|
+
"""List all migration files from package and user directories.
|
|
119
|
+
|
|
120
|
+
Collects migrations from:
|
|
121
|
+
1. Package migrations directory
|
|
122
|
+
2. User directory (./sql/migrations/) if it exists
|
|
123
|
+
|
|
124
|
+
Files are sorted by name, so use numbered prefixes to control order:
|
|
125
|
+
- 001-099: Reserved for package migrations
|
|
126
|
+
- 100+: Recommended for user migrations
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Sorted list of all migration file paths (by filename)
|
|
130
|
+
"""
|
|
131
|
+
all_migrations = []
|
|
132
|
+
seen_names = set()
|
|
133
|
+
|
|
134
|
+
# Package migrations first
|
|
135
|
+
for f in list_package_migrations():
|
|
136
|
+
if f.name not in seen_names:
|
|
137
|
+
all_migrations.append(f)
|
|
138
|
+
seen_names.add(f.name)
|
|
139
|
+
|
|
140
|
+
# User migrations second
|
|
141
|
+
for f in list_user_migrations():
|
|
142
|
+
if f.name not in seen_names:
|
|
143
|
+
all_migrations.append(f)
|
|
144
|
+
seen_names.add(f.name)
|
|
145
|
+
|
|
146
|
+
return sorted(all_migrations, key=lambda p: p.name)
|
rem/utils/sql_types.py
CHANGED
|
@@ -16,6 +16,7 @@ Best Practices:
|
|
|
16
16
|
- UUID for identifiers in Union types
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
import types
|
|
19
20
|
from datetime import date, datetime, time
|
|
20
21
|
from typing import Any, Union, get_args, get_origin
|
|
21
22
|
from uuid import UUID
|
|
@@ -78,8 +79,9 @@ def get_sql_type(field_info: FieldInfo, field_name: str) -> str:
|
|
|
78
79
|
return "TEXT"
|
|
79
80
|
|
|
80
81
|
# Handle Union types (including Optional[T] which is Union[T, None])
|
|
82
|
+
# Also handles Python 3.10+ `X | None` syntax which uses types.UnionType
|
|
81
83
|
origin = get_origin(annotation)
|
|
82
|
-
if origin is Union:
|
|
84
|
+
if origin is Union or isinstance(annotation, types.UnionType):
|
|
83
85
|
args = get_args(annotation)
|
|
84
86
|
# Filter out NoneType
|
|
85
87
|
non_none_args = [arg for arg in args if arg is not type(None)]
|
rem/utils/vision.py
CHANGED
|
@@ -11,7 +11,6 @@ markdown descriptions of images.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import base64
|
|
14
|
-
import os
|
|
15
14
|
from enum import Enum
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
from typing import Optional
|
|
@@ -19,6 +18,9 @@ from typing import Optional
|
|
|
19
18
|
import requests
|
|
20
19
|
from loguru import logger
|
|
21
20
|
|
|
21
|
+
from rem.utils.constants import HTTP_TIMEOUT_LONG, VISION_MAX_TOKENS
|
|
22
|
+
from rem.utils.mime_types import EXTENSION_TO_MIME
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
class VisionProvider(str, Enum):
|
|
24
26
|
"""Supported vision providers."""
|
|
@@ -141,14 +143,7 @@ class ImageAnalyzer:
|
|
|
141
143
|
|
|
142
144
|
# Detect media type
|
|
143
145
|
suffix = image_path.suffix.lower()
|
|
144
|
-
|
|
145
|
-
".png": "image/png",
|
|
146
|
-
".jpg": "image/jpeg",
|
|
147
|
-
".jpeg": "image/jpeg",
|
|
148
|
-
".gif": "image/gif",
|
|
149
|
-
".webp": "image/webp",
|
|
150
|
-
}
|
|
151
|
-
media_type = media_type_map.get(suffix, "image/png")
|
|
146
|
+
media_type = EXTENSION_TO_MIME.get(suffix, "image/png")
|
|
152
147
|
|
|
153
148
|
logger.info(f"Analyzing {image_path.name} with {self.provider.value} ({self.model})")
|
|
154
149
|
|
|
@@ -190,7 +185,7 @@ class ImageAnalyzer:
|
|
|
190
185
|
|
|
191
186
|
body = {
|
|
192
187
|
"model": self.model,
|
|
193
|
-
"max_tokens":
|
|
188
|
+
"max_tokens": VISION_MAX_TOKENS,
|
|
194
189
|
"messages": [
|
|
195
190
|
{
|
|
196
191
|
"role": "user",
|
|
@@ -216,7 +211,7 @@ class ImageAnalyzer:
|
|
|
216
211
|
"https://api.anthropic.com/v1/messages",
|
|
217
212
|
headers=headers,
|
|
218
213
|
json=body,
|
|
219
|
-
timeout=
|
|
214
|
+
timeout=HTTP_TIMEOUT_LONG,
|
|
220
215
|
)
|
|
221
216
|
|
|
222
217
|
if response.status_code != 200:
|
|
@@ -261,7 +256,7 @@ class ImageAnalyzer:
|
|
|
261
256
|
url,
|
|
262
257
|
params=params,
|
|
263
258
|
json=body,
|
|
264
|
-
timeout=
|
|
259
|
+
timeout=HTTP_TIMEOUT_LONG,
|
|
265
260
|
)
|
|
266
261
|
|
|
267
262
|
if response.status_code != 200:
|
|
@@ -311,14 +306,14 @@ class ImageAnalyzer:
|
|
|
311
306
|
],
|
|
312
307
|
}
|
|
313
308
|
],
|
|
314
|
-
"max_tokens":
|
|
309
|
+
"max_tokens": VISION_MAX_TOKENS,
|
|
315
310
|
}
|
|
316
311
|
|
|
317
312
|
response = requests.post(
|
|
318
313
|
url,
|
|
319
314
|
headers=headers,
|
|
320
315
|
json=body,
|
|
321
|
-
timeout=
|
|
316
|
+
timeout=HTTP_TIMEOUT_LONG,
|
|
322
317
|
)
|
|
323
318
|
|
|
324
319
|
if response.status_code != 200:
|
rem/workers/README.md
CHANGED
|
@@ -207,7 +207,7 @@ Reads recent activity to generate comprehensive user profiles.
|
|
|
207
207
|
|
|
208
208
|
**CLI:**
|
|
209
209
|
```bash
|
|
210
|
-
rem-dreaming user-model
|
|
210
|
+
rem-dreaming user-model
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
**Frequency:** Daily (runs as part of full workflow)
|
|
@@ -235,13 +235,13 @@ Extracts temporal narratives from resources.
|
|
|
235
235
|
**CLI:**
|
|
236
236
|
```bash
|
|
237
237
|
# Process last 24 hours
|
|
238
|
-
rem-dreaming moments
|
|
238
|
+
rem-dreaming moments
|
|
239
239
|
|
|
240
240
|
# Custom lookback
|
|
241
|
-
rem-dreaming moments
|
|
241
|
+
rem-dreaming moments --lookback-hours=48
|
|
242
242
|
|
|
243
243
|
# Limit resources processed
|
|
244
|
-
rem-dreaming moments
|
|
244
|
+
rem-dreaming moments --limit=100
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
**Frequency:** Daily or on-demand
|
|
@@ -283,13 +283,13 @@ Builds semantic relationships between resources.
|
|
|
283
283
|
**CLI:**
|
|
284
284
|
```bash
|
|
285
285
|
# Semantic mode (fast, cheap)
|
|
286
|
-
rem-dreaming affinity
|
|
286
|
+
rem-dreaming affinity
|
|
287
287
|
|
|
288
288
|
# LLM mode (intelligent, expensive)
|
|
289
|
-
rem-dreaming affinity
|
|
289
|
+
rem-dreaming affinity --use-llm --limit=100
|
|
290
290
|
|
|
291
291
|
# Custom lookback
|
|
292
|
-
rem-dreaming affinity
|
|
292
|
+
rem-dreaming affinity --lookback-hours=168
|
|
293
293
|
```
|
|
294
294
|
|
|
295
295
|
**Frequency:**
|
|
@@ -308,13 +308,13 @@ Runs all operations in sequence.
|
|
|
308
308
|
**CLI:**
|
|
309
309
|
```bash
|
|
310
310
|
# Single tenant
|
|
311
|
-
rem-dreaming full
|
|
311
|
+
rem-dreaming full
|
|
312
312
|
|
|
313
313
|
# All active tenants (daily cron)
|
|
314
314
|
rem-dreaming full --all-tenants
|
|
315
315
|
|
|
316
316
|
# Use LLM affinity mode
|
|
317
|
-
rem-dreaming full
|
|
317
|
+
rem-dreaming full --use-llm-affinity
|
|
318
318
|
```
|
|
319
319
|
|
|
320
320
|
**Frequency:** Daily at 3 AM UTC
|
|
@@ -455,16 +455,16 @@ export REM_API_URL=http://localhost:8000
|
|
|
455
455
|
export OPENAI_API_KEY=sk-...
|
|
456
456
|
|
|
457
457
|
# Run user model update
|
|
458
|
-
python -m rem.cli.dreaming user-model
|
|
458
|
+
python -m rem.cli.dreaming user-model
|
|
459
459
|
|
|
460
460
|
# Run moment construction
|
|
461
|
-
python -m rem.cli.dreaming moments
|
|
461
|
+
python -m rem.cli.dreaming moments --lookback-hours=24
|
|
462
462
|
|
|
463
463
|
# Run affinity (semantic mode)
|
|
464
|
-
python -m rem.cli.dreaming affinity
|
|
464
|
+
python -m rem.cli.dreaming affinity
|
|
465
465
|
|
|
466
466
|
# Run full workflow
|
|
467
|
-
python -m rem.cli.dreaming full
|
|
467
|
+
python -m rem.cli.dreaming full
|
|
468
468
|
```
|
|
469
469
|
|
|
470
470
|
### Testing with Docker
|
|
@@ -478,7 +478,7 @@ docker run --rm \
|
|
|
478
478
|
-e REM_API_URL=http://host.docker.internal:8000 \
|
|
479
479
|
-e OPENAI_API_KEY=$OPENAI_API_KEY \
|
|
480
480
|
rem-stack:latest \
|
|
481
|
-
python -m rem.cli.dreaming full
|
|
481
|
+
python -m rem.cli.dreaming full
|
|
482
482
|
```
|
|
483
483
|
|
|
484
484
|
## Architecture Decisions
|
rem/workers/__init__.py
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Maintainer Worker.
|
|
3
|
+
|
|
4
|
+
Handles background maintenance tasks for PostgreSQL:
|
|
5
|
+
1. Cleaning up expired rate limit counters (UNLOGGED table).
|
|
6
|
+
2. Refreshing materialized views (if any).
|
|
7
|
+
3. Vacuuming specific tables (if needed).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python -m rem.workers.db_maintainer
|
|
11
|
+
|
|
12
|
+
# Or via docker-compose:
|
|
13
|
+
# command: python -m rem.workers.db_maintainer
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import signal
|
|
18
|
+
from loguru import logger
|
|
19
|
+
|
|
20
|
+
from ..services.postgres.service import PostgresService
|
|
21
|
+
from ..services.rate_limit import RateLimitService
|
|
22
|
+
|
|
23
|
+
class DatabaseMaintainer:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.running = False
|
|
26
|
+
self.db = PostgresService()
|
|
27
|
+
self.rate_limiter = RateLimitService(self.db)
|
|
28
|
+
|
|
29
|
+
async def start(self):
|
|
30
|
+
"""Start maintenance loop."""
|
|
31
|
+
self.running = True
|
|
32
|
+
logger.info("Starting Database Maintainer Worker")
|
|
33
|
+
|
|
34
|
+
await self.db.connect()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
while self.running:
|
|
38
|
+
await self._run_maintenance_cycle()
|
|
39
|
+
# Sleep for 5 minutes
|
|
40
|
+
await asyncio.sleep(300)
|
|
41
|
+
finally:
|
|
42
|
+
await self.db.disconnect()
|
|
43
|
+
|
|
44
|
+
async def _run_maintenance_cycle(self):
|
|
45
|
+
"""Execute maintenance tasks."""
|
|
46
|
+
logger.debug("Running maintenance cycle...")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# 1. Cleanup Rate Limits
|
|
50
|
+
await self.rate_limiter.cleanup_expired()
|
|
51
|
+
|
|
52
|
+
# 2. (Future) Refresh Views
|
|
53
|
+
# await self.db.execute("REFRESH MATERIALIZED VIEW ...")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Maintenance cycle failed: {e}")
|
|
57
|
+
|
|
58
|
+
def stop(self):
|
|
59
|
+
"""Stop worker gracefully."""
|
|
60
|
+
self.running = False
|
|
61
|
+
logger.info("Stopping Database Maintainer Worker...")
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
worker = DatabaseMaintainer()
|
|
65
|
+
|
|
66
|
+
# Handle signals
|
|
67
|
+
loop = asyncio.get_running_loop()
|
|
68
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
69
|
+
loop.add_signal_handler(sig, worker.stop)
|
|
70
|
+
|
|
71
|
+
await worker.start()
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
asyncio.run(main())
|