PyLinks 0.0.0.dev47__tar.gz → 0.0.0.dev49__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 (37) hide show
  1. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/PKG-INFO +3 -3
  2. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/pyproject.toml +3 -3
  3. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/PyLinks.egg-info/PKG-INFO +3 -3
  4. pylinks-0.0.0.dev49/src/PyLinks.egg-info/requires.txt +3 -0
  5. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/api/github.py +132 -36
  6. pylinks-0.0.0.dev49/src/pylinks/api/zenodo.py +240 -0
  7. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/exception/api.py +10 -4
  8. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/http.py +2 -2
  9. pylinks-0.0.0.dev47/src/PyLinks.egg-info/requires.txt +0 -3
  10. pylinks-0.0.0.dev47/src/pylinks/api/zenodo.py +0 -147
  11. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/README.md +0 -0
  12. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/setup.cfg +0 -0
  13. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/PyLinks.egg-info/SOURCES.txt +0 -0
  14. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/PyLinks.egg-info/dependency_links.txt +0 -0
  15. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/PyLinks.egg-info/not-zip-safe +0 -0
  16. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/PyLinks.egg-info/top_level.txt +0 -0
  17. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/__init__.py +0 -0
  18. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/_settings.py +0 -0
  19. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/api/__init__.py +0 -0
  20. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/api/doi.py +0 -0
  21. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/api/orcid.py +0 -0
  22. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/exception/__init__.py +0 -0
  23. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/exception/base.py +0 -0
  24. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/exception/media_type.py +0 -0
  25. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/exception/uri.py +0 -0
  26. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/media_type.py +0 -0
  27. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/__init__.py +0 -0
  28. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/binder.py +0 -0
  29. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/conda.py +0 -0
  30. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/github.py +0 -0
  31. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/lib_io.py +0 -0
  32. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/pypi.py +0 -0
  33. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/site/readthedocs.py +0 -0
  34. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/string.py +0 -0
  35. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/uri/__init__.py +0 -0
  36. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/uri/data.py +0 -0
  37. {pylinks-0.0.0.dev47 → pylinks-0.0.0.dev49}/src/pylinks/url.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyLinks
3
- Version: 0.0.0.dev47
3
+ Version: 0.0.0.dev49
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: requests<3,>=2.31.0
6
- Requires-Dist: ExceptionMan==0.0.0.dev34
7
- Requires-Dist: MDit==0.0.0.dev34
6
+ Requires-Dist: ExceptionMan==0.0.0.dev36
7
+ Requires-Dist: MDit==0.0.0.dev36
@@ -17,12 +17,12 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.0.0.dev47"
20
+ version = "0.0.0.dev49"
21
21
  name = "PyLinks"
22
22
  dependencies = [
23
23
  "requests >= 2.31.0, < 3",
24
- "ExceptionMan == 0.0.0.dev34",
25
- "MDit == 0.0.0.dev34",
24
+ "ExceptionMan == 0.0.0.dev36",
25
+ "MDit == 0.0.0.dev36",
26
26
  ]
27
27
  requires-python = ">=3.10"
28
28
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyLinks
3
- Version: 0.0.0.dev47
3
+ Version: 0.0.0.dev49
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: requests<3,>=2.31.0
6
- Requires-Dist: ExceptionMan==0.0.0.dev34
7
- Requires-Dist: MDit==0.0.0.dev34
6
+ Requires-Dist: ExceptionMan==0.0.0.dev36
7
+ Requires-Dist: MDit==0.0.0.dev36
@@ -0,0 +1,3 @@
1
+ requests<3,>=2.31.0
2
+ ExceptionMan==0.0.0.dev36
3
+ MDit==0.0.0.dev36
@@ -1,5 +1,6 @@
1
1
  # Standard libraries
2
- from typing import Optional, Literal
2
+ from __future__ import annotations as _annotations
3
+ from typing import TYPE_CHECKING as _TYPE_CHECKING
3
4
  from pathlib import Path
