├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── tests.yaml │ └── version_update.yaml ├── .gitignore ├── .zenodo.json ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── src └── zenodopy │ ├── __init__.py │ ├── py.typed │ └── zenodopy.py ├── tests ├── __init__.py ├── test_version.py └── test_zenodopy.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest] 13 | python-version: [3.12] 14 | # ["3.6", "3.7", "3.8", "3.9"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install -r requirements_dev.txt 29 | pip install -e . 30 | 31 | - name: Run pytest 32 | env: 33 | ZENODO_TOKEN: ${{ secrets.ZENODO_TOKEN }} 34 | DEPOSITION_ID: ${{ secrets.DEPOSITION_ID }} 35 | run: pytest tests/test_zenodopy.py -v 36 | -------------------------------------------------------------------------------- /.github/workflows/version_update.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the main branch 5 | push: 6 | branches: [main] 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | branches: [main] 11 | # Allows you to run this workflow manually from the Actions tab 12 | # workflow_dispatch: 13 | 14 | jobs: 15 | 16 | UploadToZenodo: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: 3.12 27 | 28 | - name: install req 29 | run: pip install git+https://github.com/drifter089/zenodopy.git@basic_test#egg=zenodopy 30 | 31 | - name: Update Zenodo Deposition 32 | run: | 33 | python tests/test_version.py \ 34 | --version_tag "${{ github.ref_name }}" \ 35 | --zenodo_token "${{ secrets.ZENODO_TOKEN }}" \ 36 | --dep_id "${{ secrets.DEPOSITION_ID }}" \ 37 | --base_dir "${{ github.workspace }}" \ 38 | --metadata_file "${{ github.workspace }}/.zenodo.json" \ 39 | --upload_dir "${{ github.workspace }}/" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "title": "Zenodo CI test", 4 | "upload_type": "other", 5 | "description": "", 6 | "version": "0.1.0", 7 | "access_right": "open", 8 | "license": "Apache-2.0", 9 | "keywords": ["zenodo", "github", "git"], 10 | "publication_date":"", 11 | "creators": [ 12 | { 13 | "name": "Jhon, Doe", 14 | "orcid": "0000-0003-2584-3576" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Luke Gloege 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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("") 77 | 78 | # delete project 79 | zeno._delete_project(dep_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. 91 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | # addopts = "--cov=zenodopy" 7 | testpaths = [ 8 | "tests", 9 | ] 10 | 11 | [tool.mypy] 12 | mypy_path = "src" 13 | check_untyped_defs = true 14 | disallow_any_generics = true 15 | ignore_missing_imports = true 16 | no_implicit_optional = true 17 | show_error_codes = true 18 | strict_equality = true 19 | warn_redundant_casts = true 20 | warn_return_any = true 21 | warn_unreachable = true 22 | warn_unused_configs = true 23 | no_implicit_reexport = true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.26.0 2 | types-requests==2.27.7 3 | wget==3.2 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | tox==3.24.3 3 | pytest==6.2.5 4 | pytest-cov==2.12.1 5 | mypy==0.910 6 | requests==2.26.0 7 | types-requests==2.27.7 8 | wget==3.2 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = zenodopy 3 | description = manage zenodo project 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown 6 | version=0.3.0 7 | author = Luke Gloege 8 | author_email=ljg2157@columbia.edu 9 | url=https://github.com/lgloege/zenodopy 10 | keywords=zenodo 11 | license = MIT 12 | license_file = LICENSE 13 | platforms = unix, linux, osx, cygwin, win32 14 | classifiers = 15 | Development Status :: 3 - Alpha 16 | Intended Audience :: Developers 17 | Topic :: Software Development :: Build Tools 18 | License :: OSI Approved :: MIT License 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3 :: Only 21 | Programming Language :: Python :: 3.6 22 | Programming Language :: Python :: 3.7 23 | Programming Language :: Python :: 3.8 24 | Programming Language :: Python :: 3.9 25 | 26 | [options] 27 | packages = 28 | zenodopy 29 | install_requires = 30 | requests>=2 31 | types-requests>=2 32 | wget>=3 33 | 34 | python_requires = >=3.6 35 | package_dir = 36 | =src 37 | zip_safe = no 38 | 39 | [options.extras_require] 40 | testing = 41 | pytest>=6.0 42 | pytest-cov>=2.0 43 | mypy>=0.910 44 | flake8>=3.9 45 | tox>=3.24 46 | 47 | [options.package_data] 48 | zenodopy = py.typed 49 | 50 | [flake8] 51 | max-line-length = 160 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | -------------------------------------------------------------------------------- /src/zenodopy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set up module access for the base package 3 | """ 4 | from .zenodopy import Client 5 | from .zenodopy import ZenodoMetadata 6 | 7 | __all__ = ['Client','ZenodoMetadata'] 8 | -------------------------------------------------------------------------------- /src/zenodopy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgloege/zenodopy/faef6f40d2ea9e427db2d3c3ebabdb17150833c6/src/zenodopy/py.typed -------------------------------------------------------------------------------- /src/zenodopy/zenodopy.py: -------------------------------------------------------------------------------- 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 datetime import datetime 10 | import time 11 | from dataclasses import dataclass, field 12 | from typing import Optional, List 13 | 14 | def validate_url(url): 15 | """validates if URL is formatted correctly 16 | 17 | Returns: 18 | bool: True is URL is acceptable False if not acceptable 19 | """ 20 | regex = re.compile( 21 | r'^(?:http|ftp)s?://' # http:// or https:// 22 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 23 | r'localhost|' # localhost... 24 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 25 | r'(?::\d+)?' # optional port 26 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 27 | 28 | return re.match(regex, url) is not None 29 | 30 | 31 | def make_tarfile(output_file, source_dir): 32 | """tar a directory 33 | args 34 | ----- 35 | output_file: path to output file 36 | source_dir: path to source directory 37 | 38 | returns 39 | ----- 40 | tarred directory will be in output_file 41 | """ 42 | with tarfile.open(output_file, "w:gz") as tar: 43 | tar.add(source_dir, arcname=os.path.basename(source_dir)) 44 | 45 | 46 | def make_zipfile(path, ziph): 47 | # ziph is zipfile handle 48 | for root, dirs, files in os.walk(path): 49 | for file in files: 50 | ziph.write(os.path.join(root, file), 51 | os.path.relpath(os.path.join(root, file), 52 | os.path.join(path, '..'))) 53 | 54 | @dataclass 55 | class ZenodoMetadata: 56 | title: str 57 | upload_type: str = "other" 58 | description: Optional[str] = None 59 | publication_date: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d")) 60 | version: str = "0.1.0" 61 | access_right: str = "open" 62 | license: str = "Apache-2.0" 63 | keywords: List[str] = field(default_factory=lambda: ["zenodo", "github", "git"]) 64 | creators: List[dict] = field(default_factory=lambda: [{"name": "Jhon, Doe", "orcid": "0000-0003-2584-3576"}]) 65 | @classmethod 66 | def parse_metadata_from_json(cls, json_file_path: Path) -> 'ZenodoMetadata': 67 | """Parse metadata from a JSON file into a ZenodoMetadata object.""" 68 | json_file_path = Path(json_file_path).expanduser() 69 | if not json_file_path.exists(): 70 | raise ValueError( 71 | f"{json_file_path} does not exist. Please check you entered the correct path." 72 | ) 73 | 74 | with json_file_path.open("r") as json_file: 75 | data = json.load(json_file) 76 | 77 | metadata_dict = data.get("metadata", {}) 78 | return cls(**metadata_dict) 79 | 80 | 81 | class BearerAuth(requests.auth.AuthBase): 82 | """Bearer Authentication""" 83 | 84 | def __init__(self, token): 85 | self.token = token 86 | 87 | def __call__(self, r): 88 | r.headers["authorization"] = "Bearer " + self.token 89 | return r 90 | 91 | 92 | class Client(object): 93 | """Zenodo Client object 94 | 95 | Use this class to instantiate a zenodopy object 96 | to interact with your Zenodo account 97 | 98 | ``` 99 | import zenodopy 100 | zeno = zenodopy.Client() 101 | zeno.help() 102 | ``` 103 | 104 | Setup instructions: 105 | ``` 106 | zeno.setup_instructions 107 | ``` 108 | """ 109 | 110 | def __init__(self, title=None, bucket=None, deposition_id=None, sandbox=None, token=None): 111 | """initialization method""" 112 | if sandbox: 113 | self._endpoint = "https://sandbox.zenodo.org/api" 114 | else: 115 | self._endpoint = "https://zenodo.org/api" 116 | 117 | self.title = title 118 | self.bucket = bucket 119 | self.deposition_id = deposition_id 120 | self.sandbox = sandbox 121 | self._token = self._read_from_config if token is None else token 122 | self._bearer_auth = BearerAuth(self._token) 123 | # 'metadata/prereservation_doi/doi' 124 | 125 | def __repr__(self): 126 | return f"zenodoapi('{self.title}','{self.bucket}','{self.deposition_id}')" 127 | 128 | def __str__(self): 129 | return f"{self.title} --- {self.deposition_id}" 130 | 131 | # --------------------------------------------- 132 | # hidden functions 133 | # --------------------------------------------- 134 | 135 | @staticmethod 136 | def _get_upload_types(): 137 | """Acceptable upload types 138 | 139 | Returns: 140 | list: contains acceptable upload_types 141 | """ 142 | return [ 143 | "Publication", 144 | "Poster", 145 | "Presentation", 146 | "Dataset", 147 | "Image", 148 | "Video/Audio", 149 | "Software", 150 | "Lesson", 151 | "Physical object", 152 | "Other" 153 | ] 154 | 155 | @staticmethod 156 | def _read_config(path=None): 157 | """reads the configuration file 158 | 159 | Configuration file should be ~/.zenodo_token 160 | 161 | Args: 162 | path (str): location of the file with ACCESS_TOKEN 163 | 164 | Returns: 165 | dict: dictionary with API ACCESS_TOKEN 166 | """ 167 | 168 | if path is None: 169 | print("You need to supply a path") 170 | 171 | full_path = os.path.expanduser(path) 172 | if not Path(full_path).exists(): 173 | print(f"{path} does not exist. Please check you entered the correct path") 174 | 175 | config = {} 176 | with open(path) as file: 177 | for line in file.readlines(): 178 | if ":" in line: 179 | key, value = line.strip().split(":", 1) 180 | if key in ("ACCESS_TOKEN", "ACCESS_TOKEN-sandbox"): 181 | config[key] = value.strip() 182 | return config 183 | 184 | @property 185 | def _read_from_config(self): 186 | """reads the web3.storage token from configuration file 187 | configuration file is ~/.web3_storage_token 188 | Returns: 189 | str: ACCESS_TOKEN to connect to web3 storage 190 | """ 191 | if self.sandbox: 192 | dotrc = os.environ.get("ACCESS_TOKEN-sandbox", os.path.expanduser("~/.zenodo_token")) 193 | else: 194 | dotrc = os.environ.get("ACCESS_TOKEN", os.path.expanduser("~/.zenodo_token")) 195 | 196 | if os.path.exists(dotrc): 197 | config = self._read_config(dotrc) 198 | key = config.get("ACCESS_TOKEN-sandbox") if self.sandbox else config.get("ACCESS_TOKEN") 199 | return key 200 | else: 201 | print(' ** No token was found, check your ~/.zenodo_token file ** ') 202 | 203 | def _get_depositions(self): 204 | """gets the current project deposition 205 | 206 | this provides details on the project, including metadata 207 | 208 | Returns: 209 | dict: dictionary containing project details 210 | """ 211 | # get request, returns our response 212 | r = requests.get(f"{self._endpoint}/deposit/depositions", 213 | auth=self._bearer_auth) 214 | if r.ok: 215 | return r.json() 216 | else: 217 | return r.raise_for_status() 218 | 219 | def _get_depositions_by_id(self): 220 | """gets the deposition based on project id 221 | 222 | this provides details on the project, including metadata 223 | 224 | Args: 225 | dep_id (str): project deposition ID 226 | 227 | Returns: 228 | dict: dictionary containing project details 229 | """ 230 | # get request, returns our response 231 | if self.deposition_id is not None: 232 | r = requests.get(f"{self._endpoint}/deposit/depositions/{self.deposition_id}", 233 | auth=self._bearer_auth) 234 | else: 235 | print(' ** no deposition id is set on the project ** ') 236 | return None 237 | if r.ok: 238 | return r.json() 239 | else: 240 | return r.raise_for_status() 241 | 242 | def _get_depositions_files(self): 243 | """gets the file deposition 244 | 245 | ** not used, can safely be removed ** 246 | 247 | Returns: 248 | dict: dictionary containing project details 249 | """ 250 | # get request, returns our response 251 | if self.deposition_id is not None: 252 | r = requests.get(f"{self._endpoint}/deposit/depositions/{self.deposition_id}/files", 253 | auth=self._bearer_auth) 254 | else: 255 | print(' ** no deposition id is set on the project ** ') 256 | 257 | if r.ok: 258 | return r.json() 259 | else: 260 | return r.raise_for_status() 261 | 262 | def _get_bucket_by_title(self, title=None): 263 | """gets the bucket URL by project title 264 | 265 | This URL is what you upload files to 266 | 267 | Args: 268 | title (str): project title 269 | 270 | Returns: 271 | str: the bucket URL to upload files to 272 | """ 273 | dic = self.list_projects 274 | dep_id = dic[title] if dic is not None else None 275 | 276 | # get request, returns our response, this the records metadata 277 | r = requests.get(f"{self._endpoint}/deposit/depositions/{dep_id}", 278 | auth=self._bearer_auth) 279 | 280 | if r.ok: 281 | return r.json()['links']['bucket'] 282 | else: 283 | return r.raise_for_status() 284 | 285 | def _get_bucket_by_id(self, dep_id=None): 286 | """gets the bucket URL by project deposition ID 287 | 288 | This URL is what you upload files to 289 | 290 | Args: 291 | dep_id (str): project deposition ID 292 | 293 | Returns: 294 | str: the bucket URL to upload files to 295 | """ 296 | # get request, returns our response 297 | if dep_id is not None: 298 | r = requests.get(f"{self._endpoint}/deposit/depositions/{dep_id}", 299 | auth=self._bearer_auth) 300 | else: 301 | r = requests.get(f"{self._endpoint}/deposit/depositions/{self.deposition_id}", 302 | auth=self._bearer_auth) 303 | 304 | if r.ok: 305 | return r.json()['links']['bucket'] 306 | else: 307 | return r.raise_for_status() 308 | 309 | def _get_api(self): 310 | # get request, returns our response 311 | r = requests.get(f"{self._endpoint}", auth=self._bearer_auth) 312 | 313 | if r.ok: 314 | return r.json() 315 | else: 316 | return r.raise_for_status() 317 | 318 | # --------------------------------------------- 319 | # user facing functions/properties 320 | # --------------------------------------------- 321 | @property 322 | def setup_instructions(self): 323 | """instructions to setup zenodoPy 324 | """ 325 | print( 326 | ''' 327 | # ============================================== 328 | # Follow these steps to setup zenodopy 329 | # ============================================== 330 | 1. Create a Zenodo account: https://zenodo.org/ 331 | 332 | 2. Create a personal access token 333 | 2.1 Log into your Zenodo account: https://zenodo.org/ 334 | 2.2 Click on the drop down in the top right and navigate to "application" 335 | 2.3 Click "new token" in "personal access token" 336 | 2.4 Copy the token into ~/.zenodo_token using the following terminal command 337 | 338 | { echo 'ACCESS_TOKEN: YOUR_KEY_GOES_HERE' } > ~/.zenodo_token 339 | 340 | 2.5 Make sure this file was creates (tail ~/.zenodo_token) 341 | 342 | 3. Now test you can access the token from Python 343 | 344 | import zenodopy 345 | zeno = zenodopy.Client() 346 | zeno._token # this should display your ACCESS_TOKEN 347 | ''' 348 | ) 349 | 350 | @property 351 | def list_projects(self): 352 | """list projects connected to the supplied ACCESS_KEY 353 | 354 | prints to the screen the "Project Name" and "ID" 355 | """ 356 | tmp = self._get_depositions() 357 | 358 | if isinstance(tmp, list): 359 | print('Project Name ---- ID ---- Status ---- Latest Published ID') 360 | print('---------------------------------------------------------') 361 | for file in tmp: 362 | status = {} # just to rename the file outputs and deal with exceptions 363 | if file['submitted']: 364 | status['submitted'] = 'published' 365 | else: 366 | status['submitted'] = 'unpublished' 367 | 368 | status["latest"] = self._get_latest_record(file['id']) 369 | 370 | print(f"{file['title']} ---- {file['id']} ---- {status['submitted']} ---- {status['latest']}") 371 | else: 372 | print(' ** need to setup ~/.zenodo_token file ** ') 373 | 374 | @property 375 | def list_files(self): 376 | """list files in current project 377 | 378 | prints filenames to screen 379 | """ 380 | dep_id = self.deposition_id 381 | dep = self._get_depositions_by_id() 382 | if dep is not None: 383 | print('Files') 384 | print('------------------------') 385 | for file in dep['files']: 386 | print(file['filename']) 387 | else: 388 | print(" ** the object is not pointing to a project. Use either .set_project() or .create_project() before listing files ** ") 389 | # except UserWarning: 390 | # warnings.warn("The object is not pointing to a project. Either create a project or explicity set the project'", UserWarning) 391 | 392 | def create_project( 393 | self, metadata:ZenodoMetadata, 394 | ): 395 | """Creates a new project 396 | 397 | After a project is creates the zenodopy object 398 | willy point to the project 399 | 400 | title is required. If upload_type or description 401 | are not specified, then default values will be used 402 | 403 | Args: 404 | title (str): new title of project 405 | metadata_json (str): path to json file with metadata 406 | """ 407 | 408 | # get request, returns our response 409 | r = requests.post( 410 | f"{self._endpoint}/deposit/depositions", 411 | auth=self._bearer_auth, 412 | data=json.dumps({}), 413 | headers={"Content-Type": "application/json"}, 414 | ) 415 | 416 | if r.ok: 417 | 418 | self.deposition_id = r.json()["id"] 419 | self.bucket = r.json()["links"]["bucket"] 420 | 421 | self.change_metadata( 422 | metadata=metadata, 423 | ) 424 | 425 | else: 426 | print("** Project not created, something went wrong. Check that your ACCESS_TOKEN is in ~/.zenodo_token ") 427 | 428 | def set_project(self, dep_id=None): 429 | '''set the project by id''' 430 | projects = self._get_depositions() 431 | 432 | if projects is not None: 433 | project_list = [ 434 | d 435 | for d in projects 436 | if self._check_parent_doi(dep_id=dep_id, project_obj=d) 437 | ] 438 | if len(project_list) > 0: 439 | self.title = project_list[0]["title"] 440 | self.bucket = self._get_bucket_by_id(project_list[0]["id"]) 441 | self.deposition_id = project_list[0]["id"] 442 | 443 | else: 444 | print(f' ** Deposition ID: {dep_id} does not exist in your projects ** ') 445 | 446 | def _check_parent_doi(self, dep_id, project_obj): 447 | if project_obj["id"] == int(dep_id): 448 | return True 449 | concept_doi = project_obj.get("conceptdoi", None) 450 | if concept_doi != None: 451 | return int(dep_id) == int(concept_doi.split(".")[-1]) 452 | return False 453 | 454 | def change_metadata(self, metadata: ZenodoMetadata): 455 | """ 456 | Change project's metadata. 457 | 458 | Args: 459 | metadata (ZenodoMetadata): The metadata to update. 460 | 461 | Returns: 462 | dict: Dictionary with the updated metadata if the request is successful. 463 | Raises an error if the request fails. 464 | 465 | This function updates the project's metadata on Zenodo. 466 | It sets the `publication_date` to the current date and prepares the metadata for the API request. 467 | The metadata is sent as a JSON payload to the Zenodo API endpoint using a PUT request. 468 | If the request is successful, it returns the updated metadata as a dictionary. 469 | If the request fails, it raises an error with the status of the failed request. 470 | """ 471 | metadata.publication_date = datetime.now().strftime("%Y-%m-%d") 472 | 473 | data = { 474 | "metadata": metadata.__dict__ 475 | } 476 | 477 | r = requests.put( 478 | f"{self._endpoint}/deposit/depositions/{self.deposition_id}", 479 | auth=self._bearer_auth, 480 | data=json.dumps(data), 481 | headers={"Content-Type": "application/json"}, 482 | ) 483 | 484 | if r.ok: 485 | return r.json() 486 | else: 487 | return r.raise_for_status() 488 | 489 | def upload_file(self, file_path=None, publish=False): 490 | """upload a file to a project 491 | 492 | Args: 493 | file_path (str): name of the file to upload 494 | publish (bool): whether implemente publish action or not 495 | """ 496 | if file_path is None: 497 | print("You need to supply a path") 498 | 499 | if not Path(os.path.expanduser(file_path)).exists(): 500 | print(f"{file_path} does not exist. Please check you entered the correct path") 501 | 502 | if self.bucket is None: 503 | print("You need to create a project with zeno.create_project() " 504 | "or set a project zeno.set_project() before uploading a file") 505 | else: 506 | bucket_link = self.bucket 507 | 508 | with open(file_path, "rb") as fp: 509 | # text after last '/' is the filename 510 | filename = file_path.split('/')[-1] 511 | r = requests.put(f"{bucket_link}/{filename}", 512 | auth=self._bearer_auth, 513 | data=fp,) 514 | 515 | print(f"{file_path} successfully uploaded!") if r.ok else print("Oh no! something went wrong") 516 | 517 | if publish: 518 | return self.publish() 519 | 520 | def upload_zip(self, source_dir=None, output_file=None, publish=False): 521 | """upload a directory to a project as zip 522 | 523 | This will: 524 | 1. zip the directory, 525 | 2. upload the zip directory to your project 526 | 3. remove the zip file from your local machine 527 | 528 | Args: 529 | source_dir (str): path to directory to tar 530 | output_file (str): name of output file (optional) 531 | defaults to using the source_dir name as output_file 532 | publish (bool): whether implemente publish action or not, argument for `upload_file` 533 | """ 534 | # make sure source directory exists 535 | source_dir = os.path.expanduser(source_dir) 536 | source_obj = Path(source_dir) 537 | if not source_obj.exists(): 538 | raise FileNotFoundError(f"{source_dir} does not exist") 539 | 540 | # acceptable extensions for outputfile 541 | acceptable_extensions = ['.zip'] 542 | 543 | # use name of source_dir for output_file if none is included 544 | if not output_file: 545 | output_file = f"{source_obj.stem}.zip" 546 | output_obj = Path(output_file) 547 | else: 548 | output_file = os.path.expanduser(output_file) 549 | output_obj = Path(output_file) 550 | extension = ''.join(output_obj.suffixes) # gets extension like .tar.gz 551 | # make sure extension is acceptable 552 | if extension not in acceptable_extensions: 553 | raise Exception(f"Extension must be in {acceptable_extensions}") 554 | # add an extension if not included 555 | if not extension: 556 | output_file = os.path.expanduser(output_file + '.zip') 557 | output_obj = Path(output_file) 558 | 559 | # check to make sure outputfile doesn't already exist 560 | if output_obj.exists(): 561 | raise Exception(f"{output_obj} already exists. Please chance the name") 562 | 563 | # create tar directory if does not exist 564 | if output_obj.parent.exists(): 565 | with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zipf: 566 | make_zipfile(source_dir, zipf) 567 | else: 568 | os.makedirs(output_obj.parent) 569 | with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zipf: 570 | make_zipfile(source_dir, zipf) 571 | 572 | # upload the file 573 | self.upload_file(file_path=output_file, publish=publish) 574 | 575 | # remove tar file after uploading it 576 | os.remove(output_file) 577 | 578 | def upload_tar(self, source_dir=None, output_file=None, publish=False): 579 | """upload a directory to a project 580 | 581 | This will: 582 | 1. tar the directory, 583 | 2. upload the tarred directory to your project 584 | 3. remove the tar file from your local machine 585 | 586 | Args: 587 | source_dir (str): path to directory to tar 588 | output_file (str): name of output file (optional) 589 | defaults to using the source_dir name as output_file 590 | publish (bool): whether implemente publish action or not, argument for `upload_file` 591 | """ 592 | # output_file = './tmp/tarTest.tar.gz' 593 | # source_dir = '/Users/gloege/test' 594 | 595 | # make sure source directory exists 596 | source_dir = os.path.expanduser(source_dir) 597 | source_obj = Path(source_dir) 598 | if not source_obj.exists(): 599 | raise FileNotFoundError(f"{source_dir} does not exist") 600 | 601 | # acceptable extensions for outputfile 602 | acceptable_extensions = ['.tar.gz'] 603 | 604 | # use name of source_dir for output_file if none is included 605 | if not output_file: 606 | output_file = f"{source_obj.stem}.tar.gz" 607 | output_obj = Path(output_file) 608 | else: 609 | output_file = os.path.expanduser(output_file) 610 | output_obj = Path(output_file) 611 | extension = ''.join(output_obj.suffixes) # gets extension like .tar.gz 612 | # make sure extension is acceptable 613 | if extension not in acceptable_extensions: 614 | raise Exception(f"Extension must be in {acceptable_extensions}") 615 | # add an extension if not included 616 | if not extension: 617 | output_file = os.path.expanduser(output_file + '.tar.gz') 618 | output_obj = Path(output_file) 619 | 620 | # check to make sure outputfile doesn't already exist 621 | if output_obj.exists(): 622 | raise Exception(f"{output_obj} already exists. Please chance the name") 623 | 624 | # create tar directory if does not exist 625 | if output_obj.parent.exists(): 626 | make_tarfile(output_file=output_file, source_dir=source_dir) 627 | else: 628 | os.makedirs(output_obj.parent) 629 | make_tarfile(output_file=output_file, source_dir=source_dir) 630 | 631 | # upload the file 632 | self.upload_file(file_path=output_file, publish=publish) 633 | 634 | # remove tar file after uploading it 635 | os.remove(output_file) 636 | 637 | def update(self, metadata:ZenodoMetadata, source=None, output_file=None, publish=False): 638 | """update an existed record 639 | 640 | Args: 641 | source (str): path to directory or file to upload 642 | output_file (str): name of output file (optional) 643 | defaults to using the source_dir name as output_file 644 | publish (bool): whether implemente publish action or not, argument for `upload_file` 645 | """ 646 | # create a draft deposition 647 | url_action = self._get_depositions_by_id()['links']['newversion'] 648 | r = requests.post(url_action, auth=self._bearer_auth) 649 | r.raise_for_status() 650 | 651 | # parse current project to the draft deposition 652 | new_dep_id = r.json()['links']['latest_draft'].split('/')[-1] 653 | 654 | # adding this to let new id propogate in the backend 655 | time.sleep(2) 656 | 657 | self.set_project(new_dep_id) 658 | 659 | time.sleep(5) 660 | 661 | self.change_metadata(metadata=metadata) 662 | # invoke upload funcions 663 | if not source: 664 | print("You need to supply a path") 665 | 666 | if Path(source).exists(): 667 | if Path(source).is_file(): 668 | self.upload_file(source, publish=publish) 669 | elif Path(source).is_dir(): 670 | if not output_file: 671 | self.upload_zip(source, publish=publish) 672 | elif '.zip' in ''.join(Path(output_file).suffixes).lower(): 673 | self.upload_zip(source, output_file, publish=publish) 674 | elif '.tar.gz' in ''.join(Path(output_file).suffixes).lower(): 675 | self.upload_tar(source, output_file, publish=publish) 676 | else: 677 | raise FileNotFoundError(f"{source} does not exist") 678 | 679 | def publish(self): 680 | """ publish a record 681 | """ 682 | url_action = self._get_depositions_by_id()['links']['publish'] 683 | r = requests.post(url_action, auth=self._bearer_auth) 684 | r.raise_for_status() 685 | return r 686 | 687 | def download_file(self, filename=None, dst_path=None): 688 | """download a file from project 689 | 690 | Args: 691 | filename (str): name of the file to download 692 | dst_path (str): destination path to download the data (default is current directory) 693 | """ 694 | if filename is None: 695 | print(" ** filename not supplied ** ") 696 | 697 | bucket_link = self.bucket 698 | 699 | if bucket_link is not None: 700 | if validate_url(bucket_link): 701 | r = requests.get(f"{bucket_link}/{filename}", 702 | auth=self._bearer_auth) 703 | 704 | # if dst_path is not set, set download to current directory 705 | # else download to set dst_path 706 | if dst_path: 707 | if os.path.isdir(dst_path): 708 | filename = dst_path + '/' + filename 709 | else: 710 | raise FileNotFoundError(f'{dst_path} does not exist') 711 | 712 | if r.ok: 713 | with open(filename, 'wb') as f: 714 | f.write(r.content) 715 | else: 716 | print(f" ** Something went wrong, check that {filename} is in your poject ** ") 717 | 718 | else: 719 | print(f' ** {bucket_link}/{filename} is not a valid URL ** ') 720 | 721 | def _is_doi(self, string=None): 722 | """test if string is of the form of a zenodo doi 723 | 10.5281.zenodo.[0-9]+ 724 | 725 | Args: 726 | string (strl): string to test. Defaults to None. 727 | 728 | Returns: 729 | bool: true is string is doi-like 730 | """ 731 | import re 732 | pattern = re.compile("10.5281/zenodo.[0-9]+") 733 | return pattern.match(string) 734 | 735 | def _get_record_id_from_doi(self, doi=None): 736 | """return the record id for given doi 737 | 738 | Args: 739 | doi (string, optional): the zenodo doi. Defaults to None. 740 | 741 | Returns: 742 | str: the record id from the doi (just the last numbers) 743 | """ 744 | return doi.split('.')[-1] 745 | 746 | def get_urls_from_doi(self, doi=None): 747 | """the files urls for the given doi 748 | 749 | Args: 750 | doi (str): the doi you want the urls from. Defaults to None. 751 | 752 | Returns: 753 | list: a list of the files urls for the given doi 754 | """ 755 | if self._is_doi(doi): 756 | record_id = self._get_record_id_from_doi(doi) 757 | else: 758 | print(f"{doi} must be of the form: 10.5281/zenodo.[0-9]+") 759 | 760 | # get request (do not need to provide access token since public 761 | r = requests.get(f"https://zenodo.org/api/records/{record_id}") # params={'access_token': ACCESS_TOKEN}) 762 | return [f['links']['self'] for f in r.json()['files']] 763 | 764 | def _get_latest_record(self, record_id=None): 765 | """return the latest record id for given record id 766 | 767 | Args: 768 | record_id (str or int): the record id you known. Defaults to None. 769 | 770 | Returns: 771 | str: the latest record id or 'None' if not found 772 | """ 773 | try: 774 | record = self._get_depositions_by_id()['links']['latest'].split('/')[-1] 775 | except: 776 | record = 'None' 777 | return record 778 | 779 | def delete_file(self, filename=None): 780 | """delete a file from a project 781 | 782 | Args: 783 | filename (str): the name of file to delete 784 | """ 785 | bucket_link = self.bucket 786 | 787 | # with open(file_path, "rb") as fp: 788 | _ = requests.delete(f"{bucket_link}/{filename}", 789 | auth=self._bearer_auth) 790 | 791 | def _delete_project(self, dep_id=None): 792 | """delete a project from repository by ID 793 | 794 | Args: 795 | dep_id (str): The project deposition ID 796 | """ 797 | print('') 798 | # if input("are you sure you want to delete this project? (y/n)") == "y": 799 | # delete requests, we are deleting the resource at the specified URL 800 | r = requests.delete( 801 | f"{self._endpoint}/deposit/depositions/{self.deposition_id}", 802 | auth=self._bearer_auth, 803 | ) 804 | # response status 805 | print(r.status_code) 806 | 807 | # reset class variables to None 808 | self.title = None 809 | self.bucket = None 810 | self.deposition_id = None 811 | # else: 812 | # print(f'Project title {self.title} is still available.') 813 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgloege/zenodopy/faef6f40d2ea9e427db2d3c3ebabdb17150833c6/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import json 4 | from pathlib import Path 5 | import argparse 6 | import zenodopy 7 | 8 | def main(): 9 | # Set up argument parsing 10 | parser = argparse.ArgumentParser(description='Update Zenodo deposition with new version and files.') 11 | parser.add_argument('--version_tag', required=True, help='The version tag for the new release.') 12 | parser.add_argument('--zenodo_token', required=True, help='The Zenodo API token.') 13 | parser.add_argument('--dep_id', required=True, type=int, help='The Zenodo deposition ID.') 14 | parser.add_argument('--base_dir', required=True, help='The base directory path.') 15 | parser.add_argument('--metadata_file', required=True, help='The metadata JSON file path.') 16 | parser.add_argument('--upload_dir', required=True, help='The directory containing files to upload.') 17 | 18 | args = parser.parse_args() 19 | 20 | version_tag = args.version_tag 21 | zenodo_token = args.zenodo_token 22 | dep_id = args.dep_id 23 | base_dir = Path(args.base_dir) 24 | zenodo_metadata_file = Path(args.metadata_file) 25 | upload_dir = Path(args.upload_dir) 26 | 27 | print("Version Tag:", version_tag) 28 | 29 | # Parse and update metadata with new version tag 30 | metadata = zenodopy.ZenodoMetadata.parse_metadata_from_json(zenodo_metadata_file) 31 | metadata.version = version_tag 32 | 33 | max_retries = 5 34 | 35 | for attempt in range(1, max_retries + 1): 36 | try: 37 | zeno = zenodopy.Client( 38 | sandbox=True, 39 | token=zenodo_token, 40 | ) 41 | 42 | zeno.set_project(dep_id=dep_id) 43 | 44 | zeno.update( 45 | source=str(upload_dir), 46 | publish=True, 47 | metadata=metadata, 48 | ) 49 | print("Update succeeded.") 50 | break 51 | 52 | except Exception as e: 53 | print(f"Attempt {attempt} failed with error: {e}") 54 | 55 | time.sleep(2) # Optional: Wait before retrying 56 | 57 | zeno._delete_project(dep_id=dep_id) 58 | 59 | if attempt == max_retries: 60 | print("Max retries reached. Exiting.") 61 | raise 62 | else: 63 | time.sleep(2) 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /tests/test_zenodopy.py: -------------------------------------------------------------------------------- 1 | import zenodopy as zen 2 | """ 3 | This module contains tests for the zenodopy library using pytest. 4 | Functions: 5 | test_client: Tests the initialization of the zen.Client object with and without a token. 6 | test_read_config: Tests the _read_config method of the zen.Client object to ensure it raises a TypeError. 7 | test_get_baseurl: Tests the _endpoint attribute of the zen.Client object for both sandbox and production environments. 8 | test_get_depositions: Tests the _get_depositions, _get_depositions_by_id, and _get_depositions_files methods of the zen.Client object. 9 | test_get_bucket: Tests the _get_bucket_by_id method of the zen.Client object. 10 | test_get_projects_and_files: Tests the list_projects and list_files properties of the zen.Client object. 11 | Note: 12 | The update and change_metadata functions have been updated to add new versions to existing depositions. 13 | This functionality is being tested in test_version. We will bring back individual tests once these changes 14 | have been merged upstream to keep the changes incremental. 15 | """ 16 | import pytest 17 | 18 | # use this when using pytest 19 | import os 20 | ACCESS_TOKEN = os.getenv('ZENODO_TOKEN') 21 | DEPOSITION_ID = os.getenv('DEPOSITION_ID') 22 | 23 | # can also hardcode sandbox token using tox locally 24 | # ACCESS_TOKEN = '' 25 | 26 | 27 | def test_client(): 28 | _ = zen.Client() 29 | _ = zen.Client(token=ACCESS_TOKEN, sandbox=True) 30 | # zeno.list_projects 31 | 32 | 33 | def test_read_config(): 34 | zeno = zen.Client() 35 | with pytest.raises(TypeError): 36 | zeno._read_config() 37 | 38 | 39 | def test_get_baseurl(): 40 | zeno = zen.Client(sandbox=True) 41 | assert zeno._endpoint == 'https://sandbox.zenodo.org/api' 42 | 43 | zeno = zen.Client() 44 | assert zeno._endpoint == 'https://zenodo.org/api' 45 | 46 | 47 | # def test_get_key(): 48 | # zeno = zen.Client(token=ACCESS_TOKEN, sandbox=True) 49 | # zeno._get_key() 50 | # zeno.title 51 | # zeno.bucket 52 | # zeno.deposition_id 53 | # zeno.sandbox 54 | 55 | # zobj = zen.Client() 56 | # if zobj._get_key() is None: 57 | # pass 58 | 59 | 60 | # def test_get_headers(): 61 | # zeno = zen.Client(token=ACCESS_TOKEN, sandbox=True) 62 | # zeno._get_headers() 63 | 64 | # zeno = zen.Client() 65 | # if zeno._get_headers() is None: 66 | # pass 67 | 68 | 69 | def test_get_depositions(): 70 | zeno = zen.Client(sandbox=True,token=ACCESS_TOKEN) 71 | dep_id=DEPOSITION_ID 72 | zeno.set_project(dep_id=dep_id) 73 | depositions = zeno._get_depositions() 74 | deposition_by_id = zeno._get_depositions_by_id() 75 | deposition_files = zeno._get_depositions_files() 76 | if len(depositions) > 0: 77 | pass 78 | elif int(deposition_by_id['id']) == dep_id or int(deposition_by_id['conceptrecid']) == dep_id: 79 | pass 80 | elif len(deposition_files) >0: 81 | pass 82 | else: 83 | raise ValueError('Depositions not found') 84 | 85 | def test_get_bucket(): 86 | zeno = zen.Client(sandbox=True,token=ACCESS_TOKEN) 87 | dep_id=DEPOSITION_ID 88 | zeno.set_project(dep_id=dep_id) 89 | bucket_link = zeno._get_bucket_by_id() 90 | assert bucket_link.startswith('https://sandbox.zenodo.org/api/files/') 91 | # if zeno._get_bucket_by_title(title='fake title') is None: 92 | # pass 93 | 94 | 95 | def test_get_projects_and_files(): 96 | zeno = zen.Client(sandbox=True,token=ACCESS_TOKEN) 97 | dep_id=DEPOSITION_ID 98 | zeno.set_project(dep_id=dep_id) 99 | _ = zeno.list_projects 100 | _ = zeno.list_files 101 | 102 | 103 | # @pytest.mark.filterwarnings('ignore::UserWarning') 104 | # def test_create_project(): 105 | # zeno = zen.Client(sandbox=True) 106 | # zeno.create_project(title='test', upload_type='other') 107 | # zeno.create_project(title='test') 108 | 109 | 110 | # def test_set_project(): 111 | # zeno = zen.Client() 112 | # zeno.set_project(dep_id='123') 113 | 114 | 115 | # # don't know how to mock inputs 116 | # def test_delete_project(): 117 | # pass 118 | 119 | 120 | # def test_change_metadata(): 121 | # zeno = zen.Client(sandbox=True) 122 | # zeno.change_metadata(dep_id='fake_ID', title='fake_title') 123 | 124 | 125 | # def test_upload_file(): 126 | # zeno = zen.Client(sandbox=True) 127 | # zeno.upload_file(file_path='path') 128 | 129 | 130 | # def test_download_file(): 131 | # zeno = zen.Client(sandbox=True) 132 | # zeno.download_file(filename='test') 133 | # zeno.bucket = 'invalid_url' 134 | # zeno.download_file(filename='test') 135 | 136 | 137 | # def test_tutorial(): 138 | # zeno = zen.Client(ACCESS_TOKEN=ACCESS_TOKEN, sandbox=True) 139 | # zeno.list_projects 140 | # zeno.list_files 141 | # zeno.title 142 | # zeno.bucket 143 | # zeno.deposition_id 144 | # zeno.sandbox 145 | 146 | # params = {'title': 'test_set', 'upload_type': 'other'} 147 | # zeno.create_project(**params) 148 | 149 | # # params = {'title': 'test', 'upload_type': 'other'} 150 | # # zeno.create_project(**params) 151 | # zeno.list_projects 152 | 153 | # # with open("/home/test_file.txt", "w+") as f: 154 | # # f.write("test") 155 | 156 | # # zeno.upload_file("/home/test_file.txt") 157 | # zeno.list_files 158 | # zeno.list_projects 159 | # _ = zeno.change_metadata(zeno.deposition_id, title='test_new') 160 | # zeno.list_projects 161 | # # zeno.download_file('test_file.txt') 162 | # # zeno.delete_file('test_file.txt') 163 | # zeno.list_files 164 | # # zeno._delete_project(zeno.deposition_id) 165 | # zeno.list_projects 166 | # zeno._delete_project(zeno.deposition_id) 167 | # zeno.list_projects 168 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.8.0 3 | envlist = py36, py37, py38, py39, flake8, mypy 4 | isolated_build = true 5 | skip_missing_interpreters = 6 | true 7 | 8 | [gh-actions] 9 | python = 10 | 3.6: py36 11 | 3.7: py37 12 | 3.8: py38 13 | 3.9: py39, mypy, flake8 14 | 15 | [testenv] 16 | setenv = 17 | PYTHONPATH = {toxinidir} 18 | deps = 19 | -r{toxinidir}/requirements_dev.txt 20 | codecov 21 | commands = 22 | pip install -e . 23 | pytest --basetemp={envtmpdir} 24 | codecov 25 | 26 | [testenv:flake8] 27 | basepython = python3.9 28 | deps = flake8 29 | commands = flake8 src tests 30 | 31 | [testenv:mypy] 32 | basepython = python3.9 33 | deps = 34 | -r{toxinidir}/requirements_dev.txt 35 | commands = mypy src 36 | 37 | --------------------------------------------------------------------------------