diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 1d6abc2..90732a1 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -2,22 +2,44 @@ name: Run tests on: [push, pull_request] jobs: - build: + test: runs-on: ubuntu-latest strategy: - max-parallel: 4 + fail-fast: false matrix: - python-version: [2.7, 3.6, 3.7, 3.8] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --python ${{ matrix.python-version }} + + - name: Run tests + run: uv run --python ${{ matrix.python-version }} pytest + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --all-extras + + - name: Lint with ruff + run: uv run ruff check . || true + + - name: Check formatting with ruff + run: uv run ruff format --check . || true diff --git a/.gitignore b/.gitignore index 926c686..f13fbc3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,32 @@ develop-eggs parts src/pyoai.egg-info .tox + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ + +# uv (library - do not commit lockfile) +uv.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Testing +.pytest_cache/ +.coverage +htmlcov/ diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 9e7e8ec..0000000 --- a/.hgignore +++ /dev/null @@ -1,5 +0,0 @@ -src/pyoai.egg-info -bin -parts -.installed.cfg -develop-eggs diff --git a/.hgtags b/.hgtags deleted file mode 100644 index b2798a6..0000000 --- a/.hgtags +++ /dev/null @@ -1,21 +0,0 @@ -d5a3ef73faa2d52ee05571021fccabd3967312b6 pyoai-2_0b1 -6adf7a5390092088c5ed121965caa185fae4767e pyoai-2_0 -89abb3fc4659a08a4b232298d9848b0c7d7bd0ea eepi-2.1-prerelease -2531c56e02c0828b26b707d59540a56cb6afc3da pyoai-2.1.2 -191ae315d02db00c42822dc6a1b6f08f181bbe3a pyoai-2.1.3 -64b86a11ecf6107316baa836480e8a4a619eb44e pyoai-2.1.4 -3754c3f119fa72b5174c5358048c1fd644f983d0 pyoai-2.1.6 -0000000000000000000000000000000000000000 pyoai-2.1.6 -65d0f7bdee6a5b5ff386d153dfb4ebdd458a3fab pyoai-2.1.5 -fffb45120065457f6ac2d397b97c8c1069ff1697 pyoai-2.2.1 -c3ae70b661a8bec2273432f2d540fe963c7d32c0 pyoai-2.3 -63ad54d4a44a623786cc123f76b2cfa59edb1ebe pyoai-2.3.1 -9a9e75ac23adbe19bb015a29faf464c882057378 pyoai-2.4 -77c9da2756cc17de4ea226de7d04737daed0e7e8 pyoai-2.4.1 -e659e2a4e8d7a07cebf58b6838b7738a0f8a306b pyoai-2.4.2 -0000000000000000000000000000000000000000 pyoai-2.4.2 -712f939900749717ecabddbb39f2a716bf8838a4 pyoai-2.4.2 -0000000000000000000000000000000000000000 pyoai-2.4.2 -88386ea25a94fae2815f1f364394c389ecd98351 pyoai-2.4.2 -780e7c76d845999d8b2797ff2a43a1e17bb268e9 pyoai-2.4.3 -570b3c00bbfff2341bae2c69ec12a1529624ea91 2.4.4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7885cfb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: detect-private-key + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index d4d62dd..0000000 --- a/INSTALL.txt +++ /dev/null @@ -1,28 +0,0 @@ -Installation -============ - -python setup.py install - -will install the oaipmh module in your Python's site-packages. - -Python version -============== - -The module should work for Python versions 2.3 and up. - -Dependencies -============ - -The oaipmh module needs the lxml python bindings for -libxml2/libxslt. You can find lxml here: - -http://codespeak.net/lxml - -lxml needs libxml2 and libxslt (though not their Python bindings; -installing those is optional). libxml2 can can be found here: - -http://xmlsoft.org/ - -and libxslt can be found here: - -http://xmlsoft.org/XSLT diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 64c47a1..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -recursive-include src * -recursive-include doc * -include * diff --git a/README.rst b/README.rst index f7602d3..cb0f4e2 100644 --- a/README.rst +++ b/README.rst @@ -1,33 +1,68 @@ ====== -OAIPMH +pyoai ====== +.. image:: https://img.shields.io/pypi/v/pyoai.svg + :target: https://pypi.python.org/pypi/pyoai -.. image:: https://github.com/infrae/pyoai/workflows/Run%20tests/badge.svg - :target: https://github.com/infrae/pyoai/actions?query=workflow%3A%22Run+tests%22 - -The oaipmh module is a Python implementation of an "Open Archives -Initiative Protocol for Metadata Harvesting" (version 2) client and -server. The protocol is described here: +.. image:: https://github.com/mpasternak/pyoai/actions/workflows/run_tests.yml/badge.svg + :target: https://github.com/mpasternak/pyoai/actions/workflows/run_tests.yml -http://www.openarchives.org/OAI/openarchivesprotocol.html +.. image:: https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue + :target: https://www.python.org/ -Below is a simple implementation of an OAIPMH client: +The ``oaipmh`` module is a Python implementation of the +`Open Archives Initiative Protocol for Metadata Harvesting`_ (version 2) +client and server. ->>> from oaipmh.client import Client ->>> from oaipmh.metadata import MetadataRegistry, oai_dc_reader +.. _Open Archives Initiative Protocol for Metadata Harvesting: http://www.openarchives.org/OAI/openarchivesprotocol.html ->>> URL = 'http://uni.edu/ir/oaipmh' +Installation +============ ->>> registry = MetadataRegistry() ->>> registry.registerReader('oai_dc', oai_dc_reader) ->>> client = Client(URL, registry) +.. code-block:: bash ->>> for record in client.listRecords(metadataPrefix='oai_dc'): ->>> print record + pip install pyoai +Or with `uv`_:: -The pyoai package also contains a generic server implementation of the -OAIPMH protocol, this is used as the foundation of the `MOAI Server Platform`_ + uv add pyoai -.. _MOAI Server Platform: http://pypi.python.org/pypi/MOAI +.. _uv: https://docs.astral.sh/uv/ + +Requirements +============ + +* Python 3.10+ +* `lxml `_ + +Example +======= + +A simple OAI-PMH client: + +.. code-block:: python + + from oaipmh.client import Client + from oaipmh.metadata import MetadataRegistry, oai_dc_reader + + URL = 'http://uni.edu/ir/oaipmh' + + registry = MetadataRegistry() + registry.registerReader('oai_dc', oai_dc_reader) + client = Client(URL, registry) + + for record in client.listRecords(metadataPrefix='oai_dc'): + print(record) + +The pyoai package also contains a generic server implementation of the +OAI-PMH protocol. It is used as the foundation of the +`MOAI Server Platform `_. + +Development +=========== + +.. code-block:: bash + + uv sync --all-extras + uv run pytest diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 4803cfa..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[buildout] -develop = . -parts = devpython test -newest = false - -[devpython] -recipe = zc.recipe.egg -interpreter = devpython -eggs = pyoai - -[test] -recipe = zc.recipe.testrunner -eggs = pyoai -defaults = ['--tests-pattern', '^f?tests$', '-v'] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0e5eb4d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = ["setuptools>=75.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyoai" +version = "2.5.2" +description = "The oaipmh module is a Python implementation of an \"Open Archives Initiative Protocol for Metadata Harvesting\" (version 2) client and server." +readme = "README.rst" +requires-python = ">=3.10" +license = "BSD-3-Clause" +authors = [ + {name = "Infrae", email = "info@infrae.com"}, +] +keywords = ["OAI-PMH", "xml", "archive"] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Environment :: Web Environment", +] +dependencies = [ + "lxml", +] + +[project.urls] +Homepage = "http://www.infrae.com/download/oaipmh" +Repository = "https://github.com/infrae/pyoai" + +[project.optional-dependencies] +dev = [ + "pytest", + "pre-commit", + "ruff", + "bumpver", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W"] + +[tool.pytest.ini_options] +testpaths = ["src/oaipmh/tests"] +python_classes = ["*TestCase"] + +[tool.bumpver] +current_version = "2.5.2" +version_pattern = "MAJOR.MINOR.PATCH" +commit_message = "Bump version {old_version} -> {new_version}" +commit = true +tag = true +push = false + +[tool.bumpver.file_patterns] +"pyproject.toml" = [ + 'current_version = "{version}"', + 'version = "{version}"', +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 7ef6617..0000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -from setuptools import setup, find_packages -from os.path import join, dirname - -setup( - name='pyoai', - version='2.5.2pre', - author='Infrae', - author_email='info@infrae.com', - url='http://www.infrae.com/download/oaipmh', - classifiers=["Development Status :: 4 - Beta", - "Programming Language :: Python", - "License :: OSI Approved :: BSD License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Environment :: Web Environment"], - description="""The oaipmh module is a Python implementation of an "Open Archives Initiative Protocol for Metadata Harvesting" (version 2) client and server.""", - long_description=(open(join(dirname(__file__), 'README.rst')).read()+ - '\n\n'+ - open(join(dirname(__file__), 'HISTORY.txt')).read()), - long_description_content_type='text/x-rst', - packages=find_packages('src'), - package_dir = {'': 'src'}, - zip_safe=False, - license='BSD', - keywords='OAI-PMH xml archive', - install_requires=['lxml', 'six'], -) diff --git a/src/oaipmh/client.py b/src/oaipmh/client.py index fc8dba5..f39b28b 100644 --- a/src/oaipmh/client.py +++ b/src/oaipmh/client.py @@ -3,7 +3,6 @@ from __future__ import nested_scopes from __future__ import absolute_import -import six try: import urllib.request as urllib2 @@ -114,14 +113,12 @@ def parse(self, xml): # and we're basically hacking around non-wellformedness anyway, # but oh well if self._ignore_bad_character_hack: - xml = six.text_type(xml, 'UTF-8', 'replace') + xml = str(xml, 'UTF-8', 'replace') # also get rid of character code 12 xml = xml.replace(chr(12), '?') xml = xml.encode('UTF-8') - if six.PY3: - if hasattr(xml, "encode"): - xml = xml.encode("utf-8") - # xml = xml.encode("utf-8") + if hasattr(xml, "encode"): + xml = xml.encode("utf-8") return etree.XML(xml) # implementation of the various methods, delegated here by @@ -143,11 +140,11 @@ def GetMetadata_impl(self, args, tree): def Identify_impl(self, args, tree): namespaces = self.getNamespaces() evaluator = etree.XPathEvaluator(tree, namespaces=namespaces) - identify_node = evaluator.evaluate( + identify_node = evaluator( '/oai:OAI-PMH/oai:Identify')[0] identify_evaluator = etree.XPathEvaluator(identify_node, namespaces=namespaces) - e = identify_evaluator.evaluate + e = identify_evaluator repositoryName = e('string(oai:repositoryName/text())') baseURL = e('string(oai:baseURL/text())') @@ -180,12 +177,12 @@ def ListMetadataFormats_impl(self, args, tree): evaluator = etree.XPathEvaluator(tree, namespaces=namespaces) - metadataFormat_nodes = evaluator.evaluate( + metadataFormat_nodes = evaluator( '/oai:OAI-PMH/oai:ListMetadataFormats/oai:metadataFormat') metadataFormats = [] for metadataFormat_node in metadataFormat_nodes: e = etree.XPathEvaluator(metadataFormat_node, - namespaces=namespaces).evaluate + namespaces=namespaces) metadataPrefix = e('string(oai:metadataPrefix/text())') schema = e('string(oai:schema/text())') metadataNamespace = e('string(oai:metadataNamespace/text())') @@ -229,17 +226,17 @@ def buildRecords(self, # first find resumption token if available evaluator = etree.XPathEvaluator(tree, namespaces=namespaces) - token = evaluator.evaluate( + token = evaluator( 'string(/oai:OAI-PMH/*/oai:resumptionToken/text())') if token.strip() == '': token = None - record_nodes = evaluator.evaluate( + record_nodes = evaluator( '/oai:OAI-PMH/*/oai:record') result = [] for record_node in record_nodes: record_evaluator = etree.XPathEvaluator(record_node, namespaces=namespaces) - e = record_evaluator.evaluate + e = record_evaluator # find header node header_node = e('oai:header')[0] # create header @@ -261,12 +258,12 @@ def buildIdentifiers(self, namespaces, tree): evaluator = etree.XPathEvaluator(tree, namespaces=namespaces) # first find resumption token is available - token = evaluator.evaluate( + token = evaluator( 'string(/oai:OAI-PMH/*/oai:resumptionToken/text())') #'string(/oai:OAI-PMH/oai:ListIdentifiers/oai:resumptionToken/text())') if token.strip() == '': token = None - header_nodes = evaluator.evaluate( + header_nodes = evaluator( '/oai:OAI-PMH/oai:ListIdentifiers/oai:header') result = [] for header_node in header_nodes: @@ -278,20 +275,20 @@ def buildSets(self, namespaces, tree): evaluator = etree.XPathEvaluator(tree, namespaces=namespaces) # first find resumption token if available - token = evaluator.evaluate( + token = evaluator( 'string(/oai:OAI-PMH/oai:ListSets/oai:resumptionToken/text())') if token.strip() == '': token = None - set_nodes = evaluator.evaluate( + set_nodes = evaluator( '/oai:OAI-PMH/oai:ListSets/oai:set') sets = [] for set_node in set_nodes: e = etree.XPathEvaluator(set_node, - namespaces=namespaces).evaluate + namespaces=namespaces) # make sure we get back unicode strings instead # of lxml.etree._ElementUnicodeResult objects. - setSpec = six.text_type(e('string(oai:setSpec/text())')) - setName = six.text_type(e('string(oai:setName/text())')) + setSpec = str(e('string(oai:setSpec/text())')) + setName = str(e('string(oai:setName/text())')) # XXX setDescription nodes sets.append((setSpec, setName, None)) return sets, token @@ -368,7 +365,7 @@ def makeRequest(self, **kw): def buildHeader(header_node, namespaces): e = etree.XPathEvaluator(header_node, - namespaces=namespaces).evaluate + namespaces=namespaces) identifier = e('string(oai:identifier/text())') datestamp = datestamp_to_datetime( str(e('string(oai:datestamp/text())'))) diff --git a/src/oaipmh/common.py b/src/oaipmh/common.py index c602ada..bc0af16 100644 --- a/src/oaipmh/common.py +++ b/src/oaipmh/common.py @@ -1,4 +1,4 @@ -import pkg_resources +from importlib.metadata import PackageNotFoundError, version as _pkg_version from oaipmh import error @@ -61,11 +61,9 @@ def __init__(self, repositoryName, baseURL, protocolVersion, adminEmails, self._descriptions = [] if toolkit_description: - req = pkg_resources.Requirement.parse('pyoai') - egg = pkg_resources.working_set.find(req) - if egg: - version = '%s' % egg.version - else: + try: + version = '%s' % _pkg_version('pyoai') + except PackageNotFoundError: version = '' self.add_description( '')[-1].split('')[0] first_el = xml.split('>')[0] self.assertTrue(first_el.startswith('