Skip to content

Commit ce58673

Browse files
author
Nicholas Car
committedJan 28, 2020
ConnegP supported, view -> profile, format -> metia type
1 parent 4d966e1 commit ce58673

21 files changed

+751
-707
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ rofr.ttl
3131
# the config
3232
_config/__init__.py
3333

34+
.pytest_cache/

‎README.rst

+31-50
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ The Python Linked Data API (pyLDAPI) is:
1313
What is it?
1414
-----------
1515

16-
This module contains only a small Python module which is intended to be added (imported) into a `Python Flask`_ installation in order to add a series of extra functions to endpoints to the ones defined by you as a Flask user (URL routes).
16+
This module contains a small Python module which is intended to be added (imported) into a `Python Flask <http://flask.pocoo.org/>`_ installation to add a small library of ``Renderer`` classes which can be used to handle requests and return responses in a manner consistent with `Linked Data <https://en.wikipedia.org/wiki/Linked_data>`__ principles of operation.
1717

18-
.. _Python Flask: http://flask.pocoo.org/
18+
The intention is to make it easy to "Linked Data-enable" web APIs.
1919

2020
An API using this module will get:
2121

22-
* an *alternates view* for each *Register* and *Object* that the API delivers
23-
* if the API declares the appropriate *model views* for each item
22+
* an *alt profile* for each endpoint that uses a ``Renderer`` class to return responses that the API delivers
23+
* this is a *profile*, or *view* of the resource that lists all other available profiles
2424
* a *Register of Registers*
2525
* a start-up function that auto-generates a Register of Registers is run when the API is launched.
2626
* a basic, over-writeable template for Registers' HTML & RDF
27-
* all of the functionality defined by the W3C's specification `Content Negotiation by Profile`_
27+
* all of the functionality defined by the W3C's `Content Negotiation by Profile <https://www.w3.org/TR/dx-prof-conneg/>`_ specification
2828
* to allow for requests of content that conform to data specifications and profiles
2929

3030
The main parts of pyLDAPI are as follows:
@@ -37,23 +37,16 @@ The main parts of pyLDAPI are as follows:
3737

3838
Web requests arrive at a Web Server, such as *Apache* or *nginx*, which then forwards (some of) them on to *Flask*, a Python web framework. Flask calls Python functions for web requests defined in a request/function mapping and may call pyLDAPI elements. Flask need not call pyLDAPI for all requests, just as Apache/nginx need not forward all web request to flask. pyLDAPI may then draw on any Python data source, such as database APIs, and uses the *rdflib* Python module to formulate RDF responses.
3939

40-
.. _Content Negotiation by Profile: https://www.w3.org/TR/dx-prof-conneg/
41-
4240
Definitions
4341
-----------
4442

45-
Alternates View
46-
~~~~~~~~~~~~~~~
47-
The *model view* that lists all other views. This API uses the definition of *alternates view* presented at `https://promsns.org/def/alt`_.
48-
49-
.. _https://promsns.org/def/alt: https://promsns.org/def/alt
43+
Alt Profile
44+
~~~~~~~~~~~
45+
The *model view* that lists all other views. This API uses the definition of *alternates profile* presented at `https://promsns.org/def/alt <https://promsns.org/def/alt>`_.
5046

5147
Linked Data Principles
5248
~~~~~~~~~~~~~~~~~~~~~~
53-
The principles of making things available over the internet in both human and machine-readable forms. Codified by the World Wide Web Consortium. See `https://www.w3.org/standards/semanticweb/data`_.
54-
55-
.. _https://www.w3.org/standards/semanticweb/data: https://www.w3.org/standards/semanticweb/data
56-
49+
The principles of making things available over the internet in both human and machine-readable forms. Codified by the World Wide Web Consortium. See `https://www.w3.org/standards/semanticweb/data <https://www.w3.org/standards/semanticweb/data>`_.
5750

5851
Model View
5952
~~~~~~~~~~
@@ -77,14 +70,10 @@ pyLDAPI in action
7770
-----------------
7871

7972
* Register of Media Types
80-
* `https://w3id.org/mediatype/`_
81-
82-
.. _https://w3id.org/mediatype/: https://w3id.org/mediatype/
73+
* `https://w3id.org/mediatype/ <https://w3id.org/mediatype/>`_
8374

8475
* Linked Data version of the Geocoded National Address File
85-
* `http://linked.data.gov.au/dataset/gnaf`_
86-
87-
.. _http://linked.data.gov.au/dataset/gnaf: http://linked.data.gov.au/dataset/gnaf
76+
* `http://linked.data.gov.au/dataset/gnaf <http://linked.data.gov.au/dataset/gnaf>`_
8877

8978
|gnaf|
9079

@@ -95,14 +84,10 @@ Parts of the GNAF implementation
9584
:alt: Block diagram of the GNAF implementation
9685

9786
* Geoscience Australia's Sites, Samples Surveys Linked Data API
98-
* `http://pid.geoscience.gov.au/sample/`_
99-
100-
.. _http://pid.geoscience.gov.au/sample/: http://pid.geoscience.gov.au/sample/
87+
* `http://pid.geoscience.gov.au/sample/ <http://pid.geoscience.gov.au/sample/>`_
10188

10289
* Linked Data version of the Australian Statistical Geography Standard product
103-
* `http://linked.data.gov.au/dataset/asgs`_
104-
105-
.. _http://linked.data.gov.au/dataset/asgs: http://linked.data.gov.au/dataset/asgs
90+
* `http://linked.data.gov.au/dataset/asgs <http://linked.data.gov.au/dataset/asgs>`_
10691

10792
|asgs|
10893

@@ -115,19 +100,13 @@ Parts of the ASGS implementation
115100
Documentation
116101
-------------
117102

118-
Detailed documentation can be found at `https://pyldapi.readthedocs.io/`_
119-
120-
.. _https://pyldapi.readthedocs.io/: https://pyldapi.readthedocs.io/
121-
103+
Detailed documentation can be found at `https://pyldapi.readthedocs.io/ <https://pyldapi.readthedocs.io/>`_
122104

123105

124106
Licence
125107
-------
126108

127-
This is licensed under GNU General Public License (GPL) v3.0. See the `LICENSE deed`_ for more details.
128-
129-
.. _LICENSE deed: https://raw.githubusercontent.com/RDFLib/pyLDAPI/master/LICENSE
130-
109+
This is licensed under GNU General Public License (GPL) v3.0. See the `LICENSE deed <https://raw.githubusercontent.com/RDFLib/pyLDAPI/master/LICENSE>`_ for more details.
131110

132111

