From 992b99e5bade91ac5cac3487e55a8e56509bb5de Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 7 Jul 2026 15:49:06 +0200 Subject: [PATCH] Release v0.1.5 --- .gitea/ISSUE_TEMPLATE/bug_report.md | 35 ++++ .gitea/ISSUE_TEMPLATE/config.yaml | 1 + .gitea/ISSUE_TEMPLATE/docs_workflow.md | 27 +++ .gitea/ISSUE_TEMPLATE/feature_request.md | 32 ++++ .gitea/ISSUE_TEMPLATE/task.md | 28 +++ .gitea/ISSUE_TEMPLATE/tech_debt.md | 25 +++ .gitea/PULL_REQUEST_TEMPLATE.md | 15 ++ README.md | 8 + pyproject.toml | 25 +++ src/govoplan_policy.egg-info/PKG-INFO | 18 ++ src/govoplan_policy.egg-info/SOURCES.txt | 16 ++ .../dependency_links.txt | 1 + src/govoplan_policy.egg-info/entry_points.txt | 2 + src/govoplan_policy.egg-info/requires.txt | 2 + src/govoplan_policy.egg-info/top_level.txt | 1 + src/govoplan_policy/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 196 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 196 bytes src/govoplan_policy/backend/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 232 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 232 bytes .../__pycache__/manifest.cpython-312.pyc | Bin 0 -> 805 bytes .../__pycache__/manifest.cpython-313.pyc | Bin 0 -> 803 bytes src/govoplan_policy/backend/api/__init__.py | 0 .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 170 bytes .../api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 170 bytes .../backend/api/v1/__init__.py | 0 .../v1/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 173 bytes .../v1/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 173 bytes .../api/v1/__pycache__/routes.cpython-312.pyc | Bin 0 -> 8458 bytes .../api/v1/__pycache__/routes.cpython-313.pyc | Bin 0 -> 8212 bytes .../v1/__pycache__/schemas.cpython-312.pyc | Bin 0 -> 5834 bytes .../v1/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 5964 bytes src/govoplan_policy/backend/api/v1/routes.py | 168 ++++++++++++++++++ src/govoplan_policy/backend/api/v1/schemas.py | 110 ++++++++++++ src/govoplan_policy/backend/manifest.py | 23 +++ src/govoplan_policy/py.typed | 1 + 37 files changed, 542 insertions(+) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/config.yaml create mode 100644 .gitea/ISSUE_TEMPLATE/docs_workflow.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/ISSUE_TEMPLATE/task.md create mode 100644 .gitea/ISSUE_TEMPLATE/tech_debt.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/govoplan_policy.egg-info/PKG-INFO create mode 100644 src/govoplan_policy.egg-info/SOURCES.txt create mode 100644 src/govoplan_policy.egg-info/dependency_links.txt create mode 100644 src/govoplan_policy.egg-info/entry_points.txt create mode 100644 src/govoplan_policy.egg-info/requires.txt create mode 100644 src/govoplan_policy.egg-info/top_level.txt create mode 100644 src/govoplan_policy/__init__.py create mode 100644 src/govoplan_policy/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_policy/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/__init__.py create mode 100644 src/govoplan_policy/backend/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/__pycache__/manifest.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/api/__init__.py create mode 100644 src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__init__.py create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc create mode 100644 src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc create mode 100644 src/govoplan_policy/backend/api/v1/routes.py create mode 100644 src/govoplan_policy/backend/api/v1/schemas.py create mode 100644 src/govoplan_policy/backend/manifest.py create mode 100644 src/govoplan_policy/py.typed diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..de6be63 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: "Bug" +about: "Report a reproducible defect, regression, or incorrect behavior" +title: "[Bug] " +labels: + - type/bug + - status/triage + - module/policy +--- + +## Scope + +- Repository: +- Area/module: +- Affected version or commit: + +## Behavior + +Expected: + +Actual: + +## Reproduction + +1. +2. +3. + +## Evidence + +Logs, screenshots, traces, or failing test output: + +## Verification Target + +Command or workflow that should pass when fixed: diff --git a/.gitea/ISSUE_TEMPLATE/config.yaml b/.gitea/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.gitea/ISSUE_TEMPLATE/docs_workflow.md b/.gitea/ISSUE_TEMPLATE/docs_workflow.md new file mode 100644 index 0000000..af46c1b --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/docs_workflow.md @@ -0,0 +1,27 @@ +--- +name: "Docs / workflow" +about: "Request documentation, process, or developer workflow changes" +title: "[Docs] " +labels: + - type/docs + - status/triage + - module/policy + - area/docs +--- + +## Scope + +- Repository: +- Document or workflow: + +## Current State + +What is missing, unclear, duplicated, or stale? + +## Desired State + +What should the docs or workflow make clear? + +## Verification Target + +How should this be checked? diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..8f29421 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: "Feature" +about: "Propose new user-visible behavior or platform capability" +title: "[Feature] " +labels: + - type/feature + - status/triage + - module/policy +--- + +## Problem + +What user, operator, or developer problem should this solve? + +## Proposed Capability + +What should exist when this is done? + +## Ownership + +- Owning repository: +- Related module repositories: +- Extension point or integration boundary: + +## Acceptance Criteria + +- [ ] +- [ ] + +## Verification Target + +Command, scenario, or UI flow that should prove completion: diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..95b5e56 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -0,0 +1,28 @@ +--- +name: "Task" +about: "Track implementation, maintenance, or migration work" +title: "[Task] " +labels: + - type/task + - status/triage + - module/policy +--- + +## Objective + +What needs to be completed? + +## Scope + +- Owning repository: +- In-scope: +- Out-of-scope: + +## Checklist + +- [ ] +- [ ] + +## Verification Target + +Command or manual check: diff --git a/.gitea/ISSUE_TEMPLATE/tech_debt.md b/.gitea/ISSUE_TEMPLATE/tech_debt.md new file mode 100644 index 0000000..cb0408a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/tech_debt.md @@ -0,0 +1,25 @@ +--- +name: "Tech debt" +about: "Track cleanup, refactoring, risk reduction, or deferred engineering work" +title: "[Debt] " +labels: + - type/debt + - status/triage + - module/policy +--- + +## Current Cost + +What does this make harder, riskier, slower, or more fragile? + +## Desired Shape + +What should the code, tests, or architecture look like afterwards? + +## Constraints + +What behavior, compatibility, or module boundary must be preserved? + +## Verification Target + +Focused checks that should pass: diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1984736 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Issue + +Closes # + +## Summary + +- + +## Verification + +- + +## Notes + +Follow-up issues: diff --git a/README.md b/README.md new file mode 100644 index 0000000..032dd5f --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# GovOPlaN Policy + +`govoplan-policy` owns policy and retention API route contributions during the +GovOPlaN module split. + +The current package delegates to the legacy access administration +implementation while route ownership is separated before model migration. + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9a48aef --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "govoplan-policy" +version = "0.1.5" +description = "GovOPlaN policy platform module." +readme = "README.md" +requires-python = ">=3.12" +authors = [{ name = "GovOPlaN" }] +dependencies = [ + "govoplan-core>=0.1.5", + "govoplan-access>=0.1.5", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +govoplan_policy = ["py.typed"] + +[project.entry-points."govoplan.modules"] +policy = "govoplan_policy.backend.manifest:get_manifest" + diff --git a/src/govoplan_policy.egg-info/PKG-INFO b/src/govoplan_policy.egg-info/PKG-INFO new file mode 100644 index 0000000..1015a51 --- /dev/null +++ b/src/govoplan_policy.egg-info/PKG-INFO @@ -0,0 +1,18 @@ +Metadata-Version: 2.4 +Name: govoplan-policy +Version: 0.1.4 +Summary: GovOPlaN policy platform module. +Author: GovOPlaN +Requires-Python: >=3.12 +Description-Content-Type: text/markdown +Requires-Dist: govoplan-core>=0.1.4 +Requires-Dist: govoplan-access>=0.1.4 + +# GovOPlaN Policy + +`govoplan-policy` owns policy and retention API route contributions during the +GovOPlaN module split. + +The current package delegates to the legacy access administration +implementation while route ownership is separated before model migration. + diff --git a/src/govoplan_policy.egg-info/SOURCES.txt b/src/govoplan_policy.egg-info/SOURCES.txt new file mode 100644 index 0000000..9060033 --- /dev/null +++ b/src/govoplan_policy.egg-info/SOURCES.txt @@ -0,0 +1,16 @@ +README.md +pyproject.toml +src/govoplan_policy/__init__.py +src/govoplan_policy/py.typed +src/govoplan_policy.egg-info/PKG-INFO +src/govoplan_policy.egg-info/SOURCES.txt +src/govoplan_policy.egg-info/dependency_links.txt +src/govoplan_policy.egg-info/entry_points.txt +src/govoplan_policy.egg-info/requires.txt +src/govoplan_policy.egg-info/top_level.txt +src/govoplan_policy/backend/__init__.py +src/govoplan_policy/backend/manifest.py +src/govoplan_policy/backend/api/__init__.py +src/govoplan_policy/backend/api/v1/__init__.py +src/govoplan_policy/backend/api/v1/routes.py +src/govoplan_policy/backend/api/v1/schemas.py \ No newline at end of file diff --git a/src/govoplan_policy.egg-info/dependency_links.txt b/src/govoplan_policy.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_policy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/govoplan_policy.egg-info/entry_points.txt b/src/govoplan_policy.egg-info/entry_points.txt new file mode 100644 index 0000000..eee1d6e --- /dev/null +++ b/src/govoplan_policy.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[govoplan.modules] +policy = govoplan_policy.backend.manifest:get_manifest diff --git a/src/govoplan_policy.egg-info/requires.txt b/src/govoplan_policy.egg-info/requires.txt new file mode 100644 index 0000000..58ec5d6 --- /dev/null +++ b/src/govoplan_policy.egg-info/requires.txt @@ -0,0 +1,2 @@ +govoplan-core>=0.1.4 +govoplan-access>=0.1.4 diff --git a/src/govoplan_policy.egg-info/top_level.txt b/src/govoplan_policy.egg-info/top_level.txt new file mode 100644 index 0000000..8e22451 --- /dev/null +++ b/src/govoplan_policy.egg-info/top_level.txt @@ -0,0 +1 @@ +govoplan_policy diff --git a/src/govoplan_policy/__init__.py b/src/govoplan_policy/__init__.py new file mode 100644 index 0000000..509a9a2 --- /dev/null +++ b/src/govoplan_policy/__init__.py @@ -0,0 +1,2 @@ +"""GovOPlaN policy module.""" + diff --git a/src/govoplan_policy/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b52f7e847485553fafe486200fec254fd48daf63 GIT binary patch literal 196 zcmX@j%ge<81V1{xv*dyFV-N=h7@>^M96-i&h7^V CBQ;F` literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e43ecc7788208e4522e8b55a323e54430d12326 GIT binary patch literal 196 zcmey&%ge<81V1{xv*dyFV-N=h7@>^M96-iYhG2#whIB?vrYdpw{4)Q5oJ2o`g8ZD! z^M96-i&h7^V`)wo?n(^M96-iYhG2#whIB?vrYd8n#N_PMycC7Zypq)P zqQsKS{5*xU{33;tj8p~p{4)Q5oJ2o`g8ZD!`)wo?n(02a{dc<034s7o}CuQ!gQArX|`WWhO0FRC@64 zvA;p|pX?t9gA0NO@uqf9d-BbsDfnO>@4b2RecyY`WxL%%V1GY7d3A{q`XQ6rtSZ2p z#}KR$K?F}xibojBSet68KGIWTWI*l^{i88zXowobAPr&?i#ER6Um6w0$RsXlLWi|% zdF@}a=wabRS(X=3k>nZoG|?X9WIm-2^Q@q63el<3L6jw9%8MA}?M#D6M+k2YAXuXh zAPoe8xc4CL9e$2HeaK`Ju$U~L7nA|Py-B{vXVWMPXZbXVm;TEreoZstN3+CVocOAd zGdVYOTX7{s+C?VMnzpmut+=v8oL(%88H`bnfx`v zozNnzZ`d6u-GamA{#NLlv%h+(nungonowf~I9RO7XfbCQlYhgrMJo))b12X-WFP{G z!+NV>oHObxm#VM96@+@IR>mZsB4)Q`w3BvYpH(j&up_vT^YLSFzjci96}tNqU2IGP i_x^N{dstd~WwTql`(^j2?Del5)74h?br0hMh4>Fy|IeHN literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc b/src/govoplan_policy/backend/__pycache__/manifest.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adb126a540f74427148f742b4ae3c4c06488dfc4 GIT binary patch literal 803 zcmY*X&59F25bmD8Ofr)!$u25k4`I*Sd!$Q&wq&gJw5|?%*8l@^C(xVZ$^i4yI3a-%n1L0PF3rGxY=RZIL0NT6<4U{w zL&CPHaH2HL@+eQTl(#ie?PtUJggwpDoW0LQZI||=G#N3T$0+XBG>mjY==KPMHR+%* zl!MYz+9I#%ebUwkATJ?{1=&1jfCLZ5*&>@wqBNXklO$exZ=?7fONU-GOT5LIr!I0R z$#d63_f;n)?@>R~>(xFdWUXz`_tv1QE?* zt6`i0^OT#aufY|BdZ<}Hz(*OVf literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/__init__.py b/src/govoplan_policy/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cbbae671296e31b6e6bf59375d5182bbd8ff7eb GIT binary patch literal 170 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!^3>1GE75mx3~|&? z&n(eT&o9d_$Vtr8Ey&NwOs>=~E=q=r#Y4rC5|gu2^HTH^3o`ZN<1_OzOXB183Mzkb f*yQG?l;)(`6|n+MW(4A55aS~=BO_xGGmr%UQD!TB literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/backend/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..727ddffb02dc49e829bb6a4421a3521e98be2a53 GIT binary patch literal 170 zcmey&%ge<81ULJ=vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iSer{fgzKdgs zqkej3iGF&1S$;uIVxDe6eokg`rG9ZyGF&VkDwdR(oSmANqMulhsUIJonU`4-AFo$X hd5gm)H$SB`C)KWq6=*WZ=3)@zBQql-V-Yiu1prXTD^dUe literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__init__.py b/src/govoplan_policy/backend/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e6b8e13d97e2a71588a76941a8cf9abca85b5ad GIT binary patch literal 173 zcmX@j%ge<81ULJ=vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!^3l)DE75mx3~|&? z&n(eT&o9d_$Vtr8Ey&NwOs>=~E=q=r#Y4rC5|gu2^HTH^3o`Y~4E5vVGxIV_;^XxS iDt~d<uWe1ma>4<0CU8BV!RWkOctT%q&&_ literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa48d0e62a40fc556453965cb33cfcdcebb6535e GIT binary patch literal 173 zcmey&%ge<81ULJ=vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~i4er{fgzKdgs zqkej3iGF&1S$;uIVxDe6eokg`rG9ZyGF&VkDwdR(oSmANqMulhsb6NOA0MBYmst`Y kuUAlci^C>2KczG$)vkyYXgJ97Vi4maGb1Bo5i^hl0Ne~LD*ylh literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64bd49ad598847afa103ed7f0efe94c78c114683 GIT binary patch literal 8458 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 TOalf)432k+{h9-i?%{s|C&I+i literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68ae94d30311f1663473abcaaf4512eb7c66c72f GIT binary patch literal 8212 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 literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..619bf004ec0de52f3a6e1de87180891c76d8bcea GIT binary patch literal 5834 zcmcIoTWlN06`duQZ;I4|lJ&46#&+s357CZaavVFcEy+?W(+|lBOxNA4I3s!O<&v3Q z%9SbuLX!Xr5TI`J5fy1swf%`p0qdVP5Y9*v_I(wy(<}@?n#LSekW1S&!s=O zlR?@+z3VQ+c=HHdYL05#_1qzbSGG*jZm#L2vni&hivBA~3?^`>w00OOpAf5J z>nA@WDTfC4HE462%tDxb?D)tdgXT6F1u2QzEfRAi#$dU-E)9$gB*uo%CzK24M}|*d zQ3i(xM*5Yr16M}D?7YdQO$b;LeR4LQHmrF6$+461NzIB+W~MS(T{T|LW^^qz9XENZ zqEkw`aW!?588ohDwfNNjxS6`n(yG~;owlQtO{h8DQdC{f+*06XToK=?=vrE{Ox^}B z?SR|t1TjY*Kl|EW0>>A2&!4^%`EdBl!126%{6A2bGMGQ~x5+7-C0^igQcvQ=w;)kk-23= zE1455x;HBF?bT?^E1YHfHB&Q8OEpr=mMJEt|1DL|u>sCA+;%}0r%l^wTAa7QoNReK zlhI>t+XV@mV|K%Yrt6SlA}mbXZ)A8HGI@qk9>L}wF=a+=UrJ}Hp9NBv+6pl#Y23)+zjBC$s^Ii6yp7RwHU13508?Ke}IEK{uMEK5A`gGZyY z(L}~zwzp`|sO{3#ai-su>~J~LC)5;x>GX_G*G%gwbh>7T)NEGQ0AEEUnNLC(HE4P% z3dn!Wb{7LJ=C{3yVyJ1RD7Ifw(ixi5u^v>EcXO&H*cj!E{!uNhV;Fge02M=a$H zC_*R@Fl~7vr|UC6C~=Idd+~AaOVPcs>qERUiuJT+Xn;j~ukj}A_Y#OF;f+v59J^+> zZwINidz``48BAP;$YrV0mI9*?f}sHkF0Ji^3T)(+&1qH=fl+D;(Cti&-IaJ0nn8ge z2A}Ip=_c=ix{2+g(IN`&R=yhsXHIOt(!Ed7|8;yRuErc$caz3f9z6HZDuhP>H$@KI zmBq9;?!&elHjW%O#6zrw?*|=!1;qgrUThM?h0&Fi;B>;J9CJ`4@3y`xdINa^KZOFh zw;f37s%fT~bv;A*SyUrMs><{}d_;k7H~$3U1G3W6y*N~8d3kQ+t3A)><*tRXCHeW4 z1APx8kDe_Y7|F|f76Ac9R^kUgAA6`4;zM~ky7nv31CW>p#uR%;0woDaE$zILvrTjrvsAm*qA|2 z+BQK+*(iegG`_AM#P^1cL4F3u7(y`&q8ckihRmG)j-`1TK)8q_*8)PW5I&dpo-4-c z2-suI^^%__aAsnapElCVq%-+F0|^Wi84;5PpVq9)B>jANQ#U6SMf$21=TOR z1#Gq+dslt8k0Q6b#NN#@piGsd5WAjOP(hv&G2iySM9Q_%w20)JQ1~)7LGd=+CX&i( z_k7)ZAm5)V^wPP(eDKABOp7su?dz|`99XJXV_RzM2EyeLXi_1R zsfY0LOs8Pz_2@s)oXsy$O!0$Y+O0JsmDMr-<~R{aE~xnxoVo~>Ls(0qxQb#^uwaaE z6xZN35lN7o55fLh#mztsHwJfAMnR*jpm|`j+IkR{D-V zqJ_TEyxfuRI#iHHi?M~n*N-jyYp9N`t(jI!+C^@)R2b}~oUtXDiqsX@DxX$(&Pc>O zn->XLQ4tt_gf+2dn^pxEgQJ*)+iU|-okCwl+UL&9j}_$h;`d?0`rp?Mmg?`@$Q_7j z*^bnHNim2yh8_|-6V7lFF?sX!>&^Ea=)zH?;WqID!HdDpg4|i0A)4TLf}ewup9B$e zZ;(hqe2au%*e{{I4SxnFazHNOXOJT_a^%yt8-7eRV^R=Kj%Nh6H#pyu#9DRu+3URC91thKS=lANY6jXsU>o1)$ML`=CuUtD;T~MyNiL2uq z57hFcYb_*6C)T_Hscn6yOFFu6be%x4-WZn7NsArp1k3e3VJRUkYWS3ZC#S@u&%lP| jde9?{NuR~m36|>-r_`|k4^XTJC8=xS=)VaR!WsVsHnp1^ literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc b/src/govoplan_policy/backend/api/v1/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64b18c82d8914a010da841a69bb05bc96e9f3faa GIT binary patch literal 5964 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 literal 0 HcmV?d00001 diff --git a/src/govoplan_policy/backend/api/v1/routes.py b/src/govoplan_policy/backend/api/v1/routes.py new file mode 100644 index 0000000..df4d67f --- /dev/null +++ b/src/govoplan_policy/backend/api/v1/routes.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope, require_scope +from govoplan_core.audit.logging import audit_from_principal +from govoplan_core.db.session import get_session +from govoplan_core.privacy.retention import ( + PrivacyPolicyError, + apply_retention_policy, + effective_privacy_policy, + effective_privacy_policy_sources, + get_privacy_policy_for_scope, + parent_privacy_policy, + parent_privacy_policy_sources, + set_privacy_policy_for_scope, +) + +from .schemas import ( + PrivacyRetentionPolicyItem, + PrivacyRetentionPolicyScopeRequest, + PrivacyRetentionPolicyScopeResponse, + RetentionRunRequest, + RetentionRunResponse, +) + +router = APIRouter(prefix="/admin", tags=["admin"]) + + +def _require_permission(principal: ApiPrincipal, scope: str) -> None: + if not has_scope(principal, scope): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing scope: {scope}") + + +def _require_privacy_policy_read(principal: ApiPrincipal, scope_type: str) -> None: + if scope_type == "system": + _require_permission(principal, "system:settings:read") + else: + _require_permission(principal, "admin:policies:read") + + +def _require_privacy_policy_write(principal: ApiPrincipal, scope_type: str) -> None: + if scope_type == "system": + _require_permission(principal, "system:settings:write") + else: + _require_permission(principal, "admin:policies:write") + + +@router.get("/privacy-retention/policies/{scope_type}", response_model=PrivacyRetentionPolicyScopeResponse) +def read_privacy_retention_policy( + scope_type: str, + scope_id: str | None = Query(default=None), + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(get_api_principal), +): + clean_scope = scope_type.strip().casefold() + _require_privacy_policy_read(principal, clean_scope) + try: + policy = get_privacy_policy_for_scope(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + effective = _effective_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + parent = _parent_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + return PrivacyRetentionPolicyScopeResponse( + scope_type=clean_scope, + scope_id=scope_id, + policy=policy, + effective_policy=PrivacyRetentionPolicyItem.model_validate(effective.model_dump(mode="json")), + parent_policy=PrivacyRetentionPolicyItem.model_validate(parent.model_dump(mode="json")) if parent else None, + effective_policy_sources=_effective_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id), + parent_policy_sources=_parent_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id), + ) + except PrivacyPolicyError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + + +@router.put("/privacy-retention/policies/{scope_type}", response_model=PrivacyRetentionPolicyScopeResponse) +def write_privacy_retention_policy( + scope_type: str, + payload: PrivacyRetentionPolicyScopeRequest, + scope_id: str | None = Query(default=None), + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(get_api_principal), +): + clean_scope = scope_type.strip().casefold() + _require_privacy_policy_write(principal, clean_scope) + try: + policy = set_privacy_policy_for_scope( + session, + tenant_id=principal.tenant_id, + scope_type=clean_scope, + scope_id=scope_id, + policy=payload.policy.model_dump(mode="json", exclude_none=True), + ) + session.commit() + effective = _effective_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + parent = _parent_privacy_policy_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id) + return PrivacyRetentionPolicyScopeResponse( + scope_type=clean_scope, + scope_id=scope_id, + policy=policy, + effective_policy=PrivacyRetentionPolicyItem.model_validate(effective.model_dump(mode="json")), + parent_policy=PrivacyRetentionPolicyItem.model_validate(parent.model_dump(mode="json")) if parent else None, + effective_policy_sources=_effective_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id), + parent_policy_sources=_parent_privacy_policy_sources_for_response(session, tenant_id=principal.tenant_id, scope_type=clean_scope, scope_id=scope_id), + ) + except PrivacyPolicyError as exc: + session.rollback() + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc + + +def _parent_privacy_policy_sources_for_response(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None): + if scope_type == "system": + return [] + return parent_privacy_policy_sources(session, tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id or (tenant_id if scope_type == "tenant" else None)) + + +def _effective_privacy_policy_sources_for_response(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None): + if scope_type == "system": + return effective_privacy_policy_sources(session) + if scope_type == "tenant": + return effective_privacy_policy_sources(session, tenant_id=scope_id or tenant_id) + if scope_type == "campaign" and scope_id: + return effective_privacy_policy_sources(session, campaign_id=scope_id) + if scope_type == "user" and scope_id: + return effective_privacy_policy_sources(session, tenant_id=tenant_id, owner_user_id=scope_id) + if scope_type == "group" and scope_id: + return effective_privacy_policy_sources(session, tenant_id=tenant_id, owner_group_id=scope_id) + return effective_privacy_policy_sources(session, tenant_id=tenant_id) + + +def _parent_privacy_policy_for_response(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None): + if scope_type == "system": + return None + return parent_privacy_policy(session, tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id or (tenant_id if scope_type == "tenant" else None)) + + +def _effective_privacy_policy_for_response(session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None): + if scope_type == "system": + return effective_privacy_policy(session) + if scope_type == "tenant": + return effective_privacy_policy(session, tenant_id=scope_id or tenant_id) + if scope_type == "campaign" and scope_id: + return effective_privacy_policy(session, campaign_id=scope_id) + if scope_type == "user" and scope_id: + return effective_privacy_policy(session, tenant_id=tenant_id, owner_user_id=scope_id) + if scope_type == "group" and scope_id: + return effective_privacy_policy(session, tenant_id=tenant_id, owner_group_id=scope_id) + return effective_privacy_policy(session, tenant_id=tenant_id) + + +@router.post("/system/retention/run", response_model=RetentionRunResponse) +def run_retention_policy( + payload: RetentionRunRequest, + session: Session = Depends(get_session), + principal: ApiPrincipal = Depends(require_scope("system:settings:write")), +): + result = apply_retention_policy(session, dry_run=payload.dry_run) + audit_from_principal( + session, + principal, + action="retention_policy.run", + scope="system", + object_type="retention_policy", + object_id="global", + details={"dry_run": payload.dry_run, "counts": result.get("counts")}, + ) + session.commit() + return RetentionRunResponse(result=result) diff --git a/src/govoplan_policy/backend/api/v1/schemas.py b/src/govoplan_policy/backend/api/v1/schemas.py new file mode 100644 index 0000000..30ff092 --- /dev/null +++ b/src/govoplan_policy/backend/api/v1/schemas.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +RETENTION_DAY_KEYS = ( + "raw_campaign_json_retention_days", + "generated_eml_retention_days", + "stored_report_detail_retention_days", + "mock_mailbox_retention_days", + "audit_detail_retention_days", +) + + +RETENTION_POLICY_FIELD_KEYS = ( + "store_raw_campaign_json", + *RETENTION_DAY_KEYS, + "audit_detail_level", +) + + +def default_allow_lower_level_limits() -> dict[str, bool]: + return {key: True for key in RETENTION_POLICY_FIELD_KEYS} + + +def normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool) -> dict[str, bool] | None: + if value in (None, ""): + return default_allow_lower_level_limits() if fill_defaults else None + if not isinstance(value, dict): + raise ValueError("allow_lower_level_limits must be an object") + normalized = default_allow_lower_level_limits() if fill_defaults else {} + for key, allowed in value.items(): + clean_key = str(key) + if clean_key not in RETENTION_POLICY_FIELD_KEYS: + raise ValueError(f"Unknown retention policy field: {clean_key}") + normalized[clean_key] = bool(allowed) + return normalized + + +class PolicySourceStepItem(BaseModel): + scope_type: str + scope_id: str | None = None + label: str + applied_fields: list[str] = Field(default_factory=list) + policy: dict[str, Any] = Field(default_factory=dict) + + +class PrivacyRetentionPolicyItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + store_raw_campaign_json: bool = True + raw_campaign_json_retention_days: int | None = Field(default=None, ge=0) + generated_eml_retention_days: int | None = Field(default=None, ge=0) + stored_report_detail_retention_days: int | None = Field(default=None, ge=0) + mock_mailbox_retention_days: int | None = Field(default=None, ge=0) + audit_detail_retention_days: int | None = Field(default=None, ge=0) + audit_detail_level: Literal["full", "redacted", "minimal"] = "full" + allow_lower_level_limits: dict[str, bool] = Field(default_factory=default_allow_lower_level_limits) + + @field_validator("allow_lower_level_limits", mode="before") + @classmethod + def _normalize_allow_lower_level_limits(cls, value: Any) -> Any: + return normalize_allow_lower_level_limits(value, fill_defaults=True) + + +class PrivacyRetentionPolicyPatchItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + store_raw_campaign_json: bool | None = None + raw_campaign_json_retention_days: int | None = Field(default=None, ge=0) + generated_eml_retention_days: int | None = Field(default=None, ge=0) + stored_report_detail_retention_days: int | None = Field(default=None, ge=0) + mock_mailbox_retention_days: int | None = Field(default=None, ge=0) + audit_detail_retention_days: int | None = Field(default=None, ge=0) + audit_detail_level: Literal["full", "redacted", "minimal"] | None = None + allow_lower_level_limits: dict[str, bool] | None = None + + @field_validator("allow_lower_level_limits", mode="before") + @classmethod + def _normalize_allow_lower_level_limits(cls, value: Any) -> Any: + return normalize_allow_lower_level_limits(value, fill_defaults=False) + + +class PrivacyRetentionPolicyScopeRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + policy: PrivacyRetentionPolicyPatchItem = Field(default_factory=PrivacyRetentionPolicyPatchItem) + + +class PrivacyRetentionPolicyScopeResponse(BaseModel): + scope_type: Literal["system", "tenant", "user", "group", "campaign"] + scope_id: str | None = None + policy: dict[str, Any] + effective_policy: PrivacyRetentionPolicyItem + parent_policy: PrivacyRetentionPolicyItem | None = None + effective_policy_sources: list[PolicySourceStepItem] = Field(default_factory=list) + parent_policy_sources: list[PolicySourceStepItem] = Field(default_factory=list) + + +class RetentionRunRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + dry_run: bool = True + + +class RetentionRunResponse(BaseModel): + result: dict[str, Any] diff --git a/src/govoplan_policy/backend/manifest.py b/src/govoplan_policy/backend/manifest.py new file mode 100644 index 0000000..5357155 --- /dev/null +++ b/src/govoplan_policy/backend/manifest.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from govoplan_core.core.modules import ModuleContext, ModuleManifest + + +def _route_factory(context: ModuleContext): + del context + from govoplan_policy.backend.api.v1.routes import router + + return router + + +manifest = ModuleManifest( + id="policy", + name="Policy", + version="0.1.5", + dependencies=("access",), + route_factory=_route_factory, +) + + +def get_manifest() -> ModuleManifest: + return manifest diff --git a/src/govoplan_policy/py.typed b/src/govoplan_policy/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_policy/py.typed @@ -0,0 +1 @@ +