Metadata-Version: 2.4
Name: dms-core
Version: 0.11.0
Summary: A Python package for DocMesh, a document management system.
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiosqlite>=0.22.1
Requires-Dist: asyncpg>=0.31.0
Requires-Dist: minio>=7.2.20
Requires-Dist: psycopg[binary]>=3.3.4
Requires-Dist: pydantic>=2.13.4
Requires-Dist: sqlalchemy>=2.0.51
Dynamic: license-file

# Document Management Service

호스트 애플리케이션이 제공하는 문서 정보 저장소와 문서 본문 저장소를 통해 문서 등록·조회·삭제·복구를 수행하는 Python SDK입니다.

`dms`는 독립 실행형 API 서버가 아니라 다른 프로젝트에서 import 해서 사용하는 라이브러리입니다.

## Installation

```bash
uv add dms-core
```
## Quick start

호스트 애플리케이션이 생성한 SQLAlchemy Engine과 MinIO client를 `DocumentManagementSDKFactory`에 전달하는 방식으로 조립합니다. SDK는 저장소 연결이나 인프라 client를 생성하지 않습니다.

SQL dialect에 맞는 adapter와 업로드 작업 저장소는 자동으로 조립됩니다.

```python
from dms import DocumentManagementSDKFactory, DocumentPartition, UploadDocumentRequest

sdk = DocumentManagementSDKFactory(
    engine=engine,
    minio_client=minio_client,
    bucket_name="documents",
).create()
partition = DocumentPartition.personal("user-123")
result = sdk.upload_document(
    UploadDocumentRequest(
        content=b"hello world",
        filename="hello.txt",
        content_type="text/plain",
    ),
    partition=partition,
)
```

`document_id`를 생략하면 문서 정보 저장소의 데이터베이스 자동 증가 식별자가 등록 시 발급되어 결과에 반환됩니다. 호출자가 `document_id`를 지정한 경우에는 해당 식별자를 그대로 사용합니다.

업로드 요청의 `metadata`는 호출자가 문서와 함께 보존하는 애플리케이션 소유의 부가 정보입니다. DMS는 그 형식, 업무 스키마, 보안, 정규화 및 직렬화 규칙을 정의하거나 검증하지 않으며, 제공된 값을 문서 정보에 연결해 저장하고 반환합니다. 해당 값의 보안 및 외부 직렬화 가능성은 호출자가 책임집니다.

주입된 저장소와 연결의 생성·readiness 확인·종료는 호스트 애플리케이션 또는 별도 인프라 통합 계층이 담당합니다. SDK는 호출자가 제공한 저장소를 종료하지 않습니다.
비동기 호스트는 `AsyncEngine`과 `minio`의 동기 MinIO client를 전달해야 하며, blocking 호출은 event loop 밖의 thread에서 실행합니다.

```python
from dms import (
    AsyncDocumentManagementSDKFactory,
    DocumentPartition,
    UploadDocumentRequest,
)

sdk = AsyncDocumentManagementSDKFactory(
    engine=async_engine,
    minio_client=async_minio_client,
    bucket_name="documents",
).create()
partition = DocumentPartition.group("group-456")
result = await sdk.upload_document(
    UploadDocumentRequest(
        content=b"hello world",
        filename="hello.txt",
        content_type="text/plain",
    ),
    partition=partition,
)
metadata = await sdk.get_document_metadata(
    result.document_id,
    partition=partition,
)
```

비동기 SDK도 전역 client lifecycle을 소유하지 않습니다. `get_document_content_async_stream(...)`처럼 SDK가 직접 연 본문 스트림은 사용이 끝나면 `aclose()`로 정리해야 하며, 호출자가 제공한 입력 스트림은 SDK가 닫지 않습니다. 네이티브 비동기 처리는 `AsyncDocumentManagementSDKFactory`를 사용합니다.

## 문서 파티션과 접근 제어 책임