133112
Contact
@@ -136,29 +115,31 @@ Contact
136115
Dr Nicholas Car (lead)
137116
~~~~~~~~~~~~~~~~~~~~~~
138117
| *Data Systems Architect*
139-
| `SURROUND Australia Pty Ltd`_
140-
| `nicholas.car@surroundaustralia.com`_
141-
| `https://orcid.org/0000-0002-8742-7730`_
142-
143-
.. _SURROUND Australia Pty Ltd: https://surroundaustralia.com
144-
.. _nicholas.car@surroundaustralia.com: nicholas.car@surroundaustralia.com
145-
.. _http://orcid.org/0000-0002-8742-7730: http://orcid.org/0000-0002-8742-7730
118+
| `SURROUND Australia Pty Ltd <https://surroundaustralia.com>`_
119+
| `nicholas.car@surroundaustralia.com <nicholas.car@surroundaustralia.com>`_
120+
| `https://orcid.org/0000-0002-8742-7730 <https://orcid.org/0000-0002-8742-7730>`_
146121
147122
Ashley Sommer (senior developer)
148123
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
149124
| *Informatics Software Engineer*
150-
| `CSIRO Land and Water`_
151-
| `ashley.sommer@csiro.au`_
152-
153-
.. _ashley.sommer@csiro.au: ashley.sommer@csiro.au
154-
.. _CSIRO Land and Water: https://www.csiro.au/en/Research/LWF
125+
| `CSIRO Land and Water <https://www.csiro.au/en/Research/LWF>`_
126+
| `ashley.sommer@csiro.au <ashley.sommer@csiro.au>`_
155127
156128

157129
Related work
158130
------------
159131

160-
`pyLDAPI Client`_
132+
`pyLDAPI Client <http://pyldapi-client.readthedocs.io/>`_
161133

162134
* *A Simple helper library for consuming registers, indexes, and instances of classes exposed via a pyLDAPI endpoint.*
163135

164-
.. _pyLDAPI Client: http://pyldapi-client.readthedocs.io/
136+
137+
Changelog
138+
---------
139+
**3.0**
140+
141+
* Content Negotiation specification by Profile supported
142+
* replaced all references to "format" with "Media Type" and "view" with "profile"
143+
* renamed class View to Profile
144+
* added unit tests for all profile functions
145+
* added unit tests for main ConnegP functions

‎README_OLD.md

-53
This file was deleted.

‎docs/source/alternates_template.rst

+7-7
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ Example of a generic alternates template:
1616
{% if instance_uri %}
1717
<h3>Instance <a href="{{ instance_uri }}">{{ instance_uri }}</a></h3>
1818
{% endif %}
19-
<p>Default view: <a href="{{ request.base_url }}?_view={{ default_view_token }}">{{ default_view_token }}</a></p>
19+
<p>Default profile: <a href="{{ request.base_url }}?_profile={{ default_profile_token }}">{{ default_profile_token }}</a></p>
2020
<table class="pretty">
2121
<tr><th>View</th><th>Formats</th><th>View Desc.</th><th>View Namespace</th></tr>
22-
{% for v, vals in views.items() %}
22+
{% for v, vals in profiles.items() %}
2323
<tr>
24-
<td><a href="{{ request.base_url }}?_view={{ v }}">{{ v }}</a></td>
24+
<td><a href="{{ request.base_url }}?_profile={{ v }}">{{ v }}</a></td>
2525
<td>
2626
{% for f in vals['formats'] %}
27-
<a href="{{ request.base_url }}?_view={{ v }}&_format={{ f }}">{{ f }}</a>
27+
<a href="{{ request.base_url }}?_profile={{ v }}&_format={{ f }}">{{ f }}</a>
2828
{% if loop.index != vals['formats']|length %}<br />{% endif %}
2929
{% endfor %}
3030
</td>
@@ -35,7 +35,7 @@ Example of a generic alternates template:
3535
</table>
3636
{% endblock %}
3737

38-
The alternates view template is shared for both a Register's alternates view as well as a class instance item's alternates view. In any case, since a :class:`.RegisterRenderer` class and a :ref:`example-renderer-reference` class both inherit from the base class :class:`.Renderer`, then they can both easily render the alternates view by calling the base class' :func:`pyldapi.Renderer.render_alternates_view` method. One distinct difference is that pyLDAPI will handle the alternates view automatically for a :class:`.RegisterRenderer` whereas a :ref:`example-renderer-reference` will have to explicitly call the :func:`pyldapi.Renderer.render_alternates_view`.
38+
The alternates profile template is shared for both a Register's alternates profile as well as a class instance item's alternates profile. In any case, since a :class:`.RegisterRenderer` class and a :ref:`example-renderer-reference` class both inherit from the base class :class:`.Renderer`, then they can both easily render the alternates profile by calling the base class' :func:`pyldapi.Renderer.render_alternates_profile` method. One distinct difference is that pyLDAPI will handle the alternates profile automatically for a :class:`.RegisterRenderer` whereas a :ref:`example-renderer-reference` will have to explicitly call the :func:`pyldapi.Renderer.render_alternates_profile`.
3939

4040
Example usage for a :ref:`example-renderer-reference`:
4141

@@ -48,6 +48,6 @@ Example usage for a :ref:`example-renderer-reference`:
4848
# this is an implementation of the abstract render() of the base class Renderer
4949
def render(self):
5050
# ...
51-
if self.view == 'alternates':
52-
return self.render_alternates_view() # render the alternates view for this class instance
51+
if self.profile == 'alternates':
52+
return self.render_alternates_profile() # render the alternates profile for this class instance
5353
# ...

‎docs/source/exempler_custom_renderer_example.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from flask import Response, render_template
22
from SPARQLWrapper import SPARQLWrapper, JSON
33
from rdflib import Graph, URIRef, Namespace, RDF, RDFS, XSD, OWL, Literal
4-
from pyldapi import Renderer, View
4+
from pyldapi import Renderer, Profile
55
import _conf as conf
66

77

