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('