commonground-api-common 1.13.4__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/METADATA +22 -23
- {commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/RECORD +29 -27
- {commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/WHEEL +1 -1
- vng_api_common/__init__.py +1 -1
- vng_api_common/admin.py +1 -20
- vng_api_common/authorizations/admin.py +1 -1
- vng_api_common/authorizations/middleware.py +15 -6
- vng_api_common/authorizations/migrations/0016_remove_authorizationsconfig_api_root_and_more.py +76 -0
- vng_api_common/authorizations/models.py +38 -3
- vng_api_common/authorizations/utils.py +17 -0
- vng_api_common/client.py +56 -27
- vng_api_common/conf/api.py +0 -3
- vng_api_common/management/commands/generate_swagger.py +1 -1
- vng_api_common/migrations/0006_delete_apicredential.py +119 -0
- vng_api_common/mocks.py +4 -1
- vng_api_common/models.py +0 -111
- vng_api_common/notifications/api/views.py +1 -1
- vng_api_common/notifications/handlers.py +8 -3
- vng_api_common/notifications/migrations/0011_remove_subscription_config_and_more.py +23 -0
- vng_api_common/oas.py +0 -3
- vng_api_common/routers.py +3 -3
- vng_api_common/tests/schema.py +12 -0
- vng_api_common/utils.py +0 -22
- vng_api_common/validators.py +0 -38
- vng_api_common/views.py +26 -22
- vng_api_common/notifications/constants.py +0 -3
- vng_api_common/notifications/models.py +0 -97
- {commonground_api_common-1.13.4.data → commonground_api_common-2.0.0.data}/scripts/generate_schema +0 -0
- {commonground_api_common-1.13.4.data → commonground_api_common-2.0.0.data}/scripts/patch_content_types +0 -0
- {commonground_api_common-1.13.4.data → commonground_api_common-2.0.0.data}/scripts/use_external_components +0 -0
- {commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/top_level.txt +0 -0
{commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: commonground-api-common
|
3
|
-
Version:
|
3
|
+
Version: 2.0.0
|
4
4
|
Summary: Commonground API tooling
|
5
5
|
Home-page: https://github.com/maykinmedia/commonground-api-common
|
6
6
|
Author: Maykin Media, VNG-Realisatie
|
@@ -9,7 +9,6 @@ License: EUPL 1.2
|
|
9
9
|
Keywords: openapi,swagger,django
|
10
10
|
Classifier: Development Status :: 5 - Production/Stable
|
11
11
|
Classifier: Framework :: Django
|
12
|
-
Classifier: Framework :: Django :: 3.2
|
13
12
|
Classifier: Framework :: Django :: 4.2
|
14
13
|
Classifier: Intended Audience :: Developers
|
15
14
|
Classifier: Operating System :: Unix
|
@@ -20,35 +19,27 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
22
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
23
|
-
Requires-Dist: django>=
|
24
|
-
Requires-Dist: django-filter
|
22
|
+
Requires-Dist: django>=4.2.0
|
23
|
+
Requires-Dist: django-filter<=25.1,>=23.1
|
25
24
|
Requires-Dist: django-solo
|
26
|
-
Requires-Dist: djangorestframework>=3.
|
27
|
-
Requires-Dist:
|
25
|
+
Requires-Dist: djangorestframework>=3.15.0
|
26
|
+
Requires-Dist: djangorestframework_camel_case>=1.2.0
|
28
27
|
Requires-Dist: django-rest-framework-condition
|
29
28
|
Requires-Dist: drf-yasg>=1.20.0
|
30
|
-
Requires-Dist: drf-nested-routers>=0.
|
31
|
-
Requires-Dist: gemma-zds-client>=0.14.0
|
29
|
+
Requires-Dist: drf-nested-routers>=0.94.1
|
32
30
|
Requires-Dist: iso-639
|
33
31
|
Requires-Dist: isodate
|
34
|
-
Requires-Dist: notifications-api-common>=0.
|
32
|
+
Requires-Dist: notifications-api-common>=0.3.0
|
33
|
+
Requires-Dist: zgw-consumers>=0.35.1
|
35
34
|
Requires-Dist: oyaml
|
36
35
|
Requires-Dist: PyJWT>=2.0.0
|
37
36
|
Requires-Dist: requests
|
37
|
+
Requires-Dist: requests-mock
|
38
38
|
Requires-Dist: coreapi
|
39
|
-
|
40
|
-
|
41
|
-
Provides-Extra: docs
|
42
|
-
Requires-Dist: psycopg2; extra == "docs"
|
43
|
-
Requires-Dist: sphinx; extra == "docs"
|
44
|
-
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
45
|
-
Provides-Extra: markdown_docs
|
39
|
+
Requires-Dist: ape-pie
|
40
|
+
Provides-Extra: markdown-docs
|
46
41
|
Requires-Dist: django-markup<=1.3; extra == "markdown-docs"
|
47
42
|
Requires-Dist: markdown; extra == "markdown-docs"
|
48
|
-
Provides-Extra: pep8
|
49
|
-
Requires-Dist: flake8; extra == "pep8"
|
50
|
-
Provides-Extra: release
|
51
|
-
Requires-Dist: bump2version; extra == "release"
|
52
43
|
Provides-Extra: tests
|
53
44
|
Requires-Dist: psycopg2; extra == "tests"
|
54
45
|
Requires-Dist: pytest; extra == "tests"
|
@@ -59,6 +50,17 @@ Requires-Dist: isort; extra == "tests"
|
|
59
50
|
Requires-Dist: black; extra == "tests"
|
60
51
|
Requires-Dist: requests-mock; extra == "tests"
|
61
52
|
Requires-Dist: freezegun; extra == "tests"
|
53
|
+
Requires-Dist: zgw-consumers-oas; extra == "tests"
|
54
|
+
Provides-Extra: pep8
|
55
|
+
Requires-Dist: flake8; extra == "pep8"
|
56
|
+
Provides-Extra: coverage
|
57
|
+
Requires-Dist: pytest-cov; extra == "coverage"
|
58
|
+
Provides-Extra: docs
|
59
|
+
Requires-Dist: psycopg2; extra == "docs"
|
60
|
+
Requires-Dist: sphinx; extra == "docs"
|
61
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
62
|
+
Provides-Extra: release
|
63
|
+
Requires-Dist: bump2version; extra == "release"
|
62
64
|
|
63
65
|
===================================================
|
64
66
|
Commonground-API-common - Tooling voor RESTful APIs
|
@@ -97,9 +99,6 @@ Features
|
|
97
99
|
* ``UniekeIdentificatieValidator`` (in combinatie met organisatie)
|
98
100
|
* ``InformatieObjectUniqueValidator`` om te valideren dat M2M entries
|
99
101
|
slechts eenmalig voorkomen
|
100
|
-
* ``ObjectInformatieObjectValidator`` om te valideren dat de synchronisatie
|
101
|
-
van een object-informatieobject relatie pas kan nadat deze relatie in het
|
102
|
-
DRC gemaakt is
|
103
102
|
* ``IsImmutableValidator`` - valideer dat bepaalde velden niet gewijzigd
|
104
103
|
worden bij een (partial) update, maar wel mogen gezet worden bij een create
|
105
104
|
* ``ResourceValidator`` - valideer dat een URL een bepaalde resource ontsluit
|
@@ -1,12 +1,12 @@
|
|
1
|
-
commonground_api_common-
|
2
|
-
commonground_api_common-
|
3
|
-
commonground_api_common-
|
4
|
-
vng_api_common/__init__.py,sha256=
|
5
|
-
vng_api_common/admin.py,sha256=
|
1
|
+
commonground_api_common-2.0.0.data/scripts/generate_schema,sha256=UKhznmbHX1zUjPx8G3XtxUPQiqnyd3TOIXs27Tnjl7U,952
|
2
|
+
commonground_api_common-2.0.0.data/scripts/patch_content_types,sha256=dpGpYrZOZe8O5CHWd0F0QnP6Wk_7lK6DyuVZpBPr4mY,319
|
3
|
+
commonground_api_common-2.0.0.data/scripts/use_external_components,sha256=xvvbngO2aDUagVXv4xRUqPaVtH_eOaVMWLQ8lyAPhEA,369
|
4
|
+
vng_api_common/__init__.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
|
5
|
+
vng_api_common/admin.py,sha256=iFtUPGf-ha0I-bXgq8QIFrP23Kzk_H3FlgAjt0U-ip0,259
|
6
6
|
vng_api_common/apps.py,sha256=wOQuxUImMpH39R0JrDdCZp5ADaUM8jU8vFPFW9froSs,3458
|
7
7
|
vng_api_common/checks.py,sha256=tOyfV7MMLGh4anrd_W30LvJCxiyQ4sFs1mGd9mtrEc0,1175
|
8
8
|
vng_api_common/choices.py,sha256=dboFRoM34GpRUpxB9WexexccopcQSogu1QIyY4B9ACY,541
|
9
|
-
vng_api_common/client.py,sha256=
|
9
|
+
vng_api_common/client.py,sha256=EZWygrJum6pLeKS77mzynfV9kq0gPTjXXSHEpeyEZHg,1888
|
10
10
|
vng_api_common/compat.py,sha256=n4jDFSPzXnh-D_VXjftKMCJYj_q_t6eJrUTWLayl8jQ,768
|
11
11
|
vng_api_common/constants.py,sha256=yYtnYId9alRuhGBoqaO8lkXYi_ATi2FW-wEDGRpiRuQ,10948
|
12
12
|
vng_api_common/decorators.py,sha256=_p-mAyi5Na_-ll00ErcQ3mRoZNCsJzYDAliXn50Bmes,278
|
@@ -20,22 +20,22 @@ vng_api_common/filtersets.py,sha256=Ioarp5t_cz6Hf3iIcP2Rc0D7tGYhcMQPVdIJJ2dvtv8,
|
|
20
20
|
vng_api_common/generators.py,sha256=tD7ZyyFgCQ3KiktpBFtn5YiVdl3jioGxzyvkEZFYdQQ,4911
|
21
21
|
vng_api_common/geo.py,sha256=AZbrw0rwGYOmaSUk8JJSkx-4_tVrfT_cgggh9omRwhU,1862
|
22
22
|
vng_api_common/middleware.py,sha256=2DRw0hPpvUMqwoH1Ze8S7tDB16lmj4Bnd6vN5ijkGM8,875
|
23
|
-
vng_api_common/mocks.py,sha256=
|
24
|
-
vng_api_common/models.py,sha256=
|
25
|
-
vng_api_common/oas.py,sha256=
|
23
|
+
vng_api_common/mocks.py,sha256=0sELLs-uy2ndu29m5P6FN_p1ehNhYv7IoRayYPs379Y,6200
|
24
|
+
vng_api_common/models.py,sha256=3WgpCWQpkCOrMCtqp7EbnP681II43Sg_cch8ZOm4EcU,1707
|
25
|
+
vng_api_common/oas.py,sha256=v_M_nsPRk12VF4lFa3Kw2c-wUThfY8PJ6JlEgeUhz9E,2618
|
26
26
|
vng_api_common/pagination.py,sha256=sI6VJ5AHDBhtNRQ8OFrD2heQK-NxaL_6n73ijiqWDLc,255
|
27
27
|
vng_api_common/permissions.py,sha256=ayDxk9Wt8j0yO57FhZ8XaRkPET4lAqd8SSw3m4o3EGs,7562
|
28
28
|
vng_api_common/polymorphism.py,sha256=N-x39pG2unD4N0ZbxFvuhE8ibPaJ0eKeqfvAJW3NnEU,6623
|
29
|
-
vng_api_common/routers.py,sha256=
|
29
|
+
vng_api_common/routers.py,sha256=hEnhBulkgMM-7W_lYaykKTgTBj3-avl7DGsR9P7BbTU,1897
|
30
30
|
vng_api_common/schema.py,sha256=1Bt9dH67AOLpfPNF7Q4IH1cXJeaIevSamZQzJ8DJ7ks,6144
|
31
31
|
vng_api_common/scopes.py,sha256=PGs6CkXorAAdWXGFY1bSy-jmsPn122Njen9aFFOpFIQ,2351
|
32
32
|
vng_api_common/search.py,sha256=yehS6boCOk1JXLCqAMU-B62hWtbTBSf_WKIVGPgp0Mg,1045
|
33
33
|
vng_api_common/serializers.py,sha256=7_7IotlRfVb0woe6Wy79l_el3sPzrSY8-bfiYLbEUqs,10162
|
34
34
|
vng_api_common/urls.py,sha256=9IWHYLlEIIHNaZ_Zq02qNQ2HJpETb7o-89r7yBM_tQs,270
|
35
|
-
vng_api_common/utils.py,sha256=
|
36
|
-
vng_api_common/validators.py,sha256=
|
35
|
+
vng_api_common/utils.py,sha256=zrtpssOA-NcJHhAlxioBiXeY3G2R_uf0l8oWkDD_EiE,8511
|
36
|
+
vng_api_common/validators.py,sha256=Y1OQrmnH3U01hnCizWc_xjpSSGUWKlwiFxeHHGCThpI,11012
|
37
37
|
vng_api_common/version.py,sha256=yJV9_yTM7Qnzg0zGNkJQkN9Uai3I_ZUkcyseJRPRk5I,129
|
38
|
-
vng_api_common/views.py,sha256=
|
38
|
+
vng_api_common/views.py,sha256=seRGa-_2BPtrtHP6C6mmQCnT10q9ECfRHa8lpwyEcmM,7230
|
39
39
|
vng_api_common/viewsets.py,sha256=z5pzvSymFiiuCjP_-uuW-3OJKZY_psPAt8fWeWySU0c,2278
|
40
40
|
vng_api_common/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
41
|
vng_api_common/api/permissions.py,sha256=okbwiscxlAtbQWTCRDLL2reOxgj0rRDZeDcrtXAYq00,739
|
@@ -75,10 +75,11 @@ vng_api_common/audittrails/migrations/0018_auto_20221212_0745.py,sha256=pf4NBYI0
|
|
75
75
|
vng_api_common/audittrails/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
76
76
|
vng_api_common/audittrails/migrations/_operations.py,sha256=UOMv0zAK8CIQ73cSu6wwQG_hkW46Isdy7JCnljn8GII,3280
|
77
77
|
vng_api_common/authorizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
78
|
-
vng_api_common/authorizations/admin.py,sha256=
|
79
|
-
vng_api_common/authorizations/middleware.py,sha256=
|
80
|
-
vng_api_common/authorizations/models.py,sha256=
|
78
|
+
vng_api_common/authorizations/admin.py,sha256=Tk0yYKbb005E0XZaYYWbucMf_K5M8Hhz62wSBDi8rhM,813
|
79
|
+
vng_api_common/authorizations/middleware.py,sha256=KJ3znCXPRMOVqSur62SmBjvC6RcKxtcWq1rzaHdYR98,8416
|
80
|
+
vng_api_common/authorizations/models.py,sha256=0bcZwRc0ntIlPzUM7I8R5yhQRrnXvAZ_l9FpTnpbnxk,5207
|
81
81
|
vng_api_common/authorizations/serializers.py,sha256=3HeKWEqhI3UWwI8SttC4rEID-Epbk7SWsC-bEjolbaw,5151
|
82
|
+
vng_api_common/authorizations/utils.py,sha256=VCXdU4q3CQ1cvuYkg1dPWSbZym1Ufoz5gSmbHYkQcgw,521
|
82
83
|
vng_api_common/authorizations/validators.py,sha256=u7fKm0QgGy8fiAeYmIEB9Gy-yIE9C-tC2ZpnNQBXPso,2816
|
83
84
|
vng_api_common/authorizations/migrations/0001_initial.py,sha256=ooAZtQeDtWgDxXzAP-KnSyyFYLRPM-PMrK5RgOnTPjQ,4360
|
84
85
|
vng_api_common/authorizations/migrations/0002_authorizationsconfig.py,sha256=m4taH6ClHI-YHYGGOKaq_qYXGx9lq1InXOGLQKg9MSw,1364
|
@@ -95,6 +96,7 @@ vng_api_common/authorizations/migrations/0012_auto_20200619_0545.py,sha256=ECvJJ
|
|
95
96
|
vng_api_common/authorizations/migrations/0013_auto_20201207_0846.py,sha256=fyJ0HpQMM0d1UyK9AK5_c7S_as_NejzrOdbMYiIogOA,1788
|
96
97
|
vng_api_common/authorizations/migrations/0014_auto_20201221_0905.py,sha256=pkg7Y9yvUIY-QB9elvCxfR7OZJ3xMBIha_4WSZeDlwY,1659
|
97
98
|
vng_api_common/authorizations/migrations/0015_auto_20220318_1608.py,sha256=jR1MDuqapBZU4bYVx0V3MRauRpDk_DtCVcBSe7gmtUA,1751
|
99
|
+
vng_api_common/authorizations/migrations/0016_remove_authorizationsconfig_api_root_and_more.py,sha256=-UbTWExHaPL0O8585mm4_OFCm8xJXAmfwmU7V0ysSgg,2407
|
98
100
|
vng_api_common/authorizations/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
99
101
|
vng_api_common/caching/__init__.py,sha256=v_FC6HaHESwr_J94TWzaQBKtIjDLnsQADvqHylEW7g8,687
|
100
102
|
vng_api_common/caching/decorators.py,sha256=lU5dnBjD9aHjoYaV32FlYxY7VFZPdMZe26fVDa_h3Q4,1804
|
@@ -104,7 +106,7 @@ vng_api_common/caching/models.py,sha256=RroS9HFiKNXDV59Odh0x8BO8Az8E81v4gwuprF1A
|
|
104
106
|
vng_api_common/caching/registry.py,sha256=mY1r99x7m0DQ1BN9lZF2zMjFj_oDNq7urxDkOH7-8Yg,6205
|
105
107
|
vng_api_common/caching/signals.py,sha256=78ej5cVan-JpNHKzZNAfs0m2ON_TXKphe8ZKtP-6FVY,4615
|
106
108
|
vng_api_common/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
|
-
vng_api_common/conf/api.py,sha256=
|
109
|
+
vng_api_common/conf/api.py,sha256=bRc488POvBdKeaKVit7s1_LFP-BRLpETmTzcQ6JD614,4372
|
108
110
|
vng_api_common/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
109
111
|
vng_api_common/db/operations.py,sha256=3vj9zD4EntMSmYsNGN1pRDjAcVM68MESDdoAk9wl0U0,3333
|
110
112
|
vng_api_common/inspectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -122,7 +124,7 @@ vng_api_common/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
122
124
|
vng_api_common/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
123
125
|
vng_api_common/management/commands/generate_autorisaties.py,sha256=kMaZQo6e6vCXA9y0e6P2PlxObyjHMyNynRcM4WtzVZI,1130
|
124
126
|
vng_api_common/management/commands/generate_notificaties.py,sha256=0p7CkLkQo-BeV2IljGv0rcRNw2aWY_HwlvybjggZ_gs,1090
|
125
|
-
vng_api_common/management/commands/generate_swagger.py,sha256=
|
127
|
+
vng_api_common/management/commands/generate_swagger.py,sha256=DyRRSDMFEZeXSJp_PWiMc6vYFK3GPEO4WIRWZV-tlm0,6120
|
126
128
|
vng_api_common/management/commands/patch_error_contenttypes.py,sha256=xyFIQ4JRutLZCTcLrWqon0FDeAJCw1bkVg4jM8-EM24,2025
|
127
129
|
vng_api_common/management/commands/use_external_components.py,sha256=FLkhZ5jAIZoAXQWwxqu4yT94LiBJ4GRr6g4ZRp7b_Eg,2936
|
128
130
|
vng_api_common/migrations/0001_initial.py,sha256=frE83rux6uVHK4v-1fsb_1C00TBQQ6koFBA3DoVPHmk,895
|
@@ -130,15 +132,14 @@ vng_api_common/migrations/0002_apicredential.py,sha256=AYgkLMizI0g-Ogth8j_FVJHTD
|
|
130
132
|
vng_api_common/migrations/0003_auto_20190417_1145.py,sha256=0ABqDggAdjmZxhSsLrMb2ZBwTcR1BOWo1Tn8StfiG0A,1001
|
131
133
|
vng_api_common/migrations/0004_auto_20190517_0903.py,sha256=-Qcei-AJx4SXbEYMojmYInhV74uKtDncGP1PLs6hn0M,707
|
132
134
|
vng_api_common/migrations/0005_auto_20190614_1346.py,sha256=5n8SVpbEJzLrSx-7d446Vy1--SaDfO05hAltrSRQx6o,2949
|
135
|
+
vng_api_common/migrations/0006_delete_apicredential.py,sha256=VMVwH4vj9Zq-oF_Z1Do9wobgePtkujwd8bO8M57ACt0,3621
|
133
136
|
vng_api_common/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
134
137
|
vng_api_common/notifications/__init__.py,sha256=UvHr_M8s-dZjCQOp60rLDqP_T3D0CgRIqc8ry5bIM8c,229
|
135
138
|
vng_api_common/notifications/apps.py,sha256=7grk1hxQn5usAWV-sWATmNJ4TwSvT3izXVvV-L-zb_s,208
|
136
|
-
vng_api_common/notifications/
|
137
|
-
vng_api_common/notifications/handlers.py,sha256=-r52nIKbHdFUxE7hob9PiFRe0D3cizhkxbW8qhpK-Ks,2005
|
138
|
-
vng_api_common/notifications/models.py,sha256=Ez3JYeM7BNSiFYtxAtVxWUWDc8t7jOFQlRTjrtE6SlI,2942
|
139
|
+
vng_api_common/notifications/handlers.py,sha256=xjuE_qn5h4gbRNGMsTaCTrHIjXTBcF8na6oMURGAuUU,2089
|
139
140
|
vng_api_common/notifications/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
140
141
|
vng_api_common/notifications/api/urls.py,sha256=c1T_6tuRpgN0V80Xg_ehZzGGlF1FNMmHdBEPAjR736I,164
|
141
|
-
vng_api_common/notifications/api/views.py,sha256=
|
142
|
+
vng_api_common/notifications/api/views.py,sha256=EvppqaANutuOIrkNepavmzFTX6fxo32c6l5c6DKHe8k,2326
|
142
143
|
vng_api_common/notifications/migrations/0001_initial.py,sha256=zEIVIB--6FStaTl3r9GCXxHL1cSCWRO3T90rEpJr3hY,3049
|
143
144
|
vng_api_common/notifications/migrations/0002_subscription__subscription.py,sha256=eqaj6-FAl7vuCswdvin9Lf8nzjP8U6WqVQO4opNKKCc,550
|
144
145
|
vng_api_common/notifications/migrations/0003_auto_20190319_1048.py,sha256=C-ccYUwQPNCjTctpDDFhnQbm_OcpAlTjYQbEuiEi-ys,987
|
@@ -149,6 +150,7 @@ vng_api_common/notifications/migrations/0007_auto_20190429_1442.py,sha256=S9Q_WC
|
|
149
150
|
vng_api_common/notifications/migrations/0008_auto_20190502_0415.py,sha256=Wwh3BsSg0r1I7HZZmrcCHEQMLmaeWEjbqYKapVafb2s,523
|
150
151
|
vng_api_common/notifications/migrations/0009_auto_20190729_0427.py,sha256=Bfw-Axr9lNiXL8Ghns3RK33Dmjvpd_E5MWC740WJlb8,529
|
151
152
|
vng_api_common/notifications/migrations/0010_auto_20220704_1419.py,sha256=Y57o91fbzWZyQO4tgg9Tn4r99hRErRlcrzpLii9Tm3I,1593
|
153
|
+
vng_api_common/notifications/migrations/0011_remove_subscription_config_and_more.py,sha256=3FA3rlpRDp8FB8Azl22pv7C4i1oUAQkRHHK9PRpNeJU,499
|
152
154
|
vng_api_common/notifications/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
153
155
|
vng_api_common/static/vng_api_common/css/screen.css,sha256=1Zt-yVFbm8qR9LakcYTF5x8p_91r8dMA-VRjIadYeP0,4674
|
154
156
|
vng_api_common/static/vng_api_common/css/admin/admin_overrides.css,sha256=81xuIyYfiTgq8O9h5NsSf-v8Sx885ICSEx1f-5fWy4U,1748
|
@@ -187,9 +189,9 @@ vng_api_common/templatetags/vng_api_common.py,sha256=j4Fu4rNErLcXStBbuZ9Zd_xD_ft
|
|
187
189
|
vng_api_common/tests/__init__.py,sha256=zBI2etwu2lEkKkIN1_cPuugJgPcoohQuJbdiUGtoceU,426
|
188
190
|
vng_api_common/tests/auth.py,sha256=IKDWTEFv4Bign4F70-ibsFcnJqRxEJaXvqaPQJWa1xY,4544
|
189
191
|
vng_api_common/tests/caching.py,sha256=zfIw5cRRvO9cekHZZKfRqZc8cx5IfJUYNmcH6cuIMg4,624
|
190
|
-
vng_api_common/tests/schema.py,sha256=
|
192
|
+
vng_api_common/tests/schema.py,sha256=irt_kIp1uJsnYIJlrpk0-qM9W6i44WyX3GSiJvGUfdU,2288
|
191
193
|
vng_api_common/tests/urls.py,sha256=PFrYzQbBC0TFPMEn3uPhcBG0IQs9JsEPqckicJT1UA4,2159
|
192
|
-
commonground_api_common-
|
193
|
-
commonground_api_common-
|
194
|
-
commonground_api_common-
|
195
|
-
commonground_api_common-
|
194
|
+
commonground_api_common-2.0.0.dist-info/METADATA,sha256=hkvF9wlMO9qfbWy3oEhGKRQpuVRJUuzzODv5Rx4Ogic,6416
|
195
|
+
commonground_api_common-2.0.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
196
|
+
commonground_api_common-2.0.0.dist-info/top_level.txt,sha256=vPismc83zPzWXTmlNCCwfDlFV9iygJYxNJW5iDjKTgw,15
|
197
|
+
commonground_api_common-2.0.0.dist-info/RECORD,,
|
vng_api_common/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "2.0.0"
|
vng_api_common/admin.py
CHANGED
@@ -1,29 +1,10 @@
|
|
1
1
|
from django.contrib import admin
|
2
2
|
from django.utils.translation import gettext_lazy as _
|
3
3
|
|
4
|
-
from .models import
|
4
|
+
from .models import JWTSecret
|
5
5
|
|
6
6
|
|
7
7
|
@admin.register(JWTSecret)
|
8
8
|
class JWTSecretAdmin(admin.ModelAdmin):
|
9
9
|
list_display = ("identifier",)
|
10
10
|
search_fields = ("identifier",)
|
11
|
-
|
12
|
-
|
13
|
-
@admin.register(APICredential)
|
14
|
-
class APICredentialAdmin(admin.ModelAdmin):
|
15
|
-
list_display = ("label", "api_root", "client_id", "user_id")
|
16
|
-
search_fields = ("label", "api_root")
|
17
|
-
fieldsets = (
|
18
|
-
(_("external API"), {"fields": ["api_root", "label"]}),
|
19
|
-
(
|
20
|
-
_("credentials"),
|
21
|
-
{
|
22
|
-
"description": _(
|
23
|
-
"Credentials that indicate how this API or application identifies itself at the external "
|
24
|
-
"API."
|
25
|
-
),
|
26
|
-
"fields": ["client_id", "secret", "user_id", "user_representation"],
|
27
|
-
},
|
28
|
-
),
|
29
|
-
)
|
@@ -7,7 +7,7 @@ from .models import Applicatie, AuthorizationsConfig, Autorisatie
|
|
7
7
|
|
8
8
|
@admin.register(AuthorizationsConfig)
|
9
9
|
class AuthorizationsConfigAdmin(SingletonModelAdmin):
|
10
|
-
list_display = ("
|
10
|
+
list_display = ("authorizations_api_service", "component")
|
11
11
|
|
12
12
|
|
13
13
|
@admin.register(Autorisatie)
|
@@ -8,9 +8,10 @@ from django.utils.translation import gettext as _
|
|
8
8
|
|
9
9
|
import jwt
|
10
10
|
from djangorestframework_camel_case.util import underscoreize
|
11
|
+
from requests import RequestException
|
11
12
|
from rest_framework.exceptions import PermissionDenied
|
12
|
-
from zds_client.client import ClientError
|
13
13
|
|
14
|
+
from vng_api_common.client import ClientError, to_internal_data
|
14
15
|
from vng_api_common.constants import VertrouwelijkheidsAanduiding
|
15
16
|
|
16
17
|
from ..models import JWTSecret
|
@@ -51,10 +52,18 @@ class JWTAuth:
|
|
51
52
|
|
52
53
|
def _request_auth(self) -> list:
|
53
54
|
client = AuthorizationsConfig.get_client()
|
55
|
+
|
56
|
+
if not client:
|
57
|
+
logger.warning("Authorization component can't be accessed")
|
58
|
+
return []
|
59
|
+
|
54
60
|
try:
|
55
|
-
response = client.
|
56
|
-
|
57
|
-
)
|
61
|
+
response = client.get("applicaties", params={"clientIds": self.client_id})
|
62
|
+
|
63
|
+
data = to_internal_data(response)
|
64
|
+
except RequestException:
|
65
|
+
logger.warning("Authorization component can't be accessed")
|
66
|
+
return []
|
58
67
|
except ClientError as exc:
|
59
68
|
response = exc.args[0]
|
60
69
|
# friendly debug - hint at where the problem is located
|
@@ -64,10 +73,10 @@ class JWTAuth:
|
|
64
73
|
"authorizations could not be retrieved"
|
65
74
|
)
|
66
75
|
raise PermissionDenied(detail=detail, code="not_authenticated_for_ac")
|
67
|
-
logger.
|
76
|
+
logger.warning("Authorization component can't be accessed")
|
68
77
|
return []
|
69
78
|
|
70
|
-
return underscoreize(
|
79
|
+
return underscoreize(data["results"])
|
71
80
|
|
72
81
|
def _get_auth(self):
|
73
82
|
return Applicatie.objects.filter(client_ids__contains=[self.client_id])
|
vng_api_common/authorizations/migrations/0016_remove_authorizationsconfig_api_root_and_more.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Generated by Django 5.1.2 on 2024-10-25 14:07
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import django.db.models.deletion
|
6
|
+
from django.db import migrations, models
|
7
|
+
from django.utils.text import slugify
|
8
|
+
|
9
|
+
from zgw_consumers.constants import APITypes, AuthTypes
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def migrate_authorization_config_to_service(apps, _) -> None:
|
15
|
+
AuthorizationsConfig = apps.get_model("authorizations", "AuthorizationsConfig")
|
16
|
+
Service = apps.get_model("zgw_consumers", "Service")
|
17
|
+
|
18
|
+
service_label = "Authorization API service"
|
19
|
+
|
20
|
+
config, _ = AuthorizationsConfig.objects.get_or_create()
|
21
|
+
service, created = Service.objects.get_or_create(
|
22
|
+
api_root=config.api_root,
|
23
|
+
defaults=dict(
|
24
|
+
label="Authorization API service",
|
25
|
+
slug=slugify(service_label),
|
26
|
+
api_type=APITypes.ac,
|
27
|
+
auth_type=AuthTypes.zgw,
|
28
|
+
),
|
29
|
+
)
|
30
|
+
config.authorizations_api_service = service
|
31
|
+
config.save()
|
32
|
+
|
33
|
+
if created:
|
34
|
+
logger.info(f"Created new Service for {config.api_root}")
|
35
|
+
else:
|
36
|
+
logger.info(f"Existing service found for {config.api_root}")
|
37
|
+
|
38
|
+
|
39
|
+
def migrate_authorization_config_to_config(apps, _) -> None:
|
40
|
+
AuthorizationsConfig = apps.get_model("authorizations", "AuthorizationsConfig")
|
41
|
+
|
42
|
+
config, _ = AuthorizationsConfig.objects.get_or_create()
|
43
|
+
if config.authorizations_api_service:
|
44
|
+
config.api_root = config.authorizations_api_service.api_root
|
45
|
+
config.save()
|
46
|
+
|
47
|
+
|
48
|
+
class Migration(migrations.Migration):
|
49
|
+
|
50
|
+
dependencies = [
|
51
|
+
("authorizations", "0015_auto_20220318_1608"),
|
52
|
+
("zgw_consumers", "0022_set_default_service_slug"),
|
53
|
+
]
|
54
|
+
|
55
|
+
operations = [
|
56
|
+
migrations.AddField(
|
57
|
+
model_name="authorizationsconfig",
|
58
|
+
name="authorizations_api_service",
|
59
|
+
field=models.ForeignKey(
|
60
|
+
blank=True,
|
61
|
+
limit_choices_to={"api_type": "ac", "auth_type": "zgw"},
|
62
|
+
null=True,
|
63
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
64
|
+
to="zgw_consumers.service",
|
65
|
+
verbose_name="autorisations api service",
|
66
|
+
),
|
67
|
+
),
|
68
|
+
migrations.RunPython(
|
69
|
+
migrate_authorization_config_to_service,
|
70
|
+
reverse_code=migrate_authorization_config_to_config,
|
71
|
+
),
|
72
|
+
migrations.RemoveField(
|
73
|
+
model_name="authorizationsconfig",
|
74
|
+
name="api_root",
|
75
|
+
),
|
76
|
+
]
|
@@ -1,17 +1,28 @@
|
|
1
1
|
import uuid
|
2
|
+
from typing import Optional
|
2
3
|
|
3
4
|
from django.contrib.postgres.fields import ArrayField
|
4
5
|
from django.db import models
|
5
6
|
from django.utils.translation import gettext_lazy as _
|
6
7
|
|
8
|
+
from solo.models import SingletonModel
|
9
|
+
from zgw_consumers.constants import APITypes, AuthTypes
|
10
|
+
|
11
|
+
from vng_api_common.client import Client, get_client
|
12
|
+
|
7
13
|
from ..constants import ComponentTypes, VertrouwelijkheidsAanduiding
|
8
14
|
from ..decorators import field_default
|
9
15
|
from ..fields import VertrouwelijkheidsAanduidingField
|
10
|
-
from ..models import APIMixin
|
16
|
+
from ..models import APIMixin
|
17
|
+
|
11
18
|
|
19
|
+
class AuthorizationsConfigManager(models.Manager):
|
20
|
+
def get_queryset(self):
|
21
|
+
qs = super().get_queryset()
|
22
|
+
return qs.select_related("authorizations_api_service")
|
12
23
|
|
13
|
-
|
14
|
-
class AuthorizationsConfig(
|
24
|
+
|
25
|
+
class AuthorizationsConfig(SingletonModel):
|
15
26
|
component = models.CharField(
|
16
27
|
_("component"),
|
17
28
|
max_length=50,
|
@@ -19,9 +30,33 @@ class AuthorizationsConfig(ClientConfig):
|
|
19
30
|
default=ComponentTypes.zrc,
|
20
31
|
)
|
21
32
|
|
33
|
+
authorizations_api_service = models.ForeignKey(
|
34
|
+
"zgw_consumers.Service",
|
35
|
+
limit_choices_to=dict(
|
36
|
+
api_type=APITypes.ac,
|
37
|
+
auth_type=AuthTypes.zgw,
|
38
|
+
),
|
39
|
+
verbose_name=_("autorisations api service"),
|
40
|
+
on_delete=models.SET_NULL,
|
41
|
+
blank=True,
|
42
|
+
null=True,
|
43
|
+
)
|
44
|
+
|
45
|
+
objects = AuthorizationsConfigManager()
|
46
|
+
|
22
47
|
class Meta:
|
23
48
|
verbose_name = _("Autorisatiecomponentconfiguratie")
|
24
49
|
|
50
|
+
@classmethod
|
51
|
+
def get_client(cls) -> Optional[Client]:
|
52
|
+
"""
|
53
|
+
Construct a client, prepared with the required auth.
|
54
|
+
"""
|
55
|
+
config = cls.get_solo()
|
56
|
+
if config.authorizations_api_service:
|
57
|
+
return get_client(config.authorizations_api_service.api_root)
|
58
|
+
return None
|
59
|
+
|
25
60
|
|
26
61
|
class ApplicatieManager(models.Manager):
|
27
62
|
def get_by_natural_key(self, uuid):
|
@@ -0,0 +1,17 @@
|
|
1
|
+
def generate_jwt(client_id, secret, user_id, user_representation):
|
2
|
+
from zgw_consumers.client import ZGWAuth
|
3
|
+
|
4
|
+
class FakeService:
|
5
|
+
def __init__(self, **kwargs):
|
6
|
+
for key, value in kwargs.items():
|
7
|
+
setattr(self, key, value)
|
8
|
+
|
9
|
+
auth = ZGWAuth(
|
10
|
+
service=FakeService( # type: ignore
|
11
|
+
client_id=client_id,
|
12
|
+
secret=secret,
|
13
|
+
user_id=user_id,
|
14
|
+
user_representation=user_representation,
|
15
|
+
)
|
16
|
+
)
|
17
|
+
return f"Bearer {auth._token}"
|
vng_api_common/client.py
CHANGED
@@ -1,44 +1,73 @@
|
|
1
1
|
"""
|
2
|
-
Interface to get a
|
2
|
+
Interface to get a client object for a given URL.
|
3
3
|
"""
|
4
4
|
|
5
|
-
|
5
|
+
import logging
|
6
|
+
from typing import Any, Optional
|
6
7
|
|
7
|
-
from django.apps import apps
|
8
8
|
from django.conf import settings
|
9
9
|
from django.utils.module_loading import import_string
|
10
10
|
|
11
|
-
from
|
11
|
+
from ape_pie import APIClient
|
12
|
+
from requests import JSONDecodeError, RequestException, Response
|
12
13
|
|
14
|
+
logger = logging.getLogger(__name__)
|
13
15
|
|
14
|
-
def get_client(url: str, url_is_api_root=False) -> Optional[Client]:
|
15
|
-
"""
|
16
|
-
Get a client instance for the given URL.
|
17
16
|
|
18
|
-
|
19
|
-
|
17
|
+
class ClientError(RuntimeError):
|
18
|
+
pass
|
20
19
|
|
21
|
-
If no suitable client is found, ``None`` is returned.
|
22
|
-
"""
|
23
|
-
custom_client_fetcher = getattr(settings, "CUSTOM_CLIENT_FETCHER", None)
|
24
|
-
if custom_client_fetcher:
|
25
|
-
client_getter = import_string(custom_client_fetcher)
|
26
|
-
return client_getter(url)
|
27
20
|
|
28
|
-
|
29
|
-
|
21
|
+
# TODO: use more approriate method name
|
22
|
+
def to_internal_data(response: Response) -> dict | list | None:
|
23
|
+
try:
|
24
|
+
response_json = response.json()
|
25
|
+
except JSONDecodeError:
|
26
|
+
logger.exception("Unable to parse json from response")
|
27
|
+
response_json = None
|
30
28
|
|
31
|
-
|
32
|
-
|
29
|
+
try:
|
30
|
+
response.raise_for_status()
|
31
|
+
except RequestException as exc:
|
32
|
+
if response.status_code >= 500:
|
33
|
+
raise
|
34
|
+
raise ClientError(response_json if response_json is not None else {}) from exc
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
assert response_json
|
37
|
+
return response_json
|
38
|
+
|
39
|
+
|
40
|
+
class Client(APIClient):
|
41
|
+
def request(
|
42
|
+
self, method: str | bytes, url: str | bytes, *args, **kwargs
|
43
|
+
) -> Response:
|
44
|
+
|
45
|
+
headers = kwargs.pop("headers", {})
|
46
|
+
headers.setdefault("Accept", "application/json")
|
47
|
+
headers.setdefault("Content-Type", "application/json")
|
48
|
+
|
49
|
+
kwargs["headers"] = headers
|
50
|
+
|
51
|
+
data = kwargs.get("data", {})
|
37
52
|
|
38
|
-
|
53
|
+
if data:
|
54
|
+
kwargs["json"] = data
|
39
55
|
|
40
|
-
|
41
|
-
|
56
|
+
return super().request(method, url, *args, **kwargs)
|
57
|
+
|
58
|
+
|
59
|
+
def get_client(url: str) -> Client | None:
|
60
|
+
"""
|
61
|
+
Get a client instance for the given URL.
|
62
|
+
If no suitable client is found, ``None`` is returned.
|
63
|
+
"""
|
64
|
+
from zgw_consumers.client import build_client
|
65
|
+
from zgw_consumers.models import Service
|
66
|
+
|
67
|
+
service: Optional[Service] = Service.get_service(url)
|
68
|
+
|
69
|
+
if not service:
|
70
|
+
logger.warning(f"No service configured for {url}")
|
71
|
+
return None
|
42
72
|
|
43
|
-
|
44
|
-
return client
|
73
|
+
return build_client(service, client_factory=Client)
|
vng_api_common/conf/api.py
CHANGED
@@ -4,7 +4,6 @@ __all__ = [
|
|
4
4
|
"BASE_SWAGGER_SETTINGS",
|
5
5
|
"COMMON_SPEC",
|
6
6
|
"LINK_FETCHER",
|
7
|
-
"ZDS_CLIENT_CLASS",
|
8
7
|
"GEMMA_URL_TEMPLATE",
|
9
8
|
"GEMMA_URL_COMPONENTTYPE",
|
10
9
|
"GEMMA_URL_INFORMATIEMODEL",
|
@@ -94,8 +93,6 @@ REDOC_SETTINGS = {"EXPAND_RESPONSES": "200,201", "SPEC_URL": "openapi.json"}
|
|
94
93
|
# See: https://github.com/Rebilly/ReDoc#redoc-options-object
|
95
94
|
LINK_FETCHER = "requests.get"
|
96
95
|
|
97
|
-
ZDS_CLIENT_CLASS = "zds_client.Client"
|
98
|
-
|
99
96
|
GEMMA_URL_TEMPLATE = "https://www.gemmaonline.nl/index.php/{informatiemodel}_{versie}/doc/{componenttype}/{component}"
|
100
97
|
GEMMA_URL_COMPONENTTYPE = "objecttype"
|
101
98
|
GEMMA_URL_INFORMATIEMODEL = "Rgbz"
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# Generated by Django 5.1.2 on 2024-10-24 13:51
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Set
|
5
|
+
|
6
|
+
from django.db import migrations, models
|
7
|
+
from django.utils.text import slugify
|
8
|
+
|
9
|
+
from zgw_consumers.constants import APITypes, AuthTypes
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def _get_api_type(api_root: str) -> APITypes:
|
15
|
+
mapping = {
|
16
|
+
"/autorisaties/api/": APITypes.ac,
|
17
|
+
"/zaken/api/": APITypes.zrc,
|
18
|
+
"/catalogi/api/": APITypes.ztc,
|
19
|
+
"/documenten/api/": APITypes.drc,
|
20
|
+
"/besluiten/api/": APITypes.drc,
|
21
|
+
}
|
22
|
+
|
23
|
+
for path, _type in mapping.items():
|
24
|
+
if path in api_root.lower():
|
25
|
+
return _type
|
26
|
+
|
27
|
+
return APITypes.orc
|
28
|
+
|
29
|
+
|
30
|
+
def _get_service_slug(credential: models.Model, existing_slugs: Set[str]) -> str:
|
31
|
+
default_slug: str = slugify(credential.label)
|
32
|
+
|
33
|
+
if default_slug not in existing_slugs or not existing_slugs:
|
34
|
+
return default_slug
|
35
|
+
|
36
|
+
count = 2
|
37
|
+
slug = f"{default_slug}-{count}"
|
38
|
+
|
39
|
+
while slug in existing_slugs:
|
40
|
+
count += 1
|
41
|
+
slug = f"{default_slug}-{count}"
|
42
|
+
|
43
|
+
return slug
|
44
|
+
|
45
|
+
|
46
|
+
def migrate_credentials_to_service(apps, _) -> None:
|
47
|
+
APICredential = apps.get_model("vng_api_common", "APICredential")
|
48
|
+
Service = apps.get_model("zgw_consumers", "Service")
|
49
|
+
|
50
|
+
credentials = APICredential.objects.all()
|
51
|
+
|
52
|
+
existings_service_slugs = set(Service.objects.values_list("slug", flat=True))
|
53
|
+
|
54
|
+
for credential in credentials:
|
55
|
+
logger.info(f"Creating Service for {credential.client_id}")
|
56
|
+
|
57
|
+
service_slug = _get_service_slug(credential, existings_service_slugs)
|
58
|
+
|
59
|
+
_, created = Service.objects.get_or_create(
|
60
|
+
api_root=credential.api_root,
|
61
|
+
defaults=dict(
|
62
|
+
label=credential.label,
|
63
|
+
slug=service_slug,
|
64
|
+
api_type=_get_api_type(credential.api_root),
|
65
|
+
auth_type=AuthTypes.zgw,
|
66
|
+
client_id=credential.client_id,
|
67
|
+
secret=credential.secret,
|
68
|
+
user_id=credential.user_id,
|
69
|
+
user_representation=credential.user_representation,
|
70
|
+
),
|
71
|
+
)
|
72
|
+
|
73
|
+
existings_service_slugs.add(service_slug)
|
74
|
+
|
75
|
+
if created:
|
76
|
+
logger.info(f"Created new Service for {credential.api_root}")
|
77
|
+
else:
|
78
|
+
logger.info(f"Existing service found for {credential.api_root}")
|
79
|
+
|
80
|
+
|
81
|
+
def migrate_service_to_credentials(apps, _) -> None:
|
82
|
+
APICredential = apps.get_model("vng_api_common", "APICredential")
|
83
|
+
Service = apps.get_model("zgw_consumers", "Service")
|
84
|
+
|
85
|
+
services = Service.objects.filter(auth_type=AuthTypes.zgw)
|
86
|
+
|
87
|
+
for service in services:
|
88
|
+
logger.info(f"Creating APICredentials for {service.client_id}")
|
89
|
+
|
90
|
+
_, created = APICredential.objects.get_or_create(
|
91
|
+
api_root=service.api_root,
|
92
|
+
defaults=dict(
|
93
|
+
label=f"Migrated credentials for {service.client_id}",
|
94
|
+
client_id=service.client_id,
|
95
|
+
secret=service.secret,
|
96
|
+
user_id=service.user_id,
|
97
|
+
user_representation=service.user_representation,
|
98
|
+
),
|
99
|
+
)
|
100
|
+
if created:
|
101
|
+
logger.info(f"Created new APICredentials for {service.api_root}")
|
102
|
+
else:
|
103
|
+
logger.info(f"Existing APICredentials found for {service.api_root}")
|
104
|
+
|
105
|
+
|
106
|
+
class Migration(migrations.Migration):
|
107
|
+
|
108
|
+
dependencies = [
|
109
|
+
("vng_api_common", "0005_auto_20190614_1346"),
|
110
|
+
]
|
111
|
+
|
112
|
+
operations = [
|
113
|
+
migrations.RunPython(
|
114
|
+
migrate_credentials_to_service, reverse_code=migrate_service_to_credentials
|
115
|
+
),
|
116
|
+
migrations.DeleteModel(
|
117
|
+
name="APICredential",
|
118
|
+
),
|
119
|
+
]
|
vng_api_common/mocks.py
CHANGED
vng_api_common/models.py
CHANGED
@@ -1,15 +1,7 @@
|
|
1
|
-
from typing import Optional, Union
|
2
|
-
from urllib.parse import urlsplit, urlunsplit
|
3
|
-
|
4
1
|
from django.db import models
|
5
|
-
from django.db.models.functions import Length
|
6
2
|
from django.utils.translation import gettext_lazy as _
|
7
3
|
|
8
4
|
from rest_framework.reverse import reverse
|
9
|
-
from solo.models import SingletonModel
|
10
|
-
from zds_client import Client, ClientAuth
|
11
|
-
|
12
|
-
from .client import get_client as _get_client
|
13
5
|
|
14
6
|
|
15
7
|
class APIMixin:
|
@@ -69,106 +61,3 @@ class JWTSecret(models.Model):
|
|
69
61
|
|
70
62
|
def __str__(self):
|
71
63
|
return self.identifier
|
72
|
-
|
73
|
-
|
74
|
-
class APICredential(models.Model):
|
75
|
-
"""
|
76
|
-
Store credentials for external APIs.
|
77
|
-
|
78
|
-
When we need to authenticate against a remote API, we need to know which
|
79
|
-
client ID and secret to use to sign the JWT.
|
80
|
-
"""
|
81
|
-
|
82
|
-
api_root = models.URLField(
|
83
|
-
_("API-root"),
|
84
|
-
unique=True,
|
85
|
-
help_text=_(
|
86
|
-
"URL of the external API, ending in a trailing slash. Example: https://example.com/api/v1/"
|
87
|
-
),
|
88
|
-
)
|
89
|
-
label = models.CharField(
|
90
|
-
_("label"),
|
91
|
-
max_length=100,
|
92
|
-
default="",
|
93
|
-
help_text=_("Human readable label of the external API."),
|
94
|
-
)
|
95
|
-
client_id = models.CharField(
|
96
|
-
_("client ID"),
|
97
|
-
max_length=255,
|
98
|
-
help_text=_("Client ID to identify this API at the external API."),
|
99
|
-
)
|
100
|
-
secret = models.CharField(
|
101
|
-
_("secret"), max_length=255, help_text=_("Secret belonging to the client ID.")
|
102
|
-
)
|
103
|
-
user_id = models.CharField(
|
104
|
-
_("user ID"),
|
105
|
-
max_length=255,
|
106
|
-
help_text=_(
|
107
|
-
"User ID to use for the audit trail. Although these external API credentials are typically used by"
|
108
|
-
"this API itself instead of a user, the user ID is required."
|
109
|
-
),
|
110
|
-
)
|
111
|
-
user_representation = models.CharField(
|
112
|
-
_("user representation"),
|
113
|
-
max_length=255,
|
114
|
-
default="",
|
115
|
-
help_text=_("Human readable representation of the user."),
|
116
|
-
)
|
117
|
-
|
118
|
-
class Meta:
|
119
|
-
verbose_name = _("external API credential")
|
120
|
-
verbose_name_plural = _("external API credentials")
|
121
|
-
|
122
|
-
def __str__(self):
|
123
|
-
return self.api_root
|
124
|
-
|
125
|
-
@classmethod
|
126
|
-
def get_auth(cls, url: str, **kwargs) -> Union[ClientAuth, None]:
|
127
|
-
split_url = urlsplit(url)
|
128
|
-
scheme_and_domain = urlunsplit(split_url[:2] + ("", "", ""))
|
129
|
-
|
130
|
-
candidates = (
|
131
|
-
cls.objects.filter(api_root__startswith=scheme_and_domain)
|
132
|
-
.annotate(api_root_length=Length("api_root"))
|
133
|
-
.order_by("-api_root_length")
|
134
|
-
)
|
135
|
-
|
136
|
-
# select the one matching
|
137
|
-
for candidate in candidates.iterator():
|
138
|
-
if url.startswith(candidate.api_root):
|
139
|
-
credentials = candidate
|
140
|
-
break
|
141
|
-
else:
|
142
|
-
return None
|
143
|
-
|
144
|
-
auth = ClientAuth(
|
145
|
-
client_id=credentials.client_id,
|
146
|
-
secret=credentials.secret,
|
147
|
-
user_id=credentials.user_id,
|
148
|
-
user_representation=credentials.user_representation,
|
149
|
-
**kwargs,
|
150
|
-
)
|
151
|
-
return auth
|
152
|
-
|
153
|
-
|
154
|
-
class ClientConfig(SingletonModel):
|
155
|
-
api_root = models.URLField(_("api root"), unique=True)
|
156
|
-
|
157
|
-
class Meta:
|
158
|
-
abstract = True
|
159
|
-
|
160
|
-
def __str__(self):
|
161
|
-
return self.api_root
|
162
|
-
|
163
|
-
def save(self, *args, **kwargs):
|
164
|
-
if not self.api_root.endswith("/"):
|
165
|
-
self.api_root = f"{self.api_root}/"
|
166
|
-
super().save(*args, **kwargs)
|
167
|
-
|
168
|
-
@classmethod
|
169
|
-
def get_client(cls) -> Optional[Client]:
|
170
|
-
"""
|
171
|
-
Construct a client, prepared with the required auth.
|
172
|
-
"""
|
173
|
-
config = cls.get_solo()
|
174
|
-
return _get_client(config.api_root, url_is_api_root=True)
|
@@ -3,6 +3,7 @@ from django.utils.module_loading import import_string
|
|
3
3
|
|
4
4
|
from drf_yasg.utils import swagger_auto_schema
|
5
5
|
from notifications_api_common.api.serializers import NotificatieSerializer
|
6
|
+
from notifications_api_common.constants import SCOPE_NOTIFICATIES_PUBLICEREN_LABEL
|
6
7
|
from rest_framework import status
|
7
8
|
from rest_framework.response import Response
|
8
9
|
from rest_framework.views import APIView
|
@@ -10,7 +11,6 @@ from rest_framework.views import APIView
|
|
10
11
|
from ...permissions import AuthScopesRequired
|
11
12
|
from ...scopes import Scope
|
12
13
|
from ...serializers import FoutSerializer, ValidatieFoutSerializer
|
13
|
-
from ..constants import SCOPE_NOTIFICATIES_PUBLICEREN_LABEL
|
14
14
|
|
15
15
|
|
16
16
|
class NotificationBaseView(APIView):
|
@@ -4,7 +4,7 @@ from djangorestframework_camel_case.util import underscoreize
|
|
4
4
|
|
5
5
|
from ..authorizations.models import Applicatie
|
6
6
|
from ..authorizations.serializers import ApplicatieUuidSerializer
|
7
|
-
from ..client import get_client
|
7
|
+
from ..client import get_client, to_internal_data
|
8
8
|
from ..constants import CommonResourceAction
|
9
9
|
from ..utils import get_uuid_from_path
|
10
10
|
|
@@ -20,8 +20,13 @@ class LoggingHandler:
|
|
20
20
|
class AuthHandler:
|
21
21
|
def _request_auth(self, url: str) -> dict:
|
22
22
|
client = get_client(url)
|
23
|
-
|
24
|
-
|
23
|
+
|
24
|
+
if not client:
|
25
|
+
return {}
|
26
|
+
|
27
|
+
response = client.get(url)
|
28
|
+
data = to_internal_data(response)
|
29
|
+
return underscoreize(data)
|
25
30
|
|
26
31
|
def handle(self, message: dict) -> None:
|
27
32
|
uuid = get_uuid_from_path(message["resource_url"])
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Generated by Django 5.1.2 on 2024-10-25 14:07
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
("notifications", "0010_auto_20220704_1419"),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RemoveField(
|
14
|
+
model_name="subscription",
|
15
|
+
name="config",
|
16
|
+
),
|
17
|
+
migrations.DeleteModel(
|
18
|
+
name="NotificationsConfig",
|
19
|
+
),
|
20
|
+
migrations.DeleteModel(
|
21
|
+
name="Subscription",
|
22
|
+
),
|
23
|
+
]
|
vng_api_common/oas.py
CHANGED
vng_api_common/routers.py
CHANGED
@@ -8,7 +8,7 @@ class APIRootView(_APIRootView):
|
|
8
8
|
permission_classes = ()
|
9
9
|
|
10
10
|
|
11
|
-
class
|
11
|
+
class NestedRegisteringMixin:
|
12
12
|
_nested_router = None
|
13
13
|
|
14
14
|
def __init__(self, *args, **kwargs):
|
@@ -47,11 +47,11 @@ class ZDSNestedRegisteringMixin:
|
|
47
47
|
)
|
48
48
|
|
49
49
|
|
50
|
-
class NestedSimpleRouter(
|
50
|
+
class NestedSimpleRouter(NestedRegisteringMixin, routers.NestedSimpleRouter):
|
51
51
|
pass
|
52
52
|
|
53
53
|
|
54
|
-
class DefaultRouter(
|
54
|
+
class DefaultRouter(NestedRegisteringMixin, routers.DefaultRouter):
|
55
55
|
APIRootView = APIRootView
|
56
56
|
|
57
57
|
|
vng_api_common/tests/schema.py
CHANGED
@@ -5,6 +5,8 @@ from urllib.parse import urlparse
|
|
5
5
|
from django.conf import settings
|
6
6
|
|
7
7
|
import yaml
|
8
|
+
from requests_mock import Mocker
|
9
|
+
from zgw_consumers_oas.schema_loading import read_schema
|
8
10
|
|
9
11
|
DEFAULT_PATH_PARAMETERS = {"version": "1"}
|
10
12
|
|
@@ -69,3 +71,13 @@ def get_validation_errors(response, field, index=0):
|
|
69
71
|
return error
|
70
72
|
|
71
73
|
i += 1
|
74
|
+
|
75
|
+
|
76
|
+
def mock_service_oas_get(
|
77
|
+
mock: Mocker, url: str, service: str, oas_url: str = ""
|
78
|
+
) -> None:
|
79
|
+
if not oas_url:
|
80
|
+
oas_url = f"{url}schema/openapi.yaml?v=3"
|
81
|
+
|
82
|
+
content = read_schema(service)
|
83
|
+
mock.get(oas_url, content=content)
|
vng_api_common/utils.py
CHANGED
@@ -13,9 +13,6 @@ from django.utils.encoding import smart_str
|
|
13
13
|
from django.utils.module_loading import import_string
|
14
14
|
|
15
15
|
from rest_framework.utils import formatting
|
16
|
-
from zds_client.client import ClientError
|
17
|
-
|
18
|
-
from .client import get_client
|
19
16
|
|
20
17
|
try:
|
21
18
|
from djangorestframework_camel_case.util import (
|
@@ -186,25 +183,6 @@ def get_uuid_from_path(path: str) -> str:
|
|
186
183
|
return uuid_str
|
187
184
|
|
188
185
|
|
189
|
-
def request_object_attribute(
|
190
|
-
url: str, attribute: str, resource: Union[str, None] = None
|
191
|
-
) -> str:
|
192
|
-
client = get_client(url)
|
193
|
-
|
194
|
-
try:
|
195
|
-
result = client.retrieve(resource, url=url)[attribute]
|
196
|
-
except (ClientError, KeyError) as exc:
|
197
|
-
logger.warning(
|
198
|
-
"%s was retrieved from %s with the %s: %s",
|
199
|
-
attribute,
|
200
|
-
url,
|
201
|
-
exc.__class__.__name__,
|
202
|
-
exc,
|
203
|
-
)
|
204
|
-
result = ""
|
205
|
-
return result
|
206
|
-
|
207
|
-
|
208
186
|
def generate_unique_identification(instance: models.Model, date_field_name: str):
|
209
187
|
model = type(instance)
|
210
188
|
model_name = getattr(model, "IDENTIFICATIE_PREFIX", model._meta.model_name.upper())
|
vng_api_common/validators.py
CHANGED
@@ -11,10 +11,8 @@ from django.utils.deconstruct import deconstructible
|
|
11
11
|
from django.utils.module_loading import import_string
|
12
12
|
from django.utils.translation import gettext_lazy as _
|
13
13
|
|
14
|
-
import requests
|
15
14
|
from rest_framework import serializers, validators
|
16
15
|
|
17
|
-
from .client import get_client
|
18
16
|
from .constants import RSIN_LENGTH
|
19
17
|
from .oas import fetcher, obj_has_shape
|
20
18
|
|
@@ -221,42 +219,6 @@ class InformatieObjectUniqueValidator(validators.UniqueTogetherValidator):
|
|
221
219
|
super().__call__(attrs)
|
222
220
|
|
223
221
|
|
224
|
-
class ObjectInformatieObjectValidator:
|
225
|
-
"""
|
226
|
-
Validate that the INFORMATIEOBJECT is linked already in the DRC.
|
227
|
-
"""
|
228
|
-
|
229
|
-
message = _(
|
230
|
-
"Het informatieobject is in het DRC nog niet gerelateerd aan dit object."
|
231
|
-
)
|
232
|
-
code = "inconsistent-relation"
|
233
|
-
requires_context = True
|
234
|
-
|
235
|
-
def __call__(self, informatieobject: str, serializer):
|
236
|
-
object_url = serializer.context["parent_object"].get_absolute_api_url(
|
237
|
-
self.request
|
238
|
-
)
|
239
|
-
|
240
|
-
# dynamic so that it can be mocked in tests easily
|
241
|
-
client = get_client(informatieobject)
|
242
|
-
|
243
|
-
try:
|
244
|
-
oios = client.list(
|
245
|
-
"objectinformatieobject",
|
246
|
-
query_params={
|
247
|
-
"informatieobject": informatieobject,
|
248
|
-
"object": object_url,
|
249
|
-
},
|
250
|
-
)
|
251
|
-
except requests.HTTPError as exc:
|
252
|
-
raise serializers.ValidationError(
|
253
|
-
exc.args[0], code="relation-validation-error"
|
254
|
-
) from exc
|
255
|
-
|
256
|
-
if len(oios) == 0:
|
257
|
-
raise serializers.ValidationError(self.message, code=self.code)
|
258
|
-
|
259
|
-
|
260
222
|
@deconstructible
|
261
223
|
class UntilNowValidator:
|
262
224
|
"""
|
vng_api_common/views.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
from collections import OrderedDict
|
4
|
+
from typing import Optional
|
4
5
|
|
5
6
|
from django.apps import apps
|
6
7
|
from django.conf import settings
|
@@ -13,7 +14,8 @@ import requests
|
|
13
14
|
from rest_framework import exceptions as drf_exceptions, status
|
14
15
|
from rest_framework.response import Response
|
15
16
|
from rest_framework.views import exception_handler as drf_exception_handler
|
16
|
-
|
17
|
+
|
18
|
+
from vng_api_common.client import Client, ClientError
|
17
19
|
|
18
20
|
from . import exceptions
|
19
21
|
from .compat import sentry_client
|
@@ -128,12 +130,20 @@ def _test_ac_config() -> list:
|
|
128
130
|
auth_config = AuthorizationsConfig.get_solo()
|
129
131
|
|
130
132
|
# check if AC auth is configured
|
131
|
-
ac_client = AuthorizationsConfig.get_client()
|
132
|
-
has_ac_auth = ac_client.auth is not None
|
133
|
+
ac_client: Optional[Client] = AuthorizationsConfig.get_client()
|
134
|
+
has_ac_auth = ac_client.auth is not None if ac_client else False
|
133
135
|
|
134
136
|
checks = [
|
135
137
|
(_("Type of component"), auth_config.get_component_display(), None),
|
136
|
-
(
|
138
|
+
(
|
139
|
+
_("AC"),
|
140
|
+
(
|
141
|
+
auth_config.authorizations_api_service.api_root
|
142
|
+
if ac_client
|
143
|
+
else _("Missing")
|
144
|
+
),
|
145
|
+
bool(ac_client),
|
146
|
+
),
|
137
147
|
(
|
138
148
|
_("Credentials for AC"),
|
139
149
|
_("Configured") if has_ac_auth else _("Missing"),
|
@@ -145,18 +155,17 @@ def _test_ac_config() -> list:
|
|
145
155
|
if has_ac_auth:
|
146
156
|
error = False
|
147
157
|
|
158
|
+
client_id = ac_client.auth.service.client_id
|
159
|
+
|
148
160
|
try:
|
149
|
-
ac_client.
|
150
|
-
"
|
161
|
+
response: requests.Response = ac_client.get(
|
162
|
+
"applicaties", params={"clientIds": client_id}
|
151
163
|
)
|
152
|
-
|
164
|
+
|
165
|
+
response.raise_for_status()
|
166
|
+
except requests.RequestException:
|
153
167
|
error = True
|
154
168
|
message = _("Could not connect with AC")
|
155
|
-
except ClientError as exc:
|
156
|
-
error = True
|
157
|
-
message = _(
|
158
|
-
"Cannot retrieve authorizations: HTTP {status_code} - {error_code}"
|
159
|
-
).format(status_code=exc.args[0]["status"], error_code=exc.args[0]["code"])
|
160
169
|
else:
|
161
170
|
message = _("Can retrieve authorizations")
|
162
171
|
|
@@ -166,14 +175,13 @@ def _test_ac_config() -> list:
|
|
166
175
|
|
167
176
|
|
168
177
|
def _test_nrc_config() -> list:
|
169
|
-
if not apps.is_installed("
|
178
|
+
if not apps.is_installed("notifications_api_common"):
|
170
179
|
return []
|
171
180
|
|
172
181
|
from notifications_api_common.models import NotificationsConfig, Subscription
|
173
182
|
|
174
183
|
nrc_config = NotificationsConfig.get_solo()
|
175
|
-
|
176
|
-
nrc_client = NotificationsConfig.get_client()
|
184
|
+
nrc_client: Optional[Client] = NotificationsConfig.get_client()
|
177
185
|
|
178
186
|
has_nrc_auth = nrc_client.auth is not None if nrc_client else False
|
179
187
|
|
@@ -199,15 +207,11 @@ def _test_nrc_config() -> list:
|
|
199
207
|
error = False
|
200
208
|
|
201
209
|
try:
|
202
|
-
nrc_client.
|
203
|
-
|
210
|
+
response: requests.Response = nrc_client.get("kanaal")
|
211
|
+
response.raise_for_status()
|
212
|
+
except requests.RequestException:
|
204
213
|
error = True
|
205
214
|
message = _("Could not connect with NRC")
|
206
|
-
except ClientError as exc:
|
207
|
-
error = True
|
208
|
-
message = _(
|
209
|
-
"Cannot retrieve kanalen: HTTP {status_code} - {error_code}"
|
210
|
-
).format(status_code=exc.args[0]["status"], error_code=exc.args[0]["code"])
|
211
215
|
else:
|
212
216
|
message = _("Can retrieve kanalen")
|
213
217
|
|
@@ -1,97 +0,0 @@
|
|
1
|
-
import uuid
|
2
|
-
from urllib.parse import urljoin
|
3
|
-
|
4
|
-
from django.contrib.postgres.fields import ArrayField
|
5
|
-
from django.db import models
|
6
|
-
from django.utils.translation import gettext_lazy as _
|
7
|
-
|
8
|
-
from zds_client import ClientAuth
|
9
|
-
|
10
|
-
from ..client import get_client
|
11
|
-
from ..decorators import field_default
|
12
|
-
from ..models import APICredential, ClientConfig
|
13
|
-
|
14
|
-
|
15
|
-
@field_default("api_root", "https://notificaties-api.vng.cloud/api/v1/")
|
16
|
-
class NotificationsConfig(ClientConfig):
|
17
|
-
class Meta:
|
18
|
-
verbose_name = _("Notificatiescomponentconfiguratie")
|
19
|
-
|
20
|
-
def get_auth(self) -> ClientAuth:
|
21
|
-
auth = APICredential.get_auth(self.api_root)
|
22
|
-
return auth
|
23
|
-
|
24
|
-
|
25
|
-
class Subscription(models.Model):
|
26
|
-
"""
|
27
|
-
A single subscription.
|
28
|
-
|
29
|
-
TODO: on change/update, update the subscription
|
30
|
-
"""
|
31
|
-
|
32
|
-
config = models.ForeignKey("NotificationsConfig", on_delete=models.CASCADE)
|
33
|
-
|
34
|
-
callback_url = models.URLField(
|
35
|
-
_("callback url"), help_text=_("Where to send the notifications (webhook url)")
|
36
|
-
)
|
37
|
-
client_id = models.CharField(
|
38
|
-
_("client ID"),
|
39
|
-
max_length=50,
|
40
|
-
help_text=_("Client ID to construct the auth token"),
|
41
|
-
)
|
42
|
-
secret = models.CharField(
|
43
|
-
_("client secret"),
|
44
|
-
max_length=50,
|
45
|
-
help_text=_("Secret to construct the auth token"),
|
46
|
-
)
|
47
|
-
channels = ArrayField(
|
48
|
-
models.CharField(max_length=100),
|
49
|
-
verbose_name=_("channels"),
|
50
|
-
help_text=_("Comma-separated list of channels to subscribe to"),
|
51
|
-
)
|
52
|
-
|
53
|
-
_subscription = models.URLField(
|
54
|
-
_("NC subscription"),
|
55
|
-
blank=True,
|
56
|
-
editable=False,
|
57
|
-
help_text=_("Subscription as it is known in the NC"),
|
58
|
-
)
|
59
|
-
|
60
|
-
class Meta:
|
61
|
-
verbose_name = _("Webhook subscription")
|
62
|
-
verbose_name_plural = _("Webhook subscriptions")
|
63
|
-
|
64
|
-
def __str__(self):
|
65
|
-
return f"{', '.join(self.channels)} - {self.callback_url}"
|
66
|
-
|
67
|
-
def register(self) -> None:
|
68
|
-
"""
|
69
|
-
Registers the webhook with the notification component.
|
70
|
-
"""
|
71
|
-
dummy_detail_url = urljoin(self.config.api_root, f"foo/{uuid.uuid4()}")
|
72
|
-
client = get_client(dummy_detail_url)
|
73
|
-
|
74
|
-
# This authentication is for the NC to call us. Thus, it's *not* for
|
75
|
-
# calling the NC to create a subscription.
|
76
|
-
self_auth = ClientAuth(
|
77
|
-
client_id=self.client_id,
|
78
|
-
secret=self.secret,
|
79
|
-
)
|
80
|
-
data = {
|
81
|
-
"callbackUrl": self.callback_url,
|
82
|
-
"auth": self_auth.credentials()["Authorization"],
|
83
|
-
"kanalen": [
|
84
|
-
{
|
85
|
-
"naam": channel,
|
86
|
-
# FIXME: You need to be able to configure these.
|
87
|
-
"filters": {},
|
88
|
-
}
|
89
|
-
for channel in self.channels
|
90
|
-
],
|
91
|
-
}
|
92
|
-
|
93
|
-
# register the subscriber
|
94
|
-
subscriber = client.create("abonnement", data=data)
|
95
|
-
|
96
|
-
self._subscription = subscriber["url"]
|
97
|
-
self.save(update_fields=["_subscription"])
|
{commonground_api_common-1.13.4.data → commonground_api_common-2.0.0.data}/scripts/generate_schema
RENAMED
File without changes
|
File without changes
|
File without changes
|
{commonground_api_common-1.13.4.dist-info → commonground_api_common-2.0.0.dist-info}/top_level.txt
RENAMED
File without changes
|