88
class MediaTypeRenderer(Renderer):
99
def __init__(self, request, instance_uri):
10-
views = {
11-
'mt': View(
10+
profiles = {
11+
'mt': Profile(
1212
'Mediatype View',
1313
'Basic properties of a Media Type, as recorded by IANA',
1414
['text/html'] + Renderer.RDF_MIMETYPES,
@@ -20,17 +20,17 @@ def __init__(self, request, instance_uri):
2020
super(MediaTypeRenderer, self).__init__(
2121
request,
2222
instance_uri,
23-
views,
23+
profiles,
2424
'mt'
2525
)
2626

2727
def render(self):
2828
if hasattr(self, 'vf_error'):
2929
return Response(self.vf_error, status=406, mimetype='text/plain')
3030
else:
31-
if self.view == 'alternates':
32-
return self._render_alternates_view()
33-
elif self.view == 'mt':
31+
if self.profile == 'alternates':
32+
return self._render_alternates_profile()
33+
elif self.profile == 'mt':
3434
if self.format in Renderer.RDF_MIMETYPES:
3535
rdf = self._get_instance_rdf()
3636
if rdf is None:

‎docs/source/register_template.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ Example of a generic register template:
3131
{{ pagination.links }}
3232
</td>
3333
<td style="vertical-align:top;">
34-
<h3>Alternate views</h3>
35-
<p>Different views of this register are listed at its <a href="{{ request.base_url }}?_view=alternates">Alternate views</a> page.</p>
34+
<h3>Alternate profiles</h3>
35+
<p>Different profiles of this register are listed at its <a href="{{ request.base_url }}?_profile=alternates">Alternate profiles</a> page.</p>
3636
<h3>Automated Pagination</h3>
3737
<p>To page through these items, use the query string arguments 'page' for the page number and 'per_page' for the number of items per page. HTTP <code>Link</code> headers of <code>first</code>, <code>prev</code>, <code>next</code> &amp; <code>last</code> indicate URIs to the first, a previous, a next and the last page.</p>
3838
<p>Example:</p>
@@ -73,4 +73,4 @@ Variables used by the register template:
7373
pagination=pagination # pagination object from module flask_paginate
7474
)
7575
76-
See :class:`.RegisterRenderer` for an example on how to render the register view.
76+
See :class:`.RegisterRenderer` for an example on how to render the register profile.

‎pyldapi/__init__.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
from pyldapi.exceptions import ViewsFormatsException, PagingError
44
from pyldapi.renderer import Renderer
5-
from pyldapi.register_renderer import RegisterRenderer,\
6-
RegisterOfRegistersRenderer
7-
from pyldapi.view import View
5+
from pyldapi.register_renderer import RegisterRenderer, RegisterOfRegistersRenderer
6+
from pyldapi.profile import Profile
87
from pyldapi.helpers import setup
98

10-
__version__ = '2.1.3'
9+
__version__ = '3.0'
1110

12-
__all__ = ['Renderer', 'RegisterRenderer', 'RegisterOfRegistersRenderer',
13-
'View', 'ViewsFormatsException', 'PagingError', 'setup',
14-
'__version__']
11+
__all__ = [
12+
'Renderer',
13+
'RegisterRenderer',
14+
'RegisterOfRegistersRenderer',
15+
'Profile',
16+
'ViewsFormatsException',
17+
'PagingError',
18+
'setup',
19+
'__version__'
20+
]

‎pyldapi/helpers.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ def _make_rofr_rdf(app, api_home_dir, api_uri):
4848
# get the RDF for each Register, extract the bits we need, write them to graph g
4949
for rule in app.url_map.iter_rules():
5050
if '<' not in str(rule): # no registers can have a Flask variable in their path
51-
# make the register view URI for each possible register
51+
# make the register profile URI for each possible register
5252
try:
53-
endpoint_func = app.view_functions[rule.endpoint]
53+
endpoint_func = app.profile_functions[rule.endpoint]
5454
except (AttributeError, KeyError):
5555
continue
5656
candidate_register_uri = api_uri + str(rule)
5757
try:
5858
dummy_request_uri = "http://localhost:5000" + str(
59-
rule) + '?_view=reg&_format=_internal'
59+
rule) + '?_profile=reg&_format=_internal'
6060
test_context = app.test_request_context(dummy_request_uri)
6161
with test_context:
6262
resp = endpoint_func()
@@ -70,13 +70,13 @@ def _make_rofr_rdf(app, api_home_dir, api_uri):
7070
with test_context:
7171
try:
7272
resp.format = 'text/html'
73-
html_resp = resp._render_reg_view_html()
73+
html_resp = resp._render_reg_profile_html()
7474
except TemplateNotFound: # missing html template
7575
pass # TODO: Fail on this error
7676
resp.format = 'application/json'
77-
json_resp = resp._render_reg_view_json()
77+
json_resp = resp._render_reg_profile_json()
7878
resp.format = 'text/turtle'
79-
rdf_resp = resp._render_reg_view_rdf()
79+
rdf_resp = resp._render_reg_profile_rdf()
8080

8181
_filter_register_graph(candidate_register_uri, rdf_resp, g)
8282

@@ -138,14 +138,14 @@ def _filter_register_graph(register_uri, r, g):
138138
# import requests
139139
# from pyldapi.exceptions import ViewsFormatsException
140140
# assert isinstance(g, Graph)
141-
# logging.debug('assessing register candidate ' + register_uri.replace('?_view=reg&_format=text/turtle', ''))
141+
# logging.debug('assessing register candidate ' + register_uri.replace('?_profile=reg&_format=text/turtle', ''))
142142
# try:
143143
# r = requests.get(register_uri)
144144
# print('getting ' + register_uri)
145145
# except ViewsFormatsException as e:
146-
# return False # ignore these exceptions as are just a result of requesting a view/format combo of something like a page
146+
# return False # ignore these exceptions as are just a result of requesting a profile/format combo of something like a page
147147
# if r.status_code == 200:
148-
# return _filter_register_graph(register_uri.replace('?_view=reg&_format=text/turtle', ''), r, g)
148+
# return _filter_register_graph(register_uri.replace('?_profile=reg&_format=text/turtle', ''), r, g)
149149
# logging.debug('{} returns no HTTP 200'.format(register_uri))
150150
# return False # no valid response from endpoint so not register
151151

‎pyldapi/view.py ‎pyldapi/profile.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
# -*- coding: utf-8 -*-
2-
class View:
2+
class Profile:
33
"""
44
A class containing elements for a Linked Data 'model view',
5-
including MIME type 'formats'.
5+
including MIME type 'mediatypes'.
66
7-
The syntax for formats can be found at iana org: https://www.iana.org/assignments/media-types/media-types.xhtml
7+
The syntax for mediatypes can be found at iana org: https://www.iana.org/assignments/media-types/media-types.xhtml
88
9-
Example of common media formats and languages as a list:
9+
Example of common mediatypes and languages as a list:
1010
1111
.. code-block:: python
1212
13-
formats = ['text/html', 'text/turtle', 'application/rdf+xml', 'application/rdf+json']
13+
mediatypes = ['text/html', 'text/turtle', 'application/rdf+xml', 'application/rdf+json']
1414
languages = ['en', 'pl'] # 'en' for English and 'pl' for Polish.
1515
1616
"""
1717
def __init__(
1818
self,
1919
label,
2020
comment,
21-
formats,
22-
default_format,
21+
mediatypes,
22+
default_mediatype,
2323
languages=None,
2424
default_language='en',
2525
profile_uri=None
@@ -31,10 +31,10 @@ def __init__(
3131
:type label: str
3232
:param comment: The comment describing the view.
3333
:type comment: str
34-
:param formats: The list of formats according to iana org.
35-
:type formats: list
36-
:param default_format: The default format according to iana org.
37-
:type default_format: str
34+
:param mediatypes: The list of mediatypes according to iana org.
35+
:type mediatypes: list
36+
:param default_mediatype: The default mediatype according to iana org.
37+
:type default_mediatype: str
3838
:param languages: A list of languages as strings.
3939
:type languages: list
4040
:param default_language: The default language, by default it is 'en' English.
@@ -44,8 +44,8 @@ def __init__(
4444
"""
4545
self.label = label
4646
self.comment = comment
47-
self.formats = formats
48-
self.default_format = default_format
47+
self.mediatypes = mediatypes
48+
self.default_mediatype = default_mediatype
4949
self.languages = languages if languages is not None else ['en']
5050
self.default_language = default_language
5151
self.namespace = profile_uri

‎pyldapi/register_renderer.py

+56-45
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from rdflib.term import Identifier
77
import json
88
from pyldapi.renderer import Renderer
9-
from pyldapi.view import View
9+
from pyldapi.profile import Profile
1010
from pyldapi.exceptions import ViewsFormatsException, RegOfRegTtlError
1111

1212

@@ -25,8 +25,8 @@ def __init__(self,
2525
contained_item_classes,
2626
register_total_count,
2727
*args,
28-
views=None,
29-
default_view_token=None,
28+
profiles=None,
29+
default_profile_token=None,
3030
super_register=None,
3131
page_size_max=1000,
3232
register_template=None,
@@ -43,35 +43,42 @@ def __init__(self,
4343
:type label: str
4444
:param comment: A description of the Register.
4545
:type comment: str
46-
:param register_items: The items within this register as a list of URI strings or tuples with string elements like (URI, label). They can also be tuples like (URI, URI, label) if you want to manually specify an item's class.
46+
:param register_items: The items within this register as a list of URI strings or tuples with string elements
47+
like (URI, label). They can also be tuples like (URI, URI, label) if you want to manually specify an item's
48+
class.
4749
:type register_items: list
48-
:param contained_item_classes: The list of URI strings of each distinct class of item contained in this Register.
50+
:param contained_item_classes: The list of URI strings of each distinct class of item contained in this
51+
Register.
4952
:type contained_item_classes: list
50-
:param register_total_count: The total number of items in this Register (not of a page but the register as a whole).
53+
:param register_total_count: The total number of items in this Register (not of a page but the register as a
54+
whole).
5155
:type register_total_count: int
52-
:param views: A dictionary of named :class:`.View` objects available for this Register, apart from 'reg' which is auto-created.
53-
:type views: dict
54-
:param default_view_token: The ID of the default :class:`.View` (key of a view in the list of Views).
55-
:type default_view_token: str
56+
:param profiles: A dictionary of named :class:`.View` objects available for this Register, apart from 'reg'
57+
which is auto-created.
58+
:type profiles: dict
59+
:param default_profile_token: The ID of the default :class:`.View` (key of a profile in the list of Views).
60+
:type default_profile_token: str
5661
:param super_register: A super-Register URI for this register. Can be within this API or external.
5762
:type super_register: str
58-
:param register_template: The Jinja2 template to use for rendering the HTML view of the register. If None, then it will default to try and use a template called :code:`alternates.html`.
63+
:param register_template: The Jinja2 template to use for rendering the HTML profile of the register. If None,
64+
then it will default to try and use a template called :code:`alt.html`.
5965
:type register_template: str or None
60-
:param per_page: Number of items to show per page if not specified in request. If None, then it will default to RegisterRenderer.DEFAULT_ITEMS_PER_PAGE.
66+
:param per_page: Number of items to show per page if not specified in request. If None, then it will default to
67+
RegisterRenderer.DEFAULT_ITEMS_PER_PAGE.
6168
:type per_page: int or None
6269
"""
63-
if views is None:
64-
views = {}
65-
for k, v in views.items():
70+
if profiles is None:
71+
profiles = {}
72+
for k, v in profiles.items():
6673
if k == 'reg':
6774
raise ViewsFormatsException(
68-
'You must not manually add a view with token \'reg\' as this is auto-created'
75+
'You must not manually add a profile with token \'reg\' as this is auto-created'
6976
)
70-
views.update(self._add_standard_reg_view())
71-
if default_view_token is None:
72-
default_view_token = 'reg'
73-
super(RegisterRenderer, self).__init__(request, instance_uri, views,
74-
default_view_token, **kwargs)
77+
profiles.update(self._add_standard_reg_profile())
78+
if default_profile_token is None:
79+
default_profile_token = 'reg'
80+
super(RegisterRenderer, self).__init__(request, instance_uri, profiles,
81+
default_profile_token, **kwargs)
7582
if self.vf_error is None:
7683
self.label = label
7784
self.comment = comment
@@ -81,7 +88,11 @@ def __init__(self,
8188
self.register_items = []
8289
self.contained_item_classes = contained_item_classes
8390
self.register_total_count = register_total_count
84-
self.per_page = request.args.get('per_page', type=int, default=(per_page or RegisterRenderer.DEFAULT_ITEMS_PER_PAGE))
91+
self.per_page = request.args.get(
92+
'per_page',
93+
type=int,
94+
default=(per_page or RegisterRenderer.DEFAULT_ITEMS_PER_PAGE)
95+
)
8596
self.page = request.args.get('page', type=int, default=1)
8697
self.super_register = super_register
8798
self.page_size_max = page_size_max
@@ -145,31 +156,31 @@ def _paging(self):
145156

146157
def render(self):
147158
"""
148-
Renders the register view.
159+
Renders the register profile.
149160
150161
:return: A Flask Response object.
151162
:rtype: :py:class:`flask.Response`
152163
"""
153164
response = super(RegisterRenderer, self).render()
154-
if response is None and self.view == 'reg':
165+
if response is None and self.profile == 'reg':
155166
if self.paging_error is None:
156-
response = self._render_reg_view()
167+
response = self._render_reg_profile()
157168
else: # there is a paging error (e.g. page > last_page)
158169
response = Response(self.paging_error, status=400, mimetype='text/plain')
159170
return response
160171

161-
def _render_reg_view(self):
162-
# add link headers for all formats of reg view
163-
if self.format == '_internal':
172+
def _render_reg_profile(self):
173+
# add link headers for all formats of reg profile
174+
if self.mediatype == '_internal':
164175
return self
165-
elif self.format == 'text/html':
166-
return self._render_reg_view_html()
167-
elif self.format in Renderer.RDF_MIMETYPES:
168-
return self._render_reg_view_rdf()
176+
elif self.mediatype == 'text/html':
177+
return self._render_reg_profile_html()
178+
elif self.mediatype in Renderer.RDF_MIMETYPES:
179+
return self._render_reg_profile_rdf()
169180
else:
170-
return self._render_reg_view_json()
181+
return self._render_reg_profile_json()
171182

172-
def _render_reg_view_html(self, template_context=None):
183+
def _render_reg_profile_html(self, template_context=None):
173184
pagination = Pagination(page=self.page, per_page=self.per_page,
174185
total=self.register_total_count,
175186
page_parameter='page', per_page_parameter='per_page')
@@ -201,7 +212,7 @@ def _render_reg_view_html(self, template_context=None):
201212
headers=self.headers
202213
)
203214

204-
def _generate_reg_view_rdf(self):
215+
def _generate_reg_profile_rdf(self):
205216
g = Graph()
206217

207218
REG = Namespace('http://purl.org/linked-data/registry#')
@@ -301,30 +312,30 @@ def _generate_reg_view_rdf(self):
301312

302313
return g
303314

304-
def _render_reg_view_rdf(self):
305-
g = self._generate_reg_view_rdf()
315+
def _render_reg_profile_rdf(self):
316+
g = self._generate_reg_profile_rdf()
306317
return self._make_rdf_response(g)
307318

308-
def _render_reg_view_json(self):
319+
def _render_reg_profile_json(self):
309320
return Response(
310321
json.dumps({
311322
'uri': self.instance_uri,
312323
'label': self.label,
313324
'comment': self.comment,
314-
'views': list(self.views.keys()),
315-
'default_view': self.default_view_token,
325+
'profiles': list(self.profiles.keys()),
326+
'default_profile': self.default_profile_token,
316327
'contained_item_classes': self.contained_item_classes,
317328
'register_items': self.register_items
318329
}),
319330
mimetype='application/json',
320331
headers=self.headers
321332
)
322333

323-
def _add_standard_reg_view(self):
334+
def _add_standard_reg_profile(self):
324335
return {
325-
'reg': View(
336+
'reg': Profile(
326337
'Registry Ontology',
327-
'A simple list-of-items view taken from the Registry Ontology',
338+
'A simple list-of-items profile taken from the Registry Ontology',
328339
['text/html', 'application/json'] + self.RDF_MIMETYPES,
329340
'text/html',
330341
languages=['en'], # default 'en' only for now
@@ -393,8 +404,8 @@ def __init__(self, request, instance_uri, label, comment, rofr_file_path, *args,
393404
self.register_items.append((r['uri'], r['label']))
394405
self.register_total_count = len(self.register_items)
395406

396-
def _generate_reg_view_rdf(self):
397-
g = super(RegisterOfRegistersRenderer, self)._generate_reg_view_rdf()
407+
def _generate_reg_profile_rdf(self):
408+
g = super(RegisterOfRegistersRenderer, self)._generate_reg_profile_rdf()
398409
REG = Namespace('http://purl.org/linked-data/registry#')
399410
for uri_str, cics in self.subregister_cics.items():
400411
uri = URIRef(uri_str)

‎pyldapi/renderer.py

+133-252
Large diffs are not rendered by default.

‎pyldapi/templates/alt.html

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{% extends "page.html" %}
2+
3+
{% block content %}
4+
<h1>Alternates View</h1>
5+
<h2>Instance <a href="{{ uri }}">{{ name }}</a></h2>
6+
<h4>Default view: <a href="{{ request.base_url }}?_profile={{ default_profile_token }}">{{ request.base_url }}?_profile={{ default_profile_token }}</a></h4>
7+
<div class="overflow">
8+
<table class="layout">
9+
<tr>
10+
<th style="font-weight: bold;">Token</th>
11+
<th style="font-weight: bold;">Name</th>
12+
<th style="font-weight: bold;">Formats</th>
13+
<th style="font-weight: bold; padding-right: 30px;">Languages</th>
14+
<th style="font-weight: bold;">Description</th>
15+
<th style="font-weight: bold;">Namespace</th>
16+
</tr>
17+
{% for token, vals in views.items() %}
18+
<tr style="border-bottom: 1px solid black; border-top: 1px solid black;">
19+
<td style="padding-right: 30px;"><a href="{{ request.base_url }}?_profile={{ token }}&_mediatype={{ vals['default_format'] }}">{{ token }}</a></td>
20+
<td>{{ vals['label'] }}</td>
21+
<td>
22+
{% for f in vals['formats'] %}
23+
<a href="{{ request.base_url }}?_profile={{ token }}&_format={{ f }}">{{ f }}</a><br />
24+
{% endfor %}
25+
</td>
26+
<td style="text-align: center;">
27+
{% for l in vals['languages'] %}
28+
<a href="{{ request.base_url }}?_profile={{ token }}&_lang={{ l }}">{{ l }}</a><br />
29+
{% endfor %}
30+
</td>
31+
<td>{{ vals['comment'] }}</td>
32+
{% if vals['namespace'] is not none %}
33+
<td><a href="{{ vals['namespace'] }}">{{ vals['namespace'] }}</a></td>
34+
{% endif %}
35+
</tr>
36+
{% endfor %}
37+
</table>
38+
</div>
39+
{% endblock %}

‎pyldapi/templates/register.html

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{% extends "page.html" %}
2+
3+
{% block content %}
4+
<div class="row">
5+
<div class="col-lg-12">
6+
<h1>Register</h1>
7+
<h2>Of <a href="{{ contained_item_classes[0] }}"><em>{{ contained_item_classes[0].split('#')[1] }}s</em></a></h2>
8+
<p></p>
9+
{% if search_enabled %}
10+
<form action="?search=">
11+
Search <em>{{ register_item_type_string }}:</em><br>
12+
<input type="text" name="search">
13+
<input type="submit" value="Submit">
14+
</form>
15+
{% endif %}
16+
17+
<h3>Instances</h3>
18+
{% if query %}
19+
<p><em>with search query "<strong>{{ query }}</strong>"</em></p>
20+
<form action="">
21+
<input type="submit" value="Go back to all items">
22+
</form>
23+
<br>
24+
{% endif %}
25+
</div>
26+
<div class="col-md-8">
27+
<ul>
28+
{%- for item in register_items -%}
29+
<li><a href="{{ item[0].replace('http://linked.data.gov.au/dataset/qld-structural-framework', 'http://localhost:5000') }}">{{ item[1] }}</a></li>
30+
{%- endfor -%}
31+
</ul>
32+
</div>
33+
<div class="col-md-4">
34+
<div class="altview">
35+
<h4>Alternates Profiles</h4>
36+
<p>Different profile views of this register are here: <a href="{{ request.base_url }}?_profile=all">All Profiles</a>.</p>
37+
</div>
38+
39+
<div class="autopaginationinfo">
40+
<div><h4>Automated Pagination&nbsp;<span id="collapsible-toggle" class="collapsible" style="font-size: 14px;">(more)</span></h4></div>
41+
<div id="content-pagination" class="collapsible-content">
42+
<p>To page through these items, use query string arguments 'page' and 'per_page'. HTTP <code>Link</code> headers of <code>first</code>, <code>prev</code>, <code>next</code> &amp; <code>last</code> are given in web responses.</p>
43+
<p>Example, assuming 500 items, page 7, of 50 per page, is given by:</p>
44+
<pre>.../?page=7&amp;per_page=50
45+
</pre>
46+
<p>Link header with a response assuming 500 items would be:</p>
47+
<pre>Link: &lt;.../?per_page=500&gt; rel="first",
48+
&lt;.../?per_page=500&amp;page=6&gt; rel="prev",
49+
&lt;.../?per_page=500&amp;page=8&gt; rel="next",
50+
&lt;.../?per_page=500&amp;page=10&gt; rel="last"
51+
</pre>
52+
<p>If you want to page through the whole collection, start at <code>first</code> and follow link headers until you reach <code>last</code> or until no <code>last</code> is given. You shouldn't try to calculate each <code>page</code> query string argument yourself.</p>
53+
</div>
54+
</div>
55+
</div>
56+
</div>
57+
<!-- pagination links as per MediaTypes -->
58+
{% if pagination.links %}
59+
<h5>Paging</h5>
60+
{% endif %}
61+
{{ pagination.links }}
62+
<script>
63+
var coll = document.getElementById("collapsible-toggle");
64+
65+
coll.addEventListener("click", function() {
66+
var content = document.getElementById("content-pagination");
67+
if (content.style.display === "inline") {
68+
content.style.display = "none";
69+
document.getElementsByClassName("collapsible")[0].innerHTML = "(more)";
70+
} else {
71+
content.style.display = "inline";
72+
document.getElementsByClassName("collapsible")[0].innerHTML = "(less)";
73+
}
74+
});
75+
76+
let cards = document.getElementsByClassName("card")
77+
let i;
78+
for (i = 0; i < cards.length; i++) {
79+
if (cards[i].children[1].children[0] === undefined) {
80+
cards[i].children[1].innerHTML = "<em>No metadata found.</em>";
81+
}
82+
}
83+
</script>
84+
{% endblock %}
85+

‎pyldapi/tests/test_conneg_by_p.py

-2
This file was deleted.

‎pyldapi/tests/test_renderer.py

-207
This file was deleted.

‎requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
sphinx
22
sphinx_rtd_theme
3+
pytest

‎setup.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ def open_local(paths, mode='r', encoding='utf8'):
3434
description='A very small module to add Linked Data API functionality to '
3535
'a Python Flask installation',
3636
author='Nicholas Car',
37-
author_email='nicholas.car@csiro.au',
38-
url='https://github.com/CSIRO-enviro-informatics/pyldapi',
39-
download_url='https://github.com/CSIRO-enviro-informatics/'
40-
'pyldapi/archive/v{:s}.tar.gz'.format(version),
37+
author_email='nicholas.car@surroundaustralia.com',
38+
url='https://github.com/RDFLib/pyLDAPI',
39+
download_url='https://github.com/RDFLib/pyLDAPI'
40+
'/archive/v{:s}.tar.gz'.format(version),
4141
license='LICENSE.txt',
4242
keywords=['Linked Data', 'Semantic Web', 'Flask', 'Python', 'API', 'RDF'],
4343
long_description=long_description,
@@ -59,9 +59,8 @@ def open_local(paths, mode='r', encoding='utf8'):
5959
'Topic :: Software Development :: Libraries :: Python Modules',
6060
],
6161
project_urls={
62-
'Bug Reports':
63-
'https://github.com/CSIRO-enviro-informatics/pyldapi/issues',
64-
'Source': 'https://github.com/CSIRO-enviro-informatics/pyldapi/',
62+
'Bug Reports': 'https://github.com/RDFLib/pyLDAPI/issues',
63+
'Source': 'https://github.com/RDFLib/pyLDAPI/',
6564
},
6665
install_requires=install_requires,
6766
)

‎tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import pyldapi

‎example.py ‎tests/example.py

+49-45
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,56 @@
22

33
from flask import Flask, Response, request, render_template
44
from pyldapi import setup as pyldapi_setup
5-
from pyldapi import RegisterOfRegistersRenderer, RegisterRenderer, Renderer, View
5+
from pyldapi import RegisterOfRegistersRenderer, RegisterRenderer, Renderer, Profile
66

77
API_BASE = 'http://127.0.0.1:8081'
88

99
cats = [
1010
{
11-
"name": "Jonny",
12-
"breed": "DomesticShorthair",
13-
"age": 10,
14-
"color": "tortoiseshell",
11+
'name': 'Jonny',
12+
'breed': 'DomesticShorthair',
13+
'age': 10,
14+
'color': 'tortoiseshell',
1515
}, {
16-
"name": "Sally",
17-
"breed": "Manx",
18-
"age": 3,
19-
"color": "brown",
16+
'name': 'Sally',
17+
'breed': 'Manx',
18+
'age': 3,
19+
'color': 'brown',
2020
}, {
21-
"name": "Spud",
22-
"breed": "Persian",
23-
"age": 7,
24-
"color": "grey",
21+
'name': 'Spud',
22+
'breed': 'Persian',
23+
'age': 7,
24+
'color': 'grey',
2525
}
2626
]
2727

2828
dogs = [
2929
{
30-
"name": "Rex",
31-
"breed": "Dachshund",
32-
"age": 7,
33-
"color": "brown",
30+
'name': 'Rex',
31+
'breed': 'Dachshund',
32+
'age': 7,
33+
'color': 'brown',
3434
}, {
35-
"name": "Micky",
36-
"breed": "Alsatian",
37-
"age": 3,
38-
"color": "black",
35+
'name': 'Micky',
36+
'breed': 'Alsatian',
37+
'age': 3,
38+
'color': 'black',
3939
}
4040
]
4141

42-
MyPetView = View("PetView", "A profile of my pet.", ['text/html', 'application/json'],
43-
'text/html', profile_uri="http://example.org/def/mypetview")
42+
MyPetView = Profile(
43+
'PetView',
44+
'A profile of my pet.',
45+
['text/html', 'application/json'],
46+
'text/html',
47+
profile_uri='http://example.org/def/mypetprofile')
4448

4549
app = Flask(__name__)
4650

4751

4852
class PetRenderer(Renderer):
4953
def __init__(self, request, instance_uri, instance, pet_html_template, **kwargs):
50-
self.views = {'mypetview': MyPetView}
54+
self.profiles = {'mypetprofile': MyPetView}
5155
self.default_view_token = 'mypetview'
5256
super(PetRenderer, self).__init__(
5357
request, instance_uri, self.views, self.default_view_token, **kwargs)
@@ -56,19 +60,19 @@ def __init__(self, request, instance_uri, instance, pet_html_template, **kwargs)
5660

5761
def _render_mypetview(self):
5862
self.headers['Profile'] = 'http://example.org/def/mypetview'
59-
if self.format == "application/json":
63+
if self.format == 'application/json':
6064
return Response(json.dumps(self.instance),
61-
mimetype="application/json", status=200)
62-
elif self.format == "text/html":
65+
mimetype='application/json', status=200)
66+
elif self.format == 'text/html':
6367
return Response(render_template(self.pet_html_template, **self.instance))
6468

6569
# All `Renderer` subclasses _must_ implement render
6670
def render(self):
6771
response = super(PetRenderer, self).render()
68-
if not response and self.view == 'mypetview':
72+
if not response and self.profile == 'mypetview':
6973
response = self._render_mypetview()
7074
else:
71-
raise NotImplementedError(self.view)
75+
raise NotImplementedError(self.profile)
7276
return response
7377

7478

@@ -80,7 +84,7 @@ def dog_instance(dog_id):
8084
instance = d
8185
break
8286
if instance is None:
83-
return Response("Not Found", status=404)
87+
return Response('Not Found', status=404)
8488
renderer = PetRenderer(request, request.base_url, instance, 'dog.html')
8589
return renderer.render()
8690

@@ -93,34 +97,34 @@ def cat_instance(cat_id):
9397
instance = c
9498
break
9599
if instance is None:
96-
return Response("Not Found", status=404)
100+
return Response('Not Found', status=404)
97101
renderer = PetRenderer(request, request.base_url, instance, 'cat.html')
98102
return renderer.render()
99103

100104

101105
@app.route('/cats')
102106
def cats_reg():
103-
cat_items = [("http://example.com/id/cat/{}".format(i['name']), i['name']) for i in cats]
107+
cat_items = [('http://example.com/id/cat/{}'.format(i['name']), i['name']) for i in cats]
104108
r = RegisterRenderer(request,
105109
API_BASE + '/cats',
106-
"Cats Register",
107-
"A complete register of my cats.",
110+
'Cats Register',
111+
'A complete register of my cats.',
108112
cat_items,
109-
["http://example.com/Cat"],
113+
['http://example.com/Cat'],
110114
len(cat_items),
111115
super_register=API_BASE + '/'
112116
)
113117
return r.render()
114118

115119
@app.route('/dogs')
116120
def dogs_reg():
117-
dog_items = [("http://example.com/id/dog/{}".format(i['name']), i['name']) for i in dogs]
121+
dog_items = [('http://example.com/id/dog/{}'.format(i['name']), i['name']) for i in dogs]
118122
r = RegisterRenderer(request,
119123
API_BASE + '/dogs',
120-
"Dogs Register",
121-
"A complete register of my dogs.",
124+
'Dogs Register',
125+
'A complete register of my dogs.',
122126
dog_items,
123-
["http://example.com/Dog"],
127+
['http://example.com/Dog'],
124128
len(dog_items),
125129
super_register=API_BASE + '/',
126130
register_template='register.html',
@@ -133,13 +137,13 @@ def dogs_reg():
133137
def index():
134138
rofr = RegisterOfRegistersRenderer(request,
135139
API_BASE,
136-
"Register of Registers",
137-
"A register of all of my registers.",
138-
"./rofr.ttl"
140+
'Register of Registers',
141+
'A register of all of my registers.',
142+
'./rofr.ttl'
139143
)
140144
return rofr.render()
141145

142146

143-
if __name__ == "__main__":
144-
pyldapi_setup(app, '.', API_BASE)
145-
app.run("127.0.0.1", 8081, debug=True, threaded=True, use_reloader=False)
147+
if __name__ == '__main__':
148+
pyldapi_setup(app, '..', API_BASE)
149+
app.run('127.0.0.1', 8081, debug=True, threaded=True, use_reloader=False)

‎tests/test_conneg_by_p.py

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from pyldapi import Renderer, Profile
2+
3+
4+
# Mock class
5+
class Request:
6+
pass
7+
8+
9+
class MockRenderer(Renderer):
10+
def render(self):
11+
# use the now gotten profile & format to create a response
12+
pass
13+
14+
15+
def setup():
16+
# profiles for testing
17+
global profiles
18+
profiles = {
19+
'agor': Profile(
20+
'AGOR Profile',
21+
'A profile of organisations according to the Australian Government Organisations Register',
22+
['text/html'] + Renderer.RDF_MIMETYPES,
23+
'text/turtle',
24+
profile_uri='http://linked.data.gov.au/def/agor'
25+
),
26+
'fake': Profile(
27+
'Fake Profile',
28+
'A fake Profile for testing',
29+
['text/xml'],
30+
'text/xml',
31+
profile_uri='http://fake.com'
32+
),
33+
'other': Profile(
34+
'Another Testing Profile',
35+
'Another profile for testing',
36+
['text/html', 'text/xml'],
37+
'text/html',
38+
profile_uri='http://other.com'
39+
)
40+
# 'alternates' # included by default
41+
# 'all' # included by default
42+
}
43+
44+
# this tests Accept-Profile selection of 'fake' profile
45+
mr = Request()
46+
mr.url = 'http://whocares.com'
47+
mr.values = {}
48+
mr.headers = {
49+
'Accept-Profile': 'http://nothing.com ,' # ignored - broken, no <>
50+
'http://nothing-else.com,' # ignored - broken, no <>
51+
'<http://notavailable.com>; q=0.9, ' # not available
52+
'<http://linked.data.gov.au/def/agor>; q=0.1, ' # available but lower weight
53+
'<http://fake.com>; q=0.2', # should be this
54+
'Accept': 'text/turtle'
55+
}
56+
57+
# this tests QSA selection of 'alternates' profile
58+
mr2 = Request()
59+
mr2.url = 'http://whocares.com'
60+
mr2.values = {'_profile': 'alternates'}
61+
mr2.headers = {}
62+
63+
global r
64+
r = MockRenderer(
65+
mr,
66+
'http://whocares.com',
67+
profiles,
68+
'agor'
69+
)
70+
71+
global r2
72+
r2 = MockRenderer(
73+
mr2,
74+
'http://whocares.com',
75+
profiles,
76+
'agor'
77+
)
78+
79+
80+
def test_content_profile():
81+
expected = '<http://fake.com>'
82+
actual = r.headers.get('Content-Profile')
83+
assert actual == expected, \
84+
'test_list_profiles() test 1: Content-Profile expected to be {}, was {}'.format(
85+
expected,
86+
actual
87+
)
88+
89+
90+
def test_list_profiles():
91+
expected = \
92+
'<http://www.w3.org/ns/dx/prof/Profile>; rel="type"; token="agor"; anchor=<http://linked.data.gov.au/def/agor>, ' \
93+
'<http://www.w3.org/ns/dx/prof/Profile>; rel="type"; token="fake"; anchor=<http://fake.com>, ' \
94+
'<http://www.w3.org/ns/dx/prof/Profile>; rel="type"; token="other"; anchor=<http://other.com>, ' \
95+
'<http://www.w3.org/ns/dx/prof/Profile>; rel="type"; token="alt"; anchor=<http://www.w3.org/ns/dx/conneg/altr>, ' \
96+
\
97+
'<http://whocares.com?_profile=agor&_format=text/html>; rel="alternate"; type="text/html"; profile="http://linked.data.gov.au/def/agor", ' \
98+
'<http://whocares.com?_profile=agor&_format=text/turtle>; rel="alternate"; type="text/turtle"; profile="http://linked.data.gov.au/def/agor", ' \
99+
'<http://whocares.com?_profile=agor&_format=application/rdf+xml>; rel="alternate"; type="application/rdf+xml"; profile="http://linked.data.gov.au/def/agor", ' \
100+
'<http://whocares.com?_profile=agor&_format=application/ld+json>; rel="alternate"; type="application/ld+json"; profile="http://linked.data.gov.au/def/agor", ' \
101+
'<http://whocares.com?_profile=agor&_format=text/n3>; rel="alternate"; type="text/n3"; profile="http://linked.data.gov.au/def/agor", ' \
102+
'<http://whocares.com?_profile=agor&_format=application/n-triples>; rel="alternate"; type="application/n-triples"; profile="http://linked.data.gov.au/def/agor", ' \
103+
'<http://whocares.com?_profile=fake&_format=text/xml>; rel="alternate"; type="text/xml"; profile="http://fake.com", ' \
104+
'<http://whocares.com?_profile=other&_format=text/html>; rel="alternate"; type="text/html"; profile="http://other.com", ' \
105+
'<http://whocares.com?_profile=other&_format=text/xml>; rel="alternate"; type="text/xml"; profile="http://other.com", ' \
106+
'<http://whocares.com?_profile=alt&_format=text/html>; rel="alternate"; type="text/html"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
107+
'<http://whocares.com?_profile=alt&_format=application/json>; rel="alternate"; type="application/json"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
108+
'<http://whocares.com?_profile=alt&_format=text/turtle>; rel="alternate"; type="text/turtle"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
109+
'<http://whocares.com?_profile=alt&_format=application/rdf+xml>; rel="alternate"; type="application/rdf+xml"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
110+
'<http://whocares.com?_profile=alt&_format=application/ld+json>; rel="alternate"; type="application/ld+json"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
111+
'<http://whocares.com?_profile=alt&_format=text/n3>; rel="alternate"; type="text/n3"; profile="http://www.w3.org/ns/dx/conneg/altr", ' \
112+
'<http://whocares.com?_profile=alt&_format=application/n-triples>; rel="alternate"; type="application/n-triples"; profile="http://www.w3.org/ns/dx/conneg/altr"'
113+
actual = r.headers.get('Link')
114+
assert actual == expected, \
115+
'test_list_profiles() test 1: Content-Profile expected to be {}, was {}'.format(
116+
expected,
117+
actual
118+
)
119+
120+
121+
if __name__ == '__main__':
122+
setup()
123+
# test_content_profile()
124+
test_list_profiles()
125+
126+
print('Passed all tests')

‎tests/test_renderer.py

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from pyldapi import Renderer, Profile
2+
3+
4+
# Mock class
5+
class Request:
6+
pass
7+
8+
9+
class MockRenderer(Renderer):
10+
def render(self):
11+
# use the now gotten view & format to create a response
12+
pass
13+
14+
15+
def setup():
16+
# profiles for testing
17+
global profiles
18+
profiles = {
19+
'agor': Profile(
20+
'AGOR Profile',
21+
'A profile of organisations according to the Australian Government Organisations Register',
22+
['text/html'] + Renderer.RDF_MIMETYPES,
23+
'text/turtle',
24+
profile_uri='http://linked.data.gov.au/def/agor'
25+
),
26+
'fake': Profile(
27+
'Fake Profile',
28+
'A fake Profile for testing',
29+
['text/xml'],
30+
'text/xml',
31+
profile_uri='http://fake.com'
32+
),
33+
'other': Profile(
34+
'Another Testing Profile',
35+
'Another profile for testing',
36+
['text/html', 'text/xml'],
37+
'text/html',
38+
profile_uri='http://other.com'
39+
)
40+
# 'alt' # included by default
41+
}
42+
43+
# this tests Accept-Profile selection of 'fake' profile
44+
mr = Request()
45+
mr.url = 'http://whocares.com'
46+
mr.values = {}
47+
mr.headers = {
48+
'Accept-Profile': 'http://nothing.com ,' # ignored - broken, no <>
49+
'http://nothing-else.com,' # ignored - broken, no <>
50+
'<http://notavailable.com>; q=0.9, ' # not available
51+
'<http://linked.data.gov.au/def/agor>; q=0.1, ' # available but lower weight
52+
'<http://fake.com>; q=0.2', # should be this
53+
'Accept': 'text/turtle'
54+
}
55+
56+
# this tests QSA selection of 'alt' profile
57+
mr2 = Request()
58+
mr2.url = 'http://whocares.com'
59+
mr2.values = {'_profile': 'alt'}
60+
mr2.headers = {}
61+
62+
global r
63+
r = MockRenderer(
64+
mr,
65+
'http://whocares.com',
66+
profiles,
67+
'agor'
68+
)
69+
70+
global r2
71+
r2 = MockRenderer(
72+
mr2,
73+
'http://whocares.com',
74+
profiles,
75+
'agor'
76+
)
77+
78+
79+
def test_get_profiles_from_http():
80+
expected = ['fake', 'agor']
81+
actual = r._get_profiles_from_http()
82+
assert actual == expected, \
83+
'r failed _get_profiles_from_http() test 1. Got {}, expected {}'.format(actual, expected)
84+
85+
expected = None
86+
actual = r2._get_profiles_from_http()
87+
assert actual == expected, \
88+
'r2 failed _get_profiles_from_http() test 2. Got {}, expected {}'.format(actual, expected)
89+
90+
91+
def test_get_profiles_from_qsa():
92+
expected = None
93+
actual = r._get_profiles_from_qsa()
94+
assert actual == expected, \
95+
'r failed _get_profiles_from_qsa() test 1. Got {}, expected {}'.format(actual, expected)
96+
97+
expected = ['alt']
98+
actual = r2._get_profiles_from_qsa()
99+
assert actual == expected, \
100+
'r2 failed _get_profiles_from_qsa() test 2. Got {}, expected {}'.format(actual, expected)
101+
102+
103+
def test_get_available_profiles():
104+
expected = {'agor', 'alt', 'fake', 'other'}
105+
actual = set(r._get_available_profiles().values())
106+
assert actual == expected, \
107+
'r failed test_get_available_profiles() test 1. Got {}, expected {}'.format(actual, expected)
108+
109+
110+
def test_get_profile():
111+
expected = 'fake'
112+
actual = r._get_profile()
113+
assert actual == expected, \
114+
'r failed test_get_profile() test 1. Got {}, expected {}'.format(actual, expected)
115+
116+
expected = 'alt'
117+
actual = r2._get_profile()
118+
assert actual == expected, \
119+
'r2 failed test_get_profile() test 2. Got {}, expected {}'.format(actual, expected)
120+
121+
# testing the return of default ('agor') when no existing profiles are quested for
122+
mr3 = Request()
123+
mr3.url = 'http://whocares.com'
124+
mr3.values = {}
125+
mr3.headers = {
126+
'Accept-Profile': '<http://junk.com>; q=0.9, '
127+
'<http://otherjunk.com>; q=0.1',
128+
'Accept': 'text/turtle'
129+
}
130+
131+
global profiles
132+
r3 = MockRenderer(
133+
mr3,
134+
'http://whocares.com',
135+
profiles,
136+
'agor'
137+
)
138+
139+
expected = 'agor' # default, since requests gets no legit profile
140+
actual = r3._get_profile()
141+
assert actual == expected, \
142+
'r failed test_get_profile() test 3. Got {}, expected {}'.format(actual, expected)
143+
144+
145+
def test_get_mediatype():
146+
mr4 = Request()
147+
mr4.url = 'http://whocares.com'
148+
mr4.values = {'_mediatype': 'text/turtle;q=0.5,application/rdf+xml,application/json+ld;q=0.6'}
149+
mr4.headers = {}
150+
151+
r4 = MockRenderer(
152+
mr4,
153+
'http://whocares.com',
154+
profiles,
155+
'agor' # default view
156+
)
157+
expected = 'application/rdf+xml'
158+
actual = r4._get_mediatype()
159+
assert actual == expected, \
160+
'r4 failed test_get_mediatype() test 1. Got {}, expected {}'.format(actual, expected)
161+
162+
163+
if __name__ == '__main__':
164+
setup()
165+
test_get_profiles_from_http()
166+
test_get_profiles_from_qsa()
167+
test_get_available_profiles()
168+
test_get_profile()
169+
test_get_mediatype()
170+
171+
print('Passed all tests')

0 commit comments

Comments
 (0)
Please sign in to comment.