d582f15681
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses. - Created tests for the login page, validating user interactions and error handling. - Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values. - Developed tests for query keys, ensuring proper scoping and invalidation roots. chore: setup testing environment and configuration - Added setup file for testing with Vitest and Testing Library. - Configured TypeScript and Vite for the frontend project. - Created Docker and Nginx configurations for local development and production environments. - Established a PostgreSQL initialization script for setting up application roles. docs: add README and environment configuration for infra - Included README for the infra directory, detailing local development setup and service descriptions. - Created an example environment configuration file for local development with placeholders for sensitive information.
78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
import os
|
|
from pathlib import Path
|
|
from typing import Protocol
|
|
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
class BlobStore(Protocol):
|
|
def put(self, key: str, data: bytes) -> None: ...
|
|
def get(self, key: str) -> bytes: ...
|
|
def delete(self, key: str) -> None: ...
|
|
|
|
|
|
class LocalDirectoryBlobStore:
|
|
"""S3-neutral local store; production swaps in an S3-compatible client."""
|
|
|
|
def __init__(self, base_dir: str) -> None:
|
|
self._base = Path(base_dir)
|
|
|
|
def _path(self, key: str) -> Path:
|
|
if "/" in key or ".." in key or not key:
|
|
raise ValueError("invalid blob key")
|
|
return self._base / key
|
|
|
|
def put(self, key: str, data: bytes) -> None:
|
|
path = self._path(key)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(data)
|
|
|
|
def get(self, key: str) -> bytes:
|
|
return self._path(key).read_bytes()
|
|
|
|
def delete(self, key: str) -> None:
|
|
path = self._path(key)
|
|
if path.exists():
|
|
path.unlink()
|
|
|
|
|
|
class S3BlobStore:
|
|
def __init__(self) -> None:
|
|
settings = get_settings()
|
|
self._bucket = settings.s3_bucket_statements
|
|
self._client = boto3.client(
|
|
"s3",
|
|
endpoint_url=settings.s3_endpoint_url,
|
|
region_name=settings.s3_region,
|
|
aws_access_key_id=settings.s3_access_key_id,
|
|
aws_secret_access_key=settings.s3_secret_access_key,
|
|
)
|
|
try:
|
|
self._client.head_bucket(Bucket=self._bucket)
|
|
except ClientError:
|
|
self._client.create_bucket(Bucket=self._bucket)
|
|
|
|
def put(self, key: str, data: bytes) -> None:
|
|
self._client.put_object(Bucket=self._bucket, Key=key, Body=data)
|
|
|
|
def get(self, key: str) -> bytes:
|
|
response = self._client.get_object(Bucket=self._bucket, Key=key)
|
|
return response["Body"].read()
|
|
|
|
def delete(self, key: str) -> None:
|
|
self._client.delete_object(Bucket=self._bucket, Key=key)
|
|
|
|
|
|
def get_blob_store() -> BlobStore:
|
|
settings = get_settings()
|
|
if settings.s3_endpoint_url:
|
|
return S3BlobStore()
|
|
return LocalDirectoryBlobStore(settings.blob_dir)
|
|
|
|
|
|
def new_object_key() -> str:
|
|
return os.urandom(16).hex()
|