esuls 0.1.1__tar.gz → 0.1.3__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.1/src/esuls.egg-info → esuls-0.1.3}/PKG-INFO +3 -1
- {esuls-0.1.1 → esuls-0.1.3}/pyproject.toml +3 -1
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls/db_cli.py +9 -10
- esuls-0.1.3/src/esuls/download_icon.py +123 -0
- {esuls-0.1.1 → esuls-0.1.3/src/esuls.egg-info}/PKG-INFO +3 -1
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls.egg-info/SOURCES.txt +1 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls.egg-info/requires.txt +2 -0
- {esuls-0.1.1 → esuls-0.1.3}/LICENSE +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/README.md +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/setup.cfg +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls/__init__.py +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls/request_cli.py +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls/utils.py +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls.egg-info/dependency_links.txt +0 -0
- {esuls-0.1.1 → esuls-0.1.3}/src/esuls.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: esuls
|
|
3
|
-
Version: 0.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
|
|
@@ -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.3"
|
|
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"
|
|
@@ -25,6 +25,8 @@ dependencies = [
|
|
|
25
25
|
"fake-useragent>=2.2.0",
|
|
26
26
|
"httpx>=0.28.1",
|
|
27
27
|
"loguru>=0.7.3",
|
|
28
|
+
"pillow>=12.0.0",
|
|
29
|
+
"python-magic>=0.4.27",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
[project.urls]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
# print(f"Save error (skipped): {e}")
|
|
330
|
+
logger.warning(f"Save error (skipped): {e}")
|
|
332
331
|
return False
|
|
333
332
|
raise
|
|
334
333
|
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|