- 모든 문서는 `personal` 또는 `group` 중 정확히 하나의 파티션에 속합니다.
- 일반 문서 등록·조회·목록·본문·삭제·복구 작업에는 `partition=`을 필수로 전달합니다. 업로드 요청 안에 파티션을 중복해서 넣지 않습니다.
- 개인 파티션에는 호스트의 사용자 식별값을, 그룹 파티션에는 호스트의 그룹 식별값을 사용합니다. 식별값은 비어 있지 않은 불투명 문자열입니다.
- DMS는 전달받은 파티션을 문서 정보, 본문 경로, 커서, 멱등성 작업, 삭제 및 복구의 관리 범위로 사용합니다. 같은 식별값이어도 `personal`과 `group`은 서로 다른 파티션입니다.
- 인증과 그룹 구성원 확인은 호스트 애플리케이션이 수행합니다. DMS는 사용자·그룹 디렉터리나 구성원 정보를 저장·관리하지 않습니다.
- 호스트는 `DocumentAccessPolicy`와 호출별 `AccessContext`를 제공하여 문서 작업의 접근 제어를 DMS에 위임할 수 있습니다. 정책이 제공되면 DMS는 등록·조회·목록·본문·삭제·복구·초기화 작업 전에 정책을 실행합니다.
- `AccessContext`의 사용자·그룹·역할 값은 호스트가 인증하고 구성원 관계를 해석한 뒤 전달하는 불투명한 값입니다. DMS는 해당 값의 진위를 재검증하지 않습니다.
- `access_policy`가 없는 기존 조립은 현재와 같은 신뢰 파티션 동작을 유지합니다. 외부 요청자가 SDK를 직접 호출할 수 있는 경우에는 호스트가 정책을 반드시 주입해야 합니다.
- 다른 파티션에만 존재하는 문서를 단건 조회·삭제하거나 문서 정보가 필요한 단건 복구를 실행하면 `DocumentNotFoundError`가 발생합니다. 접근 정책이 제공된 경우 정책의 거부 결과는 `AccessDeniedError`로 구분됩니다.
- 문서 목록과 복구 대상 목록은 다른 파티션의 항목을 페이지 제한 전에 제외합니다. 문서 점검은 실제 부재와 다른 파티션을 구분하지 않고 문서 정보 없음 결과를 반환하며, 다른 파티션에서 커서를 재사용하면 `ValidationError`가 발생합니다.
- 이 변경은 이전 사용자 범위 스키마, 본문 경로, 커서 및 멱등성 기록과 호환되지 않습니다. 기존 데이터 이전은 제공하지 않으므로 새 빈 스키마와 저장 범위로 시작해야 합니다.

### 호스트 제공 접근 정책

- `DocumentAccessPolicy.allows(operation=..., context=..., metadata=...)`는 호스트의 인증·구성원 확인 결과를 바탕으로 작업 허용 여부를 반환합니다.
- `metadata`는 항상 공개 문서 정보이며 `storage_key`를 포함하지 않습니다. 업로드, 목록, 데이터 초기화처럼 특정 문서가 없는 작업에서는 `None`입니다.
- 정책이 `False`를 반환하거나 판정 중 오류가 발생하면 `AccessDeniedError`가 발생합니다. 정책 오류의 상세 내용은 외부 오류 메시지에 노출되지 않습니다.
- 논리 삭제와 완전 삭제는 각각 `document.delete`와 `document.hard_delete` 작업으로 정책에 전달되어 서로 다른 권한을 부여할 수 있습니다.
- native async 조립에서는 `AsyncDocumentAccessPolicy`를 사용할 수 있으며, 동기 정책은 event loop 밖에서 실행됩니다.
- 정책 규칙의 저장·변경, 사용자 인증, 그룹 구성원 추가·삭제는 호스트 애플리케이션의 책임입니다. DMS는 정책을 실행하고 결과를 문서 작업에 적용하는 역할만 담당합니다.

## Public API overview

공개 API는 package root의 export와 공개 계약 테스트를 기준으로 관리합니다.

기본 import 경계는 `from dms import ...`이며, 내부 adapter와 저장소 구현은 공개 API로 간주하지 않습니다. API 문서 끝의 추적성 매트릭스는 각 공개 영역을 구현 파일, 검증 테스트, 실행 예제에 연결합니다.

## Integration boundary

- 저장소 연결 생성, 환경변수 해석, database 준비, readiness 및 운영용 health endpoint는 호스트 애플리케이션 또는 별도 인프라 패키지가 담당합니다.
- SDK 공개 조립 API는 `DocumentManagementSDKFactory.create()`와 `AsyncDocumentManagementSDKFactory.create()`입니다.
- SDK 조립 시 지정한 MinIO bucket이 없으면 SDK가 생성하며, 생성한 bucket을 자동으로 삭제하지 않습니다.
- SDK는 주입된 저장소 연결의 lifecycle을 취득하지 않으며 전역 `close()`·`aclose()`를 제공하지 않습니다.
- SDK가 문서 처리 중 직접 연 파일·본문 스트림은 SDK가 닫고, 호출자가 제공한 스트림과 출력 대상은 닫지 않습니다.

## 공개 문서 정보와 삭제 조회

