Do not create automated submission-tags by default #240

Merged
cmo merged 4 commits from no-forced-submission-tags into master 2026-06-29 09:28:23 +00:00
10 changed files with 80 additions and 31 deletions

View file

@ -1,20 +1,30 @@
# 6.3.0 (2026-06-29)
## Changes
- Automated annotation adding in write-record-endpoints has been removed.
The new query parameter `add_submission_tag` is added to write-record-endpoints.
If set to True, the server will add an automated annotation with the annotation
tags that are defined in the server-configuration (the default is False).
# 6.2.2 (2026-06-26) # 6.2.2 (2026-06-26)
# Bugfixes ## Bugfixes
- Fix a bug in authentication when non-config tokens are used. - Fix a bug in authentication when non-config tokens are used.
# 6.2.1 (2026-06-26) # 6.2.1 (2026-06-26)
# Bugfixes ## Bugfixes
- Fix a bug in curator-writing when non-config tokens are used. - Fix a bug in curator-writing when non-config tokens are used.
# 6.2.0 (2026-06-24) # 6.2.0 (2026-06-24)
# New features ## New features
- Add PUT-methods to the administration endpoints `/tokens`, `/collections` and - Add PUT-methods to the administration endpoints `/tokens`, `/collections` and
`/admin_tokens`. This allows to update collections, tokens, and `/admin_tokens`. This allows to update collections, tokens, and
@ -31,7 +41,7 @@
# 6.1.0 (2026-06-22) # 6.1.0 (2026-06-22)
# New features ## New features
- Add the option: `--ignore-default-config-file`. If the option is provided - Add the option: `--ignore-default-config-file`. If the option is provided
a version 6 server will not try to initialize its configuration from the a version 6 server will not try to initialize its configuration from the

View file

@ -1 +1 @@
__version__ = '6.2.2' __version__ = '6.3.0'

View file

