esuls 0.1.1__py3-none-any.whl → 0.1.3__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.
esuls/db_cli.py CHANGED
@@ -9,6 +9,7 @@ from functools import lru_cache
9
9
  import uuid
10
10
  import contextlib
11
11
  import enum
12
+ from loguru import logger
12
13
 
13
14
  T = TypeVar('T')
14
15
  SchemaType = TypeVar('SchemaType', bound='BaseModel')
@@ -64,7 +65,7 @@ class AsyncDB(Generic[SchemaType]):
64
65
 
65
66
  async def _init_schema(self, db: aiosqlite.Connection) -> None:
66
67
  """Generate schema from dataclass structure with support for field additions."""
67
- print(f"Initializing schema for {self.schema_class.__name__} in table {self.table_name}")
68
+ logger.debug(f"Initializing schema for {self.schema_class.__name__} in table {self.table_name}")
68
69
 
69
70
  field_defs = []
70
71
  indexes = []
@@ -85,12 +86,12 @@ class AsyncDB(Generic[SchemaType]):
85
86
 
86
87
  # Process all fields in the dataclass - ONLY THIS SCHEMA CLASS
87
88
  schema_fields = fields(self.schema_class)
88
- print(f"Processing {len(schema_fields)} fields for {self.schema_class.__name__}")
89
+ logger.debug(f"Processing {len(schema_fields)} fields for {self.schema_class.__name__}")
89
90
 
90
91
  for f in schema_fields:
91
92
  field_name = f.name
92
93
  field_type = self._type_hints.get(field_name)
93
- print(f" Field: {field_name} -> {field_type}")
94
+ logger.debug(f" Field: {field_name} -> {field_type}")
94
95
 
95
96
  # Map Python types to SQLite types
96
97
  if field_type in (int, bool):
@@ -123,7 +124,7 @@ class AsyncDB(Generic[SchemaType]):
123
124
  elif field_name not in existing_columns:
124
125
  # Alter table to add the new column without NOT NULL constraint
125
126
  alter_sql = f"ALTER TABLE {self.table_name} ADD COLUMN {field_name} {sql_type}"
126
- print(f" Adding new column: {alter_sql}")
127
+ logger.debug(f" Adding new column: {alter_sql}")
127
128
  await db.execute(alter_sql)
128
129
  await db.commit()
129
130
 
@@ -147,7 +148,7 @@ class AsyncDB(Generic[SchemaType]):
147
148
  {', '.join(field_defs)}{constraints_sql}
148
149
  )
