esuls 0.1.16__tar.gz → 0.1.18__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.
- {esuls-0.1.16/src/esuls.egg-info → esuls-0.1.18}/PKG-INFO +1 -1
- {esuls-0.1.16 → esuls-0.1.18}/pyproject.toml +1 -1
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/db_cli.py +15 -1
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/request_cli.py +30 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/tests/test_db_fixes.py +1 -1
- {esuls-0.1.16 → esuls-0.1.18/src/esuls.egg-info}/PKG-INFO +1 -1
- {esuls-0.1.16 → esuls-0.1.18}/LICENSE +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/README.md +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/setup.cfg +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/__init__.py +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/download_icon.py +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/tests/test_db_concurrent.py +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls/utils.py +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls.egg-info/SOURCES.txt +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls.egg-info/dependency_links.txt +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls.egg-info/requires.txt +0 -0
- {esuls-0.1.16 → esuls-0.1.18}/src/esuls.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "esuls"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.18"
|
|
8
8
|
description = "Utility library for async database operations, HTTP requests, and parallel execution"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
@@ -46,6 +46,8 @@ class AsyncDB(Generic[SchemaType]):
|
|
|
46
46
|
_schema_init_lock: asyncio.Lock = None
|
|
47
47
|
# Threading lock to guard class-level dict mutations
|
|
48
48
|
_db_locks_guard = threading.Lock()
|
|
49
|
+
# Track all instances for cleanup
|
|
50
|
+
_instances: list['AsyncDB'] = []
|
|
49
51
|
|
|
50
52
|
def __init__(self, db_path: Union[str, Path], table_name: str, schema_class: Type[SchemaType]):
|
|
51
53
|
"""Initialize AsyncDB with a path and schema dataclass."""
|
|
@@ -76,6 +78,9 @@ class AsyncDB(Generic[SchemaType]):
|
|
|
76
78
|
# Persistent connection (lazy init)
|
|
77
79
|
self._connection: Optional[aiosqlite.Connection] = None
|
|
78
80
|
|
|
81
|
+
# Track instance for cleanup
|
|
82
|
+
AsyncDB._instances.append(self)
|
|
83
|
+
|
|
79
84
|
# Use a class-level set to track initialized schemas
|
|
80
85
|
if not hasattr(AsyncDB, '_initialized_schemas'):
|
|
81
86
|
AsyncDB._initialized_schemas = set()
|
|
@@ -138,6 +143,13 @@ class AsyncDB(Generic[SchemaType]):
|
|
|
138
143
|
pass
|
|
139
144
|
self._connection = None
|
|
140
145
|
|
|
146
|
+
@classmethod
|
|
147
|
+
async def close_all(cls) -> None:
|
|
148
|
+
"""Close all AsyncDB instances."""
|
|
149
|
+
for instance in cls._instances:
|
|
150
|
+
await instance.close()
|
|
151
|
+
cls._instances.clear()
|
|
152
|
+
|
|
141
153
|
async def _init_schema(self, db: aiosqlite.Connection) -> None:
|
|
142
154
|
"""Generate schema from dataclass structure with support for field additions."""
|
|
143
155
|
logger.debug(f"Initializing schema for {self.schema_class.__name__} in table {self.table_name}")
|
|
@@ -334,9 +346,11 @@ class AsyncDB(Generic[SchemaType]):
|
|
|
334
346
|
columns = ','.join(field_names)
|
|
335
347
|
placeholders = ','.join('?' for _ in field_names)
|
|
336
348
|
|
|
349
|
+
set_clause = ','.join(f'{col}=excluded.{col}' for col in field_names if col != 'created_at')
|
|
337
350
|
return f"""
|
|
338
|
-
INSERT
|
|
351
|
+
INSERT INTO {self.table_name} ({columns},id)
|
|
339
352
|
VALUES ({placeholders},?)
|
|
353
|
+
ON CONFLICT(id) DO UPDATE SET {set_clause}
|
|
340
354
|
"""
|
|
341
355
|
|
|
342
356
|
def _prepare_item(self, item: SchemaType) -> Tuple[str, List[Any]]:
|
|
@@ -3,6 +3,7 @@ from functools import lru_cache
|
|
|
3
3
|
from typing import TypeAlias, Union, Optional, Dict, Any, AsyncContextManager, Literal
|
|
4
4
|
from urllib.parse import urlparse
|
|
5
5
|
import asyncio
|
|
6
|
+
import atexit
|
|
6
7
|
import json
|
|
7
8
|
import random
|
|
8
9
|
import ssl
|
|
@@ -256,6 +257,35 @@ async def close_shared_client() -> None:
|
|
|
256
257
|
_domain_clients.clear()
|
|
257
258
|
|
|
258
259
|
|
|
260
|
+
async def cleanup_all() -> None:
|
|
261
|
+
"""Close all global HTTP resources (domain clients + cffi session) and DB connections."""
|
|
262
|
+
await close_shared_client()
|
|
263
|
+
if _get_session_cffi.cache_info().currsize > 0:
|
|
264
|
+
cffi_session = _get_session_cffi()
|
|
265
|
+
await cffi_session.close()
|
|
266
|
+
_get_session_cffi.cache_clear()
|
|
267
|
+
# Close all AsyncDB instances
|
|
268
|
+
from esuls.db_cli import AsyncDB
|
|
269
|
+
await AsyncDB.close_all()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _atexit_cleanup() -> None:
|
|
273
|
+
"""Run async cleanup at process exit."""
|
|
274
|
+
try:
|
|
275
|
+
loop = asyncio.get_event_loop()
|
|
276
|
+
if loop.is_running():
|
|
277
|
+
loop.create_task(cleanup_all())
|
|
278
|
+
else:
|
|
279
|
+
loop.run_until_complete(cleanup_all())
|
|
280
|
+
except RuntimeError:
|
|
281
|
+
loop = asyncio.new_event_loop()
|
|
282
|
+
loop.run_until_complete(cleanup_all())
|
|
283
|
+
loop.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
atexit.register(_atexit_cleanup)
|
|
287
|
+
|
|
288
|
+
|
|
259
289
|
async def close_domain_client(url: str, http2: Optional[bool] = None) -> None:
|
|
260
290
|
"""Close HTTP client for a specific domain. If http2 is None, closes both h1 and h2 clients."""
|
|
261
291
|
domain = _extract_domain(url)
|
|
@@ -379,7 +379,7 @@ async def test_prepare_item_dedup(temp_db):
|
|
|
379
379
|
try:
|
|
380
380
|
item = TestItem(name="test", value=5)
|
|
381
381
|
sql, values = db._prepare_item(item)
|
|
382
|
-
assert "
|
|
382
|
+
assert "ON CONFLICT(id) DO UPDATE SET" in sql
|
|
383
383
|
assert "test" in values
|
|
384
384
|
assert 5 in values
|
|
385
385
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|