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.
Files changed (34) hide show
  1. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/PKG-INFO +2 -2
  2. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/pyproject.toml +2 -2
  3. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/PKG-INFO +2 -2
  4. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/SOURCES.txt +9 -1
  5. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/__init__.py +2 -4
  6. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/doi.py +6 -6
  7. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/github.py +35 -18
  8. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/orcid.py +2 -2
  9. pylinks-0.0.0.dev25/src/pylinks/exception/__init__.py +2 -0
  10. pylinks-0.0.0.dev25/src/pylinks/exception/base.py +23 -0
  11. pylinks-0.0.0.dev25/src/pylinks/exception/media_type.py +20 -0
  12. pylinks-0.0.0.dev25/src/pylinks/exception/uri.py +13 -0
  13. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/http.py +30 -27
  14. pylinks-0.0.0.dev25/src/pylinks/media_type.py +111 -0
  15. pylinks-0.0.0.dev25/src/pylinks/site/__init__.py +1 -0
  16. pylinks-0.0.0.dev25/src/pylinks/site/binder.py +185 -0
  17. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/conda.py +4 -5
  18. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/github.py +32 -20
  19. pylinks-0.0.0.dev25/src/pylinks/site/lib_io.py +67 -0
  20. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/pypi.py +4 -5
  21. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/site/readthedocs.py +7 -8
  22. pylinks-0.0.0.dev25/src/pylinks/uri/__init__.py +1 -0
  23. pylinks-0.0.0.dev25/src/pylinks/uri/data.py +200 -0
  24. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/url.py +7 -25
  25. PyLinks-0.0.0.dev23/src/pylinks/site/__init__.py +0 -1
  26. PyLinks-0.0.0.dev23/src/pylinks/site/binder.py +0 -2
  27. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/setup.cfg +0 -0
  28. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/dependency_links.txt +0 -0
  29. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/not-zip-safe +0 -0
  30. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/requires.txt +0 -0
  31. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/PyLinks.egg-info/top_level.txt +0 -0
  32. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/_settings.py +0 -0
  33. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/api/__init__.py +0 -0
  34. {PyLinks-0.0.0.dev23 → pylinks-0.0.0.dev25}/src/pylinks/exceptions.py +0 -0
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyLinks
3
- Version: 0.0.0.dev23
4
- Requires-Python: >=3.9
3
+ Version: 0.0.0.dev25
4
+ Requires-Python: >=3.10
5
5
  Requires-Dist: requests<3,>=2.31.0
@@ -17,10 +17,10 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.0.0.dev23"
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.9"
25
+ requires-python = ">=3.10"
26
26
 
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyLinks
3
- Version: 0.0.0.dev23
4
- Requires-Python: >=3.9
3
+ Version: 0.0.0.dev25
4
+ Requires-Python: >=3.10
5
5
  Requires-Dist: requests<3,>=2.31.0
@@ -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 .url import url
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
- from pylinks.http import request as _request
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 _request(
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 _request(
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 _request(
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 _request(
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 _request(
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}/{query}",
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}/{query}",
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
- pylinks.download(
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
- pylinks.download(
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[tuple[int, int, int]]:
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, tag[0].split("."))) for tag in tags])
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 = pylinks.request(
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,2 @@
1
+ from pylinks.exception.base import PyLinksException
2
+ from pylinks.exception import media_type
@@ -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
- from pylinks import exceptions
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
- response = requests.request(
154
- method=verb,
155
- url=str(url),
156
- params=params,
157
- data=data,
158
- headers=headers,
159
- cookies=cookies,
160
- files=files,
161
- auth=auth,
162
- timeout=timeout,
163
- allow_redirects=allow_redirects,
164
- proxies=proxies,
165
- hooks=hooks,
166
- stream=stream,
167
- verify=verify,
168
- cert=cert,
169
- json=json,
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