eaf-base-api 4.2.1__tar.gz → 4.2.2__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.
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/PKG-INFO +1 -1
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/__init__.py +5 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/base.py +8 -1
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/provider.py +5 -0
- eaf_base_api-4.2.2/base_api/modules/urls.py +51 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/pyproject.toml +1 -1
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/pyproject.toml.orig +1 -1
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/LICENSE +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/README.md +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/__init__.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/config.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/errors.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/ffmpeg.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/logger.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/progress_bars.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/static_functions.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/modules/type_hints.py +0 -0
- {eaf_base_api-4.2.1 → eaf_base_api-4.2.2}/base_api/py.typed +0 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
+
"append_url_path",
|
|
3
|
+
"profile_base_url",
|
|
4
|
+
"replace_url_path",
|
|
5
|
+
"update_url_query",
|
|
2
6
|
"BaseCore",
|
|
3
7
|
"BaseMedia",
|
|
4
8
|
"Cache",
|
|
@@ -69,6 +73,7 @@ __all__ = [
|
|
|
69
73
|
|
|
70
74
|
|
|
71
75
|
from base_api.modules import errors
|
|
76
|
+
from base_api.modules.urls import append_url_path, profile_base_url, replace_url_path, update_url_query
|
|
72
77
|
from base_api.modules.progress_bars import Callback
|
|
73
78
|
from base_api.modules.errors import (
|
|
74
79
|
DataNotLoadedError,
|
|
@@ -29,6 +29,7 @@ from typing import (
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# 1. Standardize on relative imports
|
|
32
|
+
from base_api.modules.urls import FetchedText
|
|
32
33
|
from base_api.modules.errors import *
|
|
33
34
|
from base_api.modules.type_hints import (
|
|
34
35
|
DownloadReport, ResultOrder, ErrorMode, ErrorAction, ScrapeStage,
|
|
@@ -422,6 +423,9 @@ class BaseMedia:
|
|
|
422
423
|
url: str
|
|
423
424
|
core: object
|
|
424
425
|
|
|
426
|
+
# Transport metadata is separate from lazily loaded fields and the caller's URL.
|
|
427
|
+
resolved_url: str | None = field(default=None, init=False, repr=False, compare=False)
|
|
428
|
+
|
|
425
429
|
loader_methods: ClassVar[Mapping[str, str]] = {}
|
|
426
430
|
|
|
427
431
|
_load_lock: asyncio.Lock = field(
|
|
@@ -2097,7 +2101,10 @@ class BaseCore:
|
|
|
2097
2101
|
params=params,
|
|
2098
2102
|
retry_non_idempotent=retry_non_idempotent,
|
|
2099
2103
|
)
|
|
2100
|
-
content =
|
|
2104
|
+
content = FetchedText(
|
|
2105
|
+
self._decode_response(response, url, self.logger),
|
|
2106
|
+
url=getattr(response, "url", None) or url,
|
|
2107
|
+
)
|
|
2101
2108
|
if key is not None:
|
|
2102
2109
|
self.cache.set_response(key, content)
|
|
2103
2110
|
assert pending is not None
|
|
@@ -7,6 +7,7 @@ from functools import wraps
|
|
|
7
7
|
|
|
8
8
|
from base_api.modules import errors
|
|
9
9
|
from base_api.modules.logger import class_name, get_logger, log_context
|
|
10
|
+
from base_api.modules.urls import same_site_url
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
async def fetch_content(core, url: str, *, logger, owner=None, get_json=False,
|
|
@@ -20,6 +21,10 @@ async def fetch_content(core, url: str, *, logger, owner=None, get_json=False,
|
|
|
20
21
|
with log_context(owner, url):
|
|
21
22
|
try:
|
|
22
23
|
content = await core.fetch_text(url)
|
|
24
|
+
resolved = getattr(content, "url", None)
|
|
25
|
+
if (isinstance(resolved, str) and owner is not None
|
|
26
|
+
and getattr(owner, "url", None) == url and same_site_url(url, resolved)):
|
|
27
|
+
owner.resolved_url = resolved
|
|
23
28
|
return json.loads(content, strict=False) if get_json else content
|
|
24
29
|
except errors.DownloadCancelled:
|
|
25
30
|
raise
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Small URL operations shared by provider pagination and profile adapters."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def update_url_query(url: str, **changes: str | int | None) -> str:
|
|
7
|
+
"""Replace named query keys without losing filters or treating anchors as HTTP."""
|
|
8
|
+
parsed = urlsplit(url)
|
|
9
|
+
pairs = [(key, value) for key, value in parse_qsl(parsed.query, keep_blank_values=True) if key not in changes]
|
|
10
|
+
pairs.extend((key, str(value)) for key, value in changes.items() if value is not None)
|
|
11
|
+
return urlunsplit(parsed._replace(query=urlencode(pairs), fragment=""))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def replace_url_path(url: str, path: str) -> str:
|
|
15
|
+
parsed = urlsplit(url)
|
|
16
|
+
return urlunsplit(parsed._replace(path=path, fragment=""))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def append_url_path(url: str, *parts: str | int) -> str:
|
|
20
|
+
path = urlsplit(url).path.rstrip("/")
|
|
21
|
+
return replace_url_path(url, path + "/" + "/".join(str(part).strip("/") for part in parts))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def profile_base_url(url: str, *, depth: int = 2) -> str:
|
|
25
|
+
"""Remove a provider's tab/page suffix, retaining its host, slug and filters."""
|
|
26
|
+
parsed = urlsplit(url)
|
|
27
|
+
parts = [part for part in parsed.path.split("/") if part]
|
|
28
|
+
if len(parts) < depth:
|
|
29
|
+
raise ValueError("An individual profile URL with a name or ID is required")
|
|
30
|
+
return update_url_query(replace_url_path(url, "/" + "/".join(parts[:depth])), page=None)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def same_site_url(original: str, resolved: str) -> bool:
|
|
34
|
+
"""Accept response metadata on the same site, including localized subdomains."""
|
|
35
|
+
before, after = urlsplit(original), urlsplit(resolved)
|
|
36
|
+
if after.scheme not in {"http", "https"} or not before.hostname or not after.hostname:
|
|
37
|
+
return False
|
|
38
|
+
# The supported providers use these two-label domains, except RedTube Brazil.
|
|
39
|
+
depth = 3 if before.hostname.endswith(".com.br") else 2
|
|
40
|
+
return before.hostname.split(".")[-depth:] == after.hostname.split(".")[-depth:]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FetchedText(str):
|
|
44
|
+
"""A string that retains response identity through text caches and parsers."""
|
|
45
|
+
|
|
46
|
+
url: str
|
|
47
|
+
|
|
48
|
+
def __new__(cls, content: str, *, url: str):
|
|
49
|
+
value = super().__new__(cls, content)
|
|
50
|
+
value.url = url
|
|
51
|
+
return value
|
|
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
|