From 6ba724cb2cb9f6791217c0935438cfd7740a56f6 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 11 Nov 2024 11:55:42 +0100 Subject: [PATCH 1/6] Modernize readthedocs build --- .readthedocs.yaml | 14 ++++++++------ dev-docs/requirements.txt | 9 +++------ dev-docs/source/conf.py | 15 +-------------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7d914618b..6230b69e3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,13 +6,15 @@ sphinx: formats: [] build: - image: testing # For Python 3.9 -# Not needed yet because of conf.py hack: -# apt_packages: -# - libgdal20 -# - libproj13 + os: ubuntu-24.04 + apt_packages: + # To allow running autodoc + - libgdal20 + - libproj13 + tools: + python: "3.11" python: - version: 3.9 install: + - requirements: src/requirements.txt - requirements: dev-docs/requirements.txt diff --git a/dev-docs/requirements.txt b/dev-docs/requirements.txt index 5876349e0..ab8709f03 100644 --- a/dev-docs/requirements.txt +++ b/dev-docs/requirements.txt @@ -1,6 +1,3 @@ -Sphinx >= 5.3.0 -sphinx-rtd-theme >= 1.1.1 -sphinxcontrib-django == 0.5.1 - -# And the rest of the project to handle autodoc --r ../src/requirements.txt +Sphinx >= 8.1.3 +sphinx-rtd-theme >= 3.0.1 +sphinxcontrib-django == 2.5 diff --git a/dev-docs/source/conf.py b/dev-docs/source/conf.py index 902e79b50..3c11f50e6 100644 --- a/dev-docs/source/conf.py +++ b/dev-docs/source/conf.py @@ -15,26 +15,12 @@ from datetime import date import django -from sphinx.ext.autodoc.mock import _MockModule sys.path.insert(0, os.path.abspath("../../src")) os.environ["DJANGO_DEBUG"] = "false" os.environ["DJANGO_SETTINGS_MODULE"] = "dso_api.settings" os.environ["SCHEMA_URL"] = "https://schemas.data.amsterdam.nl/" -# At readthedocs, GDAL is not part of the build container. -# Feature request here: https://github.com/readthedocs/readthedocs.org/issues/8160 -# The workaround to use 'autodoc_mock_imports' doesn't work either, and is applied too late. -# Instead, the internal machinery of 'autodoc_mock_imports' is reused here to avoid GDAL imports. - - -class GDALMockModule(_MockModule): - GDAL_VERSION = (3, 0) - - -sys.modules["django.contrib.gis.geos.libgeos"] = _MockModule("django.contrib.gis.geos.libgeos") -sys.modules["django.contrib.gis.gdal.libgdal"] = GDALMockModule("django.contrib.gis.gdal.libgdal") - django.setup() @@ -95,6 +81,7 @@ class GDALMockModule(_MockModule): # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/") intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), From c714cd94846e10a7670eaabbed109b9e7c6856cc Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 11 Nov 2024 11:56:34 +0100 Subject: [PATCH 2/6] docs: bring documentation up-to-date * usage of database roles * why `ChunkedQuerySetIterator` is used despite similar Django 4.1 feature * new filterAuth authorization and improve bits * remove airflow references --- dev-docs/source/auth.rst | 28 +++++++++++++++++++---- dev-docs/source/database.rst | 38 +++++++++++++++++++++++++++++++ dev-docs/source/features.rst | 2 +- dev-docs/source/howto/install.rst | 15 ++++++------ dev-docs/source/index.rst | 5 ++-- dev-docs/source/streaming.rst | 18 ++++----------- 6 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 dev-docs/source/database.rst diff --git a/dev-docs/source/auth.rst b/dev-docs/source/auth.rst index b20bc8598..6db9d74ee 100644 --- a/dev-docs/source/auth.rst +++ b/dev-docs/source/auth.rst @@ -31,7 +31,7 @@ The schema definitions can add an ``auth`` field on various levels: The absence of an ``auth`` field makes a resource publicly available. At every level, the ``auth`` field contains a list of *scopes*. -The JWT token of the request must contain one of these scopes to access the resource. +The JWT token of the request must contain *at least one* of these scopes to access the resource. When there is a scope at both the dataset, table and field level these should *all* be satisfied to have access to the field. @@ -44,6 +44,24 @@ the field is omitted from the response. Sometimes it's not possible to remove a field (for example, a geometry field for Mapbox Vector Tiles). In that case, the endpoints produces a HTTP 403 error to completely deny access. +.. note:: + The schema validation also require an ``authReason`` to be present when ``auth`` is used. + Government data is expected to be public, unless there is a valid reason for it. + The ``authReason`` field forces schema authors to consider why data has to be restricted. + +Restricting Querying +~~~~~~~~~~~~~~~~~~~~ + +Besides the ``auth`` field, the ``filterAuth`` attribute allows restricting queries for a field. +This feature can be utilized to make it *harder* to query for a particular field. +Let's say, avoid retrieving all properties owned by a real estate owner. + +.. warning:: + + If someone manages to dump the whole table, they can off course still query everything within their local copy. + Hence, it is generally better to restrict access to a field entirely using ``auth`` instead. + The ``filterAuth`` feature is useful for well-monitored internal data, that is already protected using the ``auth`` field. + Profiles ~~~~~~~~ @@ -137,9 +155,10 @@ See the :doc:`wfs` documentation for more details. When changing the authorization logic, make sure to test the WFS server endpoint too. While most logic is shared, it's important to double-check no additional data is exposed. +.. _create-test-tokens: -Testing -------- +Creating Test Tokens +-------------------- When testing datasets with authorization from the command line you can use the `maketoken` management command, which generates @@ -150,7 +169,8 @@ This requires DSO-API to be installed in the current virtualenv After setting the latter and getting a token with :: - export PUB_JWKS="$(cat jwks_test.json)" # in src/ + cd src/ + export PUB_JWKS="$(cat jwks_test.json)" token=$(python manage.py maketoken BRK/RO BRK/RS BRK/RSN) you can issue a curl command such as diff --git a/dev-docs/source/database.rst b/dev-docs/source/database.rst new file mode 100644 index 000000000..3312804f9 --- /dev/null +++ b/dev-docs/source/database.rst @@ -0,0 +1,38 @@ +Database Notes +============== + +Database Roles +-------------- + +The end-user context is provided to the database. This helps: + +* Restrict table/field access on a database level using PostgreSQL roles. +* The database logs show who performed a query (by setting application name). +* The application can't accidentally query sensitive data (by switching roles). + +For every internal user, there should be a :samp:`{username}_role` present in the database. +This is created by our internal *dp-infra* repository. + +When such user is not present, all ``@amsterdam.nl`` addresses will fallback to an +internal ``medewerker_role``. The other accounts fallback to a ``anonymous_role``. + +Switching Roles +~~~~~~~~~~~~~~~ + +The application user has been granted a role that includes *all* user roles +with ``NOINHERIT``. This way, the application-user can perform a ``SET ROLE`` command, +to switch the user role based on the session. There is a separate role for anonymous access. + +The application-user is configured to switch to a role that +has sufficient permission to read metadata about datasets after ``LOGIN`` + +Note that when switching to a role, another ``SET ROLE`` command is still possible +because the current user doesn't change; only the current role does. + +Installing Roles +~~~~~~~~~~~~~~~~ + +The ``schema permission apply`` command will parse all Amsterdam Schema files, +and install the database roles and grants for all permission types. + +Each user-role becomes a member of these groups, based on their membership in Active Directory. diff --git a/dev-docs/source/features.rst b/dev-docs/source/features.rst index f0142622e..240eb1d78 100644 --- a/dev-docs/source/features.rst +++ b/dev-docs/source/features.rst @@ -30,4 +30,4 @@ The following features are implemented in the application: * Browsable API (default for browsers). * Azure BLOB fields for large documents. * Internal schema reload endpoint (though unused). -* Database routing to read tables for other databases (``DATABASE_SCHEMAS`` setting). +* :doc:`Database roles to have per-user permissions `. diff --git a/dev-docs/source/howto/install.rst b/dev-docs/source/howto/install.rst index f0530f916..1d05dcb23 100644 --- a/dev-docs/source/howto/install.rst +++ b/dev-docs/source/howto/install.rst @@ -30,14 +30,7 @@ Database Setup -------------- DSO-API talks to a PostgreSQL instance that contains its data. -Normally, you should use the one from the dataservices-airflow project. -If you don't already have it running, do:: - - git clone https://github.com/Amsterdam/dataservices-airflow - cd dataservices-airflow - docker-compose up dso_database - -Then point to that PostgreSQL in the environment:: +The database endpoint can be configured in the environment:: DATABASE_URL=postgres://dataservices:insecure@localhost:5416/dataservices @@ -49,6 +42,7 @@ Then point to that PostgreSQL in the environment:: Add a ``.envrc`` to your project folder, and direnv ensures the proper environment variables are loaded when you cd into your project directory. + Using a Virtual Machine ~~~~~~~~~~~~~~~~~~~~~~~ @@ -119,6 +113,11 @@ Then start DSO-API: The API can now be accessed at: http://localhost:8000. +.. tip:: + + If you need a token to access a view, + it can be :ref:`generated ` using the ``manage.py maketoken`` command. + API key middleware ------------------ diff --git a/dev-docs/source/index.rst b/dev-docs/source/index.rst index 551d2b7ac..0a5c29d2c 100644 --- a/dev-docs/source/index.rst +++ b/dev-docs/source/index.rst @@ -44,7 +44,7 @@ A simplified diagram of the main project dependencies: schematools_contrib_django [label="schematools.contrib.django"] ams [label="Amsterdam Schema", shape=note] - airflow [label="Airflow", shape=cylinder] + databricks [label="Databricks", shape=cylinder] schematools_contrib_django -> dso_api django -> drf @@ -54,7 +54,7 @@ A simplified diagram of the main project dependencies: django -> schematools_contrib_django schematools -> schematools_contrib_django ams -> dso_api [style=dotted, label="import"] - airflow -> dso_api [style=dotted, label="data"] + databricks -> dso_api [style=dotted, label="data"] } .. toctree:: @@ -66,6 +66,7 @@ A simplified diagram of the main project dependencies: features dynamic_models dynamic_api + database remote temporal streaming diff --git a/dev-docs/source/streaming.rst b/dev-docs/source/streaming.rst index dc9b62562..0a737b190 100644 --- a/dev-docs/source/streaming.rst +++ b/dev-docs/source/streaming.rst @@ -172,16 +172,8 @@ on the embedded section to avoid many repeated queries. Prefetching Optimization ~~~~~~~~~~~~~~~~~~~~~~~~ -One problem with ``QuerySet.iterator()`` is that it's incompatible with ``QuerySet.prefetch_related()``. -This happens because ``prefetch_related()`` reads over the internal results to collect all -identifiers that need to be "prefetched" with a single query. - -To have the best of both words, the ``ChunkedQuerySetIterator`` avoids this problem by reading -the table in chunks of 1000 records. For every batch, records are prefetched and given to -the next generator. It also tracks the most recently retrieved prefetches so the next batch -likely doesn't need an extra prefetch. But even when it does, -this is still better then no having prefetching at all. - -Also note that internally, Django's ``QuerySet.iterator()`` may still request 1000 records from the -database cursor at once. Hence, the ``ChunkedQuerySetIterator`` also follows this pattern -to request the exact same amount of records. +Before Django 4.1, using ``QuerySet.iterator()`` was incompatible with ``QuerySet.prefetch_related()``. +This was fixed by letting Django fetch the results in chunks and perform ``prefetch_related()`` on each chunk to retrieve related objects. + +However, this optimization is avoided here as our ``ChunkedQuerySetIterator`` has more optimizations. +It also tracks the most recently retrieved prefetches so the next batch likely doesn't need an extra prefetch. From 6c68e62f0fd45f933a0a59a96603e001172a9b91 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 11 Nov 2024 15:16:49 +0100 Subject: [PATCH 3/6] docs: bring autodoc modules up to date --- ...on.rst => dso_api.dynamic_api.filters.rst} | 4 +- .../api/dso_api.dynamic_api.filterset.rst | 7 ---- dev-docs/source/api/dso_api.dynamic_api.rst | 2 +- .../source/api/dso_api.dynamic_api.views.rst | 38 +++++++++++++++++++ .../api/rest_framework_dso.embedding.rst | 7 ++++ .../source/api/rest_framework_dso.filters.rst | 16 -------- .../api/rest_framework_dso.iterators.rst | 7 ++++ dev-docs/source/api/rest_framework_dso.rst | 3 +- .../source/api/schematools.permissions.rst | 6 +++ dev-docs/source/api/schematools.rst | 2 +- src/dso_api/dynamic_api/filters/__init__.py | 3 +- src/dso_api/dynamic_api/filters/openapi.py | 5 +-- src/dso_api/dynamic_api/filters/parser.py | 17 +++++---- src/dso_api/dynamic_api/views/doc.py | 14 ++++++- src/dso_api/dynamic_api/views/mvt.py | 6 ++- 15 files changed, 94 insertions(+), 43 deletions(-) rename dev-docs/source/api/{schematools.datasetcollection.rst => dso_api.dynamic_api.filters.rst} (52%) delete mode 100644 dev-docs/source/api/dso_api.dynamic_api.filterset.rst create mode 100644 dev-docs/source/api/rest_framework_dso.embedding.rst delete mode 100644 dev-docs/source/api/rest_framework_dso.filters.rst create mode 100644 dev-docs/source/api/rest_framework_dso.iterators.rst create mode 100644 dev-docs/source/api/schematools.permissions.rst diff --git a/dev-docs/source/api/schematools.datasetcollection.rst b/dev-docs/source/api/dso_api.dynamic_api.filters.rst similarity index 52% rename from dev-docs/source/api/schematools.datasetcollection.rst rename to dev-docs/source/api/dso_api.dynamic_api.filters.rst index 0826bd716..dafa1030d 100644 --- a/dev-docs/source/api/schematools.datasetcollection.rst +++ b/dev-docs/source/api/dso_api.dynamic_api.filters.rst @@ -1,7 +1,7 @@ -schematools.datasetcollection module +dso\_api.dynamic\_api.filters module ==================================== -.. automodule:: schematools.datasetcollection +.. automodule:: dso_api.dynamic_api.filters :members: :undoc-members: :show-inheritance: diff --git a/dev-docs/source/api/dso_api.dynamic_api.filterset.rst b/dev-docs/source/api/dso_api.dynamic_api.filterset.rst deleted file mode 100644 index 70bf5ab6f..000000000 --- a/dev-docs/source/api/dso_api.dynamic_api.filterset.rst +++ /dev/null @@ -1,7 +0,0 @@ -dso\_api.dynamic\_api.filterset module -====================================== - -.. automodule:: dso_api.dynamic_api.filterset - :members: - :undoc-members: - :show-inheritance: diff --git a/dev-docs/source/api/dso_api.dynamic_api.rst b/dev-docs/source/api/dso_api.dynamic_api.rst index 630453e61..d86c8d8c8 100644 --- a/dev-docs/source/api/dso_api.dynamic_api.rst +++ b/dev-docs/source/api/dso_api.dynamic_api.rst @@ -9,7 +9,7 @@ dso\_api.dynamic\_api package .. toctree:: :maxdepth: 1 - dso_api.dynamic_api.filterset + dso_api.dynamic_api.filters dso_api.dynamic_api.openapi dso_api.dynamic_api.permissions dso_api.dynamic_api.remote diff --git a/dev-docs/source/api/dso_api.dynamic_api.views.rst b/dev-docs/source/api/dso_api.dynamic_api.views.rst index 78d96513c..a749c627b 100644 --- a/dev-docs/source/api/dso_api.dynamic_api.views.rst +++ b/dev-docs/source/api/dso_api.dynamic_api.views.rst @@ -3,6 +3,10 @@ dso\_api.dynamic\_api.views package .. automodule:: dso_api.dynamic_api.views +.. autoclass:: dso_api.dynamic_api.views.APIIndexView + :show-inheritance: + + REST API -------- @@ -32,3 +36,37 @@ WFS API .. autoclass:: dso_api.dynamic_api.views.wfs.AuthenticatedFeatureType :show-inheritance: :members: + + +MVT API +------- + +.. automodule:: dso_api.dynamic_api.views.mvt + +.. autoclass:: dso_api.dynamic_api.views.DatasetMVTIndexView + :show-inheritance: + +.. autoclass:: dso_api.dynamic_api.views.DatasetMVTSingleView + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: dso_api.dynamic_api.views.DatasetMVTView + :show-inheritance: + :members: + + +Documentation +------------- + +.. automodule:: dso_api.dynamic_api.views.doc + +.. autoclass:: dso_api.dynamic_api.views.DocsOverview + :show-inheritance: + :members: + +.. autoclass:: dso_api.dynamic_api.views.DatasetDocView + :show-inheritance: + +.. autoclass:: dso_api.dynamic_api.views.DatasetWFSDocView + :show-inheritance: diff --git a/dev-docs/source/api/rest_framework_dso.embedding.rst b/dev-docs/source/api/rest_framework_dso.embedding.rst new file mode 100644 index 000000000..56e4f43d7 --- /dev/null +++ b/dev-docs/source/api/rest_framework_dso.embedding.rst @@ -0,0 +1,7 @@ +rest\_framework\_dso.embedding module +===================================== + +.. automodule:: rest_framework_dso.embedding + :members: + :undoc-members: + :show-inheritance: diff --git a/dev-docs/source/api/rest_framework_dso.filters.rst b/dev-docs/source/api/rest_framework_dso.filters.rst deleted file mode 100644 index a8a0586ef..000000000 --- a/dev-docs/source/api/rest_framework_dso.filters.rst +++ /dev/null @@ -1,16 +0,0 @@ -rest\_framework\_dso.filters module -=================================== - -.. automodule:: rest_framework_dso.filters - -Filter Backends ---------------- - -.. autoclass:: rest_framework_dso.filters.DSOFilterBackend - -.. autoclass:: rest_framework_dso.filters.DSOOrderingFilter - -Filterset classes ------------------ - -.. autoclass:: rest_framework_dso.filters.DSOFilterSet diff --git a/dev-docs/source/api/rest_framework_dso.iterators.rst b/dev-docs/source/api/rest_framework_dso.iterators.rst new file mode 100644 index 000000000..53c98fcb5 --- /dev/null +++ b/dev-docs/source/api/rest_framework_dso.iterators.rst @@ -0,0 +1,7 @@ +rest\_framework\_dso.iterators module +===================================== + +.. automodule:: rest_framework_dso.iterators + :members: + :undoc-members: + :show-inheritance: diff --git a/dev-docs/source/api/rest_framework_dso.rst b/dev-docs/source/api/rest_framework_dso.rst index 7f7dbdf78..c6caffef3 100644 --- a/dev-docs/source/api/rest_framework_dso.rst +++ b/dev-docs/source/api/rest_framework_dso.rst @@ -5,9 +5,10 @@ rest\_framework\_dso package :maxdepth: 1 rest_framework_dso.crs + rest_framework_dso.embedding rest_framework_dso.exceptions rest_framework_dso.fields - rest_framework_dso.filters + rest_framework_dso.iterators rest_framework_dso.openapi rest_framework_dso.pagination rest_framework_dso.parsers diff --git a/dev-docs/source/api/schematools.permissions.rst b/dev-docs/source/api/schematools.permissions.rst new file mode 100644 index 000000000..d8574c81d --- /dev/null +++ b/dev-docs/source/api/schematools.permissions.rst @@ -0,0 +1,6 @@ +schematools.permissions module +============================= + +.. automodule:: schematools.permissions + :members: + :undoc-members: diff --git a/dev-docs/source/api/schematools.rst b/dev-docs/source/api/schematools.rst index 27e3e16c5..fab04707d 100644 --- a/dev-docs/source/api/schematools.rst +++ b/dev-docs/source/api/schematools.rst @@ -9,9 +9,9 @@ schematools package .. toctree:: :maxdepth: 1 - schematools.datasetcollection schematools.factories schematools.loaders schematools.naming + schematools.permissions schematools.types schematools.validation diff --git a/src/dso_api/dynamic_api/filters/__init__.py b/src/dso_api/dynamic_api/filters/__init__.py index 37ea4312c..3e189accb 100644 --- a/src/dso_api/dynamic_api/filters/__init__.py +++ b/src/dso_api/dynamic_api/filters/__init__.py @@ -1,9 +1,10 @@ from . import lookups # noqa: F401 (needed for registration) from .backends import DynamicFilterBackend, DynamicOrderingFilter -from .parser import FilterInput +from .parser import FilterInput, QueryFilterEngine __all__ = ( "DynamicFilterBackend", "DynamicOrderingFilter", "FilterInput", + "QueryFilterEngine", ) diff --git a/src/dso_api/dynamic_api/filters/openapi.py b/src/dso_api/dynamic_api/filters/openapi.py index c853cbbdc..46874660b 100644 --- a/src/dso_api/dynamic_api/filters/openapi.py +++ b/src/dso_api/dynamic_api/filters/openapi.py @@ -12,7 +12,6 @@ from schematools.types import DatasetFieldSchema, DatasetTableSchema, Temporal from dso_api.dynamic_api.filters import parser -from dso_api.dynamic_api.filters.parser import ALLOWED_SCALAR_LOOKUPS, QueryFilterEngine RE_GEOJSON_TYPE = re.compile(r"^https://geojson\.org/schema/(?P[a-zA-Z]+)\.json$") @@ -99,7 +98,7 @@ def get_table_filter_params(table_schema: DatasetTableSchema) -> list[dict]: # if temporal is not None: for name, fields in temporal.dimensions.items(): start = fields.start # Assume fields.end has the same type. - lookups = ALLOWED_SCALAR_LOOKUPS[start.format or start.type] + lookups = parser.ALLOWED_SCALAR_LOOKUPS[start.format or start.type] for lookup in lookups: openapi_params.append( @@ -123,7 +122,7 @@ def _get_field_openapi_params(field: DatasetFieldSchema, prefix="") -> list[dict prefix = f"{prefix}{field.parent_field.name}." openapi_params = [] - for lookup in QueryFilterEngine.get_allowed_lookups(field): + for lookup in parser.QueryFilterEngine.get_allowed_lookups(field): param = { "name": _get_filter_name(prefix, field, lookup), "in": "query", diff --git a/src/dso_api/dynamic_api/filters/parser.py b/src/dso_api/dynamic_api/filters/parser.py index 8e2483344..8a801512e 100644 --- a/src/dso_api/dynamic_api/filters/parser.py +++ b/src/dso_api/dynamic_api/filters/parser.py @@ -82,8 +82,8 @@ class FilterInput: """The data for a single filter parameter. - This contains the parsed details from a single parameter, - for example: "?someField[isnull]=false" + This contains the parsed details from a single query-string parameter, + for example: ``?someField[isnull]=false``. """ def __init__(self, key: str, path: list[str], lookup: str | None, raw_values: list[str]): @@ -157,10 +157,10 @@ class QueryFilterEngine: querystring are directly mapped against schema fields. """ + #: Which querystring fields should not be treated as table-field names. + #: Except for "page", the non-underscore-prefixed parameters are for backward compatibility. NON_FILTER_PARAMS = { # Allowed request parameters. - # Except for "page", all the non-underscore-prefixed parameters - # are for backward compatibility. "_count", "_expand", "_expandScope", @@ -176,7 +176,7 @@ class QueryFilterEngine: } @classmethod - def from_request(cls, request): + def from_request(cls, request) -> QueryFilterEngine: """Construct the parser from the request data.""" return cls( user_scopes=request.user_scopes, @@ -188,6 +188,7 @@ def from_request(cls, request): def __init__( self, user_scopes: UserScopes, query: MultiValueDict, input_crs: CRS, request_date=None ): + """Initialize the filtering engine using the context provided by the request.""" self.user_scopes = user_scopes self.query = query self.input_crs = input_crs @@ -382,7 +383,9 @@ def _translate_raw_value( # noqa: C901 @staticmethod def get_allowed_lookups(field_schema: DatasetFieldSchema) -> set[str]: - """Find which field[lookup] values are possible, given the field type.""" + """Find which ``field[lookup]`` values are possible, given the field type. + This function feeds the documentation and OpenAPI spec. + """ try: if field_schema.is_relation or field_schema.is_primary: # The 'string' type is needed for the deprecated ?temporalRelationId=.. filter. @@ -397,7 +400,7 @@ def get_allowed_lookups(field_schema: DatasetFieldSchema) -> set[str]: def to_orm_path( field_path: str, table_schema: DatasetTableSchema, user_scopes: UserScopes ) -> str: - """Translate a field name into a ORM path. + """Translate a field name into an ORM path. This also checks whether the field is accessible according to the request scope. """ parts = _parse_filter_path(field_path.split("."), table_schema, user_scopes) diff --git a/src/dso_api/dynamic_api/views/doc.py b/src/dso_api/dynamic_api/views/doc.py index 9419d4196..310ed9f17 100644 --- a/src/dso_api/dynamic_api/views/doc.py +++ b/src/dso_api/dynamic_api/views/doc.py @@ -1,4 +1,8 @@ -"""Dataset documentation views.""" +"""These views service documentation of the available datasets. + +By implementing this part of the documentation as Django views, +the dataset definitions are always in sync with the actual live data models. +""" import logging import operator @@ -66,6 +70,8 @@ def search_index(_request) -> HttpResponse: @method_decorator(decorators, name="get") class GenericDocs(View): + """Documentation pages from ``/v1/docs/generic/...``.""" + PRE = f""" @@ -126,6 +132,8 @@ def get(self, request, category, topic="index", *args, **kwargs): @method_decorator(decorators, name="dispatch") class DocsOverview(TemplateView): + """The ``/v1/docs/index.html`` page.""" + template_name = "dso_api/dynamic_api/docs/overview.html" def get_context_data(self, **kwargs): @@ -153,6 +161,8 @@ def get_context_data(self, **kwargs): @method_decorator(decorators, name="dispatch") class DatasetDocView(TemplateView): + """REST API-specific documentation for a single dataset (``/v1/docs/datasets/...```).""" + template_name = "dso_api/dynamic_api/docs/dataset.html" def get_context_data(self, **kwargs): @@ -194,7 +204,7 @@ def get_context_data(self, **kwargs): @method_decorator(decorators, name="dispatch") class DatasetWFSDocView(TemplateView): - """WFS-specific documentation for a single dataset.""" + """WFS-specific documentation for a single dataset (``/v1/docs/wfs-datasets/...``).""" template_name = "dso_api/dynamic_api/docs/dataset_wfs.html" diff --git a/src/dso_api/dynamic_api/views/mvt.py b/src/dso_api/dynamic_api/views/mvt.py index 0c644ab55..b1cf7db6d 100644 --- a/src/dso_api/dynamic_api/views/mvt.py +++ b/src/dso_api/dynamic_api/views/mvt.py @@ -65,7 +65,7 @@ def get_related_apis(self, ds: Dataset, base: str): class DatasetMVTSingleView(TemplateView): - """Shows info about a dataset and its geo-tables.""" + """Shows an HTML page about a dataset and its geo-tables.""" template_name = "dso_api/dynamic_api/mvt_single.html" @@ -95,7 +95,9 @@ def get_context_data(self, **kwargs): class DatasetMVTView(CheckPermissionsMixin, MVTView): - """An MVT view for a single dataset.""" + """An MVT view for a single table. + This view generates the Mapbox Vector Tile format as output. + """ def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) From 7632bafbeb868bb2a15ab44c73337652ddab2abf Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 11 Nov 2024 15:17:04 +0100 Subject: [PATCH 4/6] docs: add symlink to quickly find end-user docs --- docs | 1 + 1 file changed, 1 insertion(+) create mode 120000 docs diff --git a/docs b/docs new file mode 120000 index 000000000..c9dc89909 --- /dev/null +++ b/docs @@ -0,0 +1 @@ +src/templates/dso_api/dynamic_api/docs \ No newline at end of file From 5e89f66c05228e433e1dacbfc841e69a010d0428 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 11 Nov 2024 17:07:11 +0100 Subject: [PATCH 5/6] docs: improve styling of end-user docs * Fixed Bootstrap table style * Fixed tip/warning/note style box * Fixed HTML markup in temporal date fields. * Removed static template HTML from Python code. --- src/dso_api/dynamic_api/urls.py | 7 +- src/dso_api/dynamic_api/views/doc.py | 78 +++++-------------- .../static/dso_api/dynamic_api/css/docs.css | 34 ++++++++ .../dso_api/dynamic_api/docs/dataset.html | 15 ++-- .../dso_api/dynamic_api/docs/dataset_wfs.html | 9 ++- .../dso_api/dynamic_api/docs/overview.html | 9 ++- .../dso_api/dynamic_api/docs/rest/base.html | 43 ++++++++++ .../dso_api/dynamic_api/docs/search.html | 7 +- 8 files changed, 124 insertions(+), 78 deletions(-) create mode 100644 src/dso_api/static/dso_api/dynamic_api/css/docs.css create mode 100644 src/templates/dso_api/dynamic_api/docs/rest/base.html diff --git a/src/dso_api/dynamic_api/urls.py b/src/dso_api/dynamic_api/urls.py index 0d97b7b2b..bb86f7c5b 100644 --- a/src/dso_api/dynamic_api/urls.py +++ b/src/dso_api/dynamic_api/urls.py @@ -13,25 +13,28 @@ def get_patterns(router_urls): """Generate the actual URL patterns for this file.""" return [ + # Doc endpoints path( - "docs/generic/.html", + "docs/generic/.html", GenericDocs.as_view(), name="docs-generic", ), path( - "docs/generic//.html", + "docs/generic//.html", GenericDocs.as_view(), name="docs-generic", ), path("docs/index.html", DocsOverview.as_view(), name="docs-index"), path("docs/search.html", search), path("docs/searchindex.json", search_index), + # Override some API endpoints: path( "haalcentraal/bag/", HaalCentraalBAG.as_view(), name="haalcentraal-bag" ), path( "haalcentraal/brk/", HaalCentraalBRK.as_view(), name="haalcentraal-brk" ), + # All API types: path("mvt/", views.DatasetMVTIndexView.as_view(), name="mvt-index"), path("wfs/", views.DatasetWFSIndexView.as_view()), path("", include(router_urls), name="api-root"), diff --git a/src/dso_api/dynamic_api/views/doc.py b/src/dso_api/dynamic_api/views/doc.py index 310ed9f17..10e7cc1a7 100644 --- a/src/dso_api/dynamic_api/views/doc.py +++ b/src/dso_api/dynamic_api/views/doc.py @@ -18,7 +18,6 @@ from django.urls import NoReverseMatch, reverse from django.utils.decorators import method_decorator from django.utils.safestring import mark_safe -from django.views import View from django.views.decorators.cache import cache_page from django.views.generic import TemplateView from markdown import Markdown @@ -68,66 +67,30 @@ def search_index(_request) -> HttpResponse: return JsonResponse(index) -@method_decorator(decorators, name="get") -class GenericDocs(View): +# @method_decorator(decorators, name="get") +class GenericDocs(TemplateView): """Documentation pages from ``/v1/docs/generic/...``.""" - PRE = f""" - - - - - Amsterdam DataPunt API Documentatie - - - - - -
- -

