Skip to content

Commit 940d5bb

Browse files
earayuclaude
andauthored
refactor(phase8-g1): carve export v1 shim to knowledge_base domain (#47) (#1677)
* refactor(phase8-g1): carve export v1 shim to knowledge_base domain (#47) Per PM scope (msg=7733c905) and Bryce inventory G1: complete the export carve trail started by #1670 (ExportTask ORM) and #1672 (export_collection_task). Pure carve / move; URL contract `/api/v1/export*` and `/api/v1/export-tasks*` unchanged. Changes: - aperag/views/export.py → aperag/domains/knowledge_base/api/export_routes.py (3 routes: create_export_task / get_export_task / download_export; switched User ORM dep → AuthenticatedUser Protocol, matching the canonical KB routes pattern) - aperag/service/export_service.py → aperag/domains/knowledge_base/service/export_service.py (git mv preserves history; replaced view_models.ExportTaskResponse references with bare ExportTaskResponse imported from KB schemas) - ExportTaskResponse schema migrated from aperag/schema/view_models.py to aperag/domains/knowledge_base/schemas.py (added to __all__); view_models.py keeps a backward-compat re-export following the same pattern used for knowledge_graph/retrieval schemas - aperag/app.py: import switched from aperag.views.export to aperag.domains.knowledge_base.api.export_routes; mount prefix remains /api/v1 (D1 hard-cut to /api/v2 deferred to later G3+ batch) Verification: - pytest tests/unit_test/test_modularization_boundaries.py -x → 21 passed (G1-G19 all green; new KB route does not import legacy aggregates) - pytest tests/unit_test/ → 686 passed, 29 skipped (matches phase8 baseline) - grep "from aperag.service.export_service|from aperag.views.export" returns 0 hits in aperag/, tests/, config/ - python -c "from aperag.app import app; ..." confirms 3 export endpoints remain at /api/v1/collections/{id}/export, /api/v1/export-tasks/{id}, /api/v1/export-tasks/{id}/download aperag/service/ retains exactly the 3 permanent seam files (quota_service / prompt_template_service / search_pipeline_service); no Layer A / Layer B violation. Ghost-check: none. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(phase8-g1): D7 v2 hard-cut for export endpoints (#47) Per @符炫炜 D7 canonical update (msg=30405f5b) + PM msg=3b0c2589: G1 includes URL prefix migration `/api/v1/export*` → `/api/v2/export*`, not just file carve. - aperag/app.py: export_router mount /api/v1 → /api/v2 - aperag/domains/knowledge_base/api/export_routes.py: docstring updated to reflect D7 v2 hard-cut - aperag/domains/knowledge_base/service/export_service.py: download_url template /api/v1/... → /api/v2/... - web/src/api-v2/schema.d.ts: 3 export route key strings v1 → v2 - web/src/features/collection/client-api.ts: 2 client call paths v1 → v2 - tests/unit_test/test_web_typed_api_contract.py: assertions and comment updated to expect v2 paths post-D7 Verification: - pytest tests/unit_test/test_modularization_boundaries.py tests/unit_test/test_web_typed_api_contract.py -x → 37 passed - python -c "from aperag.app import app; ..." → 3 export endpoints now mounted at /api/v2/{collections/{id}/export, export-tasks/{id}, export-tasks/{id}/download} - grep "/api/v1/export" aperag/ tests/ web/ config/ → 0 hits (a single unrelated historical comment about KG-eval export remains in knowledge_graph/api/routes.py:259, untouched) Ghost-check: none. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 12e5671 commit 940d5bb

8 files changed

Lines changed: 76 additions & 45 deletions

File tree

aperag/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
from aperag.domains.knowledge_base.service.collection_service import (
6868
set_search_pipeline_ops as _kb_set_search_pipeline_ops,
6969
)
70+
from aperag.domains.knowledge_base.api.export_routes import router as export_router
7071
from aperag.domains.knowledge_graph.api.routes import router as knowledge_graph_router
7172
from aperag.domains.marketplace.api.routes import router as marketplace_router
7273
from aperag.domains.marketplace.service.marketplace_collection_service import (
@@ -85,7 +86,6 @@
8586
from aperag.openapi_spec import custom_generate_unique_id
8687
from aperag.service.quota_service import quota_service as _legacy_quota_service
8788
from aperag.service.search_pipeline_service import search_pipeline_service as _legacy_search_pipeline_service
88-
from aperag.views.export import router as export_router
8989
from aperag.views.prompts import router as prompts_router
9090
from aperag.views.settings import router as settings_router
9191

@@ -223,7 +223,7 @@ async def health_check():
223223

224224

225225
app.include_router(auth_router, prefix="/api/v2/auth")
226-
app.include_router(export_router, prefix="/api/v1") # Add export router
226+
app.include_router(export_router, prefix="/api/v2") # KB-domain export router (Phase 8 #47 G1, D7 v2 hard-cut)
227227
app.include_router(governance_router, prefix="/api/v1") # Governance domain router (api_key + audit)
228228
app.include_router(llm_router, prefix="/api/v1") # Model platform: embed/rerank (OpenAI-compat)
229229
app.include_router(

aperag/views/export.py renamed to aperag/domains/knowledge_base/api/export_routes.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""Export operations HTTP router (Phase 8 #47 G1 carve, D7 v2 hard-cut).
16+
17+
Carved from legacy ``aperag/views/export.py``. URLs migrated to v2
18+
(``/api/v2/collections/{id}/export`` + ``/api/v2/export-tasks/*``);
19+
mounted at ``/api/v2`` in ``aperag/app.py`` per D7 canonical
20+
(uniform ``/api/v2`` for all backend routes; OpenAI-compat
21+
``/api/v1/embeddings`` + ``/api/v1/rerank`` are the only remaining
22+
v1 allowlist).
23+
"""
24+
1525
import logging
1626

1727
from fastapi import APIRouter, Depends, Request
1828

19-
from aperag.domains.identity.db.models import User
2029
from aperag.domains.identity.service.auth_dependencies import required_user
21-
from aperag.schema import view_models
22-
from aperag.service.export_service import export_service
30+
from aperag.domains.knowledge_base.ports import AuthenticatedUser
31+
from aperag.domains.knowledge_base.schemas import ExportTaskResponse
32+
from aperag.domains.knowledge_base.service.export_service import export_service
2333

2434
logger = logging.getLogger(__name__)
2535

@@ -35,8 +45,8 @@
3545
async def create_export_task_view(
3646
request: Request,
3747
collection_id: str,
38-
user: User = Depends(required_user),
39-
) -> view_models.ExportTaskResponse:
48+
user: AuthenticatedUser = Depends(required_user),
49+
) -> ExportTaskResponse:
4050
"""Create an async export task to package all object-store files under the collection."""
4151
return await export_service.create_export_task(str(user.id), collection_id)
4252

@@ -49,8 +59,8 @@ async def create_export_task_view(
4959
async def get_export_task_view(
5060
request: Request,
5161
task_id: str,
52-
user: User = Depends(required_user),
53-
) -> view_models.ExportTaskResponse:
62+
user: AuthenticatedUser = Depends(required_user),
63+
) -> ExportTaskResponse:
5464
"""Query the status and progress of an export task."""
5565
return await export_service.get_export_task(str(user.id), task_id)
5666

@@ -63,7 +73,7 @@ async def get_export_task_view(
6373
async def download_export_view(
6474
request: Request,
6575
task_id: str,
66-
user: User = Depends(required_user),
76+
user: AuthenticatedUser = Depends(required_user),
6777
):
6878
"""Stream the completed export ZIP file to the client."""
6979
return await export_service.download_export(str(user.id), task_id)

aperag/domains/knowledge_base/schemas.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"SharingStatusResponse",
7878
"MineruTokenTestRequest",
7979
"MineruTokenTestResponse",
80+
"ExportTaskResponse",
8081
]
8182

8283

@@ -381,3 +382,33 @@ class MineruTokenTestResponse(BaseModel):
381382
default_factory=dict,
382383
description="Passthrough body from MinerU upstream",
383384
)
385+
386+
387+
# ---------- Phase 8 #47 G1: export envelope ---------- #
388+
#
389+
# Carved from ``aperag.schema.view_models.ExportTaskResponse`` so the
390+
# KB domain owns the response shape used by its export routes
391+
# (``POST /collections/{id}/export`` + ``GET /export-tasks/*``).
392+
# ``aperag.schema.view_models`` continues to re-export this name during
393+
# the cleanup window.
394+
395+
396+
class ExportTaskResponse(BaseModel):
397+
export_task_id: str = Field(..., description="Unique ID of the export task")
398+
status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED", "EXPIRED"] = Field(
399+
..., description="Current status of the export task"
400+
)
401+
progress: Optional[conint(ge=0, le=100)] = Field(None, description="Progress percentage (0-100)")
402+
message: Optional[str] = Field(None, description="Human-readable status message")
403+
error_message: Optional[str] = Field(None, description="Error detail when status is FAILED")
404+
download_url: Optional[str] = Field(
405+
None,
406+
description="URL to download the ZIP file (only set when status is COMPLETED)",
407+
)
408+
file_size: Optional[int] = Field(None, description="Size of the ZIP file in bytes")
409+
gmt_created: Optional[datetime] = None
410+
gmt_completed: Optional[datetime] = None
411+
gmt_expires: Optional[datetime] = Field(
412+
None,
413+
description="Time when the export file will be automatically deleted (7 days after creation)",
414+
)

aperag/service/export_service.py renamed to aperag/domains/knowledge_base/service/export_service.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
ExportTask,
2727
ExportTaskStatus,
2828
)
29+
from aperag.domains.knowledge_base.schemas import ExportTaskResponse
2930
from aperag.exceptions import CollectionNotFoundException, PermissionDeniedError
3031
from aperag.objectstore.base import get_async_object_store
31-
from aperag.schema import view_models
3232
from aperag.utils.utils import utc_now
3333

3434
logger = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ class ExportService:
4040
def __init__(self):
4141
self.db_ops = async_db_ops
4242

43-
async def create_export_task(self, user_id: str, collection_id: str) -> view_models.ExportTaskResponse:
43+
async def create_export_task(self, user_id: str, collection_id: str) -> ExportTaskResponse:
4444
async def _create(session):
4545
# Verify collection exists and user is owner
4646
result = await session.execute(
@@ -103,14 +103,14 @@ async def _create(session):
103103

104104
export_collection_task.delay(task.id)
105105

106-
return view_models.ExportTaskResponse(
106+
return ExportTaskResponse(
107107
export_task_id=task.id,
108108
status=task.status,
109109
progress=task.progress,
110110
message=task.message,
111111
)
112112

113-
async def get_export_task(self, user_id: str, task_id: str) -> view_models.ExportTaskResponse:
113+
async def get_export_task(self, user_id: str, task_id: str) -> ExportTaskResponse:
114114
async def _get(session):
115115
result = await session.execute(
116116
select(ExportTask).where(and_(ExportTask.id == task_id, ExportTask.user == user_id))
@@ -123,9 +123,9 @@ async def _get(session):
123123

124124
download_url = None
125125
if task.status == ExportTaskStatus.COMPLETED:
126-
download_url = f"/api/v1/export-tasks/{task_id}/download"
126+
download_url = f"/api/v2/export-tasks/{task_id}/download"
127127

128-
return view_models.ExportTaskResponse(
128+
return ExportTaskResponse(
129129
export_task_id=task.id,
130130
status=task.status,
131131
progress=task.progress,

aperag/schema/view_models.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -649,25 +649,13 @@ class EvaluationChatWithAgentResponse(RootModel[Union[ChatSuccessResponse, Agent
649649
# AgentMessage``) keep resolving the same class object.
650650

651651

652-
class ExportTaskResponse(BaseModel):
653-
export_task_id: str = Field(..., description="Unique ID of the export task")
654-
status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED", "EXPIRED"] = Field(
655-
..., description="Current status of the export task"
656-
)
657-
progress: Optional[conint(ge=0, le=100)] = Field(None, description="Progress percentage (0-100)")
658-
message: Optional[str] = Field(None, description="Human-readable status message")
659-
error_message: Optional[str] = Field(None, description="Error detail when status is FAILED")
660-
download_url: Optional[str] = Field(
661-
None,
662-
description="URL to download the ZIP file (only set when status is COMPLETED)",
663-
)
664-
file_size: Optional[int] = Field(None, description="Size of the ZIP file in bytes")
665-
gmt_created: Optional[datetime] = None
666-
gmt_completed: Optional[datetime] = None
667-
gmt_expires: Optional[datetime] = Field(
668-
None,
669-
description="Time when the export file will be automatically deleted (7 days after creation)",
670-
)
652+
# ``ExportTaskResponse`` was carved to
653+
# ``aperag.domains.knowledge_base.schemas`` in Phase 8 #47 G1 so the
654+
# KB domain owns its export envelope. The end-of-file
655+
# ``from aperag.domains.knowledge_base.schemas import (...)`` re-export
656+
# block keeps pre-migration callers
657+
# (``from aperag.schema.view_models import ExportTaskResponse``)
658+
# resolving the same class object.
671659

672660

673661
# ---------------------------------------------------------------------------
@@ -684,6 +672,9 @@ class ExportTaskResponse(BaseModel):
684672
# directly from the canonical modules; consumers outside
685673
# ``aperag/domains/**`` may continue to use either path during the
686674
# transition window.
675+
from aperag.domains.knowledge_base.schemas import ( # noqa: E402,F401
676+
ExportTaskResponse,
677+
)
687678
from aperag.domains.knowledge_graph.schemas import ( # noqa: E402,F401
688679
GraphCurationRunSummary,
689680
GraphEdge,

tests/unit_test/test_web_typed_api_contract.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -639,14 +639,13 @@ def test_collection_feature_uses_v2_typed_api_boundary():
639639
assert "from '@/features/collection/types'" in export_dialog_tsx
640640

641641
# features/collection adapter must only reach v2 typed paths and not
642-
# fall back to the old `@/api` generated SDK or raw fetch. The still-v1
643-
# export-task paths are allowed as Phase 1b typed-wrapping, not a v1→v2
644-
# rename.
642+
# fall back to the old `@/api` generated SDK or raw fetch. Export
643+
# endpoints were migrated v1→v2 by Phase 8 #47 G1 (D7 hard-cut).
645644
assert "from '@/api'" not in feature_sources
646645
assert "fetch(" not in feature_sources
647646
assert "/api/v2/collections" in feature_sources
648-
assert "'/api/v1/collections/{collection_id}/export'" in feature_sources
649-
assert "'/api/v1/export-tasks/{task_id}'" in feature_sources
647+
assert "'/api/v2/collections/{collection_id}/export'" in feature_sources
648+
assert "'/api/v2/export-tasks/{task_id}'" in feature_sources
650649

651650
# Positive: schema-derived types + fail-fast export-task adapter.
652651
types_ts = (REPO_ROOT / "web/src/features/collection/types.ts").read_text()

web/src/api-v2/schema.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ export interface paths {
549549
patch?: never;
550550
trace?: never;
551551
};
552-
"/api/v1/collections/{collection_id}/export": {
552+
"/api/v2/collections/{collection_id}/export": {
553553
parameters: {
554554
query?: never;
555555
header?: never;
@@ -569,7 +569,7 @@ export interface paths {
569569
patch?: never;
570570
trace?: never;
571571
};
572-
"/api/v1/export-tasks/{task_id}": {
572+
"/api/v2/export-tasks/{task_id}": {
573573
parameters: {
574574
query?: never;
575575
header?: never;
@@ -589,7 +589,7 @@ export interface paths {
589589
patch?: never;
590590
trace?: never;
591591
};
592-
"/api/v1/export-tasks/{task_id}/download": {
592+
"/api/v2/export-tasks/{task_id}/download": {
593593
parameters: {
594594
query?: never;
595595
header?: never;

web/src/features/collection/client-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export async function createExportTask(
121121
collectionId: string,
122122
): Promise<ExportTaskResponse> {
123123
const { data } = await browserApiClient.POST(
124-
'/api/v1/collections/{collection_id}/export',
124+
'/api/v2/collections/{collection_id}/export',
125125
{
126126
params: { path: { collection_id: collectionId } },
127127
},
@@ -140,7 +140,7 @@ export async function getExportTask(
140140
taskId: string,
141141
): Promise<ExportTaskResponse> {
142142
const { data } = await browserApiClient.GET(
143-
'/api/v1/export-tasks/{task_id}',
143+
'/api/v2/export-tasks/{task_id}',
144144
{
145145
params: { path: { task_id: taskId } },
146146
},

0 commit comments

Comments
 (0)