aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +293 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +190 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a2.dist-info/RECORD +356 -0
- aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
from contextlib import contextmanager
|
|
2
|
-
import datetime
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import mimetypes
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import shutil
|
|
9
|
-
import tempfile
|
|
10
|
-
from typing import Any, BinaryIO
|
|
11
|
-
from urllib.parse import unquote, urlparse
|
|
12
|
-
from urllib.request import url2pathname
|
|
13
|
-
|
|
14
|
-
from aethergraph.contracts.services.artifacts import Artifact
|
|
15
|
-
|
|
16
|
-
from .utils import (
|
|
17
|
-
_content_addr_dir_path,
|
|
18
|
-
_content_addr_path,
|
|
19
|
-
_maybe_cleanup_tmp_parent,
|
|
20
|
-
_now_iso,
|
|
21
|
-
_sha256_file,
|
|
22
|
-
_tree_manifest_and_hash,
|
|
23
|
-
_write_json,
|
|
24
|
-
to_thread,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _to_file_uri(path_str: str) -> str:
|
|
29
|
-
"""Canonical RFC-8089 file URI (file:///C:/..., forward slashes)."""
|
|
30
|
-
return Path(path_str).resolve().as_uri()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _from_uri_or_path(s: str) -> Path:
|
|
34
|
-
"""Robustly turn a file:// URI or plain path into a local Path."""
|
|
35
|
-
if "://" not in s:
|
|
36
|
-
return Path(s)
|
|
37
|
-
u = urlparse(s)
|
|
38
|
-
if (u.scheme or "").lower() != "file":
|
|
39
|
-
raise ValueError(f"Unsupported URI scheme: {u.scheme}")
|
|
40
|
-
# if u.netloc:
|
|
41
|
-
# raw = f"//{u.netloc}{u.path}" # UNC: file://server/share/...
|
|
42
|
-
# else:
|
|
43
|
-
# raw = u.path # Local drive: file:///C:/...
|
|
44
|
-
raw = f"//{u.netloc}{u.path}" if u.netloc else u.path
|
|
45
|
-
return Path(url2pathname(unquote(raw)))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _normalize_pretty_path(base_dir_for_pretty: str, suggested_uri: str) -> str:
|
|
49
|
-
"""Normalize a suggested_uri into a local filesystem path for pretty linking.
|
|
50
|
-
Args:
|
|
51
|
-
base_dir_for_pretty (str): Base directory to resolve relative paths against.
|
|
52
|
-
suggested_uri (str): The suggested URI, which may be a file:// URI or a relative/absolute path.
|
|
53
|
-
Returns:
|
|
54
|
-
str: The normalized local filesystem path.
|
|
55
|
-
|
|
56
|
-
Example:
|
|
57
|
-
- suggested_uri = "file://./outputs/my_artifact.txt" -> "./outputs/my_artifact.txt
|
|
58
|
-
- suggested_uri = "./outputs/my_artifact.txt" -> "./outputs/my_artifact.txt
|
|
59
|
-
- suggested_uri = "/var/data/my_artifact.txt" -> "/var/data/my_artifact.txt
|
|
60
|
-
NOTE:
|
|
61
|
-
Only used for local filesystem paths. For other URI schemes, additional handling would be needed.
|
|
62
|
-
"""
|
|
63
|
-
p = _from_uri_or_path(suggested_uri)
|
|
64
|
-
if not p.is_absolute():
|
|
65
|
-
p = Path(base_dir_for_pretty) / p
|
|
66
|
-
return str(p.resolve())
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class _Writer:
|
|
70
|
-
"""Helper class for streaming writes to a temp file."""
|
|
71
|
-
|
|
72
|
-
def __init__(self, tmp_dir: str, planned_ext: str | None):
|
|
73
|
-
self.tmp_dir = tmp_dir
|
|
74
|
-
suffix = planned_ext or ""
|
|
75
|
-
file_dir, self.tmp_path = tempfile.mkstemp(suffix=suffix, dir=tmp_dir)
|
|
76
|
-
os.close(file_dir)
|
|
77
|
-
with open(self.tmp_path, "wb") as f:
|
|
78
|
-
self._f = f
|
|
79
|
-
self._labels = {}
|
|
80
|
-
self._metrics = {}
|
|
81
|
-
|
|
82
|
-
def write(self, chunk: bytes):
|
|
83
|
-
self._f.write(chunk)
|
|
84
|
-
|
|
85
|
-
def add_labels(self, labels: dict):
|
|
86
|
-
self._labels.update(labels or {})
|
|
87
|
-
|
|
88
|
-
def add_metrics(self, metrics: dict):
|
|
89
|
-
self._metrics.update(metrics or {})
|
|
90
|
-
|
|
91
|
-
def close(self):
|
|
92
|
-
if not self._f.closed:
|
|
93
|
-
self._f.close()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class _Reader:
|
|
97
|
-
"""Helper class for reading from a file."""
|
|
98
|
-
|
|
99
|
-
def __init__(self, path: str, f: BinaryIO):
|
|
100
|
-
self._path, self._f = path, f
|
|
101
|
-
|
|
102
|
-
def read(self, n: int = -1) -> bytes:
|
|
103
|
-
return self._f.read(n)
|
|
104
|
-
|
|
105
|
-
def as_local_path(self) -> str:
|
|
106
|
-
return self._path
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class FileArtifactStoreSync:
|
|
110
|
-
"""
|
|
111
|
-
Synchronous file-based artifact store.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
def __init__(self, base_dir: str):
|
|
115
|
-
# base directory for content-addressed storage
|
|
116
|
-
self.base_dir = os.path.abspath(base_dir)
|
|
117
|
-
os.makedirs(self.base_dir, exist_ok=True)
|
|
118
|
-
|
|
119
|
-
# temporary staging area for in-progress writes
|
|
120
|
-
self._tmp_root = os.path.join(self.base_dir, "_tmp")
|
|
121
|
-
os.makedirs(self._tmp_root, exist_ok=True)
|
|
122
|
-
|
|
123
|
-
self.last_artifact: Artifact | None = None
|
|
124
|
-
|
|
125
|
-
@property
|
|
126
|
-
def base_uri(self) -> str:
|
|
127
|
-
return _to_file_uri(self.base_dir)
|
|
128
|
-
|
|
129
|
-
def tmp_path(self, suffix: str = "") -> str:
|
|
130
|
-
"""Return a temporary path for external tools to write to."""
|
|
131
|
-
os.makedirs(self._tmp_root, exist_ok=True)
|
|
132
|
-
fd, p = tempfile.mkstemp(suffix=suffix, dir=self._tmp_root)
|
|
133
|
-
os.close(fd)
|
|
134
|
-
return p
|
|
135
|
-
|
|
136
|
-
def save_file(
|
|
137
|
-
self,
|
|
138
|
-
path: str,
|
|
139
|
-
*,
|
|
140
|
-
kind: str,
|
|
141
|
-
run_id: str,
|
|
142
|
-
graph_id: str,
|
|
143
|
-
node_id: str,
|
|
144
|
-
tool_name: str,
|
|
145
|
-
tool_version: str,
|
|
146
|
-
suggested_uri: str | None = None,
|
|
147
|
-
pin: bool = False,
|
|
148
|
-
labels: dict | None = None,
|
|
149
|
-
metrics: dict | None = None,
|
|
150
|
-
preview_uri: str | None = None,
|
|
151
|
-
cleanup: bool = True,
|
|
152
|
-
) -> Artifact:
|
|
153
|
-
"""
|
|
154
|
-
Save a file into content-addressed storage and return an Artifact record.
|
|
155
|
-
Args:
|
|
156
|
-
path (str): The file path to save.
|
|
157
|
-
kind (str): The kind of artifact.
|
|
158
|
-
run_id (str): The run ID.
|
|
159
|
-
graph_id (str): The graph ID.
|
|
160
|
-
node_id (str): The node ID.
|
|
161
|
-
tool_name (str): The tool name.
|
|
162
|
-
tool_version (str): The tool version.
|
|
163
|
-
suggested_uri (Optional[str]): A suggested URI for the artifact.
|
|
164
|
-
pin (bool): Whether to pin the artifact.
|
|
165
|
-
labels (Optional[dict]): Labels to attach to the artifact.
|
|
166
|
-
metrics (Optional[dict]): Metrics to attach to the artifact.
|
|
167
|
-
preview_uri (Optional[str]): A preview URI for the artifact.
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
Artifact: The created Artifact object.
|
|
171
|
-
|
|
172
|
-
It computes the SHA-256 hash of the file, moves it into a content-addressed storage
|
|
173
|
-
structure, and optionally creates a "pretty" symlink if a suggested URI is provided.
|
|
174
|
-
"""
|
|
175
|
-
sha, nbytes = _sha256_file(path) # compute hash + size
|
|
176
|
-
ext = os.path.splitext(path)[1]
|
|
177
|
-
target = _content_addr_path(self.base_dir, sha, ext)
|
|
178
|
-
|
|
179
|
-
if not os.path.exists(target):
|
|
180
|
-
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
181
|
-
|
|
182
|
-
src_path = os.path.abspath(path)
|
|
183
|
-
src_parent = os.path.dirname(src_path)
|
|
184
|
-
|
|
185
|
-
if cleanup:
|
|
186
|
-
shutil.move(src_path, target)
|
|
187
|
-
else:
|
|
188
|
-
shutil.copy2(src_path, target)
|
|
189
|
-
# 🔐 Only clean up if the source file lived DIRECTLY under _tmp (mkstemp case).
|
|
190
|
-
# If it was inside a staged dir like _tmp/dir_xxx, DO NOT prune that dir here.
|
|
191
|
-
if cleanup and os.path.normcase(os.path.abspath(src_parent)) == os.path.normcase(
|
|
192
|
-
os.path.abspath(self._tmp_root)
|
|
193
|
-
):
|
|
194
|
-
_maybe_cleanup_tmp_parent(self._tmp_root, src_path)
|
|
195
|
-
|
|
196
|
-
mime, _ = mimetypes.guess_type(target)
|
|
197
|
-
uri = _to_file_uri(target)
|
|
198
|
-
|
|
199
|
-
# optional "pretty" mirror path (symlink) if suggested
|
|
200
|
-
if suggested_uri:
|
|
201
|
-
pretty = _normalize_pretty_path(self.base_dir, suggested_uri)
|
|
202
|
-
os.makedirs(os.path.dirname(pretty), exist_ok=True)
|
|
203
|
-
if not os.path.exists(pretty):
|
|
204
|
-
try:
|
|
205
|
-
os.symlink(target, pretty)
|
|
206
|
-
except OSError:
|
|
207
|
-
shutil.copy2(target, pretty)
|
|
208
|
-
|
|
209
|
-
# ✅ Remove this unconditional cleanup (it could wipe staged dirs):
|
|
210
|
-
# _maybe_cleanup_tmp_parent(self._tmp_root, path)
|
|
211
|
-
|
|
212
|
-
# Ensure _tmp exists for future staging even if it was emptied
|
|
213
|
-
os.makedirs(self._tmp_root, exist_ok=True)
|
|
214
|
-
|
|
215
|
-
a = Artifact(
|
|
216
|
-
artifact_id=sha,
|
|
217
|
-
uri=uri,
|
|
218
|
-
kind=kind,
|
|
219
|
-
bytes=nbytes,
|
|
220
|
-
sha256=sha,
|
|
221
|
-
mime=mime,
|
|
222
|
-
run_id=run_id,
|
|
223
|
-
graph_id=graph_id,
|
|
224
|
-
node_id=node_id,
|
|
225
|
-
tool_name=tool_name,
|
|
226
|
-
tool_version=tool_version,
|
|
227
|
-
created_at=_now_iso(),
|
|
228
|
-
labels=labels or {},
|
|
229
|
-
metrics=metrics or {},
|
|
230
|
-
preview_uri=preview_uri,
|
|
231
|
-
pinned=pin,
|
|
232
|
-
)
|
|
233
|
-
self.last_artifact = a
|
|
234
|
-
return a
|
|
235
|
-
|
|
236
|
-
def save_text(self, payload: str, *, suggested_uri: str | None = None):
|
|
237
|
-
"""Save a text payload as an artifact."""
|
|
238
|
-
staged_path = self.tmp_path(suffix=".txt")
|
|
239
|
-
with open(staged_path, "w", encoding="utf-8") as f:
|
|
240
|
-
f.write(payload)
|
|
241
|
-
a = self.save_file(
|
|
242
|
-
path=staged_path,
|
|
243
|
-
kind="text",
|
|
244
|
-
run_id="ad-hoc",
|
|
245
|
-
graph_id="ad-hoc",
|
|
246
|
-
node_id="ad-hoc",
|
|
247
|
-
tool_name="fs_store.save_text",
|
|
248
|
-
tool_version="0.1.0",
|
|
249
|
-
suggested_uri=suggested_uri,
|
|
250
|
-
cleanup=True,
|
|
251
|
-
)
|
|
252
|
-
return a
|
|
253
|
-
|
|
254
|
-
def save_json(self, payload: str, *, suggested_uri: str | None = None):
|
|
255
|
-
"""Save a JSON payload as an artifact."""
|
|
256
|
-
import json
|
|
257
|
-
|
|
258
|
-
staged_path = self.tmp_path(suffix=".json")
|
|
259
|
-
with open(staged_path, "w", encoding="utf-8") as f:
|
|
260
|
-
json.dump(payload, f, indent=2)
|
|
261
|
-
a = self.save_file(
|
|
262
|
-
path=staged_path,
|
|
263
|
-
kind="json",
|
|
264
|
-
run_id="ad-hoc",
|
|
265
|
-
graph_id="ad-hoc",
|
|
266
|
-
node_id="ad-hoc",
|
|
267
|
-
tool_name="fs_store.save_json",
|
|
268
|
-
tool_version="0.1.0",
|
|
269
|
-
suggested_uri=suggested_uri,
|
|
270
|
-
cleanup=True,
|
|
271
|
-
)
|
|
272
|
-
return a
|
|
273
|
-
|
|
274
|
-
@contextmanager
|
|
275
|
-
def open_writer(
|
|
276
|
-
self,
|
|
277
|
-
*,
|
|
278
|
-
kind: str,
|
|
279
|
-
run_id: str,
|
|
280
|
-
graph_id: str,
|
|
281
|
-
node_id: str,
|
|
282
|
-
tool_name: str,
|
|
283
|
-
tool_version: str,
|
|
284
|
-
planned_ext: str | None = None,
|
|
285
|
-
pin: bool = False,
|
|
286
|
-
):
|
|
287
|
-
"""Context manager that yields a streaming ArtifactWriter."""
|
|
288
|
-
w = _Writer(self._tmp_root, planned_ext)
|
|
289
|
-
try:
|
|
290
|
-
yield w
|
|
291
|
-
w.close()
|
|
292
|
-
sha, nbytes = _sha256_file(w.tmp_path)
|
|
293
|
-
target = _content_addr_path(self.base_dir, sha, planned_ext)
|
|
294
|
-
if not os.path.exists(target):
|
|
295
|
-
shutil.move(w.tmp_path, target)
|
|
296
|
-
_maybe_cleanup_tmp_parent(self._tmp_root, w.tmp_path)
|
|
297
|
-
else:
|
|
298
|
-
os.remove(w.tmp_path) # already present => dedup
|
|
299
|
-
_maybe_cleanup_tmp_parent(self._tmp_root, w.tmp_path)
|
|
300
|
-
mime, _ = mimetypes.guess_type(target)
|
|
301
|
-
a = Artifact(
|
|
302
|
-
artifact_id=sha,
|
|
303
|
-
uri=_to_file_uri(target),
|
|
304
|
-
kind=kind,
|
|
305
|
-
bytes=nbytes,
|
|
306
|
-
sha256=sha,
|
|
307
|
-
mime=mime,
|
|
308
|
-
run_id=run_id,
|
|
309
|
-
graph_id=graph_id,
|
|
310
|
-
node_id=node_id,
|
|
311
|
-
tool_name=tool_name,
|
|
312
|
-
tool_version=tool_version,
|
|
313
|
-
created_at=_now_iso(),
|
|
314
|
-
labels=w._labels,
|
|
315
|
-
metrics=w._metrics,
|
|
316
|
-
pinned=pin,
|
|
317
|
-
)
|
|
318
|
-
# stash on the writer so caller can grab it after context exits
|
|
319
|
-
w._artifact = a
|
|
320
|
-
self.last_artifact = a
|
|
321
|
-
except Exception:
|
|
322
|
-
try:
|
|
323
|
-
w.close()
|
|
324
|
-
if os.path.exists(w.tmp_path):
|
|
325
|
-
os.remove(w.tmp_path)
|
|
326
|
-
finally:
|
|
327
|
-
raise
|
|
328
|
-
|
|
329
|
-
@contextmanager
|
|
330
|
-
def open_reader(self, uri: str):
|
|
331
|
-
"""Context manager that yields an ArtifactReader for a given URI."""
|
|
332
|
-
path = _from_uri_or_path(uri)
|
|
333
|
-
if os.path.isdir(path):
|
|
334
|
-
raise IsADirectoryError(f"Expected file, got directory: {path}")
|
|
335
|
-
# use a 'with' so the file is closed automatically even if yield is interrupted
|
|
336
|
-
with open(path, "rb") as f:
|
|
337
|
-
yield _Reader(str(path), f)
|
|
338
|
-
|
|
339
|
-
# --------------------- advanced flow for external tools ------------------
|
|
340
|
-
def plan_staging_path(self, planned_ext: str = "") -> str:
|
|
341
|
-
"""Return a temp path that an external tool can write to directly."""
|
|
342
|
-
os.makedirs(self._tmp_root, exist_ok=True) # ensure _tmp exists
|
|
343
|
-
return self.tmp_path(suffix=planned_ext)
|
|
344
|
-
|
|
345
|
-
def ingest_staged_file(
|
|
346
|
-
self,
|
|
347
|
-
staged_path: str,
|
|
348
|
-
*,
|
|
349
|
-
kind: str,
|
|
350
|
-
run_id: str,
|
|
351
|
-
graph_id: str,
|
|
352
|
-
node_id: str,
|
|
353
|
-
tool_name: str,
|
|
354
|
-
tool_version: str,
|
|
355
|
-
pin: bool = False,
|
|
356
|
-
labels: dict | None = None,
|
|
357
|
-
metrics: dict | None = None,
|
|
358
|
-
preview_uri: str | None = None,
|
|
359
|
-
suggested_uri: str | None = None,
|
|
360
|
-
) -> Artifact:
|
|
361
|
-
"""Turn a staged file into a content-addressed artifact + (optional) pretty link."""
|
|
362
|
-
return self.save_file(
|
|
363
|
-
path=staged_path,
|
|
364
|
-
kind=kind,
|
|
365
|
-
run_id=run_id,
|
|
366
|
-
graph_id=graph_id,
|
|
367
|
-
node_id=node_id,
|
|
368
|
-
tool_name=tool_name,
|
|
369
|
-
tool_version=tool_version,
|
|
370
|
-
suggested_uri=suggested_uri,
|
|
371
|
-
pin=pin,
|
|
372
|
-
labels=labels,
|
|
373
|
-
metrics=metrics,
|
|
374
|
-
preview_uri=preview_uri,
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
def plan_staging_dir(self, suffix: str = "") -> str:
|
|
378
|
-
"""Return an empty directory path that an external tool can write into."""
|
|
379
|
-
# make a unique folder under _tmp
|
|
380
|
-
d = tempfile.mkdtemp(prefix="dir_", suffix=suffix, dir=self._tmp_root)
|
|
381
|
-
return d
|
|
382
|
-
|
|
383
|
-
def ingest_directory(
|
|
384
|
-
self,
|
|
385
|
-
*,
|
|
386
|
-
staged_dir: str,
|
|
387
|
-
kind: str = "dataset",
|
|
388
|
-
run_id: str,
|
|
389
|
-
graph_id: str,
|
|
390
|
-
node_id: str,
|
|
391
|
-
tool_name: str,
|
|
392
|
-
tool_version: str,
|
|
393
|
-
include: list[str] | None = None,
|
|
394
|
-
exclude: list[str] | None = None,
|
|
395
|
-
index_children: bool = False,
|
|
396
|
-
pin: bool = False,
|
|
397
|
-
labels: dict | None = None,
|
|
398
|
-
metrics: dict | None = None,
|
|
399
|
-
suggested_uri: str | None = None,
|
|
400
|
-
archive: bool = False,
|
|
401
|
-
archive_name: str = "bundle.tar.gz",
|
|
402
|
-
cleanup: bool = True,
|
|
403
|
-
store: str
|
|
404
|
-
| None = None, # NEW: "archive" | "copy" | "manifest"; None -> derive from 'archive'
|
|
405
|
-
) -> Artifact:
|
|
406
|
-
if not os.path.isdir(staged_dir):
|
|
407
|
-
raise ValueError(f"ingest_directory: not a directory: {staged_dir}")
|
|
408
|
-
|
|
409
|
-
if store is None:
|
|
410
|
-
store = "archive" if archive else "manifest" # previous default was manifest-only
|
|
411
|
-
|
|
412
|
-
manifest_entries, tree_sha = _tree_manifest_and_hash(staged_dir, include, exclude)
|
|
413
|
-
cas_dir = _content_addr_dir_path(self.base_dir, tree_sha)
|
|
414
|
-
manifest_path = os.path.join(cas_dir, "manifest.json")
|
|
415
|
-
if not os.path.exists(manifest_path):
|
|
416
|
-
_write_json(
|
|
417
|
-
manifest_path,
|
|
418
|
-
{
|
|
419
|
-
"files": manifest_entries,
|
|
420
|
-
"created_at": _now_iso(),
|
|
421
|
-
"tool_name": tool_name,
|
|
422
|
-
"tool_version": tool_version,
|
|
423
|
-
},
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
archive_uri = None
|
|
427
|
-
if store == "archive":
|
|
428
|
-
archive_path = os.path.join(cas_dir, archive_name)
|
|
429
|
-
if not os.path.exists(archive_path):
|
|
430
|
-
import tarfile
|
|
431
|
-
|
|
432
|
-
with tarfile.open(archive_path, mode="w:gz") as tar:
|
|
433
|
-
for e in sorted(manifest_entries, key=lambda x: x["path"]):
|
|
434
|
-
abs_file = os.path.join(staged_dir, e["path"])
|
|
435
|
-
tar.add(abs_file, arcname=e["path"])
|
|
436
|
-
archive_uri = _to_file_uri(archive_path)
|
|
437
|
-
|
|
438
|
-
elif store == "copy":
|
|
439
|
-
dst_root = os.path.join(cas_dir, "tree")
|
|
440
|
-
for e in manifest_entries:
|
|
441
|
-
src = os.path.join(staged_dir, e["path"])
|
|
442
|
-
dst = os.path.join(dst_root, e["path"])
|
|
443
|
-
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
444
|
-
if not os.path.exists(dst):
|
|
445
|
-
shutil.copy2(src, dst)
|
|
446
|
-
|
|
447
|
-
elif store == "manifest":
|
|
448
|
-
if cleanup:
|
|
449
|
-
raise ValueError(
|
|
450
|
-
"store='manifest' with cleanup=True would lose bytes; set cleanup=False or use store='archive'/'copy'."
|
|
451
|
-
)
|
|
452
|
-
else:
|
|
453
|
-
raise ValueError(f"unknown store mode: {store}")
|
|
454
|
-
|
|
455
|
-
# Pretty link: try to symlink the whole directory to CAS dir (best UX)
|
|
456
|
-
if suggested_uri:
|
|
457
|
-
pretty_dir = _normalize_pretty_path(self.base_dir, suggested_uri)
|
|
458
|
-
parent = os.path.dirname(pretty_dir)
|
|
459
|
-
os.makedirs(parent, exist_ok=True)
|
|
460
|
-
try:
|
|
461
|
-
# prefer a directory symlink if pretty path doesn't exist
|
|
462
|
-
if not os.path.lexists(pretty_dir): # avoid overwriting
|
|
463
|
-
os.symlink(cas_dir, pretty_dir, target_is_directory=True)
|
|
464
|
-
except OSError:
|
|
465
|
-
# Fallback: ensure dir exists and link/copy small files inside
|
|
466
|
-
os.makedirs(pretty_dir, exist_ok=True)
|
|
467
|
-
pm = os.path.join(pretty_dir, "manifest.json")
|
|
468
|
-
if not os.path.exists(pm):
|
|
469
|
-
try:
|
|
470
|
-
os.symlink(manifest_path, pm)
|
|
471
|
-
except OSError:
|
|
472
|
-
shutil.copy2(manifest_path, pm)
|
|
473
|
-
|
|
474
|
-
if store == "archive" and archive_uri:
|
|
475
|
-
pa = os.path.join(pretty_dir, archive_name)
|
|
476
|
-
if not os.path.exists(pa):
|
|
477
|
-
src = archive_uri[len("file://") :]
|
|
478
|
-
try:
|
|
479
|
-
os.symlink(src, pa)
|
|
480
|
-
except OSError:
|
|
481
|
-
shutil.copy2(src, pa)
|
|
482
|
-
|
|
483
|
-
elif store == "copy":
|
|
484
|
-
# copy small files (under 1MB) for convenience
|
|
485
|
-
for e in manifest_entries:
|
|
486
|
-
if e["bytes"] <= 1024 * 1024:
|
|
487
|
-
src = os.path.join(cas_dir, "tree", e["path"])
|
|
488
|
-
dst = os.path.join(pretty_dir, e["path"])
|
|
489
|
-
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
490
|
-
if not os.path.exists(dst):
|
|
491
|
-
try:
|
|
492
|
-
os.symlink(src, dst)
|
|
493
|
-
except OSError:
|
|
494
|
-
shutil.copy2(src, dst)
|
|
495
|
-
|
|
496
|
-
total_bytes = sum(e["bytes"] for e in manifest_entries)
|
|
497
|
-
a = Artifact(
|
|
498
|
-
artifact_id=tree_sha,
|
|
499
|
-
uri=_to_file_uri(cas_dir),
|
|
500
|
-
kind=kind,
|
|
501
|
-
bytes=total_bytes,
|
|
502
|
-
sha256=tree_sha,
|
|
503
|
-
mime="application/vnd.aethergraph.bundle+dir",
|
|
504
|
-
run_id=run_id,
|
|
505
|
-
graph_id=graph_id,
|
|
506
|
-
node_id=node_id,
|
|
507
|
-
tool_name=tool_name,
|
|
508
|
-
tool_version=tool_version,
|
|
509
|
-
created_at=_now_iso(),
|
|
510
|
-
labels=labels or {},
|
|
511
|
-
metrics=metrics or {},
|
|
512
|
-
preview_uri=archive_uri,
|
|
513
|
-
pinned=pin,
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
self.last_artifact = a
|
|
517
|
-
|
|
518
|
-
if cleanup and store in ("archive", "copy"):
|
|
519
|
-
try:
|
|
520
|
-
shutil.rmtree(staged_dir, ignore_errors=True)
|
|
521
|
-
except Exception:
|
|
522
|
-
logger = logging.getLogger("aethergraph.services.artifacts.fs_store")
|
|
523
|
-
logger.warning(f"ingest_directory: failed to cleanup staged dir: {staged_dir}")
|
|
524
|
-
|
|
525
|
-
return a
|
|
526
|
-
|
|
527
|
-
def cleanup_tmp(self, max_age_hours: int = 24):
|
|
528
|
-
now = datetime.now(datetime.timezone.utc).timestamp()
|
|
529
|
-
for p in Path(self._tmp_root).rglob("*"):
|
|
530
|
-
try:
|
|
531
|
-
age_h = (now - p.stat().st_mtime) / 3600.0
|
|
532
|
-
if age_h > max_age_hours:
|
|
533
|
-
if p.is_file():
|
|
534
|
-
p.unlink(missing_ok=True)
|
|
535
|
-
else:
|
|
536
|
-
shutil.rmtree(p, ignore_errors=True)
|
|
537
|
-
except Exception:
|
|
538
|
-
pass
|
|
539
|
-
|
|
540
|
-
def load_bytes(self, uri: str) -> bytes:
|
|
541
|
-
path = _from_uri_or_path(uri)
|
|
542
|
-
if os.path.isdir(path):
|
|
543
|
-
raise IsADirectoryError(f"Expected file, got directory: {path}")
|
|
544
|
-
with open(path, "rb") as f:
|
|
545
|
-
return f.read()
|
|
546
|
-
|
|
547
|
-
def load_text(
|
|
548
|
-
self,
|
|
549
|
-
uri: str,
|
|
550
|
-
*,
|
|
551
|
-
encoding: str = "utf-8",
|
|
552
|
-
errors: str = "strict",
|
|
553
|
-
) -> str:
|
|
554
|
-
data = self.load_bytes(uri)
|
|
555
|
-
return data.decode(encoding, errors)
|
|
556
|
-
|
|
557
|
-
def load_json(
|
|
558
|
-
self,
|
|
559
|
-
uri: str,
|
|
560
|
-
*,
|
|
561
|
-
encoding: str = "utf-8",
|
|
562
|
-
errors: str = "strict",
|
|
563
|
-
) -> Any:
|
|
564
|
-
text = self.load_text(uri, encoding=encoding, errors=errors)
|
|
565
|
-
return json.loads(text)
|
|
566
|
-
|
|
567
|
-
def load_artifact(self, uri: str) -> str | bytes:
|
|
568
|
-
"""Load an artifact by URI.
|
|
569
|
-
|
|
570
|
-
- If it's a directory, return the directory path as a string.
|
|
571
|
-
- If it's a file, return the file contents as bytes.
|
|
572
|
-
"""
|
|
573
|
-
path = _from_uri_or_path(uri)
|
|
574
|
-
if os.path.isdir(path):
|
|
575
|
-
return path
|
|
576
|
-
with open(path, "rb") as f:
|
|
577
|
-
return f.read()
|
|
578
|
-
|
|
579
|
-
def load_artifact_bytes(self, uri: str) -> bytes:
|
|
580
|
-
"""Load a file artifact and return its bytes.
|
|
581
|
-
|
|
582
|
-
Raises:
|
|
583
|
-
IsADirectoryError: if the URI points to a directory.
|
|
584
|
-
"""
|
|
585
|
-
path = _from_uri_or_path(uri)
|
|
586
|
-
if os.path.isdir(path):
|
|
587
|
-
raise IsADirectoryError(f"Expected file, got directory: {path}")
|
|
588
|
-
with open(path, "rb") as f:
|
|
589
|
-
return f.read()
|
|
590
|
-
|
|
591
|
-
def load_artifact_dir(self, uri: str) -> str:
|
|
592
|
-
"""Return the path when the artifact is a directory."""
|
|
593
|
-
path = _from_uri_or_path(uri)
|
|
594
|
-
if not os.path.isdir(path):
|
|
595
|
-
raise NotADirectoryError(f"Expected directory, got file: {path}")
|
|
596
|
-
return path
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
class FSArtifactStore: # implements AsyncArtifactStore
|
|
600
|
-
def __init__(self, base_dir: str):
|
|
601
|
-
self._sync = FileArtifactStoreSync(base_dir)
|
|
602
|
-
|
|
603
|
-
@property
|
|
604
|
-
def base_uri(self) -> str:
|
|
605
|
-
return self._sync.base_uri
|
|
606
|
-
|
|
607
|
-
def tmp_path(self, suffix: str = "") -> str:
|
|
608
|
-
return self._sync.tmp_path(suffix=suffix)
|
|
609
|
-
|
|
610
|
-
async def save_file(self, **kw) -> Any:
|
|
611
|
-
return await to_thread(self._sync.save_file, **kw)
|
|
612
|
-
|
|
613
|
-
async def save_text(self, **kw) -> Any:
|
|
614
|
-
return await to_thread(self._sync.save_text, **kw)
|
|
615
|
-
|
|
616
|
-
async def save_json(self, **kw) -> Any:
|
|
617
|
-
return await to_thread(self._sync.save_json, **kw)
|
|
618
|
-
|
|
619
|
-
async def open_writer(self, **kw):
|
|
620
|
-
# Wrap the sync contextmanager so 'with' usage in Facade stays the same.
|
|
621
|
-
# Return the sync contextmanager directly; user code runs inside with-block
|
|
622
|
-
# but all disk ops inside are already sync and cheap; or expose an async CM.
|
|
623
|
-
return self._sync.open_writer(**kw)
|
|
624
|
-
|
|
625
|
-
async def plan_staging_path(self, planned_ext: str = "") -> str:
|
|
626
|
-
return await to_thread(self._sync.plan_staging_path, planned_ext)
|
|
627
|
-
|
|
628
|
-
async def ingest_staged_file(self, **kw) -> Any:
|
|
629
|
-
return await to_thread(self._sync.ingest_staged_file, **kw)
|
|
630
|
-
|
|
631
|
-
async def plan_staging_dir(self, suffix: str = "") -> str:
|
|
632
|
-
return await to_thread(self._sync.plan_staging_dir, suffix)
|
|
633
|
-
|
|
634
|
-
async def ingest_directory(self, **kw) -> Any:
|
|
635
|
-
return await to_thread(self._sync.ingest_directory, **kw)
|
|
636
|
-
|
|
637
|
-
async def load_bytes(self, uri: str) -> bytes:
|
|
638
|
-
return await to_thread(self._sync.load_bytes, uri)
|
|
639
|
-
|
|
640
|
-
async def load_text(self, uri: str, *, encoding: str = "utf-8", errors: str = "strict") -> str:
|
|
641
|
-
return await to_thread(self._sync.load_text, uri, encoding=encoding, errors=errors)
|
|
642
|
-
|
|
643
|
-
async def load_json(self, uri: str, *, encoding: str = "utf-8", errors: str = "strict") -> Any:
|
|
644
|
-
return await to_thread(self._sync.load_json, uri, encoding=encoding, errors=errors)
|
|
645
|
-
|
|
646
|
-
async def load_artifact(self, uri: str):
|
|
647
|
-
return await to_thread(self._sync.load_artifact, uri)
|
|
648
|
-
|
|
649
|
-
async def load_artifact_bytes(self, uri: str) -> bytes:
|
|
650
|
-
return await to_thread(self._sync.load_artifact_bytes, uri)
|
|
651
|
-
|
|
652
|
-
async def load_artifact_dir(self, uri: str) -> str:
|
|
653
|
-
return await to_thread(self._sync.load_artifact_dir, uri)
|
|
654
|
-
|
|
655
|
-
async def cleanup_tmp(self, max_age_hours: int = 24):
|
|
656
|
-
return await to_thread(self._sync.cleanup_tmp, max_age_hours)
|