- 업로드, 일반 문서 정보 조회, 목록 및 커서 페이지는 파티션 정보는 포함하고 내부 저장 위치는 제외한 `PublicDocumentMetadata`를 반환합니다.
- 저장 위치가 필요한 복구·관리 작업만 `get_internal_document_metadata()`를 명시적으로 사용해야 합니다.
- 일반 단건·목록·커서 조회는 논리 삭제 및 삭제 진행 상태의 문서를 숨깁니다. 삭제 상태 확인은 `get_internal_document_metadata()`와 복구 API처럼 명시적인 관리 경로를 사용해야 합니다.
- 삭제된 문서의 본문 및 본문 스트림 조회는 `DocumentDeletedError`를 발생시킵니다.
- `PublicDocumentMetadata.to_dict()`는 v0.6 호환 필드명을 유지하고, 외부 응답용 `to_public_dict()`는 부가 정보를 `metadata` 필드로 노출합니다. 부가 정보의 외부 직렬화 가능성은 호출자가 책임집니다.
- 공개 결과 모델은 `json_schema()`와 `model_json_schema()`를 제공하며 시스템 관리 필드의 구조를 설명합니다. 호출자 부가 정보의 내부 구조는 제한하지 않고, 공개 dump와 schema에는 `storage_key`가 존재하지 않습니다.
- 모든 `DmsError` 하위 오류는 안정적인 `code`, 상위 `category`, `retryable` 값을 제공합니다. 문서 관련 오류는 가능한 경우 `document_id`도 제공합니다.
- `DocumentContentStream`은 컨텍스트 관리자로 사용할 수 있습니다. 호스트가 본문 반복자만 전달하는 경우에는 `iter_chunks_closing()` 또는 `aiter_chunks_closing()`을 사용하면 정상 소진, 읽기 오류, 취소 및 반복자 명시 종료에서 SDK 소유 스트림을 정리합니다.

## 목록 페이지네이션

- `list_documents(partition=..., cursor=None, limit=100, status=None)`는 기본 목록 API이며 `DocumentPage`를 반환합니다.
- 다음 페이지는 반환된 `next_cursor`를 같은 파티션, 상태 필터 및 페이지 크기로 전달하여 조회합니다. 마지막 페이지에서는 `next_cursor`가 `None`입니다.
- 커서는 파티션 종류·식별값, 상태 필터 및 페이지 크기에 결합됩니다. 변조된 커서나 다른 조건에 재사용한 커서는 `ValidationError`로 거부됩니다.
- 목록 조회는 커서 방식만 지원합니다. 기존 오프셋 기반 목록 API는 제거되었습니다.

## 전체 데이터 삭제와 신규 적재 초기화

- `clear_all_data()`는 DMS가 관리하는 문서 본문(`documents/` prefix), 문서 정보 및 업로드 작업 기록을 완전 삭제하고 `DataResetResult`로 저장소별 삭제 건수를 반환합니다. 문서 정보가 없는 orphan 본문도 함께 정리합니다.
- `initialize_for_data_load()`는 같은 범위를 비운 뒤 새 데이터 적재를 시작할 수 있는 빈 상태를 반환합니다. 이미 빈 상태에서 호출해도 성공하는 멱등 작업입니다.
- `clear_partition_data(partition=...)`와 `initialize_partition_for_data_load(partition=...)`는 지정된 파티션의 문서 정보, 본문 및 업로드 작업 기록만 정리합니다.
- 전체 범위와 파티션 범위는 별도 관리 작업입니다. 일반 문서 작업에서 `partition=None`을 전체 범위로 해석하지 않습니다. 접근 정책이 제공되면 해당 관리 작업에도 정책이 적용되며, 정책이 없으면 호출자가 실행 권한을 보장해야 합니다.
- 문서 정보 저장소, 문서 본문 저장소 및 업로드 작업 저장소는 분산 트랜잭션으로 묶이지 않습니다. 한 저장소가 실패해도 나머지 저장소 정리를 시도하며, 전체 완료가 되지 않으면 부분 삭제 건수와 `failed_stores`를 가진 `DataResetError`를 발생시킵니다. 이때 `error.result.ready_for_data_load`는 `False`입니다.
- `AsyncDocumentManagementSDK`에서도 전체 및 파티션별 작업을 awaitable 방식으로 제공합니다.

## 업로드와 비동기 본문 스트리밍

- `AsyncDocumentManagementSDK`는 등록, 문서 정보 및 목록 조회, 본문 조회, 삭제, 복구 및 초기화를 awaitable 방식으로 제공합니다. `AsyncDocumentManagementSDKFactory`는 비동기 SQLAlchemy와 동기 MinIO client를 받으며, blocking MinIO 호출은 event loop 밖의 thread에서 실행합니다. 동기 `Engine` 호환 facade를 사용하는 경우에도 동기 저장소 작업은 event loop 밖에서 실행됩니다.
- 업로드 입력은 메모리 바이트, 파일 경로, 정확한 크기가 선언된 동기 바이너리 스트림의 세 범주를 지원합니다. 파일 경로는 SDK가 열고 닫으며, 호출자가 제공한 스트림은 SDK가 닫지 않습니다.
- 스트림 등록은 정확한 양수 크기를 필수로 받고, 실제 읽은 크기가 선언값과 다르면 업로드 객체를 정리한 뒤 유효성 오류를 반환합니다. 최대 파일 크기는 조립 시 설정한 공통 정책으로 적용합니다.
- 크기를 알 수 없는 입력, 비동기 입력 스트림, 요청별 최대 크기, 업로드 chunk 조절 및 스트림 멱등성은 지원하지 않습니다. 네이티브 비동기 SDK의 `upload_document_stream(...)`도 입력 계약은 정확한 크기를 가진 동기 바이너리 스트림이며, MinIO 업로드와 메타데이터 처리는 비동기로 수행합니다.
- `get_document_content_async_stream(...)`은 전체 본문을 메모리에 적재하지 않는 비동기 반복 스트림을 반환합니다.
- 다운로드 스트림은 성공, 실패, 취소 및 컨텍스트 종료 시 정리됩니다.
- 비동기 본문 스트림은 `async with`와 반복 호출에 안전한 `aclose()`를 지원합니다. 비동기 SDK 자체는 전역 lifecycle을 관리하지 않습니다.

### 업로드 API 축소 이전 안내

- 크기를 알 수 없는 입력은 호출자가 임시 파일 등으로 먼저 크기를 확정한 뒤 파일 또는 동기 스트림 등록 경로를 사용해야 합니다.
- 제거된 bounded·unknown-size·비동기 입력 스트림 요청 타입과 메서드는 `UploadDocumentStreamRequest` 및 `upload_document_stream(...)`으로 자동 호환되지 않습니다. 호출자가 정확한 `size`를 제공해야 합니다.

## v0.4 공개 반환값 이전 안내

- 기존 `result.storage_key` 사용 코드는 관리 작업에 한해 `sdk.get_internal_document_metadata(result.document_id, partition=partition).storage_key`로 이전해야 합니다.
- 기존 일반 조회와 목록에서 `storage_key`를 읽던 코드는 공개 반환값에서 해당 필드를 제거해야 합니다.
- 내부 저장 위치를 외부 응답이나 업무 메타데이터로 전달하지 말고, 명시적 관리·복구 경로 안에서만 사용해야 합니다.

## Document guide

- 제품 요구사항: `docs/prd.md`
- 소프트웨어 요구사항: `docs/srs.md`

## Integration tests

저장소 adapter와 실제 외부 서비스 readiness 검증은 호스트 애플리케이션 또는 별도 인프라 패키지의 책임입니다. 이 저장소의 핵심 테스트는 포트 구현 대역을 주입하여 문서 서비스 계약을 검증합니다.
테스트가 Docker Compose를 생성하거나 실행하지 않습니다.

Factory가 실제 PostgreSQL·MinIO client를 통해 문서를 등록하고 조회하는 통합 테스트를
제공합니다. MinIO bucket은 SDK 조립 과정에서 없으면 생성되며, 테스트는 전용 bucket과
고유 문서 ID를 사용하고 종료 시 생성한 자원을 정리합니다.

```bash
# 기본 테스트(통합 테스트 제외)
uv run pytest test_dms -m "not integration" -q

# 사전에 실행 중인 PostgreSQL·MinIO를 사용하는 Factory 통합 테스트
uv run pytest test_dms/test_sdk_factory_integration.py -m integration -v

# 전체 테스트
uv run pytest test_dms -q
```

## Out of scope

현재 범위 밖 항목:
- 인증 helper
- presigned URL 발급
- 문서 검색/필터링
- 독립 실행형 비동기 작업 처리 서비스
- 메시지 브로커 연계 API
- 사용자 인증 및 인증 토큰 검증
- 사용자·그룹·구성원 정보와 접근 정책 규칙의 저장·관리
- PostgreSQL·SQLite·MinIO client 생성 및 client lifecycle 관리
- 인프라 readiness 또는 운영용 health endpoint
