mirror of
https://github.com/dnlbauer/django-signposting.git
synced 2026-09-10 22:15:30 +00:00
Compare commits
37 Commits
0.0.2
...
hermes/cur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3b7ab7bd | ||
|
|
b58c0acda5 | ||
|
|
4ab82d452a | ||
|
|
25fd191ca0 | ||
|
|
96f9b0ee94 | ||
|
|
8d7cd2fd32 | ||
|
|
b29f19d78a | ||
|
|
06d745e4a4 | ||
|
|
7a414e5772 | ||
|
|
d11f2246df | ||
|
|
f2a803106f | ||
|
|
6b1ea1403c | ||
|
|
f97c33e418 | ||
|
|
0f27560015 | ||
|
|
7b93eae884 | ||
|
|
0cd321112f | ||
|
|
bbf0527733 | ||
|
|
8b09f4feb5 | ||
|
|
01affafd7a | ||
|
|
76e791914d | ||
|
|
cbd621435c | ||
|
|
8c548d2392 | ||
|
|
1bc2b60609 | ||
|
|
aafd404f90 | ||
|
|
2014a4f024 | ||
|
|
31fcf221f0 | ||
|
|
bf2604e303 | ||
|
|
2d3dafbacc | ||
|
|
076273d014 | ||
|
|
93738b046f | ||
|
|
a863272c7e | ||
|
|
bbd07d3fab | ||
|
|
695c167d32 | ||
|
|
0a31aa21ff | ||
|
|
aa5850b0bd | ||
|
|
b18a48cf7e | ||
|
|
7a421160b2 |
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
exclude = .git, __pycache__, .pytest_cache, .venv, venv, build
|
||||
157
.github/workflows/hermes_github.yml
vendored
Normal file
157
.github/workflows/hermes_github.yml
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
# SPDX-FileCopyrightText: 2023 German Aerospace Center (DLR), Forschungszentrum Jülich, Helmholtz-Zentrum Dresden-Rossendorf
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
###############################################################################################################
|
||||
# TEMPLATE! To use this template, do the following:
|
||||
#
|
||||
# 1. Copy this file into the .github/workflows/ directory in your repository and remove the 'TEMPLATE_' prefix.
|
||||
# 2. Go through the file and carefully adapt to your needs, as marked by an '# ADAPT' comment.
|
||||
###############################################################################################################
|
||||
|
||||
name: Software Publication
|
||||
|
||||
on:
|
||||
# ADAPT
|
||||
# See the events you can react to at https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# NOTE: Do not delete the trigger on closed pull requests, the HERMES workflow needs this.
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
hermes-prepare:
|
||||
name: Prepare Metadata for Curation
|
||||
runs-on: ubuntu-latest
|
||||
# This condition becomes much easier when we only react to push events on the release branch.
|
||||
# We still need to exclude the merge commit push of the post processing PR
|
||||
|
||||
# ADAPT
|
||||
# Depending on the event you react to in the 'on:' section above, you will need to adapt this
|
||||
# to react on the specific events.
|
||||
# NOTE: You will probably still need to keep the exclusion check for commit messages provided by the workflow ('hermes/'/'hermes/post').
|
||||
if: >
|
||||
github.event_name == 'push' && ! (
|
||||
startsWith(github.ref_name, 'hermes/') ||
|
||||
contains(github.event.head_commit.message, 'hermes/post')
|
||||
)
|
||||
|
||||
permissions:
|
||||
contents: write # Allow creation of new branches
|
||||
pull-requests: write # Postprocessing should be able to create a pull request with changes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- run: pip install git+https://github.com/softwarepub/hermes.git
|
||||
- run: hermes harvest
|
||||
- run: hermes process
|
||||
- run: hermes curate
|
||||
|
||||
- run: |
|
||||
# Cache current branch for PR close job
|
||||
git branch --show-current > .hermes/curate/target_branch
|
||||
|
||||
# Shorten the SHA for the PR title
|
||||
SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c -8)
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
|
||||
# Create a curation branch
|
||||
git branch -c "hermes/curate-$SHORT_SHA"
|
||||
git push origin "hermes/curate-$SHORT_SHA"
|
||||
|
||||
# Explicitly add to-be-curated metadata (which is ignored via .gitignore!)
|
||||
git add -f .hermes/curate
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
base: hermes/curate-${{ env.SHORT_SHA }}
|
||||
branch: hermes/curate-result-${{ env.SHORT_SHA }}
|
||||
title: Metadata Curation for Commit ${{ env.SHORT_SHA }}
|
||||
body: |
|
||||
Please carefully review the attached metadata.
|
||||
If you are satisfied with the result, you may merge this PR, which will trigger publication.
|
||||
(Any temporary branches will be cleaned up.)
|
||||
delete-branch: true
|
||||
|
||||
hermes-curate:
|
||||
name: Publish Software with Curated Metadata
|
||||
if: github.event.pull_request.merged == true && startsWith( github.base_ref , 'hermes/curate-')
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Allow creation of new branches
|
||||
pull-requests: write # Postprocessing should be able to create a pull request with changes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- run: pip install git+https://github.com/softwarepub/hermes.git
|
||||
|
||||
# ADAPT
|
||||
# If you want to publish artifacts (e.g., a zipped snapshot of your repository),
|
||||
# you can prepare this here.
|
||||
- run: git archive --format zip HEAD example tests django_signposting > artifact.zip
|
||||
|
||||
# Run the HERMES deposition and postprocessing steps.
|
||||
# ADAPT
|
||||
# 1. You need to have an authentication token for your target publication platform
|
||||
# as a GitHub secret in your repository (in this example, this is called ZENODO_SANDBOX).
|
||||
# 2. Adapt the files you want to deposit. In the example, showcase.zip and README.md are deposited alongside the metadata.
|
||||
# 3. Check if you want to run with '--initial', as this may potentially create a completely new record (collection),
|
||||
# rather than a new version of the same collection!
|
||||
- run: hermes deposit --initial -O invenio_rdm.auth_token ${{ secrets.ZENODO_SANDBOX }} --file artifact.zip --file README.md
|
||||
|
||||
# ADAPT
|
||||
# Remove this command if you don't want to do any postprocessing
|
||||
- run: hermes postprocess
|
||||
|
||||
# ADAPT
|
||||
# If you don't want to run postprocessing, remove this complete section (next '-run' and 'uses: peter-evans/...' bullets).
|
||||
#
|
||||
# Note 1: We change the base branch here for the PR. This flow runs so far within the "curated-metadata-*" branch,
|
||||
# but now we want to add the changes done by deposit and post processing to the branch that was initially
|
||||
# meant to be published using HERMES.
|
||||
# Note 2: The create-pull-request action will NOT inherit the commits we did in the previous job. It will only look at the
|
||||
# changes within this local workspace we did *now*.
|
||||
- run: echo "TARGET_BRANCH=$(cat .hermes/curate/target_branch)" >> "$GITHUB_ENV"
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
branch: hermes/post-${{ github.run_id }}
|
||||
base: ${{ env.TARGET_BRANCH }}
|
||||
title: Review hermes post-processing results
|
||||
body: |
|
||||
This is an automated pull request created by HERMES post-processing.
|
||||
|
||||
Please carefully review the changes and finally merge them into your
|
||||
|
||||
# Delete all the curation branches
|
||||
- run: |
|
||||
for BRANCH in $(git ls-remote origin 'refs/heads/hermes/curate-*' | cut -f2 | cut -d'/' -f'3-'); do
|
||||
git push origin --delete "$BRANCH"
|
||||
done
|
||||
# TODO: if: failure() --- delete the curation branches when the deposition failed
|
||||
|
||||
|
||||
hermes-cleanup:
|
||||
name: Cleanup aborted curation branches
|
||||
if: github.event.pull_request.merged == false && startsWith( github.base_ref , 'hermes/curate-')
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Allow creation of new branches
|
||||
pull-requests: write # Postprocessing should be able to create a pull request with changes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# Delete all the curation branches
|
||||
- run: |
|
||||
for BRANCH in $(git ls-remote origin 'refs/heads/hermes/curate-*' | cut -f2 | cut -d'/' -f'3-'); do
|
||||
git push origin --delete "$BRANCH"
|
||||
done
|
||||
5
.github/workflows/python-package.yml
vendored
5
.github/workflows/python-package.yml
vendored
@@ -32,10 +32,7 @@ jobs:
|
||||
python -m pip install -e '.[dev]'
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
flake8 . --count --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
|
||||
7
.github/workflows/python-publish.yml
vendored
7
.github/workflows/python-publish.yml
vendored
@@ -29,7 +29,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
python -m pip install build
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pip install .
|
||||
python -m pip install -e '.[dev]'
|
||||
pytest
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,3 +4,7 @@ dist
|
||||
.idea
|
||||
build
|
||||
__pycache__
|
||||
*.sqlite3
|
||||
# Ignoring all HERMES cache files
|
||||
.hermes/
|
||||
hermes.log
|
||||
|
||||
31
.hermes/curate/hermes.json
Normal file
31
.hermes/curate/hermes.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://doi.org/10.5063/schema/codemeta-2.0",
|
||||
{
|
||||
"hermes": "https://software-metadata.pub/ns/hermes/"
|
||||
}
|
||||
],
|
||||
"@type": "SoftwareSourceCode",
|
||||
"author": [
|
||||
{
|
||||
"@id": "https://orcid.org/0000-0001-9447-460X",
|
||||
"@type": "Person",
|
||||
"affiliation": {
|
||||
"@type": "Organization",
|
||||
"legalName": "Senckenberg Society for Nature Research"
|
||||
},
|
||||
"familyName": "Bauer",
|
||||
"givenName": "Daniel",
|
||||
"email": "daniel.bauer@senckenberg.de"
|
||||
}
|
||||
],
|
||||
"description": "django_signposting is a Django middleware library that facilitates the addition of FAIR signposting headers to HTTP responses. This middleware helps in making your data more FAIR (Findable, accessible, interoperable, reuseable) by embedding signposting headers in responses, guiding clients to relevant resources linked to the response content.",
|
||||
"keywords": [
|
||||
"django",
|
||||
"FAIR",
|
||||
"signposting",
|
||||
"links"
|
||||
],
|
||||
"license": "https://spdx.org/licenses/MIT",
|
||||
"name": "FAIR signposting middleware for Django"
|
||||
}
|
||||
1
.hermes/curate/target_branch
Normal file
1
.hermes/curate/target_branch
Normal file
@@ -0,0 +1 @@
|
||||
main
|
||||
3
.pre-commit-config.yaml
Normal file
3
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
hooks:
|
||||
- id: flake8
|
||||
29
CITATION.cff
Normal file
29
CITATION.cff
Normal file
@@ -0,0 +1,29 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: FAIR signposting middleware for Django
|
||||
message: >-
|
||||
If you use this software, please cite it using the
|
||||
metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- given-names: Daniel
|
||||
family-names: Bauer
|
||||
email: daniel.bauer@senckenberg.de
|
||||
affiliation: Senckenberg Society for Nature Research
|
||||
orcid: 'https://orcid.org/0000-0001-9447-460X'
|
||||
abstract: >-
|
||||
django_signposting is a Django middleware library that
|
||||
facilitates the addition of FAIR signposting headers to
|
||||
HTTP responses. This middleware helps in making your data
|
||||
more FAIR (Findable, accessible, interoperable, reuseable)
|
||||
by embedding signposting headers in responses, guiding
|
||||
clients to relevant resources linked to the response
|
||||
content.
|
||||
keywords:
|
||||
- django
|
||||
- FAIR
|
||||
- signposting
|
||||
- links
|
||||
license: MIT
|
||||
157
README.md
157
README.md
@@ -1,26 +1,84 @@
|
||||
# FAIR signposting for Django
|
||||
[](https://github.com/dnlbauer/django-signposting/actions/workflows/python-package.yml)
|
||||
|
||||
# FAIR signposting middleware for Django
|
||||
|
||||
`django_signposting` is a Django middleware library that facilitates the addition of
|
||||
FAIR signposting headers to HTTP responses.
|
||||
This middleware helps in making your data more FAIR (Findable, accessible, interoperable, reuseable) by
|
||||
embedding signposting headers in responses, guiding clients to relevant resources linked to the response content.
|
||||
|
||||
Based on the [Signposting](https://github.com/stain/signposting) library.
|
||||
|
||||
## Features
|
||||
- Automatically adds signposting headers to HTTP responses.
|
||||
- Signposts can be added manually or automatically be parsed from JSON-LD/schema.org
|
||||
- Supports multiple relation types with optional media type specification.
|
||||
- Easily integrable with existing Django applications.
|
||||
|
||||
### Signposts are formatted and added as Link headers
|
||||
|
||||
```bash
|
||||
curl -I http://localhost:8000
|
||||
HTTP/2 200
|
||||
...
|
||||
link: <https://schema.org/Dataset> ; rel="type" ,
|
||||
<https://orcid.org/0000-0001-9447-460X> ; rel="author" ,
|
||||
<https://example.com/download.zip> ; rel="item" ; type="application/zip"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install django_signposting
|
||||
pip install django-signposting
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Add Middleware
|
||||
### Automatic parsing of JSON-LD
|
||||
|
||||
Add the middleware to your Django project's `MIDDLEWARE` setting in `settings.py`:
|
||||
The library can automatically add signposts to web pages that already make
|
||||
metadata available via JSON-LD in HTML via `<script type="application/ld+json">` tags, i.e.:
|
||||
|
||||
```HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<script type="application/ld+json">{"@context": "https://schema.org", "@type": ["WebSite", "Dataset"], "author": {"@type": "Person", "name": "Daniel Bauer", "url": "https://orcid.org/0000-0001-9447-460X"}, "description": "A dataset of things.", "hasPart": [{"@type": "ImageObject", "encodingFormat": "image/png", "url": "http://example.com/image.png"}, {"@type": "ImageObject", "encodingFormat": "image/png", "url": "http://example.com/image2.png"}], "license": {"@type": "CreativeWork", "name": "CC BY 4.0", "url": "https://creativecommons.org/licenses/by/4.0/"}, "name": "My Dataset", "sameAs": [{"@type": "MediaObject", "contentUrl": "https://example.com/download.zip", "encodingFormat": "application/zip"}, {"@type": "MediaObject", "contentUrl": "https://example.com/metadata.json"}], "url": "https://example.com"}</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, world!</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
To enable automatic parsing of JSON-LD, add the following middleware classes to your Django project's MIDDLEWARE setting in settings.py:
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
...,
|
||||
'django_signposting.middleware.SignpostingMiddleware',
|
||||
'django_signposting.middleware.JsonLdSignpostingParserMiddleware',
|
||||
...,
|
||||
]
|
||||
```
|
||||
|
||||
This extracts supported metadatad properties from JSON-LD and adds the corresponding signposting headers to the `HttpResponse` automatically.
|
||||
|
||||
Automatic parsing is compatible with extensions that provide JSON-LD as part of a web page, such as [django-json-ld](https://pypi.org/project/django-json-ld/).
|
||||
It can also extract signposts from rich metadata descriptions of datasets such as detached [RO-Crates](https://www.researchobject.org/ro-crate) (see [example views](./example/example/views.py)).
|
||||
|
||||
> Note: The middleware order is important! Place `SignpostingMiddleware` before `JsonLdSignpostingParserMiddleware` to ensure proper extraction and processing of JSON-LD content.
|
||||
|
||||
## Alternative approaches
|
||||
|
||||
### Manual signposting
|
||||
|
||||
For cases where there is no embedded JSON-LD, or you want to specify additional headers manually, you can use the `add_signposts` utility function.
|
||||
This still requires the `SignpostingMiddleware` to inject the links into
|
||||
the response:
|
||||
|
||||
1. **Add Middleware**: Add the `SignpostingMiddleware` to your Django project's `MIDDLEWARE` setting in `settings.py`:
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
@@ -30,60 +88,83 @@ MIDDLEWARE = [
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Add Signposts to your Views
|
||||
|
||||
You can add signposting headers in your Django views using the provided `add_signposts` utility function.
|
||||
Here's how you can use it:
|
||||
2. **Add Signposts to your Views:** Use the `add_signposts` utility function:
|
||||
|
||||
```python
|
||||
from django.http import HttpResponse
|
||||
from django_signposting.util import add_signposts
|
||||
from django_signposting.utils import add_signposts
|
||||
from signposting import Signpost, LinkRel
|
||||
|
||||
def my_view(request):
|
||||
response = HttpResponse("Hello, world!")
|
||||
|
||||
# Add signpostings as string
|
||||
add_signposts(response,
|
||||
type="https://schema.org/Dataset",
|
||||
author="https://orcid.org/0000-0001-9447-460X")
|
||||
add_signposts(
|
||||
response,
|
||||
Signpost(LinkRel.type, "https://schema.org/Dataset"),
|
||||
Signpost(LinkRel.author, "https://orcid.org/0000-0001-9447-460X")
|
||||
Signpost(LinkRel.item, "https://example.com/download.zip", "application/zip")
|
||||
)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
Multiple links with the same link type can be added as lists and the content type of a link
|
||||
can be defined by using tuples:
|
||||
### Manual parsing of JSON-LD
|
||||
|
||||
If you have metadata in JSON-LD available, but it is not rendered as part of the response, you can still parse it and add the signposting links manually:
|
||||
|
||||
```python
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django_signposting.util import add_signposts
|
||||
from django_signposting.utils import add_signposts, jsonld_to_signposts
|
||||
|
||||
def my_view(request):
|
||||
response = HttpResponse("Hello, world!")
|
||||
|
||||
# Add signpostings as string
|
||||
add_signposts(response,
|
||||
type="https://schema.org/Dataset",
|
||||
author="https://orcid.org/0000-0001-9447-460X",
|
||||
item=[
|
||||
("https://example.com/image.png", "image/png"),
|
||||
("https://example.com/download.zip", "application/zip")
|
||||
])
|
||||
|
||||
return response
|
||||
response = HttpResponse("Hello World")
|
||||
json_ld = {
|
||||
"@context": "http://schema.org/",
|
||||
"@graph": [
|
||||
...
|
||||
]
|
||||
}
|
||||
signposts = jsonld_to_signposts(json_ld)
|
||||
add_signposts(response, **signposts)
|
||||
```
|
||||
|
||||
### 3. Signposts are formatted and added as Link headers by the middleware:
|
||||
### Add signposts as HTML links instead of headers
|
||||
|
||||
```bash
|
||||
curl -I https://example.com
|
||||
HTTP/2 200
|
||||
...
|
||||
link: <https://schema.org/Dataset> ; rel="type" ,
|
||||
<https://orcid.org/0000-0001-9447-460X> ; rel="author" ,
|
||||
<https://example.com/image.png> ; rel="item" ; type="application/json+ld"
|
||||
In cases where you want to include signposting directly in the HTML response rather than as HTTP headers
|
||||
(for example because headers are overwritten by a proxy or something else),
|
||||
you can use the `HtmlSignpostingMiddleware` instead of `SignpostingMiddleware`.
|
||||
|
||||
This middleware automatically adds <link> elements to the HTML <head> section based
|
||||
on the detected signposting metadata and produces output similar to the one shown here:
|
||||
|
||||
```HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
(...)
|
||||
<link href="http://schema.org/WebSite" rel="type"/>
|
||||
<link href="http://schema.org/Dataset" rel="type"/>
|
||||
<link href="https://orcid.org/0000-0001-9447-460X" rel="author"/>
|
||||
<link href="https://creativecommons.org/licenses/by/4.0/" rel="license"/>
|
||||
<link href="https://example.com" rel="cite-as"/>
|
||||
<link href="https://example.com/download.zip" rel="describedby" type="application/zip"/>
|
||||
<link href="https://example.com/metadata.json" rel="describedby"/>
|
||||
<link href="http://example.com/image.png" rel="item" type="image/png"/>
|
||||
<link href="http://example.com/image2.png" rel="item" type="image/png"/>
|
||||
</head>
|
||||
...
|
||||
</html>
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add support for link sets
|
||||
- [ ] Add support for specifying profile extension attribute
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the MIT License.
|
||||
Licensed under the MIT License.
|
||||
|
||||
## References
|
||||
|
||||
You can cite this library using this DOI: [10.5281/zenodo.15008517](https://doi.org/10.5281/zenodo.15008517).
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from typing import Callable
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from signposting import Signpost
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import json
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.conf import settings
|
||||
from .utils import jsonld_to_signposts
|
||||
|
||||
|
||||
class SignpostingMiddleware:
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -16,25 +22,85 @@ class SignpostingMiddleware:
|
||||
|
||||
if not hasattr(response, "_signposts"):
|
||||
return response
|
||||
|
||||
self._add_signposts(response, response._signposts)
|
||||
|
||||
return response
|
||||
|
||||
def _add_signposts(self, response: HttpResponse, typed_links: dict[str, list[str|tuple[str, str]]]):
|
||||
""" Adds signposting headers to the respones.
|
||||
def _add_signposts(self, response: HttpResponse, signposts: list[Signpost]):
|
||||
"""Adds signposting headers to the respones.
|
||||
params:
|
||||
response - the response object
|
||||
typed_links - a map of relation types to a list of corresponding links. Each link can be a link or a tuple of link and media type.
|
||||
signposts - a list of Signposts
|
||||
"""
|
||||
|
||||
link_snippets = []
|
||||
for relation_type in typed_links.keys():
|
||||
links = typed_links.get(relation_type, [])
|
||||
for link in links:
|
||||
if isinstance(link, tuple) and len(link) > 1:
|
||||
link_snippets.append(f'<{link[0]}> ; rel="{relation_type}" ; type="{link[1]}"')
|
||||
else:
|
||||
link_snippets.append(f'<{link}> ; rel="{relation_type}"')
|
||||
for signpost in signposts:
|
||||
link_snippets.append(f'<{signpost.target}> ; rel="{signpost.rel}"')
|
||||
if signpost.type:
|
||||
link_snippets[-1] += f' ; type="{signpost.type}"'
|
||||
|
||||
response["Link"] = " , ".join(link_snippets)
|
||||
|
||||
|
||||
class HtmlSignpostingMiddleware(SignpostingMiddleware):
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
response = self.get_response(request)
|
||||
|
||||
if not hasattr(response, "_signposts"):
|
||||
return response
|
||||
|
||||
# Adding Signposts via HTML is only supported for HTML responses
|
||||
if not response.headers["Content-Type"].startswith("text/html"):
|
||||
return response
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
|
||||
# Html should have a root element
|
||||
if not soup.html:
|
||||
raise Exception("Could not find HTML root element")
|
||||
|
||||
# Add head element if not present
|
||||
if not soup.head:
|
||||
head_tag = soup.new_tag("head")
|
||||
soup.html.insert(0, head_tag)
|
||||
|
||||
# BUild links for each signpost and add them to the html
|
||||
for signpost in response._signposts:
|
||||
link_tag = soup.new_tag("link")
|
||||
link_tag["rel"] = signpost.rel
|
||||
link_tag["href"] = signpost.target
|
||||
if signpost.type:
|
||||
link_tag["type"] = signpost.type
|
||||
soup.head.append(link_tag)
|
||||
|
||||
# Override the original content with the new HTML
|
||||
response.content = soup.prettify().encode("utf-8")
|
||||
response["Content-Length"] = len(response.content)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class JsonLdSignpostingParserMiddleware(MiddlewareMixin):
|
||||
def process_response(
|
||||
self, request: HttpRequest, response: HttpResponse
|
||||
) -> HttpResponse:
|
||||
if not getattr(settings, "SIGNPOSTING_PARSE_JSONLD", True):
|
||||
return response
|
||||
|
||||
if response.get("Content-Type", "").startswith("text/html"):
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
for script in soup.find_all("script", type="application/ld+json"):
|
||||
try:
|
||||
jsonld = json.loads(script.string)
|
||||
signposts = jsonld_to_signposts(jsonld)
|
||||
|
||||
response._signposts = signposts
|
||||
except json.JSONDecodeError as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
return response
|
||||
|
||||
150
django_signposting/sparql.py
Normal file
150
django_signposting/sparql.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from rdflib import Graph
|
||||
from rdflib.query import Result
|
||||
|
||||
|
||||
def root_element_query(g: Graph) -> Result:
|
||||
return g.query("""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT DISTINCT ?rootElement
|
||||
WHERE {
|
||||
# has a type
|
||||
?rootElement a ?type ;
|
||||
# has a license or an author or a creator
|
||||
(schema:license | schema:author | schema:creator ) ?value .
|
||||
|
||||
# No incoming edges for ?rootElement
|
||||
FILTER NOT EXISTS {
|
||||
?incoming ?p ?rootElement .
|
||||
|
||||
# also allow incoming edges that link entities BACK to the dataset
|
||||
FILTER(?p != schema:isPartOf)
|
||||
FILTER(?p != schema:mainEntityOfPage)
|
||||
FILTER(?p != schema:recordedIn)
|
||||
FILTER(?p != schema:exampleOfWork)
|
||||
FILTER(?p != schema:includedInDataCatalogue)
|
||||
FILTER(?p != schema:subjectOf)
|
||||
FILTER(?p != schema:dataset)
|
||||
|
||||
# allow incoming edges from ro-crate-metadata.json
|
||||
FILTER(CONTAINS(?incoming, "ro-crate-metadata.json")) .
|
||||
}
|
||||
}
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
|
||||
def type_query(g: Graph, rootElement: str) -> Graph:
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <?context>
|
||||
|
||||
SELECT DISTINCT ?type
|
||||
WHERE {
|
||||
?rootElement a ?type .
|
||||
}
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
|
||||
|
||||
def author_query(g: Graph, rootElement: str):
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT ?author_id ?identifier ?url
|
||||
WHERE {
|
||||
?rootElement schema:author ?author .
|
||||
|
||||
BIND(?author AS ?author_id) # Get the @id of the author
|
||||
OPTIONAL { ?author schema:identifier ?identifier }
|
||||
OPTIONAL { ?author schema:url ?url }
|
||||
}
|
||||
LIMIT 1
|
||||
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
|
||||
|
||||
def license_query(g: Graph, rootElement: str):
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT ?element_id ?identifier ?url
|
||||
WHERE {
|
||||
?rootElement schema:license ?element .
|
||||
|
||||
BIND(?element AS ?element_id) # Get the @id of the element
|
||||
OPTIONAL { ?element schema:identifier ?identifier }
|
||||
OPTIONAL { ?element schema:url ?url }
|
||||
}
|
||||
LIMIT 1
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
|
||||
|
||||
def cite_query(g: Graph, rootElement: str):
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT ?element_id ?identifier ?url
|
||||
WHERE {
|
||||
?rootElement schema:url ?element .
|
||||
|
||||
BIND(?element AS ?element_id) # Get the @id of the element
|
||||
OPTIONAL { ?element schema:identifier ?identifier }
|
||||
OPTIONAL { ?element schema:url ?url }
|
||||
}
|
||||
LIMIT 1
|
||||
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
|
||||
|
||||
def sameas_query(g: Graph, rootElement: str):
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT ?element_id ?contentUrl ?identifier ?url ?encoding
|
||||
WHERE {
|
||||
?rootElement schema:sameAs ?element .
|
||||
|
||||
BIND(?element AS ?element_id) # Get the @id of the element
|
||||
OPTIONAL { ?element schema:contentUrl ?contentUrl }
|
||||
OPTIONAL { ?element schema:identifier ?identifier }
|
||||
OPTIONAL { ?element schema:url ?url }
|
||||
OPTIONAL { ?element schema:encodingFormat ?encoding }
|
||||
}
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
|
||||
|
||||
def item_query(g: Graph, rootElement: str):
|
||||
return g.query(
|
||||
"""
|
||||
PREFIX schema: <http://schema.org/>
|
||||
|
||||
SELECT ?element_id ?identifier ?url ?contentUrl ?encoding
|
||||
WHERE {
|
||||
?rootElement schema:hasPart ?element .
|
||||
#VALUES ?element_type { schema:MediaObject schema:Dataset }
|
||||
#?element a ?element_type .
|
||||
|
||||
BIND(?element AS ?element_id) # Get the @id of the element
|
||||
OPTIONAL { ?element schema:contentUrl ?contentUrl }
|
||||
OPTIONAL { ?element schema:identifier ?identifier }
|
||||
OPTIONAL { ?element schema:url ?url }
|
||||
OPTIONAL { ?element schema:encodingFormat ?encoding }
|
||||
}
|
||||
|
||||
""",
|
||||
initBindings={"rootElement": rootElement},
|
||||
)
|
||||
@@ -1,23 +1,86 @@
|
||||
import json
|
||||
from django.http import HttpResponse
|
||||
from rdflib import Graph
|
||||
from signposting import Signpost, LinkRel
|
||||
import re
|
||||
|
||||
from django_signposting import sparql
|
||||
|
||||
|
||||
def add_signposts(response: HttpResponse, **kwargs):
|
||||
def add_signposts(response: HttpResponse, *args: Signpost):
|
||||
""" Adds signposting headers to the responses.
|
||||
params:
|
||||
response - the response object
|
||||
kwargs - a map of relation types to a list of corresponding links. Each link can be a link or a tuple of link and media type.
|
||||
args - a list of signposts to add to this resposnse.
|
||||
"""
|
||||
|
||||
if not hasattr(response, '_signposts'):
|
||||
response._signposts = {}
|
||||
response._signposts = []
|
||||
|
||||
for key in kwargs.keys():
|
||||
for signpost in args:
|
||||
if signpost not in response._signposts:
|
||||
response._signposts.append(signpost)
|
||||
|
||||
values = kwargs[key]
|
||||
if isinstance(values, str) or isinstance(values, tuple):
|
||||
values = [values]
|
||||
|
||||
if key not in response._signposts:
|
||||
response._signposts[key] = values
|
||||
else:
|
||||
response._signposts[key] += values
|
||||
def select_url(elements: tuple[str, ...]) -> str | None:
|
||||
def is_url(url: str) -> bool:
|
||||
url_pattern = re.compile(
|
||||
r"^(https?|ftp)://" # protocol
|
||||
r"(?:(?:[a-zA-Z0-9-_]+\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,6})" # domain
|
||||
r"(?::\d{1,5})?" # optional port
|
||||
r"(?:/.*)?$" # path
|
||||
)
|
||||
return bool(url_pattern.match(url))
|
||||
|
||||
for elem in elements[::-1]:
|
||||
if is_url(str(elem)):
|
||||
return str(elem)
|
||||
return None
|
||||
|
||||
|
||||
def jsonld_to_signposts(jsonld: dict) -> []:
|
||||
signposts = []
|
||||
# TODO use jsonld context in query as prefix
|
||||
g = Graph().parse(data=json.dumps(jsonld), format="json-ld")
|
||||
|
||||
rootElement = next(iter(sparql.root_element_query(g)), None)
|
||||
if rootElement:
|
||||
rootElement = rootElement[0]
|
||||
else:
|
||||
print("No root element found")
|
||||
return {}
|
||||
types = sparql.type_query(g, rootElement)
|
||||
for type in types:
|
||||
signposts.append(Signpost(LinkRel.type, str(type[0])))
|
||||
|
||||
authors = sparql.author_query(g, rootElement)
|
||||
for author in authors:
|
||||
author = select_url(author)
|
||||
if author:
|
||||
signposts.append(Signpost(LinkRel.author, author))
|
||||
|
||||
license = next(iter(sparql.license_query(g, rootElement)), [])
|
||||
license = select_url(license)
|
||||
if license:
|
||||
signposts.append(Signpost(LinkRel.license, license))
|
||||
|
||||
citations = sparql.cite_query(g, rootElement)
|
||||
for citation in citations:
|
||||
citation = select_url(citation)
|
||||
if citation:
|
||||
signposts.append(Signpost(LinkRel.cite_as, citation))
|
||||
|
||||
sameas = sparql.sameas_query(g, rootElement)
|
||||
for sa in sameas:
|
||||
sa_media_type = sa[-1]
|
||||
sa = select_url(sa[:-1])
|
||||
if sa:
|
||||
signposts.append(Signpost(LinkRel.describedby, sa, sa_media_type))
|
||||
|
||||
items = sparql.item_query(g, rootElement)
|
||||
for item in items:
|
||||
item_media_type = item[-1]
|
||||
item = select_url(item[:-1])
|
||||
if item:
|
||||
signposts.append(Signpost(LinkRel.item, item, item_media_type))
|
||||
return signposts
|
||||
|
||||
0
example/example/__init__.py
Normal file
0
example/example/__init__.py
Normal file
16
example/example/asgi.py
Normal file
16
example/example/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for example project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
129
example/example/settings.py
Normal file
129
example/example/settings.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Django settings for example project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.1.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-0_!deorp-j4ng0d$4#jrwcbw#@#)a5qf)_wgxq%or7ic4v3euc'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_json_ld',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django_signposting.middleware.SignpostingMiddleware',
|
||||
# 'django_signposting.middleware.HtmlSignpostingMiddleware',
|
||||
'django_signposting.middleware.JsonLdSignpostingParserMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'example.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
BASE_DIR / 'example/templates',
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'example.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
11
example/example/templates/jsonld.html
Normal file
11
example/example/templates/jsonld.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% load render_json_ld from json_ld %}
|
||||
{% render_json_ld sd %}
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, world!</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
example/example/urls.py
Normal file
25
example/example/urls.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
URL configuration for simple project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.SimpleView.as_view()),
|
||||
path("jsonld", views.JsonLdView.as_view()),
|
||||
path("ro-crate", views.rocrate.as_view()),
|
||||
]
|
||||
138
example/example/views.py
Normal file
138
example/example/views.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django_json_ld.views import JsonLdContextMixin
|
||||
from signposting import LinkRel, Signpost
|
||||
|
||||
from django_signposting.utils import add_signposts, jsonld_to_signposts
|
||||
|
||||
|
||||
class SimpleView(View):
|
||||
""" A simple view that adds signposting headers to the response."""
|
||||
|
||||
def get(self, request):
|
||||
response = HttpResponse("Hello, world!")
|
||||
|
||||
# Add signpostings as string
|
||||
add_signposts(
|
||||
response,
|
||||
Signpost(LinkRel.type, "http://schema.org/Dataset"),
|
||||
Signpost(LinkRel.author, "https://orcid.org/0000-0001-9447-460X"),
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class JsonLdView(JsonLdContextMixin, View):
|
||||
""" A view that automatically adds signposting headers to the response based on
|
||||
the JSON-LD metadata in the response."""
|
||||
|
||||
sd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": ["WebSite", "Dataset"],
|
||||
"name": "My Dataset",
|
||||
"description": "A dataset of things.",
|
||||
"url": "https://example.com",
|
||||
"sameAs": [
|
||||
{
|
||||
"@type": "MediaObject",
|
||||
"contentUrl": "https://example.com/download.zip",
|
||||
"encodingFormat": "application/zip",
|
||||
},
|
||||
{
|
||||
"@type": "MediaObject",
|
||||
"contentUrl": "https://example.com/metadata.json",
|
||||
},
|
||||
],
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Daniel Bauer",
|
||||
"url": "https://orcid.org/0000-0001-9447-460X",
|
||||
},
|
||||
"license": {
|
||||
"@type": "CreativeWork",
|
||||
"name": "CC BY 4.0",
|
||||
"url": "https://creativecommons.org/licenses/by/4.0/",
|
||||
},
|
||||
"hasPart": [
|
||||
{
|
||||
"@type": "ImageObject",
|
||||
"url": "http://example.com/image.png",
|
||||
"encodingFormat": "image/png",
|
||||
},
|
||||
{
|
||||
"@type": "ImageObject",
|
||||
"url": "http://example.com/image2.png",
|
||||
"encodingFormat": "image/png",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
return render(request, "jsonld.html", context={"sd": self.sd})
|
||||
|
||||
|
||||
class rocrate(JsonLdContextMixin, View):
|
||||
""" A view that adds signposting headers from the metadata of a detached RO-Crate."""
|
||||
|
||||
def get(self, request):
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
# Build an RO-Crate and serve its preview
|
||||
from rocrate.model.person import Person
|
||||
from rocrate.model.creativework import CreativeWork
|
||||
from rocrate.rocrate import ROCrate
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
crate = ROCrate(gen_preview=True)
|
||||
crate.add_file(
|
||||
"http://example.com/test.pdf",
|
||||
properties={"name": "test file", "encodingFormat": "application/pdf"},
|
||||
)
|
||||
author = crate.add(
|
||||
Person(
|
||||
crate,
|
||||
"https://orcid.org/0000-0001-9447-460X",
|
||||
properties={"name": "Daniel Bauer"},
|
||||
)
|
||||
)
|
||||
license = crate.add(
|
||||
CreativeWork(
|
||||
crate,
|
||||
"https://spdx.org/licenses/CC0-1.0",
|
||||
properties={
|
||||
"@id": "https://spdx.org/licenses/CC0-1.0",
|
||||
"@type": "CreativeWork",
|
||||
"name": "CC0-1.0",
|
||||
"description": "Creative Commons Zero v1.0 Universal",
|
||||
},
|
||||
)
|
||||
)
|
||||
sameAs = crate.add(
|
||||
CreativeWork(
|
||||
crate,
|
||||
"https://example.com/ro-crate-metadata.json",
|
||||
properties={"encodingFormat": "application/ld+json"},
|
||||
)
|
||||
)
|
||||
|
||||
crate.root_dataset["name"] = "My Dataset"
|
||||
crate.root_dataset["description"] = "A dataset of things."
|
||||
crate.root_dataset["author"] = author
|
||||
crate.root_dataset["creator"] = author
|
||||
crate.root_dataset["license"] = license
|
||||
crate.root_dataset["url"] = "https://example.com"
|
||||
crate.root_dataset["sameAs"] = sameAs
|
||||
crate.write(d)
|
||||
|
||||
# Extract JSON-LD from RO-Crate metadata and build signposts from it
|
||||
metadata = open(os.path.join(d, "ro-crate-metadata.json"), "r").read()
|
||||
metadata = json.loads(metadata)
|
||||
signposts = jsonld_to_signposts(metadata)
|
||||
|
||||
preview = open(os.path.join(d, "ro-crate-preview.html"), "r").read()
|
||||
response = HttpResponse(preview)
|
||||
add_signposts(response, *signposts)
|
||||
return response
|
||||
16
example/example/wsgi.py
Normal file
16
example/example/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for example project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
example/manage.py
Executable file
22
example/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
18
example/requirements.txt
Normal file
18
example/requirements.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
asgiref==3.8.1
|
||||
beautifulsoup4==4.12.3
|
||||
certifi==2024.8.30
|
||||
charset-normalizer==3.4.0
|
||||
django==5.1.7
|
||||
django-json-ld==0.0.5
|
||||
django-signposting==0.10.0
|
||||
httplink==0.2.0
|
||||
idna==3.10
|
||||
pyparsing==3.2.0
|
||||
rdflib==7.1.1
|
||||
requests==2.32.3
|
||||
rfc3987==1.3.8
|
||||
signposting==0.9.9
|
||||
soupsieve==2.6
|
||||
sqlparse==0.5.1
|
||||
urllib3==2.2.3
|
||||
rocrate==0.11.0
|
||||
9
hermes.toml
Normal file
9
hermes.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[harvest]
|
||||
sources = [ "cff",]
|
||||
|
||||
[deposit]
|
||||
target = "invenio_rdm"
|
||||
|
||||
[deposit.invenio_rdm]
|
||||
site_url = "https://zenodo.org/"
|
||||
access_right = "open"
|
||||
@@ -7,13 +7,17 @@ name = "django_signposting"
|
||||
authors = [
|
||||
{name = "Daniel Bauer", email = "github@dbauer.me"}
|
||||
]
|
||||
description = "Add FAIR signpostings to response headers"
|
||||
version = "0.0.2"
|
||||
description = "Add FAIR signpostings to response headers in Django"
|
||||
version = "0.11.0"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"Django>=3.0",
|
||||
"beautifulsoup4>=4.12.3",
|
||||
"django>=3.0",
|
||||
"signposting>=0.9.9",
|
||||
"rdflib>=7.1.1",
|
||||
]
|
||||
license = {file= "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/dnlbauer/django-signposting"
|
||||
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -1,7 +1,5 @@
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
print("test")
|
||||
settings.configure()
|
||||
|
||||
112
tests/test_html_signposting_middleware.py
Normal file
112
tests/test_html_signposting_middleware.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django_signposting.middleware import HtmlSignpostingMiddleware
|
||||
from signposting import LinkRel, Signpost
|
||||
import pytest
|
||||
|
||||
|
||||
def assert_links_exist(response: HttpResponse, signposts: list[Signpost] = None):
|
||||
if signposts is None:
|
||||
signposts = response._signposts
|
||||
|
||||
soup = BeautifulSoup(response.content.decode("utf-8"), "html.parser")
|
||||
for signpost in signposts:
|
||||
links = soup.find_all("link", href=signpost.target, rel=signpost.rel)
|
||||
assert len(links) == 1
|
||||
|
||||
assert len(soup.find_all("link")) == len(signposts)
|
||||
|
||||
|
||||
def test_middleware_no_signposting():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 200
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
|
||||
assert_links_exist(response, [])
|
||||
|
||||
|
||||
def test_middleware_no_html():
|
||||
response = JsonResponse({"hello": "world"})
|
||||
response.status_code = 200
|
||||
response._signpost = [Signpost(LinkRel.author, "http://example.com")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
middleware(None)
|
||||
assert_links_exist(response, [])
|
||||
|
||||
|
||||
def test_middleware_malformed_html():
|
||||
response = HttpResponse("Hello world")
|
||||
response.status_code = 200
|
||||
response.content_type = "text/html"
|
||||
response._signposts = [Signpost(LinkRel.author, "http://example.com")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
with pytest.raises(Exception):
|
||||
middleware(None)
|
||||
|
||||
|
||||
def test_middleware_signposting_without_head():
|
||||
response = HttpResponse("<html><body></body></html>")
|
||||
response.status_code = 200
|
||||
response._signposts = [Signpost(LinkRel.author, "http://example.com")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
middleware(None)
|
||||
assert_links_exist(response)
|
||||
|
||||
|
||||
def test_middleware_signposting():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 200
|
||||
response._signposts = [Signpost(LinkRel.author, "http://example.com")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
middleware(None)
|
||||
assert_links_exist(response)
|
||||
|
||||
|
||||
def test_middleware_multiple_signposts():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 200
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.author, "http://example.com"),
|
||||
Signpost(LinkRel.author, "http://example2.com"),
|
||||
Signpost(LinkRel.cite_as, "http://example3.com"),
|
||||
]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert_links_exist(response)
|
||||
|
||||
|
||||
def test_middleware_signpost_with_content_type():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 200
|
||||
response._signposts = [Signpost(LinkRel.item, "http://example.com", "text/json")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert_links_exist(response)
|
||||
|
||||
|
||||
def test_middleware_ignore_error_responses():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 400
|
||||
response._signposts = [Signpost(LinkRel.author, "http://example.com")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert_links_exist(response)
|
||||
|
||||
|
||||
def test_middleware_type_link():
|
||||
response = HttpResponse("<html><head></head><body></body></html>")
|
||||
response.status_code = 200
|
||||
response._signposts = [Signpost(LinkRel.type, "http://schema.org/Dataset")]
|
||||
|
||||
middleware = HtmlSignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert_links_exist(response)
|
||||
270
tests/test_jsonld_signposting_middleware.py
Normal file
270
tests/test_jsonld_signposting_middleware.py
Normal file
@@ -0,0 +1,270 @@
|
||||
import json
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from django_signposting.middleware import JsonLdSignpostingParserMiddleware
|
||||
from signposting import Signpost, LinkRel
|
||||
|
||||
|
||||
def jsonld_test_runner(jsonld, expected_signposts):
|
||||
response = HttpResponse(f"""
|
||||
<html><head>
|
||||
<script type="application/ld+json">{json.dumps(jsonld)}</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
""")
|
||||
response.status_code = 200
|
||||
|
||||
middleware = JsonLdSignpostingParserMiddleware(lambda request: response)
|
||||
response = middleware(HttpRequest())
|
||||
|
||||
for expected_signpost in expected_signposts:
|
||||
assert expected_signpost in response._signposts
|
||||
|
||||
assert len(expected_signposts) == len(response._signposts)
|
||||
|
||||
|
||||
def test_jsonld_signposting_basic():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "WebPage",
|
||||
"author": {"url": "http://example.com/author"},
|
||||
"url": "http://example.com/url",
|
||||
"license": {"identifier": "http://example.com/license"},
|
||||
"hasPart": [
|
||||
{
|
||||
"url": "http://example.com/image1.jpg",
|
||||
},
|
||||
{
|
||||
"url": "http://example.com/image2.jpg",
|
||||
},
|
||||
],
|
||||
"sameAs": [
|
||||
{
|
||||
"url": "http://example.com/metadata.json",
|
||||
},
|
||||
{
|
||||
"url": "http://example.com/metadata.xml",
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/WebPage"),
|
||||
Signpost(LinkRel.author, "http://example.com/author"),
|
||||
Signpost(LinkRel.cite_as, "http://example.com/url"),
|
||||
Signpost(LinkRel.license, "http://example.com/license"),
|
||||
Signpost(LinkRel.item, "http://example.com/image1.jpg"),
|
||||
Signpost(LinkRel.item, "http://example.com/image2.jpg"),
|
||||
Signpost(LinkRel.describedby, "http://example.com/metadata.json"),
|
||||
Signpost(LinkRel.describedby, "http://example.com/metadata.xml"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_empty_signposting():
|
||||
jsonld_test_runner({}, [])
|
||||
|
||||
|
||||
def test_jsonld_signposting_media_types():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "WebPage",
|
||||
"author": {"url": "http://example.com/author"},
|
||||
"url": "http://example.com/url",
|
||||
"license": {"identifier": "http://example.com/license"},
|
||||
"hasPart": [
|
||||
{
|
||||
"url": "http://example.com/image1.jpg",
|
||||
"encodingFormat": "image/jpeg",
|
||||
},
|
||||
{"url": "http://example.com/image2.jpg", "encodingFormat": "image/png"},
|
||||
],
|
||||
"sameAs": [
|
||||
{
|
||||
"url": "http://example.com/metadata.json",
|
||||
"encodingFormat": "application/json",
|
||||
},
|
||||
{
|
||||
"url": "http://example.com/metadata.xml",
|
||||
"encodingFormat": "application/xml",
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/WebPage"),
|
||||
Signpost(LinkRel.author, "http://example.com/author"),
|
||||
Signpost(LinkRel.cite_as, "http://example.com/url"),
|
||||
Signpost(LinkRel.license, "http://example.com/license"),
|
||||
Signpost(LinkRel.item, "http://example.com/image1.jpg", "image/jpeg"),
|
||||
Signpost(LinkRel.item, "http://example.com/image2.jpg", "image/png"),
|
||||
Signpost(
|
||||
LinkRel.describedby,
|
||||
"http://example.com/metadata.json",
|
||||
"application/json",
|
||||
),
|
||||
Signpost(
|
||||
LinkRel.describedby,
|
||||
"http://example.com/metadata.xml",
|
||||
"application/xml",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_signposting_property_precedence():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "WebPage",
|
||||
"author": {
|
||||
"identifier": "http://example.com/author-ident",
|
||||
"url": "http://example.com/author-url",
|
||||
},
|
||||
"hasPart": [
|
||||
{
|
||||
"identifier": "http://example.com/image1-identifier.jpg",
|
||||
"url": "http://example.com/image1-url.jpg",
|
||||
"contentUrl": "http://example.com/image1-content.jpg",
|
||||
},
|
||||
{
|
||||
"identifier": "http://example.com/image2-identifier.jpg",
|
||||
"url": "http://example.com/image2-url.jpg",
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/WebPage"),
|
||||
Signpost(LinkRel.author, "http://example.com/author-url"),
|
||||
Signpost(LinkRel.item, "http://example.com/image1-content.jpg"),
|
||||
Signpost(LinkRel.item, "http://example.com/image2-url.jpg"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_signposting_ids():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebPage",
|
||||
"author": {"@id": "http://example.com/author"},
|
||||
"url": "http://example.com/url",
|
||||
"license": {"@id": "http://example.com/license"},
|
||||
"hasPart": [
|
||||
{
|
||||
"@id": "http://example.com/image1.jpg",
|
||||
},
|
||||
],
|
||||
"sameAs": [
|
||||
{
|
||||
"@id": "http://example.com/metadata.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
{"@id": "http://example.com/author", "@type": "Person"},
|
||||
{"@id": "http://example.com/license"},
|
||||
{
|
||||
"@id": "http://example.com/image1.jpg",
|
||||
"encodingFormat": "image/jpeg",
|
||||
},
|
||||
{"@id": "http://example.com/metadata.json"},
|
||||
],
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/WebPage"),
|
||||
Signpost(LinkRel.author, "http://example.com/author"),
|
||||
Signpost(LinkRel.cite_as, "http://example.com/url"),
|
||||
Signpost(LinkRel.license, "http://example.com/license"),
|
||||
Signpost(LinkRel.item, "http://example.com/image1.jpg", "image/jpeg"),
|
||||
Signpost(LinkRel.describedby, "http://example.com/metadata.json"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_signposting_ids_with_url():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebPage",
|
||||
"author": {"@id": "http://example.com/author"},
|
||||
},
|
||||
{
|
||||
"@id": "http://example.com/author",
|
||||
"url": "http://example.com/realAuthorUrl",
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/WebPage"),
|
||||
Signpost(LinkRel.author, "http://example.com/realAuthorUrl"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_signposting_multiple_types():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Dataset",
|
||||
"author": "http://example.com/author",
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/Dataset"),
|
||||
Signpost(LinkRel.author, "http://example.com/author"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_jsonld_signposting_rocrate():
|
||||
jsonld_test_runner(
|
||||
{
|
||||
"@context": "http://w3id.org/ro/crate/1.1/context",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "CreativeWork",
|
||||
"@id": "ro-crate-metadata.json",
|
||||
"conformsTo": {"@id": "http://w3id.org/ro/crate/1.1"},
|
||||
"about": {"@id": "http://example.com/myCrate"}
|
||||
},
|
||||
{
|
||||
"@type": "Dataset",
|
||||
"@id": "http://example.com/myCrate",
|
||||
"author": {"@id": "http://example.com/author"},
|
||||
"datePublished": "2024",
|
||||
"name": "Test crate",
|
||||
"description": "This is a detached test create",
|
||||
"license": {"@id": "http://example.com/license"},
|
||||
"hasPart": [
|
||||
{"@id": "http://example.com/file"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@id": "http://example.com/author",
|
||||
"@type": "Person",
|
||||
"name": "Daniel Bauer"
|
||||
},
|
||||
{
|
||||
"@id": "http://example.com/license",
|
||||
"@type": "CreativeWork",
|
||||
"name": "Test License"
|
||||
},
|
||||
{
|
||||
"@id": "http://example.com/file",
|
||||
"@type": "File",
|
||||
"contentUrl": "http://example.com/image1.jpg",
|
||||
"encodingFormat": "image/jpeg"
|
||||
}
|
||||
]
|
||||
},
|
||||
[
|
||||
Signpost(LinkRel.type, "http://schema.org/Dataset"),
|
||||
Signpost(LinkRel.author, "http://example.com/author"),
|
||||
Signpost(LinkRel.license, "http://example.com/license"),
|
||||
Signpost(LinkRel.item, "http://example.com/image1.jpg", "image/jpeg"),
|
||||
],
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.http import HttpResponse
|
||||
from django_signposting.middleware import SignpostingMiddleware
|
||||
from signposting import LinkRel, Signpost
|
||||
|
||||
|
||||
def test_middleware_no_signposting():
|
||||
@@ -14,7 +15,9 @@ def test_middleware_no_signposting():
|
||||
def test_middleware_signposting():
|
||||
response = HttpResponse()
|
||||
response.status_code = 200
|
||||
response._signposts = {"author": ["http://example.com"]}
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.author, "http://example.com")
|
||||
]
|
||||
|
||||
middleware = SignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
@@ -24,15 +27,11 @@ def test_middleware_signposting():
|
||||
def test_middleware_multiple_signposts():
|
||||
response = HttpResponse()
|
||||
response.status_code = 200
|
||||
response._signposts = {
|
||||
"author": [
|
||||
"http://example.com",
|
||||
"http://example2.com"
|
||||
],
|
||||
"cite-as": [
|
||||
"http://example3.com"
|
||||
]
|
||||
}
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.author, "http://example.com"),
|
||||
Signpost(LinkRel.author, "http://example2.com"),
|
||||
Signpost(LinkRel.cite_as, "http://example3.com"),
|
||||
]
|
||||
|
||||
middleware = SignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
@@ -45,24 +44,34 @@ def test_middleware_multiple_signposts():
|
||||
def test_middleware_signpost_with_content_type():
|
||||
response = HttpResponse()
|
||||
response.status_code = 200
|
||||
response._signposts = {
|
||||
"item": [
|
||||
("http://example.com", "test/json"),
|
||||
]
|
||||
}
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.item, "http://example.com", "text/json")
|
||||
]
|
||||
|
||||
middleware = SignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert response.headers["Link"] == '<http://example.com> ; rel="item" ; type="test/json"'
|
||||
assert response.headers["Link"] == '<http://example.com> ; rel="item" ; type="text/json"'
|
||||
|
||||
|
||||
def test_middleware_ignore_error_responses():
|
||||
response = HttpResponse()
|
||||
response.status_code = 400
|
||||
response._signposts = {
|
||||
"author": ["https://example.com"]
|
||||
}
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.author, "http://example.com")
|
||||
]
|
||||
|
||||
middleware = SignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert "Link" not in response.headers
|
||||
|
||||
|
||||
def test_middleware_type_link():
|
||||
response = HttpResponse()
|
||||
response.status_code = 200
|
||||
response._signposts = [
|
||||
Signpost(LinkRel.type, "http://schema.org/Dataset")
|
||||
]
|
||||
|
||||
middleware = SignpostingMiddleware(lambda request: response)
|
||||
response = middleware(None)
|
||||
assert response.headers["Link"] == '<http://schema.org/Dataset> ; rel="type"'
|
||||
@@ -1,47 +1,37 @@
|
||||
from django.http import HttpResponse
|
||||
from django_signposting.utils import add_signposts
|
||||
from signposting import Signpost, LinkRel
|
||||
|
||||
|
||||
def test_add_signpost():
|
||||
response = HttpResponse()
|
||||
add_signposts(response, item="http://example.com")
|
||||
add_signposts(response, Signpost(LinkRel.item, "http://example.com"))
|
||||
|
||||
assert response._signposts["item"] == ["http://example.com"]
|
||||
assert len(response._signposts) == 1
|
||||
|
||||
|
||||
def test_add_multiple_signposts():
|
||||
response = HttpResponse()
|
||||
add_signposts(response,
|
||||
item="http://example.com",
|
||||
author=["https://example2.com", "https://example3.com"]
|
||||
Signpost(LinkRel.item, "http://example.com"),
|
||||
Signpost(LinkRel.author, "https://example2.com"),
|
||||
Signpost(LinkRel.author, "https://example3.com"),
|
||||
)
|
||||
|
||||
assert response._signposts == {
|
||||
"item": ["http://example.com"],
|
||||
"author": [
|
||||
"https://example2.com",
|
||||
"https://example3.com",
|
||||
]
|
||||
}
|
||||
assert len(response._signposts) == 3
|
||||
|
||||
|
||||
def test_add_signposts_with_content_type():
|
||||
def test_add_signpost_call_multiple_times():
|
||||
response = HttpResponse()
|
||||
add_signposts(response,
|
||||
item=("http://example.com", "text/json"),
|
||||
author=["https://example2.com", "https://example3.com"]
|
||||
)
|
||||
add_signposts(response, Signpost(LinkRel.item, "http://example.com"))
|
||||
add_signposts(response, Signpost(LinkRel.item, "http://example2.com"))
|
||||
|
||||
assert response._signposts == {
|
||||
"item": [("http://example.com", "text/json")],
|
||||
"author": [
|
||||
"https://example2.com",
|
||||
"https://example3.com",
|
||||
]
|
||||
}
|
||||
assert len(response._signposts) == 2
|
||||
|
||||
|
||||
def test_add_signposts_from_dict():
|
||||
def test_add_signpost_duplicate():
|
||||
response = HttpResponse()
|
||||
add_signposts(response, **{"cite-as": ["https://example.com"]})
|
||||
assert response._signposts["cite-as"] == ["https://example.com"]
|
||||
add_signposts(response, Signpost(LinkRel.item, "http://example.com"))
|
||||
add_signposts(response, Signpost(LinkRel.item, "http://example.com"))
|
||||
|
||||
assert len(response._signposts) == 1
|
||||
|
||||
346
uv.lock
generated
Normal file
346
uv.lock
generated
Normal file
@@ -0,0 +1,346 @@
|
||||
version = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/85/ba2c2b83ba8b95354f99ed8344405d9571109ce0175028876209d6b93fba/Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a", size = 10698518 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f6/88ed57e1b3ed54ff18c1da352aecbd6f51784c3e642d97586b61f050f5b1/Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818", size = 8276180 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-signposting"
|
||||
version = "0.10.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "django" },
|
||||
{ name = "rdflib" },
|
||||
{ name = "signposting" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "beautifulsoup4", specifier = ">=4.12.3" },
|
||||
{ name = "django", specifier = ">=3.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "rdflib", specifier = ">=7.1.1" },
|
||||
{ name = "signposting", specifier = ">=0.9.9" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httplink"
|
||||
version = "0.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3f/d43dbecbec06fe5654b85b2f5f562c877586dfcde587dd4954d9eeb07f76/httplink-0.2.0-py3-none-any.whl", hash = "sha256:f4688080f92306dbb3faa71ea8232e1e8cb4a4a7f543bc9663b05dca8a2820c1", size = 5290 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isodate"
|
||||
version = "0.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdflib"
|
||||
version = "7.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "isodate", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pyparsing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/b6/4cee25689c7a3dfafeb481d358e74696fe3a8543da4cb2c1b71c0cb6583d/rdflib-7.1.1.tar.gz", hash = "sha256:164de86bd3564558802ca983d84f6616a4a1a420c7a17a8152f5016076b2913e", size = 4864216 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/7d/9add7b22a4e695c4f3011ed084ecf725e1f03954b9fa071f1766890b4989/rdflib-7.1.1-py3-none-any.whl", hash = "sha256:e590fa9a2c34ba33a667818b5a84be3fb8a4d85868f8038f17912ec84f912a25", size = 562443 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc3987"
|
||||
version = "1.3.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/bb/f1395c4b62f251a1cb503ff884500ebd248eed593f41b469f89caa3547bd/rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733", size = 20700 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d4/f7407c3d15d5ac779c3dd34fbbc6ea2090f77bd7dd12f207ccf881551208/rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53", size = 13377 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signposting"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "httplink" },
|
||||
{ name = "requests" },
|
||||
{ name = "rfc3987" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/68/3697204ad23a2654793a75a8d1fb386be9f2a8c1219771716742b46801c1/signposting-0.9.9.tar.gz", hash = "sha256:193b5f912290e7763782e7e8d1e423edb9cf6cca80f2864f45a4a3c67f8c5b87", size = 39913 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/bc/abc7f282b7cf809bb3f9cc43e599c02c2a645682cdd9fcabc72cab82ace1/signposting-0.9.9-py3-none-any.whl", hash = "sha256:8e9575ddbac52a0acbec9aeaf83edeadb3a4ff512f4563ec8903f7616a38eaaa", size = 34992 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2024.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
|
||||
]
|
||||
Reference in New Issue
Block a user