feat(core): define managed tabular file contract
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-core"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from govoplan_core.core.access import ResourceAccessExplanationProvider
|
||||
@@ -10,6 +11,48 @@ from govoplan_core.core.access import ResourceAccessExplanationProvider
|
||||
CAPABILITY_FILES_ACCESS = "files.access"
|
||||
CAPABILITY_FILES_ARTIFACT_STORE = "files.artifact_store"
|
||||
CAPABILITY_FILES_POSTBOX_REFERENCES = "files.postbox_references"
|
||||
CAPABILITY_FILES_TABULAR_CONTENT = "files.tabular_content"
|
||||
|
||||
|
||||
class ManagedTabularFileError(ValueError):
|
||||
"""Stable base error for exact-version managed tabular file access."""
|
||||
|
||||
|
||||
class ManagedTabularFileNotFoundError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileAccessError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileUnavailableError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileValidationError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManagedTabularFile:
|
||||
"""Authorized metadata for one immutable managed file version."""
|
||||
|
||||
file_asset_id: str
|
||||
file_version_id: str
|
||||
filename: str
|
||||
display_path: str
|
||||
content_type: str | None
|
||||
size_bytes: int
|
||||
sha256: str
|
||||
updated_at: datetime | None = None
|
||||
current_version: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManagedTabularFileContent:
|
||||
file: ManagedTabularFile
|
||||
payload: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -91,6 +134,39 @@ class PostboxFileReferenceProvider(Protocol):
|
||||
) -> tuple[PostboxFileReferenceRef, ...]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ManagedTabularFileProvider(Protocol):
|
||||
"""List and open authorized CSV/XLSX content without exposing Files internals."""
|
||||
|
||||
def list_tabular_files(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
query: str = "",
|
||||
limit: int = 100,
|
||||
) -> tuple[ManagedTabularFile, ...]: ...
|
||||
|
||||
def get_tabular_file(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
file_asset_id: str,
|
||||
file_version_id: str | None = None,
|
||||
) -> ManagedTabularFile | None: ...
|
||||
|
||||
def read_tabular_file(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
file_asset_id: str,
|
||||
file_version_id: str,
|
||||
max_bytes: int,
|
||||
) -> ManagedTabularFileContent: ...
|
||||
|
||||
|
||||
def postbox_file_reference_provider(
|
||||
registry: object | None,
|
||||
) -> PostboxFileReferenceProvider | None:
|
||||
@@ -107,3 +183,46 @@ def postbox_file_reference_provider(
|
||||
"PostboxFileReferenceProvider"
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
def managed_tabular_file_provider(
|
||||
registry: object | None,
|
||||
) -> ManagedTabularFileProvider | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_FILES_TABULAR_CONTENT)
|
||||
):
|
||||
return None
|
||||
provider = registry.require_capability(CAPABILITY_FILES_TABULAR_CONTENT)
|
||||
if not isinstance(provider, ManagedTabularFileProvider):
|
||||
raise TypeError(
|
||||
"files.tabular_content provider does not implement "
|
||||
"ManagedTabularFileProvider"
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_FILES_ACCESS",
|
||||
"CAPABILITY_FILES_ARTIFACT_STORE",
|
||||
"CAPABILITY_FILES_POSTBOX_REFERENCES",
|
||||
"CAPABILITY_FILES_TABULAR_CONTENT",
|
||||
"FileAccessProvider",
|
||||
"ManagedArtifactRef",
|
||||
"ManagedArtifactStore",
|
||||
"ManagedArtifactWriteRequest",
|
||||
"ManagedTabularFile",
|
||||
"ManagedTabularFileAccessError",
|
||||
"ManagedTabularFileContent",
|
||||
"ManagedTabularFileError",
|
||||
"ManagedTabularFileNotFoundError",
|
||||
"ManagedTabularFileProvider",
|
||||
"ManagedTabularFileUnavailableError",
|
||||
"ManagedTabularFileValidationError",
|
||||
"PostboxFileReferenceProvider",
|
||||
"PostboxFileReferenceRef",
|
||||
"PostboxFileReferenceRequest",
|
||||
"managed_tabular_file_provider",
|
||||
"postbox_file_reference_provider",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from govoplan_core.core.files import (
|
||||
CAPABILITY_FILES_TABULAR_CONTENT,
|
||||
ManagedTabularFile,
|
||||
ManagedTabularFileContent,
|
||||
ManagedTabularFileProvider,
|
||||
managed_tabular_file_provider,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
def list_tabular_files(self, session, principal, *, query="", limit=100):
|
||||
del session, principal, query, limit
|
||||
return ()
|
||||
|
||||
def get_tabular_file(
|
||||
self,
|
||||
session,
|
||||
principal,
|
||||
*,
|
||||
file_asset_id,
|
||||
file_version_id=None,
|
||||
):
|
||||
del session, principal, file_asset_id, file_version_id
|
||||
return None
|
||||
|
||||
def read_tabular_file(
|
||||
self,
|
||||
session,
|
||||
principal,
|
||||
*,
|
||||
file_asset_id,
|
||||
file_version_id,
|
||||
max_bytes,
|
||||
):
|
||||
del session, principal, file_asset_id, file_version_id, max_bytes
|
||||
file = ManagedTabularFile(
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
filename="source.csv",
|
||||
display_path="Imports/source.csv",
|
||||
content_type="text/csv",
|
||||
size_bytes=8,
|
||||
sha256="a" * 64,
|
||||
updated_at=datetime(2026, 8, 21, tzinfo=UTC),
|
||||
)
|
||||
return ManagedTabularFileContent(file=file, payload=b"id\n1\n")
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, provider):
|
||||
self.provider = provider
|
||||
|
||||
def has_capability(self, name):
|
||||
return name == CAPABILITY_FILES_TABULAR_CONTENT
|
||||
|
||||
def require_capability(self, name):
|
||||
if not self.has_capability(name):
|
||||
raise KeyError(name)
|
||||
return self.provider
|
||||
|
||||
|
||||
class ManagedTabularFileContractTests(unittest.TestCase):
|
||||
def test_runtime_protocol_and_registry_resolution(self) -> None:
|
||||
provider = _Provider()
|
||||
|
||||
self.assertIsInstance(provider, ManagedTabularFileProvider)
|
||||
self.assertIs(provider, managed_tabular_file_provider(_Registry(provider)))
|
||||
self.assertIsNone(managed_tabular_file_provider(None))
|
||||
|
||||
def test_exact_version_content_retains_safe_metadata(self) -> None:
|
||||
result = _Provider().read_tabular_file(
|
||||
object(),
|
||||
object(),
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
max_bytes=100,
|
||||
)
|
||||
|
||||
self.assertEqual("version-1", result.file.file_version_id)
|
||||
self.assertEqual("a" * 64, result.file.sha256)
|
||||
self.assertEqual(b"id\n1\n", result.payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.18",
|
||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user