Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Nov 7, 2023
1 parent 8b6b824 commit 006d69b
Show file tree
Hide file tree
Showing 12 changed files with 2,275 additions and 366 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ xmltodict==0.12.0 # MIT
# Allows custom-rendered IDs, hiding null values, and including data in error responses
git+https://github.com/cos-forks/[email protected]+cos0

git+https://github.com/aaxelb/[email protected].55
git+https://github.com/aaxelb/[email protected].57
21 changes: 11 additions & 10 deletions share/search/search_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,11 @@ class CardsearchParams(BaseTroveParams):
related_property_paths: tuple[tuple[str, ...]]
unnamed_iri_values: frozenset[str]

@staticmethod
def parse_cardsearch_queryparams(queryparams: QueryparamDict) -> dict:
@classmethod
def parse_queryparams(cls, queryparams: QueryparamDict) -> dict:
_filter_set = SearchFilter.from_queryparam_family(queryparams, 'cardSearchFilter')
return {
**super().parse_queryparams(queryparams),
'cardsearch_textsegment_set': Textsegment.from_queryparam_family(queryparams, 'cardSearchText'),
'cardsearch_filter_set': _filter_set,
'index_strategy_name': _get_single_value(queryparams, QueryparamName('indexStrategy')),
Expand Down Expand Up @@ -473,17 +474,17 @@ class ValuesearchParams(CardsearchParams):
valuesearch_filter_set: frozenset[SearchFilter]

# override CardsearchParams
@staticmethod
def from_queryparams(queryparams: QueryparamDict) -> 'ValuesearchParams':
@classmethod
def parse_queryparams(cls, queryparams: QueryparamDict) -> dict:
_raw_propertypath = _get_single_value(queryparams, QueryparamName('valueSearchPropertyPath'))
if not _raw_propertypath:
raise ValueError('TODO: 400 valueSearchPropertyPath required')
return ValuesearchParams(
**CardsearchParams.parse_cardsearch_queryparams(queryparams),
valuesearch_propertypath_set=_parse_propertypath_set(_raw_propertypath, allow_globs=False),
valuesearch_textsegment_set=Textsegment.from_queryparam_family(queryparams, 'valueSearchText'),
valuesearch_filter_set=SearchFilter.from_queryparam_family(queryparams, 'valueSearchFilter'),
)
return {
**super().parse_queryparams(queryparams),
'valuesearch_propertypath_set': _parse_propertypath_set(_raw_propertypath, allow_globs=False),
'valuesearch_textsegment_set': Textsegment.from_queryparam_family(queryparams, 'valueSearchText'),
'valuesearch_filter_set': SearchFilter.from_queryparam_family(queryparams, 'valueSearchFilter'),
}

def to_querydict(self):
_querydict = super().to_querydict()
Expand Down
71 changes: 53 additions & 18 deletions trove/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
import json
from typing import Iterable

from django.conf import settings
from primitive_metadata import primitive_rdf

from share.version import __version__
from trove.vocab.jsonapi import JSONAPI_MEMBERNAME
from trove.vocab.namespaces import TROVE, RDFS, RDF, DCTERMS
from trove.vocab.trove import TROVE_API_VOCAB

Expand All @@ -27,27 +30,27 @@ def get_trove_openapi() -> dict:
'''
# TODO: language parameter, get translations
_api_graph = primitive_rdf.RdfGraph(TROVE_API_VOCAB)
_path_iris = set(_api_graph.q(TROVE.API, TROVE.hasPath))
_label = next(_api_graph.q(TROVE.API, RDFS.label))
_comment = next(_api_graph.q(TROVE.API, RDFS.comment))
_description = next(_api_graph.q(TROVE.API, DCTERMS.description))
_path_iris = set(_api_graph.q(TROVE.search_api, TROVE.hasPath))
_label = next(_api_graph.q(TROVE.search_api, RDFS.label))
_comment = next(_api_graph.q(TROVE.search_api, RDFS.comment))
return {
'openapi': '3.1.0',
# 'externalDocs': {'url': TROVE.search_api},
'info': {
'title': _label.unicode_value,
'summary': _comment.unicode_value,
'description': _description.unicode_value,
'description': _markdown_description(TROVE.search_api, _api_graph),
'termsOfService': 'https://github.com/CenterForOpenScience/cos.io/blob/HEAD/TERMS_OF_USE.md',
'contact': {
# 'name':
# 'url': web-browsable version of this
'email': '[email protected]',
},
# 'license':
'version': '23.2.0',
'version': __version__,
},
'servers': [{
'url': 'https://share.osf.io',
'url': settings.SHARE_WEB_URL,
}],
'paths': dict(
_openapi_path(_path_iri, _api_graph)
Expand All @@ -66,42 +69,74 @@ def _openapi_parameters(path_iris: Iterable[str], api_graph: primitive_rdf.RdfGr
)))
for _param_iri in _param_iris:
# TODO: better error message on absence
try:
_jsonname = next(api_graph.q(_param_iri, JSONAPI_MEMBERNAME))
except StopIteration:
raise ValueError(f'no jsonapi membername for {_param_iri}')
_label = next(api_graph.q(_param_iri, RDFS.label))
_comment = next(api_graph.q(_param_iri, RDFS.comment))
_description = next(api_graph.q(_param_iri, DCTERMS.description))
_jsonschema = next(api_graph.q(_param_iri, TROVE.jsonSchema), None)
_required = ((_param_iri, RDF.type, TROVE.RequiredParameter) in api_graph)
_location = next(
_OPENAPI_PARAM_LOCATION_BY_RDF_TYPE[_type_iri]
for _type_iri in api_graph.q(_param_iri, RDF.type)
if _type_iri in _OPENAPI_PARAM_LOCATION_BY_RDF_TYPE
)
yield _label.unicode_value, {
yield _jsonname.unicode_value, {
'name': _label.unicode_value,
'in': _location,
'required': _required,
'summary': _comment.unicode_value,
'description': _description.unicode_value,
'description': _markdown_description(_param_iri, api_graph),
'schema': (json.loads(_jsonschema.unicode_value) if _jsonschema else None),
}


def _openapi_path(path_iri: str, api_graph: primitive_rdf.RdfGraph):
# TODO: better error message on absence
_iri_path = next(api_graph.q(path_iri, TROVE.iriPath))
_label = next(api_graph.q(path_iri, RDFS.label), None)
_description = next(api_graph.q(path_iri, DCTERMS.description), None)
_param_labels = list(api_graph.q(path_iri, {TROVE.hasParameter: {RDFS.label}}))
return _iri_path.unicode_value, {
try:
_path = next(_text(path_iri, TROVE.iriPath, api_graph))
except StopIteration:
raise ValueError(f'could not find trove:iriPath for {path_iri}')
_labels = list(_text(path_iri, RDFS.label, api_graph))
_param_labels = list(_text(path_iri, {TROVE.hasParameter: {JSONAPI_MEMBERNAME}}, api_graph))
return _path, {
'get': { # TODO (if generalizability): separate metadata by verb
# 'tags':
'summary': _label.unicode_value,
'description': _description.unicode_value,
'summary': _labels,
'description': _markdown_description(path_iri, api_graph),
# 'externalDocs':
'operationId': path_iri,
'parameters': [
{'$ref': f'#/components/parameters/{_param_label.unicode_value}'}
{'$ref': f'#/components/parameters/{_param_label}'}
for _param_label in _param_labels
],
},
}


def _concept_markdown_blocks(concept_iri: str, api_graph: primitive_rdf.RdfGraph):
for _label in api_graph.q(concept_iri, RDFS.label):
yield f'## {_label.unicode_value}'
for _comment in api_graph.q(concept_iri, RDFS.comment):
yield f'<aside>{_comment.unicode_value}</aside>'
for _desc in api_graph.q(concept_iri, DCTERMS.description):
yield _desc.unicode_value


def _text(subj, pred, api_graph):
for _obj in api_graph.q(subj, pred):
yield _obj.unicode_value


def _markdown_description(subj_iri: str, api_graph: primitive_rdf.RdfGraph):
return '\n\n'.join((
*(
_description.unicode_value
for _description in api_graph.q(TROVE.search_api, DCTERMS.description)
),
*(
'\n\n'.join(_concept_markdown_blocks(_concept_iri, api_graph))
for _concept_iri in api_graph.q(subj_iri, TROVE.usesConcept)
),
))
16 changes: 10 additions & 6 deletions trove/render/html_browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.urls import reverse
from primitive_metadata import primitive_rdf

from trove.util.iris import get_sufficiently_unique_iri
from trove.vocab.namespaces import TROVE, RDF, STATIC_SHORTHAND


Expand Down Expand Up @@ -234,15 +235,18 @@ def __leaf_link(self, iri: str):
def __href_for_iri(self, iri: str):
if self.request and (self.request.get_host() == urlsplit(iri).netloc):
return iri
if iri in TROVE:
return reverse('trove-vocab', kwargs={
'vocab_term': primitive_rdf.iri_minus_namespace(iri, namespace=TROVE),
})
return reverse('trovetrove:browse-iri', kwargs={'iri': iri})
return reverse('trovetrove:browse-iri', kwargs={
'iri': get_sufficiently_unique_iri(iri),
})

def __label_for_iri(self, iri: str):
# TODO: get actual label in requested language
return self.iri_shorthand.compact_iri(iri)
_shorthand = self.iri_shorthand.compact_iri(iri)
return (
get_sufficiently_unique_iri(iri)
if _shorthand == iri
else _shorthand
)


def _shuffled(items: Iterable):
Expand Down
4 changes: 2 additions & 2 deletions trove/render/jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def rdfobject_as_jsonld(self, rdfobject: primitive_rdf.RdfObject) -> dict:
if not rdfobject.datatype_iris:
return {'@value': rdfobject.unicode_value}
if RDF.JSON in rdfobject.datatype_iris:
# NOTE: does not reset jsonld context
# NOTE: does not reset jsonld context (is that a problem?)
return json.loads(rdfobject.unicode_value)
_language_tag = rdfobject.language_tag
_language_tag = rdfobject.language
if _language_tag: # standard language tag
return {
'@value': rdfobject.unicode_value,
Expand Down
1,820 changes: 1,820 additions & 0 deletions trove/static/js/redoc.standalone.js

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions trove/templates/trove/openapi-redoc.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
<!DOCTYPE html>
<html>
<head>
{% load static %}
<title>trove API Documentation</title>
<link rel="icon" href="images/favicon.ico">
<link rel="stylesheet" href="styles.css">
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="{% static 'favicon.ico' %}">
</head>
<body>
<redoc spec-url="openapi.json" lazy-rendering suppress-warnings hide-loading expand-responses="all"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/v1.19.3/redoc.min.js"> </script>
<redoc spec-url="openapi.json" lazy-rendering hide-loading></redoc>
<script src="{% static 'js/redoc.standalone.js' %}"></script>
</body>
</html>
6 changes: 3 additions & 3 deletions trove/trovesearch_gathering.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@

TROVE_GATHERING_NORMS = gather.GatheringNorms(
namestory=(
literal('cardsearch', language_tag='en'),
literal('search for "index cards" that describe resources', language_tag='en'),
literal('cardsearch', language='en'),
literal('search for "index cards" that describe resources', language='en'),
),
focustype_iris={
TROVE.Indexcard,
Expand All @@ -57,7 +57,7 @@

trovesearch_by_indexstrategy = gather.GatheringOrganizer(
namestory=(
literal('trove search', language_tag='en'),
literal('trove search', language='en'),
),
norms=TROVE_GATHERING_NORMS,
gatherer_kwargnames={'search_params', 'specific_index', 'use_osfmap_json'},
Expand Down
26 changes: 17 additions & 9 deletions trove/util/iris.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
IRI_SCHEME_REGEX_IGNORECASE = re.compile(IRI_SCHEME_REGEX.pattern, flags=re.IGNORECASE)
COLON = ':'
COLON_SLASH_SLASH = '://'
QUOTED_IRI_REGEX = re.compile(f'{IRI_SCHEME_REGEX.pattern}{re.escape(quote(COLON))}')
QUOTED_IRI_REGEX = re.compile(
f'{IRI_SCHEME_REGEX.pattern}{re.escape(quote(COLON))}'
f'|{re.escape(quote(COLON_SLASH_SLASH))}'
)
UNQUOTED_IRI_REGEX = re.compile(f'{IRI_SCHEME_REGEX.pattern}{COLON}|{COLON_SLASH_SLASH}')

# treat similar-enough IRIs as equivalent, based on a wild assertion:
# if two IRIs differ only in their `scheme`
Expand All @@ -34,13 +38,17 @@ def get_iri_scheme(iri: str) -> str:

def get_sufficiently_unique_iri_and_scheme(iri: str) -> tuple[str, str]:
_scheme_match = IRI_SCHEME_REGEX_IGNORECASE.match(iri)
if not _scheme_match:
raise ValueError(f'does not look like an iri (got "{iri}")')
_scheme = _scheme_match.group().lower()
_remainder = iri[_scheme_match.end():]
if not _remainder.startswith(COLON_SLASH_SLASH):
# for an iri without '://', assume nothing!
return (iri, _scheme)
if _scheme_match:
_scheme = _scheme_match.group().lower()
_remainder = iri[_scheme_match.end():]
if not _remainder.startswith(COLON_SLASH_SLASH):
# for an iri without '://', assume nothing!
return (iri, _scheme)
else: # may omit scheme only if `://`
if not iri.startswith(COLON_SLASH_SLASH):
raise ValueError(f'does not look like an iri (got "{iri}")')
_scheme = ''
_remainder = iri
# for an iri with '://', is "safe enough" to normalize a little:
_split_remainder = urlsplit(_remainder)
_cleaned_remainder = urlunsplit((
Expand Down Expand Up @@ -78,6 +86,6 @@ def unquote_iri(iri: str) -> str:
_unquoted_iri = iri
while QUOTED_IRI_REGEX.match(_unquoted_iri):
_unquoted_iri = unquote(_unquoted_iri)
if not IRI_SCHEME_REGEX.match(_unquoted_iri):
if not UNQUOTED_IRI_REGEX.match(_unquoted_iri):
raise ValueError(f'does not look like a quoted iri: {iri}')
return _unquoted_iri
4 changes: 4 additions & 0 deletions trove/views/browse.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from typing import Iterable

from django import http
from django.shortcuts import redirect
from django.views import View
from primitive_metadata import primitive_rdf

from trove import models as trove_db
from trove.render import render_response
from trove.util.iris import unquote_iri
from trove.vocab.namespaces import TROVE


class BrowseIriView(View):
def get(self, request, iri):
_iri = unquote_iri(iri)
if _iri in TROVE:
return redirect('trove-vocab', vocab_term=primitive_rdf.iri_minus_namespace(iri, TROVE))
try:
_identifier = trove_db.ResourceIdentifier.objects.get_for_iri(_iri)
except trove_db.ResourceIdentifier.DoesNotExist:
Expand Down
Loading

0 comments on commit 006d69b

Please sign in to comment.