4
5
  import re
5
6
  import mimetypes
@@ -7,6 +8,9 @@ import mimetypes
7
8
  # Non-standard libraries
8
9
  import pylinks as _pylinks
9
10
 
11
+ if _TYPE_CHECKING:
12
+ from typing import Optional, Literal, Any
13
+
10
14
 
11
15
  class GitHub:
12
16
  """GitHub API
@@ -38,13 +42,22 @@ class GitHub:
38
42
  def graphql_query(
39
43
  self,
40
44
  query: str,
45
+ variables: dict[str, tuple[Any, str, bool]] | None = None,
41
46
  extra_headers: dict | None = None,
42
47
  ) -> dict:
43
48
  headers = self._headers | extra_headers if extra_headers else self._headers
49
+ if variables:
50
+ args = ", ".join(
51
+ f"${name}:{typ}{"!" if required else ""}" for name, (_, typ, required) in variables.items()
52
+ )
53
+ sig = f"query({args})"
54
+ else:
55
+ sig = "query"
44
56
  response = _pylinks.http.graphql_query(
45
57
  url=self._endpoint["api"] / "graphql",
46
- query=f"{{{query}}}",
58
+ query=f"{sig} {{{query}}}",
47
59
  headers=headers,
60
+ variables={name: value for name, (value, _, _) in variables.items()} if variables else None
48
61
  )
49
62
  return response
50
63
 
@@ -169,10 +182,12 @@ class Repo:
169
182
  def _graphql_query(
170
183
  self,
171
184
  payload: str,
185
+ variables: dict[str, tuple[Any, str, bool]] | None = None,
172
186
  extra_headers: dict | None = None,
173
187
  ) -> dict:
174
188
  return self._github.graphql_query(
175
189
  query=f'repository(name: "{self._name}", owner: "{self._username}") {{{payload}}}',
190
+ variables=variables,
176
191
  extra_headers=extra_headers,
177
192
  )["repository"]
178
193
 
@@ -620,6 +635,8 @@ class Repo:
620
635
  self,
621
636
  number: int,
622
637
  count: int = 0,
638
+ cursor_before: str | None = None,
639
+ cursor_after: str | None = None,
623
640
  sort: Literal["first", "last"] = "last",
624
641
  ) -> list[dict]:
625
642
  """
@@ -640,20 +657,52 @@ class Repo:
640
657
  ----------
