PyLinks 0.0.0.dev23__tar.gz → 0.0.0.dev25__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.
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/PKG-INFO +2 -2
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/pyproject.toml +2 -2
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/PKG-INFO +2 -2
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/SOURCES.txt +9 -1
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/__init__.py +2 -4
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/doi.py +6 -6
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/github.py +35 -18
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/orcid.py +2 -2
- pylinks-0.0.0.dev25/src/pylinks/exception/__init__.py +2 -0
- pylinks-0.0.0.dev25/src/pylinks/exception/base.py +23 -0
- pylinks-0.0.0.dev25/src/pylinks/exception/media_type.py +20 -0
- pylinks-0.0.0.dev25/src/pylinks/exception/uri.py +13 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/http.py +30 -27
- pylinks-0.0.0.dev25/src/pylinks/media_type.py +111 -0
- pylinks-0.0.0.dev25/src/pylinks/site/__init__.py +1 -0
- pylinks-0.0.0.dev25/src/pylinks/site/binder.py +185 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/conda.py +4 -5
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/github.py +32 -20
- pylinks-0.0.0.dev25/src/pylinks/site/lib_io.py +67 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/pypi.py +4 -5
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/readthedocs.py +7 -8
- pylinks-0.0.0.dev25/src/pylinks/uri/__init__.py +1 -0
- pylinks-0.0.0.dev25/src/pylinks/uri/data.py +200 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/url.py +7 -25
- PyLinks-0.0.0.dev23/src/pylinks/site/__init__.py +0 -1
- PyLinks-0.0.0.dev23/src/pylinks/site/binder.py +0 -2
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/setup.cfg +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/dependency_links.txt +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/not-zip-safe +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/requires.txt +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/top_level.txt +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/_settings.py +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/__init__.py +0 -0
- {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/exceptions.py +0 -0
|
@@ -17,10 +17,10 @@ namespaces = true
|
|
|
17
17
|
# ----------------------------------------- Project Metadata -------------------------------------
|
|
18
18
|
#
|
|
19
19
|
[project]
|
|
20
|
-
version = "0.0.0.
|
|
20
|
+
version = "0.0.0.dev25"
|
|
21
21
|
name = "PyLinks"
|
|
22
22
|
dependencies = [
|
|
23
23
|
"requests >= 2.31.0, < 3",
|
|
24
24
|
]
|
|
25
|
-
requires-python = ">=3.
|
|
25
|
+
requires-python = ">=3.10"
|
|
26
26
|
|
|
@@ -9,14 +9,22 @@ src/pylinks/__init__.py
|
|
|
9
9
|
src/pylinks/_settings.py
|
|
10
10
|
src/pylinks/exceptions.py
|
|
11
11
|
src/pylinks/http.py
|
|
12
|
+
src/pylinks/media_type.py
|
|
12
13
|
src/pylinks/url.py
|
|
13
14
|
src/pylinks/api/__init__.py
|
|
14
15
|
src/pylinks/api/doi.py
|
|
15
16
|
src/pylinks/api/github.py
|
|
16
17
|
src/pylinks/api/orcid.py
|
|
18
|
+
src/pylinks/exception/__init__.py
|
|
19
|
+
src/pylinks/exception/base.py
|
|
20
|
+
src/pylinks/exception/media_type.py
|
|
21
|
+
src/pylinks/exception/uri.py
|
|
17
22
|
src/pylinks/site/__init__.py
|
|
18
23
|
src/pylinks/site/binder.py
|
|
19
24
|
src/pylinks/site/conda.py
|
|
20
25
|
src/pylinks/site/github.py
|
|
26
|
+
src/pylinks/site/lib_io.py
|
|
21
27
|
src/pylinks/site/pypi.py
|
|
22
|
-
src/pylinks/site/readthedocs.py
|
|
28
|
+
src/pylinks/site/readthedocs.py
|
|
29
|
+
src/pylinks/uri/__init__.py
|
|
30
|
+
src/pylinks/uri/data.py
|
|
@@ -11,7 +11,5 @@ but can be used directly from the root. It returns a URL object, also defined in
|
|
|
11
11
|
Other available modules offer shortcuts for creating useful URLs for popular online services.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from ._settings import settings
|
|
15
|
-
from
|
|
16
|
-
from .http import request, download, graphql_query
|
|
17
|
-
from . import api, site
|
|
14
|
+
from pylinks._settings import settings
|
|
15
|
+
from pylinks import url, http, api, site, exceptions, uri, media_type
|
|
@@ -5,7 +5,7 @@ import datetime
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
# Non-standard libraries
|
|
8
|
-
|
|
8
|
+
import pylinks as _pylinks
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class DOI:
|
|
@@ -58,13 +58,13 @@ class DOI:
|
|
|
58
58
|
accept += f"; style={style}"
|
|
59
59
|
if locale:
|
|
60
60
|
accept += f"; locale={locale}"
|
|
61
|
-
return
|
|
61
|
+
return _pylinks.http.request(
|
|
62
62
|
self.url, headers={"accept": accept}, encoding="utf-8", response_type="str"
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
@property
|
|
66
66
|
def bibtex(self) -> str:
|
|
67
|
-
return
|
|
67
|
+
return _pylinks.http.request(
|
|
68
68
|
self.url,
|
|
69
69
|
headers={"accept": "application/x-bibtex"},
|
|
70
70
|
encoding="utf-8",
|
|
@@ -73,7 +73,7 @@ class DOI:
|
|
|
73
73
|
|
|
74
74
|
@property
|
|
75
75
|
def ris(self) -> str:
|
|
76
|
-
return
|
|
76
|
+
return _pylinks.http.request(
|
|
77
77
|
self.url,
|
|
78
78
|
headers={"accept": "application/x-research-info-systems"},
|
|
79
79
|
encoding="utf-8",
|
|
@@ -85,7 +85,7 @@ class DOI:
|
|
|
85
85
|
"""
|
|
86
86
|
Citation data as a dictionary with Citeproc JSON schema.
|
|
87
87
|
"""
|
|
88
|
-
return
|
|
88
|
+
return _pylinks.http.request(
|
|
89
89
|
self.url,
|
|
90
90
|
headers={"accept": "application/citeproc+json"},
|
|
91
91
|
encoding="utf-8",
|
|
@@ -98,7 +98,7 @@ class DOI:
|
|
|
98
98
|
journal = data["container-title"] or None
|
|
99
99
|
journal_abbr = (
|
|
100
100
|
(
|
|
101
|
-
data.get("container-title-short") or
|
|
101
|
+
data.get("container-title-short") or _pylinks.http.request(
|
|
102
102
|
f"https://abbreviso.toolforge.org/abbreviso/a/{journal}",
|
|
103
103
|
response_type="str",
|
|
104
104
|
).title()
|
|
@@ -5,16 +5,15 @@ import re
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
|
|
7
7
|
# Non-standard libraries
|
|
8
|
-
import pylinks
|
|
9
|
-
from pylinks import request, url, graphql_query
|
|
8
|
+
import pylinks as _pylinks
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class GitHub:
|
|
13
12
|
|
|
14
13
|
def __init__(self, token: Optional[str] = None):
|
|
15
14
|
self._endpoint = {
|
|
16
|
-
"api": url("https://api.github.com"),
|
|
17
|
-
"upload": url("https://uploads.github.com"),
|
|
15
|
+
"api": _pylinks.url.create("https://api.github.com"),
|
|
16
|
+
"upload": _pylinks.url.create("https://uploads.github.com"),
|
|
18
17
|
}
|
|
19
18
|
self._token = token
|
|
20
19
|
self._headers = {"X-GitHub-Api-Version": "2022-11-28"}
|
|
@@ -31,7 +30,7 @@ class GitHub:
|
|
|
31
30
|
extra_headers: dict | None = None,
|
|
32
31
|
) -> dict:
|
|
33
32
|
headers = self._headers | extra_headers if extra_headers else self._headers
|
|
34
|
-
response = graphql_query(
|
|
33
|
+
response = _pylinks.http.graphql_query(
|
|
35
34
|
url=self._endpoint["api"] / "graphql",
|
|
36
35
|
query=f"{{{query}}}",
|
|
37
36
|
headers=headers,
|
|
@@ -50,7 +49,7 @@ class GitHub:
|
|
|
50
49
|
f'mutation($mutationInput:{mutation_input_name}!) '
|
|
51
50
|
f'{{{mutation_name}(input:$mutationInput) {{{mutation_payload}}}}}'
|
|
52
51
|
)
|
|
53
|
-
response = graphql_query(
|
|
52
|
+
response = _pylinks.http.graphql_query(
|
|
54
53
|
url=self._endpoint["api"] / "graphql",
|
|
55
54
|
query=query,
|
|
56
55
|
variables={"mutationInput": mutation_input},
|
|
@@ -69,7 +68,7 @@ class GitHub:
|
|
|
69
68
|
endpoint: Literal['api', 'upload'] = "api"
|
|
70
69
|
):
|
|
71
70
|
headers = self._headers | extra_headers if extra_headers else self._headers
|
|
72
|
-
return request(
|
|
71
|
+
return _pylinks.http.request(
|
|
73
72
|
verb=verb,
|
|
74
73
|
url=self._endpoint[endpoint] / query,
|
|
75
74
|
headers=headers,
|
|
@@ -100,8 +99,9 @@ class User:
|
|
|
100
99
|
extra_headers: dict | None = None,
|
|
101
100
|
endpoint: Literal['api', 'upload'] = "api"
|
|
102
101
|
):
|
|
102
|
+
query_part = f"/{query}" if query else ""
|
|
103
103
|
return self._github.rest_query(
|
|
104
|
-
query=f"users/{self.username}
|
|
104
|
+
query=f"users/{self.username}{query_part}",
|
|
105
105
|
verb=verb,
|
|
106
106
|
data=data,
|
|
107
107
|
json=json,
|
|
@@ -144,8 +144,9 @@ class Repo:
|
|
|
144
144
|
extra_headers: dict | None = None,
|
|
145
145
|
endpoint: Literal['api', 'upload'] = "api"
|
|
146
146
|
):
|
|
147
|
+
query_part = f"/{query}" if query else ""
|
|
147
148
|
return self._github.rest_query(
|
|
148
|
-
f"repos/{self._username}/{self._name}
|
|
149
|
+
f"repos/{self._username}/{self._name}{query_part}",
|
|
149
150
|
verb=verb,
|
|
150
151
|
data=data,
|
|
151
152
|
json=json,
|
|
@@ -328,7 +329,7 @@ class Repo:
|
|
|
328
329
|
if entry["type"] == "file":
|
|
329
330
|
filename = Path(entry["path"]).name
|
|
330
331
|
full_download_path = download_path / filename
|
|
331
|
-
|
|
332
|
+
_pylinks.http.download(
|
|
332
333
|
url=entry["download_url"], filepath=full_download_path, create_dirs=create_dirs
|
|
333
334
|
)
|
|
334
335
|
final_download_paths.append(full_download_path)
|
|
@@ -362,7 +363,7 @@ class Repo:
|
|
|
362
363
|
full_download_path = download_path / download_filename
|
|
363
364
|
else:
|
|
364
365
|
full_download_path = download_path / Path(content["path"]).name
|
|
365
|
-
|
|
366
|
+
_pylinks.http.download(
|
|
366
367
|
url=content["download_url"],
|
|
367
368
|
filepath=full_download_path,
|
|
368
369
|
create_dirs=create_dirs,
|
|
@@ -370,7 +371,7 @@ class Repo:
|
|
|
370
371
|
)
|
|
371
372
|
return full_download_path
|
|
372
373
|
|
|
373
|
-
def semantic_versions(self, tag_prefix: str = "v") -> list[
|
|
374
|
+
def semantic_versions(self, tag_prefix: str = "v") -> list[str]:
|
|
374
375
|
"""
|
|
375
376
|
Get a list of all tags from a GitHub repository that represent SemVer version numbers,
|
|
376
377
|
i.e. 'X.Y.Z' where X, Y, and Z are integers.
|
|
@@ -386,16 +387,11 @@ class Repo:
|
|
|
386
387
|
`[(0, 1, 0), (0, 1, 1), (0, 2, 0), (1, 0, 0), (1, 1, 0)]`
|
|
387
388
|
"""
|
|
388
389
|
tags = self.tag_names(pattern=rf"^{tag_prefix}(\d+\.\d+\.\d+)$")
|
|
389
|
-
return sorted([tuple(map(int,
|
|
390
|
+
return sorted((tag[0] for tag in tags), key=lambda x: tuple(map(int, x.split("."))))
|
|
390
391
|
|
|
391
392
|
def discussion_categories(self) -> list[dict[str, str]]:
|
|
392
393
|
"""Get discussion categories for a repository.
|
|
393
394
|
|
|
394
|
-
Parameters
|
|
395
|
-
----------
|
|
396
|
-
access_token : str
|
|
397
|
-
GitHub access token.
|
|
398
|
-
|
|
399
395
|
Returns
|
|
400
396
|
-------
|
|
401
397
|
A list of discussion categories as dictionaries with keys "name", "slug", and "id".
|
|
@@ -852,6 +848,12 @@ class Repo:
|
|
|
852
848
|
return output
|
|
853
849
|
|
|
854
850
|
def repo_topics_replace(self, topics: list[str]):
|
|
851
|
+
"""Replace all repository topics.
|
|
852
|
+
|
|
853
|
+
References
|
|
854
|
+
----------
|
|
855
|
+
- [GitHub API Docs](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#replace-all-repository-topics)
|
|
856
|
+
"""
|
|
855
857
|
topic_pattern = re.compile(r"^[a-z0-9][a-z0-9\-]*$")
|
|
856
858
|
for topic in topics:
|
|
857
859
|
if not isinstance(topic, str):
|
|
@@ -1508,6 +1510,21 @@ class Repo:
|
|
|
1508
1510
|
raise ValueError("At least one of the ruleset parameters must be specified.")
|
|
1509
1511
|
return self._rest_query(query=f"rulesets/{ruleset_id}", verb="PUT", json=data)
|
|
1510
1512
|
|
|
1513
|
+
def ruleset_delete(self, ruleset_id: int) -> dict:
|
|
1514
|
+
"""
|
|
1515
|
+
Delete a ruleset.
|
|
1516
|
+
|
|
1517
|
+
Parameters
|
|
1518
|
+
----------
|
|
1519
|
+
ruleset_id : int
|
|
1520
|
+
The ID of the ruleset.
|
|
1521
|
+
|
|
1522
|
+
References
|
|
1523
|
+
----------
|
|
1524
|
+
- [GitHub API Docs](https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#delete-a-repository-ruleset)
|
|
1525
|
+
"""
|
|
1526
|
+
return self._rest_query(query=f"rulesets/{ruleset_id}", verb="DELETE")
|
|
1527
|
+
|
|
1511
1528
|
def actions_permissions_workflow_default(self) -> dict:
|
|
1512
1529
|
"""
|
|
1513
1530
|
Get default workflow permissions granted to the GITHUB_TOKEN when running workflows in the repository,
|
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
import warnings
|
|
4
4
|
|
|
5
5
|
# Non-standard libraries
|
|
6
|
-
import pylinks
|
|
6
|
+
import pylinks as _pylinks
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Orcid:
|
|
@@ -20,7 +20,7 @@ class Orcid:
|
|
|
20
20
|
@property
|
|
21
21
|
def records(self) -> dict:
|
|
22
22
|
if not self._data:
|
|
23
|
-
self._data =
|
|
23
|
+
self._data = _pylinks.http.request(
|
|
24
24
|
url=f"https://pub.orcid.org/v3.0/{self.id}",
|
|
25
25
|
headers={"Accept": "application/json"},
|
|
26
26
|
response_type="json",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PyLinks base exception."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path as _Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PyLinksException(Exception):
|
|
7
|
+
"""Base exception for PyLinks.
|
|
8
|
+
|
|
9
|
+
All exceptions raised by PyLinks inherit from this class.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, message: str):
|
|
12
|
+
self.message = message
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PyLinksFileNotFoundError(PyLinksException):
|
|
18
|
+
"""File not found error."""
|
|
19
|
+
def __init__(self, path: str | _Path):
|
|
20
|
+
msg = f"No file found at input path '{path}.'"
|
|
21
|
+
super().__init__(message=msg)
|
|
22
|
+
self.path = path
|
|
23
|
+
return
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pylinks.exception import PyLinksException as _PyLinksException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PyLinksMediaTypeParseError(_PyLinksException):
|
|
5
|
+
"""Error parsing a media type."""
|
|
6
|
+
def __init__(self, message: str, media_type: str):
|
|
7
|
+
msg = f"Failed to parse media type '{media_type}': {message}"
|
|
8
|
+
super().__init__(message=msg)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.media_type = media_type
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PyLinksMediaTypeGuessError(_PyLinksException):
|
|
15
|
+
"""Error guessing the media type of a data URI."""
|
|
16
|
+
def __init__(self, path: str):
|
|
17
|
+
msg = f"Failed to guess the media type of '{path}'."
|
|
18
|
+
super().__init__(message=msg)
|
|
19
|
+
self.path = path
|
|
20
|
+
return
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pathlib import Path as _Path
|
|
2
|
+
|
|
3
|
+
from pylinks.exception import PyLinksException as _PyLinksException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PyLinksDataURIParseError(_PyLinksException):
|
|
7
|
+
"""Error parsing a data URI."""
|
|
8
|
+
def __init__(self, message: str, data_uri: str):
|
|
9
|
+
msg = f"Failed to parse data URI '{data_uri}': {message}"
|
|
10
|
+
super().__init__(message=msg)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.data_uri = data_uri
|
|
13
|
+
return
|
|
@@ -20,7 +20,7 @@ import time
|
|
|
20
20
|
from functools import wraps
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
import requests
|
|
23
|
-
|
|
23
|
+
import pylinks as _pylinks
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class RetryConfig(NamedTuple):
|
|
@@ -73,7 +73,7 @@ class HTTPRequestRetryConfig(NamedTuple):
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def request(
|
|
76
|
-
url: str,
|
|
76
|
+
url: str | _pylinks.url.URL,
|
|
77
77
|
verb: Union[str, Literal["GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"]] = "GET",
|
|
78
78
|
params: Optional[Union[dict, List[tuple], bytes]] = None,
|
|
79
79
|
data: Optional[Union[dict, List[tuple], bytes]] = None,
|
|
@@ -150,24 +150,27 @@ def request(
|
|
|
150
150
|
|
|
151
151
|
def get_response_value():
|
|
152
152
|
def get_response():
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
153
|
+
try:
|
|
154
|
+
response = requests.request(
|
|
155
|
+
method=verb,
|
|
156
|
+
url=str(url),
|
|
157
|
+
params=params,
|
|
158
|
+
data=data,
|
|
159
|
+
headers=headers,
|
|
160
|
+
cookies=cookies,
|
|
161
|
+
files=files,
|
|
162
|
+
auth=auth,
|
|
163
|
+
timeout=timeout,
|
|
164
|
+
allow_redirects=allow_redirects,
|
|
165
|
+
proxies=proxies,
|
|
166
|
+
hooks=hooks,
|
|
167
|
+
stream=stream,
|
|
168
|
+
verify=verify,
|
|
169
|
+
cert=cert,
|
|
170
|
+
json=json,
|
|
171
|
+
)
|
|
172
|
+
except requests.exceptions.RequestException as e:
|
|
173
|
+
raise _pylinks.exceptions.WebAPIRequestError(e) from e
|
|
171
174
|
_raise_for_status_code(
|
|
172
175
|
response=response,
|
|
173
176
|
temporary_error_status_codes=(
|
|
@@ -189,7 +192,7 @@ def request(
|
|
|
189
192
|
else _retry_on_exception(
|
|
190
193
|
get_response,
|
|
191
194
|
config=retry_config.config_status,
|
|
192
|
-
catch=exceptions.WebAPITemporaryStatusCodeError,
|
|
195
|
+
catch=_pylinks.exceptions.WebAPITemporaryStatusCodeError,
|
|
193
196
|
)
|
|
194
197
|
)
|
|
195
198
|
# Call the (decorated or non-decorated) response function.
|
|
@@ -212,7 +215,7 @@ def request(
|
|
|
212
215
|
if response_verifier is None or response_verifier(response_value):
|
|
213
216
|
return response_value
|
|
214
217
|
# otherwise raise
|
|
215
|
-
raise exceptions.WebAPIValueError(response_value=response_value, response_verifier=response_verifier)
|
|
218
|
+
raise _pylinks.exceptions.WebAPIValueError(response_value=response_value, response_verifier=response_verifier)
|
|
216
219
|
|
|
217
220
|
# Depending on specifications in argument `retry_config`, either decorate `get_response_value`
|
|
218
221
|
# with `retry_on_exception`, or leave it as is.
|
|
@@ -226,7 +229,7 @@ def request(
|
|
|
226
229
|
else _retry_on_exception(
|
|
227
230
|
get_response_value,
|
|
228
231
|
config=retry_config.config_response,
|
|
229
|
-
catch=exceptions.WebAPIValueError,
|
|
232
|
+
catch=_pylinks.exceptions.WebAPIValueError,
|
|
230
233
|
)
|
|
231
234
|
)
|
|
232
235
|
# Call the (decorated or non-decorated) response-value function and return.
|
|
@@ -266,9 +269,9 @@ def graphql_query(
|
|
|
266
269
|
response = request(**args)
|
|
267
270
|
if isinstance(response, dict):
|
|
268
271
|
if "errors" in response:
|
|
269
|
-
raise exceptions.WebAPIError(response)
|
|
272
|
+
raise _pylinks.exceptions.WebAPIError(response)
|
|
270
273
|
elif "data" not in response:
|
|
271
|
-
raise exceptions.WebAPIError(response)
|
|
274
|
+
raise _pylinks.exceptions.WebAPIError(response)
|
|
272
275
|
else:
|
|
273
276
|
response = response["data"]
|
|
274
277
|
return response
|
|
@@ -352,9 +355,9 @@ def _raise_for_status_code(
|
|
|
352
355
|
temporary_error_status_codes is not None
|
|
353
356
|
and response.status_code in temporary_error_status_codes
|
|
354
357
|
):
|
|
355
|
-
raise exceptions.WebAPITemporaryStatusCodeError(response)
|
|
358
|
+
raise _pylinks.exceptions.WebAPITemporaryStatusCodeError(response)
|
|
356
359
|
if error_status_code_range[0] <= response.status_code <= error_status_code_range[1]:
|
|
357
|
-
raise exceptions.WebAPIPersistentStatusCodeError(response)
|
|
360
|
+
raise _pylinks.exceptions.WebAPIPersistentStatusCodeError(response)
|
|
358
361
|
return
|
|
359
362
|
|
|
360
363
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Create and parse [media types](https://en.wikipedia.org/wiki/Media_type).
|
|
2
|
+
|
|
3
|
+
References
|
|
4
|
+
----------
|
|
5
|
+
- [Offical IANA Media Types](https://www.iana.org/assignments/media-types/media-types.xml)
|
|
6
|
+
- [Common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
|
|
7
|
+
- [Database of mime types](https://github.com/jshttp/mime-db/tree/master)
|
|
8
|
+
- [Database of mime types](https://github.com/patrickmccallum/mimetype-io/blob/master/src/mimeData.json)
|
|
9
|
+
- [JSON list of file extensions with their mime types](https://github.com/micnic/mime.json)
|
|
10
|
+
- [Python mimetypes](https://docs.python.org/3/library/mimetypes.html)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import mimetypes as _mimetypes
|
|
14
|
+
import dataclasses as _dataclasses
|
|
15
|
+
import re as _re
|
|
16
|
+
|
|
17
|
+
from pylinks import exception as _exception
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@_dataclasses.dataclass
|
|
21
|
+
class MediaType:
|
|
22
|
+
"""A [Media Type](https://www.iana.org/assignments/media-types/media-types.xhtml) (aka MIME type).
|
|
23
|
+
|
|
24
|
+
A media type consists of a type, a subtype with optional tree prefix,
|
|
25
|
+
optional suffixes, and optional parameters in the following format:
|
|
26
|
+
```
|
|
27
|
+
media-type = type "/" [tree "."] subtype ["+" suffix]* [";" parameter];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
type : str, default: ""
|
|
33
|
+
Type of the media. Standard types are
|
|
34
|
+
'application', 'audio', 'example', 'font', 'haptics',
|
|
35
|
+
'image', 'message', 'model', 'multipart', 'text', and 'video'.
|
|
36
|
+
tree : str, default: ""
|
|
37
|
+
Tree prefix of the media type, e.g., 'vnd', 'prs', 'x'.
|
|
38
|
+
subtype : str, default: ""
|
|
39
|
+
Subtype of the media (without the tree prefix), e.g.,
|
|
40
|
+
'json', 'ms-excel', 'oasis.opendocument.text'.
|
|
41
|
+
suffixes : list[str], default: []
|
|
42
|
+
Type suffixes, e.g., 'json', 'xml', 'zip'.
|
|
43
|
+
parameters : dict[str, str | None], default: {}
|
|
44
|
+
Additional parameters, e.g., `{"charset": "UTF-8"}`.
|
|
45
|
+
"""
|
|
46
|
+
type: str
|
|
47
|
+
subtype: str
|
|
48
|
+
tree: str = ""
|
|
49
|
+
suffixes: list[str] = _dataclasses.field(default_factory=list)
|
|
50
|
+
parameters: dict[str, str | None] = _dataclasses.field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
suffixes = "".join(f"+{suffix}" for suffix in self.suffixes)
|
|
54
|
+
if self.parameters:
|
|
55
|
+
joined = "; ".join(f"{k}={v}" if v else k for k, v in self.parameters.items())
|
|
56
|
+
params = f"; {joined}"
|
|
57
|
+
else:
|
|
58
|
+
params = ""
|
|
59
|
+
full_subtype = f"{self.tree}.{self.subtype}" if self.tree else self.subtype
|
|
60
|
+
return f"{self.type}/{full_subtype}{suffixes}{params}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse(media_type: str) -> MediaType:
|
|
64
|
+
regex = _re.compile(
|
|
65
|
+
r"""
|
|
66
|
+
^
|
|
67
|
+
(?P<type>[\w\-]+)
|
|
68
|
+
/
|
|
69
|
+
(?:(?P<tree>[\w\-]+)\.)?
|
|
70
|
+
(?P<subtype>[\w\-.]+)
|
|
71
|
+
(?P<suffixes>(\+[\w\-.]+)*)?
|
|
72
|
+
(?:\s*;\s*(?P<parameters>.*))?
|
|
73
|
+
$
|
|
74
|
+
""",
|
|
75
|
+
_re.VERBOSE
|
|
76
|
+
)
|
|
77
|
+
match = regex.match(media_type)
|
|
78
|
+
if not match:
|
|
79
|
+
raise _exception.media_type.PyLinksMediaTypeParseError(
|
|
80
|
+
f"The input does not match the regex pattern '{regex.pattern}'.",
|
|
81
|
+
media_type
|
|
82
|
+
)
|
|
83
|
+
mime = match.groupdict()
|
|
84
|
+
mime["suffixes"] = [suffix for suffix in mime.get("suffixes", "").split("+") if suffix]
|
|
85
|
+
params = {}
|
|
86
|
+
if mime["parameters"]:
|
|
87
|
+
for param in mime["parameters"].split(";"):
|
|
88
|
+
key, *value = param.split("=", 1)
|
|
89
|
+
params[key.strip()] = value[0].strip() if value else None
|
|
90
|
+
mime["parameters"] = params
|
|
91
|
+
return MediaType(**mime)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def guess_from_uri(uri: str) -> MediaType:
|
|
95
|
+
"""Guess the media type of a given URI (e.g. URL or filepath).
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
uri : str
|
|
100
|
+
The URI to guess the media type from.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
MediaType
|
|
105
|
+
The guessed media type.
|
|
106
|
+
"""
|
|
107
|
+
uri = str(uri)
|
|
108
|
+
mimetype = _mimetypes.guess_type(uri)[0]
|
|
109
|
+
if mimetype is None:
|
|
110
|
+
raise _exception.media_type.PyLinksMediaTypeGuessError(uri)
|
|
111
|
+
return parse(mimetype)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from pylinks.site import binder, conda, github, pypi, readthedocs, lib_io
|