diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..0056d36
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,56 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ main, develop ]
+ schedule:
+ - cron: '34 21 * * 1'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'python' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 0000000..620005d
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,45 @@
+# This workflow will upload a Python Package when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload Python Package
+
+on:
+ # normal behavior: run when a new release is created
+ release:
+ types: [published]
+ # allow running manually on main
+ workflow_dispatch:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ pypi-publish:
+ name: Upload release to PyPI
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/project/viapy/
+ permissions:
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build
+ - name: Build package
+ run: python -m build
+ - name: Publish package
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
new file mode 100644
index 0000000..3d9ab20
--- /dev/null
+++ b/.github/workflows/unit_tests.yml
@@ -0,0 +1,55 @@
+name: unit tests
+
+on:
+ push: # run on every push or PR to any branch
+ pull_request:
+ schedule: # run automatically on main branch each Tuesday at 11am
+ - cron: "0 16 * * 2"
+
+jobs:
+ python-unit:
+ name: Python unit tests
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python: ["3.9", "3.10", "3.11", "3.12"]
+ django: [0, "3.2", "4.0", "4.1", "4.2", "5.0"]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+
+ # Base python cache on the hash of pyproject.toml with requirements
+ # if the file changes, the cache is invalidated.
+ - name: Cache pip
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: pip-${{ hashFiles('pyproject.toml') }}
+ restore-keys: |
+ pip-${{ hashFiles('pyproject.toml') }}
+ pip-
+
+ - name: Install package with dependencies
+ run: |
+ if [ "${{ matrix.django }}" -gt "0"]; then pip install -q Django==${{ matrix.django }} '.[django_test]'; fi
+ pip install .
+ pip install '.[test]'
+ pip install codecov
+
+ - name: Setup test settings
+ run: |
+ cp ci/testsettings.py testsettings.py
+ python -c "import uuid; print('SECRET_KEY = \'%s\'' % uuid.uuid4())" >> testsettings.py
+
+ - name: Run pytest
+ env:
+ SOLR_VERSION: "${{ matrix.solr }}"
+ run: py.test --cov=viapy --cov-report=xml
+
+ - name: Upload test coverage to Codecov
+ uses: codecov/codecov-action@v3
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1a1494d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-language: python
-python:
-- 3.5
-- 3.6
-env:
-- DJANGO=""
-- DJANGO=1.11
-- DJANGO=2.0
-- DJANGO=2.1
-- DJANGO=2.2
-- DJANGO=3.0
-jobs:
- exclude:
- - python: 3.5
- env: DJANGO=3.0
-install:
-- pip install -e .
-- pip install -e '.[test]'
-- if [ ! -z $DJANGO ]; then pip install -q Django==$DJANGO; fi
-- if [ $DJANGO != '' ]; then pip install django-autocomplete-light pytest-django;
- fi
-- pip install codecov
-- cp ci/testsettings.py testsettings.py
-- python -c "import uuid; print('SECRET_KEY = \'%s\'' % uuid.uuid4())" >> testsettings.py
-script:
-- py.test --cov=viapy
-after_success:
-- codecov
-notifications:
- slack:
- secure: fGFC9zMgsSTUlFFtS4mmUyFq/eZLABF2Q9VmfIWOdZjHBLu9pXrX7x4trrmG6+ZMDf0tLIyyO6Wa8W+zz6xkhpW+bhdMVnDJomLmLfc/ZbavdkY1LGM+Dj6CDMtPIU26z1y8PmxhpWd/uO0JT6QtYqkfmMy6OaK7BU4NCXse3HYD434UBvce30x2w2Q9JQHDSWvSMqP17vdJMLmpmQl4Nl5gjduR8sqe/itxwlvShxwBJjjQjZSAcCUNqjSSQNjdaLM7hdu6byQhVCbuEi7IQMXvSSPTkyQpIvTAoXJQ/SoYOxmI6fA5vHNO8sSO3yTJPpZCm+KzjE8wJEun8lPkd7vExrw7iwVXPtCeL6PE/9k8ax1lTn68Sc+FdGlDgiHqZ/2b5/btuuhsjY3DibPDmbJy7C42cO0YvvCkUaY2NlbWn59p92NBcttyVUJ8fSE/iJZmIOqHsfREVzB4liOwpQYiNe9LB4WxmbCFssxMvIqR5+86n0bdV4dDu+u+9IibwQhfOVliGaoDnt8sEc/ESrBwvPcev55nJcuag1gnjQAk3azCKbYrMUe3+c6DquiwQpQmY/uIaQhuILeEyEZSlcCjB9wrs7hbdnH5ZVNsmGe7ZrgR+0ySwdZrQl0FxYgCXMIQyTSkMJnlZy7U544HBlYFXvX2UcVIMLCH268yorM=
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3944472..f202872 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,14 @@
CHANGELOG
=========
+0.3
+---
+
+* Handle negative years when parsing birth and death dates
+* Now tested on python 3.9 through 3.12
+* Now tested against Django 3.2 through 5.0
+* Migrate continous integration to GitHub Actions
+
0.2
---
diff --git a/README.rst b/README.rst
index 1b89ff2..23c2342 100644
--- a/README.rst
+++ b/README.rst
@@ -13,8 +13,8 @@ Authority File) data and APIs.
**viapy** provides optional Django integration; this currently includes a
django-autocomplete-light lookup view and a VIAF url widget.
-.. image:: https://travis-ci.org/Princeton-CDH/viapy.svg
- :target: https://travis-ci.org/Princeton-CDH/viapy
+.. image:: https://github.com/Princeton-CDH/viapy/actions/workflows/unit_tests.yml/badge.svg
+ :target: https://github.com/Princeton-CDH/viapy/actions/workflows/unit_tests.yml
:alt: Build status
.. image:: https://codecov.io/gh/Princeton-CDH/viapy/branch/master/graph/badge.svg
@@ -65,7 +65,7 @@ Include the viapy urls at the desired base url with the namespace::
urlpatterns = [
...
- url(r'^viaf/', include('viapy.urls', namespace='viaf')),
+ path(r'viaf/', include('viapy.urls', namespace='viaf')),
...
]
@@ -79,15 +79,14 @@ This git repository uses `git flow`_ branching conventions.
Initial setup and installation:
-- Recommended: create and activate a python 3.5 virtualenv::
+- Recommended: create and activate a python 3.11 virtualenv::
- virtualenv viapy -p python3.5
+ python3 -m venv viapy
source viapy/bin/activate
-- pip install the package with its python dependencies::
+- pip install the package with all development and test dependencies::
- pip install -e .
- pip install -e ".[django]""
+ pip install -e ".[dev]""
Unit Testing
@@ -115,7 +114,7 @@ Documentation
Documentation is generated using `sphinx `_.
To generate documentation, first install development requirements::
- pip install -e ".[docs]"
+ pip install -e ".[dev]"
Then build the documentation using the customized make file in the `docs`
directory::
@@ -133,7 +132,7 @@ License
**viapy** is distributed under the Apache 2.0 License.
-©2017 Trustees of Princeton University. Permission granted via
+©2024 Trustees of Princeton University. Permission granted via
Princeton Docket #18-3449-1 for distribution online under a standard Open Source
license. Ownership rights transferred to Rebecca Koeser provided software
is distributed online via open source.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..02c4397
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,59 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "viapy"
+description = "Python module for interacting with VIAF data & APIs"
+authors = [
+ {name = "Center for Digital Humanities at Princeton", email = "cdh@princeton.edu"},
+]
+requires-python = ">=3.9"
+readme = "README.rst"
+license = {text = "Apache-2"}
+classifiers = [
+ "Environment :: Web Environment",
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.0",
+ "Framework :: Django :: 4.1",
+ "Framework :: Django :: 4.2",
+ "Framework :: Django :: 5.0",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+dynamic = ["version"]
+dependencies = [
+ "requests", "rdflib", "cached_property", "attrdict3"
+]
+
+[project.urls]
+#Documentation = "https://readthedocs.org"
+Repository = "https://github.com/Princeton-CDH/viapy"
+Changelog = "https://github.com/Princeton-CDH/viapy/blob/main/CHANGELOG.rst"
+
+[tool.hatch.version]
+path = "viapy/__init__.py"
+
+[project.optional-dependencies]
+test = [
+ "pytest",
+ "pytest-cov"
+]
+django = ["django>=3.2", "django-autocomplete-light"]
+django_test = ["pytest-django", "viapy[django]"]
+docs = ["sphinx"]
+test_all = ["viapy[test]", "viapy[django_test]"]
+dev = ["pre-commit", "viapy[django]", "viapy[test_all]", "viapy[docs]"]
+
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "testsettings"
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 3b8b73f..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-DJANGO_SETTINGS_MODULE=testsettings
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 9b2bc50..0000000
--- a/setup.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import os
-from setuptools import find_packages, setup
-
-from viapy import __version__
-
-with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
- README = readme.read()
-
-# allow setup.py to be run from any path
-os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
-
-REQUIREMENTS = ['requests', 'rdflib', 'cached_property', 'attrdict']
-TEST_REQUIREMENTS = ['pytest', 'pytest-cov']
-DJANGO_REQS = ['django>=1.11,<3.1', 'django-autocomplete-light']
-DJANGO_TEST_REQS = ['pytest-django']
-
-
-setup(
- name='viapy',
- version=__version__,
- packages=find_packages(),
- include_package_data=True,
- license='Apache License, Version 2.0',
- description='Python module for interacting with VIAF data & APIs',
- long_description=README,
- url='https://github.com/Princeton-CDH/viapy',
- install_requires=REQUIREMENTS,
- setup_requires=['pytest-runner'],
- tests_require=TEST_REQUIREMENTS,
- extras_require={
- 'django': DJANGO_REQS,
- 'test': TEST_REQUIREMENTS,
- 'test_all': TEST_REQUIREMENTS + DJANGO_REQS + DJANGO_TEST_REQS,
- 'docs': ['sphinx']
- },
- author='CDH @ Princeton',
- author_email='digitalhumanities@princeton.edu',
- classifiers=[
- 'Environment :: Web Environment',
- 'Framework :: Django',
- 'Framework :: Django :: 1.11',
- 'Framework :: Django :: 2.0',
- 'Framework :: Django :: 2.1',
- 'Framework :: Django :: 2.2',
- 'Framework :: Django :: 3.0',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: Apache Software License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Topic :: Internet :: WWW/HTTP',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- ],
-)
diff --git a/sphinx-docs/conf.py b/sphinx-docs/conf.py
index de4b33b..05de731 100644
--- a/sphinx-docs/conf.py
+++ b/sphinx-docs/conf.py
@@ -25,9 +25,9 @@
import sys
import django
-sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath("."))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'docsettings'
+os.environ["DJANGO_SETTINGS_MODULE"] = "docsettings"
django.setup()
from viapy import __version__
@@ -43,29 +43,29 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.intersphinx',
- 'sphinx.ext.coverage',
- 'sphinx.ext.viewcode',
- 'sphinx.ext.githubpages'
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.githubpages",
]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = 'viapy'
-copyright = '2017, CDH @ Princeton'
-author = 'CDH @ Princeton'
+project = "viapy"
+copyright = "2017, CDH @ Princeton"
+author = "CDH @ Princeton"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -81,15 +81,15 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@@ -100,7 +100,7 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'alabaster'
+html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -108,18 +108,19 @@
#
# html_theme_options = {}
html_theme_options = {
- 'description': 'VIAF via Python',
- 'github_user': 'Princeton-CDH',
- 'github_repo': 'viapy',
- 'travis_button': True,
- 'codecov_button': True,
+ "description": "VIAF via Python",
+ "github_user": "Princeton-CDH",
+ "github_repo": "viapy",
+ "travis_button": True,
+ "codecov_button": True,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+# currently unused
+# html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
@@ -127,12 +128,12 @@
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
- '**': [
- 'about.html',
- 'navigation.html',
+ "**": [
+ "about.html",
+ "navigation.html",
# 'relations.html', # needs 'show_related': True theme option to display
- 'searchbox.html',
- 'sidebar_footer.html',
+ "searchbox.html",
+ "sidebar_footer.html",
]
}
@@ -140,7 +141,7 @@
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
-htmlhelp_basename = 'viapydoc'
+htmlhelp_basename = "viapydoc"
# -- Options for LaTeX output ---------------------------------------------
@@ -149,15 +150,12 @@
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
-
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
-
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
-
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@@ -167,8 +165,7 @@
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'viapy.tex', 'viapy Documentation',
- 'CDH @ Princeton', 'manual'),
+ (master_doc, "viapy.tex", "viapy Documentation", "CDH @ Princeton", "manual"),
]
@@ -176,10 +173,7 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'viapy', 'viapy Documentation',
- [author], 1)
-]
+man_pages = [(master_doc, "viapy", "viapy Documentation", [author], 1)]
# -- Options for Texinfo output -------------------------------------------
@@ -188,11 +182,23 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'viapy', 'viapy Documentation',
- author, 'viapy', 'One line description of project.',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "viapy",
+ "viapy Documentation",
+ author,
+ "viapy",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
]
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None}
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", None),
+ "django": (
+ "http://docs.djangoproject.com/en/dev/",
+ "http://docs.djangoproject.com/en/dev/_objects/",
+ ),
+}
diff --git a/viapy/__init__.py b/viapy/__init__.py
index f255f68..493f741 100644
--- a/viapy/__init__.py
+++ b/viapy/__init__.py
@@ -1,7 +1 @@
-__version_info__ = (0, 2, 0, None)
-
-
-# Dot-connect all but the last. Last is dash-connected if not None.
-__version__ = '.'.join([str(i) for i in __version_info__[:-1]])
-if __version_info__[-1] is not None:
- __version__ += ('-%s' % (__version_info__[-1],))
+__version__ = "0.3.0"
diff --git a/viapy/api.py b/viapy/api.py
index a871d28..0499084 100644
--- a/viapy/api.py
+++ b/viapy/api.py
@@ -11,7 +11,7 @@
logger = logging.getLogger(__name__)
-SCHEMA_NS = Namespace('http://schema.org/')
+SCHEMA_NS = Namespace("http://schema.org/")
class ViafAPI(object):
@@ -33,20 +33,26 @@ def uri_from_id(cls, viaf_id):
return "%s/%s" % (cls.uri_base, viaf_id)
def suggest(self, term):
- '''Query autosuggest API. Returns a list of results, or an
- empty list if no suggestions are found or if something went wrong'''
+ """Query autosuggest API. Returns a list of results, or an
+ empty list if no suggestions are found or if something went wrong"""
# 'viaf/AutoSuggest?query=[searchTerms]&callback[optionalCallbackName]
- autosuggest_url = '%s/AutoSuggest' % self.api_base
- response = requests.get(autosuggest_url,
- params={'query': term},
- headers={'accept': 'application/json'})
- logger.debug('autosuggest \'%s\': %s %s, %0.2f',
- term, response.status_code, response.reason,
- response.elapsed.total_seconds())
+ autosuggest_url = "%s/AutoSuggest" % self.api_base
+ response = requests.get(
+ autosuggest_url,
+ params={"query": term},
+ headers={"accept": "application/json"},
+ )
+ logger.debug(
+ "autosuggest '%s': %s %s, %0.2f",
+ term,
+ response.status_code,
+ response.reason,
+ response.elapsed.total_seconds(),
+ )
if response.status_code == requests.codes.ok:
- return response.json().get('result', None) or []
+ return response.json().get("result", None) or []
# if there was an http error, raise it
response.raise_for_status()
@@ -54,23 +60,27 @@ def suggest(self, term):
return []
def search(self, query):
- '''Query VIAF seach interface. Returns a list of :class:`SRUItem`
+ """Query VIAF seach interface. Returns a list of :class:`SRUItem`
:param query: CQL query in viaf syntax (e.g., ``cql.any all "term"``)
- '''
- search_url = '%s/search' % self.api_base
+ """
+ search_url = "%s/search" % self.api_base
params = {
- 'query': query,
- 'httpAccept': 'application/json',
- 'maximumRecords': 50, # TODO: configurable ?
+ "query": query,
+ "httpAccept": "application/json",
+ "maximumRecords": 50, # TODO: configurable ?
# sort by number of holdings (default sort on web search)
# - so better known names show up first
- 'sortKeys': 'holdingscount'
+ "sortKeys": "holdingscount",
}
response = requests.get(search_url, params=params)
- logger.debug('search \'%s\': %s %s, %0.2f',
- params['query'], response.status_code, response.reason,
- response.elapsed.total_seconds())
+ logger.debug(
+ "search '%s': %s %s, %0.2f",
+ params["query"],
+ response.status_code,
+ response.reason,
+ response.elapsed.total_seconds(),
+ )
if response.status_code == requests.codes.ok:
data = SRUResult(response.json())
if data.total_results:
@@ -83,23 +93,24 @@ def _find_type(self, fltr, value):
return self.search('%s all "%s"' % (fltr, value))
def find_person(self, name):
- '''Search VIAF for local.personalNames'''
- return self._find_type('local.personalNames', name)
+ """Search VIAF for local.personalNames"""
+ return self._find_type("local.personalNames", name)
def find_corporate(self, name):
- '''Search VIAF for local.corporateNames'''
- return self._find_type('local.corporateNames', name)
+ """Search VIAF for local.corporateNames"""
+ return self._find_type("local.corporateNames", name)
def find_place(self, name):
- '''Search VIAF for local.geographicNames'''
- return self._find_type('local.geographicNames', name)
+ """Search VIAF for local.geographicNames"""
+ return self._find_type("local.geographicNames", name)
class ViafEntity(object):
- '''Object for working with a single VIAF entity.
+ """Object for working with a single VIAF entity.
:param viaf_id: viaf identifier (either integer or uri)
- '''
+ """
+
def __init__(self, viaf_id):
try:
int(viaf_id)
@@ -111,94 +122,98 @@ def __init__(self, viaf_id):
@property
def uriref(self):
- '''VIAF URI reference as instance of :class:`rdflib.URIRef`'''
+ """VIAF URI reference as instance of :class:`rdflib.URIRef`"""
return rdflib.URIRef(self.uri)
@cached_property
def rdf(self):
- '''VIAF data for this entity as :class:`rdflib.Graph`'''
+ """VIAF data for this entity as :class:`rdflib.Graph`"""
start = time.time()
graph = rdflib.Graph()
graph.parse(self.uri)
- logger.debug('Loaded VIAF RDF %s: %0.2f sec',
- self.uri, time.time() - start)
+ logger.debug("Loaded VIAF RDF %s: %0.2f sec", self.uri, time.time() - start)
return graph
# person-specific properties
@property
def birthdate(self):
- '''schema birthdate as :class:`rdflib.Literal`'''
+ """schema birthdate as :class:`rdflib.Literal`"""
return self.rdf.value(self.uriref, SCHEMA_NS.birthDate)
@property
def deathdate(self):
- '''schema deathdate as :class:`rdflib.Literal`'''
+ """schema deathdate as :class:`rdflib.Literal`"""
return self.rdf.value(self.uriref, SCHEMA_NS.deathDate)
@property
def birthyear(self):
- '''birth year'''
+ """birth year"""
if self.birthdate:
return self.year_from_isodate(str(self.birthdate))
@property
def deathyear(self):
- '''death year'''
+ """death year"""
if self.deathdate:
return self.year_from_isodate(str(self.deathdate))
# utility method for date parsing
@classmethod
def year_from_isodate(cls, date):
- '''Return just the year portion of an ISO8601 date. Expects
- a string, returns an integer'''
- return int(date.split('-')[0])
+ """Return just the year portion of an ISO8601 date. Expects
+ a string, returns an integer. Supports negative dates."""
+ negative = False
+ # if the date starts with a dash, strip off before trying to split
+ if date.startswith("-"):
+ date = date[1:]
+ negative = True
+ value = int(date.split("-")[0])
+ if negative:
+ return -value
+ return value
class SRUResult(object):
- '''SRU search result object, for use with :meth:`ViafAPI.search`.'''
+ """SRU search result object, for use with :meth:`ViafAPI.search`."""
def __init__(self, data):
- self._data = data.get('searchRetrieveResponse', {})
+ self._data = data.get("searchRetrieveResponse", {})
@cached_property
def total_results(self):
- '''number of records matching the query'''
- return int(self._data.get('numberOfRecords', 0))
+ """number of records matching the query"""
+ return int(self._data.get("numberOfRecords", 0))
@cached_property
def records(self):
- '''list of results as :class:`SRUItem`.'''
- return [SRUItem(d['record']) for d in self._data.get('records', [])]
+ """list of results as :class:`SRUItem`."""
+ return [SRUItem(d["record"]) for d in self._data.get("records", [])]
class SRUItem(AttrMap):
- '''Single item returned by a SRU search, for use with
- :meth:`ViafAPI.search` and :class:`SRUResult`.'''
+ """Single item returned by a SRU search, for use with
+ :meth:`ViafAPI.search` and :class:`SRUResult`."""
@property
def uri(self):
- '''VIAF URI for this result'''
- return self.recordData.Document['@about']
+ """VIAF URI for this result"""
+ return self.recordData.Document["@about"]
@property
def viaf_id(self):
- '''VIAF numeric identifier'''
+ """VIAF numeric identifier"""
return self.recordData.viafID
-
@property
def nametype(self):
- '''type of name (personal, corporate, title, etc)'''
+ """type of name (personal, corporate, title, etc)"""
return self.recordData.nameType
@property
def label(self):
- '''first main heading for this item'''
+ """first main heading for this item"""
try:
return self.recordData.mainHeadings.data[0].text
except KeyError:
return self.recordData.mainHeadings.data.text
-
-
diff --git a/viapy/test_urls.py b/viapy/test_urls.py
index af0149c..d5a3511 100644
--- a/viapy/test_urls.py
+++ b/viapy/test_urls.py
@@ -1,12 +1,12 @@
"""Test URL configuration for viapy
"""
try:
- from django.conf.urls import url, include
+ from django.urls import path, include
from viapy import urls as viapy_urls
urlpatterns = [
- url(r'^viaf/', include(viapy_urls, namespace='viaf')),
+ path(r"viaf/", include(viapy_urls, namespace="viaf")),
]
except ImportError:
diff --git a/viapy/tests/test_api.py b/viapy/tests/test_api.py
index feaaef1..a5dbc47 100644
--- a/viapy/tests/test_api.py
+++ b/viapy/tests/test_api.py
@@ -7,106 +7,109 @@
from viapy.api import ViafAPI, ViafEntity, SRUResult, SRUItem
-FIXTURES_PATH = os.path.join(os.path.dirname(__file__), 'fixtures')
+FIXTURES_PATH = os.path.join(os.path.dirname(__file__), "fixtures")
class TestViafAPI(object):
-
def test_get_uri(self):
- assert ViafAPI.uri_from_id('1234') == \
- 'http://viaf.org/viaf/1234'
+ assert ViafAPI.uri_from_id("1234") == "http://viaf.org/viaf/1234"
# numeric id should also work
- assert ViafAPI.uri_from_id(1234) == \
- 'http://viaf.org/viaf/1234'
+ assert ViafAPI.uri_from_id(1234) == "http://viaf.org/viaf/1234"
- @patch('viapy.api.requests')
+ @patch("viapy.api.requests")
def test_suggest(self, mockrequests):
viaf = ViafAPI()
mockrequests.codes = requests.codes
# Check that query with no matches still returns an empty list
- mock_result = {'query': 'notanauthor', 'result': None}
+ mock_result = {"query": "notanauthor", "result": None}
mockrequests.get.return_value.status_code = requests.codes.ok
mockrequests.get.return_value.json.return_value = mock_result
- assert viaf.suggest('notanauthor') == []
+ assert viaf.suggest("notanauthor") == []
mockrequests.get.assert_called_with(
- 'https://www.viaf.org/viaf/AutoSuggest',
- headers={'accept': 'application/json'},
- params={'query': 'notanauthor'})
+ "https://www.viaf.org/viaf/AutoSuggest",
+ headers={"accept": "application/json"},
+ params={"query": "notanauthor"},
+ )
# valid (abbreviated) response
- mock_result['result'] = [{
- "term": "Austen, Jane, 1775-1817",
- "displayForm": "Austen, Jane, 1775-1817",
- "recordID": "102333412"
- }]
+ mock_result["result"] = [
+ {
+ "term": "Austen, Jane, 1775-1817",
+ "displayForm": "Austen, Jane, 1775-1817",
+ "recordID": "102333412",
+ }
+ ]
mockrequests.get.return_value.json.return_value = mock_result
- assert viaf.suggest('austen') == mock_result['result']
+ assert viaf.suggest("austen") == mock_result["result"]
# bad status code on the response - should still return an empty list
mockrequests.get.return_value.status_code = requests.codes.forbidden
- assert viaf.suggest('test') == []
+ assert viaf.suggest("test") == []
-
- @patch('viapy.api.requests')
+ @patch("viapy.api.requests")
def test_search(self, mockrequests):
viaf = ViafAPI()
mockrequests.codes = requests.codes
- sru_fixture = os.path.join(FIXTURES_PATH, 'sru_search.json')
+ sru_fixture = os.path.join(FIXTURES_PATH, "sru_search.json")
with open(sru_fixture) as srufile:
mock_result = json.load(srufile)
mockrequests.get.return_value.status_code = requests.codes.ok
mockrequests.get.return_value.json.return_value = mock_result
- results = viaf.search('stephen benet')
+ results = viaf.search("stephen benet")
assert isinstance(results, list)
assert isinstance(results[0], SRUItem)
mockrequests.get.assert_called_with(
- 'https://www.viaf.org/viaf/search',
+ "https://www.viaf.org/viaf/search",
# headers={'accept': 'application/json'},
- params={'query': 'stephen benet', 'httpAccept': 'application/json',
- 'maximumRecords': 50, 'sortKeys': 'holdingscount'})
+ params={
+ "query": "stephen benet",
+ "httpAccept": "application/json",
+ "maximumRecords": 50,
+ "sortKeys": "holdingscount",
+ },
+ )
# sample empty result
mockrequests.get.return_value.json.return_value = {
- 'searchRetrieveResponse': {
- 'version': "1.1",
- 'numberOfRecords': "0",
- 'resultSetIdleTime': "1"
+ "searchRetrieveResponse": {
+ "version": "1.1",
+ "numberOfRecords": "0",
+ "resultSetIdleTime": "1",
}
}
- results = viaf.search('stephen benet')
+ results = viaf.search("stephen benet")
assert not results
def test_find_person(self):
viaf = ViafAPI()
- term = 'stephen benet'
- with patch.object(viaf, 'search') as mocksearch:
+ term = "stephen benet"
+ with patch.object(viaf, "search") as mocksearch:
viaf.find_person(term)
mocksearch.assert_called_with('local.personalNames all "%s"' % term)
def test_find_corporate(self):
viaf = ViafAPI()
- term = 'library of congress'
- with patch.object(viaf, 'search') as mocksearch:
+ term = "library of congress"
+ with patch.object(viaf, "search") as mocksearch:
viaf.find_corporate(term)
mocksearch.assert_called_with('local.corporateNames all "%s"' % term)
def test_find_place(self):
viaf = ViafAPI()
- term = 'princeton'
- with patch.object(viaf, 'search') as mocksearch:
+ term = "princeton"
+ with patch.object(viaf, "search") as mocksearch:
viaf.find_place(term)
mocksearch.assert_called_with('local.geographicNames all "%s"' % term)
class TestViafEntity(object):
-
test_id = 102333412
- test_uri = 'http://viaf.org/viaf/102333412'
- rdf_fixture = os.path.join(FIXTURES_PATH, '102333412_rdf.xml')
+ test_uri = "http://viaf.org/viaf/102333412"
+ rdf_fixture = os.path.join(FIXTURES_PATH, "102333412_rdf.xml")
def test_init(self):
# numeric id (either int or string should work)
@@ -122,38 +125,39 @@ def test_uriref(self):
ent = ViafEntity(self.test_uri)
assert ent.uriref == rdflib.URIRef(self.test_uri)
- @patch('viapy.api.rdflib')
+ @patch("viapy.api.rdflib")
def test_rdf(self, mockrdflib):
ent = ViafEntity(self.test_uri)
assert ent.rdf == mockrdflib.Graph.return_value
# should initialize a graph and parse uri data
mockrdflib.Graph.assert_called_with()
- mockrdflib.Graph.return_value.parse.assert_called_with(
- self.test_uri)
+ mockrdflib.Graph.return_value.parse.assert_called_with(self.test_uri)
def test_properties(self):
# use viaf id matching fixture rdf file
- ent = ViafEntity('89599270')
+ ent = ViafEntity("89599270")
# load fixture
test_rdf = rdflib.Graph()
test_rdf.parse(self.rdf_fixture)
# patch fixture in to ViafEntity rdf property
- with patch.object(ViafEntity, 'rdf', new=test_rdf):
- assert str(ent.birthdate) == '69'
- assert str(ent.deathdate) == '140'
+ with patch.object(ViafEntity, "rdf", new=test_rdf):
+ assert str(ent.birthdate) == "69"
+ assert str(ent.deathdate) == "140"
assert ent.birthyear == 69
assert ent.deathyear == 140
def test_year_from_isodate(self):
- assert ViafEntity.year_from_isodate('2001') == 2001
- assert ViafEntity.year_from_isodate('2002-01') == 2002
- assert ViafEntity.year_from_isodate('2004-03-05') == 2004
+ assert ViafEntity.year_from_isodate("2001") == 2001
+ assert ViafEntity.year_from_isodate("2002-01") == 2002
+ assert ViafEntity.year_from_isodate("2004-03-05") == 2004
+ # negative years, e.g. Aeschylus
+ assert ViafEntity.year_from_isodate("-525") == -525
def test_sru_result():
# test SRUResult class properties
- sru_fixture = os.path.join(FIXTURES_PATH, 'sru_search.json')
+ sru_fixture = os.path.join(FIXTURES_PATH, "sru_search.json")
with open(sru_fixture) as srufile:
sru_data = json.load(srufile)
sru_res = SRUResult(sru_data)
@@ -165,17 +169,21 @@ def test_sru_result():
def test_sru_item():
# test SRUItem class
- sru_fixture = os.path.join(FIXTURES_PATH, 'sru_search.json')
+ sru_fixture = os.path.join(FIXTURES_PATH, "sru_search.json")
with open(sru_fixture) as srufile:
sru_data = json.load(srufile)
sru_item = SRUResult(sru_data).records[0]
assert sru_item.uri == "http://viaf.org/viaf/888145424579886830405/"
assert sru_item.viaf_id == "888145424579886830405"
assert sru_item.nametype == "UniformTitleExpression"
- assert sru_item.label == "Benét, Stephen Vincent, 1898-1943. | John Brown's Body | Russian 1979"
+ assert (
+ sru_item.label
+ == "Benét, Stephen Vincent, 1898-1943. | John Brown's Body | Russian 1979"
+ )
# label when data is a list
- sru_item.recordData.mainHeadings.data = [
- sru_item.recordData.mainHeadings.data
- ]
- assert sru_item.label == "Benét, Stephen Vincent, 1898-1943. | John Brown's Body | Russian 1979"
\ No newline at end of file
+ sru_item.recordData.mainHeadings.data = [sru_item.recordData.mainHeadings.data]
+ assert (
+ sru_item.label
+ == "Benét, Stephen Vincent, 1898-1943. | John Brown's Body | Russian 1979"
+ )
diff --git a/viapy/urls.py b/viapy/urls.py
index bc51938..8e35c1f 100644
--- a/viapy/urls.py
+++ b/viapy/urls.py
@@ -1,15 +1,23 @@
-from django.conf.urls import url
+from django.urls import path
from viapy.views import ViafLookup, ViafSearch
-app_name = 'viapy'
+app_name = "viapy"
urlpatterns = [
- url(r'^suggest/$', ViafLookup.as_view(), name='suggest'),
- url(r'^suggest/person/$', ViafLookup.as_view(),
- {'nametype': 'personal'}, name='person-suggest'),
- url(r'^search/$', ViafSearch.as_view(), name='search'),
- url(r'^search/person/$', ViafSearch.as_view(),
- {'nametype': 'personal'}, name='person-search'),
+ path("suggest/", ViafLookup.as_view(), name="suggest"),
+ path(
+ "suggest/person/",
+ ViafLookup.as_view(),
+ {"nametype": "personal"},
+ name="person-suggest",
+ ),
+ path("search/", ViafSearch.as_view(), name="search"),
+ path(
+ "search/person/",
+ ViafSearch.as_view(),
+ {"nametype": "personal"},
+ name="person-search",
+ ),
]
diff --git a/viapy/widgets.py b/viapy/widgets.py
index 3332c92..885c6f3 100644
--- a/viapy/widgets.py
+++ b/viapy/widgets.py
@@ -3,8 +3,8 @@
class ViafWidget(autocomplete.Select2):
- '''Custom autocomplete select widget that displays VIAF id as a link.
- Extends :class:`dal.autocomplete.Select2`.'''
+ """Custom autocomplete select widget that displays VIAF id as a link.
+ Extends :class:`dal.autocomplete.Select2`."""
def render(self, name, value, renderer=None, attrs=None):
# select2 filters based on existing choices (non-existent here),
@@ -13,7 +13,6 @@ def render(self, name, value, renderer=None, attrs=None):
self.choices = [(value, value)]
widget = super(ViafWidget, self).render(name, value, attrs)
return mark_safe(
- '%s
%s
' % \
- (widget, value or '', value or ''))
-
-
+ '%s
%s
'
+ % (widget, value or "", value or "")
+ )