641
658
  - [GitHub API Docs](https://docs.github.com/en/rest/pulls/commits?apiVersion=2022-11-28#list-commits-on-a-pull-request)
642
659
  """
643
- payload = f"pullRequest(number: {number})"
644
- commits = "{{commits()"
645
-
646
- commits = []
647
- page = 1
660
+ def post_process():
661
+ out = []
662
+ for datum in data:
663
+ commits = datum["pullRequest"]["commits"]["nodes"]
664
+ if sort == "last":
665
+ commits = reversed(commits)
666
+ for commit in commits:
667
+ commit["commit"]["authors"] = commit["commit"]["authors"]["nodes"]
668
+ out.append(commit)
669
+ return out
670
+
671
+ git_actor_fields = "{name, email, date user {id, login}}"
672
+ commit_fields = f"{{abbreviatedOid, additions, deletions, authors(first: 100) {{nodes {git_actor_fields}}}, committer {git_actor_fields}, authoredByCommitter, authoredDate, committedDate, message, messageBody, messageHeadline, oid, id, resourcePath, url}}"
673
+ page_info_fields = "pageInfo {startCursor, endCursor, hasNextPage, hasPreviousPage}"
674
+ commits_args = ["after: $after", "before: $before", f"{sort}: {100 if count <= 0 else min(count, 100)}"]
675
+ commits_fields = f"nodes {{id, resourcePath, url, commit {commit_fields} }}"
676
+ commits_sig = f"commits({", ".join(commits_args)})"
677
+ payload = f"pullRequest(number: {number}) {{ {commits_sig} {{ {commits_fields} {page_info_fields} }} }}"
678
+ variables = {
679
+ "after": (cursor_after, "String", False),
680
+ "before": (cursor_before, "String", False),
681
+ }
682
+ data = [self._graphql_query(payload, variables)]
683
+ total_downloaded = 100
648
684
  while True:
649
- response = self._rest_query(f"pulls/{number}/commits?per_page=100&page={page}")
650
- commits.extend(response)
651
- page += 1
652
- if len(response) < 100:
653
- break
654
- return commits
655
-
656
-
685
+ page_info = data[-1]["pullRequest"]["commits"]["pageInfo"]
686
+ if count <= 0:
687
+ must_continue = page_info["hasNextPage" if sort == "first" else "hasPreviousPage"]
688
+ if not must_continue:
689
+ return post_process()
690
+ elif total_downloaded >= count:
691
+ return post_process()
692
+ else:
693
+ variables[sort] = page_info["endCursor" if sort == "first" else "startCursor"]
694
+ data.append(self._graphql_query(payload, variables))
695
+ total_downloaded += 100
696
+
697
+ # commits = []
698
+ # page = 1
699
+ # while True:
700
+ # response = self._rest_query(f"pulls/{number}/commits?per_page=100&page={page}")
701
+ # commits.extend(response)
702
+ # page += 1
703
+ # if len(response) < 100:
704
+ # break
705
+ # return commits
657
706
 
658
707
  def pull_create(
659
708
  self,
@@ -1046,15 +1095,22 @@ class Repo:
1046
1095
  raise ValueError("At least one of 'new_name', 'color', or 'description' must be specified.")
1047
1096
  return self._rest_query(query=f"labels/{name}", verb="PATCH", json=data)
1048
1097
 
1098
+ def release_get(self, release_id: int) -> dict:
1099
+ return self._rest_query(query=f"releases/{release_id}")
1100
+
1101
+ def release_delete(self, release_id: int) -> None:
1102
+ self._rest_query(query=f"releases/{release_id}", verb="DELETE", response_type=None)
1103
+ return
1104
+
1049
1105
  def release_create(
1050
1106
  self,
1051
1107
  tag_name: str,
1052
- name: str,
1053
- body: str,
1054
- target_commitish: str = "",
1108
+ name: str | None = None,
1109
+ body: str | None = None,
1110
+ target_commitish: str | None = None,
1055
1111
  draft: bool = False,
1056
1112
  prerelease: bool = False,
1057
- discussion_category_name: str = "",
1113
+ discussion_category_name: str | None = None,
1058
1114
  generate_release_notes: bool = False,
1059
1115
  make_latest: Literal['true', 'false', 'legacy'] = 'true'
1060
1116
  ):
@@ -1094,22 +1150,62 @@ class Repo:
1094
1150
  ----------
1095
1151
  [GitHub API Docs](https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release)
1096
1152
  """
1097
- data = {
1098
- "tag_name": tag_name,
1099
- "draft": draft,
1100
- "prerelease": prerelease,
1101
- "generate_release_notes": generate_release_notes,
1102
- "make_latest": make_latest,
1103
- }
1104
- if name:
1105
- data["name"] = name
1106
- if body:
1107
- data["body"] = body
1108
- if target_commitish:
1109
- data["target_commitish"] = target_commitish
1110
- if discussion_category_name:
1111
- data["discussion_category_name"] = discussion_category_name
1112
- return self._rest_query(query="releases", verb="POST", json=data)
1153
+ data = {k: v for k, v in locals().items() if k != "self" and v is not None}
1154
+ return self._rest_query(query=f"releases", verb="POST", json=data)
1155
+
1156
+ def release_update(
1157
+ self,
1158
+ release_id: int,
1159
+ tag_name: str | None = None,
1160
+ name: str | None = None,
1161
+ body: str | None = None,
1162
+ target_commitish: str | None = None,
1163
+ draft: bool | None = None,
1164
+ prerelease: bool | None = None,
1165
+ discussion_category_name: str | None = None,
1166
+ make_latest: Literal['true', 'false', 'legacy'] | None = None
1167
+ ):
1168
+ """Update a release.
1169
+
1170
+ Parameters
1171
+ ----------
1172
+ tag_name : str
1173
+ The name of the tag.
1174
+ name : str
1175
+ The name of the release.
1176
+ body : str
1177
+ The body of the release post, i.e. text describing the release.
1178
+ target_commitish : str, optional
1179
+ The commitish value that determines where the Git tag is created from.
1180
+ Can be any branch or commit SHA. Unused if the Git tag already exists.
1181
+ The default is the repository's default branch.
1182
+ draft : bool, optional
1183
+ `True` to create a draft (unpublished) release, `False` to create a published one.
1184
+ prerelease : bool, default: False
1185
+ `True` to identify the release as a prerelease, `False` to identify it as a full release.
1186
+ discussion_category_name : str, optional
1187
+ The name of the discussion category for the release, to be created and linked to the release.
1188
+ The value must be a category that already exists in the repository.
1189
+ make_latest : {'true', 'false', 'legacy'}, optional
1190
+ Whether this release should be set as the latest release for the repository.
1191
+ Drafts and prereleases cannot be set as latest.
1192
+ Defaults to 'true' for newly published releases.
1193
+ 'legacy' specifies that the latest release should be determined based on
1194
+ the release creation date and higher semantic version.
1195
+
1196
+ References
1197
+ ----------
1198
+ [GitHub API Docs](https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release)
1199
+ """
1200
+ data = {k: v for k, v in locals().items() if k not in ("self", "release_id") and v is not None}
1201
+ return self._rest_query(query=f"releases/{release_id}", verb="PATCH", json=data)
1202
+
1203
+ def release_asset_list(self, release_id: int) -> list[dict]:
1204
+ return self._rest_query(query=f"releases/{release_id}/assets")
1205
+
1206
+ def release_asset_delete(self, asset_id: int) -> None:
1207
+ self._rest_query(query=f"releases/assets/{asset_id}", verb="DELETE", response_type=None)
1208
+ return
1113
1209
 
1114
1210
  def release_asset_upload(
1115
1211
  self,
@@ -1701,7 +1797,7 @@ class Repo:
1701
1797
  """
1702
1798
  payload = "branchProtectionRules(first: 100) {nodes {id, pattern}}"
1703
1799
  data = self._graphql_query(payload)
1704
- return data["repository"]["branchProtectionRules"]["nodes"]
1800
+ return data["branchProtectionRules"]["nodes"]
1705
1801
 
1706
1802
  def branch_protection_rule_create(
1707
1803
  self,
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from typing import TYPE_CHECKING as _TYPE_CHECKING
4
+ from pathlib import Path as _Path
5
+ import pylinks as _pylinks
6
+
7
+ if _TYPE_CHECKING:
8
+ from typing import Literal
9
+
10
+
11
+ class Zenodo:
12
+ """Zenodo API.
13
+
14
+ References
15
+ ----------
16
+ - [API Manual](https://developers.zenodo.org/)
17
+ - [Main Repository](https://github.com/zenodo/zenodo)
18
+ """
19
+ def __init__(self, token: str, sandbox: bool = False):
20
+ self._sandbox = sandbox
21
+ self._url = _pylinks.url.create(
22
+ "https://sandbox.zenodo.org/api" if sandbox else "https://zenodo.org/api"
23
+ )
24
+ self._headers = {"Authorization": f"Bearer {token}"}
25
+ return
26
+
27
+ def rest_query(
28
+ self,
29
+ query: str,
30
+ verb: Literal["GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"] = "GET",
31
+ params: dict | None = None,
32
+ data = None,
33
+ json = None,
34
+ content_type: str | None = "application/json",
35
+ response_type: Literal["str", "json", "bytes"] | None = "json"
36
+ ) -> dict | list:
37
+ content_header = {"Content-Type": content_type} if content_type else {}
38
+ return _pylinks.http.request(
39
+ url=self._url / query,
40
+ verb=verb,
41
+ params=params,
42
+ data=data,
43
+ json=json,
44
+ headers=self._headers | content_header,
45
+ response_type=response_type, # All responses are JSON (https://developers.zenodo.org/#responses)
46
+ )
47
+
48
+ def create_and_publish(
49
+ self,
50
+ metadata: dict,
51
+ files: list[str | _Path | tuple[str | _Path, str]],
52
+ previous_id: str | int | None = None
53
+ ):
54
+ def add_files(bucket_id: str):
55
+ for file in files:
56
+ if not isinstance(file, (str, _Path)):
57
+ filepath = file[0]
58
+ name = file[1]
59
+ else:
60
+ filepath = file
61
+ name = None
62
+ self.file_create(
63
+ bucket_id=bucket_id,
64
+ filepath=filepath,
65
+ name=name,
66
+ )
67
+ return
68
+
69
+ if not previous_id:
70
+ new_depo = self.deposition_create(metadata=metadata)
71
+ add_files(bucket_id=new_depo["links"]["bucket"])
72
+ return self.deposition_publish(new_depo["id"])
73
+ new_ver = self.deposition_new_version(deposition_id=previous_id)
74
+ for previous_file in new_ver["files"]:
75
+ self.file_delete(deposition_id=new_ver["id"], file_id=previous_file["id"])
76
+ add_files(new_ver["links"]["bucket"])
77
+ return self.deposition_publish(new_ver["id"])
78
+
79
+
80
+ def deposition_list(
81
+ self,
82
+ query: str | None = None,
83
+ status: Literal["draft", "published"] | None = None,
84
+ sort: Literal["bestmatch", "mostrecent", "-bestmatch", "-mostrecent"] | None = None,
85
+ page: int | None = None,
86
+ size: int | None = None,
87
+ all_versions: bool | None = None,
88
+ ):
89
+ params = {k: v for k, v in locals().items() if k not in ("self", "query") and v}
90
+ if query:
91
+ params["q"] = query
92
+ return self.rest_query(
93
+ "deposit/depositions",
94
+ verb="GET",
95
+ params=params,
96
+ )
97
+
98
+ def deposition_retrieve(self, deposition_id: str | int):
99
+ return self.rest_query(
100
+ f"deposit/depositions/{deposition_id}",
101
+ verb="GET",
102
+ )
103
+
104
+ def deposition_delete(self, deposition_id: str | int):
105
+ self.rest_query(
106
+ f"deposit/depositions/{deposition_id}",
107
+ verb="DELETE",
108
+ response_type=None
109
+ )
110
+ return
111
+
112
+ def deposition_create(self, metadata: dict | None = None) -> dict:
113
+ """Create a new deposition.
114
+
115
+ Returns
116
+ -------
117
+
118
+ Example response:
119
+ :::{code-block} json
120
+
121
+ {
122
+ "conceptrecid": "542200",
123
+ "created": "2020-05-19T11:58:41.606998+00:00",
124
+ "files": [],
125
+ "id": 542201,
126
+ "links": {
127
+ "bucket": "https://zenodo.org/api/files/568377dd-daf8-4235-85e1-a56011ad454b",
128
+ "discard": "https://zenodo.org/api/deposit/depositions/542201/actions/discard",
129
+ "edit": "https://zenodo.org/api/deposit/depositions/542201/actions/edit",
130
+ "files": "https://zenodo.org/api/deposit/depositions/542201/files",
131
+ "html": "https://zenodo.org/deposit/542201",
132
+ "latest_draft": "https://zenodo.org/api/deposit/depositions/542201",
133
+ "latest_draft_html": "https://zenodo.org/deposit/542201",
134
+ "publish": "https://zenodo.org/api/deposit/depositions/542201/actions/publish",
135
+ "self": "https://zenodo.org/api/deposit/depositions/542201"
136
+ },
137
+ "metadata": {
138
+ "prereserve_doi": {
139
+ "doi": "10.5072/zenodo.542201",
140
+ "recid": 542201
141
+ }
142
+ },
143
+ "modified": "2020-05-19T11:58:41.607012+00:00",
144
+ "owner": 12345,
145
+ "record_id": 542201,
146
+ "state": "unsubmitted",
147
+ "submitted": false,
148
+ "title": ""
149
+ }
150
+ :::
151
+ """
152
+ return self.rest_query(
153
+ query="deposit/depositions",
154
+ verb="POST",
155
+ json={"metadata": metadata} if metadata else {},
156
+ )
157
+
158
+ def deposition_new_version(self, deposition_id: int | str):
159
+ """Create a new version of a deposition as a draft."""
160
+ return self.rest_query(
161
+ query=f"deposit/depositions/{deposition_id}/actions/newversion",
162
+ verb="POST",
163
+ )
164
+
165
+ def deposition_update(self, deposition_id: int | str, metadata: dict):
166
+ """Update and existing deposition."""
167
+ return self.rest_query(
168
+ query=f"deposit/depositions/{deposition_id}",
169
+ verb="PUT",
170
+ json={"metadata": metadata},
171
+ )
172
+
173
+ def deposition_publish(self, deposition_id: int | str) -> dict:
174
+ """Publish a deposition."""
175
+ return self.rest_query(
176
+ query=f"deposit/depositions/{deposition_id}/actions/publish",
177
+ verb="POST",
178
+ )
179
+
180
+ def file_list(self, deposition_id: str | int):
181
+ return self.rest_query(
182
+ f"deposit/depositions/{deposition_id}/files",
183
+ verb="GET"
184
+ )
185
+
186
+ def file_create(
187
+ self,
188
+ bucket_id: str,
189
+ filepath: str | _Path,
190
+ name: str | None = None,
191
+ ) -> dict:
192
+ """Upload a file to a Zenodo bucket.
193
+
194
+ Parameters
195
+ ----------
196
+ bucket_id
197
+ Bucket ID (e.g., `"d7524553-7f8c-4632-bffb-8bea6a90b88b"`)
198
+ or bucket URL (e.g., `"https://zenodo.org/api/files/d7524553-7f8c-4632-bffb-8bea6a90b88b"`)
199
+
200
+ Returns
201
+ -------
202
+
203
+ Example response:
204
+ :::{code-block} json
205
+
206
+ {
207
+ "key": "my-file.zip",
208
+ "mimetype": "application/zip",
209
+ "checksum": "md5:2942bfabb3d05332b66eb128e0842cff",
210
+ "version_id": "38a724d3-40f1-4b27-b236-ed2e43200f85",
211
+ "size": 13264,
212
+ "created": "2020-02-26T14:20:53.805734+00:00",
213
+ "updated": "2020-02-26T14:20:53.811817+00:00",
214
+ "links": {
215
+ "self": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf",
216
+ "version": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf?versionId=38a724d3-40f1-4b27-b236-ed2e43200f85",
217
+ "uploads": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf?uploads"
218
+ },
219
+ "is_head": true,
220
+ "delete_marker": false
221
+ }
222
+ :::
223
+ """
224
+ bucket_id = bucket_id.removeprefix(f"{self._url}/files/")
225
+ filepath = _Path(filepath)
226
+ name = name or filepath.name
227
+ with open(filepath, "rb") as file:
228
+ return self.rest_query(
229
+ query=f"files/{bucket_id}/{name}",
230
+ verb="PUT",
231
+ data=file,
232
+ content_type=None,
233
+ )
234
+
235
+ def file_delete(self, deposition_id: str | int, file_id: str | int):
236
+ return self.rest_query(
237
+ f"deposit/depositions/{deposition_id}/files/{file_id}",
238
+ verb="DELETE",
239
+ response_type=None,
240
+ )
@@ -101,7 +101,7 @@ class GraphQLResponseError(WebAPIError):
101
101
  Exception class for GraphQL
102
102
  """
103
103
 
104
- def __init__(self, response: dict):
104
+ def __init__(self, response: dict, query: str):
105
105
  if "errors" in response:
106
106
  intro = "GraphQL response contains errors."
107
107
  elif "data" not in response:
@@ -109,10 +109,16 @@ class GraphQLResponseError(WebAPIError):
109
109
  super().__init__(
110
110
  title="GraphQL Response Error",
111
111
  intro=intro,
112
- details=_mdit.element.code_block(
113
- json.dumps(response, indent=3), language="json", caption="GraphQL Response"
114
- ),
112
+ details=_mdit.block_container(
113
+ _mdit.element.code_block(
114
+ json.dumps(response, indent=3), language="json", caption="GraphQL Response"
115
+ ),
116
+ _mdit.element.code_block(
117
+ query, language="graphql", caption="GraphQL Query"
118
+ ),
119
+ )
115
120
  )
121
+ self.query = query
116
122
  return
117
123
 
118
124
 
@@ -277,9 +277,9 @@ def graphql_query(
277
277
  if isinstance(response, dict):
278
278
  error_title = "GraphQL API Error"
279
279
  if "errors" in response:
280
- raise _exception.GraphQLResponseError(response)
280
+ raise _exception.GraphQLResponseError(response, query)
281
281
  elif "data" not in response:
282
- raise _exception.GraphQLResponseError(response)
282
+ raise _exception.GraphQLResponseError(response, query)
283
283
  else:
284
284
  response = response["data"]
285
285
  return response
@@ -1,3 +0,0 @@
1
- requests<3,>=2.31.0
2
- ExceptionMan==0.0.0.dev34
3
- MDit==0.0.0.dev34
@@ -1,147 +0,0 @@
1
- from __future__ import annotations as _annotations
2
-
3
- from typing import TYPE_CHECKING as _TYPE_CHECKING
4
- from pathlib import Path as _Path
5
- import pylinks as _pylinks
6
-
7
- if _TYPE_CHECKING:
8
- from typing import Literal
9
-
10
-
11
- class Zenodo:
12
- """Zenodo API.
13
-
14
- References
15
- ----------
16
- - [API Manual](https://developers.zenodo.org/)
17
- - [Main Repository](https://github.com/zenodo/zenodo)
18
- """
19
- def __init__(self, token: str, sandbox: bool = False):
20
- self._sandbox = sandbox
21
- self._url = _pylinks.url.create("https://zenodo.org/api")
22
- self._url_sandbox = _pylinks.url.create("https://sandbox.zenodo.org/api")
23
- self._headers = {"Authorization": token}
24
- return
25
-
26
- def rest_query(
27
- self,
28
- query: str,
29
- verb: Literal["GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"] = "GET",
30
- data = None,
31
- json = None,
32
- content_type: str | None = "application/json",
33
- sandbox: bool | None = None,
34
- ) -> dict | list:
35
- sandbox = sandbox if sandbox is not None else self._sandbox
36
- base_url = self._url_sandbox if sandbox else self._url
37
- content_header = {"Content-Type": content_type} if content_type else {}
38
- return _pylinks.http.request(
39
- url=base_url / query,
40
- verb=verb,
41
- data=data,
42
- json=json,
43
- headers=self._headers | content_header,
44
- response_type="json", # All responses are JSON (https://developers.zenodo.org/#responses)
45
- )
46
-
47
- def deposition_create(
48
- self,
49
- metadata: dict | None = None,
50
- sandbox: bool | None = None,
51
- ) -> dict:
52
- """Create a new deposition.
53
-
54
- Returns
55
- -------
56
-
57
- Example response:
58
- :::{code-block} json
59
-
60
- {
61
- "conceptrecid": "542200",
62
- "created": "2020-05-19T11:58:41.606998+00:00",
63
- "files": [],
64
- "id": 542201,
65
- "links": {
66
- "bucket": "https://zenodo.org/api/files/568377dd-daf8-4235-85e1-a56011ad454b",
67
- "discard": "https://zenodo.org/api/deposit/depositions/542201/actions/discard",
68
- "edit": "https://zenodo.org/api/deposit/depositions/542201/actions/edit",
69
- "files": "https://zenodo.org/api/deposit/depositions/542201/files",
70
- "html": "https://zenodo.org/deposit/542201",
71
- "latest_draft": "https://zenodo.org/api/deposit/depositions/542201",
72
- "latest_draft_html": "https://zenodo.org/deposit/542201",
73
- "publish": "https://zenodo.org/api/deposit/depositions/542201/actions/publish",
74
- "self": "https://zenodo.org/api/deposit/depositions/542201"
75
- },
76
- "metadata": {
77
- "prereserve_doi": {
78
- "doi": "10.5072/zenodo.542201",
79
- "recid": 542201
80
- }
81
- },
82
- "modified": "2020-05-19T11:58:41.607012+00:00",
83
- "owner": 12345,
84
- "record_id": 542201,
85
- "state": "unsubmitted",
86
- "submitted": false,
87
- "title": ""
88
- }
89
- :::
90
- """
91
- return self.rest_query(
92
- query="deposit/depositions",
93
- verb="POST",
94
- json=metadata or {},
95
- sandbox=sandbox,
96
- )
97
-
98
- def deposition_publish(self, deposition_id: str) -> dict:
99
- """Publish a deposition."""
100
- return self.rest_query(
101
- query=f"deposit/depositions/{deposition_id}/actions/publish",
102
- verb="POST",
103
- )
104
-
105
- def file_create(
106
- self,
107
- bucket_url: str | _pylinks.url.URL,
108
- filepath: str | _Path,
109
- upload_path: str | None = None,
110
- ) -> dict:
111
- """Upload a file to a Zenodo bucket.
112
-
113
- Returns
114
- -------
115
-
116
- Example response:
117
- :::{code-block} json
118
-
119
- {
120
- "key": "my-file.zip",
121
- "mimetype": "application/zip",
122
- "checksum": "md5:2942bfabb3d05332b66eb128e0842cff",
123
- "version_id": "38a724d3-40f1-4b27-b236-ed2e43200f85",
124
- "size": 13264,
125
- "created": "2020-02-26T14:20:53.805734+00:00",
126
- "updated": "2020-02-26T14:20:53.811817+00:00",
127
- "links": {
128
- "self": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf",
129
- "version": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf?versionId=38a724d3-40f1-4b27-b236-ed2e43200f85",
130
- "uploads": "https://zenodo.org/api/files/44cc40bc-50fd-4107-b347-00838c79f4c1/dummy_example.pdf?uploads"
131
- },
132
- "is_head": true,
133
- "delete_marker": false
134
- }
135
- :::
136
- """
137
- if not isinstance(bucket_url, _pylinks.url.URL):
138
- bucket_url = _pylinks.url.create(bucket_url)
139
- filepath = _Path(filepath)
140
- upload_path = upload_path or filepath.name
141
- with open(filepath, "rb") as file:
142
- return self.rest_query(
143
- query=bucket_url / upload_path,
144
- verb="PUT",
145
- data=file,
146
- content_type=None,
147
- )
File without changes
File without changes