149
150
  """
150
- print(f"Creating table: {create_sql}")
151
+ logger.debug(f"Creating table: {create_sql}")
151
152
  await db.execute(create_sql)
152
153
 
153
154
  # Create indexes
@@ -155,7 +156,7 @@ class AsyncDB(Generic[SchemaType]):
155
156
  await db.execute(idx_stmt)
156
157
 
157
158
  await db.commit()
158
- print(f"Schema initialization complete for {self.schema_class.__name__}")
159
+ logger.debug(f"Schema initialization complete for {self.schema_class.__name__}")
159
160
 
160
161
  @contextlib.asynccontextmanager
161
162
  async def transaction(self):
@@ -279,8 +280,7 @@ class AsyncDB(Generic[SchemaType]):
279
280
 
280
281
  except Exception as e:
281
282
  if skip_errors:
282
- # Optionally log the error for debugging
283
- # print(f"Save error (skipped): {e}")
283
+ logger.warning(f"Save error (skipped): {e}")
284
284
  continue
285
285
  raise
286
286
 
@@ -327,8 +327,7 @@ class AsyncDB(Generic[SchemaType]):
327
327
 
328
328
  except Exception as e:
329
329
  if skip_errors:
330
- # Optionally log the error for debugging
331
- # print(f"Save error (skipped): {e}")
330
+ logger.warning(f"Save error (skipped): {e}")
332
331
  return False
333
332
  raise
334
333
 
esuls/download_icon.py ADDED
@@ -0,0 +1,123 @@
1
+ from urllib.parse import urlparse, unquote
2
+ from typing import TypedDict, Optional, Dict, Any
3
+ import asyncio
4
+ import io
5
+ import magic
6
+ from PIL import Image
7
+ from .request_cli import make_request
8
+
9
+ # Type definition
10
+ class IconData(TypedDict):
11
+ data: bytes
12
+ size: int
13
+ mimetype: str
14
+ name: str
15
+
16
+ # MIME types mapping
17
+ MIME_TO_EXT: Dict[str, str] = {
18
+ 'image/jpeg': '.jpg',
19
+ 'image/png': '.png',
20
+ 'image/gif': '.gif',
21
+ 'image/webp': '.webp',
22
+ 'image/svg+xml': '.svg',
23
+ 'image/x-icon': '.ico',
24
+ 'image/vnd.microsoft.icon': '.ico',
25
+ }
26
+
27
+ async def download_icon(url: str, filename: Optional[str] = None) -> Optional[IconData]:
28
+ """
29
+ Download and validate an image from a URL.
30
+
31
+ Args:
32
+ url: Target image URL
33
+ filename: Optional custom filename
34
+
35
+ Returns:
36
+ IconData object or None if download/validation fails
37
+ """
38
+ if not url:
39
+ return None
40
+
41
+ # Process filename
42
+ if not filename:
43
+ filename = _extract_filename(url)
44
+
45
+ if any(term in filename.lower() for term in ['unknown', 'missing']):
46
+ return None
47
+
48
+ # Fetch image data
49
+ response = await make_request(url, max_attempt=3, add_user_agent=True)
50
+ if not response:
51
+ return None
52
+
53
+ file_buffer = response.content
54
+ mime_type = _detect_mime_type(file_buffer)
55
+
56
+ if not mime_type:
57
+ return None
58
+
59
+ if not verify_image(file_buffer, mime_type):
60
+ return None
61
+
62
+ # Generate filename with correct extension
63
+ base_name = filename.rsplit('.', 1)[0]
64
+ extension = MIME_TO_EXT.get(mime_type, '')
65
+ final_filename = f"{base_name}{extension}"
66
+
67
+ return {
68
+ "data": file_buffer,
69
+ "size": len(file_buffer),
70
+ "mimetype": mime_type,
71
+ "name": final_filename,
72
+ }
73
+
74
+ def _extract_filename(url: str) -> str:
75
+ """Extract filename from URL path."""
76
+ parsed_url = urlparse(url)
77
+ path_components = parsed_url.path.split('/')
78
+ filename = next((comp for comp in reversed(path_components) if comp))
79
+ return unquote(filename)
80
+
81
+ def _detect_mime_type(data: bytes) -> Optional[str]:
82
+ """Detect MIME type from file content."""
83
+ mime = magic.Magic(mime=True)
84
+ return mime.from_buffer(data)
85
+
86
+ def verify_image(data: bytes, mime_type: str) -> bool:
87
+ """Verify image data integrity."""
88
+ if not mime_type.startswith('image/'):
89
+ return False
90
+
91
+ try:
92
+ # SVG validation (basic XML check)
93
+ if mime_type == 'image/svg+xml':
94
+ return b'<svg' in data and b'</svg>' in data
95
+
96
+ # Standard image validation through PIL
97
+ with Image.open(io.BytesIO(data)) as img:
98
+ img.verify()
99
+ return True
100
+ except Exception:
101
+ return False
102
+
103
+
104
+
105
+ if __name__ == "__main__":
106
+ url = "https://pbs.twimg.com/profile_images/1899026397915488256/mc-jPC-w.jpg"
107
+ icon_data = asyncio.run(download_icon(url))
108
+ if icon_data:
109
+ print(f"Downloaded: {icon_data['name']}")
110
+ print(f"MIME type: {icon_data['mimetype']}")
111
+ print(f"Size: {icon_data['size']} bytes")
112
+
113
+ # Display image if it's a standard format (not SVG)
114
+ if icon_data['mimetype'] != 'image/svg+xml':
115
+ try:
116
+ img = Image.open(io.BytesIO(icon_data['data']))
117
+ print(f"Image dimensions: {img.size}")
118
+ img.show()
119
+ print("Image displayed!")
120
+ except Exception as e:
121
+ print(f"Could not display image: {e}")
122
+ else:
123
+ print("SVG image downloaded (cannot display with PIL)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esuls
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -17,6 +17,8 @@ Requires-Dist: curl-cffi>=0.13.0
17
17
  Requires-Dist: fake-useragent>=2.2.0
18
18
  Requires-Dist: httpx>=0.28.1
19
19
  Requires-Dist: loguru>=0.7.3
20
+ Requires-Dist: pillow>=12.0.0
21
+ Requires-Dist: python-magic>=0.4.27
20
22
  Dynamic: license-file
21
23
 
22
24
  # esuls
@@ -0,0 +1,10 @@
1
+ esuls/__init__.py,sha256=z77yDz8qY_y0dTvy76Sg8_oyNSLSBE8zE1d49EBUb30,467
2
+ esuls/db_cli.py,sha256=9mxN4sl6QYNZwWDLqeaaMM4dSlWwHXZhfk5jLtQfq04,17380
3
+ esuls/download_icon.py,sha256=w-bWbyPSbWvonzq43aDDtdxIvdKSa7OSyZ7LaN0uudg,3623
4
+ esuls/request_cli.py,sha256=8wt2MQ4Y3J-vEVacizbbuHLSrENm41UhnnGGzi-DfDE,14277
5
+ esuls/utils.py,sha256=R0peIanodvDrKYFWWdLZ9weIPAUZX787XIjZH40qNo0,677
6
+ esuls-0.1.3.dist-info/licenses/LICENSE,sha256=AY0N01ARt0kbKB7CkByYLqqNQU-yalb-rpv-eXITEWA,1066
7
+ esuls-0.1.3.dist-info/METADATA,sha256=RBaJF1tlrSNSK2exr-UkEwa_8rqiJFWHYdWtOiX6J1U,6994
8
+ esuls-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ esuls-0.1.3.dist-info/top_level.txt,sha256=WWBDHRhQ0DQLBZKD7Un8uFN93GvVQnP4WvJKkvbACVA,6
10
+ esuls-0.1.3.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- esuls/__init__.py,sha256=z77yDz8qY_y0dTvy76Sg8_oyNSLSBE8zE1d49EBUb30,467
2
- esuls/db_cli.py,sha256=YOPBlKpLV4GG5WzcHov6hpZkmW-vaL1BNlBcAm8bz4k,17424
3
- esuls/request_cli.py,sha256=8wt2MQ4Y3J-vEVacizbbuHLSrENm41UhnnGGzi-DfDE,14277
4
- esuls/utils.py,sha256=R0peIanodvDrKYFWWdLZ9weIPAUZX787XIjZH40qNo0,677
5
- esuls-0.1.1.dist-info/licenses/LICENSE,sha256=AY0N01ARt0kbKB7CkByYLqqNQU-yalb-rpv-eXITEWA,1066
6
- esuls-0.1.1.dist-info/METADATA,sha256=tDJJpenllntwVCcVvKp_aCFFnEyvOSbRt2G1aP4Vcak,6928
7
- esuls-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- esuls-0.1.1.dist-info/top_level.txt,sha256=WWBDHRhQ0DQLBZKD7Un8uFN93GvVQnP4WvJKkvbACVA,6
9
- esuls-0.1.1.dist-info/RECORD,,
File without changes