@ -84,10 +84,11 @@ _endpoint_template = """
def {name}( def {name}(
data: {model_var_name}.{class_name} | Annotated[str, Body(media_type='text/plain')], data: {model_var_name}.{class_name} | Annotated[str, Body(media_type='text/plain')],
api_key: str = Depends(api_key_header_scheme), api_key: str = Depends(api_key_header_scheme),
add_submission_tag: bool = False,
format: Format = Format.json, format: Format = Format.json,
) -> JSONResponse | PlainTextResponse: ) -> JSONResponse | PlainTextResponse:
logger.info('{name}(%s, %s, %s, %s)', repr(data), repr('{class_name}'), repr({model_var_name}), repr(format)) logger.info('{name}(%s, %s, %s, %s, %s)', repr(data), repr('{class_name}'), repr({model_var_name}), repr(add_submission_tag), repr(format))
return {handler}('{collection}', data, '{class_name}', {model_var_name}, format, api_key) return {handler}('{collection}', data, '{class_name}', {model_var_name}, format, add_submission_tag, api_key)
""" """
_endpoint_curated_template = """ _endpoint_curated_template = """
@ -536,6 +537,7 @@ def store_record(
class_name: str, class_name: str,
model: Any, model: Any,
input_format: Format, input_format: Format,
add_submission_tag: bool,
api_key: str | None = Depends(api_key_header_scheme), api_key: str | None = Depends(api_key_header_scheme),
) -> JSONResponse | PlainTextResponse: ) -> JSONResponse | PlainTextResponse:
if input_format == Format.json and isinstance(data, str): if input_format == Format.json and isinstance(data, str):
@ -597,7 +599,10 @@ def store_record(
instance_state.validators[collection].validate(record) instance_state.validators[collection].validate(record)
with wrap_http_exception(CurieResolutionError): with wrap_http_exception(CurieResolutionError):
stored_records = store.store_object(obj=record, submitter=user_id) stored_records = store.store_object(
obj=record,
submitter=user_id if add_submission_tag else None,
)
if input_format == Format.ttl: if input_format == Format.ttl:
format_converter = FormatConverter( format_converter = FormatConverter(

View file

@ -24,7 +24,6 @@ from dump_things_service import (
from dump_things_service.abstract_config import ( from dump_things_service.abstract_config import (
Configuration, Configuration,
CollectionConfig, CollectionConfig,
StrictModel,
store_config, store_config,
get_config, get_token_permissions, get_config, get_token_permissions,
) )
@ -59,9 +58,6 @@ class TagSpec(BaseModel):
submission_time_tag: str = 'http://semanticscience.org/resource/SIO_001083' submission_time_tag: str = 'http://semanticscience.org/resource/SIO_001083'
from pydantic import ConfigDict, Field
from dump_things_service.abstract_config import RecordDirBackendConfig, SQLiteBackendConfig, GitAuditBackendConfig
class CollectionRequest(CollectionConfig): class CollectionRequest(CollectionConfig):
name: str name: str
@ -124,7 +120,7 @@ async def create_or_replace_collection(
# Check for distinct directories. # Check for distinct directories.
# TODO: we skip this currently because a number of version 5 installations # TODO: we skip this currently because a number of version 5 installations
# deliberately put inboxes into the same path. Those configuration cannot # deliberately put inboxes into the same path. Those configurations cannot
# be established if this check is performed. Instead of the `if False:`- # be established if this check is performed. Instead of the `if False:`-
# clause, we should introduce a configuration for the server to specify # clause, we should introduce a configuration for the server to specify
# whether unique directories are required. # whether unique directories are required.

View file

@ -45,7 +45,7 @@ class _ModelStore:
def store_object( def store_object(
self, self,
obj: BaseModel, obj: BaseModel,
submitter: str, 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 dict()):
return [] return []
@ -72,7 +72,7 @@ class _ModelStore:
def _store_flat_object( def _store_flat_object(
self, self,
obj: BaseModel, obj: BaseModel,
submitter: str, submitter: str | None,
) -> dict: ) -> dict:
iri = self.pid_to_iri(obj.pid) iri = self.pid_to_iri(obj.pid)
class_name = obj.__class__.__name__ class_name = obj.__class__.__name__
@ -83,7 +83,9 @@ class _ModelStore:
) )
# Add the submitter id to the record annotations # Add the submitter id to the record annotations
self.annotate(json_object, submitter) if submitter:
self.annotate(json_object, submitter)
self.backend.add_record( self.backend.add_record(
iri=iri, iri=iri,
class_name=class_name, class_name=class_name,

View file

@ -202,9 +202,7 @@ def test_store_record(fastapi_client_simple):
headers={'x-dumpthings-token': token}, headers={'x-dumpthings-token': token},
) )
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
assert ( assert response.json() == extra_record
cleaned_json(response.json(), remove_keys=('annotations',)) == extra_record
)
# Check that other collections do not report the new record # Check that other collections do not report the new record
for i in range(3, 6): for i in range(3, 6):
@ -223,13 +221,12 @@ def test_store_record(fastapi_client_simple):
f'/collection_{i}/records/Thing', f'/collection_{i}/records/Thing',
headers={'x-dumpthings-token': token}, headers={'x-dumpthings-token': token},
) )
cleaned_response = cleaned_json(response.json(), remove_keys=('annotations',)) assert extra_record in response.json()
assert extra_record in cleaned_response
assert { assert {
'schema_type': 'abc:Person', 'schema_type': 'abc:Person',
'pid': pid, 'pid': pid,
'given_name': given_name, 'given_name': given_name,
} in cleaned_response } in response.json()
# Check pagination # Check pagination
for i, token in basic_write_locations: for i, token in basic_write_locations:
@ -240,14 +237,12 @@ def test_store_record(fastapi_client_simple):
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
for key in ('items', 'total', 'page', 'size', 'pages'): for key in ('items', 'total', 'page', 'size', 'pages'):
assert key in response.json() assert key in response.json()
records = response.json()['items'] assert extra_record in response.json()['items']
cleaned_response = cleaned_json(records, remove_keys=('annotations',))
assert extra_record in cleaned_response
assert { assert {
'schema_type': 'abc:Person', 'schema_type': 'abc:Person',
'pid': pid, 'pid': pid,
'given_name': given_name, 'given_name': given_name,
} in cleaned_response } in response.json()['items']
def test_encoding(fastapi_client_simple): def test_encoding(fastapi_client_simple):

View file

@ -144,7 +144,7 @@ def test_collection_adding(fastapi_client_simple):
headers={'x-dumpthings-token': new_token_representation}, headers={'x-dumpthings-token': new_token_representation},
) )
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
assert cleaned_json(response.json()[0], ('annotations',)) == new_record assert response.json()[0] == new_record
# Remove the token # Remove the token
response = test_client.delete( response = test_client.delete(

View file

@ -24,6 +24,30 @@ ttl_result_record_a = """@prefix abc: <http://example.org/person-schema/abc/> .
@prefix oxo: <http://purl.obolibrary.org/obo/> . @prefix oxo: <http://purl.obolibrary.org/obo/> .
@prefix xyz: <http://example.org/person-schema/xyz/> . @prefix xyz: <http://example.org/person-schema/xyz/> .
xyz:HenryAdams a abc:Person ;
abc:annotations [ a abc:Annotation ;
abc:annotation_tag <http://semanticscience.org/resource/SIO_001083> ;
abc:annotation_value "1970-01-01T00:00:00" ] ;
abc:given_name "Henryöäß" ;
abc:schema_type "abc:Person" .
"""
ttl_result_record_b = """@prefix abc: <http://example.org/person-schema/abc/> .
@prefix oxo: <http://purl.obolibrary.org/obo/> .
@prefix xyz: <http://example.org/person-schema/xyz/> .
xyz:HenryAdams a abc:Person ;
abc:annotations [ a abc:Annotation ;
abc:annotation_tag oxo:NCIT_C54269 ;
abc:annotation_value "test_user_1" ] ;
abc:given_name "Henryöäß" ;
abc:schema_type "abc:Person" .
"""
ttl_result_record_with_annotations_a = """@prefix abc: <http://example.org/person-schema/abc/> .
@prefix oxo: <http://purl.obolibrary.org/obo/> .
@prefix xyz: <http://example.org/person-schema/xyz/> .
xyz:HenryAdams a abc:Person ; xyz:HenryAdams a abc:Person ;
abc:annotations [ a abc:Annotation ; abc:annotations [ a abc:Annotation ;
abc:annotation_tag <http://semanticscience.org/resource/SIO_001083> ; abc:annotation_tag <http://semanticscience.org/resource/SIO_001083> ;
@ -35,7 +59,7 @@ xyz:HenryAdams a abc:Person ;
abc:schema_type "abc:Person" . abc:schema_type "abc:Person" .
""" """
ttl_result_record_b = """@prefix abc: <http://example.org/person-schema/abc/> . ttl_result_record_with_annotations_b = """@prefix abc: <http://example.org/person-schema/abc/> .
@prefix oxo: <http://purl.obolibrary.org/obo/> . @prefix oxo: <http://purl.obolibrary.org/obo/> .
@prefix xyz: <http://example.org/person-schema/xyz/> . @prefix xyz: <http://example.org/person-schema/xyz/> .

View file

@ -35,7 +35,23 @@ dlflatsocial:test_john_ttl a dlflatsocial:Person ;
dlsocialmx:given_name "Johnöüß" . dlsocialmx:given_name "Johnöüß" .
""" """
ttl_output_record_a = """@prefix dlflat: <https://concepts.datalad.org/s/flat/unreleased/> . ttl_output_record_a = """@prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> .
@prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> .
dlflatsocial:another_john_ttl a dlflatsocial:Person ;
dlsocialmx:given_name "Johnöüß" .
"""
ttl_output_record_b = """@prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> .
@prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> .
dlflatsocial:another_john_ttl a dlflatsocial:Person ;
dlsocialmx:given_name "Johnöüß" .
"""
ttl_output_record_with_annotation_a = """@prefix dlflat: <https://concepts.datalad.org/s/flat/unreleased/> .
@prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> . @prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> .
@prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> . @prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> .
@prefix dlthings: <https://concepts.datalad.org/s/things/v1/> . @prefix dlthings: <https://concepts.datalad.org/s/things/v1/> .
@ -52,7 +68,7 @@ dlflatsocial:another_john_ttl a dlflatsocial:Person ;
""" """
ttl_output_record_b = """@prefix dlflat: <https://concepts.datalad.org/s/flat/unreleased/> . ttl_output_record_with_annotation_b = """@prefix dlflat: <https://concepts.datalad.org/s/flat/unreleased/> .
@prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> . @prefix dlflatsocial: <https://concepts.datalad.org/s/flat-social/unreleased/> .
@prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> . @prefix dlsocialmx: <https://concepts.datalad.org/s/social-mixin/unreleased/> .
@prefix dlthings: <https://concepts.datalad.org/s/things/v1/> . @prefix dlthings: <https://concepts.datalad.org/s/things/v1/> .
@ -108,7 +124,7 @@ def test_json_ttl_json_dlflatsocial(fastapi_client_simple):
headers={'x-dumpthings-token': 'token-all'}, headers={'x-dumpthings-token': 'token-all'},
) )
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
json_object = cleaned_json(response.json(), remove_keys=('annotations',)) json_object = response.json()
assert cleaned_json(json_object, remove_keys=('schema_type',)) != json_record assert cleaned_json(json_object, remove_keys=('schema_type',)) != json_record
json_object['pid'] = json_record['pid'] json_object['pid'] = json_record['pid']
assert json_object == json_record_out assert json_object == json_record_out

View file

@ -38,6 +38,7 @@ def validate_record(
class_name: str, class_name: str,
model: Any, model: Any,
input_format: Format, input_format: Format,
_: bool,
api_key: str | None = Depends(api_key_header_scheme), api_key: str | None = Depends(api_key_header_scheme),
) -> JSONResponse: ) -> JSONResponse: