zennewpy 0.1.0__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.
zennewpy-0.1.0/LICENSE ADDED
File without changes
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.1
2
+ Name: zennewpy
3
+ Version: 0.1.0
4
+ Summary: Client to work with Zenodo API
5
+ Home-page: https://github.com/yourusername/zennewpy
6
+ Author: Marco Mancini
7
+ Author-email: marco.mancini@obspm.fr
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+
16
+ # zenodopy
17
+
18
+ ![Tests](https://github.com/lgloege/zenodopy/actions/workflows/tests.yaml/badge.svg)
19
+ [![codecov](https://codecov.io/gh/lgloege/zenodopy/branch/main/graph/badge.svg?token=FVCS71HPHC)](https://codecov.io/gh/lgloege/zenodopy)
20
+ [![pypi](https://badgen.net/pypi/v/zenodopy)](https://pypi.org/project/zenodopy)
21
+ [![License:MIT](https://img.shields.io/badge/License-MIT-lightgray.svg?style=flt-square)](https://opensource.org/licenses/MIT)
22
+ [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/lgloege/zenodopy/issues)
23
+
24
+ ### Project under active deveopment, not production ready
25
+
26
+ A Python 3.6+ package to manage [Zenodo](https://zenodo.org/) repositories.
27
+
28
+ ### Functions Implemented
29
+
30
+ - `.create_project()`: create a new project
31
+ - `.upload_file()`: upload file to project
32
+ - `.download_file()`: download a file from a project
33
+ - `.delete_file()`: permanently removes a file from a project
34
+ - `.get_urls_from_doi()`: returns the files urls for a given doi
35
+
36
+ Installing
37
+ ----------
38
+
39
+ ### PyPi
40
+
41
+ ```sh
42
+ pip install zenodopy==0.3.0
43
+ ```
44
+
45
+ ### GitHub
46
+
47
+ ```sh
48
+ pip install -e git+https://github.com/lgloege/zenodopy.git#egg=zenodopy
49
+ ```
50
+
51
+ Using the Package
52
+ -----------------
53
+
54
+ 1. **Create a Zenodo access token** by first logging into your account and clicking on your username in the top right corner. Navigate to "Applications" and then "+new token" under "Personal access tokens". Keep this window open while you proceed to step 2 because **the token is only displayed once**. Note that Sandbox.zenodo is used for testing and zenodo for production. If you want to use both, create for each a token as desribed above.
55
+ 2. **Store the token** in `~/.zenodo_token` using the following command.
56
+
57
+ ```sh
58
+ # zenodo token
59
+ { echo 'ACCESS_TOKEN: your_access_token_here' } > ~/.zenodo_token
60
+
61
+ # sandbox.zenodo token
62
+ { echo 'ACCESS_TOKEN-sandbox: your_access_token_here' } > ~/.zenodo_token
63
+ ```
64
+
65
+ 3. **start using the `zenodopy` package**
66
+
67
+ ```python
68
+ import zenodopy
69
+
70
+ # always start by creating a Client object
71
+ zeno = zenodopy.Client(sandbox=True)
72
+
73
+ # list project id's associated to zenodo account
74
+ zeno.list_projects
75
+
76
+ # create a project
77
+ zeno.create_project(title="test_project", upload_type="other")
78
+ # your zeno object now points to this newly created project
79
+
80
+ # create a file to upload
81
+ with open("~/test_file.txt", "w+") as f:
82
+ f.write("Hello from zenodopy")
83
+
84
+ # upload file to zenodo
85
+ zeno.upload_file("~/test.file.txt")
86
+
87
+ # list files of project
88
+ zeno.list_files
89
+
90
+ # set project to other project id's
91
+ zeno.set_project("<id>")
92
+
93
+ # delete project
94
+ zeno._delete_project(dep_id="<id>")
95
+ ```
96
+
97
+ Notes
98
+ -----
99
+
100
+ This project is under active development. Here is a list of things that needs improvement:
101
+
102
+ - **more tests**: need to test uploading and downloading files
103
+ - **documentation**: need to setup a readthedocs
104
+ - **download based on DOI**: right now you can only download from your own projects. Would be nice to download from
105
+ - **asyncronous functions**: use `asyncio` and `aiohttp` to write async functions. This will speed up downloading multiple files.
106
+
107
+
@@ -0,0 +1,90 @@
1
+ # zenodopy
2
+
3
+ ![Tests](https://github.com/lgloege/zenodopy/actions/workflows/tests.yaml/badge.svg)
4
+ [![codecov](https://codecov.io/gh/lgloege/zenodopy/branch/main/graph/badge.svg?token=FVCS71HPHC)](https://codecov.io/gh/lgloege/zenodopy)
5
+ [![pypi](https://badgen.net/pypi/v/zenodopy)](https://pypi.org/project/zenodopy)
6
+ [![License:MIT](https://img.shields.io/badge/License-MIT-lightgray.svg?style=flt-square)](https://opensource.org/licenses/MIT)
7
+ [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/lgloege/zenodopy/issues)
8
+
9
+ ### Project under active deveopment, not production ready
10
+
11
+ A Python 3.6+ package to manage [Zenodo](https://zenodo.org/) repositories.
12
+
13
+ ### Functions Implemented
14
+
15
+ - `.create_project()`: create a new project
16
+ - `.upload_file()`: upload file to project
17
+ - `.download_file()`: download a file from a project
18
+ - `.delete_file()`: permanently removes a file from a project
19
+ - `.get_urls_from_doi()`: returns the files urls for a given doi
20
+
21
+ Installing
22
+ ----------
23
+
24
+ ### PyPi
25
+
26
+ ```sh
27
+ pip install zenodopy==0.3.0
28
+ ```
29
+
30
+ ### GitHub
31
+
32
+ ```sh
33
+ pip install -e git+https://github.com/lgloege/zenodopy.git#egg=zenodopy
34
+ ```
35
+
36
+ Using the Package
37
+ -----------------
38
+
39
+ 1. **Create a Zenodo access token** by first logging into your account and clicking on your username in the top right corner. Navigate to "Applications" and then "+new token" under "Personal access tokens". Keep this window open while you proceed to step 2 because **the token is only displayed once**. Note that Sandbox.zenodo is used for testing and zenodo for production. If you want to use both, create for each a token as desribed above.
40
+ 2. **Store the token** in `~/.zenodo_token` using the following command.
41
+
42
+ ```sh
43
+ # zenodo token
44
+ { echo 'ACCESS_TOKEN: your_access_token_here' } > ~/.zenodo_token
45
+
46
+ # sandbox.zenodo token
47
+ { echo 'ACCESS_TOKEN-sandbox: your_access_token_here' } > ~/.zenodo_token
48
+ ```
49
+
50
+ 3. **start using the `zenodopy` package**
51
+
52
+ ```python
53
+ import zenodopy
54
+
55
+ # always start by creating a Client object
56
+ zeno = zenodopy.Client(sandbox=True)
57
+
58
+ # list project id's associated to zenodo account
59
+ zeno.list_projects
60
+
61
+ # create a project
62
+ zeno.create_project(title="test_project", upload_type="other")
63
+ # your zeno object now points to this newly created project
64
+
65
+ # create a file to upload
66
+ with open("~/test_file.txt", "w+") as f:
67
+ f.write("Hello from zenodopy")
68
+
69
+ # upload file to zenodo
70
+ zeno.upload_file("~/test.file.txt")
71
+
72
+ # list files of project
73
+ zeno.list_files
74
+
75
+ # set project to other project id's
76
+ zeno.set_project("<id>")
77
+
78
+ # delete project
79
+ zeno._delete_project(dep_id="<id>")
80
+ ```
81
+
82
+ Notes
83
+ -----
84
+
85
+ This project is under active development. Here is a list of things that needs improvement:
86
+
87
+ - **more tests**: need to test uploading and downloading files
88
+ - **documentation**: need to setup a readthedocs
89
+ - **download based on DOI**: right now you can only download from your own projects. Would be nice to download from
90
+ - **asyncronous functions**: use `asyncio` and `aiohttp` to write async functions. This will speed up downloading multiple files.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="zennewpy",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ 'requests',
9
+ 'tabulate',
10
+ ],
11
+ author="Marco Mancini",
12
+ author_email="marco.mancini@obspm.fr",
13
+ description="Client to work with Zenodo API",
14
+ long_description=open("README.md").read(),
15
+ long_description_content_type="text/markdown",
16
+ url="https://github.com/yourusername/zennewpy",
17
+ classifiers=[
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ ],
22
+ )
@@ -0,0 +1,2 @@
1
+ from .zennewpy_core import *
2
+ #
@@ -0,0 +1,756 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ import re
5
+ import requests
6
+ import warnings
7
+ import tarfile
8
+ import zipfile
9
+ from tabulate import tabulate
10
+ import time
11
+
12
+ def validate_url(url):
13
+ """validates if URL is formatted correctly
14
+
15
+ Returns:
16
+ bool: True is URL is acceptable False if not acceptable
17
+ """
18
+ regex = re.compile(
19
+ r'^(?:http|ftp)s?://' # http:// or https://
20
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
21
+ r'localhost|' # localhost...
22
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
23
+ r'(?::\d+)?' # optional port
24
+ r'(?:/?|[/?]\S+)$', re.IGNORECASE)
25
+
26
+ return re.match(regex, url) is not None
27
+
28
+
29
+ def make_tarfile(output_file, source_dir):
30
+ """tar a directory
31
+ args
32
+ -----
33
+ output_file: path to output file
34
+ source_dir: path to source directory
35
+
36
+ returns
37
+ -----
38
+ tarred directory will be in output_file
39
+ """
40
+ with tarfile.open(output_file, "w:gz") as tar:
41
+ tar.add(source_dir, arcname=os.path.basename(source_dir))
42
+
43
+
44
+ def make_zipfile(path, ziph):
45
+ # ziph is zipfile handle
46
+ for root, dirs, files in os.walk(path):
47
+ for file in files:
48
+ ziph.write(os.path.join(root, file),
49
+ os.path.relpath(os.path.join(root, file),
50
+ os.path.join(path, '..')))
51
+
52
+
53
+ class BearerAuth(requests.auth.AuthBase):
54
+ """Bearer Authentication"""
55
+
56
+ def __init__(self, token):
57
+ self.token = token
58
+
59
+ def __call__(self, r):
60
+ r.headers["authorization"] = "Bearer " + self.token
61
+ return r
62
+
63
+
64
+ class Client(object):
65
+ """Zenodo Client object
66
+
67
+ Use this class to instantiate a zenodopy object
68
+ to interact with your Zenodo account
69
+
70
+ ```
71
+ import zenodopy
72
+ zeno = zenodopy.Client()
73
+ zeno.help()
74
+ ```
75
+
76
+ Setup instructions:
77
+ ```
78
+ zeno.setup_instructions
79
+ ```
80
+ """
81
+
82
+ def __init__(self, title=None, bucket=None, deposition_id=None, sandbox=None, token=None):
83
+ """initialization method"""
84
+ if sandbox:
85
+ self._endpoint = "https://sandbox.zenodo.org/api"
86
+ self._doi_pattern = r'^10\.5072/zenodo\.\d+$'
87
+ else:
88
+ self._endpoint = "https://zenodo.org/api"
89
+ self._doi_pattern = r'^10\.5281/zenodo\.\d+$'
90
+
91
+ self.title = title
92
+ self.bucket = bucket
93
+ self.deposition_id = deposition_id
94
+ self.sandbox = sandbox
95
+ self._token = self._read_from_config if token is None else token
96
+ self._bearer_auth = BearerAuth(self._token)
97
+ self.concept_id = None
98
+ self.associated = False
99
+ # 'metadata/prereservation_doi/doi'
100
+
101
+ def __repr__(self):
102
+ return f"zenodoapi('{self.title}','{self.bucket}','{self.deposition_id}')"
103
+
104
+ def __str__(self):
105
+ return f"{self.title} --- {self.deposition_id}"
106
+
107
+ # ---------------------------------------------
108
+ # hidden functions
109
+ # ---------------------------------------------
110
+
111
+ @staticmethod
112
+ def _get_upload_types():
113
+ """Acceptable upload types
114
+
115
+ Returns:
116
+ list: contains acceptable upload_types
117
+ """
118
+ return [
119
+ "publication",
120
+ "poster",
121
+ "presentation",
122
+ "dataset",
123
+ "image",
124
+ "video",
125
+ "software",
126
+ "lesson",
127
+ "physicalobject",
128
+ "other"
129
+ ]
130
+
131
+ @staticmethod
132
+ def _read_config(path=None):
133
+ """reads the configuration file
134
+
135
+ Configuration file should be ~/.zenodo_token
136
+
137
+ Args:
138
+ path (str): location of the file with ACCESS_TOKEN
139
+
140
+ Returns:
141
+ dict: dictionary with API ACCESS_TOKEN
142
+ """
143
+
144
+ if path is None:
145
+ print("You need to supply a path")
146
+
147
+ full_path = os.path.expanduser(path)
148
+ if not Path(full_path).exists():
149
+ print(f"{path} does not exist. Please check you entered the correct path")
150
+
151
+ config = {}
152
+ with open(path) as file:
153
+ for line in file.readlines():
154
+ if ":" in line:
155
+ key, value = line.strip().split(":", 1)
156
+ if key in ("ACCESS_TOKEN", "ACCESS_TOKEN-sandbox"):
157
+ config[key] = value.strip()
158
+ return config
159
+
160
+ @property
161
+ def _read_from_config(self):
162
+ """reads the web3.storage token from configuration file
163
+ configuration file is ~/.web3_storage_token
164
+ Returns:
165
+ str: ACCESS_TOKEN to connect to web3 storage
166
+ """
167
+ if self.sandbox:
168
+ dotrc = os.environ.get("ACCESS_TOKEN-sandbox", os.path.expanduser("~/.zenodo_token"))
169
+ else:
170
+ dotrc = os.environ.get("ACCESS_TOKEN", os.path.expanduser("~/.zenodo_token"))
171
+
172
+ if os.path.exists(dotrc):
173
+ config = self._read_config(dotrc)
174
+ key = config.get("ACCESS_TOKEN-sandbox") if self.sandbox else config.get("ACCESS_TOKEN")
175
+ return key
176
+ else:
177
+ print(' ** No token was found, check your ~/.zenodo_token file ** ')
178
+
179
+
180
+ def get_all_depositions(self):
181
+ """
182
+ Retrieves all depositions for the client, but only the last version with all metadata.
183
+
184
+ Returns:
185
+ list: A list of dictionaries containing the latest version of each deposition with full metadata.
186
+ """
187
+ url = f"{self._endpoint}/deposit/depositions"
188
+ params = {
189
+ "size": 1000, # Adjust this value based on your needs
190
+ "sort": "mostrecent",
191
+ "all_versions": True
192
+ }
193
+
194
+ all_depositions = []
195
+ concept_ids_processed = set()
196
+
197
+ while True:
198
+ response = requests.get(url, auth=self._bearer_auth,params=params)
199
+ response.raise_for_status()
200
+ depositions = response.json()
201
+
202
+ if not depositions:
203
+ break
204
+
205
+ for deposition in depositions:
206
+ concept_id = deposition['conceptrecid']
207
+ if concept_id not in concept_ids_processed:
208
+ # This is the latest version of this deposition
209
+ all_depositions.append(deposition)
210
+ concept_ids_processed.add(concept_id)
211
+
212
+ # Check if there are more pages
213
+ links = response.links
214
+ if 'next' not in links:
215
+ break
216
+ url = links['next']['url']
217
+
218
+ return all_depositions
219
+
220
+
221
+
222
+ def find_community_identifier(self,community_name):
223
+ """Find a community id from community name"""
224
+ params = {
225
+ "q": community_name,
226
+ "size": 100 # Adjust as needed
227
+ }
228
+
229
+ response = requests.get(self._endpoint+'/communities', params=params)
230
+
231
+ if response.status_code == 200:
232
+ data = response.json()
233
+ for community in data['hits']['hits']:
234
+ if community['metadata']['title'].lower() == community_name.lower():
235
+ return community['id']
236
+
237
+ return None
238
+
239
+
240
+ def set_deposition(self, id_value):
241
+ """
242
+ Sets the client to a specific deposition's latest version using a given deposition_id or concept_id.
243
+
244
+ Args:
245
+ id_value (int): The ID of the deposition or the concept ID to set.
246
+
247
+ Raises:
248
+ ValueError: If no valid deposition is found for the given ID.
249
+ """
250
+ if not id_value:
251
+ raise ValueError("You must provide an ID.")
252
+
253
+ # First, try to retrieve the deposition directly
254
+ try:
255
+ deposition = self.get_deposition_by_id(id_value)
256
+ concept_id = deposition.get('conceptrecid')
257
+ except requests.exceptions.HTTPError:
258
+ # If that fails, assume it's a concept ID
259
+ concept_id = id_value
260
+
261
+ # Retrieve the latest version of the deposition using concept ID
262
+ url = f"{self._endpoint}/deposit/depositions"
263
+ params = {
264
+ "q": f"conceptrecid:{concept_id}",
265
+ "sort": "mostrecent",
266
+ "size": 1,
267
+ }
268
+
269
+ # hack to the problem that the deposition_id is not created instantaneously on the server
270
+ n_try = 8
271
+ response = requests.get(url, auth=self._bearer_auth, params=params)
272
+ depositions = response.json()
273
+ while not depositions and n_try > 0:
274
+ response = requests.get(url, auth=self._bearer_auth, params=params)
275
+ depositions = response.json()
276
+ time.sleep(1)
277
+ n_try -=1
278
+
279
+ response.raise_for_status()
280
+
281
+
282
+
283
+ if not depositions:
284
+ raise ValueError(f"No depositions found for ID {id_value}.")
285
+
286
+ latest_deposition = depositions[0]
287
+
288
+ # Set class variables based on the latest version of the deposition
289
+ self.title = latest_deposition['metadata'].get('title', None)
290
+ self.bucket = latest_deposition['links'].get('bucket', 'N/A')
291
+
292
+ self.deposition_id = latest_deposition['id']
293
+ self.concept_id = latest_deposition['conceptrecid']
294
+ self.associated = True
295
+
296
+
297
+
298
+ def unset_deposition(self):
299
+ """
300
+ Unsets the current deposition settings, resetting related attributes.
301
+ """
302
+ self.title = None
303
+ self.bucket = None
304
+ self.deposition_id = None
305
+ self.concept_id = None
306
+ self.associated = False
307
+
308
+ def get_deposition_by_id(self, deposition_id=None):
309
+ """
310
+ Retrieves a specific deposition by its ID.
311
+
312
+ Args:
313
+ deposition_id (int): The ID of the deposition to retrieve.
314
+
315
+ Returns:
316
+ dict: A dictionary containing the full metadata of the deposition.
317
+ """
318
+ if deposition_id is None and self.associated :
319
+ deposition_id = self.deposition_id
320
+ url = f"{self._endpoint}/deposit/depositions/{deposition_id}"
321
+
322
+ response = requests.get(url,auth=self._bearer_auth)
323
+ response.raise_for_status()
324
+ return response.json()
325
+
326
+ @property
327
+ def deposition(self):
328
+ return self.get_deposition_by_id()
329
+
330
+
331
+ def pretty_print_depositions(self, depositions=None):
332
+ """
333
+ Pretty prints all depositions with their metadata in a table format.
334
+
335
+ Args:
336
+ depositions (list): A list of deposition dictionaries to print.
337
+ """
338
+ if depositions is None: depositions = self.get_all_depositions()
339
+
340
+ table_data = []
341
+
342
+ for deposition in depositions:
343
+ title = deposition['metadata'].get('title',None)
344
+ cid = deposition['conceptrecid']
345
+ dep_id = deposition['id']
346
+ published = 'Yes' if deposition['submitted'] else 'No'
347
+ doi = deposition.get('doi', 'N/A')
348
+
349
+ # Check if this is the currently set deposition
350
+ flag = '*' if self.deposition_id == dep_id else ''
351
+
352
+ table_data.append([title, cid, dep_id, published, doi, flag])
353
+
354
+ headers = ["Title", "CID", "ID", "Published", "DOI", "Set"]
355
+ print(tabulate(table_data, headers=headers, tablefmt="pretty"))
356
+
357
+
358
+ def create_new_deposition(self):
359
+ """
360
+ Creates a new deposition.
361
+
362
+ Returns:
363
+ int: The ID of the newly created deposition.
364
+ """
365
+ url = f"{self._endpoint}/deposit/depositions"
366
+ headers = {"Content-Type": "application/json"}
367
+
368
+ response = requests.post(url, auth=self._bearer_auth, json={}, headers=headers)
369
+ response.raise_for_status()
370
+
371
+ deposition_data = response.json()
372
+ return deposition_data['id']
373
+
374
+ def delete_deposition(self, deposition_id=None):
375
+ """
376
+ Deletes a deposition.
377
+
378
+ Args:
379
+ deposition_id (int): The ID of the deposition to delete.
380
+ """
381
+
382
+ if deposition_id is None : deposition_id = self.deposition_id
383
+
384
+ if not type(deposition_id)==list :
385
+ deposition_id = [deposition_id]
386
+
387
+ for ii in deposition_id:
388
+ url = f"{self._endpoint}/deposit/depositions/{ii}"
389
+ response = requests.delete(url, auth=self._bearer_auth)
390
+ #response.raise_for_status()
391
+ if(ii==self.deposition_id) : self.unset_deposition()
392
+ if response.status_code < 400:
393
+ print(f"Deposition {ii} deleted")
394
+
395
+ def create_metadata(self, metadata, **kwargs):
396
+ """
397
+ Creates or updates metadata for a deposition.
398
+
399
+ Args:
400
+ deposition_id (int): The ID of the deposition.
401
+ metadata (dict): The metadata to set.
402
+
403
+ Returns:
404
+ dict: The updated deposition data.
405
+ """
406
+ if not self.associated:
407
+ print("create_metadata: deposition is not associated.")
408
+ return
409
+ # Update metadata with additional fields from kwargs
410
+ for key, value in kwargs.items():
411
+ if isinstance(value, dict):
412
+ # If the value is a dictionary, update or add it to metadata
413
+ metadata[key] = metadata.get(key, {})
414
+ metadata[key].update(value)
415
+ else:
416
+ # If it's not a dictionary, simply add or update the field
417
+ metadata[key] = value
418
+
419
+
420
+ url = f"{self._endpoint}/deposit/depositions/{self.deposition_id}"
421
+ headers = {"Content-Type": "application/json"}
422
+ data = {"metadata": metadata}
423
+
424
+ response = requests.put(url, auth=self._bearer_auth, data=json.dumps(data), headers=headers)
425
+ if response.status_code >= 500:
426
+ self.delete_deposition()
427
+ response.raise_for_status()
428
+
429
+
430
+ return response.json()
431
+
432
+ def upload_file(self,file_path, remote_filename=None, file_id=None):
433
+ """
434
+ Uploads a file to a Zenodo deposition using PUT (if file_id is provided) or POST (for new files).
435
+
436
+ Args:
437
+ file_path (str): The local path of the file to upload.
438
+ remote_filename (str, optional): The filename to use on Zenodo. If None, uses the local filename.
439
+ file_id (str, optional): The ID of an existing file to update. If None, a new file will be created.
440
+
441
+ Returns:
442
+ dict: The uploaded file data, including the file_id.
443
+ """
444
+ deposition_id = self.deposition_id
445
+
446
+ if remote_filename is None:
447
+ remote_filename = os.path.basename(file_path)
448
+
449
+ if file_id:
450
+ print("this functionality does not work at moment on Zenodo!!")
451
+ # Update existing file using PUT
452
+ url = f"{self._endpoint}/deposit/depositions/{deposition_id}/files/{file_id}"
453
+ with open(file_path, "rb") as file:
454
+ response = requests.put(url, auth=self._bearer_auth, data=file)
455
+ else:
456
+ # Upload new file using POST
457
+ url = f"{self._endpoint}/deposit/depositions/{deposition_id}/files"
458
+ with open(file_path, "rb") as file:
459
+ data = {"name": remote_filename}
460
+ files = {"file": file}
461
+ response = requests.post(url, auth=self._bearer_auth, data=data, files=files)
462
+
463
+ response.raise_for_status()
464
+ file_data = response.json()
465
+
466
+ return file_data
467
+
468
+ def get_file_ids(self, deposition_id=None):
469
+ """
470
+ Retrieves the file IDs for all files in a deposition.
471
+
472
+ Args:
473
+ deposition_id (int): The ID of the deposition.
474
+
475
+ Returns:
476
+ dict: A dictionary mapping filenames to their file IDs.
477
+ """
478
+ if deposition_id is None and self.associated :
479
+ deposition_id = self.deposition_id
480
+ deposition = self.get_deposition_by_id(deposition_id)
481
+ return {file['filename']: file['id'] for file in deposition.get('files', [])}
482
+
483
+
484
+ def get_doi(self,deposition_id=None):
485
+ if deposition_id is None and self.associated :
486
+ deposition_id = self.deposition_id
487
+ deposition = self.get_deposition_by_id(deposition_id)
488
+ return deposition.get('doi',None)
489
+
490
+
491
+ def publish_deposition(self):
492
+ """
493
+ Publishes a deposition.
494
+
495
+ Args:
496
+ deposition_id (int): The ID of the deposition to publish.
497
+
498
+ Returns:
499
+ dict: The published deposition data.
500
+ """
501
+ url = f"{self._endpoint}/deposit/depositions/{self.deposition_id}/actions/publish"
502
+
503
+
504
+ try:
505
+ response = requests.post(url, auth=self._bearer_auth)
506
+ response.raise_for_status()
507
+ return True # If we get here, the request was successful
508
+ except requests.exceptions.RequestException as e:
509
+ # Log the error for debugging purposes
510
+ print(f"Error publishing deposition: {str(e)}")
511
+ return False # Return False if any exception occurs
512
+ except requests.exceptions.HTTPError as err:
513
+ print(f"HTTP error occurred: {err}")
514
+ print(f"Response: {response.text}") # For more detailed error information
515
+ except KeyError:
516
+ print("KeyError: Failed to retrieve 'publish' link. Check if the deposition is ready for publication.")
517
+ except Exception as e:
518
+ print(f"An error occurred: {e}")
519
+
520
+ @property
521
+ def is_published(self):
522
+ return self.get_deposition_by_id()['submitted']
523
+
524
+ def create_new_version(self):
525
+ """
526
+ Creates a new version of an existing deposition.
527
+
528
+
529
+ Returns:
530
+ dict: The new deposition data.
531
+ """
532
+ url = f"{self._endpoint}/deposit/depositions/{self.deposition_id}/actions/newversion"
533
+
534
+ try:
535
+ response = requests.post(url, auth=self._bearer_auth)
536
+ response.raise_for_status()
537
+ new_id = response.json()['id']
538
+
539
+ # Wait briefly to allow Zenodo to index the new version
540
+ # time.sleep(2) # Adjust as necessary
541
+
542
+ self.set_deposition(new_id)
543
+ return new_id
544
+
545
+ except requests.exceptions.RequestException as e:
546
+ if hasattr(e, 'response'):
547
+ print(f"Response content: {e.response.content}")
548
+ raise
549
+
550
+
551
+ def modify_metadata(self, metadata_updates, **kwargs):
552
+ """
553
+ Modifies metadata of a deposition (published or not).
554
+
555
+ Args:
556
+ metadata_updates (dict): The metadata fields to update.
557
+ **kwargs: Additional metadata fields to update.
558
+
559
+ Returns:
560
+ dict: The updated deposition data.
561
+ """
562
+ if not self.associated:
563
+ print (f'Error in "modify_update" Deposition is not associated !')
564
+ return None
565
+
566
+ # Check if the deposition is published
567
+ current_deposition = self.deposition
568
+
569
+ if current_deposition.get('state') == 'done':
570
+ new_id = self.create_new_version()
571
+ print(f"New deposition_id {new_id} was created for concept_id {self.concept_id}")
572
+ current_deposition = self.deposition
573
+
574
+ current_metadata = current_deposition['metadata']
575
+
576
+ # Update the metadata
577
+ current_metadata.update(metadata_updates)
578
+
579
+
580
+ # Update metadata with additional fields from kwargs
581
+ for key, value in kwargs.items():
582
+ if isinstance(value, dict):
583
+ current_metadata[key] = current_metadata.get(key, {})
584
+ current_metadata[key].update(value)
585
+ else:
586
+ current_metadata[key] = value
587
+
588
+ # Use the create_metadata method to update
589
+ result = self.create_metadata(current_metadata)
590
+ return result
591
+
592
+ def update_file(self, file_path,remote_filename=None):
593
+ """
594
+ Updates a file in a deposition (published or not).
595
+
596
+ Args:
597
+ file_path (str): The path to the new file.
598
+ remote_filename (str) : The remote name.
599
+
600
+ Returns:
601
+ dict: The updated file data.
602
+ """
603
+
604
+ if self.is_published :
605
+ print("Deposition is published. Before to update file please create a new version")
606
+ return
607
+
608
+ file_id = self.get_file_ids()[remote_filename]
609
+
610
+ # Check if the new file exists
611
+ if not os.path.exists(file_path):
612
+ raise FileNotFoundError(f"The file {file_path} does not exist.")
613
+
614
+ # Check if the new file is readable
615
+ if not os.access(file_path, os.R_OK):
616
+ raise PermissionError(f"No permission to read the file {file_path}.")
617
+
618
+ # First, try to delete the existing file
619
+ delete_url = f"{self._endpoint}/deposit/depositions/{self.deposition_id}/files/{file_id}"
620
+
621
+ try:
622
+ delete_response = requests.delete(delete_url, auth=self._bearer_auth)
623
+ delete_response.raise_for_status()
624
+ except requests.exceptions.HTTPError as e:
625
+ if e.response.status_code == 404:
626
+ # File not found, log a warning and continue with upload
627
+ print(f"Warning: File {file_id} not found. Proceeding with upload.")
628
+ else:
629
+ raise Exception(f"Failed to delete existing file: {str(e)}")
630
+
631
+ # Then, upload the new file
632
+ try:
633
+ return self.upload_file(file_path=file_path,remote_filename=remote_filename)
634
+ except Exception as e:
635
+ raise Exception(f"Failed to upload new file: {str(e)}")
636
+
637
+
638
+ def get_file_ids(self):
639
+ """
640
+ Retrieves the file IDs for all files in a deposition.
641
+
642
+ Args:
643
+ deposition_id (int): The ID of the deposition.
644
+
645
+ Returns:
646
+ dict: A dictionary mapping filenames to their file IDs.
647
+ """
648
+
649
+ deposition = self.get_deposition_by_id(self.deposition_id)
650
+ return {file['filename']: file['id'] for file in deposition.get('files', [])}
651
+
652
+
653
+ def title_exists(self, title):
654
+ """
655
+ Check if depositions with the given title exist in Zenodo,
656
+ and return their IDs if found.
657
+
658
+ Args:
659
+ title (str): The title to search for.
660
+
661
+ Returns:
662
+ dict: A dictionary containing 'exists' (bool) and 'ids' (list of str)
663
+ """
664
+ result = {
665
+ 'exists': False,
666
+ 'ids': []
667
+ }
668
+
669
+ search_url = f"{self._endpoint}/deposit/depositions"
670
+ params = {
671
+ 'size': 9999 # Adjust this value based on your needs
672
+ }
673
+
674
+ try:
675
+ response = requests.get(search_url, params=params, auth=self._bearer_auth)
676
+ response.raise_for_status()
677
+
678
+ depositions = response.json()
679
+ for deposition in depositions:
680
+ if deposition.get('metadata', {}).get('title', '').lower() == title.lower():
681
+ result['exists'] = True
682
+ deposition_id = deposition.get('id')
683
+ result['ids'].append(deposition_id)
684
+ status = "published" if deposition.get('submitted', False) else "draft"
685
+ print(f"Found {status} deposition with title: {title}, ID: {deposition_id}")
686
+
687
+ if not result['exists']:
688
+ print(f"No deposition found with title: {title}")
689
+ elif len(result['ids']) > 1:
690
+ print(f"Warning: Multiple depositions found with title: {title}")
691
+
692
+ except requests.exceptions.RequestException as e:
693
+ print(f"Error searching depositions: {e}")
694
+ print(f"Response status code: {e.response.status_code if e.response else 'N/A'}")
695
+ print(f"Response content: {e.response.text if e.response else 'N/A'}")
696
+
697
+ return result
698
+
699
+ def get_metadata(self,dep_id: str=None):
700
+ """
701
+ Retrieves the current metadata of a deposition on Zenodo.
702
+
703
+ Args:
704
+ dep_id (str): The ID of the deposition.
705
+
706
+ Returns:
707
+ response (dict): The current metadata from the Zenodo API.
708
+ """
709
+ if dep_id is None :
710
+ dep_id = self.deposition_id
711
+ url = f"{self._endpoint}/deposit/depositions/{dep_id}"
712
+
713
+ # Fetch the existing metadata
714
+ response = requests.get(url, auth=self._bearer_auth)
715
+
716
+ if response.status_code == 200:
717
+ return response.json()["metadata"]
718
+ else:
719
+ print(f"Failed to fetch metadata. Status code: {response.status_code}")
720
+ return None
721
+
722
+
723
+
724
+ if __name__ == '__main__':
725
+ zcd = Client(sandbox=True)
726
+
727
+ # Create a new deposition
728
+ deposition_id = zcd.create_new_deposition()
729
+
730
+ # Create metadata
731
+ metadata = {
732
+ 'title': 'My New Dataset',
733
+ 'description': 'This is a test dataset',
734
+ 'upload_type': 'dataset',
735
+ 'creators': [{'name': 'Doe, John', 'affiliation': 'zcddo'}]
736
+ }
737
+
738
+
739
+ zcd.set_deposition(deposition_id)
740
+
741
+ zcd.create_metadata(metadata)
742
+
743
+ # Upload a file
744
+ zcd.upload_file('/tmp/eos.zip', 'remote_filename.zip')
745
+
746
+ # Publish the deposition
747
+ #zcd.publish_deposition()
748
+
749
+ # Modify metadata (even after publishing)
750
+ zcd.modify_metadata({'title': 'Updated Dataset Title'})
751
+
752
+ zcd.update_file('/tmp/eos2.zip',remote_filename='remote_filename.zip')
753
+
754
+
755
+ # Delete a deposition (only works for unpublished depositions)
756
+ #zcd.delete_deposition(deposition_id)
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.1
2
+ Name: zennewpy
3
+ Version: 0.1.0
4
+ Summary: Client to work with Zenodo API
5
+ Home-page: https://github.com/yourusername/zennewpy
6
+ Author: Marco Mancini
7
+ Author-email: marco.mancini@obspm.fr
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+
16
+ # zenodopy
17
+
18
+ ![Tests](https://github.com/lgloege/zenodopy/actions/workflows/tests.yaml/badge.svg)
19
+ [![codecov](https://codecov.io/gh/lgloege/zenodopy/branch/main/graph/badge.svg?token=FVCS71HPHC)](https://codecov.io/gh/lgloege/zenodopy)
20
+ [![pypi](https://badgen.net/pypi/v/zenodopy)](https://pypi.org/project/zenodopy)
21
+ [![License:MIT](https://img.shields.io/badge/License-MIT-lightgray.svg?style=flt-square)](https://opensource.org/licenses/MIT)
22
+ [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/lgloege/zenodopy/issues)
23
+
24
+ ### Project under active deveopment, not production ready
25
+
26
+ A Python 3.6+ package to manage [Zenodo](https://zenodo.org/) repositories.
27
+
28
+ ### Functions Implemented
29
+
30
+ - `.create_project()`: create a new project
31
+ - `.upload_file()`: upload file to project
32
+ - `.download_file()`: download a file from a project
33
+ - `.delete_file()`: permanently removes a file from a project
34
+ - `.get_urls_from_doi()`: returns the files urls for a given doi
35
+
36
+ Installing
37
+ ----------
38
+
39
+ ### PyPi
40
+
41
+ ```sh
42
+ pip install zenodopy==0.3.0
43
+ ```
44
+
45
+ ### GitHub
46
+
47
+ ```sh
48
+ pip install -e git+https://github.com/lgloege/zenodopy.git#egg=zenodopy
49
+ ```
50
+
51
+ Using the Package
52
+ -----------------
53
+
54
+ 1. **Create a Zenodo access token** by first logging into your account and clicking on your username in the top right corner. Navigate to "Applications" and then "+new token" under "Personal access tokens". Keep this window open while you proceed to step 2 because **the token is only displayed once**. Note that Sandbox.zenodo is used for testing and zenodo for production. If you want to use both, create for each a token as desribed above.
55
+ 2. **Store the token** in `~/.zenodo_token` using the following command.
56
+
57
+ ```sh
58
+ # zenodo token
59
+ { echo 'ACCESS_TOKEN: your_access_token_here' } > ~/.zenodo_token
60
+
61
+ # sandbox.zenodo token
62
+ { echo 'ACCESS_TOKEN-sandbox: your_access_token_here' } > ~/.zenodo_token
63
+ ```
64
+
65
+ 3. **start using the `zenodopy` package**
66
+
67
+ ```python
68
+ import zenodopy
69
+
70
+ # always start by creating a Client object
71
+ zeno = zenodopy.Client(sandbox=True)
72
+
73
+ # list project id's associated to zenodo account
74
+ zeno.list_projects
75
+
76
+ # create a project
77
+ zeno.create_project(title="test_project", upload_type="other")
78
+ # your zeno object now points to this newly created project
79
+
80
+ # create a file to upload
81
+ with open("~/test_file.txt", "w+") as f:
82
+ f.write("Hello from zenodopy")
83
+
84
+ # upload file to zenodo
85
+ zeno.upload_file("~/test.file.txt")
86
+
87
+ # list files of project
88
+ zeno.list_files
89
+
90
+ # set project to other project id's
91
+ zeno.set_project("<id>")
92
+
93
+ # delete project
94
+ zeno._delete_project(dep_id="<id>")
95
+ ```
96
+
97
+ Notes
98
+ -----
99
+
100
+ This project is under active development. Here is a list of things that needs improvement:
101
+
102
+ - **more tests**: need to test uploading and downloading files
103
+ - **documentation**: need to setup a readthedocs
104
+ - **download based on DOI**: right now you can only download from your own projects. Would be nice to download from
105
+ - **asyncronous functions**: use `asyncio` and `aiohttp` to write async functions. This will speed up downloading multiple files.
106
+
107
+
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ zennewpy/__init__.py
5
+ zennewpy/zennewpy_core.py
6
+ zennewpy.egg-info/PKG-INFO
7
+ zennewpy.egg-info/SOURCES.txt
8
+ zennewpy.egg-info/dependency_links.txt
9
+ zennewpy.egg-info/requires.txt
10
+ zennewpy.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests
2
+ tabulate
@@ -0,0 +1 @@
1
+ zennewpy