Packaging/dev-tools/docs update #244
70 changed files with 1518 additions and 1327 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
---
|
---
|
||||||
name: Codespell
|
name: Codespell
|
||||||
|
|
||||||
on: workflow_dispatch
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -10,13 +10,13 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
codespell:
|
codespell:
|
||||||
name: Check for spelling errors
|
name: Check for spelling errors
|
||||||
runs-on: ubuntu-latest
|
runs-on: debian-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Codespell
|
- name: Codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: https://github.com/codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: crate
|
ignore_words_list: crate
|
||||||
|
|
|
||||||
36
.forgejo/workflows/mypy-pr.yml
Normal file
36
.forgejo/workflows/mypy-pr.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
name: Type annotation (PR)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'dump_things_service/**.py'
|
||||||
|
- '!**/tests/**.py'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install hatch
|
||||||
|
run: uv tool install hatch
|
||||||
|
- name: Get Python changed files
|
||||||
|
id: changed-py-files
|
||||||
|
uses: https://github.com/tj-actions/changed-files@v46
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
*.py
|
||||||
|
**/*.py
|
||||||
|
- name: Type check changed files
|
||||||
|
if: steps.changed-py-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
# get any type stubs that mypy thinks it needs
|
||||||
|
# run mypy on the modified files only, and do not even follow imports.
|
||||||
|
# this results is a fairly superficial test, but given the overall
|
||||||
|
# state of annotations, we strive to become more correct incrementally
|
||||||
|
# with focused error reports, rather than barfing a huge complaint
|
||||||
|
# that is unrelated to the changeset someone has been working on.
|
||||||
|
# run on the oldest supported Python version
|
||||||
|
hatch run types:mypy --install-types --non-interactive --python-version 3.11 --follow-imports skip --pretty --show-error-context ${{ steps.changed-py-files.outputs.all_changed_files }}
|
||||||
17
.forgejo/workflows/ruff.yml
Normal file
17
.forgejo/workflows/ruff.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
name: Ruff
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
ruff:
|
||||||
|
name: Code linting
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install hatch
|
||||||
|
run: uv tool install hatch
|
||||||
|
- name: Check code
|
||||||
|
run: hatch check code
|
||||||
|
- name: Check formatting
|
||||||
|
run: hatch check fmt
|
||||||
|
|
@ -12,9 +12,6 @@ jobs:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
|
|
@ -23,14 +20,14 @@ jobs:
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
hatch run tests:run \
|
hatch test \
|
||||||
--ignore=dump_things_service/tests/test_generators.py \
|
--ignore=dump_things_service/tests/test_generators.py \
|
||||||
--ignore=dump_things_service/tests/test_ifabsent_patch.py
|
--ignore=dump_things_service/tests/test_ifabsent_patch.py
|
||||||
|
|
||||||
- name: Run generator tests
|
- name: Run generator tests
|
||||||
run: |
|
run: |
|
||||||
hatch run tests:run dump_things_service/tests/test_generators.py
|
hatch test dump_things_service/tests/test_generators.py
|
||||||
|
|
||||||
- name: Run ifabsent-patch tests
|
- name: Run ifabsent-patch tests
|
||||||
run: |
|
run: |
|
||||||
hatch run tests:run dump_things_service/tests/test_ifabsent_patch.py
|
hatch test dump_things_service/tests/test_ifabsent_patch.py
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,3 +3,5 @@ dist/**
|
||||||
tmp/**
|
tmp/**
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
**/.hypothesis
|
**/.hypothesis
|
||||||
|
.*.swp
|
||||||
|
dump_things_service/_version.py
|
||||||
|
|
|
||||||
31
.readthedocs.yaml
Normal file
31
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the OS, Python version and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-24.04
|
||||||
|
tools:
|
||||||
|
python: "3.12"
|
||||||
|
|
||||||
|
# Build documentation in the "docs/" directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
# Optionally build your docs in additional formats such as PDF and ePub
|
||||||
|
# formats:
|
||||||
|
# - pdf
|
||||||
|
# - epub
|
||||||
|
|
||||||
|
# Optional but recommended, declare the Python requirements required
|
||||||
|
# to build your documentation
|
||||||
|
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- method: pip
|
||||||
|
path: .
|
||||||
|
extra_requirements:
|
||||||
|
- docs
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
3. The top-level mapping `admin_tokens` was added.
|
3. The top-level mapping `admin_tokens` was added.
|
||||||
|
|
||||||
- Configuration files are no longer read when the service is started. Instead
|
- Configuration files are no longer read when the service is started. Instead
|
||||||
the service reads its configuration from the store, if it is present. Thw tool
|
the service reads its configuration from the store, if it is present. The tool
|
||||||
(`dump-things-load-config`) can read an existing configuration
|
(`dump-things-load-config`) can read an existing configuration
|
||||||
file and manifest the described configuration on a running dump-things server.
|
file and manifest the described configuration on a running dump-things server.
|
||||||
It supports pre version 6 config files and converts them to the new
|
It supports pre version 6 config files and converts them to the new
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ Most endpoints require a *collection*. These correspond to the names of the "dat
|
||||||
|
|
||||||
The service provides the following user endpoints (In addition to user endpoints, there exist endpoints for curators. To view them, check the `/docs`-path in an installed service):
|
The service provides the following user endpoints (In addition to user endpoints, there exist endpoints for curators. To view them, check the `/docs`-path in an installed service):
|
||||||
|
|
||||||
- `POST /maintenance`: this endpoint allows to set a collection into mantenance mode.
|
- `POST /maintenance`: this endpoint allows to set a collection into maintenance mode.
|
||||||
In maintenance mode, only tokens with curator-privileges can access the collection.
|
In maintenance mode, only tokens with curator-privileges can access the collection.
|
||||||
The posted data is a JSON that contains the name of the collection and whether the maintenance state should be active or not, for example:
|
The posted data is a JSON that contains the name of the collection and whether the maintenance state should be active or not, for example:
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
2
docs/.gitignore
vendored
Normal file
2
docs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
_build
|
||||||
|
generated
|
||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?= --fail-on-warning
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
48
docs/conf.py
Normal file
48
docs/conf.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
import dump_things_service
|
||||||
|
|
||||||
|
project = 'dump-things-server'
|
||||||
|
copyright = '2025-2026, Christian Mönch'
|
||||||
|
author = 'Christian Mönch'
|
||||||
|
release = dump_things_service.__version__
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autosummary',
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx_autodoc_typehints',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
]
|
||||||
|
|
||||||
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
primary_domain = 'py'
|
||||||
|
autoclass_content = "both"
|
||||||
|
|
||||||
|
typehints_use_signature = True
|
||||||
|
typehints_use_signature_return = True
|
||||||
|
|
||||||
|
# we build some docstrings from loguru. define some no-op substitutions
|
||||||
|
# to avoid errors
|
||||||
|
rst_prolog = """
|
||||||
|
.. |Logger| replace:: Logger
|
||||||
|
.. |add| replace:: add
|
||||||
|
.. |sys.stderr| replace:: sys.stderr
|
||||||
|
.. |str.format| replace:: str.format
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
html_static_path = ['_static']
|
||||||
10
docs/index.rst
Normal file
10
docs/index.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
The `dump-thing-server` documentation
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
HERE BE CONTENT...
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
@ -20,8 +20,9 @@ from starlette.status import (
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from dump_things_service._version import __version__
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Format',
|
|
||||||
'HTTP_200_OK',
|
'HTTP_200_OK',
|
||||||
'HTTP_201_CREATED',
|
'HTTP_201_CREATED',
|
||||||
'HTTP_300_MULTIPLE_CHOICES',
|
'HTTP_300_MULTIPLE_CHOICES',
|
||||||
|
|
@ -37,6 +38,8 @@ __all__ = [
|
||||||
'HTTP_503_SERVICE_UNAVAILABLE',
|
'HTTP_503_SERVICE_UNAVAILABLE',
|
||||||
'JSON',
|
'JSON',
|
||||||
'YAML',
|
'YAML',
|
||||||
|
'Format',
|
||||||
|
'__version__',
|
||||||
'config_file_name',
|
'config_file_name',
|
||||||
'reserved_collection_names',
|
'reserved_collection_names',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import enum
|
import enum
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import (
|
from pathlib import (
|
||||||
Path,
|
Path,
|
||||||
PurePosixPath,
|
PurePosixPath,
|
||||||
)
|
)
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
|
||||||
Iterable,
|
|
||||||
Literal,
|
Literal,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
@ -17,7 +16,8 @@ from fastapi import HTTPException
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
ConfigDict,
|
||||||
Field, ValidationError,
|
Field,
|
||||||
|
ValidationError,
|
||||||
)
|
)
|
||||||
from yaml.scanner import ScannerError
|
from yaml.scanner import ScannerError
|
||||||
|
|
||||||
|
|
@ -27,12 +27,11 @@ from dump_things_service import (
|
||||||
)
|
)
|
||||||
from dump_things_service.audit.gitaudit import GitAuditBackend
|
from dump_things_service.audit.gitaudit import GitAuditBackend
|
||||||
from dump_things_service.backends.record_dir import (
|
from dump_things_service.backends.record_dir import (
|
||||||
_RecordDirStore,
|
|
||||||
RecordDirStore,
|
RecordDirStore,
|
||||||
|
_RecordDirStore,
|
||||||
)
|
)
|
||||||
from dump_things_service.exceptions import ConfigError
|
from dump_things_service.exceptions import ConfigError
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
g_abstract_configuration = None
|
g_abstract_configuration = None
|
||||||
|
|
@ -103,7 +102,9 @@ class CollectionConfig(BaseModel):
|
||||||
curated: PurePosixPath
|
curated: PurePosixPath
|
||||||
schema_location: str = Field(alias='schema')
|
schema_location: str = Field(alias='schema')
|
||||||
incoming: PurePosixPath | None = None
|
incoming: PurePosixPath | None = None
|
||||||
backend: RecordDirBackendConfig | SQLiteBackendConfig = RecordDirBackendConfig(type='record_dir+stl')
|
backend: RecordDirBackendConfig | SQLiteBackendConfig = RecordDirBackendConfig(
|
||||||
|
type='record_dir+stl'
|
||||||
|
)
|
||||||
auth_sources: list[ForgejoAuthSpec | ConfigAuthSpec] = [ConfigAuthSpec()]
|
auth_sources: list[ForgejoAuthSpec | ConfigAuthSpec] = [ConfigAuthSpec()]
|
||||||
audit_backends: list[GitAuditBackendConfig] = []
|
audit_backends: list[GitAuditBackendConfig] = []
|
||||||
submission_tags: TagSpec = TagSpec()
|
submission_tags: TagSpec = TagSpec()
|
||||||
|
|
@ -211,9 +212,7 @@ def get_config_backends(
|
||||||
|
|
||||||
if config_backend is None:
|
if config_backend is None:
|
||||||
config_backend = RecordDirStore(
|
config_backend = RecordDirStore(
|
||||||
config_path,
|
config_path, mapping_functions[MappingMethod.digest_md5], 'yaml'
|
||||||
mapping_functions[MappingMethod.digest_md5],
|
|
||||||
'yaml'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
audit_path = store_path / config_audit_path
|
audit_path = store_path / config_audit_path
|
||||||
|
|
@ -259,7 +258,7 @@ def get_config() -> Configuration:
|
||||||
if not g_abstract_configuration:
|
if not g_abstract_configuration:
|
||||||
msg = 'Configuration not yet loaded'
|
msg = 'Configuration not yet loaded'
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
return cast(Configuration, g_abstract_configuration)
|
return cast('Configuration', g_abstract_configuration)
|
||||||
|
|
||||||
|
|
||||||
def store_config(
|
def store_config(
|
||||||
|
|
@ -274,7 +273,7 @@ def store_config(
|
||||||
config_backend.add_record(
|
config_backend.add_record(
|
||||||
iri=dump_things_config_iri,
|
iri=dump_things_config_iri,
|
||||||
class_name='DumpThingsConfig',
|
class_name='DumpThingsConfig',
|
||||||
json_object=json_object
|
json_object=json_object,
|
||||||
)
|
)
|
||||||
audit_backend.add_record(
|
audit_backend.add_record(
|
||||||
record=json_object,
|
record=json_object,
|
||||||
|
|
@ -314,10 +313,9 @@ def check_label(
|
||||||
from dump_things_service.utils import get_on_disk_labels
|
from dump_things_service.utils import get_on_disk_labels
|
||||||
|
|
||||||
"""Check that a label exists in a collection configuration or on disk"""
|
"""Check that a label exists in a collection configuration or on disk"""
|
||||||
if (
|
if label not in get_config_labels(
|
||||||
label not in get_config_labels(abstract_config, collection)
|
abstract_config, collection
|
||||||
and label not in get_on_disk_labels(store_path, abstract_config, collection)
|
) and label not in get_on_disk_labels(store_path, abstract_config, collection):
|
||||||
):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_404_NOT_FOUND,
|
status_code=HTTP_404_NOT_FOUND,
|
||||||
detail=f"No incoming label: '{label}' in collection: '{collection}'.",
|
detail=f"No incoming label: '{label}' in collection: '{collection}'.",
|
||||||
|
|
@ -336,10 +334,7 @@ def get_config_labels(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_default_token_name(
|
def get_default_token_name(abstract_config: Configuration, collection: str) -> str:
|
||||||
abstract_config: Configuration,
|
|
||||||
collection: str
|
|
||||||
) -> str:
|
|
||||||
check_collection(abstract_config, collection)
|
check_collection(abstract_config, collection)
|
||||||
return abstract_config.collections[collection].default_token
|
return abstract_config.collections[collection].default_token
|
||||||
|
|
||||||
|
|
@ -377,7 +372,6 @@ def get_token_infos_for_collection(
|
||||||
abstract_config: Configuration,
|
abstract_config: Configuration,
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
) -> Iterable[tuple[str, TokenConfig, TokenCollectionConfig]]:
|
) -> Iterable[tuple[str, TokenConfig, TokenCollectionConfig]]:
|
||||||
|
|
||||||
yield from {
|
yield from {
|
||||||
(token_name, token_config, token_collection_config)
|
(token_name, token_config, token_collection_config)
|
||||||
for token_name, token_config in abstract_config.tokens.items()
|
for token_name, token_config in abstract_config.tokens.items()
|
||||||
|
|
@ -391,7 +385,6 @@ def get_token_config_for_representation_and_collection(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
token_representation: str,
|
token_representation: str,
|
||||||
) -> tuple[str, TokenConfig, TokenCollectionConfig] | None:
|
) -> tuple[str, TokenConfig, TokenCollectionConfig] | None:
|
||||||
|
|
||||||
token_info = get_token_info_by_representation(
|
token_info = get_token_info_by_representation(
|
||||||
abstract_config=abstract_config,
|
abstract_config=abstract_config,
|
||||||
token_representation=token_representation,
|
token_representation=token_representation,
|
||||||
|
|
@ -421,7 +414,6 @@ def get_default_token_config(
|
||||||
abstract_config: Configuration,
|
abstract_config: Configuration,
|
||||||
collection: str,
|
collection: str,
|
||||||
) -> TokenConfig | None:
|
) -> TokenConfig | None:
|
||||||
|
|
||||||
default_token_name = get_collection_config_by_name(
|
default_token_name = get_collection_config_by_name(
|
||||||
abstract_config,
|
abstract_config,
|
||||||
collection,
|
collection,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from dump_things_service.abstract_config import (
|
||||||
)
|
)
|
||||||
from dump_things_service.instance_state import InstanceState
|
from dump_things_service.instance_state import InstanceState
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ committed.
|
||||||
|
|
||||||
Changes are annotated with a time stamp and a user-id
|
Changes are annotated with a time stamp and a user-id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -23,12 +24,11 @@ import yaml
|
||||||
from datalad_core.git_utils import apply_changeset
|
from datalad_core.git_utils import apply_changeset
|
||||||
from datalad_core.repo import Repo
|
from datalad_core.repo import Repo
|
||||||
from datalad_core.runners import (
|
from datalad_core.runners import (
|
||||||
call_git,
|
|
||||||
CommandError,
|
CommandError,
|
||||||
|
call_git,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import AuditBackend
|
from dump_things_service.audit import AuditBackend
|
||||||
|
|
||||||
|
|
||||||
index_file_name = 'gitaudit_index.log'
|
index_file_name = 'gitaudit_index.log'
|
||||||
|
|
||||||
|
|
@ -56,7 +56,6 @@ class FlushingThread(Thread):
|
||||||
|
|
||||||
|
|
||||||
class GitAuditBackend(AuditBackend):
|
class GitAuditBackend(AuditBackend):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: Path,
|
path: Path,
|
||||||
|
|
@ -69,7 +68,8 @@ class GitAuditBackend(AuditBackend):
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.last_flush_time = 0
|
self.last_flush_time = 0
|
||||||
if auto_flush_timeout < 1:
|
if auto_flush_timeout < 1:
|
||||||
raise ValueError('auto_flush_timeout must be greater or equal to 1')
|
msg = 'auto_flush_timeout must be greater or equal to 1'
|
||||||
|
raise ValueError(msg)
|
||||||
self.flushing_thread = FlushingThread(self, auto_flush_timeout)
|
self.flushing_thread = FlushingThread(self, auto_flush_timeout)
|
||||||
self.flushing_thread.start()
|
self.flushing_thread.start()
|
||||||
self._init_repo()
|
self._init_repo()
|
||||||
|
|
@ -125,32 +125,42 @@ class GitAuditBackend(AuditBackend):
|
||||||
# the records
|
# the records
|
||||||
changes = []
|
changes = []
|
||||||
yaml_location, log_location = map(str, self._get_location_for(record_id)[1:])
|
yaml_location, log_location = map(str, self._get_location_for(record_id)[1:])
|
||||||
commit_hashes = call_git(
|
commit_hashes = (
|
||||||
|
call_git(
|
||||||
['log', '--format=%H', '--', log_location],
|
['log', '--format=%H', '--', log_location],
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
).decode().splitlines()
|
)
|
||||||
|
.decode()
|
||||||
|
.splitlines()
|
||||||
|
)
|
||||||
for commit_hash in commit_hashes:
|
for commit_hash in commit_hashes:
|
||||||
log_diff_lines = call_git(
|
log_diff_lines = (
|
||||||
|
call_git(
|
||||||
['show', '--format=%b', commit_hash, '--', log_location],
|
['show', '--format=%b', commit_hash, '--', log_location],
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
).decode().splitlines()
|
)
|
||||||
|
.decode()
|
||||||
|
.splitlines()
|
||||||
|
)
|
||||||
# Get the log entry
|
# Get the log entry
|
||||||
log_line = tuple(
|
log_line = next(filter(
|
||||||
filter(
|
|
||||||
lambda l: not l.startswith('+++') and l.startswith('+'),
|
lambda l: not l.startswith('+++') and l.startswith('+'),
|
||||||
log_diff_lines,
|
log_diff_lines,
|
||||||
)
|
))[1:]
|
||||||
)[0][1:]
|
|
||||||
log_entry = json.loads(log_line)
|
log_entry = json.loads(log_line)
|
||||||
|
|
||||||
# Get the YAML diff
|
# Get the YAML diff
|
||||||
yaml_diff_lines = call_git(
|
yaml_diff_lines = (
|
||||||
|
call_git(
|
||||||
['show', '--format=%b', commit_hash, '--', yaml_location],
|
['show', '--format=%b', commit_hash, '--', yaml_location],
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
).decode().splitlines()
|
)
|
||||||
|
.decode()
|
||||||
|
.splitlines()
|
||||||
|
)
|
||||||
yaml_diff = '\n'.join(filter(lambda l: l != '', yaml_diff_lines)) + '\n'
|
yaml_diff = '\n'.join(filter(lambda l: l != '', yaml_diff_lines)) + '\n'
|
||||||
|
|
||||||
# Get the YAML content
|
# Get the YAML content
|
||||||
|
|
@ -247,7 +257,7 @@ class GitAuditBackend(AuditBackend):
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
try:
|
try:
|
||||||
return call_git(
|
return call_git(
|
||||||
['cat-file', '-p', f'master:{str(path)}'],
|
['cat-file', '-p', f'master:{path!s}'],
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|
@ -290,7 +300,7 @@ class GitAuditBackend(AuditBackend):
|
||||||
record_id: str,
|
record_id: str,
|
||||||
) -> tuple[str, Path, Path]:
|
) -> tuple[str, Path, Path]:
|
||||||
base = hashlib.sha1(record_id.encode()).hexdigest()
|
base = hashlib.sha1(record_id.encode()).hexdigest()
|
||||||
dir_1, dir_2, name = base[0:3], base[3:6], base[6:]
|
dir_1, dir_2, _name = base[0:3], base[3:6], base[6:]
|
||||||
location_dir = Path(dir_1) / Path(dir_2)
|
location_dir = Path(dir_1) / Path(dir_2)
|
||||||
return (
|
return (
|
||||||
base,
|
base,
|
||||||
|
|
@ -321,8 +331,8 @@ class GitAuditBackend(AuditBackend):
|
||||||
if not self.index_path.exists():
|
if not self.index_path.exists():
|
||||||
self._rebuild_index()
|
self._rebuild_index()
|
||||||
|
|
||||||
with open(self.index_path, 'rt') as f:
|
with open(self.index_path) as f:
|
||||||
self.index = set(line.strip() for line in f.readlines())
|
self.index = {line.strip() for line in f}
|
||||||
|
|
||||||
def _add_to_index(
|
def _add_to_index(
|
||||||
self,
|
self,
|
||||||
|
|
@ -333,16 +343,20 @@ class GitAuditBackend(AuditBackend):
|
||||||
self.index.add(record_id)
|
self.index.add(record_id)
|
||||||
|
|
||||||
def _rebuild_index(self):
|
def _rebuild_index(self):
|
||||||
tree_entries = call_git(
|
tree_entries = (
|
||||||
|
call_git(
|
||||||
['ls-tree', '-r', 'master:'],
|
['ls-tree', '-r', 'master:'],
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
).decode().splitlines()
|
)
|
||||||
with open(self.index_path, 'wt') as f:
|
.decode()
|
||||||
|
.splitlines()
|
||||||
|
)
|
||||||
|
with open(self.index_path, 'w') as f:
|
||||||
for line in tree_entries:
|
for line in tree_entries:
|
||||||
if not line.endswith('.yaml'):
|
if not line.endswith('.yaml'):
|
||||||
continue
|
continue
|
||||||
flag, object_type, object_hash, file_name = line.split(maxsplit=3)
|
_flag, _object_type, object_hash, _file_name = line.split(maxsplit=3)
|
||||||
record = yaml.safe_load(
|
record = yaml.safe_load(
|
||||||
call_git(
|
call_git(
|
||||||
['show', object_hash],
|
['show', object_hash],
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ def _get_audit_log_lines(backend: GitAuditBackend, record_id: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def test_gitaudit_basic(tmp_path_factory):
|
def test_gitaudit_basic(tmp_path_factory):
|
||||||
tmp_path = tmp_path_factory.mktemp("gitaudit_backend")
|
tmp_path = tmp_path_factory.mktemp('gitaudit_backend')
|
||||||
|
|
||||||
backend = GitAuditBackend(tmp_path)
|
backend = GitAuditBackend(tmp_path)
|
||||||
|
|
||||||
|
|
@ -44,14 +44,13 @@ def test_gitaudit_basic(tmp_path_factory):
|
||||||
# Check that the changes are reported
|
# Check that the changes are reported
|
||||||
changes = backend.get_audit_log(record_id)
|
changes = backend.get_audit_log(record_id)
|
||||||
assert len(changes) == 4
|
assert len(changes) == 4
|
||||||
assert tuple(map(lambda e: e[0:2], changes.values())) == tuple(
|
assert tuple(e[0:2] for e in changes.values()) == tuple(
|
||||||
(f'committer_{100 + i}@x.org', f'author_{i}@y.org')
|
(f'committer_{100 + i}@x.org', f'author_{i}@y.org') for i in range(4)
|
||||||
for i in range(4)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_gitaudit_identical_change(tmp_path_factory):
|
def test_gitaudit_identical_change(tmp_path_factory):
|
||||||
tmp_path = tmp_path_factory.mktemp("gitaudit_backend")
|
tmp_path = tmp_path_factory.mktemp('gitaudit_backend')
|
||||||
|
|
||||||
backend = GitAuditBackend(tmp_path)
|
backend = GitAuditBackend(tmp_path)
|
||||||
|
|
||||||
|
|
@ -83,7 +82,7 @@ def test_gitaudit_identical_change(tmp_path_factory):
|
||||||
|
|
||||||
|
|
||||||
def test_gitaudit_huge_log(tmp_path_factory):
|
def test_gitaudit_huge_log(tmp_path_factory):
|
||||||
tmp_path = tmp_path_factory.mktemp("gitaudit_backend")
|
tmp_path = tmp_path_factory.mktemp('gitaudit_backend')
|
||||||
|
|
||||||
backend = GitAuditBackend(tmp_path)
|
backend = GitAuditBackend(tmp_path)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ determine:
|
||||||
- the incoming_label to be used with the token
|
- the incoming_label to be used with the token
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"""Use configuration information to fetch token permissions, ids, and incoming_label"""
|
"""Use configuration information to fetch token permissions, ids, and incoming_label"""
|
||||||
|
|
||||||
from dump_things_service.abstract_config import Configuration
|
from dump_things_service.abstract_config import (
|
||||||
|
Configuration,
|
||||||
|
get_token_config_for_representation_and_collection,
|
||||||
|
get_token_permissions,
|
||||||
|
)
|
||||||
from dump_things_service.auth import (
|
from dump_things_service.auth import (
|
||||||
AuthenticationInfo,
|
AuthenticationInfo,
|
||||||
AuthenticationSource,
|
AuthenticationSource,
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
|
||||||
get_token_permissions,
|
|
||||||
get_token_config_for_representation_and_collection,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigAuthenticationSource(AuthenticationSource):
|
class ConfigAuthenticationSource(AuthenticationSource):
|
||||||
|
|
@ -25,7 +25,6 @@ class ConfigAuthenticationSource(AuthenticationSource):
|
||||||
self,
|
self,
|
||||||
token_representation: str,
|
token_representation: str,
|
||||||
) -> AuthenticationInfo:
|
) -> AuthenticationInfo:
|
||||||
|
|
||||||
result = get_token_config_for_representation_and_collection(
|
result = get_token_config_for_representation_and_collection(
|
||||||
self.abstract_configuration,
|
self.abstract_configuration,
|
||||||
self.collection_name,
|
self.collection_name,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@ Note: for some reason, the request:
|
||||||
does not require a token. If the owner and the repo are known, the request
|
does not require a token. If the owner and the repo are known, the request
|
||||||
will emit a complete repository-record including the complete owner-record.
|
will emit a complete repository-record including the complete owner-record.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
|
|
@ -22,13 +23,16 @@ from dump_things_service import (
|
||||||
HTTP_300_MULTIPLE_CHOICES,
|
HTTP_300_MULTIPLE_CHOICES,
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
from dump_things_service.abstract_config import TokenPermission
|
||||||
from dump_things_service.auth import (
|
from dump_things_service.auth import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
AuthenticationInfo,
|
AuthenticationInfo,
|
||||||
AuthenticationSource,
|
AuthenticationSource,
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import TokenPermission
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
|
|
@ -47,6 +51,7 @@ class MethodCache:
|
||||||
duration: int = 300,
|
duration: int = 300,
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""Cache results for a given time (default: 300 seconds)"""
|
"""Cache results for a given time (default: 300 seconds)"""
|
||||||
|
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
|
@ -56,12 +61,15 @@ class MethodCache:
|
||||||
if cached_data is None or time.time() - cached_data[0] > duration:
|
if cached_data is None or time.time() - cached_data[0] > duration:
|
||||||
self.__cached_data[key] = (time.time(), func(*args, **kwargs))
|
self.__cached_data[key] = (time.time(), func(*args, **kwargs))
|
||||||
return self.__cached_data[key][1]
|
return self.__cached_data[key][1]
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class RemoteAuthenticationError(AuthenticationError):
|
class RemoteAuthenticationError(AuthenticationError):
|
||||||
"""Exception for remote authentication errors."""
|
"""Exception for remote authentication errors."""
|
||||||
|
|
||||||
def __init__(self, status: int, message: str):
|
def __init__(self, status: int, message: str):
|
||||||
self.status = status
|
self.status = status
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
@ -133,7 +141,8 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache):
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
if r.status_code >= HTTP_300_MULTIPLE_CHOICES:
|
if r.status_code >= HTTP_300_MULTIPLE_CHOICES:
|
||||||
msg = f'invalid token: ({r.status_code}): {r.text}'
|
cleaned_text = r.text.replace(token, '***')
|
||||||
|
msg = f'invalid token: ({r.status_code}): {cleaned_text}'
|
||||||
raise InvalidTokenError(msg)
|
raise InvalidTokenError(msg)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
@ -197,11 +206,7 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache):
|
||||||
zones_access=is_curator,
|
zones_access=is_curator,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_unit_content(
|
def _get_unit_content(self, team: dict, unit_name: str) -> str:
|
||||||
self,
|
|
||||||
team: dict,
|
|
||||||
unit_name: str
|
|
||||||
) -> str:
|
|
||||||
permissions = team['units_map'].get(unit_name)
|
permissions = team['units_map'].get(unit_name)
|
||||||
if not permissions:
|
if not permissions:
|
||||||
logger.debug(f'no unit `repo.actions` in team {self.team}')
|
logger.debug(f'no unit `repo.actions` in team {self.team}')
|
||||||
|
|
@ -216,23 +221,22 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache):
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
def _instance_label(self) -> str:
|
def _instance_label(self) -> str:
|
||||||
return self.instance_id or hashlib.md5(
|
return self.instance_id or hashlib.md5(self.api_url.encode()).hexdigest()
|
||||||
self.api_url.encode()
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
@MethodCache.cache_temporary(duration=60)
|
@MethodCache.cache_temporary(duration=60)
|
||||||
def authenticate(
|
def authenticate(
|
||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
) -> AuthenticationInfo:
|
) -> AuthenticationInfo:
|
||||||
|
logger.debug(
|
||||||
logger.debug(f'starting Forgejo authentication: {self.api_url}, {self.organization}, {self.team}')
|
f'starting Forgejo authentication: {self.api_url}, {self.organization}, {self.team}'
|
||||||
|
)
|
||||||
|
|
||||||
user_teams = self._get_teams_for_user(token)
|
user_teams = self._get_teams_for_user(token)
|
||||||
logger.debug(f'user_teams: {user_teams}')
|
logger.debug(f'user_teams: {user_teams}')
|
||||||
|
|
||||||
if self.team not in user_teams:
|
if self.team not in user_teams:
|
||||||
logger.debug(f'{self.team} not in user\'s teams')
|
logger.debug(f"{self.team} not in user's teams")
|
||||||
msg = f'token user is not member of team `{self.team}`'
|
msg = f'token user is not member of team `{self.team}`'
|
||||||
raise RemoteAuthenticationError(
|
raise RemoteAuthenticationError(
|
||||||
status=HTTP_401_UNAUTHORIZED,
|
status=HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -281,8 +285,7 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache):
|
||||||
action_permissions,
|
action_permissions,
|
||||||
),
|
),
|
||||||
user_id=user_info['email'],
|
user_id=user_info['email'],
|
||||||
incoming_label=
|
incoming_label=f'forgejo-{self._instance_label()}-team-{organization["name"]}-{team["name"]}'
|
||||||
f'forgejo-{self._instance_label()}-team-{organization["name"]}-{team["name"]}'
|
|
||||||
if self.label_type == 'team'
|
if self.label_type == 'team'
|
||||||
else f'forgejo-{self._instance_label()}-user-{user_info["login"]}',
|
else f'forgejo-{self._instance_label()}-user-{user_info["login"]}',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,27 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from itertools import count
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
|
||||||
Depends,
|
|
||||||
FastAPI,
|
|
||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
from fastapi_pagination import (
|
|
||||||
Page,
|
|
||||||
add_pagination,
|
|
||||||
paginate,
|
|
||||||
)
|
|
||||||
|
|
||||||
from dump_things_service import (
|
from dump_things_service import (
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
HTTP_404_NOT_FOUND,
|
|
||||||
HTTP_422_UNPROCESSABLE_CONTENT, abstract_config,
|
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
check_collection,
|
check_collection,
|
||||||
read_config,
|
read_config,
|
||||||
)
|
)
|
||||||
from dump_things_service.api_key import api_key_header_scheme
|
|
||||||
from dump_things_service.auth import AuthenticationInfo
|
|
||||||
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
||||||
from dump_things_service.exceptions import CurieResolutionError
|
|
||||||
from dump_things_service.instance_state import get_instance_state
|
from dump_things_service.instance_state import get_instance_state
|
||||||
from dump_things_service.lazy_list import ModifierList
|
|
||||||
from dump_things_service.utils import (
|
from dump_things_service.utils import (
|
||||||
authenticate_token,
|
authenticate_token,
|
||||||
check_bounds,
|
|
||||||
cleaned_json,
|
|
||||||
wrap_http_exception,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pydantic import BaseModel
|
from dump_things_service.auth import AuthenticationInfo
|
||||||
|
|
||||||
from dump_things_service.backends import StorageBackend
|
from dump_things_service.backends import StorageBackend
|
||||||
from dump_things_service.lazy_list import LazyList
|
|
||||||
from dump_things_service.store.model_store import _ModelStore
|
from dump_things_service.store.model_store import _ModelStore
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,7 +29,6 @@ def get_store_and_backend(
|
||||||
collection: str,
|
collection: str,
|
||||||
plain_token: str | None,
|
plain_token: str | None,
|
||||||
) -> tuple[_ModelStore, StorageBackend, AuthenticationInfo]:
|
) -> tuple[_ModelStore, StorageBackend, AuthenticationInfo]:
|
||||||
|
|
||||||
# A token is required
|
# A token is required
|
||||||
if plain_token is None:
|
if plain_token is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -111,9 +111,7 @@ class StorageBackend(metaclass=ABCMeta):
|
||||||
self.order_by = order_by or ['pid']
|
self.order_by = order_by or ['pid']
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_uri(
|
def get_uri(self) -> str:
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -26,12 +25,12 @@ from dump_things_service.backends import (
|
||||||
from dump_things_service.backends.record_dir_index import RecordDirIndex
|
from dump_things_service.backends.record_dir_index import RecordDirIndex
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'_RecordDirStore',
|
|
||||||
'RecordDirStore',
|
'RecordDirStore',
|
||||||
|
'_RecordDirStore',
|
||||||
]
|
]
|
||||||
|
|
||||||
ignored_files = {'.', '..', config_file_name}
|
ignored_files = {'.', '..', config_file_name}
|
||||||
|
|
@ -91,9 +90,7 @@ class _RecordDirStore(StorageBackend):
|
||||||
self.suffix = suffix
|
self.suffix = suffix
|
||||||
self.index = RecordDirIndex(root, suffix)
|
self.index = RecordDirIndex(root, suffix)
|
||||||
|
|
||||||
def get_uri(
|
def get_uri(self) -> str:
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
return f'file://{self.root!s}'
|
return f'file://{self.root!s}'
|
||||||
|
|
||||||
def build_index(
|
def build_index(
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'_SchemaTypeLayer',
|
|
||||||
'SchemaTypeLayer',
|
'SchemaTypeLayer',
|
||||||
|
'_SchemaTypeLayer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,9 +81,7 @@ class _SchemaTypeLayer(StorageBackend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self.schema_model = get_schema_model_for_schema(schema)
|
self.schema_model = get_schema_model_for_schema(schema)
|
||||||
|
|
||||||
def get_uri(
|
def get_uri(self) -> str:
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
return self.backend.get_uri()
|
return self.backend.get_uri()
|
||||||
|
|
||||||
def add_record(
|
def add_record(
|
||||||
|
|
@ -96,8 +94,7 @@ class _SchemaTypeLayer(StorageBackend):
|
||||||
# don't want to store it in the files. We add `schema_type` after
|
# don't want to store it in the files. We add `schema_type` after
|
||||||
# reading the record from disk. The value of `schema_type` is determined
|
# reading the record from disk. The value of `schema_type` is determined
|
||||||
# by the class name of the record, which is stored in the path.
|
# by the class name of the record, which is stored in the path.
|
||||||
if 'schema_type' in json_object:
|
json_object.pop('schema_type', None)
|
||||||
del json_object['schema_type']
|
|
||||||
self.backend.add_record(
|
self.backend.add_record(
|
||||||
iri=iri,
|
iri=iri,
|
||||||
class_name=class_name,
|
class_name=class_name,
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'_SQLiteBackend',
|
|
||||||
'SQLiteBackend',
|
'SQLiteBackend',
|
||||||
|
'_SQLiteBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
@ -139,9 +139,7 @@ class _SQLiteBackend(StorageBackend):
|
||||||
self.engine = create_engine('sqlite:///' + str(db_path), echo=echo)
|
self.engine = create_engine('sqlite:///' + str(db_path), echo=echo)
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine)
|
||||||
|
|
||||||
def get_uri(
|
def get_uri(self) -> str:
|
||||||
self
|
|
||||||
) -> str:
|
|
||||||
return f'sqlite://{self.db_path}'
|
return f'sqlite://{self.db_path}'
|
||||||
|
|
||||||
def perform_file_name_conversion(self):
|
def perform_file_name_conversion(self):
|
||||||
|
|
@ -152,7 +150,9 @@ class _SQLiteBackend(StorageBackend):
|
||||||
logger.info('converting old style name %s', str(old_path))
|
logger.info('converting old style name %s', str(old_path))
|
||||||
|
|
||||||
# Create a backup copy
|
# Create a backup copy
|
||||||
old_backup_path = (self.db_path.parent / (old_record_file_name + '.backup')).absolute()
|
old_backup_path = (
|
||||||
|
self.db_path.parent / (old_record_file_name + '.backup')
|
||||||
|
).absolute()
|
||||||
logger.info('copying %s to %s', old_path, old_backup_path)
|
logger.info('copying %s to %s', old_path, old_backup_path)
|
||||||
shutil.copyfile(str(old_path), str(old_backup_path))
|
shutil.copyfile(str(old_path), str(old_backup_path))
|
||||||
|
|
||||||
|
|
@ -240,21 +240,20 @@ class _SQLiteBackend(StorageBackend):
|
||||||
class_names: Iterable[str],
|
class_names: Iterable[str],
|
||||||
pattern: str | None = None,
|
pattern: str | None = None,
|
||||||
) -> SQLResultList:
|
) -> SQLResultList:
|
||||||
|
|
||||||
class_list = ', '.join(f"'{cn}'" for cn in class_names)
|
class_list = ', '.join(f"'{cn}'" for cn in class_names)
|
||||||
if pattern is None:
|
if pattern is None:
|
||||||
statement = text(
|
statement = text(
|
||||||
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
||||||
'from thing '
|
'from thing '
|
||||||
f"where thing.class_name in ({class_list}) "
|
f'where thing.class_name in ({class_list}) '
|
||||||
"ORDER BY thing.sort_key"
|
'ORDER BY thing.sort_key'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
statement = text(
|
statement = text(
|
||||||
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
||||||
'from thing, json_tree(thing.object) '
|
'from thing, json_tree(thing.object) '
|
||||||
'where lower(json_tree.value) like lower(:pattern) '
|
'where lower(json_tree.value) like lower(:pattern) '
|
||||||
f"and thing.class_name in ({class_list}) "
|
f'and thing.class_name in ({class_list}) '
|
||||||
"and json_tree.type = 'text' ORDER BY thing.sort_key"
|
"and json_tree.type = 'text' ORDER BY thing.sort_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -278,7 +277,7 @@ class _SQLiteBackend(StorageBackend):
|
||||||
statement = text(
|
statement = text(
|
||||||
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
'select distinct thing.iri, thing.class_name, thing.sort_key, thing.id '
|
||||||
'from thing '
|
'from thing '
|
||||||
"ORDER BY thing.sort_key"
|
'ORDER BY thing.sort_key'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
statement = text(
|
statement = text(
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ def test_add_and_delete_record(tmp_path):
|
||||||
record_dir_store.build_index(str(schema_path))
|
record_dir_store.build_index(str(schema_path))
|
||||||
|
|
||||||
record_dir_store.add_record(
|
record_dir_store.add_record(
|
||||||
iri=iri,
|
iri=iri, class_name='Object', json_object={'pid': 'some-pid'}
|
||||||
class_name='Object',
|
|
||||||
json_object={'pid': 'some-pid'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
record = record_dir_store.get_record_by_iri(iri=iri)
|
record = record_dir_store.get_record_by_iri(iri=iri)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,19 @@ import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
# This following lines are required for dynamic endpoint generation
|
||||||
|
from typing import (
|
||||||
|
Annotated, # noqa: F401 -- used by autogenerated code
|
||||||
|
Any,
|
||||||
|
)
|
||||||
|
|
||||||
from datalad_core.runners import (
|
from datalad_core.runners import (
|
||||||
call_git_oneline,
|
|
||||||
CommandError,
|
CommandError,
|
||||||
|
call_git_oneline,
|
||||||
)
|
)
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
|
Body, # noqa: F401 -- used by autogenerated code
|
||||||
Depends,
|
Depends,
|
||||||
FastAPI,
|
FastAPI,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
|
|
@ -24,40 +30,49 @@ from starlette.responses import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from dump_things_service import (
|
from dump_things_service import (
|
||||||
Format,
|
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
HTTP_403_FORBIDDEN,
|
HTTP_403_FORBIDDEN,
|
||||||
HTTP_422_UNPROCESSABLE_CONTENT,
|
HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
Format,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
Configuration,
|
|
||||||
ConfigAuthSpec,
|
ConfigAuthSpec,
|
||||||
|
Configuration,
|
||||||
ForgejoAuthSpec,
|
ForgejoAuthSpec,
|
||||||
RecordDirBackendConfig,
|
RecordDirBackendConfig,
|
||||||
SQLiteBackendConfig,
|
SQLiteBackendConfig,
|
||||||
read_config,
|
|
||||||
check_collection,
|
check_collection,
|
||||||
|
read_config,
|
||||||
|
)
|
||||||
|
from dump_things_service.api_key import (
|
||||||
|
api_key_header_scheme,
|
||||||
)
|
)
|
||||||
from dump_things_service.audit.gitaudit import GitAuditBackend
|
from dump_things_service.audit.gitaudit import GitAuditBackend
|
||||||
from dump_things_service.auth.config import ConfigAuthenticationSource
|
from dump_things_service.auth.config import ConfigAuthenticationSource
|
||||||
from dump_things_service.auth.forgejo import ForgejoAuthenticationSource
|
from dump_things_service.auth.forgejo import ForgejoAuthenticationSource
|
||||||
from dump_things_service.backends.record_dir_index import index_file_name
|
from dump_things_service.backends.record_dir_index import index_file_name
|
||||||
from dump_things_service.backends.sqlite import record_file_name as sqlite_db_filename
|
from dump_things_service.backends.sqlite import record_file_name as sqlite_db_filename
|
||||||
|
from dump_things_service.converter import FormatConverter
|
||||||
|
from dump_things_service.curated import (
|
||||||
|
store_curated_record, # noqa: F401 -- used by autogenerated code
|
||||||
|
)
|
||||||
|
from dump_things_service.exceptions import (
|
||||||
|
ConfigCollisionError,
|
||||||
|
ConfigError,
|
||||||
|
CurieResolutionError,
|
||||||
|
)
|
||||||
|
from dump_things_service.incoming import (
|
||||||
|
store_incoming_record, # noqa: F401 -- used by autogenerated code
|
||||||
|
)
|
||||||
from dump_things_service.instance_state import (
|
from dump_things_service.instance_state import (
|
||||||
InstanceState,
|
InstanceState,
|
||||||
InstanceStateCollectionInfo,
|
InstanceStateCollectionInfo,
|
||||||
get_record_dir_config,
|
|
||||||
get_instance_state,
|
get_instance_state,
|
||||||
|
get_record_dir_config,
|
||||||
get_schema_info,
|
get_schema_info,
|
||||||
record_dir_config_file_name,
|
record_dir_config_file_name,
|
||||||
)
|
)
|
||||||
from dump_things_service.converter import FormatConverter
|
|
||||||
from dump_things_service.exceptions import (
|
|
||||||
ConfigError,
|
|
||||||
ConfigCollisionError,
|
|
||||||
CurieResolutionError,
|
|
||||||
)
|
|
||||||
from dump_things_service.model import get_model_for_schema
|
from dump_things_service.model import get_model_for_schema
|
||||||
from dump_things_service.utils import (
|
from dump_things_service.utils import (
|
||||||
combine_ttl,
|
combine_ttl,
|
||||||
|
|
@ -67,16 +82,9 @@ from dump_things_service.utils import (
|
||||||
var_escape,
|
var_escape,
|
||||||
wrap_http_exception,
|
wrap_http_exception,
|
||||||
)
|
)
|
||||||
|
from dump_things_service.validate import (
|
||||||
|
validate_record, # noqa: F401 -- used by autogenerated code
|
||||||
# This following lines are required for dynamic endpoint generation
|
)
|
||||||
from typing import Annotated # noqa 401 -- used by autogenerated code
|
|
||||||
from fastapi import Body # noqa 401 -- used by autogenerated code
|
|
||||||
from dump_things_service.api_key import api_key_header_scheme # noqa 401 -- used by autogenerated code
|
|
||||||
from dump_things_service.curated import store_curated_record # noqa 401 -- used by autogenerated code
|
|
||||||
from dump_things_service.incoming import store_incoming_record # noqa 401 -- used by autogenerated code
|
|
||||||
from dump_things_service.validate import validate_record # noqa 401 -- used by autogenerated code
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
|
|
@ -191,7 +199,7 @@ def create_collection(
|
||||||
audit_path.mkdir(parents=True)
|
audit_path.mkdir(parents=True)
|
||||||
created_directories.append(audit_path)
|
created_directories.append(audit_path)
|
||||||
|
|
||||||
except ConfigError as e:
|
except ConfigError:
|
||||||
# Delete all directories that were created in this
|
# Delete all directories that were created in this
|
||||||
for directory in created_directories:
|
for directory in created_directories:
|
||||||
shutil.rmtree(directory)
|
shutil.rmtree(directory)
|
||||||
|
|
@ -222,7 +230,7 @@ def create_collection(
|
||||||
active_classes -= set(collection_configuration.ignore_classes)
|
active_classes -= set(collection_configuration.ignore_classes)
|
||||||
instance_state.collections[collection_name] = InstanceStateCollectionInfo(
|
instance_state.collections[collection_name] = InstanceStateCollectionInfo(
|
||||||
active_classes=active_classes,
|
active_classes=active_classes,
|
||||||
tag_info=dict(),
|
tag_info={},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a validator for the collection
|
# Create a validator for the collection
|
||||||
|
|
@ -301,7 +309,8 @@ def write_record_dir_config(
|
||||||
|
|
||||||
record_dir_config_file_path = path / record_dir_config_file_name
|
record_dir_config_file_path = path / record_dir_config_file_name
|
||||||
if not record_dir_config_file_path.exists():
|
if not record_dir_config_file_path.exists():
|
||||||
record_dir_config_file_path.write_text(f"""# RecordDir Config
|
record_dir_config_file_path.write_text(
|
||||||
|
f"""# RecordDir Config
|
||||||
type: records
|
type: records
|
||||||
version: 1
|
version: 1
|
||||||
schema: {schema}
|
schema: {schema}
|
||||||
|
|
@ -340,20 +349,20 @@ def check_record_dir_compatibility(
|
||||||
backend_config: RecordDirBackendConfig,
|
backend_config: RecordDirBackendConfig,
|
||||||
schema: str,
|
schema: str,
|
||||||
):
|
):
|
||||||
|
|
||||||
# Non-existing or empty record_dir-directories are compatible
|
# Non-existing or empty record_dir-directories are compatible
|
||||||
if not store_path.exists():
|
if not store_path.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
# A record_dir-directory is considered to be empty, if it contains no
|
# A record_dir-directory is considered to be empty, if it contains no
|
||||||
# files or only an record_dir-index file
|
# files or only an record_dir-index file
|
||||||
files_in_dir = tuple(map(lambda dir_entry: dir_entry.name, os.scandir(store_path)))
|
files_in_dir = tuple(dir_entry.name for dir_entry in os.scandir(store_path))
|
||||||
if files_in_dir in ((), (index_file_name,)):
|
if files_in_dir in ((), (index_file_name,)):
|
||||||
return
|
return
|
||||||
|
|
||||||
record_dir_config = get_record_dir_config(store_path)
|
record_dir_config = get_record_dir_config(store_path)
|
||||||
if record_dir_config.schema_location != schema:
|
if record_dir_config.schema_location != schema:
|
||||||
raise ConfigCollisionError(f"Existing backend uses a different schema: '{record_dir_config.schema_location}'")
|
msg = f"Existing backend uses a different schema: '{record_dir_config.schema_location}'"
|
||||||
|
raise ConfigCollisionError(msg)
|
||||||
|
|
||||||
stored_mapping_method = record_dir_config.idfx.value
|
stored_mapping_method = record_dir_config.idfx.value
|
||||||
if stored_mapping_method != backend_config.mapping_method:
|
if stored_mapping_method != backend_config.mapping_method:
|
||||||
|
|
@ -367,8 +376,8 @@ def check_sqlite_compatibility(
|
||||||
):
|
):
|
||||||
sqlite_db_path = Path(store_path / sqlite_db_filename)
|
sqlite_db_path = Path(store_path / sqlite_db_filename)
|
||||||
if not sqlite_db_path.exists():
|
if not sqlite_db_path.exists():
|
||||||
raise ConfigError('No sqlite database found in existing store')
|
msg = 'No sqlite database found in existing store'
|
||||||
return
|
raise ConfigError(msg)
|
||||||
|
|
||||||
|
|
||||||
def check_git_audit_compatibility(
|
def check_git_audit_compatibility(
|
||||||
|
|
@ -394,9 +403,11 @@ def check_git_audit_compatibility(
|
||||||
force_c_locale=True,
|
force_c_locale=True,
|
||||||
)
|
)
|
||||||
except CommandError as ce:
|
except CommandError as ce:
|
||||||
raise ConfigError(f'No git repository in gitaudit-path: {audit_path}') from ce
|
msg = f'No git repository in gitaudit-path: {audit_path}'
|
||||||
|
raise ConfigError(msg) from ce
|
||||||
if result.strip().lower() != 'true':
|
if result.strip().lower() != 'true':
|
||||||
raise ConfigError(f'No bare git repository in gitaudit-path: {audit_path}')
|
msg = f'No bare git repository in gitaudit-path: {audit_path}'
|
||||||
|
raise ConfigError(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -413,7 +424,7 @@ def create_endpoint(
|
||||||
app: FastAPI,
|
app: FastAPI,
|
||||||
):
|
):
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Creating %s-endpoints for collection: "%s"',
|
'Creating %s-endpoints for collection: "%s"',
|
||||||
operation_name,
|
operation_name,
|
||||||
collection_name,
|
collection_name,
|
||||||
)
|
)
|
||||||
|
|
@ -421,12 +432,16 @@ def create_endpoint(
|
||||||
instance_state.collections[collection_name].tag_info[tag_group] = tag_name
|
instance_state.collections[collection_name].tag_info[tag_group] = tag_name
|
||||||
|
|
||||||
# TODO: get schema_info from instance_state!?
|
# TODO: get schema_info from instance_state!?
|
||||||
model, classes, model_var_name = get_model_for_schema(collection_config.schema_location)
|
model, _classes, model_var_name = get_model_for_schema(
|
||||||
|
collection_config.schema_location
|
||||||
|
)
|
||||||
globals()[model_var_name] = model
|
globals()[model_var_name] = model
|
||||||
|
|
||||||
active_classes = instance_state.collections[collection_name].active_classes
|
active_classes = instance_state.collections[collection_name].active_classes
|
||||||
for class_name in active_classes:
|
for class_name in active_classes:
|
||||||
endpoint_name = f'_endpoint_{var_escape(collection_name)}_{operation_name}_{class_name}'
|
endpoint_name = (
|
||||||
|
f'_endpoint_{var_escape(collection_name)}_{operation_name}_{class_name}'
|
||||||
|
)
|
||||||
endpoint_source = template.format(
|
endpoint_source = template.format(
|
||||||
name=endpoint_name,
|
name=endpoint_name,
|
||||||
model_var_name=model_var_name,
|
model_var_name=model_var_name,
|
||||||
|
|
@ -435,7 +450,7 @@ def create_endpoint(
|
||||||
info=f"'{operation_name} {collection_name}/{class_name} objects'",
|
info=f"'{operation_name} {collection_name}/{class_name} objects'",
|
||||||
handler=handler,
|
handler=handler,
|
||||||
)
|
)
|
||||||
exec(endpoint_source, globals()) # noqa S102
|
exec(endpoint_source, globals()) # noqa: S102
|
||||||
|
|
||||||
# Create an API route for the endpoint
|
# Create an API route for the endpoint
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
|
|
@ -444,7 +459,7 @@ def create_endpoint(
|
||||||
methods=['POST'],
|
methods=['POST'],
|
||||||
name=f'{operation_name} "{class_name}" object (schema: {model.linkml_meta["id"]})',
|
name=f'{operation_name} "{class_name}" object (schema: {model.linkml_meta["id"]})',
|
||||||
response_model=None,
|
response_model=None,
|
||||||
tags=[tag_name]
|
tags=[tag_name],
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -468,10 +483,38 @@ def create_endpoints_for_collection(
|
||||||
tag_group,
|
tag_group,
|
||||||
tag_name,
|
tag_name,
|
||||||
) in (
|
) in (
|
||||||
('store', 'record', _endpoint_template, 'store_record', 'write', f'Write records to collection "{collection_name}"'),
|
(
|
||||||
('validate', 'validate/record', _endpoint_template, 'validate_record', 'validate', f'Validate records for collection "{collection_name}"'),
|
'store',
|
||||||
('curated', 'curated/record', _endpoint_curated_template, 'store_curated_record', 'curated_write', f'Curated area: store records in curated area of collection "{collection_name}"'),
|
'record',
|
||||||
('incoming', 'incoming/{label}/record', _endpoint_incoming_template, 'store_incoming_record', 'incoming_write', f'Incoming area: store records in incoming area "{{label}}" of collection "{collection_name}"'),
|
_endpoint_template,
|
||||||
|
'store_record',
|
||||||
|
'write',
|
||||||
|
f'Write records to collection "{collection_name}"',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'validate',
|
||||||
|
'validate/record',
|
||||||
|
_endpoint_template,
|
||||||
|
'validate_record',
|
||||||
|
'validate',
|
||||||
|
f'Validate records for collection "{collection_name}"',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'curated',
|
||||||
|
'curated/record',
|
||||||
|
_endpoint_curated_template,
|
||||||
|
'store_curated_record',
|
||||||
|
'curated_write',
|
||||||
|
f'Curated area: store records in curated area of collection "{collection_name}"',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'incoming',
|
||||||
|
'incoming/{label}/record',
|
||||||
|
_endpoint_incoming_template,
|
||||||
|
'store_incoming_record',
|
||||||
|
'incoming_write',
|
||||||
|
f'Incoming area: store records in incoming area "{{label}}" of collection "{collection_name}"',
|
||||||
|
),
|
||||||
):
|
):
|
||||||
create_endpoint(
|
create_endpoint(
|
||||||
operation_name=operation_name,
|
operation_name=operation_name,
|
||||||
|
|
@ -491,14 +534,13 @@ def delete_endpoints_for_collection(
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
):
|
):
|
||||||
|
|
||||||
active_classes = instance_state.collections[collection_name].active_classes
|
active_classes = instance_state.collections[collection_name].active_classes
|
||||||
|
|
||||||
for operation_path in (
|
for operation_path in (
|
||||||
'record',
|
'record',
|
||||||
'validate/record',
|
'validate/record',
|
||||||
'curated/record',
|
'curated/record',
|
||||||
'incoming/{label}/record'
|
'incoming/{label}/record',
|
||||||
):
|
):
|
||||||
delete_endpoint(
|
delete_endpoint(
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
|
|
@ -516,10 +558,10 @@ def delete_endpoint(
|
||||||
):
|
):
|
||||||
from fastapi.routing import _IncludedRouter
|
from fastapi.routing import _IncludedRouter
|
||||||
|
|
||||||
remove_paths_set = set(
|
remove_paths_set = {
|
||||||
f'/{collection_name}/{operation_path}/{class_name}'
|
f'/{collection_name}/{operation_path}/{class_name}'
|
||||||
for class_name in active_classes
|
for class_name in active_classes
|
||||||
)
|
}
|
||||||
|
|
||||||
remove_indices = [
|
remove_indices = [
|
||||||
index
|
index
|
||||||
|
|
@ -584,18 +626,32 @@ def store_record(
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_format == Format.ttl:
|
if input_format == Format.ttl:
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Conversion error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Conversion error',
|
||||||
|
):
|
||||||
json_object = FormatConverter(
|
json_object = FormatConverter(
|
||||||
abstract_config.collections[collection].schema_location,
|
abstract_config.collections[collection].schema_location,
|
||||||
input_format=Format.ttl,
|
input_format=Format.ttl,
|
||||||
output_format=Format.json,
|
output_format=Format.json,
|
||||||
).convert(data, class_name)
|
).convert(data, class_name)
|
||||||
with wrap_http_exception(ValidationError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
record = TypeAdapter(getattr(model, class_name)).validate_python(json_object)
|
ValidationError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
|
record = TypeAdapter(getattr(model, class_name)).validate_python(
|
||||||
|
json_object
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
record = data
|
record = data
|
||||||
|
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
instance_state.validators[collection].validate(record)
|
instance_state.validators[collection].validate(record)
|
||||||
|
|
||||||
with wrap_http_exception(CurieResolutionError):
|
with wrap_http_exception(CurieResolutionError):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from pathlib import (
|
||||||
Path,
|
Path,
|
||||||
PurePosixPath,
|
PurePosixPath,
|
||||||
)
|
)
|
||||||
from typing import Literal
|
from typing import Annotated, Literal
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
|
|
@ -22,19 +22,19 @@ from dump_things_service import (
|
||||||
reserved_collection_names,
|
reserved_collection_names,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
Configuration,
|
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
|
Configuration,
|
||||||
|
get_config,
|
||||||
|
get_token_permissions,
|
||||||
store_config,
|
store_config,
|
||||||
get_config, get_token_permissions,
|
|
||||||
)
|
)
|
||||||
from dump_things_service.admin import authenticate_admin
|
from dump_things_service.admin import authenticate_admin
|
||||||
from dump_things_service.api_key import api_key_header_scheme
|
from dump_things_service.api_key import api_key_header_scheme
|
||||||
from dump_things_service.instance_state import get_instance_state, InstanceState
|
|
||||||
from dump_things_service.manifest import manifest_configuration
|
|
||||||
from dump_things_service.exceptions import ConfigError
|
from dump_things_service.exceptions import ConfigError
|
||||||
|
from dump_things_service.instance_state import InstanceState, get_instance_state
|
||||||
|
from dump_things_service.manifest import manifest_configuration
|
||||||
from dump_things_service.utils import wrap_http_exception
|
from dump_things_service.utils import wrap_http_exception
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -71,7 +71,7 @@ class CollectionRequest(CollectionConfig):
|
||||||
async def create_collection(
|
async def create_collection(
|
||||||
response: Response,
|
response: Response,
|
||||||
body: CollectionRequest,
|
body: CollectionRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
await create_or_replace_collection(body, api_key, allow_replace=False)
|
await create_or_replace_collection(body, api_key, allow_replace=False)
|
||||||
response.headers['Location'] = f'/collections/{quote(body.name)}'
|
response.headers['Location'] = f'/collections/{quote(body.name)}'
|
||||||
|
|
@ -86,7 +86,7 @@ async def create_collection(
|
||||||
async def replace_collection(
|
async def replace_collection(
|
||||||
response: Response,
|
response: Response,
|
||||||
body: CollectionRequest,
|
body: CollectionRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
await create_or_replace_collection(body, api_key, allow_replace=True)
|
await create_or_replace_collection(body, api_key, allow_replace=True)
|
||||||
response.headers['Location'] = f'/collections/{quote(body.name)}'
|
response.headers['Location'] = f'/collections/{quote(body.name)}'
|
||||||
|
|
@ -97,7 +97,6 @@ async def create_or_replace_collection(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
allow_replace: bool,
|
allow_replace: bool,
|
||||||
):
|
):
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -165,9 +164,8 @@ async def create_or_replace_collection(
|
||||||
name='Get existing collections',
|
name='Get existing collections',
|
||||||
)
|
)
|
||||||
async def get_collections(
|
async def get_collections(
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> list[CollectionRequest]:
|
) -> list[CollectionRequest]:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -177,7 +175,7 @@ async def get_collections(
|
||||||
CollectionRequest(
|
CollectionRequest(
|
||||||
**{
|
**{
|
||||||
'name': collection_name,
|
'name': collection_name,
|
||||||
**collection_info.model_dump(mode='json', by_alias=True)
|
**collection_info.model_dump(mode='json', by_alias=True),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for collection_name, collection_info in abstract_config.collections.items()
|
for collection_name, collection_info in abstract_config.collections.items()
|
||||||
|
|
@ -191,9 +189,8 @@ async def get_collections(
|
||||||
)
|
)
|
||||||
async def get_collection_with_name(
|
async def get_collection_with_name(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> CollectionConfig:
|
) -> CollectionConfig:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -215,9 +212,8 @@ async def get_collection_with_name(
|
||||||
)
|
)
|
||||||
async def delete_collection(
|
async def delete_collection(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -252,7 +248,9 @@ def ensure_unique_directory(
|
||||||
abs_existing_dir = (instance_state.store_path / Path(existing_dir)).absolute()
|
abs_existing_dir = (instance_state.store_path / Path(existing_dir)).absolute()
|
||||||
for collection_name, collection_config in abstract_config.collections.items():
|
for collection_name, collection_config in abstract_config.collections.items():
|
||||||
for collection_dir in collection_config.curated, collection_config.incoming:
|
for collection_dir in collection_config.curated, collection_config.incoming:
|
||||||
abs_collection_dir = (instance_state.store_path / Path(collection_dir)).absolute()
|
abs_collection_dir = (
|
||||||
|
instance_state.store_path / Path(collection_dir)
|
||||||
|
).absolute()
|
||||||
if abs_collection_dir == abs_existing_dir:
|
if abs_collection_dir == abs_existing_dir:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_409_CONFLICT,
|
status_code=HTTP_409_CONFLICT,
|
||||||
|
|
@ -273,7 +271,7 @@ def validate_incoming_paths(
|
||||||
detail = (
|
detail = (
|
||||||
f"Cannot add collection '{collection_request.name}' without "
|
f"Cannot add collection '{collection_request.name}' without "
|
||||||
f"`incoming` path, because at least token '{token_name}' "
|
f"`incoming` path, because at least token '{token_name}' "
|
||||||
f" has write access to the collection"
|
f' has write access to the collection'
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_406_NOT_ACCEPTABLE,
|
status_code=HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from collections.abc import Iterable
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
@ -16,12 +16,16 @@ from dump_things_service.backends.sqlite import _SQLiteBackend
|
||||||
from dump_things_service.exceptions import CurieResolutionError
|
from dump_things_service.exceptions import CurieResolutionError
|
||||||
from dump_things_service.instance_state import create_instance_state
|
from dump_things_service.instance_state import create_instance_state
|
||||||
from dump_things_service.manifest import manifest_configuration
|
from dump_things_service.manifest import manifest_configuration
|
||||||
from dump_things_service.store.model_store import _ModelStore
|
|
||||||
from dump_things_service.utils import (
|
from dump_things_service.utils import (
|
||||||
create_token_store,
|
create_token_store,
|
||||||
get_on_disk_labels,
|
get_on_disk_labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from dump_things_service.store.model_store import _ModelStore
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Check pids for resolvability',
|
prog='Check pids for resolvability',
|
||||||
description='This command checks for pids that are in CURIE format and '
|
description='This command checks for pids that are in CURIE format and '
|
||||||
|
|
@ -33,19 +37,7 @@ parser.add_argument(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def show_backend(model_store: _ModelStore):
|
def check_pids_in_stores(stores: Iterable[_ModelStore]) -> int:
|
||||||
backend = model_store.backend
|
|
||||||
if isinstance(backend, _SchemaTypeLayer):
|
|
||||||
backend = backend.backend
|
|
||||||
if isinstance(backend, _SQLiteBackend):
|
|
||||||
print(f'Checking: {backend.db_path}', file=sys.stderr)
|
|
||||||
else:
|
|
||||||
print(f'Checking: {backend.root}', file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def check_pids_in_stores(
|
|
||||||
stores: Iterable[_ModelStore]
|
|
||||||
) -> int:
|
|
||||||
result = 0
|
result = 0
|
||||||
for store in stores:
|
for store in stores:
|
||||||
print('checking', store.get_uri(), file=sys.stderr)
|
print('checking', store.get_uri(), file=sys.stderr)
|
||||||
|
|
@ -55,8 +47,6 @@ def check_pids_in_stores(
|
||||||
store.pid_to_iri(pid)
|
store.pid_to_iri(pid)
|
||||||
except CurieResolutionError:
|
except CurieResolutionError:
|
||||||
result += 1
|
result += 1
|
||||||
print(pid, store.get_uri())
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,7 +84,7 @@ def check_pids(
|
||||||
abstract_config,
|
abstract_config,
|
||||||
instance_state,
|
instance_state,
|
||||||
collection,
|
collection,
|
||||||
instance_state.store_path / collection_info.incoming / label
|
instance_state.store_path / collection_info.incoming / label,
|
||||||
)
|
)
|
||||||
for label in all_labels
|
for label in all_labels
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from argparse import ArgumentParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from dump_things_service.abstract_config import get_backend_and_extension
|
||||||
from dump_things_service.backends.record_dir import (
|
from dump_things_service.backends.record_dir import (
|
||||||
RecordDirStore,
|
RecordDirStore,
|
||||||
_RecordDirStore,
|
_RecordDirStore,
|
||||||
|
|
@ -17,7 +18,6 @@ from dump_things_service.backends.sqlite import (
|
||||||
from dump_things_service.backends.sqlite import (
|
from dump_things_service.backends.sqlite import (
|
||||||
record_file_name as sqlite_record_file_name,
|
record_file_name as sqlite_record_file_name,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import get_backend_and_extension
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dump_things_service.backends import StorageBackend
|
from dump_things_service.backends import StorageBackend
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,16 @@ import yaml
|
||||||
from linkml_runtime.utils.schemaview import SchemaView
|
from linkml_runtime.utils.schemaview import SchemaView
|
||||||
|
|
||||||
# Patch linkml
|
# Patch linkml
|
||||||
from dump_things_service.patches import enabled # noqa F401 -- patches LinkML
|
from dump_things_service.patches import enabled # noqa: F401 -- patches LinkML
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Create a static schema with all imported schemas integrated',
|
prog='Create a static schema with all imported schemas integrated',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument('schema', help='File containing a schema definition')
|
||||||
'schema',
|
|
||||||
help='File containing a schema definition'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_uris_for_elements(
|
def update_uris_for_elements(
|
||||||
all_elements: dict,
|
all_elements: dict, attribute_name: str, prefix_index: dict
|
||||||
attribute_name: str,
|
|
||||||
prefix_index: dict
|
|
||||||
):
|
):
|
||||||
for name, info in all_elements.items():
|
for name, info in all_elements.items():
|
||||||
uri = getattr(info, attribute_name)
|
uri = getattr(info, attribute_name)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from argparse import ArgumentParser
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Download a complete configuration of a running service',
|
prog='Download a complete configuration of a running service',
|
||||||
description='Read a configuration from dump-things endpoints and create a '
|
description='Read a configuration from dump-things endpoints and create a '
|
||||||
|
|
@ -23,22 +22,24 @@ parser.add_argument(
|
||||||
help='The base URL of the server API.',
|
help='The base URL of the server API.',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--entities', '-e',
|
'--entities',
|
||||||
|
'-e',
|
||||||
action='append',
|
action='append',
|
||||||
choices=['admin_tokens', 'collections', 'tokens'],
|
choices=['admin_tokens', 'collections', 'tokens'],
|
||||||
help='Specify for which entities the configuration should be downloaded. '
|
help='Specify for which entities the configuration should be downloaded. '
|
||||||
' Possible values are `admin_tokens`, `collections`, or `tokens` '
|
' Possible values are `admin_tokens`, `collections`, or `tokens` '
|
||||||
'(repeat to download configuration for more than one entity). If this '
|
'(repeat to download configuration for more than one entity). If this '
|
||||||
'option is not provided, configurations for all entities will be '
|
'option is not provided, configurations for all entities will be '
|
||||||
'downloaded.'
|
'downloaded.',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--format', '-f',
|
'--format',
|
||||||
|
'-f',
|
||||||
nargs='?',
|
nargs='?',
|
||||||
default='yaml',
|
default='yaml',
|
||||||
choices=['json', 'yaml'],
|
choices=['json', 'yaml'],
|
||||||
help='Specify the format of the output. Possible values are `json` '
|
help='Specify the format of the output. Possible values are `json` '
|
||||||
'and `yaml` (the default is `yaml`).'
|
'and `yaml` (the default is `yaml`).',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +91,6 @@ def get_configuration(
|
||||||
admin_token: str,
|
admin_token: str,
|
||||||
entities: list[str],
|
entities: list[str],
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
if 'collections' in entities:
|
if 'collections' in entities:
|
||||||
|
|
@ -115,7 +115,8 @@ def list_to_dict_on_key(
|
||||||
) -> dict:
|
) -> dict:
|
||||||
return {
|
return {
|
||||||
element[extract_key]: {
|
element[extract_key]: {
|
||||||
element_key: value for element_key, value in element.items()
|
element_key: value
|
||||||
|
for element_key, value in element.items()
|
||||||
if element_key != extract_key
|
if element_key != extract_key
|
||||||
}
|
}
|
||||||
for element in elements
|
for element in elements
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,12 @@ from pathlib import Path
|
||||||
|
|
||||||
from dump_things_service.audit.gitaudit import GitAuditBackend
|
from dump_things_service.audit.gitaudit import GitAuditBackend
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Rebuild the index of a `gitaudit`-database',
|
prog='Rebuild the index of a `gitaudit`-database',
|
||||||
description='This command rebuilds the index of a `gitaudit`-database.'
|
description='This command rebuilds the index of a `gitaudit`-database.',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'audit_store',
|
'audit_store', help='The directory in which the `gitaudit`-database is located.'
|
||||||
help='The directory in which the `gitaudit`-database is located.'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from pathlib import Path
|
||||||
|
|
||||||
from dump_things_service.audit.gitaudit import GitAuditBackend
|
from dump_things_service.audit.gitaudit import GitAuditBackend
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Report audit information for a PID',
|
prog='Report audit information for a PID',
|
||||||
description='Report the audit information that was stored for a specific '
|
description='Report the audit information that was stored for a specific '
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from argparse import ArgumentParser
|
||||||
|
|
||||||
from dump_things_service.abstract_config import hash_token_representation
|
from dump_things_service.abstract_config import hash_token_representation
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Hash a plain text token to create a hashed token in a dump-things server',
|
prog='Hash a plain text token to create a hashed token in a dump-things server',
|
||||||
description='Hash a token and print the calculated hash value. The hash value '
|
description='Hash a token and print the calculated hash value. The hash value '
|
||||||
|
|
@ -23,12 +22,13 @@ def main():
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
token = arguments.token.strip()
|
token = arguments.token.strip()
|
||||||
if any(map(lambda s: s.isspace(), token)):
|
if any(s.isspace() for s in token):
|
||||||
print('Whitespace are not allowed in token', file=sys.stderr, flush=True)
|
print('Whitespace are not allowed in token', file=sys.stderr, flush=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print(hash_token_representation(token))
|
print(hash_token_representation(token))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from dump_things_service import config_file_name
|
from dump_things_service import config_file_name
|
||||||
from dump_things_service.backends.record_dir_index import RecordDirIndex
|
|
||||||
from dump_things_service.abstract_config import RecordDirConfigFileContent
|
from dump_things_service.abstract_config import RecordDirConfigFileContent
|
||||||
|
from dump_things_service.backends.record_dir_index import RecordDirIndex
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Rebuild the index of a `record_dir`-store',
|
prog='Rebuild the index of a `record_dir`-store',
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import yaml
|
||||||
|
|
||||||
from dump_things_service.instance_state import get_record_dir_config
|
from dump_things_service.instance_state import get_record_dir_config
|
||||||
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
parser = ArgumentParser(
|
||||||
prog='Establish a configuration in a running service',
|
prog='Establish a configuration in a running service',
|
||||||
description='Read a configuration from a dump-things configuration-file '
|
description='Read a configuration from a dump-things configuration-file '
|
||||||
|
|
@ -27,12 +26,13 @@ parser.add_argument(
|
||||||
help='The path to the config file',
|
help='The path to the config file',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--format', '-f',
|
'--format',
|
||||||
|
'-f',
|
||||||
nargs='?',
|
nargs='?',
|
||||||
choices=['json', 'yaml'],
|
choices=['json', 'yaml'],
|
||||||
help='Specify the format of the input file. Possible values are `json` '
|
help='Specify the format of the input file. Possible values are `json` '
|
||||||
'and `yaml`. If this option is given, the '
|
'and `yaml`. If this option is given, the '
|
||||||
'suffix of the configuration file is ignored.'
|
'suffix of the configuration file is ignored.',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--send-to',
|
'--send-to',
|
||||||
|
|
@ -85,8 +85,7 @@ def main():
|
||||||
|
|
||||||
if arguments.old_format:
|
if arguments.old_format:
|
||||||
configuration = convert_config_1_to_config_2(configuration, arguments.store)
|
configuration = convert_config_1_to_config_2(configuration, arguments.store)
|
||||||
else:
|
elif arguments.store:
|
||||||
if arguments.store:
|
|
||||||
print(
|
print(
|
||||||
'Warning: ignoring `--store` option because `--old-format` '
|
'Warning: ignoring `--store` option because `--old-format` '
|
||||||
'is not provided.',
|
'is not provided.',
|
||||||
|
|
@ -94,7 +93,9 @@ def main():
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert configuration['type'] == 'collections', '`type: collections` missing in config-file'
|
assert configuration['type'] == 'collections', (
|
||||||
|
'`type: collections` missing in config-file'
|
||||||
|
)
|
||||||
assert configuration['version'] == 2, '`version: 2` missing in config-file'
|
assert configuration['version'] == 2, '`version: 2` missing in config-file'
|
||||||
|
|
||||||
if arguments.send_to:
|
if arguments.send_to:
|
||||||
|
|
@ -110,9 +111,7 @@ def main():
|
||||||
try:
|
try:
|
||||||
establish_configuration(
|
establish_configuration(
|
||||||
configuration,
|
configuration,
|
||||||
arguments.send_to[:-1]
|
arguments.send_to.removesuffix('/'),
|
||||||
if arguments.send_to.endswith('/')
|
|
||||||
else arguments.send_to,
|
|
||||||
admin_token,
|
admin_token,
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -138,7 +137,6 @@ def convert_config_1_to_config_2(
|
||||||
old_configuration: dict,
|
old_configuration: dict,
|
||||||
store_path: str | Path,
|
store_path: str | Path,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
old_version = old_configuration.get('version')
|
old_version = old_configuration.get('version')
|
||||||
if old_version != 1:
|
if old_version != 1:
|
||||||
msg = f'`Unknown old configuration format: {old_version}'
|
msg = f'`Unknown old configuration format: {old_version}'
|
||||||
|
|
@ -154,9 +152,11 @@ def convert_config_1_to_config_2(
|
||||||
f'token_{next(counter)}': {
|
f'token_{next(counter)}': {
|
||||||
**old_token_config.copy(),
|
**old_token_config.copy(),
|
||||||
'representation': token_representation,
|
'representation': token_representation,
|
||||||
'hashed': False
|
'hashed': False,
|
||||||
}
|
}
|
||||||
for token_representation, old_token_config in old_configuration['tokens'].items()
|
for token_representation, old_token_config in old_configuration[
|
||||||
|
'tokens'
|
||||||
|
].items()
|
||||||
}
|
}
|
||||||
|
|
||||||
old_to_new_token_mapping = {
|
old_to_new_token_mapping = {
|
||||||
|
|
@ -165,7 +165,7 @@ def convert_config_1_to_config_2(
|
||||||
}
|
}
|
||||||
|
|
||||||
store_path = Path(store_path) if store_path else None
|
store_path = Path(store_path) if store_path else None
|
||||||
for collection_name, collection_config in old_configuration['collections'].items():
|
for collection_config in old_configuration['collections'].values():
|
||||||
backend = collection_config.get('backend')
|
backend = collection_config.get('backend')
|
||||||
if backend and backend['type'].startswith('sqlite'):
|
if backend and backend['type'].startswith('sqlite'):
|
||||||
collection_config['schema'] = backend['schema']
|
collection_config['schema'] = backend['schema']
|
||||||
|
|
@ -174,23 +174,26 @@ def convert_config_1_to_config_2(
|
||||||
if store_path is None:
|
if store_path is None:
|
||||||
msg = '--store <path> has to be provided to convert collection with record_dir-backends'
|
msg = '--store <path> has to be provided to convert collection with record_dir-backends'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
record_dir_config = get_record_dir_config(store_path / collection_config['curated'])
|
record_dir_config = get_record_dir_config(
|
||||||
|
store_path / collection_config['curated']
|
||||||
|
)
|
||||||
collection_config['schema'] = record_dir_config.schema_location
|
collection_config['schema'] = record_dir_config.schema_location
|
||||||
backend = {
|
backend = {
|
||||||
'type': 'record_dir+stl' if not backend else backend['type'],
|
'type': 'record_dir+stl' if not backend else backend['type'],
|
||||||
'mapping_method': record_dir_config.idfx.value
|
'mapping_method': record_dir_config.idfx.value,
|
||||||
}
|
}
|
||||||
collection_config['backend'] = backend
|
collection_config['backend'] = backend
|
||||||
collection_config['default_token'] = old_to_new_token_mapping[collection_config['default_token']]
|
collection_config['default_token'] = old_to_new_token_mapping[
|
||||||
|
collection_config['default_token']
|
||||||
|
]
|
||||||
|
|
||||||
new_configuration = {
|
return {
|
||||||
'type': 'collections',
|
'type': 'collections',
|
||||||
'version': 2,
|
'version': 2,
|
||||||
'tokens': new_tokens_dict,
|
'tokens': new_tokens_dict,
|
||||||
'collections': old_configuration['collections'],
|
'collections': old_configuration['collections'],
|
||||||
'admin_tokens': {},
|
'admin_tokens': {},
|
||||||
}
|
}
|
||||||
return new_configuration
|
|
||||||
|
|
||||||
|
|
||||||
def establish_configuration(
|
def establish_configuration(
|
||||||
|
|
@ -264,7 +267,11 @@ def _post_data(
|
||||||
content_class: str,
|
content_class: str,
|
||||||
content_name: str,
|
content_name: str,
|
||||||
):
|
):
|
||||||
result = requests.put(url, headers={'x-dumpthings-token': token}, json=data,)
|
result = requests.put(
|
||||||
|
url,
|
||||||
|
headers={'x-dumpthings-token': token},
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
if result.status_code >= 300:
|
if result.status_code >= 300:
|
||||||
msg = f'Error uploading {content_class}: {content_name}: {result.text}'
|
msg = f'Error uploading {content_class}: {content_name}: {result.text}'
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ from json import loads as json_loads
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from linkml_runtime import SchemaView
|
|
||||||
from linkml.utils.datautils import (
|
from linkml.utils.datautils import (
|
||||||
get_dumper,
|
get_dumper,
|
||||||
get_loader,
|
get_loader,
|
||||||
|
|
@ -29,10 +27,11 @@ from dump_things_service.model import (
|
||||||
)
|
)
|
||||||
from dump_things_service.utils import cleaned_json
|
from dump_things_service.utils import cleaned_json
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
|
from linkml_runtime import SchemaView
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from dump_things_service.backends import RecordInfo
|
from dump_things_service.backends import RecordInfo
|
||||||
|
|
@ -47,10 +46,7 @@ class TypeValidator:
|
||||||
self.type_name = type_name
|
self.type_name = type_name
|
||||||
self.matcher = None if pattern is None else re.compile(pattern)
|
self.matcher = None if pattern is None else re.compile(pattern)
|
||||||
|
|
||||||
def validate(
|
def validate(self, value: str) -> str:
|
||||||
self,
|
|
||||||
value: str
|
|
||||||
) -> str:
|
|
||||||
if self.matcher:
|
if self.matcher:
|
||||||
match = self.matcher.match(value)
|
match = self.matcher.match(value)
|
||||||
if not match:
|
if not match:
|
||||||
|
|
@ -238,10 +234,7 @@ def _convert_format(
|
||||||
)
|
)
|
||||||
except Exception as e: # BLE001
|
except Exception as e: # BLE001
|
||||||
if load_only:
|
if load_only:
|
||||||
msg = (
|
msg = f'Validation error for instance of {target_class}: {e}, data:\n{data}'
|
||||||
f'Validation error for instance of {target_class}: {e}, '
|
|
||||||
f'data:\n{data}'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
msg = (
|
msg = (
|
||||||
f'Conversion {input_format} -> {output_format}. Error: {e}, '
|
f'Conversion {input_format} -> {output_format}. Error: {e}, '
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from itertools import count
|
from typing import TYPE_CHECKING, Annotated
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
FastAPI,
|
|
||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
from fastapi_pagination import (
|
from fastapi_pagination import (
|
||||||
|
|
@ -19,14 +17,13 @@ from fastapi_pagination import (
|
||||||
from dump_things_service import (
|
from dump_things_service import (
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
HTTP_422_UNPROCESSABLE_CONTENT, abstract_config,
|
HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
check_collection,
|
check_collection,
|
||||||
read_config,
|
read_config,
|
||||||
)
|
)
|
||||||
from dump_things_service.api_key import api_key_header_scheme
|
from dump_things_service.api_key import api_key_header_scheme
|
||||||
from dump_things_service.auth import AuthenticationInfo
|
|
||||||
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
||||||
from dump_things_service.exceptions import CurieResolutionError
|
from dump_things_service.exceptions import CurieResolutionError
|
||||||
from dump_things_service.instance_state import get_instance_state
|
from dump_things_service.instance_state import get_instance_state
|
||||||
|
|
@ -41,6 +38,7 @@ from dump_things_service.utils import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from dump_things_service.auth import AuthenticationInfo
|
||||||
from dump_things_service.backends import StorageBackend
|
from dump_things_service.backends import StorageBackend
|
||||||
from dump_things_service.lazy_list import LazyList
|
from dump_things_service.lazy_list import LazyList
|
||||||
from dump_things_service.store.model_store import _ModelStore
|
from dump_things_service.store.model_store import _ModelStore
|
||||||
|
|
@ -76,7 +74,7 @@ add_pagination(router)
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/curated/records/{class_name}',
|
'/{collection}/curated/records/{class_name}',
|
||||||
tags=['Curated area: read records'],
|
tags=['Curated area: read records'],
|
||||||
name='Read all records of the given class from the curated area'
|
name='Read all records of the given class from the curated area',
|
||||||
)
|
)
|
||||||
async def read_curated_records_of_type(
|
async def read_curated_records_of_type(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -104,7 +102,7 @@ async def read_curated_records_of_type(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/curated/records/p/{class_name}',
|
'/{collection}/curated/records/p/{class_name}',
|
||||||
tags=['Curated area: read records'],
|
tags=['Curated area: read records'],
|
||||||
name='Read all records of the given class from the curated area with pagination'
|
name='Read all records of the given class from the curated area with pagination',
|
||||||
)
|
)
|
||||||
async def read_curated_records_of_type_paginated(
|
async def read_curated_records_of_type_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -112,7 +110,6 @@ async def read_curated_records_of_type_paginated(
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: str | None = Depends(api_key_header_scheme),
|
||||||
) -> Page[dict]:
|
) -> Page[dict]:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
if class_name not in instance_state.collections[collection].active_classes:
|
if class_name not in instance_state.collections[collection].active_classes:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -133,7 +130,7 @@ async def read_curated_records_of_type_paginated(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/curated/records/',
|
'/{collection}/curated/records/',
|
||||||
tags=['Curated area: read records'],
|
tags=['Curated area: read records'],
|
||||||
name='Read all records from the curated area'
|
name='Read all records from the curated area',
|
||||||
)
|
)
|
||||||
async def read_curated_all_records(
|
async def read_curated_all_records(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -153,7 +150,7 @@ async def read_curated_all_records(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/curated/records/p/',
|
'/{collection}/curated/records/p/',
|
||||||
tags=['Curated area: read records'],
|
tags=['Curated area: read records'],
|
||||||
name='Read all records from the curated area with pagination'
|
name='Read all records from the curated area with pagination',
|
||||||
)
|
)
|
||||||
async def read_curated_all_records_paginated(
|
async def read_curated_all_records_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -174,12 +171,12 @@ async def read_curated_all_records_paginated(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/curated/record',
|
'/{collection}/curated/record',
|
||||||
tags=['Curated area: read records'],
|
tags=['Curated area: read records'],
|
||||||
name='Read the record with the given pid from the curated area'
|
name='Read the record with the given pid from the curated area',
|
||||||
)
|
)
|
||||||
async def read_curated_record_with_pid(
|
async def read_curated_record_with_pid(
|
||||||
collection: str,
|
collection: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return await _read_curated_records(
|
return await _read_curated_records(
|
||||||
collection=collection,
|
collection=collection,
|
||||||
|
|
@ -192,12 +189,12 @@ async def read_curated_record_with_pid(
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'/{collection}/curated/record',
|
'/{collection}/curated/record',
|
||||||
tags=['Curated area: delete records'],
|
tags=['Curated area: delete records'],
|
||||||
name='Delete the record with the given pid from the curated area of the given collection'
|
name='Delete the record with the given pid from the curated area of the given collection',
|
||||||
)
|
)
|
||||||
async def delete_curated_record_with_pid(
|
async def delete_curated_record_with_pid(
|
||||||
collection: str,
|
collection: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return await _delete_curated_record(
|
return await _delete_curated_record(
|
||||||
collection=collection,
|
collection=collection,
|
||||||
|
|
@ -214,7 +211,6 @@ async def _read_curated_records(
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
upper_bound: int | None = 1000,
|
upper_bound: int | None = 1000,
|
||||||
) -> LazyList | dict | None:
|
) -> LazyList | dict | None:
|
||||||
|
|
||||||
model_store, backend, _ = _get_store_and_backend(collection, api_key)
|
model_store, backend, _ = _get_store_and_backend(collection, api_key)
|
||||||
|
|
||||||
if pid:
|
if pid:
|
||||||
|
|
@ -232,9 +228,7 @@ async def _read_curated_records(
|
||||||
len(result_list),
|
len(result_list),
|
||||||
upper_bound,
|
upper_bound,
|
||||||
collection,
|
collection,
|
||||||
f'/curated/records/p/{class_name}'
|
f'/curated/records/p/{class_name}' if class_name else '/curated/records/p/',
|
||||||
if class_name
|
|
||||||
else '/curated/records/p/',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return ModifierList(
|
return ModifierList(
|
||||||
|
|
@ -264,7 +258,6 @@ def _get_store_and_backend(
|
||||||
collection: str,
|
collection: str,
|
||||||
plain_token: str | None,
|
plain_token: str | None,
|
||||||
) -> tuple[_ModelStore, StorageBackend, AuthenticationInfo]:
|
) -> tuple[_ModelStore, StorageBackend, AuthenticationInfo]:
|
||||||
|
|
||||||
# A token is required
|
# A token is required
|
||||||
if plain_token is None:
|
if plain_token is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -303,7 +296,11 @@ def store_curated_record(
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: str | None = Depends(api_key_header_scheme),
|
||||||
):
|
):
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
instance_state.validators[collection].validate(data)
|
instance_state.validators[collection].validate(data)
|
||||||
|
|
||||||
pid = data.pid
|
pid = data.pid
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Annotated
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
|
|
@ -22,8 +22,8 @@ from dump_things_service import (
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
check_collection,
|
check_collection,
|
||||||
check_label,
|
check_label,
|
||||||
get_config_labels,
|
|
||||||
get_config,
|
get_config,
|
||||||
|
get_config_labels,
|
||||||
)
|
)
|
||||||
from dump_things_service.api_key import api_key_header_scheme
|
from dump_things_service.api_key import api_key_header_scheme
|
||||||
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer
|
||||||
|
|
@ -55,25 +55,27 @@ add_pagination(router)
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/',
|
'/{collection}/incoming/',
|
||||||
tags=['Incoming area: read labels'],
|
tags=['Incoming area: read labels'],
|
||||||
name='Get all incoming labels for the given collection'
|
name='Get all incoming labels for the given collection',
|
||||||
)
|
)
|
||||||
async def incoming_read_labels(
|
async def incoming_read_labels(
|
||||||
collection: str,
|
collection: str,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: Annotated[str | None, Depends(api_key_header_scheme)],
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
# Authorize api_key
|
# Authorize api_key
|
||||||
await authorize_zones(collection, api_key)
|
await authorize_zones(collection, api_key)
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
configured_labels = get_config_labels(get_config(), collection)
|
configured_labels = get_config_labels(get_config(), collection)
|
||||||
on_disk_labels = get_on_disk_labels(instance_state.store_path, get_config(), collection)
|
on_disk_labels = get_on_disk_labels(
|
||||||
|
instance_state.store_path, get_config(), collection
|
||||||
|
)
|
||||||
return list(configured_labels.union(on_disk_labels))
|
return list(configured_labels.union(on_disk_labels))
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/{label}/records/{class_name}',
|
'/{collection}/incoming/{label}/records/{class_name}',
|
||||||
tags=['Incoming area: read records'],
|
tags=['Incoming area: read records'],
|
||||||
name='Read all records of the given class from the given incoming area'
|
name='Read all records of the given class from the given incoming area',
|
||||||
)
|
)
|
||||||
async def incoming_read_records_of_type(
|
async def incoming_read_records_of_type(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -103,7 +105,7 @@ async def incoming_read_records_of_type(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/{label}/records/p/{class_name}',
|
'/{collection}/incoming/{label}/records/p/{class_name}',
|
||||||
tags=['Incoming area: read records'],
|
tags=['Incoming area: read records'],
|
||||||
name='Read all records of the given class from the given incoming area with pagination'
|
name='Read all records of the given class from the given incoming area with pagination',
|
||||||
)
|
)
|
||||||
async def incoming_read_records_of_type_paginated(
|
async def incoming_read_records_of_type_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -112,7 +114,6 @@ async def incoming_read_records_of_type_paginated(
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: str | None = Depends(api_key_header_scheme),
|
||||||
) -> Page[dict]:
|
) -> Page[dict]:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
if class_name not in instance_state.collections[collection].active_classes:
|
if class_name not in instance_state.collections[collection].active_classes:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -134,7 +135,7 @@ async def incoming_read_records_of_type_paginated(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/{label}/records/',
|
'/{collection}/incoming/{label}/records/',
|
||||||
tags=['Incoming area: read records'],
|
tags=['Incoming area: read records'],
|
||||||
name='Read all records from the given incoming area'
|
name='Read all records from the given incoming area',
|
||||||
)
|
)
|
||||||
async def incoming_read_all_records(
|
async def incoming_read_all_records(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -156,7 +157,7 @@ async def incoming_read_all_records(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/{label}/records/p/',
|
'/{collection}/incoming/{label}/records/p/',
|
||||||
tags=['Incoming area: read records'],
|
tags=['Incoming area: read records'],
|
||||||
name='Read all records from the given incoming area with pagination'
|
name='Read all records from the given incoming area with pagination',
|
||||||
)
|
)
|
||||||
async def incoming_read_all_records_paginated(
|
async def incoming_read_all_records_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
|
|
@ -179,13 +180,13 @@ async def incoming_read_all_records_paginated(
|
||||||
@router.get(
|
@router.get(
|
||||||
'/{collection}/incoming/{label}/record',
|
'/{collection}/incoming/{label}/record',
|
||||||
tags=['Incoming area: read records'],
|
tags=['Incoming area: read records'],
|
||||||
name='Read the record with the given PID from the given incoming area'
|
name='Read the record with the given PID from the given incoming area',
|
||||||
)
|
)
|
||||||
async def incoming_read_record_with_pid(
|
async def incoming_read_record_with_pid(
|
||||||
collection: str,
|
collection: str,
|
||||||
label: str,
|
label: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return await _incoming_read_records(
|
return await _incoming_read_records(
|
||||||
collection=collection,
|
collection=collection,
|
||||||
|
|
@ -199,13 +200,13 @@ async def incoming_read_record_with_pid(
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'/{collection}/incoming/{label}/record',
|
'/{collection}/incoming/{label}/record',
|
||||||
tags=['Incoming area: delete records'],
|
tags=['Incoming area: delete records'],
|
||||||
name='Delete the record with the given PID from the given incoming area'
|
name='Delete the record with the given PID from the given incoming area',
|
||||||
)
|
)
|
||||||
async def incoming_delete_record_with_pid(
|
async def incoming_delete_record_with_pid(
|
||||||
collection: str,
|
collection: str,
|
||||||
label: str,
|
label: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return await _incoming_delete_record(
|
return await _incoming_delete_record(
|
||||||
collection=collection,
|
collection=collection,
|
||||||
|
|
@ -224,7 +225,6 @@ async def _incoming_read_records(
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
upper_bound: int = 1000,
|
upper_bound: int = 1000,
|
||||||
) -> LazyList | dict | None:
|
) -> LazyList | dict | None:
|
||||||
|
|
||||||
model_store, backend = await _get_store_and_backend(collection, label, api_key)
|
model_store, backend = await _get_store_and_backend(collection, label, api_key)
|
||||||
|
|
||||||
if pid:
|
if pid:
|
||||||
|
|
@ -244,7 +244,7 @@ async def _incoming_read_records(
|
||||||
collection,
|
collection,
|
||||||
f'/incoming/{label}/records/p/{class_name}'
|
f'/incoming/{label}/records/p/{class_name}'
|
||||||
if class_name
|
if class_name
|
||||||
else f'/incoming/{label}/records/p/'
|
else f'/incoming/{label}/records/p/',
|
||||||
)
|
)
|
||||||
|
|
||||||
return ModifierList(
|
return ModifierList(
|
||||||
|
|
@ -276,7 +276,6 @@ async def _get_store_and_backend(
|
||||||
label: str,
|
label: str,
|
||||||
plain_token: str | None,
|
plain_token: str | None,
|
||||||
) -> tuple[_ModelStore, StorageBackend]:
|
) -> tuple[_ModelStore, StorageBackend]:
|
||||||
|
|
||||||
# Authorize api_key
|
# Authorize api_key
|
||||||
await authorize_zones(collection, plain_token)
|
await authorize_zones(collection, plain_token)
|
||||||
|
|
||||||
|
|
@ -301,33 +300,6 @@ async def _get_store_and_backend(
|
||||||
store_dir=store_dir,
|
store_dir=store_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
xxx = """
|
|
||||||
# For consistency, associate the store with all matching tokens from the
|
|
||||||
# configuration file. That means with all tokens that have the same
|
|
||||||
# input
|
|
||||||
matching_tokens = [
|
|
||||||
token_name
|
|
||||||
for token_name, token_info in abstract_config.tokens.items()
|
|
||||||
if (collection, label) in [
|
|
||||||
(collection_name, token_collection_info.incoming_label)
|
|
||||||
for collection_name, token_collection_info in token_info.items()
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
for matching_token in matching_tokens:
|
|
||||||
# Associate the store with all matching tokens in the configuration.
|
|
||||||
# Note: there are stores that are not associated with a token in
|
|
||||||
# the abstract configuration. These are stores that belong to a token
|
|
||||||
# that is authenticated with an external authentication source.
|
|
||||||
token_info = instance_state.tokens[collection][matching_token]
|
|
||||||
instance_state.token_stores[collection][matching_token] = (
|
|
||||||
model_store,
|
|
||||||
matching_token,
|
|
||||||
token_info['permissions'],
|
|
||||||
token_info['user_id'],
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
backend = model_store.backend
|
backend = model_store.backend
|
||||||
if isinstance(backend, _SchemaTypeLayer):
|
if isinstance(backend, _SchemaTypeLayer):
|
||||||
return model_store, backend.backend
|
return model_store, backend.backend
|
||||||
|
|
@ -367,9 +339,12 @@ async def store_incoming_record(
|
||||||
class_name: str,
|
class_name: str,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: str | None = Depends(api_key_header_scheme),
|
||||||
):
|
):
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
instance_state.validators[collection].validate(data)
|
instance_state.validators[collection].validate(data)
|
||||||
|
|
||||||
pid = data.pid
|
pid = data.pid
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,20 @@ from __future__ import annotations
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
from functools import cache
|
from functools import cache
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType
|
|
||||||
from typing import (
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from fastapi import FastAPI
|
|
||||||
from linkml_runtime import SchemaView
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from yaml.scanner import ScannerError
|
from yaml.scanner import ScannerError
|
||||||
|
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
RecordDirConfigFileContent,
|
|
||||||
MappingMethod,
|
MappingMethod,
|
||||||
|
RecordDirConfigFileContent,
|
||||||
mapping_functions,
|
mapping_functions,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dump_things_service.converter import get_conversion_objects
|
from dump_things_service.converter import get_conversion_objects
|
||||||
from dump_things_service.exceptions import ConfigError
|
from dump_things_service.exceptions import ConfigError
|
||||||
from dump_things_service.model import (
|
from dump_things_service.model import (
|
||||||
|
|
@ -30,6 +25,13 @@ from dump_things_service.model import (
|
||||||
get_schema_view,
|
get_schema_view,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from linkml_runtime import SchemaView
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
|
|
@ -86,7 +88,9 @@ class InstanceState:
|
||||||
maintenance_mode: set = dataclasses.field(default_factory=set)
|
maintenance_mode: set = dataclasses.field(default_factory=set)
|
||||||
|
|
||||||
# Created based on abstract configuration
|
# Created based on abstract configuration
|
||||||
collections: dict[str, InstanceStateCollectionInfo] = dataclasses.field(default_factory=dict)
|
collections: dict[str, InstanceStateCollectionInfo] = dataclasses.field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
tokens: dict = dataclasses.field(default_factory=dict)
|
tokens: dict = dataclasses.field(default_factory=dict)
|
||||||
auth_sources: dict[str, list] = dataclasses.field(default_factory=dict)
|
auth_sources: dict[str, list] = dataclasses.field(default_factory=dict)
|
||||||
audit_backends: dict[str, list] = dataclasses.field(default_factory=dict)
|
audit_backends: dict[str, list] = dataclasses.field(default_factory=dict)
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,9 @@ from abc import (
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Annotated
|
||||||
|
|
||||||
from dump_things_service.abstract_config import store_config
|
from dump_things_service.abstract_config import store_config
|
||||||
from dump_things_service.commands.upload_config import convert_config_1_to_config_2
|
from dump_things_service.commands.upload_config import convert_config_1_to_config_2
|
||||||
from dump_things_service.manifest import manifest_configuration
|
from dump_things_service.manifest import manifest_configuration
|
||||||
|
|
||||||
# Perform the patching before importing any third-party libraries
|
# Perform the patching before importing any third-party libraries
|
||||||
from dump_things_service.patches import enabled # noqa F401 -- used by generated code
|
from dump_things_service.patches import enabled # noqa: F401 -- used by generated code
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
@ -57,8 +58,7 @@ from dump_things_service.converter import (
|
||||||
from dump_things_service.curated import router as curated_router
|
from dump_things_service.curated import router as curated_router
|
||||||
from dump_things_service.exceptions import CurieResolutionError
|
from dump_things_service.exceptions import CurieResolutionError
|
||||||
from dump_things_service.incoming import router as incoming_router
|
from dump_things_service.incoming import router as incoming_router
|
||||||
from dump_things_service.instance_state import create_instance_state, \
|
from dump_things_service.instance_state import create_instance_state, InstanceState
|
||||||
InstanceState
|
|
||||||
from dump_things_service.lazy_list import (
|
from dump_things_service.lazy_list import (
|
||||||
PriorityList,
|
PriorityList,
|
||||||
ModifierList,
|
ModifierList,
|
||||||
|
|
@ -106,7 +106,7 @@ logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--host', default='0.0.0.0') # noqa S104
|
parser.add_argument('--host', default='0.0.0.0') # noqa: S104
|
||||||
parser.add_argument('--port', default=8000, type=int)
|
parser.add_argument('--port', default=8000, type=int)
|
||||||
parser.add_argument('--origins', action='append', default=[])
|
parser.add_argument('--origins', action='append', default=[])
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
@ -124,8 +124,8 @@ parser.add_argument(
|
||||||
'--config',
|
'--config',
|
||||||
metavar='CONFIG_FILE',
|
metavar='CONFIG_FILE',
|
||||||
help="Read the configuration from 'CONFIG_FILE' if no persisted "
|
help="Read the configuration from 'CONFIG_FILE' if no persisted "
|
||||||
"configuration is found in the data store root directory, and "
|
'configuration is found in the data store root directory, and '
|
||||||
"initialize the persistent configuration and the service state with "
|
'initialize the persistent configuration and the service state with '
|
||||||
"the values in 'CONFIG_FILE'.",
|
"the values in 'CONFIG_FILE'.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
@ -141,10 +141,10 @@ parser.add_argument(
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--ignore-default-config-file',
|
'--ignore-default-config-file',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="If the persisted configuration is empty, do not try to initialize it "
|
help='If the persisted configuration is empty, do not try to initialize it '
|
||||||
"from an existing default-config file, i.e., do not read the file "
|
'from an existing default-config file, i.e., do not read the file '
|
||||||
"`<store>/.dumpthings.yaml`. That means the configuration be empty "
|
'`<store>/.dumpthings.yaml`. That means the configuration be empty '
|
||||||
"collections and tokens are added via the API.",
|
'collections and tokens are added via the API.',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'store',
|
'store',
|
||||||
|
|
@ -182,9 +182,8 @@ if not arguments.admin_token_hash:
|
||||||
arguments.admin_token_hash = hash_token_representation(
|
arguments.admin_token_hash = hash_token_representation(
|
||||||
os.environ.get('DTS_ADMIN_TOKEN', ''),
|
os.environ.get('DTS_ADMIN_TOKEN', ''),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# Validate the hash token format
|
# Validate the hash token format
|
||||||
if not hash_matcher.match(arguments.admin_token_hash):
|
elif not hash_matcher.match(arguments.admin_token_hash):
|
||||||
print(
|
print(
|
||||||
'Hashed admin token is not a 64-digits hex-number',
|
'Hashed admin token is not a 64-digits hex-number',
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
|
|
@ -280,11 +279,11 @@ if not (
|
||||||
):
|
):
|
||||||
if arguments.config:
|
if arguments.config:
|
||||||
config_file = arguments.config
|
config_file = arguments.config
|
||||||
else:
|
elif arguments.ignore_default_config_file:
|
||||||
if arguments.ignore_default_config_file:
|
|
||||||
config_file = None
|
config_file = None
|
||||||
else:
|
else:
|
||||||
from dump_things_service import config_file_name
|
from dump_things_service import config_file_name
|
||||||
|
|
||||||
config_file = g_instance_state.store_path / config_file_name
|
config_file = g_instance_state.store_path / config_file_name
|
||||||
if not config_file.exists():
|
if not config_file.exists():
|
||||||
config_file = None
|
config_file = None
|
||||||
|
|
@ -310,11 +309,10 @@ if not (
|
||||||
g_configuration.admin_tokens
|
g_configuration.admin_tokens
|
||||||
or g_configuration.collections
|
or g_configuration.collections
|
||||||
or g_configuration.tokens
|
or g_configuration.tokens
|
||||||
):
|
) and not g_instance_state.bootstrap_token:
|
||||||
if not g_instance_state.bootstrap_token:
|
|
||||||
print(
|
print(
|
||||||
'The server has an empty configuration and requires a bootstrap '
|
'The server has an empty configuration and requires a bootstrap '
|
||||||
'token (use `--admin-token-hash` to provide one).',
|
'token (use `--admin-token-hash` to provide one)',
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
@ -337,11 +335,7 @@ async def root() -> RedirectResponse:
|
||||||
return RedirectResponse('/docs')
|
return RedirectResponse('/docs')
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get('/server', tags=['Server management'], name='get server information')
|
||||||
'/server',
|
|
||||||
tags=['Server management'],
|
|
||||||
name='get server information'
|
|
||||||
)
|
|
||||||
async def server() -> ServerResponse:
|
async def server() -> ServerResponse:
|
||||||
return ServerResponse(
|
return ServerResponse(
|
||||||
version=__version__,
|
version=__version__,
|
||||||
|
|
@ -349,26 +343,28 @@ async def server() -> ServerResponse:
|
||||||
ServerCollectionResponse(
|
ServerCollectionResponse(
|
||||||
name=collection_name,
|
name=collection_name,
|
||||||
schema=g_configuration.collections[collection_name].schema_location,
|
schema=g_configuration.collections[collection_name].schema_location,
|
||||||
classes=g_instance_state.schema_info[g_configuration.collections[collection_name].schema_location].classes,
|
classes=g_instance_state.schema_info[
|
||||||
|
g_configuration.collections[collection_name].schema_location
|
||||||
|
].classes,
|
||||||
)
|
)
|
||||||
for collection_name in g_configuration.collections
|
for collection_name in g_configuration.collections
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
'/maintenance',
|
'/maintenance',
|
||||||
tags=['Server management'],
|
tags=['Server management'],
|
||||||
name='put a collection in maintenance mode'
|
name='put a collection in maintenance mode',
|
||||||
)
|
)
|
||||||
async def maintenance(
|
async def maintenance(
|
||||||
body: MaintenanceRequest,
|
body: MaintenanceRequest,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: Annotated[str | None, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
if api_key is None:
|
if api_key is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_400_BAD_REQUEST,
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
detail=f'Token required for this operation',
|
detail='Token required for this operation',
|
||||||
)
|
)
|
||||||
|
|
||||||
collection = body.collection
|
collection = body.collection
|
||||||
|
|
@ -387,14 +383,13 @@ async def maintenance(
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_400_BAD_REQUEST,
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
detail=f'Curator permissions required for this operation',
|
detail='Curator permissions required for this operation',
|
||||||
)
|
)
|
||||||
|
|
||||||
if active:
|
if active:
|
||||||
g_instance_state.maintenance_mode.add(collection)
|
g_instance_state.maintenance_mode.add(collection)
|
||||||
else:
|
else:
|
||||||
g_instance_state.maintenance_mode.remove(collection)
|
g_instance_state.maintenance_mode.remove(collection)
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
|
|
@ -405,7 +400,7 @@ async def maintenance(
|
||||||
async def read_record_with_pid(
|
async def read_record_with_pid(
|
||||||
collection: str,
|
collection: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
):
|
):
|
||||||
check_collection(g_configuration, collection)
|
check_collection(g_configuration, collection)
|
||||||
|
|
@ -448,7 +443,7 @@ async def read_record_with_pid(
|
||||||
async def read_all_records(
|
async def read_all_records(
|
||||||
collection: str,
|
collection: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
):
|
):
|
||||||
return await _read_all_records(
|
return await _read_all_records(
|
||||||
|
|
@ -471,7 +466,7 @@ async def read_all_records(
|
||||||
async def read_all_records_paginated(
|
async def read_all_records_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
) -> Page[dict | str]:
|
) -> Page[dict | str]:
|
||||||
result_list = await _read_all_records(
|
result_list = await _read_all_records(
|
||||||
|
|
@ -493,7 +488,7 @@ async def read_records_of_type(
|
||||||
collection: str,
|
collection: str,
|
||||||
class_name: str,
|
class_name: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
):
|
):
|
||||||
return await _read_records_of_type(
|
return await _read_records_of_type(
|
||||||
|
|
@ -518,7 +513,7 @@ async def read_records_of_type_paginated(
|
||||||
collection: str,
|
collection: str,
|
||||||
class_name: str,
|
class_name: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
) -> Page[dict | str]:
|
) -> Page[dict | str]:
|
||||||
result_list = await _read_records_of_type(
|
result_list = await _read_records_of_type(
|
||||||
|
|
@ -535,11 +530,10 @@ async def read_records_of_type_paginated(
|
||||||
async def _read_all_records(
|
async def _read_all_records(
|
||||||
collection: str,
|
collection: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
bound: int | None = None,
|
bound: int | None = None,
|
||||||
) -> LazyList:
|
) -> LazyList:
|
||||||
|
|
||||||
def convert_to_http_exception(e: BaseException):
|
def convert_to_http_exception(e: BaseException):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_400_BAD_REQUEST,
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
|
|
@ -591,7 +585,7 @@ async def _read_records_of_type(
|
||||||
collection: str,
|
collection: str,
|
||||||
class_name: str,
|
class_name: str,
|
||||||
matching: str | None = None,
|
matching: str | None = None,
|
||||||
format: Format = Format.json, # noqa A002
|
format: Format = Format.json, # noqa: A002
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: str = Depends(api_key_header_scheme),
|
||||||
bound: int | None = None,
|
bound: int | None = None,
|
||||||
) -> LazyList:
|
) -> LazyList:
|
||||||
|
|
@ -622,7 +616,9 @@ async def _read_records_of_type(
|
||||||
matching=matching,
|
matching=matching,
|
||||||
)
|
)
|
||||||
if bound:
|
if bound:
|
||||||
check_bounds(len(token_store_list), bound, collection, f'/records/p/{class_name}')
|
check_bounds(
|
||||||
|
len(token_store_list), bound, collection, f'/records/p/{class_name}'
|
||||||
|
)
|
||||||
result_list.add_list(token_store_list)
|
result_list.add_list(token_store_list)
|
||||||
|
|
||||||
if final_permissions.curated_read:
|
if final_permissions.curated_read:
|
||||||
|
|
@ -634,7 +630,12 @@ async def _read_records_of_type(
|
||||||
matching=matching,
|
matching=matching,
|
||||||
)
|
)
|
||||||
if bound:
|
if bound:
|
||||||
check_bounds(len(curated_store_list), bound, collection, f'/records/p/{class_name}')
|
check_bounds(
|
||||||
|
len(curated_store_list),
|
||||||
|
bound,
|
||||||
|
collection,
|
||||||
|
f'/records/p/{class_name}',
|
||||||
|
)
|
||||||
result_list.add_list(curated_store_list)
|
result_list.add_list(curated_store_list)
|
||||||
|
|
||||||
# Sort the result list.
|
# Sort the result list.
|
||||||
|
|
@ -664,7 +665,7 @@ async def _read_records_of_type(
|
||||||
async def delete_record(
|
async def delete_record(
|
||||||
collection: str,
|
collection: str,
|
||||||
pid: str,
|
pid: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
check_collection(g_configuration, collection)
|
check_collection(g_configuration, collection)
|
||||||
final_permissions, token_store = await process_token(
|
final_permissions, token_store = await process_token(
|
||||||
|
|
@ -682,7 +683,7 @@ async def delete_record(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_404_NOT_FOUND,
|
status_code=HTTP_404_NOT_FOUND,
|
||||||
detail=f"Could not remove record with PID '{pid}' from the "
|
detail=f"Could not remove record with PID '{pid}' from the "
|
||||||
"token associated incoming area of collection "
|
'token associated incoming area of collection '
|
||||||
f"'{collection}'.",
|
f"'{collection}'.",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from dump_things_service.collection import (
|
||||||
)
|
)
|
||||||
from dump_things_service.instance_state import InstanceState
|
from dump_things_service.instance_state import InstanceState
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
|
|
||||||
tag_groups = [
|
tag_groups = [
|
||||||
|
|
@ -58,7 +57,6 @@ openapi_tags_template = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def manifest_configuration(
|
def manifest_configuration(
|
||||||
configuration: Configuration,
|
configuration: Configuration,
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
|
|
@ -190,7 +188,6 @@ def create_openapi_tags(
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
openapi_tags_template: list[dict | str],
|
openapi_tags_template: list[dict | str],
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
|
|
||||||
# Collect tag name lists for all tag groups that we have defined.
|
# Collect tag name lists for all tag groups that we have defined.
|
||||||
tag_group_info = {
|
tag_group_info = {
|
||||||
tag_group: sorted(
|
tag_group: sorted(
|
||||||
|
|
@ -198,7 +195,7 @@ def create_openapi_tags(
|
||||||
{'name': collection_info.tag_info[tag_group]}
|
{'name': collection_info.tag_info[tag_group]}
|
||||||
for collection_info in instance_state.collections.values()
|
for collection_info in instance_state.collections.values()
|
||||||
],
|
],
|
||||||
key=lambda x: x['name']
|
key=lambda x: x['name'],
|
||||||
)
|
)
|
||||||
for tag_group in tag_groups
|
for tag_group in tag_groups
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses # noqa F401 -- used by generated code
|
import dataclasses # noqa: F401 -- used by generated code
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from functools import cache
|
from functools import cache
|
||||||
|
|
@ -11,9 +11,9 @@ from typing import (
|
||||||
)
|
)
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import annotated_types # noqa F401 -- used by generated code
|
import annotated_types # noqa: F401 -- used by generated code
|
||||||
import pydantic # noqa F401 -- used by generated code
|
import pydantic # noqa: F401 -- used by generated code
|
||||||
import pydantic_core # noqa F401 -- used by generated code
|
import pydantic_core # noqa: F401 -- used by generated code
|
||||||
from linkml.generators import (
|
from linkml.generators import (
|
||||||
PydanticGenerator,
|
PydanticGenerator,
|
||||||
PythonGenerator,
|
PythonGenerator,
|
||||||
|
|
@ -22,7 +22,7 @@ from linkml_runtime import SchemaView
|
||||||
from pydantic._internal._model_construction import ModelMetaclass
|
from pydantic._internal._model_construction import ModelMetaclass
|
||||||
|
|
||||||
# Ensure linkml is patched
|
# Ensure linkml is patched
|
||||||
import dump_things_service.patches.enabled # noqa F401 -- apply patches
|
import dump_things_service.patches.enabled # noqa: F401 -- apply patches
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
@ -67,7 +67,7 @@ def get_subclasses(
|
||||||
|
|
||||||
# TODO: shall we use the following code?
|
# TODO: shall we use the following code?
|
||||||
# The code below would use schema-definitions to determine classes and not
|
# The code below would use schema-definitions to determine classes and not
|
||||||
# go through thw pydantic module generation.
|
# go through the pydantic module generation.
|
||||||
@cache
|
@cache
|
||||||
def get_subclasses_2(
|
def get_subclasses_2(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
|
|
@ -88,8 +88,8 @@ def compile_module_with_increasing_recursion_limit(
|
||||||
|
|
||||||
module = None
|
module = None
|
||||||
module_name = (
|
module_name = (
|
||||||
urlparse(schema_location).path
|
urlparse(schema_location)
|
||||||
.replace('/', '_')
|
.path.replace('/', '_')
|
||||||
.replace('-', '_')
|
.replace('-', '_')
|
||||||
.replace('.', '_')
|
.replace('.', '_')
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ if TYPE_CHECKING:
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from dump_things_service.backends import (
|
from dump_things_service.backends import (
|
||||||
_RecordInfo,
|
|
||||||
StorageBackend,
|
StorageBackend,
|
||||||
|
_RecordInfo,
|
||||||
)
|
)
|
||||||
from dump_things_service.lazy_list import LazyList
|
from dump_things_service.lazy_list import LazyList
|
||||||
|
|
||||||
|
|
@ -28,12 +28,7 @@ submitter_namespace = 'http://purl.obolibrary.org/obo/'
|
||||||
|
|
||||||
|
|
||||||
class _ModelStore:
|
class _ModelStore:
|
||||||
def __init__(
|
def __init__(self, schema: str, backend: StorageBackend, tags: dict[str, str]):
|
||||||
self,
|
|
||||||
schema: str,
|
|
||||||
backend: StorageBackend,
|
|
||||||
tags: dict[str, str]
|
|
||||||
):
|
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
self.model = get_model_for_schema(self.schema)[0]
|
self.model = get_model_for_schema(self.schema)[0]
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
@ -47,7 +42,9 @@ class _ModelStore:
|
||||||
obj: BaseModel,
|
obj: BaseModel,
|
||||||
submitter: str | None,
|
submitter: str | None,
|
||||||
) -> Iterable[tuple[str, dict]]:
|
) -> Iterable[tuple[str, dict]]:
|
||||||
if obj.__class__.__name__ == 'Thing' and 'dlthings:placeholder' in (obj.annotations or dict()):
|
if obj.__class__.__name__ == 'Thing' and 'dlthings:placeholder' in (
|
||||||
|
obj.annotations or {}
|
||||||
|
):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Extract inlined records from the object, store individual records
|
# Extract inlined records from the object, store individual records
|
||||||
|
|
@ -146,7 +143,8 @@ class _ModelStore:
|
||||||
# Do not extract 'empty'-Thing records with an
|
# Do not extract 'empty'-Thing records with an
|
||||||
# `dlthings:placeholder` annotation. These records are just
|
# `dlthings:placeholder` annotation. These records are just
|
||||||
# placeholders for already extracted records.
|
# placeholders for already extracted records.
|
||||||
if sub_record != self.model.Thing(
|
if sub_record
|
||||||
|
!= self.model.Thing(
|
||||||
pid=sub_record.pid,
|
pid=sub_record.pid,
|
||||||
annotations={
|
annotations={
|
||||||
'dlthings:placeholder': sub_record.pid,
|
'dlthings:placeholder': sub_record.pid,
|
||||||
|
|
@ -165,7 +163,7 @@ class _ModelStore:
|
||||||
pid=sub_record_pid,
|
pid=sub_record_pid,
|
||||||
annotations={
|
annotations={
|
||||||
'dlthings:placeholder': sub_record_pid,
|
'dlthings:placeholder': sub_record_pid,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
for sub_record_pid in record.relations
|
for sub_record_pid in record.relations
|
||||||
}
|
}
|
||||||
|
|
@ -252,9 +250,8 @@ def ModelStore( # noqa: N802
|
||||||
# We store a pointer to the backend in the value to ensure that the
|
# We store a pointer to the backend in the value to ensure that the
|
||||||
# backend object exists while we use its `id` as a key.
|
# backend object exists while we use its `id` as a key.
|
||||||
_existing_model_stores[id(backend)] = existing_model_store, backend
|
_existing_model_stores[id(backend)] = existing_model_store, backend
|
||||||
else:
|
|
||||||
# Check that the schemas are compatible, if the backend is reused.
|
# Check that the schemas are compatible, if the backend is reused.
|
||||||
if existing_model_store.schema != schema:
|
elif existing_model_store.schema != schema:
|
||||||
msg = 'Backend is already used in a ModelStore with a different schema'
|
msg = 'Backend is already used in a ModelStore with a different schema'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,19 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from dump_things_service.backends.record_dir import RecordDirStore
|
|
||||||
from dump_things_service.backends.sqlite import (
|
|
||||||
SQLiteBackend,
|
|
||||||
record_file_name as sqlite_record_file_name,
|
|
||||||
)
|
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
RecordDirBackendConfig,
|
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
Configuration,
|
Configuration,
|
||||||
MappingMethod,
|
MappingMethod,
|
||||||
|
RecordDirBackendConfig,
|
||||||
mapping_functions,
|
mapping_functions,
|
||||||
)
|
)
|
||||||
|
from dump_things_service.backends.sqlite import (
|
||||||
|
SQLiteBackend,
|
||||||
|
)
|
||||||
|
from dump_things_service.backends.sqlite import (
|
||||||
|
record_file_name as sqlite_record_file_name,
|
||||||
|
)
|
||||||
from dump_things_service.model import get_model_for_schema
|
from dump_things_service.model import get_model_for_schema
|
||||||
from dump_things_service.resolve_curie import resolve_curie
|
from dump_things_service.resolve_curie import resolve_curie
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,23 @@ import yaml
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
GitAuditBackendConfig,
|
GitAuditBackendConfig,
|
||||||
SQLiteBackendConfig,
|
SQLiteBackendConfig,
|
||||||
|
TagSpec,
|
||||||
TokenCollectionConfig,
|
TokenCollectionConfig,
|
||||||
TokenModes, hash_token_representation, TagSpec,
|
TokenModes,
|
||||||
|
hash_token_representation,
|
||||||
)
|
)
|
||||||
from dump_things_service.backends import StorageBackend
|
from dump_things_service.backends import StorageBackend
|
||||||
from dump_things_service.backends.record_dir import RecordDirStore
|
from dump_things_service.backends.record_dir import RecordDirStore
|
||||||
from dump_things_service.backends.sqlite import (
|
from dump_things_service.backends.sqlite import (
|
||||||
SQLiteBackend,
|
SQLiteBackend,
|
||||||
|
)
|
||||||
|
from dump_things_service.backends.sqlite import (
|
||||||
record_file_name as sqlite_db_filename,
|
record_file_name as sqlite_db_filename,
|
||||||
)
|
)
|
||||||
from dump_things_service.collection_endpoints import CollectionRequest
|
from dump_things_service.collection_endpoints import CollectionRequest
|
||||||
from dump_things_service.instance_state import get_mapping_function_by_name
|
from dump_things_service.instance_state import get_mapping_function_by_name
|
||||||
from dump_things_service.model import get_model_for_schema
|
from dump_things_service.model import get_model_for_schema
|
||||||
from dump_things_service.resolve_curie import resolve_curie
|
from dump_things_service.resolve_curie import resolve_curie
|
||||||
from dump_things_service.token_endpoints import TokenRequest
|
|
||||||
from dump_things_service.tests.create_store import (
|
from dump_things_service.tests.create_store import (
|
||||||
pid,
|
pid,
|
||||||
pid_curated,
|
pid_curated,
|
||||||
|
|
@ -33,7 +36,7 @@ from dump_things_service.tests.create_store import (
|
||||||
test_record_curated,
|
test_record_curated,
|
||||||
test_record_trr,
|
test_record_trr,
|
||||||
)
|
)
|
||||||
|
from dump_things_service.token_endpoints import TokenRequest
|
||||||
|
|
||||||
# String representation of curated- and incoming-path
|
# String representation of curated- and incoming-path
|
||||||
curated = 'curated'
|
curated = 'curated'
|
||||||
|
|
@ -41,7 +44,9 @@ incoming = 'incoming'
|
||||||
|
|
||||||
# Path to a local simple test schema
|
# Path to a local simple test schema
|
||||||
test_schema_location = str((Path(__file__).parent / 'testschema.yaml').absolute())
|
test_schema_location = str((Path(__file__).parent / 'testschema.yaml').absolute())
|
||||||
flat_social_schema_location = 'https://concepts.datalad.org/s/flat-social/unreleased.yaml'
|
flat_social_schema_location = (
|
||||||
|
'https://concepts.datalad.org/s/flat-social/unreleased.yaml'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# The test store is created empty and collections are added via the admin
|
# The test store is created empty and collections are added via the admin
|
||||||
|
|
@ -64,7 +69,7 @@ g_default_collections[6].submission_tags = TagSpec(
|
||||||
|
|
||||||
g_default_collections.append(
|
g_default_collections.append(
|
||||||
CollectionRequest(
|
CollectionRequest(
|
||||||
name=f'collection_8',
|
name='collection_8',
|
||||||
default_token='test_default_token',
|
default_token='test_default_token',
|
||||||
curated=PurePosixPath(f'{curated}/collection_8'),
|
curated=PurePosixPath(f'{curated}/collection_8'),
|
||||||
schema=test_schema_location,
|
schema=test_schema_location,
|
||||||
|
|
@ -75,11 +80,12 @@ g_default_collections.append(
|
||||||
submission_tags=TagSpec(
|
submission_tags=TagSpec(
|
||||||
submitter_id_tag='no_default_id_tag',
|
submitter_id_tag='no_default_id_tag',
|
||||||
submission_time_tag='no_default_time_tag',
|
submission_time_tag='no_default_time_tag',
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
g_default_collections.extend([
|
g_default_collections.extend(
|
||||||
|
[
|
||||||
CollectionRequest(
|
CollectionRequest(
|
||||||
name='collection_dlflatsocial-1',
|
name='collection_dlflatsocial-1',
|
||||||
schema=flat_social_schema_location,
|
schema=flat_social_schema_location,
|
||||||
|
|
@ -106,7 +112,8 @@ g_default_collections.extend([
|
||||||
'Project',
|
'Project',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
g_default_tokens = [
|
g_default_tokens = [
|
||||||
TokenRequest(
|
TokenRequest(
|
||||||
|
|
@ -152,7 +159,7 @@ g_default_tokens = [
|
||||||
hashed=False,
|
hashed=False,
|
||||||
representation='token-2',
|
representation='token-2',
|
||||||
collections={
|
collections={
|
||||||
f'collection_2': TokenCollectionConfig(
|
'collection_2': TokenCollectionConfig(
|
||||||
mode=TokenModes.WRITE_COLLECTION,
|
mode=TokenModes.WRITE_COLLECTION,
|
||||||
incoming_label='in_token-2',
|
incoming_label='in_token-2',
|
||||||
)
|
)
|
||||||
|
|
@ -164,7 +171,7 @@ g_default_tokens = [
|
||||||
hashed=False,
|
hashed=False,
|
||||||
representation='token-8',
|
representation='token-8',
|
||||||
collections={
|
collections={
|
||||||
f'collection_8': TokenCollectionConfig(
|
'collection_8': TokenCollectionConfig(
|
||||||
mode=TokenModes.WRITE_COLLECTION,
|
mode=TokenModes.WRITE_COLLECTION,
|
||||||
incoming_label='test_user_8',
|
incoming_label='test_user_8',
|
||||||
)
|
)
|
||||||
|
|
@ -235,7 +242,7 @@ g_default_tokens = [
|
||||||
mode=TokenModes.WRITE_COLLECTION,
|
mode=TokenModes.WRITE_COLLECTION,
|
||||||
incoming_label='modes',
|
incoming_label='modes',
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
TokenRequest(
|
TokenRequest(
|
||||||
name='Test 0X000 (READ_SUBMISSIONS)',
|
name='Test 0X000 (READ_SUBMISSIONS)',
|
||||||
|
|
@ -354,7 +361,8 @@ def fastapi_app_simple(dump_stores_simple):
|
||||||
old_sys_argv = sys.argv
|
old_sys_argv = sys.argv
|
||||||
sys.argv = [
|
sys.argv = [
|
||||||
'test-runner',
|
'test-runner',
|
||||||
'--admin-token-hash', hash_token_representation(admin_token),
|
'--admin-token-hash',
|
||||||
|
hash_token_representation(admin_token),
|
||||||
'--ignore-default-config-file',
|
'--ignore-default-config-file',
|
||||||
str(tmp_path),
|
str(tmp_path),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,7 @@ user_1 = {
|
||||||
'@type': 'user',
|
'@type': 'user',
|
||||||
}
|
}
|
||||||
|
|
||||||
org_1 = {
|
org_1 = {'id': 1, 'name': 'org_1', '@type': 'org'}
|
||||||
'id': 1,
|
|
||||||
'name': 'org_1',
|
|
||||||
'@type': 'org'
|
|
||||||
}
|
|
||||||
|
|
||||||
repo_1 = {
|
repo_1 = {
|
||||||
'id': 3,
|
'id': 3,
|
||||||
|
|
@ -46,10 +42,18 @@ team_3 = json.loads(team_template.format(id=3, action='write'))
|
||||||
def setup_http_server(http_server) -> None:
|
def setup_http_server(http_server) -> None:
|
||||||
for instance in ('1', '2'):
|
for instance in ('1', '2'):
|
||||||
http_server.expect_request(f'/api/v{instance}/user').respond_with_json(user_1)
|
http_server.expect_request(f'/api/v{instance}/user').respond_with_json(user_1)
|
||||||
http_server.expect_request(f'/api/v{instance}/user/teams').respond_with_json([team_1, team_3])
|
http_server.expect_request(f'/api/v{instance}/user/teams').respond_with_json(
|
||||||
http_server.expect_request(f'/api/v{instance}/orgs/org_1').respond_with_json(org_1)
|
[team_1, team_3]
|
||||||
http_server.expect_request(f'/api/v{instance}/orgs/org_1/teams').respond_with_json([team_1, team_2, team_3])
|
)
|
||||||
http_server.expect_request(f'/api/v{instance}/repos/org_1/repo_1/teams').respond_with_json([team_1, team_2, team_3])
|
http_server.expect_request(f'/api/v{instance}/orgs/org_1').respond_with_json(
|
||||||
|
org_1
|
||||||
|
)
|
||||||
|
http_server.expect_request(
|
||||||
|
f'/api/v{instance}/orgs/org_1/teams'
|
||||||
|
).respond_with_json([team_1, team_2, team_3])
|
||||||
|
http_server.expect_request(
|
||||||
|
f'/api/v{instance}/repos/org_1/repo_1/teams'
|
||||||
|
).respond_with_json([team_1, team_2, team_3])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('repository', ['repo_1', None])
|
@pytest.mark.parametrize('repository', ['repo_1', None])
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
|
|
||||||
import pytest # F401
|
|
||||||
|
|
||||||
from . import schema_file
|
|
||||||
from .. import (
|
from .. import (
|
||||||
HTTP_200_OK,
|
HTTP_200_OK,
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
|
|
@ -11,14 +7,13 @@ from .. import (
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
from ..__about__ import __version__
|
from ..__about__ import __version__
|
||||||
from ..utils import cleaned_json
|
from . import schema_file
|
||||||
from .create_store import (
|
from .create_store import (
|
||||||
given_name,
|
given_name,
|
||||||
pid,
|
pid,
|
||||||
)
|
)
|
||||||
from .test_utils import basic_write_locations
|
from .test_utils import basic_write_locations
|
||||||
|
|
||||||
|
|
||||||
extra_record = {
|
extra_record = {
|
||||||
'schema_type': 'abc:Person',
|
'schema_type': 'abc:Person',
|
||||||
'pid': 'abc:aaaa',
|
'pid': 'abc:aaaa',
|
||||||
|
|
@ -298,7 +293,7 @@ def test_funky_pid(fastapi_client_simple):
|
||||||
|
|
||||||
|
|
||||||
def test_token_store_priority(fastapi_client_simple):
|
def test_token_store_priority(fastapi_client_simple):
|
||||||
test_client, store_dir, _ = fastapi_client_simple
|
test_client, _store_dir, _ = fastapi_client_simple
|
||||||
|
|
||||||
# Post a record with the same pid as the global store's test record, but
|
# Post a record with the same pid as the global store's test record, but
|
||||||
# with different content.
|
# with different content.
|
||||||
|
|
@ -393,7 +388,8 @@ def test_server(fastapi_client_simple):
|
||||||
'classes': test_schema_classes,
|
'classes': test_schema_classes,
|
||||||
}
|
}
|
||||||
for i in range(1, 9)
|
for i in range(1, 9)
|
||||||
] + [
|
]
|
||||||
|
+ [
|
||||||
{
|
{
|
||||||
'name': f'collection_dlflatsocial-{i}',
|
'name': f'collection_dlflatsocial-{i}',
|
||||||
'schema': 'https://concepts.datalad.org/s/flat-social/unreleased.yaml',
|
'schema': 'https://concepts.datalad.org/s/flat-social/unreleased.yaml',
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ from pathlib import (
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from dump_things_service import (
|
from dump_things_service import (
|
||||||
HTTP_201_CREATED,
|
|
||||||
HTTP_200_OK,
|
HTTP_200_OK,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_201_CREATED,
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
GitAuditBackendConfig,
|
GitAuditBackendConfig,
|
||||||
|
|
@ -19,10 +19,9 @@ from dump_things_service.abstract_config import (
|
||||||
)
|
)
|
||||||
from dump_things_service.collection_endpoints import CollectionRequest
|
from dump_things_service.collection_endpoints import CollectionRequest
|
||||||
from dump_things_service.token_endpoints import (
|
from dump_things_service.token_endpoints import (
|
||||||
TokenRequest,
|
|
||||||
AdminTokenRequest,
|
AdminTokenRequest,
|
||||||
|
TokenRequest,
|
||||||
)
|
)
|
||||||
from dump_things_service.utils import cleaned_json
|
|
||||||
|
|
||||||
# String representation of curated- and incoming-path
|
# String representation of curated- and incoming-path
|
||||||
curated = 'admin_test_curated'
|
curated = 'admin_test_curated'
|
||||||
|
|
@ -69,10 +68,7 @@ def _name_in_openapi_paths(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
response = test_client.get('/openapi.json')
|
response = test_client.get('/openapi.json')
|
||||||
open_api = response.json()
|
open_api = response.json()
|
||||||
for path in open_api['paths'].keys():
|
return any(name in path for path in open_api['paths'])
|
||||||
if name in path:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_collection_adding(fastapi_client_simple):
|
def test_collection_adding(fastapi_client_simple):
|
||||||
|
|
@ -100,7 +96,9 @@ def test_collection_adding(fastapi_client_simple):
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
new_collection_config = new_collection_request.model_dump(mode='json', by_alias=True)
|
new_collection_config = new_collection_request.model_dump(
|
||||||
|
mode='json', by_alias=True
|
||||||
|
)
|
||||||
del new_collection_config['name']
|
del new_collection_config['name']
|
||||||
assert response.json() == new_collection_config
|
assert response.json() == new_collection_config
|
||||||
|
|
||||||
|
|
@ -123,7 +121,7 @@ def test_collection_adding(fastapi_client_simple):
|
||||||
'user_id': new_token_request.user_id,
|
'user_id': new_token_request.user_id,
|
||||||
'collections': new_token_request.model_dump(mode='json')['collections'],
|
'collections': new_token_request.model_dump(mode='json')['collections'],
|
||||||
'hashed': new_token_request.hashed,
|
'hashed': new_token_request.hashed,
|
||||||
'representation': new_token_request.representation
|
'representation': new_token_request.representation,
|
||||||
}
|
}
|
||||||
|
|
||||||
new_record = {
|
new_record = {
|
||||||
|
|
@ -204,7 +202,7 @@ def test_collection_putting(fastapi_client_simple, tmp_path):
|
||||||
path=Path(tmp_path),
|
path=Path(tmp_path),
|
||||||
auto_flush_timeout=2,
|
auto_flush_timeout=2,
|
||||||
)
|
)
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the collection does not yet exist
|
# Check that the collection does not yet exist
|
||||||
|
|
@ -259,7 +257,7 @@ def test_collection_reading(fastapi_client_simple):
|
||||||
|
|
||||||
# Check that the new admin token is not yet working
|
# Check that the new admin token is not yet working
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/collections',
|
'/collections',
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
@ -273,7 +271,7 @@ def test_admin_token_management(fastapi_client_simple):
|
||||||
|
|
||||||
# Check that the new admin token is not yet working
|
# Check that the new admin token is not yet working
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/collections/collection_1',
|
'/collections/collection_1',
|
||||||
headers={'x-dumpthings-token': plain_new_admin_token},
|
headers={'x-dumpthings-token': plain_new_admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||||
|
|
@ -288,14 +286,14 @@ def test_admin_token_management(fastapi_client_simple):
|
||||||
|
|
||||||
# Try the new token
|
# Try the new token
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/collections/collection_1',
|
'/collections/collection_1',
|
||||||
headers={'x-dumpthings-token': plain_new_admin_token},
|
headers={'x-dumpthings-token': plain_new_admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
||||||
# Check that the token shows up in the token list
|
# Check that the token shows up in the token list
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/admin_tokens',
|
'/admin_tokens',
|
||||||
headers={'x-dumpthings-token': plain_new_admin_token},
|
headers={'x-dumpthings-token': plain_new_admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
@ -310,7 +308,7 @@ def test_admin_token_management(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/admin_tokens',
|
'/admin_tokens',
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,12 @@ from dump_things_service.exceptions import ConfigError
|
||||||
from dump_things_service.tests import schema_file
|
from dump_things_service.tests import schema_file
|
||||||
from dump_things_service.token_endpoints import TokenRequest
|
from dump_things_service.token_endpoints import TokenRequest
|
||||||
|
|
||||||
|
|
||||||
collection_request_pattern = CollectionRequest(
|
collection_request_pattern = CollectionRequest(
|
||||||
name='',
|
name='',
|
||||||
schema=str(schema_file),
|
schema=str(schema_file),
|
||||||
default_token='test_default_token',
|
default_token='test_default_token',
|
||||||
curated=PurePosixPath('curate_dir'),
|
curated=PurePosixPath('curate_dir'),
|
||||||
incoming=PurePosixPath(f'incoming_dir'),
|
incoming=PurePosixPath('incoming_dir'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ def test_illegal_collection_name_detection(fastapi_client_simple):
|
||||||
dump_things_private_collection_name,
|
dump_things_private_collection_name,
|
||||||
):
|
):
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
f'/collections',
|
'/collections',
|
||||||
json={
|
json={
|
||||||
**collection_request_pattern.model_dump(mode='json', by_alias=True),
|
**collection_request_pattern.model_dump(mode='json', by_alias=True),
|
||||||
'name': name,
|
'name': name,
|
||||||
|
|
@ -52,7 +51,9 @@ def test_illegal_collection_name_detection(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_409_CONFLICT
|
assert response.status_code == HTTP_409_CONFLICT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason='Reuse detection is disabled to support existing old configurations')
|
@pytest.mark.skip(
|
||||||
|
reason='Reuse detection is disabled to support existing old configurations'
|
||||||
|
)
|
||||||
def test_collection_dir_reuse_detection(fastapi_client_simple):
|
def test_collection_dir_reuse_detection(fastapi_client_simple):
|
||||||
test_client, _, admin_token = fastapi_client_simple
|
test_client, _, admin_token = fastapi_client_simple
|
||||||
|
|
||||||
|
|
@ -62,7 +63,7 @@ def test_collection_dir_reuse_detection(fastapi_client_simple):
|
||||||
('curated/collection_1', 'incoming/collection_2'),
|
('curated/collection_1', 'incoming/collection_2'),
|
||||||
):
|
):
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
f'/collections',
|
'/collections',
|
||||||
json={
|
json={
|
||||||
**collection_request_pattern.model_dump(mode='json', by_alias=True),
|
**collection_request_pattern.model_dump(mode='json', by_alias=True),
|
||||||
'curated': curated_path,
|
'curated': curated_path,
|
||||||
|
|
@ -76,15 +77,17 @@ def test_collection_dir_reuse_detection(fastapi_client_simple):
|
||||||
def test_scanner_error_detection(tmp_path_factory):
|
def test_scanner_error_detection(tmp_path_factory):
|
||||||
tmp_path = tmp_path_factory.mktemp('config_scanner_test')
|
tmp_path = tmp_path_factory.mktemp('config_scanner_test')
|
||||||
|
|
||||||
config_backend, audit_backend = get_config_backends(tmp_path)
|
config_backend, _audit_backend = get_config_backends(tmp_path)
|
||||||
config_backend.add_record(
|
config_backend.add_record(
|
||||||
iri=dump_things_config_iri,
|
iri=dump_things_config_iri,
|
||||||
class_name='DumpThingsConfig',
|
class_name='DumpThingsConfig',
|
||||||
json_object={'pid': dump_things_config_iri}
|
json_object={'pid': dump_things_config_iri},
|
||||||
)
|
)
|
||||||
|
|
||||||
md5_hexdigest = hashlib.md5(dump_things_config_iri.encode()).hexdigest()
|
md5_hexdigest = hashlib.md5(dump_things_config_iri.encode()).hexdigest()
|
||||||
config_file_path = config_backend.root / 'DumpThingsConfig' / f'{md5_hexdigest}.yaml'
|
config_file_path = (
|
||||||
|
config_backend.root / 'DumpThingsConfig' / f'{md5_hexdigest}.yaml'
|
||||||
|
)
|
||||||
config_file_path.write_text('collections: ::: -\n sdsdfsdf: xxx')
|
config_file_path.write_text('collections: ::: -\n sdsdfsdf: xxx')
|
||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
read_config(tmp_path, force_reload=True)
|
read_config(tmp_path, force_reload=True)
|
||||||
|
|
@ -93,15 +96,17 @@ def test_scanner_error_detection(tmp_path_factory):
|
||||||
def test_structure_error_detection(tmp_path_factory):
|
def test_structure_error_detection(tmp_path_factory):
|
||||||
tmp_path = tmp_path_factory.mktemp('config_scanner_test')
|
tmp_path = tmp_path_factory.mktemp('config_scanner_test')
|
||||||
|
|
||||||
config_backend, audit_backend = get_config_backends(tmp_path)
|
config_backend, _audit_backend = get_config_backends(tmp_path)
|
||||||
config_backend.add_record(
|
config_backend.add_record(
|
||||||
iri=dump_things_config_iri,
|
iri=dump_things_config_iri,
|
||||||
class_name='DumpThingsConfig',
|
class_name='DumpThingsConfig',
|
||||||
json_object={'pid': dump_things_config_iri}
|
json_object={'pid': dump_things_config_iri},
|
||||||
)
|
)
|
||||||
|
|
||||||
md5_hexdigest = hashlib.md5(dump_things_config_iri.encode()).hexdigest()
|
md5_hexdigest = hashlib.md5(dump_things_config_iri.encode()).hexdigest()
|
||||||
config_file_path = config_backend.root / 'DumpThingsConfig' / f'{md5_hexdigest}.yaml'
|
config_file_path = (
|
||||||
|
config_backend.root / 'DumpThingsConfig' / f'{md5_hexdigest}.yaml'
|
||||||
|
)
|
||||||
config_file_path.write_text('type: 1\n')
|
config_file_path.write_text('type: 1\n')
|
||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
read_config(tmp_path, force_reload=True)
|
read_config(tmp_path, force_reload=True)
|
||||||
|
|
@ -135,7 +140,7 @@ def test_missing_incoming_detection(fastapi_client_simple):
|
||||||
mode=TokenModes.CURATOR,
|
mode=TokenModes.CURATOR,
|
||||||
incoming_label='',
|
incoming_label='',
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that a write token for a collection without incoming path cannot
|
# Check that a write token for a collection without incoming path cannot
|
||||||
|
|
@ -155,7 +160,9 @@ def test_missing_incoming_detection(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
||||||
# Add a collection with incoming path
|
# Add a collection with incoming path
|
||||||
collection_request.incoming = PurePosixPath('missing_incoming_detection_test_incoming')
|
collection_request.incoming = PurePosixPath(
|
||||||
|
'missing_incoming_detection_test_incoming'
|
||||||
|
)
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
'/collections',
|
'/collections',
|
||||||
json=collection_request.model_dump(mode='json', by_alias=True),
|
json=collection_request.model_dump(mode='json', by_alias=True),
|
||||||
|
|
@ -173,10 +180,12 @@ def test_missing_incoming_detection(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_406_NOT_ACCEPTABLE
|
assert response.status_code == HTTP_406_NOT_ACCEPTABLE
|
||||||
|
|
||||||
# Check that a write token for a collection with an incoming path can be created
|
# Check that a write token for a collection with an incoming path can be created
|
||||||
token_request.collections['missing_incoming_detection_test'] = TokenCollectionConfig(
|
token_request.collections['missing_incoming_detection_test'] = (
|
||||||
|
TokenCollectionConfig(
|
||||||
mode=TokenModes.CURATOR,
|
mode=TokenModes.CURATOR,
|
||||||
incoming_label='test_incoming_label',
|
incoming_label='test_incoming_label',
|
||||||
)
|
)
|
||||||
|
)
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
'/tokens',
|
'/tokens',
|
||||||
json=token_request.model_dump(mode='json', by_alias=True),
|
json=token_request.model_dump(mode='json', by_alias=True),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
|
||||||
import time
|
import time
|
||||||
import yaml
|
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
from dump_things_service import (
|
from dump_things_service import (
|
||||||
HTTP_200_OK,
|
HTTP_200_OK,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from dump_things_service.instance_state import get_instance_state
|
from dump_things_service.instance_state import get_instance_state
|
||||||
|
|
||||||
|
|
||||||
delete_record = {
|
delete_record = {
|
||||||
'schema_type': 'abc:Person',
|
'schema_type': 'abc:Person',
|
||||||
'pid': 'abc:delete-me',
|
'pid': 'abc:delete-me',
|
||||||
|
|
@ -19,8 +19,8 @@ delete_record = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('paginate', ('', 'p/'))
|
@pytest.mark.parametrize('paginate', ['', 'p/'])
|
||||||
@pytest.mark.parametrize('class_name', ('', 'Person'))
|
@pytest.mark.parametrize('class_name', ['', 'Person'])
|
||||||
def test_read_curated_records(
|
def test_read_curated_records(
|
||||||
fastapi_client_simple,
|
fastapi_client_simple,
|
||||||
paginate,
|
paginate,
|
||||||
|
|
@ -54,10 +54,6 @@ def test_read_curated_records(
|
||||||
assert len(json_object) == count
|
assert len(json_object) == count
|
||||||
|
|
||||||
|
|
||||||
pytest.mark.parametrize(
|
|
||||||
'pid',
|
|
||||||
('abc:mode_test', 'abc:some_timee@x.com', 'abc:curated'),
|
|
||||||
)
|
|
||||||
def test_read_curated_records_by_pid(fastapi_client_simple):
|
def test_read_curated_records_by_pid(fastapi_client_simple):
|
||||||
test_client, _, _ = fastapi_client_simple
|
test_client, _, _ = fastapi_client_simple
|
||||||
|
|
||||||
|
|
@ -185,5 +181,6 @@ def test_audit_backend_auto_flush(fastapi_client_simple):
|
||||||
break
|
break
|
||||||
i += 1
|
i += 1
|
||||||
if i == 10:
|
if i == 10:
|
||||||
raise ValueError(f'auto flush did not trigger within 10 seconds')
|
msg = 'auto flush did not trigger within 10 seconds'
|
||||||
|
raise ValueError(msg)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,10 @@ empty_inlined_json_record = cleaned_json(dataclasses.asdict(empty_inlined_object
|
||||||
|
|
||||||
|
|
||||||
tree = (
|
tree = (
|
||||||
('dlflatsocial:test_extract_1', ('dlflatsocial:test_extract_1_1', 'dlflatsocial:test_extract_1_2')),
|
(
|
||||||
|
'dlflatsocial:test_extract_1',
|
||||||
|
('dlflatsocial:test_extract_1_1', 'dlflatsocial:test_extract_1_2'),
|
||||||
|
),
|
||||||
('dlflatsocial:test_extract_1_1', ('dlflatsocial:test_extract_1_1_1',)),
|
('dlflatsocial:test_extract_1_1', ('dlflatsocial:test_extract_1_1_1',)),
|
||||||
('dlflatsocial:test_extract_1_2', ()),
|
('dlflatsocial:test_extract_1_2', ()),
|
||||||
('dlflatsocial:test_extract_1_1_1', ()),
|
('dlflatsocial:test_extract_1_1_1', ()),
|
||||||
|
|
@ -184,7 +187,7 @@ def test_inline_extraction_locally():
|
||||||
tags={
|
tags={
|
||||||
'id': 'abc:id',
|
'id': 'abc:id',
|
||||||
'time': 'abc:time',
|
'time': 'abc:time',
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
store.model = MockedModule()
|
store.model = MockedModule()
|
||||||
records = store.extract_inlined(inlined_object)
|
records = store.extract_inlined(inlined_object)
|
||||||
|
|
@ -216,7 +219,7 @@ def test_dont_extract_empty_things_locally():
|
||||||
tags={
|
tags={
|
||||||
'id': 'https://id',
|
'id': 'https://id',
|
||||||
'time': 'https://time',
|
'time': 'https://time',
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
store.model = MockedModule()
|
store.model = MockedModule()
|
||||||
records = store.extract_inlined(empty_inlined_object)
|
records = store.extract_inlined(empty_inlined_object)
|
||||||
|
|
@ -257,7 +260,10 @@ def test_inline_extraction_on_service(fastapi_client_simple):
|
||||||
|
|
||||||
# Check that individual record classes were recognized
|
# Check that individual record classes were recognized
|
||||||
for class_name, pids in (
|
for class_name, pids in (
|
||||||
('Person', ('dlflatsocial:test_extract_1', 'dlflatsocial:test_extract_1_1')),
|
(
|
||||||
|
'Person',
|
||||||
|
('dlflatsocial:test_extract_1', 'dlflatsocial:test_extract_1_1'),
|
||||||
|
),
|
||||||
('Agent', ('dlflatsocial:test_extract_1_1_1',)),
|
('Agent', ('dlflatsocial:test_extract_1_1_1',)),
|
||||||
('InstantaneousEvent', ('dlflatsocial:test_extract_1_2',)),
|
('InstantaneousEvent', ('dlflatsocial:test_extract_1_2',)),
|
||||||
):
|
):
|
||||||
|
|
@ -301,7 +307,10 @@ def test_inline_ttl_processing(fastapi_client_simple):
|
||||||
|
|
||||||
# Check that individual record classes were recognized
|
# Check that individual record classes were recognized
|
||||||
for class_name, pids in (
|
for class_name, pids in (
|
||||||
('Person', ('dlflatsocial:test_ttl_inline_1', 'dlflatsocial:test_ttl_inline_1_1')),
|
(
|
||||||
|
'Person',
|
||||||
|
('dlflatsocial:test_ttl_inline_1', 'dlflatsocial:test_ttl_inline_1_1'),
|
||||||
|
),
|
||||||
('Agent', ('dlflatsocial:test_ttl_inline_1_1_1',)),
|
('Agent', ('dlflatsocial:test_ttl_inline_1_1_1',)),
|
||||||
('InstantaneousEvent', ('dlflatsocial:test_ttl_inline_1_2',)),
|
('InstantaneousEvent', ('dlflatsocial:test_ttl_inline_1_2',)),
|
||||||
):
|
):
|
||||||
|
|
@ -339,7 +348,7 @@ def _check_result_json(
|
||||||
# That breaks the tests. They assume that Person.relations has range Thing.
|
# That breaks the tests. They assume that Person.relations has range Thing.
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test_dont_extract_empty_things_on_service(fastapi_client_simple):
|
def test_dont_extract_empty_things_on_service(fastapi_client_simple):
|
||||||
test_client, store = fastapi_client_simple
|
test_client, _store = fastapi_client_simple
|
||||||
|
|
||||||
for i in range(1, 3):
|
for i in range(1, 3):
|
||||||
# Deposit JSON record
|
# Deposit JSON record
|
||||||
|
|
@ -352,7 +361,7 @@ def test_dont_extract_empty_things_on_service(fastapi_client_simple):
|
||||||
|
|
||||||
|
|
||||||
def test_store_things(fastapi_client_simple):
|
def test_store_things(fastapi_client_simple):
|
||||||
test_client, store, _ = fastapi_client_simple
|
test_client, _store, _ = fastapi_client_simple
|
||||||
|
|
||||||
simple_thing = {
|
simple_thing = {
|
||||||
'pid': 'http://test.simple.thing/1',
|
'pid': 'http://test.simple.thing/1',
|
||||||
|
|
@ -375,7 +384,7 @@ def test_store_things(fastapi_client_simple):
|
||||||
|
|
||||||
|
|
||||||
def test_store_complex_things(fastapi_client_simple):
|
def test_store_complex_things(fastapi_client_simple):
|
||||||
test_client, store, _ = fastapi_client_simple
|
test_client, _store, _ = fastapi_client_simple
|
||||||
|
|
||||||
complex_thing = {
|
complex_thing = {
|
||||||
'pid': 'http://test.complex.thing/1',
|
'pid': 'http://test.complex.thing/1',
|
||||||
|
|
@ -386,9 +395,9 @@ def test_store_complex_things(fastapi_client_simple):
|
||||||
'http://test.complex.thing/1.1.1': {
|
'http://test.complex.thing/1.1.1': {
|
||||||
'pid': 'http://test.complex.thing/1.1.1',
|
'pid': 'http://test.complex.thing/1.1.1',
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deposit JSON record
|
# Deposit JSON record
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import linkml.generators.common.ifabsent_processor as if_abs_proc
|
||||||
|
|
||||||
import dump_things_service.patches.ifabsent_processing
|
import dump_things_service.patches.ifabsent_processing
|
||||||
|
|
||||||
|
|
||||||
# Path to a local simple test schema
|
# Path to a local simple test schema
|
||||||
schema_dir = Path(__file__).parent / 'assets'
|
schema_dir = Path(__file__).parent / 'assets'
|
||||||
|
|
||||||
|
|
@ -17,10 +16,11 @@ def _original_uri_for(self, s: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def test_ifabsent_patch():
|
def test_ifabsent_patch():
|
||||||
|
|
||||||
# Patch in the faulty, original code and check for its result
|
# Patch in the faulty, original code and check for its result
|
||||||
if_abs_proc.IfAbsentProcessor._uri_for = _original_uri_for
|
if_abs_proc.IfAbsentProcessor._uri_for = _original_uri_for
|
||||||
gen1 = linkml.generators.PydanticGenerator(str(schema_dir / 'schema-ifabsent-error.yaml'))
|
gen1 = linkml.generators.PydanticGenerator(
|
||||||
|
str(schema_dir / 'schema-ifabsent-error.yaml')
|
||||||
|
)
|
||||||
x = gen1.serialize()
|
x = gen1.serialize()
|
||||||
assert 'default=XSD["04fa4r544"]' in x
|
assert 'default=XSD["04fa4r544"]' in x
|
||||||
|
|
||||||
|
|
@ -28,6 +28,8 @@ def test_ifabsent_patch():
|
||||||
reload(dump_things_service.patches.ifabsent_processing)
|
reload(dump_things_service.patches.ifabsent_processing)
|
||||||
|
|
||||||
# Check for proper code generation
|
# Check for proper code generation
|
||||||
gen2 = linkml.generators.PydanticGenerator(str(schema_dir / 'schema-ifabsent-error.yaml'))
|
gen2 = linkml.generators.PydanticGenerator(
|
||||||
|
str(schema_dir / 'schema-ifabsent-error.yaml')
|
||||||
|
)
|
||||||
y = gen2.serialize()
|
y = gen2.serialize()
|
||||||
assert 'XSD' not in y
|
assert 'XSD' not in y
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ def test_incoming_labels(fastapi_client_simple):
|
||||||
|
|
||||||
zones_filled = False
|
zones_filled = False
|
||||||
|
|
||||||
|
|
||||||
def fill_zones(test_client):
|
def fill_zones(test_client):
|
||||||
global zones_filled
|
global zones_filled
|
||||||
|
|
||||||
|
|
@ -53,15 +54,15 @@ def fill_zones(test_client):
|
||||||
json={
|
json={
|
||||||
'pid': f'abc:test_incoming-collection_{collection_id}-{token}',
|
'pid': f'abc:test_incoming-collection_{collection_id}-{token}',
|
||||||
'given_name': f'collection_{collection_id}-{token}',
|
'given_name': f'collection_{collection_id}-{token}',
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
assert result.status_code == HTTP_200_OK
|
assert result.status_code == HTTP_200_OK
|
||||||
|
|
||||||
zones_filled = True
|
zones_filled = True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('paginate', ('', 'p/'))
|
@pytest.mark.parametrize('paginate', ['', 'p/'])
|
||||||
@pytest.mark.parametrize('class_name', ('', 'Person'))
|
@pytest.mark.parametrize('class_name', ['', 'Person'])
|
||||||
def test_read_incoming_records(
|
def test_read_incoming_records(
|
||||||
fastapi_client_simple,
|
fastapi_client_simple,
|
||||||
paginate: str,
|
paginate: str,
|
||||||
|
|
@ -87,7 +88,9 @@ def test_read_incoming_records(
|
||||||
f'/collection_{collection_id}/incoming/{label}/records/{paginate}{class_name}',
|
f'/collection_{collection_id}/incoming/{label}/records/{paginate}{class_name}',
|
||||||
headers={'x-dumpthings-token': 'token_curator'},
|
headers={'x-dumpthings-token': 'token_curator'},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK, f'failed on collection: {collection_id}, label: {label}, class: {class_name}'
|
assert response.status_code == HTTP_200_OK, (
|
||||||
|
f'failed on collection: {collection_id}, label: {label}, class: {class_name}'
|
||||||
|
)
|
||||||
# We don't know the exact number of entries in each zone, because
|
# We don't know the exact number of entries in each zone, because
|
||||||
# it depends on the tests that ran before.
|
# it depends on the tests that ran before.
|
||||||
|
|
||||||
|
|
@ -103,22 +106,15 @@ def test_read_incoming_records(
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
json_object = response.json()
|
json_object = response.json()
|
||||||
if 'items' in json_object:
|
result = json_object['items'] if 'items' in json_object else json_object
|
||||||
result = json_object['items']
|
|
||||||
else:
|
|
||||||
result = json_object
|
|
||||||
matching = [
|
matching = [
|
||||||
json_object
|
json_object for json_object in result if json_object['pid'] == pattern
|
||||||
for json_object in result
|
|
||||||
if json_object['pid'] == pattern
|
|
||||||
]
|
]
|
||||||
assert len(matching) == expected_length, f'did not find {expected_length} record: collection_{collection_id}, {label}, {result}'
|
assert len(matching) == expected_length, (
|
||||||
|
f'did not find {expected_length} record: collection_{collection_id}, {label}, {result}'
|
||||||
|
|
||||||
pytest.mark.parametrize(
|
|
||||||
'pid',
|
|
||||||
('abc:mode_test', 'abc:some_timee@x.com', 'abc:curated'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_read_incoming_records_by_pid(fastapi_client_simple):
|
def test_read_incoming_records_by_pid(fastapi_client_simple):
|
||||||
test_client, _, _ = fastapi_client_simple
|
test_client, _, _ = fastapi_client_simple
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ def verify_modes(
|
||||||
|
|
||||||
|
|
||||||
def test_token_modes(fastapi_client_simple):
|
def test_token_modes(fastapi_client_simple):
|
||||||
test_client, store_dir, _ = fastapi_client_simple
|
test_client, _store_dir, _ = fastapi_client_simple
|
||||||
|
|
||||||
# Post a record to incoming of collections `collection_1`. We use it to
|
# Post a record to incoming of collections `collection_1`. We use it to
|
||||||
# validate read/write permissions on class-base
|
# validate read/write permissions on class-base
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import freezegun
|
import freezegun
|
||||||
import pytest # noqa F401
|
import pytest # noqa: F401
|
||||||
|
|
||||||
from .. import HTTP_200_OK
|
from .. import HTTP_200_OK
|
||||||
from ..utils import cleaned_json
|
from ..utils import cleaned_json
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import pytest # noqa F401
|
|
||||||
|
|
||||||
import freezegun
|
import freezegun
|
||||||
|
import pytest # noqa: F401
|
||||||
|
|
||||||
from .. import HTTP_200_OK
|
from .. import HTTP_200_OK
|
||||||
from ..utils import cleaned_json
|
from ..utils import cleaned_json
|
||||||
|
|
@ -144,7 +143,9 @@ def test_ttl_json_ttl_dlflatsocial(fastapi_client_simple):
|
||||||
},
|
},
|
||||||
data=ttl_input_record,
|
data=ttl_input_record,
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK, 'Response content: ' + response.content.decode()
|
assert response.status_code == HTTP_200_OK, (
|
||||||
|
'Response content: ' + response.content.decode()
|
||||||
|
)
|
||||||
|
|
||||||
# Retrieve JSON records
|
# Retrieve JSON records
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
|
|
@ -172,8 +173,12 @@ def test_ttl_json_ttl_dlflatsocial(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
assert (
|
assert (
|
||||||
response.text.strip()
|
response.text.strip()
|
||||||
== ttl_output_record_a.replace('dlflatsocial:test_john_ttl', new_json_pid).strip()
|
== ttl_output_record_a.replace(
|
||||||
|
'dlflatsocial:test_john_ttl', new_json_pid
|
||||||
|
).strip()
|
||||||
) or (
|
) or (
|
||||||
response.text.strip()
|
response.text.strip()
|
||||||
== ttl_output_record_b.replace('dlflatsocial:test_john_ttl', new_json_pid).strip()
|
== ttl_output_record_b.replace(
|
||||||
|
'dlflatsocial:test_john_ttl', new_json_pid
|
||||||
|
).strip()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,11 @@ def test_token_creation(fastapi_client_simple):
|
||||||
'user_id': 'u_a',
|
'user_id': 'u_a',
|
||||||
'representation': '8bb6805ff10bcb1c2ca49dcd4bfef94d',
|
'representation': '8bb6805ff10bcb1c2ca49dcd4bfef94d',
|
||||||
'collections': {
|
'collections': {
|
||||||
'collection_1': {
|
'collection_1': {'mode': 'WRITE_COLLECTION', 'incoming_label': 'i_a'}
|
||||||
'mode': 'WRITE_COLLECTION',
|
},
|
||||||
'incoming_label': 'i_a'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a token eith name 'a'
|
# Create a token with name 'a'
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
'/tokens',
|
'/tokens',
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
|
|
@ -34,7 +31,7 @@ def test_token_creation(fastapi_client_simple):
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_409_CONFLICT
|
assert response.status_code == HTTP_409_CONFLICT
|
||||||
|
|
||||||
# Try to create another token eith name 'b' and the same representation
|
# Try to create another token with name 'b' and the same representation
|
||||||
# as 'a', should result in a 4ß9-error
|
# as 'a', should result in a 4ß9-error
|
||||||
json_record['name'] = 'b'
|
json_record['name'] = 'b'
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from pathlib import Path
|
||||||
|
|
||||||
from .. import HTTP_200_OK
|
from .. import HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
# Path to a local simple test schema
|
# Path to a local simple test schema
|
||||||
schema_file = Path(__file__).parent / 'testschema.yaml'
|
schema_file = Path(__file__).parent / 'testschema.yaml'
|
||||||
|
|
||||||
|
|
@ -33,7 +32,7 @@ def test_unicode_iri(fastapi_client_simple):
|
||||||
headers={'x-dumpthings-token': 'token-1'},
|
headers={'x-dumpthings-token': 'token-1'},
|
||||||
json={
|
json={
|
||||||
'pid': 'https://en.wikipedia.org/wiki/Universita_degli_Studi_eCampus',
|
'pid': 'https://en.wikipedia.org/wiki/Universita_degli_Studi_eCampus',
|
||||||
'given_name': 'Università degli Studi eCampus (Italy)',
|
'given_name': 'Università degli Studi eCampus (Italy)', # codespell:ignore
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
|
||||||
from dump_things_service import HTTP_422_UNPROCESSABLE_CONTENT
|
from dump_things_service import HTTP_422_UNPROCESSABLE_CONTENT
|
||||||
|
|
||||||
json_records = [
|
json_records = [
|
||||||
({'name': 'Henry', 'pid': 'unknown_prefix:henry'}, HTTP_422_UNPROCESSABLE_CONTENT),
|
({'name': 'Henry', 'pid': 'unknown_prefix:henry'}, HTTP_422_UNPROCESSABLE_CONTENT),
|
||||||
({'given_name': 'Henry', 'pid': 'unknown_prefix:henry'}, HTTP_422_UNPROCESSABLE_CONTENT),
|
(
|
||||||
|
{'given_name': 'Henry', 'pid': 'unknown_prefix:henry'},
|
||||||
|
HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
),
|
||||||
({'given_name': 'Henry', 'pid': 'xyz:henry'}, 200),
|
({'given_name': 'Henry', 'pid': 'xyz:henry'}, 200),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pids = ('', '--------', '&&&&&', 'abc', 'abc&', 'abc&format=ttl')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'collection_name,class_name,query,format_name', # noqa PT006
|
'collection_name,class_name,query,format_name', # noqa: PT006
|
||||||
tuple(product(*(collection_names, class_names, queries, format_names))),
|
tuple(product(*(collection_names, class_names, queries, format_names))),
|
||||||
)
|
)
|
||||||
def test_web_interface_post_errors(
|
def test_web_interface_post_errors(
|
||||||
|
|
@ -35,7 +35,7 @@ def test_web_interface_post_errors(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'collection_name,class_name,query,format_name', # noqa PT006
|
'collection_name,class_name,query,format_name', # noqa: PT006
|
||||||
tuple(product(*(collection_names, class_names, queries, format_names))),
|
tuple(product(*(collection_names, class_names, queries, format_names))),
|
||||||
)
|
)
|
||||||
def test_web_interface_get_class_errors(
|
def test_web_interface_get_class_errors(
|
||||||
|
|
@ -60,7 +60,7 @@ def test_web_interface_get_class_errors(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'collection_name,pid,query,format_name', # noqa PT006
|
'collection_name,pid,query,format_name', # noqa: PT006
|
||||||
tuple(product(*(collection_names, pids, queries, format_names))),
|
tuple(product(*(collection_names, pids, queries, format_names))),
|
||||||
)
|
)
|
||||||
def test_web_interface_get_pid_errors(
|
def test_web_interface_get_pid_errors(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
from typing import Annotated
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
|
|
@ -30,12 +31,11 @@ from dump_things_service.abstract_config import (
|
||||||
)
|
)
|
||||||
from dump_things_service.admin import authenticate_admin
|
from dump_things_service.admin import authenticate_admin
|
||||||
from dump_things_service.api_key import api_key_header_scheme
|
from dump_things_service.api_key import api_key_header_scheme
|
||||||
from dump_things_service.instance_state import get_instance_state
|
|
||||||
from dump_things_service.exceptions import ConfigError
|
from dump_things_service.exceptions import ConfigError
|
||||||
|
from dump_things_service.instance_state import get_instance_state
|
||||||
from dump_things_service.manifest import manifest_configuration
|
from dump_things_service.manifest import manifest_configuration
|
||||||
from dump_things_service.utils import wrap_http_exception
|
from dump_things_service.utils import wrap_http_exception
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('dump_things_service')
|
logger = logging.getLogger('dump_things_service')
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -73,9 +73,8 @@ def get_token_parts(token: str) -> list[str]:
|
||||||
async def create_token(
|
async def create_token(
|
||||||
response: Response,
|
response: Response,
|
||||||
body: TokenRequest,
|
body: TokenRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> TokenRequest:
|
) -> TokenRequest:
|
||||||
|
|
||||||
token_request = create_or_replace_token(body, api_key, allow_replace=False)
|
token_request = create_or_replace_token(body, api_key, allow_replace=False)
|
||||||
response.headers['Location'] = f'/tokens/{quote(body.name)}'
|
response.headers['Location'] = f'/tokens/{quote(body.name)}'
|
||||||
return token_request
|
return token_request
|
||||||
|
|
@ -90,9 +89,8 @@ async def create_token(
|
||||||
async def replace_token(
|
async def replace_token(
|
||||||
response: Response,
|
response: Response,
|
||||||
body: TokenRequest,
|
body: TokenRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> TokenRequest:
|
) -> TokenRequest:
|
||||||
|
|
||||||
token_request = create_or_replace_token(body, api_key, allow_replace=True)
|
token_request = create_or_replace_token(body, api_key, allow_replace=True)
|
||||||
response.headers['Location'] = f'/tokens/{quote(body.name)}'
|
response.headers['Location'] = f'/tokens/{quote(body.name)}'
|
||||||
return token_request
|
return token_request
|
||||||
|
|
@ -104,7 +102,6 @@ def create_or_replace_token(
|
||||||
*,
|
*,
|
||||||
allow_replace: bool,
|
allow_replace: bool,
|
||||||
) -> TokenRequest:
|
) -> TokenRequest:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = read_config(store_path=instance_state.store_path)
|
abstract_config = read_config(store_path=instance_state.store_path)
|
||||||
|
|
||||||
|
|
@ -126,12 +123,11 @@ def create_or_replace_token(
|
||||||
# Check that incoming areas are defined if the token allows writing.
|
# Check that incoming areas are defined if the token allows writing.
|
||||||
token_permissions = get_token_permissions(token_collection_info.mode)
|
token_permissions = get_token_permissions(token_collection_info.mode)
|
||||||
if token_permissions.incoming_write or token_permissions.zones_access:
|
if token_permissions.incoming_write or token_permissions.zones_access:
|
||||||
|
|
||||||
# Check for incoming definition in collection config
|
# Check for incoming definition in collection config
|
||||||
collection_info = abstract_config.collections[collection_name]
|
collection_info = abstract_config.collections[collection_name]
|
||||||
if not collection_info.incoming:
|
if not collection_info.incoming:
|
||||||
detail = (
|
detail = (
|
||||||
f"Cannot add token with write access to collection "
|
f'Cannot add token with write access to collection '
|
||||||
f"'{collection_name}' without `incoming`."
|
f"'{collection_name}' without `incoming`."
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -154,7 +150,7 @@ def create_or_replace_token(
|
||||||
token_representation=body.representation,
|
token_representation=body.representation,
|
||||||
)
|
)
|
||||||
if existing_token_info:
|
if existing_token_info:
|
||||||
detail= f"Token with identical representation already exists."
|
detail = 'Token with identical representation already exists.'
|
||||||
raise HTTPException(status_code=HTTP_409_CONFLICT, detail=detail)
|
raise HTTPException(status_code=HTTP_409_CONFLICT, detail=detail)
|
||||||
else:
|
else:
|
||||||
# Generate a random representation that does not yet exist.
|
# Generate a random representation that does not yet exist.
|
||||||
|
|
@ -203,9 +199,8 @@ def create_or_replace_token(
|
||||||
name='Get existing tokens',
|
name='Get existing tokens',
|
||||||
)
|
)
|
||||||
async def get_tokens(
|
async def get_tokens(
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> list[TokenRequest]:
|
) -> list[TokenRequest]:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = read_config(store_path=instance_state.store_path)
|
abstract_config = read_config(store_path=instance_state.store_path)
|
||||||
|
|
||||||
|
|
@ -230,9 +225,8 @@ async def get_tokens(
|
||||||
)
|
)
|
||||||
async def get_token_with_name(
|
async def get_token_with_name(
|
||||||
token_name: str,
|
token_name: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> TokenRequest:
|
) -> TokenRequest:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -260,9 +254,8 @@ async def get_token_with_name(
|
||||||
)
|
)
|
||||||
async def delete_token_with_name(
|
async def delete_token_with_name(
|
||||||
token_name: str,
|
token_name: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -295,7 +288,7 @@ async def delete_token_with_name(
|
||||||
)
|
)
|
||||||
async def create_admin_token(
|
async def create_admin_token(
|
||||||
body: AdminTokenRequest,
|
body: AdminTokenRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return create_or_replace_admin_token(body, api_key, allow_replace=False)
|
return create_or_replace_admin_token(body, api_key, allow_replace=False)
|
||||||
|
|
||||||
|
|
@ -308,7 +301,7 @@ async def create_admin_token(
|
||||||
)
|
)
|
||||||
async def replace_admin_token(
|
async def replace_admin_token(
|
||||||
body: AdminTokenRequest,
|
body: AdminTokenRequest,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
return create_or_replace_admin_token(body, api_key, allow_replace=True)
|
return create_or_replace_admin_token(body, api_key, allow_replace=True)
|
||||||
|
|
||||||
|
|
@ -369,7 +362,7 @@ def create_or_replace_admin_token(
|
||||||
name='Get admin token names',
|
name='Get admin token names',
|
||||||
)
|
)
|
||||||
async def get_admin_token(
|
async def get_admin_token(
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = read_config(store_path=instance_state.store_path)
|
abstract_config = read_config(store_path=instance_state.store_path)
|
||||||
|
|
@ -377,10 +370,7 @@ async def get_admin_token(
|
||||||
authenticate_admin(instance_state, abstract_config, api_key)
|
authenticate_admin(instance_state, abstract_config, api_key)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{'name': token_name, **(token_value.model_dump(mode='json', by_alias=True))}
|
||||||
'name': token_name,
|
|
||||||
**(token_value.model_dump(mode='json', by_alias=True))
|
|
||||||
}
|
|
||||||
for token_name, token_value in abstract_config.admin_tokens.items()
|
for token_name, token_value in abstract_config.admin_tokens.items()
|
||||||
] + (
|
] + (
|
||||||
[]
|
[]
|
||||||
|
|
@ -401,9 +391,8 @@ async def get_admin_token(
|
||||||
)
|
)
|
||||||
async def delete_admin_token(
|
async def delete_admin_token(
|
||||||
token_name: str,
|
token_name: str,
|
||||||
api_key: str = Depends(api_key_header_scheme),
|
api_key: Annotated[str, Depends(api_key_header_scheme)],
|
||||||
):
|
):
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = read_config(store_path=instance_state.store_path)
|
abstract_config = read_config(store_path=instance_state.store_path)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ To speed up processing, multiple indices could be introduced, e.g.:
|
||||||
- token representation -> token name
|
- token representation -> token name
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -31,12 +32,10 @@ from dump_things_service.abstract_config import (
|
||||||
Configuration,
|
Configuration,
|
||||||
TokenModes,
|
TokenModes,
|
||||||
TokenPermission,
|
TokenPermission,
|
||||||
mode_mapping,
|
|
||||||
check_collection,
|
check_collection,
|
||||||
get_collection_config_by_name,
|
get_collection_config_by_name,
|
||||||
get_default_token_config,
|
|
||||||
get_mapping_function_by_name,
|
get_mapping_function_by_name,
|
||||||
get_token_config_for_representation_and_collection,
|
mode_mapping,
|
||||||
)
|
)
|
||||||
from dump_things_service.auth import (
|
from dump_things_service.auth import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
|
@ -83,7 +82,7 @@ def cleaned_json(data: JSON, remove_keys: tuple[str, ...] = ('@type',)) -> JSON:
|
||||||
return {
|
return {
|
||||||
key: cleaned_json(value, remove_keys)
|
key: cleaned_json(value, remove_keys)
|
||||||
for key, value in data.items()
|
for key, value in data.items()
|
||||||
if key not in remove_keys and data[key] is not None
|
if key not in remove_keys and value is not None
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -97,7 +96,7 @@ def combine_ttl(documents: list[str]) -> str:
|
||||||
def wrap_http_exception(
|
def wrap_http_exception(
|
||||||
exception_class: type[BaseException] = ValueError,
|
exception_class: type[BaseException] = ValueError,
|
||||||
status_code: int = HTTP_400_BAD_REQUEST,
|
status_code: int = HTTP_400_BAD_REQUEST,
|
||||||
header: str = ''
|
header: str = '',
|
||||||
):
|
):
|
||||||
"""Wrap exceptions of class `exception_class` into HTTP exceptions"""
|
"""Wrap exceptions of class `exception_class` into HTTP exceptions"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -115,7 +114,6 @@ def join_default_token_permissions(
|
||||||
permissions: TokenPermission,
|
permissions: TokenPermission,
|
||||||
collection: str,
|
collection: str,
|
||||||
) -> TokenPermission:
|
) -> TokenPermission:
|
||||||
|
|
||||||
result = permissions.model_copy()
|
result = permissions.model_copy()
|
||||||
|
|
||||||
# Get the default token name. If a default token is not defined, return
|
# Get the default token name. If a default token is not defined, return
|
||||||
|
|
@ -134,7 +132,9 @@ def join_default_token_permissions(
|
||||||
if collection not in abstract_configuration.tokens[default_token_name].collections:
|
if collection not in abstract_configuration.tokens[default_token_name].collections:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
default_token_mode = abstract_configuration.tokens[default_token_name].collections[collection].mode
|
default_token_mode = (
|
||||||
|
abstract_configuration.tokens[default_token_name].collections[collection].mode
|
||||||
|
)
|
||||||
default_token_permissions = mode_mapping[TokenModes(default_token_mode)]
|
default_token_permissions = mode_mapping[TokenModes(default_token_mode)]
|
||||||
result.curated_read = (
|
result.curated_read = (
|
||||||
permissions.curated_read | default_token_permissions.curated_read
|
permissions.curated_read | default_token_permissions.curated_read
|
||||||
|
|
@ -155,17 +155,11 @@ def get_on_disk_labels(
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
check_collection(abstract_config, collection)
|
check_collection(abstract_config, collection)
|
||||||
|
|
||||||
incoming_path = (
|
incoming_path = store_path / abstract_config.collections[collection].incoming
|
||||||
store_path / abstract_config.collections[collection].incoming
|
|
||||||
)
|
|
||||||
if not incoming_path or not incoming_path.exists():
|
if not incoming_path or not incoming_path.exists():
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
return {
|
return {path.name for path in incoming_path.iterdir() if path.is_dir()}
|
||||||
path.name
|
|
||||||
for path in incoming_path.iterdir()
|
|
||||||
if path.is_dir()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate_token(
|
def authenticate_token(
|
||||||
|
|
@ -173,7 +167,6 @@ def authenticate_token(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
token_representation: str,
|
token_representation: str,
|
||||||
) -> AuthenticationInfo:
|
) -> AuthenticationInfo:
|
||||||
|
|
||||||
# Try to authenticate the token with the authentication providers that
|
# Try to authenticate the token with the authentication providers that
|
||||||
# are associated with the collection.
|
# are associated with the collection.
|
||||||
auth_info = None
|
auth_info = None
|
||||||
|
|
@ -233,7 +226,6 @@ def get_token_store(
|
||||||
*,
|
*,
|
||||||
is_token_name: bool = False,
|
is_token_name: bool = False,
|
||||||
) -> tuple[_ModelStore, TokenPermission, str] | tuple[None, None, None, None]:
|
) -> tuple[_ModelStore, TokenPermission, str] | tuple[None, None, None, None]:
|
||||||
|
|
||||||
# If a token representation is provided, try to authenticate the token
|
# If a token representation is provided, try to authenticate the token
|
||||||
# with the authentication providers that are associated with the collection.
|
# with the authentication providers that are associated with the collection.
|
||||||
if not is_token_name:
|
if not is_token_name:
|
||||||
|
|
@ -279,11 +271,13 @@ def get_token_store(
|
||||||
if not incoming:
|
if not incoming:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
detail='No incoming area for collection ' + collection_name
|
detail='No incoming area for collection ' + collection_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check whether a store for this collection and token does already exist.
|
# Check whether a store for this collection and token does already exist.
|
||||||
store_info = instance_state.incoming_stores[collection_name].get(token_representation)
|
store_info = instance_state.incoming_stores[collection_name].get(
|
||||||
|
token_representation
|
||||||
|
)
|
||||||
if store_info:
|
if store_info:
|
||||||
return store_info
|
return store_info
|
||||||
|
|
||||||
|
|
@ -308,7 +302,9 @@ def create_store(
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
) -> _ModelStore:
|
) -> _ModelStore:
|
||||||
collection_curated_path = abstract_configuration.collections[collection_name].curated
|
collection_curated_path = abstract_configuration.collections[
|
||||||
|
collection_name
|
||||||
|
].curated
|
||||||
return create_token_store(
|
return create_token_store(
|
||||||
abstract_configuration=abstract_configuration,
|
abstract_configuration=abstract_configuration,
|
||||||
instance_state=instance_state,
|
instance_state=instance_state,
|
||||||
|
|
@ -323,8 +319,8 @@ def create_token_store(
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
store_dir: Path,
|
store_dir: Path,
|
||||||
) -> _ModelStore:
|
) -> _ModelStore:
|
||||||
from dump_things_service.backends.schema_type_layer import SchemaTypeLayer
|
|
||||||
from dump_things_service.abstract_config import get_backend_and_extension
|
from dump_things_service.abstract_config import get_backend_and_extension
|
||||||
|
from dump_things_service.backends.schema_type_layer import SchemaTypeLayer
|
||||||
from dump_things_service.exceptions import ConfigError
|
from dump_things_service.exceptions import ConfigError
|
||||||
from dump_things_service.store.model_store import ModelStore
|
from dump_things_service.store.model_store import ModelStore
|
||||||
|
|
||||||
|
|
@ -354,7 +350,6 @@ def create_token_store(
|
||||||
backend_config = abstract_configuration.collections[collection_name].backend
|
backend_config = abstract_configuration.collections[collection_name].backend
|
||||||
backend_name, extension = get_backend_and_extension(backend_config.type)
|
backend_name, extension = get_backend_and_extension(backend_config.type)
|
||||||
if backend_name == 'record_dir':
|
if backend_name == 'record_dir':
|
||||||
|
|
||||||
backend = create_record_dir_token_store_backend(
|
backend = create_record_dir_token_store_backend(
|
||||||
store_dir=store_dir,
|
store_dir=store_dir,
|
||||||
order_by=instance_state.order_by,
|
order_by=instance_state.order_by,
|
||||||
|
|
@ -376,7 +371,9 @@ def create_token_store(
|
||||||
if extension == 'stl':
|
if extension == 'stl':
|
||||||
backend = SchemaTypeLayer(backend=backend, schema=schema_uri)
|
backend = SchemaTypeLayer(backend=backend, schema=schema_uri)
|
||||||
|
|
||||||
submission_tags = abstract_configuration.collections[collection_name].submission_tags
|
submission_tags = abstract_configuration.collections[
|
||||||
|
collection_name
|
||||||
|
].submission_tags
|
||||||
return ModelStore(
|
return ModelStore(
|
||||||
schema=schema_uri,
|
schema=schema_uri,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
|
|
@ -394,8 +391,8 @@ def create_record_dir_token_store_backend(
|
||||||
mapping_function: str,
|
mapping_function: str,
|
||||||
suffix: str,
|
suffix: str,
|
||||||
) -> _RecordDirStore:
|
) -> _RecordDirStore:
|
||||||
from dump_things_service.instance_state import record_dir_config_file_name
|
|
||||||
from dump_things_service.backends.record_dir import RecordDirStore
|
from dump_things_service.backends.record_dir import RecordDirStore
|
||||||
|
from dump_things_service.instance_state import record_dir_config_file_name
|
||||||
|
|
||||||
# Write the configuration to the store, if it does not yet exist.
|
# Write the configuration to the store, if it does not yet exist.
|
||||||
if not (store_dir / record_dir_config_file_name).exists():
|
if not (store_dir / record_dir_config_file_name).exists():
|
||||||
|
|
@ -424,7 +421,8 @@ def write_record_dir_config(
|
||||||
|
|
||||||
record_dir_config_file_path = path / record_dir_config_file_name
|
record_dir_config_file_path = path / record_dir_config_file_name
|
||||||
if not record_dir_config_file_path.exists():
|
if not record_dir_config_file_path.exists():
|
||||||
record_dir_config_file_path.write_text(f"""# RecordDir Config
|
record_dir_config_file_path.write_text(
|
||||||
|
f"""# RecordDir Config
|
||||||
type: records
|
type: records
|
||||||
version: 1
|
version: 1
|
||||||
schema: {schema}
|
schema: {schema}
|
||||||
|
|
@ -450,10 +448,7 @@ def create_sqlite_token_store_backend(
|
||||||
|
|
||||||
|
|
||||||
def check_bounds(
|
def check_bounds(
|
||||||
length: int | None,
|
length: int | None, max_length: int, collection: str, alternative_url: str
|
||||||
max_length: int,
|
|
||||||
collection: str,
|
|
||||||
alternative_url: str
|
|
||||||
):
|
):
|
||||||
if length > max_length:
|
if length > max_length:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -469,7 +464,6 @@ async def process_token(
|
||||||
api_key: str | None,
|
api_key: str | None,
|
||||||
collection: str,
|
collection: str,
|
||||||
) -> tuple[TokenPermission, _ModelStore]:
|
) -> tuple[TokenPermission, _ModelStore]:
|
||||||
|
|
||||||
if api_key is None:
|
if api_key is None:
|
||||||
collection_config = get_collection_config_by_name(abstract_config, collection)
|
collection_config = get_collection_config_by_name(abstract_config, collection)
|
||||||
token_store, token_permissions, user_id = get_token_store(
|
token_store, token_permissions, user_id = get_token_store(
|
||||||
|
|
@ -480,7 +474,7 @@ async def process_token(
|
||||||
is_token_name=True,
|
is_token_name=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
token_store, token_permissions, user_id = get_token_store(
|
token_store, token_permissions, _user_id = get_token_store(
|
||||||
abstract_config,
|
abstract_config,
|
||||||
instance_state,
|
instance_state,
|
||||||
collection,
|
collection,
|
||||||
|
|
@ -492,8 +486,7 @@ async def process_token(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for maintenance mode
|
# Check for maintenance mode
|
||||||
if collection in instance_state.maintenance_mode:
|
if collection in instance_state.maintenance_mode and not (
|
||||||
if not (
|
|
||||||
final_permissions.curated_read
|
final_permissions.curated_read
|
||||||
and final_permissions.curated_write
|
and final_permissions.curated_write
|
||||||
and final_permissions.zones_access
|
and final_permissions.zones_access
|
||||||
|
|
@ -515,12 +508,7 @@ def get_required_incoming_labels(
|
||||||
abstract_config: Configuration,
|
abstract_config: Configuration,
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
return set(
|
return {x[1] for x in get_required_incoming_info(abstract_config, collection_name)}
|
||||||
map(
|
|
||||||
lambda x: x[1],
|
|
||||||
get_required_incoming_info(abstract_config, collection_name),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_required_incoming_info(
|
def get_required_incoming_info(
|
||||||
|
|
@ -531,9 +519,8 @@ def get_required_incoming_info(
|
||||||
(token_name, this_collection_info.incoming_label)
|
(token_name, this_collection_info.incoming_label)
|
||||||
for token_name, token_info in abstract_config.tokens.items()
|
for token_name, token_info in abstract_config.tokens.items()
|
||||||
for this_collection_name, this_collection_info in token_info.collections.items()
|
for this_collection_name, this_collection_info in token_info.collections.items()
|
||||||
if this_collection_name == collection_name and mode_mapping[
|
if this_collection_name == collection_name
|
||||||
TokenModes(this_collection_info.mode)
|
and mode_mapping[TokenModes(this_collection_info.mode)].incoming_write is True
|
||||||
].incoming_write is True
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ def validate_record(
|
||||||
_: bool,
|
_: bool,
|
||||||
api_key: str | None = Depends(api_key_header_scheme),
|
api_key: str | None = Depends(api_key_header_scheme),
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
|
||||||
instance_state = get_instance_state()
|
instance_state = get_instance_state()
|
||||||
abstract_config = get_config()
|
abstract_config = get_config()
|
||||||
|
|
||||||
|
|
@ -63,7 +62,7 @@ def validate_record(
|
||||||
else api_key
|
else api_key
|
||||||
)
|
)
|
||||||
|
|
||||||
store, token_permissions, user_id = get_token_store(
|
_store, token_permissions, _user_id = get_token_store(
|
||||||
abstract_config,
|
abstract_config,
|
||||||
instance_state,
|
instance_state,
|
||||||
collection,
|
collection,
|
||||||
|
|
@ -82,18 +81,30 @@ def validate_record(
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_format == Format.ttl:
|
if input_format == Format.ttl:
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Conversion error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Conversion error',
|
||||||
|
):
|
||||||
json_object = FormatConverter(
|
json_object = FormatConverter(
|
||||||
abstract_config.collections[collection].schema_location,
|
abstract_config.collections[collection].schema_location,
|
||||||
input_format=Format.ttl,
|
input_format=Format.ttl,
|
||||||
output_format=Format.json,
|
output_format=Format.json,
|
||||||
).convert(data, class_name)
|
).convert(data, class_name)
|
||||||
with wrap_http_exception(ValidationError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
|
ValidationError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
TypeAdapter(getattr(model, class_name)).validate_python(json_object)
|
TypeAdapter(getattr(model, class_name)).validate_python(json_object)
|
||||||
else:
|
else:
|
||||||
# Try to convert it into TTL to detect potential errors before storing
|
# Try to convert it into TTL to detect potential errors before storing
|
||||||
# the record
|
# the record
|
||||||
with wrap_http_exception(ValueError, status_code=HTTP_422_UNPROCESSABLE_CONTENT, header='Validation error'):
|
with wrap_http_exception(
|
||||||
|
ValueError,
|
||||||
|
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
header='Validation error',
|
||||||
|
):
|
||||||
instance_state.validators[collection].validate(data)
|
instance_state.validators[collection].validate(data)
|
||||||
|
|
||||||
return JSONResponse(True)
|
return JSONResponse(True)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = [
|
||||||
|
"hatchling",
|
||||||
|
"hatch-vcs",
|
||||||
|
]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
|
|
@ -17,9 +20,7 @@ classifiers = [
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
|
@ -42,9 +43,20 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Documentation = "https://hub.psychoinformatics.de/datalink/dump-things-server"
|
Documentation = "https://hub.psychoinformatics.de/orinoco/dump-things-server"
|
||||||
Issues = "https://codeberg.org/datalink/dump-things-server/issues"
|
Issues = "https://codeberg.org/datalink/dump-things-server/issues"
|
||||||
Source = "https://hub.psychoinformatics.de/datalink/dump-things-server"
|
Source = "https://hub.psychoinformatics.de/orinoco/dump-things-server"
|
||||||
|
Changelog = "https://hub.psychoinformatics.de/orinoco/dump-things-server/src/branch/master/CHANGELOG.md"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# this is what readthedocs consumes to decide what needs to be installed
|
||||||
|
# for compiling the docs
|
||||||
|
docs = [
|
||||||
|
"pytest",
|
||||||
|
"sphinx",
|
||||||
|
"sphinx_rtd_theme",
|
||||||
|
"sphinx_autodoc_typehints",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
dump-things-service = "dump_things_service.main:main"
|
dump-things-service = "dump_things_service.main:main"
|
||||||
|
|
@ -75,14 +87,36 @@ only-include = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.version]
|
[tool.hatch.version]
|
||||||
path = "dump_things_service/__about__.py"
|
source = "vcs"
|
||||||
|
|
||||||
|
[tool.hatch.build.hooks.vcs]
|
||||||
|
version-file = "dump_things_service/_version.py"
|
||||||
|
|
||||||
[tool.hatch.envs.types]
|
[tool.hatch.envs.types]
|
||||||
extra-dependencies = [
|
extra-dependencies = [
|
||||||
"mypy>=1.0.0",
|
"mypy>=1.0.0",
|
||||||
]
|
]
|
||||||
[tool.hatch.envs.types.scripts]
|
[tool.hatch.envs.types.scripts]
|
||||||
check = "mypy --install-types --non-interactive {args:src tests}"
|
check = "mypy --install-types --non-interactive --python-version 3.11 --follow-imports skip --pretty --show-error-context {args:dump_things_service}"
|
||||||
|
|
||||||
|
[tool.hatch.envs.docs]
|
||||||
|
description = "build Sphinx-based docs"
|
||||||
|
# also see project.optional-dependencies.docs!
|
||||||
|
# this is not considered by readthedocs
|
||||||
|
extra-dependencies = [
|
||||||
|
"pytest",
|
||||||
|
"sphinx",
|
||||||
|
"sphinx_rtd_theme",
|
||||||
|
"sphinx-autodoc-typehints",
|
||||||
|
]
|
||||||
|
[tool.hatch.envs.docs.scripts]
|
||||||
|
build = [
|
||||||
|
"make -C docs html",
|
||||||
|
]
|
||||||
|
clean = [
|
||||||
|
"rm -rf docs/generated",
|
||||||
|
"make -C docs clean",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source_pkgs = ["dump_things_service"]
|
source_pkgs = ["dump_things_service"]
|
||||||
|
|
@ -106,21 +140,19 @@ description = "fastapi dev environment"
|
||||||
[tool.hatch.envs.fastapi.scripts]
|
[tool.hatch.envs.fastapi.scripts]
|
||||||
run = "python -m dump_things_service.main {args}"
|
run = "python -m dump_things_service.main {args}"
|
||||||
|
|
||||||
[[tool.hatch.envs.tests.matrix]]
|
[[tool.hatch.envs.hatch-test.matrix]]
|
||||||
python = ["3.11", "3.12"]
|
python = ["3.11", "3.12"]
|
||||||
|
|
||||||
[tool.hatch.envs.tests]
|
[tool.hatch.envs.hatch-test]
|
||||||
|
default-args = ["dump_things_service"]
|
||||||
extra-dependencies = [
|
extra-dependencies = [
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"httpx",
|
"httpx2",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-httpserver",
|
"pytest-httpserver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.envs.tests.scripts]
|
|
||||||
run = 'python -m pytest {args}'
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
# sphinx
|
# sphinx
|
||||||
|
|
@ -130,7 +162,7 @@ extend-exclude = [
|
||||||
]
|
]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
target-version = "py39"
|
target-version = "py311"
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
# Prefer single quotes over double quotes.
|
# Prefer single quotes over double quotes.
|
||||||
quote-style = "single"
|
quote-style = "single"
|
||||||
|
|
@ -152,3 +184,6 @@ skip = '.git*'
|
||||||
check-hidden = true
|
check-hidden = true
|
||||||
# ignore-regex = ''
|
# ignore-regex = ''
|
||||||
# ignore-words-list = ''
|
# ignore-words-list = ''
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
disable_error_code = ["import-untyped"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue