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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esuls
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: Utility library for async database operations, HTTP requests, and parallel execution
5
5
  Author-email: IperGiove <ipergiove@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "esuls"
7
- version = "0.1.16"
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 OR REPLACE INTO {self.table_name} ({columns},id)
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 "INSERT OR REPLACE" in sql
382
+ assert "ON CONFLICT(id) DO UPDATE SET" in sql
383
383
  assert "test" in values
384
384
  assert 5 in values
385
385
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esuls
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: Utility library for async database operations, HTTP requests, and parallel execution
5
5
  Author-email: IperGiove <ipergiove@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes