diff --git a/codecov.yml b/.codecov.yml similarity index 100% rename from codecov.yml rename to .codecov.yml diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 0abf49a..0000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -branch = True -source = agentarchives diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..28052a5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,16 @@ +[flake8] +exclude = .tox, .git, __pycache__, .cache, build, dist, *.pyc, *.egg-info, .eggs +# Error codes: +# - https://flake8.pycqa.org/en/latest/user/error-codes.html +# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes +# - https://github.com/PyCQA/flake8-bugbear#list-of-warnings +# +# E203: whitespace before `,`, `;` or `:` +# E402: module level import not at top of file +# E501: line too long +# W503: line break before binary operator +ignore = + E203, + E402, + E501, + W503 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d2c8ed7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +--- +name: "Release" +on: "workflow_dispatch" +jobs: + build: + name: "Build" + runs-on: "ubuntu-22.04" + steps: + - name: "Check out repository" + uses: "actions/checkout@v4" + - name: "Set up Python" + uses: "actions/setup-python@v4" + with: + python-version: "3.9" + - name: "Build distribution packages" + run: make package-check + - name: "Save distribution directory" + uses: "actions/upload-artifact@v3" + with: + name: "distribution" + path: | + dist + upload: + name: "Upload" + needs: "build" + runs-on: "ubuntu-22.04" + environment: "release" + permissions: + id-token: "write" + steps: + - name: "Restore distribution directory" + uses: "actions/download-artifact@v3" + with: + name: "distribution" + path: | + dist + - name: "Upload distribution packages to PyPI" + uses: "pypa/gh-action-pypi-publish@release/v1" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06392fc..d449287 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,45 +6,37 @@ on: branches: - "master" jobs: - tox: + test: name: "Test Python ${{ matrix.python-version }}" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" strategy: fail-fast: false matrix: python-version: [ - "3.6", - "3.7", "3.8", "3.9", "3.10", "3.11", + "3.12", ] steps: - name: "Check out repository" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Set up Python ${{ matrix.python-version }}" uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" - - name: "Get pip cache dir" - id: "pip-cache" - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - name: "Cache pip packages" - uses: "actions/cache@v3" - with: - path: "${{ steps.pip-cache.outputs.dir }}" - key: "${{ runner.os }}-pip-${{ hashFiles('**/base.txt', '**/local.txt', '**/production.txt') }}" - restore-keys: | - ${{ runner.os }}-pip- + cache: "pip" + cache-dependency-path: | + requirements.txt + requirements-dev.txt - name: "Install tox" run: | python -m pip install --upgrade pip pip install tox tox-gh-actions - name: "Run tox" run: | - tox -- --cov-config .coveragerc --cov-report xml:coverage.xml + tox -- --cov agentarchives --cov-report xml:coverage.xml - name: "Upload coverage report" if: github.repository == 'artefactual-labs/agentarchives' uses: "codecov/codecov-action@v3" @@ -57,11 +49,15 @@ jobs: runs-on: "ubuntu-22.04" steps: - name: "Check out repository" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "Set up Python" uses: "actions/setup-python@v4" with: - python-version: "3.8" + python-version: "3.12" + cache: "pip" + cache-dependency-path: | + requirements.txt + requirements-dev.txt - name: "Install tox" run: | python -m pip install --upgrade pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12edf81..07bab0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,23 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.15.0 hooks: - id: pyupgrade - args: [--py3-plus, --py36-plus] + args: [--py38-plus] - repo: https://github.com/asottile/reorder_python_imports - rev: v3.10.0 + rev: v3.12.0 hooks: - id: reorder-python-imports - args: [--py3-plus, --py36-plus] + args: [--py38-plus] - repo: https://github.com/psf/black - rev: "23.7.0" + rev: "23.10.1" hooks: - id: black args: [--safe, --quiet] - language_version: python3 - repo: https://github.com/pycqa/flake8 rev: "6.1.0" hooks: - id: flake8 - language_version: python3 + additional_dependencies: + - flake8-bugbear==23.9.16 + - flake8-comprehensions==3.14.0 diff --git a/MANIFEST.in b/MANIFEST.in index cc0d116..c1a7121 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -include LICENSE \ No newline at end of file +include LICENSE +include README.md diff --git a/Makefile b/Makefile index 70c3836..494eef3 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,33 @@ -.PHONY: clean package package-deps package-source package-upload package-wheel +.DEFAULT_GOAL := help -package-deps: - pip install --upgrade twine wheel +.PHONY: clean package package-deps package-distribution package-upload pip-compile pip-upgrade -package-source: - python setup.py sdist +package-deps: ## Upgrade dependencies for packaging + python3 -m pip install --upgrade build twine -package-wheel: package-deps - python setup.py bdist_wheel +package-distribution: package-deps ## Create distribution packages + python3 -m build -package-upload: package-deps package-source package-wheel - twine upload dist/* --repository-url https://upload.pypi.org/legacy/ +package-check: package-distribution ## Check the distribution is valid + python3 -m twine check --strict dist/* + +package-upload: package-deps package-check ## Upload distribution packages + python3 -m twine upload dist/* --repository-url https://upload.pypi.org/legacy/ package: package-upload -clean: +clean: ## Clean the package directory rm -rf agentarchives.egg-info/ rm -rf build/ rm -rf dist/ + +pip-compile: ## Compile pip requirements + pip-compile --allow-unsafe --output-file=requirements.txt pyproject.toml + pip-compile --allow-unsafe --extra=dev --output-file=requirements-dev.txt pyproject.toml + +pip-upgrade: ## Upgrade pip requirements + pip-compile --allow-unsafe --upgrade --output-file=requirements.txt pyproject.toml + pip-compile --allow-unsafe --upgrade --extra=dev --output-file=requirements-dev.txt pyproject.toml + +help: ## Print this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/agentarchives/__init__.py b/agentarchives/__init__.py index f6c9f18..ca8a92c 100644 --- a/agentarchives/__init__.py +++ b/agentarchives/__init__.py @@ -1 +1,3 @@ DEFAULT_TIMEOUT = 10 + +__version__ = "0.9.0" diff --git a/agentarchives/archivesspace/client.py b/agentarchives/archivesspace/client.py index b425259..d830bf5 100644 --- a/agentarchives/archivesspace/client.py +++ b/agentarchives/archivesspace/client.py @@ -184,12 +184,16 @@ def _request(self, method, url, params, expected_response, data=None): return response - def _get(self, url, params={}, expected_response=200): + def _get(self, url, params=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.get, url, params=params, expected_response=expected_response ) - def _put(self, url, params={}, data=None, expected_response=200): + def _put(self, url, params=None, data=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.put, url, @@ -198,7 +202,9 @@ def _put(self, url, params={}, data=None, expected_response=200): expected_response=expected_response, ) - def _post(self, url, params={}, data=None, expected_response=200): + def _post(self, url, params=None, data=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.post, url, @@ -207,7 +213,9 @@ def _post(self, url, params={}, data=None, expected_response=200): expected_response=expected_response, ) - def _delete(self, url, params={}, expected_response=200): + def _delete(self, url, params=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.delete, url, params=params, expected_response=expected_response ) @@ -558,7 +566,7 @@ def get_resource_component_and_children( resource_id, resource_type="collection", level=1, - sort_data={}, + sort_data=None, recurse_max_level=False, sort_by=None, **kwargs, @@ -578,6 +586,8 @@ def get_resource_component_and_children( Consult ArchivistsToolkitClient.get_resource_component_and_children for the output format. :rtype dict: """ + if sort_data is None: + sort_data = {} resource_type = self.resource_type(resource_id) if resource_type == "resource": return self._get_resources( @@ -1027,7 +1037,7 @@ def add_child( start_date="", end_date="", date_expression="", - notes=[], + notes=None, ): """ Adds a new resource component parented within `parent`. @@ -1038,6 +1048,8 @@ def add_child( :return: The ID of the newly-created record. """ + if notes is None: + notes = [] parent_record = self.get_record(parent) record_type = self.resource_type(parent) repository = parent_record["repository"]["ref"] diff --git a/agentarchives/archivists_toolkit/client.py b/agentarchives/archivists_toolkit/client.py index c929e6b..cfa598a 100644 --- a/agentarchives/archivists_toolkit/client.py +++ b/agentarchives/archivists_toolkit/client.py @@ -158,7 +158,7 @@ def get_resource_component_children(self, resource_component_id): ) def get_resource_component_and_children( - self, resource_id, resource_type="collection", level=1, sort_data={}, **kwargs + self, resource_id, resource_type="collection", level=1, sort_data=None, **kwargs ): """ Fetch detailed metadata for the specified resource_id and all of its children. @@ -234,9 +234,10 @@ def get_resource_component_and_children( } :rtype list: """ + if sort_data is None: + sort_data = {} # we pass the sort position as a dict so it passes by reference and we # can use it to share state during recursion - recurse_max_level = kwargs.get("recurse_max_level", False) query = kwargs.get("search_pattern", "") diff --git a/agentarchives/atom/client.py b/agentarchives/atom/client.py index f6b37ae..4d52f7d 100644 --- a/agentarchives/atom/client.py +++ b/agentarchives/atom/client.py @@ -80,12 +80,16 @@ def _request(self, method, url, params, expected_response, data=None): return response - def _get(self, url, params={}, expected_response=200): + def _get(self, url, params=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.get, url, params=params, expected_response=expected_response ) - def _put(self, url, params={}, data=None, expected_response=200): + def _put(self, url, params=None, data=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.put, url, @@ -94,7 +98,9 @@ def _put(self, url, params={}, data=None, expected_response=200): expected_response=expected_response, ) - def _post(self, url, params={}, data=None, expected_response=200): + def _post(self, url, params=None, data=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.post, url, @@ -103,7 +109,9 @@ def _post(self, url, params={}, data=None, expected_response=200): expected_response=expected_response, ) - def _delete(self, url, params={}, expected_response=200): + def _delete(self, url, params=None, expected_response=200): + if params is None: + params = {} return self._request( self.session.delete, url, params=params, expected_response=expected_response ) @@ -369,7 +377,7 @@ def get_resource_component_and_children( resource_id, resource_type="collection", level=1, - sort_data={}, + sort_data=None, recurse_max_level=False, sort_by=None, **kwargs, @@ -387,6 +395,8 @@ def get_resource_component_and_children( Consult ArchivistsToolkitClient.get_resource_component_and_children for the output format. :rtype dict: """ + if sort_data is None: + sort_data = {} return self._get_resources( resource_id, recurse_max_level=recurse_max_level, sort_by=sort_by ) @@ -700,7 +710,7 @@ def add_child( start_date=None, end_date=None, date_expression=None, - notes=[], + notes=None, ): """ Adds a new resource component parented within `parent`. @@ -711,6 +721,8 @@ def add_child( :return: The ID of the newly-created record. """ + if notes is None: + notes = [] new_object = {"title": title, "level_of_description": level} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb95493 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,108 @@ +[build-system] +requires = [ + "setuptools>=68", + "wheel>=0.41", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = [""] +include = ["agentarchives*"] +namespaces = false + +[project] +name = "agentarchives" +dynamic = [ + "version", + "readme", +] +description = "Clients to retrieve, add, and modify records from archival management systems." +requires-python = ">=3.8" +license = {file = "LICENSE"} +dependencies = [ + "mysqlclient", + "requests", +] +keywords = [ + "accesstomemory", + "archivematica", + "preservation", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +authors = [ + {name = "Artefactual Systems Inc.", email = "info@artefactual.com"} +] +maintainers = [ + {name = "Artefactual Systems Inc.", email = "info@artefactual.com"} +] + +[project.urls] +homepage = "https://github.com/artefactual-labs/agentarchives/" +repository = "https://github.com/artefactual-labs/agentarchives/" +issues = "https://github.com/archivematica/Issues/issues" + +[project.optional-dependencies] +dev = [ + "coverage", + "pip-tools", + "pytest-cov", + "pytest-mock", + "pytest", + "vcrpy", +] + +[tool.setuptools.dynamic] +version = {attr = "agentarchives.__version__"} +readme = {file = ["README.md"], content-type = "text/markdown"} + +[tool.pytest.ini_options] +python_files = [ + "test_*.py", +] +testpaths = [ + "tests", +] + +[tool.coverage.run] +source = [ + "agentarchives", +] +branch = true +omit = [ + "tests/*", +] + +[tool.tox] +legacy_tox_ini = """ + [tox] + envlist = py{38,39,310,311,312}, linting + + [gh-actions] + python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + + [testenv] + deps = -r {toxinidir}/requirements-dev.txt + commands = pytest {posargs} + + [testenv:linting] + basepython = python3 + deps = pre-commit + commands = pre-commit run --all-files --show-diff-on-failure +""" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..a9cd4df --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,82 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe --extra=dev --output-file=requirements-dev.txt pyproject.toml +# +build==1.0.3 + # via pip-tools +certifi==2023.7.22 + # via requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via pip-tools +coverage[toml]==7.3.2 + # via + # agentarchives (pyproject.toml) + # pytest-cov +exceptiongroup==1.1.3 + # via pytest +idna==3.4 + # via + # requests + # yarl +importlib-metadata==6.8.0 + # via build +iniconfig==2.0.0 + # via pytest +multidict==6.0.4 + # via yarl +mysqlclient==2.2.0 + # via agentarchives (pyproject.toml) +packaging==23.2 + # via + # build + # pytest +pip-tools==7.3.0 + # via agentarchives (pyproject.toml) +pluggy==1.3.0 + # via pytest +pyproject-hooks==1.0.0 + # via build +pytest==7.4.3 + # via + # agentarchives (pyproject.toml) + # pytest-cov + # pytest-mock +pytest-cov==4.1.0 + # via agentarchives (pyproject.toml) +pytest-mock==3.12.0 + # via agentarchives (pyproject.toml) +pyyaml==6.0.1 + # via vcrpy +requests==2.31.0 + # via agentarchives (pyproject.toml) +tomli==2.0.1 + # via + # build + # coverage + # pip-tools + # pyproject-hooks + # pytest +urllib3==1.26.18 + # via + # requests + # vcrpy +vcrpy==5.1.0 + # via agentarchives (pyproject.toml) +wheel==0.41.3 + # via pip-tools +wrapt==1.15.0 + # via vcrpy +yarl==1.9.2 + # via vcrpy +zipp==3.17.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +pip==23.3.1 + # via pip-tools +setuptools==68.2.2 + # via pip-tools diff --git a/requirements.txt b/requirements.txt index ea77c2d..ae6a752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,18 @@ --r requirements/production.txt +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe --output-file=requirements.txt pyproject.toml +# +certifi==2023.7.22 + # via requests +charset-normalizer==3.3.2 + # via requests +idna==3.4 + # via requests +mysqlclient==2.2.0 + # via agentarchives (pyproject.toml) +requests==2.31.0 + # via agentarchives (pyproject.toml) +urllib3==2.0.7 + # via requests diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 694eee2..0000000 --- a/requirements/base.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests -mysqlclient diff --git a/requirements/local.txt b/requirements/local.txt deleted file mode 100644 index 2d76a0d..0000000 --- a/requirements/local.txt +++ /dev/null @@ -1,6 +0,0 @@ --r base.txt - -pytest -pytest-cov -pytest-mock -vcrpy diff --git a/requirements/production.txt b/requirements/production.txt deleted file mode 100644 index a3e81b8..0000000 --- a/requirements/production.txt +++ /dev/null @@ -1 +0,0 @@ --r base.txt diff --git a/setup.py b/setup.py deleted file mode 100644 index 1a9a35f..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -import codecs -from os import path - -from setuptools import setup - - -here = path.abspath(path.dirname(__file__)) - -with codecs.open(path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() - -setup( - name="agentarchives", - description="Clients to retrieve, add, and modify records from archival management systems", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/artefactual-labs/agentarchives", - author="Artefactual Systems", - author_email="info@artefactual.com", - license="AGPL 3", - version="0.8.0", - packages=[ - "agentarchives", - "agentarchives.archivesspace", - "agentarchives.archivists_toolkit", - "agentarchives.atom", - ], - install_requires=["requests", "mysqlclient"], - python_requires=">=3.6", - classifiers=[ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: GNU Affero General Public License v3", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ], -) diff --git a/tests/test_archivesspace_client.py b/tests/test_archivesspace_client.py index 04c74d3..9d5b3ef 100644 --- a/tests/test_archivesspace_client.py +++ b/tests/test_archivesspace_client.py @@ -2,6 +2,7 @@ import os import pytest +import requests import vcr from agentarchives.archivesspace.client import ArchivesSpaceClient @@ -50,7 +51,7 @@ def test_base_url_config(mocker, params, raises, base_url): kwargs = {"user": "foo", "passwd": "bar"} kwargs.update(params) if raises: - with pytest.raises(Exception): + with pytest.raises((AttributeError, requests.exceptions.InvalidURL)): ArchivesSpaceClient(**kwargs) return mocker.patch("agentarchives.archivesspace.ArchivesSpaceClient._login") diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 9e5879e..0000000 --- a/tox.ini +++ /dev/null @@ -1,25 +0,0 @@ -[tox] -envlist = py{36,37,38,39,310,311}, linting - -[gh-actions] -python = - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - -[testenv] -deps = -rrequirements/local.txt -commands = py.test --ignore=build -v --cov=agentarchives --cov-report=term-missing {posargs} - -[testenv:linting] -basepython = python3 -deps = pre-commit -commands = pre-commit run --all-files --show-diff-on-failure - -[flake8] -exclude = .env, .tox, .git, __pycache__, .cache, build, dist, *.pyc, *.egg-info, .eggs -application-import-names = flake8 -ignore = E501, W503