Introductie API Keys

-
-

- Het Dataplatform van de gemeente Amsterdam gaat het gebruik van een identificatie key - bij het aanroepen van haar API's vanaf 1 februari 2024 verplicht stellen. - Vanaf 1 februari 2024 kun je de API's van het Dataplatform niet meer zonder - een key gebruiken. Vraag tijdig een key aan via dit aanvraagformulier. - Doe je dit niet, dan werkt je applicatie of website vanaf 1 februari 2024 niet meer. - Dit geldt voor alle API's die op deze pagina gedocumenteerd zijn. -

-

- Door de API key kunnen we contact houden met de gebruikers van onze API's. - Zo kunnen we gebruikers informeren over updates. - Daarnaast krijgen we hiermee inzicht in het gebruik van de API's - en in wie welke dataset via de API bevraagt. - Ook voor dataeigenaren is dit waardevolle informatie. -

-

- Meer info:
- - Pagina API key aanvragen
- - Technische documentatie
- Vragen? Mail naar dataplatform@amsterdam.nl
-

-

+ template_name = "dso_api/dynamic_api/docs/rest/base.html" - """ - - POST = """
""" - - def get(self, request, category, topic="index", *args, **kwargs): - uri = request.build_absolute_uri(reverse("dynamic_api:api-root")) + def get_context_data(self, **kwargs): + category = self.kwargs["category"] + topic = self.kwargs.get("topic", "index") + uri = self.request.build_absolute_uri(reverse("dynamic_api:api-root")) template = f"dso_api/dynamic_api/docs/{category}/{topic}.md" try: md = render_to_string(template, context={"uri": uri}) except TemplateDoesNotExist as e: raise Http404() from e + html = markdown.convert(md) - return HttpResponse(self.PRE + html + self.POST) + html = html.replace("", '
') + + return { + "markdown_content": mark_safe(html), # noqa: S308 + "apikey_register_url": urljoin(settings.APIKEYSERV_API_URL, "/clients/v1/register/"), + "apikey_docs_url": urljoin(settings.APIKEYSERV_API_URL, "/clients/v1/docs/"), + } @method_decorator(decorators, name="dispatch") @@ -242,7 +205,7 @@ class LookupContext(NamedTuple): def lookup_context(op, example, descr): # disable mark_safe() warnings because this is static HTML in this very file. - return LookupContext(op, mark_safe(example), mark_safe(descr)) # noqa: B308, B703, S308 + return LookupContext(op, mark_safe(example or ""), mark_safe(descr or "")) # noqa: S308 # This should match ALLOWED_SCALAR_LOOKUPS in filters.parser (except for the "exact" lookup). @@ -293,12 +256,12 @@ def lookup_context(op, example, descr): ), lookup_context( "isnull", - "true of false", + "true | false", "Test op ontbrekende waarden (IS NULL / IS NOT NULL).", ), lookup_context( "isempty", - "true of false", + "true | false", "Test of de waarde leeg is (== '' / != '')", ), ] @@ -336,8 +299,7 @@ def _table_context(ds: Dataset, table: DatasetTableSchema): { "name": name, "type": "Datetime", - "value_example": "yyyy-mm-dd of " - "yyyy-mm-ddThh:mm[:ss[.ms]]", + "value_example": mark_safe(VALUE_EXAMPLES["date-time"]), # noqa: S308 } ) @@ -556,7 +518,7 @@ def _filter_payload( "name": name, "type": type.capitalize(), "is_deprecated": is_deprecated, - "value_example": mark_safe(value_example or ""), # noqa: B308, B703, S308 (is static HTML) + "value_example": mark_safe(value_example or ""), # noqa: S308 (is static HTML) "lookups": [LOOKUP_CONTEXT[op] for op in lookups], "auth": _fix_auth(field.auth | field.table.auth | field.table.dataset.auth), } diff --git a/src/dso_api/static/dso_api/dynamic_api/css/docs.css b/src/dso_api/static/dso_api/dynamic_api/css/docs.css new file mode 100644 index 000000000..a954e98bf --- /dev/null +++ b/src/dso_api/static/dso_api/dynamic_api/css/docs.css @@ -0,0 +1,34 @@ +/* Override bootstrap 3 defaults */ +body { line-height: 1.6; } + +a:not([href]) { color: inherit; } +a:not([href]):hover { color: inherit; text-decoration: none; } + +/* Inspired by readthedocs style */ +.tip, +.warning, +.danger, +.note { + padding: 12px; + margin: 12px 0 24px; + border-radius: 4px; + overflow: hidden; +} + +.note { background: #e7f2fa; } +.tip { background: #dbfaf4; } +.warning { background: #ffedcc; } +.danger { background: #fdf3f2; } + +.title { + font-weight: 700; + display: block; + color: #fff; + padding: 6px 12px; + margin: -12px -12px 12px; +} + +.note .title { background: #6ab0de; } +.tip .title { background: #1abc9c; } +.warning .title { background: #f0b37e; } +.danger .title { background: #f29f97; } diff --git a/src/templates/dso_api/dynamic_api/docs/dataset.html b/src/templates/dso_api/dynamic_api/docs/dataset.html index 2dd1d8835..2c023a8a6 100644 --- a/src/templates/dso_api/dynamic_api/docs/dataset.html +++ b/src/templates/dso_api/dynamic_api/docs/dataset.html @@ -1,10 +1,11 @@ -{% load i18n %} +{% load i18n static %} {% if schema.title %}{{ schema.title }}{% else %}{{ name|title }}{% endif %} — Amsterdam Datapunt API Documentatie v1 - - - + + + + {% block extrahead %}{% endblock %} @@ -78,7 +79,7 @@

{{ table.title }}

De volgende velden zijn beschikbaar:

-
+
@@ -110,7 +111,7 @@

{{ table.title }}

Veldnaam

De volgende query-parameters zijn te gebruiken:

- +
@@ -154,7 +155,7 @@

{{ table.title }}

{% if table.expands %}

Insluitbare relaties

De volgende velden kunnen ingesloten worden met ?_expandScope=...:

-
Parameter
+
diff --git a/src/templates/dso_api/dynamic_api/docs/dataset_wfs.html b/src/templates/dso_api/dynamic_api/docs/dataset_wfs.html index ad2cb27f6..0c8c52b10 100644 --- a/src/templates/dso_api/dynamic_api/docs/dataset_wfs.html +++ b/src/templates/dso_api/dynamic_api/docs/dataset_wfs.html @@ -1,11 +1,12 @@ - +{% load static %} {{ schema.title }} - - - + + + +
diff --git a/src/templates/dso_api/dynamic_api/docs/overview.html b/src/templates/dso_api/dynamic_api/docs/overview.html index 1113006ce..77aec22ad 100644 --- a/src/templates/dso_api/dynamic_api/docs/overview.html +++ b/src/templates/dso_api/dynamic_api/docs/overview.html @@ -1,11 +1,12 @@ - +{% load static %} Amsterdam DataPunt API Documentatie - - - + + + +
diff --git a/src/templates/dso_api/dynamic_api/docs/rest/base.html b/src/templates/dso_api/dynamic_api/docs/rest/base.html new file mode 100644 index 000000000..330fd96a3 --- /dev/null +++ b/src/templates/dso_api/dynamic_api/docs/rest/base.html @@ -0,0 +1,43 @@ +{% load static %} + + + + Amsterdam DataPunt API Documentatie + + + + + + +
+ +

Introductie API Keys

+
+

+ Het Dataplatform van de gemeente Amsterdam gaat het gebruik van een identificatie key + bij het aanroepen van haar API's vanaf 1 februari 2024 verplicht stellen. + Vanaf 1 februari 2024 kun je de API's van het Dataplatform niet meer zonder + een key gebruiken. Vraag tijdig een key aan via dit aanvraagformulier. + Doe je dit niet, dan werkt je applicatie of website vanaf 1 februari 2024 niet meer. + Dit geldt voor alle API's die op deze pagina gedocumenteerd zijn. +

+

+ Door de API key kunnen we contact houden met de gebruikers van onze API's. + Zo kunnen we gebruikers informeren over updates. + Daarnaast krijgen we hiermee inzicht in het gebruik van de API's + en in wie welke dataset via de API bevraagt. + Ook voor dataeigenaren is dit waardevolle informatie. +

+

+ Meer info:
+ Pagina API key aanvragen
+ Technische documentatie
+ Vragen? Mail naar dataplatform@amsterdam.nl
+

+

+ +{{ markdown_content }} + +
+ + diff --git a/src/templates/dso_api/dynamic_api/docs/search.html b/src/templates/dso_api/dynamic_api/docs/search.html index dd1f110b2..ccc60a6bf 100644 --- a/src/templates/dso_api/dynamic_api/docs/search.html +++ b/src/templates/dso_api/dynamic_api/docs/search.html @@ -2,9 +2,10 @@ Amsterdam Datapunt API Documentatie v1 - - - + + + + {% block extrahead %}{% endblock %} From e4021d9834af5c608d518fa986fbfeb238d25a21 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Wed, 13 Nov 2024 09:58:35 +0100 Subject: [PATCH 6/6] Include code syntax highlighting in docs --- src/dso_api/dynamic_api/views/doc.py | 9 ++- .../static/dso_api/dynamic_api/css/docs.css | 77 +++++++++++++++++++ src/requirements.in | 1 + src/requirements.txt | 7 +- src/requirements_dev.txt | 7 +- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/dso_api/dynamic_api/views/doc.py b/src/dso_api/dynamic_api/views/doc.py index 10e7cc1a7..ef1f29037 100644 --- a/src/dso_api/dynamic_api/views/doc.py +++ b/src/dso_api/dynamic_api/views/doc.py @@ -21,6 +21,7 @@ from django.views.decorators.cache import cache_page from django.views.generic import TemplateView from markdown import Markdown +from markdown.extensions.codehilite import CodeHiliteExtension from markdown.extensions.tables import TableExtension from schematools.contrib.django.models import Dataset from schematools.naming import to_snake_case @@ -30,7 +31,13 @@ logger = logging.getLogger(__name__) -markdown = Markdown(extensions=[TableExtension(), "fenced_code"]) +markdown = Markdown( + extensions=[ + TableExtension(), + "fenced_code", + CodeHiliteExtension(use_pygments=True, noclasses=False), + ] +) CACHE_DURATION = 3600 # seconds. diff --git a/src/dso_api/static/dso_api/dynamic_api/css/docs.css b/src/dso_api/static/dso_api/dynamic_api/css/docs.css index a954e98bf..26034f7b9 100644 --- a/src/dso_api/static/dso_api/dynamic_api/css/docs.css +++ b/src/dso_api/static/dso_api/dynamic_api/css/docs.css @@ -32,3 +32,80 @@ a:not([href]):hover { color: inherit; text-decoration: none; } .tip .title { background: #1abc9c; } .warning .title { background: #f0b37e; } .danger .title { background: #f29f97; } + +/* Generated using: pygmentize -S default -f html -a .codehilite */ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.codehilite .hll { background-color: #ffffcc } +.codehilite { background: #f8f8f8; } +.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */ +.codehilite .err { border: 1px solid #FF0000 } /* Error */ +.codehilite .k { color: #008000; font-weight: bold } /* Keyword */ +.codehilite .o { color: #666666 } /* Operator */ +.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.codehilite .cp { color: #9C6500 } /* Comment.Preproc */ +.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.codehilite .ge { font-style: italic } /* Generic.Emph */ +.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.codehilite .gr { color: #E40000 } /* Generic.Error */ +.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.codehilite .gi { color: #008400 } /* Generic.Inserted */ +.codehilite .go { color: #717171 } /* Generic.Output */ +.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.codehilite .gs { font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.codehilite .gt { color: #0044DD } /* Generic.Traceback */ +.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.codehilite .kp { color: #008000 } /* Keyword.Pseudo */ +.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.codehilite .kt { color: #B00040 } /* Keyword.Type */ +.codehilite .m { color: #666666 } /* Literal.Number */ +.codehilite .s { color: #BA2121 } /* Literal.String */ +.codehilite .na { color: #687822 } /* Name.Attribute */ +.codehilite .nb { color: #008000 } /* Name.Builtin */ +.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.codehilite .no { color: #880000 } /* Name.Constant */ +.codehilite .nd { color: #AA22FF } /* Name.Decorator */ +.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.codehilite .nf { color: #0000FF } /* Name.Function */ +.codehilite .nl { color: #767600 } /* Name.Label */ +.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.codehilite .nv { color: #19177C } /* Name.Variable */ +.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.codehilite .w { color: #bbbbbb } /* Text.Whitespace */ +.codehilite .mb { color: #666666 } /* Literal.Number.Bin */ +.codehilite .mf { color: #666666 } /* Literal.Number.Float */ +.codehilite .mh { color: #666666 } /* Literal.Number.Hex */ +.codehilite .mi { color: #666666 } /* Literal.Number.Integer */ +.codehilite .mo { color: #666666 } /* Literal.Number.Oct */ +.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */ +.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ +.codehilite .sc { color: #BA2121 } /* Literal.String.Char */ +.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ +.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.codehilite .sx { color: #008000 } /* Literal.String.Other */ +.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */ +.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ +.codehilite .ss { color: #19177C } /* Literal.String.Symbol */ +.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.codehilite .fm { color: #0000FF } /* Name.Function.Magic */ +.codehilite .vc { color: #19177C } /* Name.Variable.Class */ +.codehilite .vg { color: #19177C } /* Name.Variable.Global */ +.codehilite .vi { color: #19177C } /* Name.Variable.Instance */ +.codehilite .vm { color: #19177C } /* Name.Variable.Magic */ +.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/src/requirements.in b/src/requirements.in index 7ce287095..7c0d2d36a 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -24,6 +24,7 @@ more-ds == 0.0.6 more-itertools == 10.5.0 openapi-spec-validator == 0.7.1 orjson == 3.10.11 +pygments == 2.18.0 python-json-logger==2.0.7 python-string-utils == 1.0.0 requests == 2.32.3 diff --git a/src/requirements.txt b/src/requirements.txt index c1218660e..3b0ed1e27 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --generate-hashes --output-file=requirements.txt requirements.in +# pip-compile --generate-hashes --output-file=requirements.txt --resolver=legacy requirements.in # amsterdam-schema-tools[django]==6.1.1 \ --hash=sha256:69d71adc49540ddc59172b7e5093dd1bae654a80481652f7cffa3f64badbcd9b \ @@ -1294,14 +1294,15 @@ pyflakes==3.2.0 \ pygments==2.18.0 \ --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a - # via rich + # via + # -r requirements.in + # rich pyjwt[crypto]==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via # datadiensten-apikeyclient # msal - # pyjwt pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 diff --git a/src/requirements_dev.txt b/src/requirements_dev.txt index 6789f1772..c77114e09 100644 --- a/src/requirements_dev.txt +++ b/src/requirements_dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --generate-hashes --output-file=requirements_dev.txt requirements_dev.in +# pip-compile --generate-hashes --output-file=requirements_dev.txt --resolver=legacy requirements_dev.in # amsterdam-schema-tools[django]==6.1.1 \ --hash=sha256:69d71adc49540ddc59172b7e5093dd1bae654a80481652f7cffa3f64badbcd9b \ @@ -1376,14 +1376,15 @@ pyflakes==3.2.0 \ pygments==2.18.0 \ --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a - # via rich + # via + # -r /home/diederik/Sites/amsterdam/dso-api/src/requirements.in + # rich pyjwt[crypto]==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via # datadiensten-apikeyclient # msal - # pyjwt pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
Relatie