aria-code 4.1.3__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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
artifacts.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Local artifact paths for reports, charts, projects, and strategy output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def slugify_topic(topic: Optional[str], fallback: str = "general") -> str:
|
|
15
|
+
raw = str(topic or "").strip()
|
|
16
|
+
if not raw:
|
|
17
|
+
raw = fallback
|
|
18
|
+
raw = re.sub(r"[^A-Za-z0-9._\-\u4e00-\u9fff]+", "-", raw)
|
|
19
|
+
raw = raw.strip("-._")
|
|
20
|
+
return raw[:80] or fallback
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ArtifactRecord:
|
|
25
|
+
"""Resolved paths for one generated artifact bundle."""
|
|
26
|
+
|
|
27
|
+
category: str
|
|
28
|
+
topic: str
|
|
29
|
+
directory: Path
|
|
30
|
+
path: Path
|
|
31
|
+
metadata_path: Path
|
|
32
|
+
raw_data_path: Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ArtifactEntry:
|
|
37
|
+
"""Parsed artifact metadata with resolved paths and file state."""
|
|
38
|
+
|
|
39
|
+
metadata_path: Path
|
|
40
|
+
path: Path
|
|
41
|
+
raw_data_path: Path
|
|
42
|
+
kind: str
|
|
43
|
+
status: str
|
|
44
|
+
topic: str
|
|
45
|
+
created_at: str
|
|
46
|
+
mtime: float
|
|
47
|
+
size_bytes: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _safe_stat(path: Path) -> Optional[os.stat_result]:
|
|
51
|
+
try:
|
|
52
|
+
return path.stat()
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _artifact_entry_from_metadata(path: Path) -> Optional[ArtifactEntry]:
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
60
|
+
artifact = data.get("artifact") or {}
|
|
61
|
+
output_path = Path(str(artifact.get("path") or ""))
|
|
62
|
+
metadata_path = Path(str(artifact.get("metadata_path") or path))
|
|
63
|
+
raw_path = Path(str(artifact.get("raw_data_path") or path.with_suffix(".raw_data.json")))
|
|
64
|
+
output_stat = _safe_stat(output_path)
|
|
65
|
+
meta_stat = _safe_stat(metadata_path)
|
|
66
|
+
raw_stat = _safe_stat(raw_path)
|
|
67
|
+
mtime = 0.0
|
|
68
|
+
for stat in (output_stat, meta_stat, raw_stat):
|
|
69
|
+
if stat is not None:
|
|
70
|
+
mtime = max(mtime, float(stat.st_mtime))
|
|
71
|
+
size_bytes = 0
|
|
72
|
+
for stat in (output_stat, meta_stat, raw_stat):
|
|
73
|
+
if stat is not None:
|
|
74
|
+
size_bytes += int(stat.st_size)
|
|
75
|
+
return ArtifactEntry(
|
|
76
|
+
metadata_path=metadata_path,
|
|
77
|
+
path=output_path,
|
|
78
|
+
raw_data_path=raw_path,
|
|
79
|
+
kind=str(data.get("kind") or artifact.get("category") or "artifact"),
|
|
80
|
+
status=str(data.get("status") or "unknown"),
|
|
81
|
+
topic=str(artifact.get("topic") or data.get("symbol") or ""),
|
|
82
|
+
created_at=str(data.get("created_at") or ""),
|
|
83
|
+
mtime=mtime or float(meta_stat.st_mtime if meta_stat else 0),
|
|
84
|
+
size_bytes=size_bytes,
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cleanup_empty_dirs(start: Path, stop: Path) -> None:
|
|
91
|
+
current = start
|
|
92
|
+
try:
|
|
93
|
+
stop = stop.resolve()
|
|
94
|
+
except Exception:
|
|
95
|
+
return
|
|
96
|
+
while True:
|
|
97
|
+
try:
|
|
98
|
+
current = current.resolve()
|
|
99
|
+
except Exception:
|
|
100
|
+
break
|
|
101
|
+
if current == stop or stop not in current.parents:
|
|
102
|
+
break
|
|
103
|
+
try:
|
|
104
|
+
current.rmdir()
|
|
105
|
+
except Exception:
|
|
106
|
+
break
|
|
107
|
+
current = current.parent
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _project_artifact_root() -> Optional[Path]:
|
|
111
|
+
"""Return project-level artifact root from .ariarc when configured."""
|
|
112
|
+
try:
|
|
113
|
+
from ariarc import AriaRC
|
|
114
|
+
|
|
115
|
+
rc = AriaRC.load()
|
|
116
|
+
source = rc.source_path
|
|
117
|
+
if not source:
|
|
118
|
+
return None
|
|
119
|
+
data = getattr(rc, "_data", None)
|
|
120
|
+
if not isinstance(data, dict):
|
|
121
|
+
text = source.read_text(encoding="utf-8")
|
|
122
|
+
import json as _json
|
|
123
|
+
import re as _re
|
|
124
|
+
text = _re.sub(r"/\*.*?\*/", "", text, flags=_re.DOTALL)
|
|
125
|
+
text = _re.sub(r'(?<!:)(?<!https)//[^\n]*', "", text)
|
|
126
|
+
data = _json.loads(text)
|
|
127
|
+
configured = data.get("artifact_root") or data.get("output_dir")
|
|
128
|
+
if configured:
|
|
129
|
+
path = Path(str(configured)).expanduser()
|
|
130
|
+
if not path.is_absolute():
|
|
131
|
+
path = source.parent / path
|
|
132
|
+
return path
|
|
133
|
+
return source.parent / "aria-output"
|
|
134
|
+
except Exception:
|
|
135
|
+
return None
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def artifact_root() -> Path:
|
|
140
|
+
"""Return the per-user local artifact root.
|
|
141
|
+
|
|
142
|
+
Override with ARIA_ARTIFACT_ROOT when a user wants reports/projects under a
|
|
143
|
+
specific workspace. Defaults to a product-owned folder under that user's
|
|
144
|
+
Documents directory, not the developer's Arthera repo.
|
|
145
|
+
"""
|
|
146
|
+
configured = os.getenv("ARIA_ARTIFACT_ROOT")
|
|
147
|
+
if configured:
|
|
148
|
+
return Path(configured).expanduser()
|
|
149
|
+
project_root = _project_artifact_root()
|
|
150
|
+
if project_root:
|
|
151
|
+
return project_root
|
|
152
|
+
return Path.home() / "Documents" / "Aria Code"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def user_output_root() -> Path:
|
|
156
|
+
"""Return a user-owned output root that never falls back to project cwd.
|
|
157
|
+
|
|
158
|
+
Use this for generated code, strategies, and project scaffolds. Reports and
|
|
159
|
+
charts may still honor project `.ariarc` via `artifact_root()`, but user
|
|
160
|
+
code should not silently land in the Aria source checkout.
|
|
161
|
+
"""
|
|
162
|
+
configured = os.getenv("ARIA_USER_OUTPUT_ROOT")
|
|
163
|
+
if configured:
|
|
164
|
+
return Path(configured).expanduser()
|
|
165
|
+
return Path.home() / "Documents" / "Aria Code"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def user_projects_dir(create: bool = True) -> Path:
|
|
169
|
+
path = user_output_root() / "projects"
|
|
170
|
+
if create:
|
|
171
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
return path
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def user_generated_dir(create: bool = True) -> Path:
|
|
176
|
+
path = user_output_root() / "generated"
|
|
177
|
+
if create:
|
|
178
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def artifact_roots(*, include_user_generated: bool = True) -> list[Path]:
|
|
183
|
+
"""Return unique artifact roots that should be visible to users."""
|
|
184
|
+
roots: list[Path] = []
|
|
185
|
+
for root in (artifact_root(), user_generated_dir(create=False) if include_user_generated else None):
|
|
186
|
+
if root is None:
|
|
187
|
+
continue
|
|
188
|
+
try:
|
|
189
|
+
resolved = root.expanduser().resolve()
|
|
190
|
+
except Exception:
|
|
191
|
+
resolved = root.expanduser()
|
|
192
|
+
if not any(existing == resolved for existing in roots):
|
|
193
|
+
roots.append(resolved)
|
|
194
|
+
return roots
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def artifact_dir(category: str, topic: Optional[str] = None, create: bool = True) -> Path:
|
|
198
|
+
parts = [slugify_topic(part) for part in str(category or "artifacts").split("/") if part]
|
|
199
|
+
base = artifact_root().joinpath(*parts)
|
|
200
|
+
if topic:
|
|
201
|
+
base = base / slugify_topic(topic)
|
|
202
|
+
if create:
|
|
203
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
return base
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def create_artifact(
|
|
208
|
+
category: str,
|
|
209
|
+
topic: Optional[str],
|
|
210
|
+
stem: str,
|
|
211
|
+
suffix: str,
|
|
212
|
+
*,
|
|
213
|
+
timestamp: Optional[datetime] = None,
|
|
214
|
+
create: bool = True,
|
|
215
|
+
) -> ArtifactRecord:
|
|
216
|
+
"""Create a dated artifact bundle path and its sidecar metadata paths.
|
|
217
|
+
|
|
218
|
+
Output layout:
|
|
219
|
+
|
|
220
|
+
<root>/<category>/<topic>/YYYY-MM-DD/<HHMMSS>_<stem><suffix>
|
|
221
|
+
<root>/<category>/<topic>/YYYY-MM-DD/<HHMMSS>_<stem>.metadata.json
|
|
222
|
+
<root>/<category>/<topic>/YYYY-MM-DD/<HHMMSS>_<stem>.raw_data.json
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
ts = timestamp or datetime.now()
|
|
226
|
+
topic_slug = slugify_topic(topic)
|
|
227
|
+
stem_slug = slugify_topic(stem, fallback="artifact")
|
|
228
|
+
suffix = suffix if suffix.startswith(".") else f".{suffix}"
|
|
229
|
+
directory = artifact_dir(category, topic_slug, create=False) / ts.strftime("%Y-%m-%d")
|
|
230
|
+
prefix = ts.strftime("%H%M%S")
|
|
231
|
+
base_name = f"{prefix}_{stem_slug}"
|
|
232
|
+
if create:
|
|
233
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
return ArtifactRecord(
|
|
235
|
+
category=category,
|
|
236
|
+
topic=topic_slug,
|
|
237
|
+
directory=directory,
|
|
238
|
+
path=directory / f"{base_name}{suffix}",
|
|
239
|
+
metadata_path=directory / f"{base_name}.metadata.json",
|
|
240
|
+
raw_data_path=directory / f"{base_name}.raw_data.json",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def create_user_artifact(
|
|
245
|
+
category: str,
|
|
246
|
+
topic: Optional[str],
|
|
247
|
+
stem: str,
|
|
248
|
+
suffix: str,
|
|
249
|
+
*,
|
|
250
|
+
timestamp: Optional[datetime] = None,
|
|
251
|
+
create: bool = True,
|
|
252
|
+
) -> ArtifactRecord:
|
|
253
|
+
"""Create an artifact under the user-owned output root.
|
|
254
|
+
|
|
255
|
+
Unlike `create_artifact`, this intentionally ignores project `.ariarc`
|
|
256
|
+
output settings. Use it for charts, scripts, and generated assets the user
|
|
257
|
+
expects to find in their local Aria output folder rather than the source
|
|
258
|
+
checkout.
|
|
259
|
+
"""
|
|
260
|
+
ts = timestamp or datetime.now()
|
|
261
|
+
parts = [slugify_topic(part) for part in str(category or "generated").split("/") if part]
|
|
262
|
+
topic_slug = slugify_topic(topic)
|
|
263
|
+
stem_slug = slugify_topic(stem, fallback="artifact")
|
|
264
|
+
suffix = suffix if suffix.startswith(".") else f".{suffix}"
|
|
265
|
+
directory = user_generated_dir(create=False).joinpath(*parts, topic_slug, ts.strftime("%Y-%m-%d"))
|
|
266
|
+
prefix = ts.strftime("%H%M%S")
|
|
267
|
+
base_name = f"{prefix}_{stem_slug}"
|
|
268
|
+
if create:
|
|
269
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
return ArtifactRecord(
|
|
271
|
+
category=f"generated/{'/'.join(parts)}" if parts else "generated",
|
|
272
|
+
topic=topic_slug,
|
|
273
|
+
directory=directory,
|
|
274
|
+
path=directory / f"{base_name}{suffix}",
|
|
275
|
+
metadata_path=directory / f"{base_name}.metadata.json",
|
|
276
|
+
raw_data_path=directory / f"{base_name}.raw_data.json",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def write_artifact_metadata(record: ArtifactRecord, metadata: Dict[str, Any]) -> Path:
|
|
281
|
+
payload = {
|
|
282
|
+
"artifact": {
|
|
283
|
+
"category": record.category,
|
|
284
|
+
"topic": record.topic,
|
|
285
|
+
"path": str(record.path),
|
|
286
|
+
"metadata_path": str(record.metadata_path),
|
|
287
|
+
"raw_data_path": str(record.raw_data_path),
|
|
288
|
+
},
|
|
289
|
+
**metadata,
|
|
290
|
+
}
|
|
291
|
+
record.metadata_path.write_text(
|
|
292
|
+
json.dumps(payload, ensure_ascii=False, indent=2, default=str),
|
|
293
|
+
encoding="utf-8",
|
|
294
|
+
)
|
|
295
|
+
return record.metadata_path
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def write_artifact_raw_data(record: ArtifactRecord, data: Any) -> Path:
|
|
299
|
+
record.raw_data_path.write_text(
|
|
300
|
+
json.dumps(data, ensure_ascii=False, indent=2, default=str),
|
|
301
|
+
encoding="utf-8",
|
|
302
|
+
)
|
|
303
|
+
return record.raw_data_path
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def recent_artifacts(limit: int = 20, root: Optional[Path] = None) -> list[Dict[str, Any]]:
|
|
307
|
+
"""Return recent artifact metadata records, newest first."""
|
|
308
|
+
base = root or artifact_root()
|
|
309
|
+
if not base.exists():
|
|
310
|
+
return []
|
|
311
|
+
items: list[Dict[str, Any]] = []
|
|
312
|
+
for path in base.rglob("*.metadata.json"):
|
|
313
|
+
entry = _artifact_entry_from_metadata(path)
|
|
314
|
+
if entry is None:
|
|
315
|
+
continue
|
|
316
|
+
items.append({
|
|
317
|
+
"kind": entry.kind,
|
|
318
|
+
"status": entry.status,
|
|
319
|
+
"topic": entry.topic,
|
|
320
|
+
"path": str(entry.path) if str(entry.path) else "",
|
|
321
|
+
"metadata_path": str(entry.metadata_path),
|
|
322
|
+
"raw_data_path": str(entry.raw_data_path),
|
|
323
|
+
"created_at": entry.created_at,
|
|
324
|
+
"mtime": entry.mtime,
|
|
325
|
+
"size_bytes": entry.size_bytes,
|
|
326
|
+
})
|
|
327
|
+
items.sort(key=lambda item: item.get("mtime") or 0, reverse=True)
|
|
328
|
+
return items[: max(0, int(limit))]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def recent_artifacts_all(limit: int = 20) -> list[Dict[str, Any]]:
|
|
332
|
+
"""Return recent artifacts across project and user-generated roots."""
|
|
333
|
+
items: list[Dict[str, Any]] = []
|
|
334
|
+
seen: set[str] = set()
|
|
335
|
+
for root in artifact_roots(include_user_generated=True):
|
|
336
|
+
for item in recent_artifacts(limit=max(limit, 20), root=root):
|
|
337
|
+
marker = str(item.get("metadata_path") or item.get("path") or "")
|
|
338
|
+
if marker and marker in seen:
|
|
339
|
+
continue
|
|
340
|
+
if marker:
|
|
341
|
+
seen.add(marker)
|
|
342
|
+
item = dict(item)
|
|
343
|
+
item["root"] = str(root)
|
|
344
|
+
items.append(item)
|
|
345
|
+
items.sort(key=lambda item: item.get("mtime") or 0, reverse=True)
|
|
346
|
+
return items[: max(0, int(limit))]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def artifact_summary(root: Optional[Path] = None) -> Dict[str, Any]:
|
|
350
|
+
"""Return a lightweight inventory of artifacts under root."""
|
|
351
|
+
base = root or artifact_root()
|
|
352
|
+
if not base.exists():
|
|
353
|
+
return {
|
|
354
|
+
"root": str(base),
|
|
355
|
+
"total": 0,
|
|
356
|
+
"total_size_bytes": 0,
|
|
357
|
+
"by_kind": {},
|
|
358
|
+
"newest_mtime": 0.0,
|
|
359
|
+
"oldest_mtime": 0.0,
|
|
360
|
+
}
|
|
361
|
+
entries: list[ArtifactEntry] = []
|
|
362
|
+
for path in base.rglob("*.metadata.json"):
|
|
363
|
+
entry = _artifact_entry_from_metadata(path)
|
|
364
|
+
if entry is not None:
|
|
365
|
+
entries.append(entry)
|
|
366
|
+
by_kind: Dict[str, int] = {}
|
|
367
|
+
newest = 0.0
|
|
368
|
+
oldest = 0.0
|
|
369
|
+
total_size = 0
|
|
370
|
+
for entry in entries:
|
|
371
|
+
by_kind[entry.kind] = by_kind.get(entry.kind, 0) + 1
|
|
372
|
+
total_size += entry.size_bytes
|
|
373
|
+
if entry.mtime:
|
|
374
|
+
newest = max(newest, entry.mtime)
|
|
375
|
+
oldest = entry.mtime if not oldest else min(oldest, entry.mtime)
|
|
376
|
+
return {
|
|
377
|
+
"root": str(base),
|
|
378
|
+
"total": len(entries),
|
|
379
|
+
"total_size_bytes": total_size,
|
|
380
|
+
"by_kind": dict(sorted(by_kind.items(), key=lambda item: (-item[1], item[0]))),
|
|
381
|
+
"newest_mtime": newest,
|
|
382
|
+
"oldest_mtime": oldest,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def artifact_summary_all() -> Dict[str, Any]:
|
|
387
|
+
"""Return artifact inventory across project and user-generated roots."""
|
|
388
|
+
roots = artifact_roots(include_user_generated=True)
|
|
389
|
+
summaries = [artifact_summary(root) for root in roots]
|
|
390
|
+
by_kind: Dict[str, int] = {}
|
|
391
|
+
total = 0
|
|
392
|
+
total_size = 0
|
|
393
|
+
newest = 0.0
|
|
394
|
+
oldest = 0.0
|
|
395
|
+
for summary in summaries:
|
|
396
|
+
total += int(summary.get("total") or 0)
|
|
397
|
+
total_size += int(summary.get("total_size_bytes") or 0)
|
|
398
|
+
newest = max(newest, float(summary.get("newest_mtime") or 0))
|
|
399
|
+
old = float(summary.get("oldest_mtime") or 0)
|
|
400
|
+
if old:
|
|
401
|
+
oldest = old if not oldest else min(oldest, old)
|
|
402
|
+
for kind, count in (summary.get("by_kind") or {}).items():
|
|
403
|
+
by_kind[str(kind)] = by_kind.get(str(kind), 0) + int(count)
|
|
404
|
+
return {
|
|
405
|
+
"roots": [str(root) for root in roots],
|
|
406
|
+
"total": total,
|
|
407
|
+
"total_size_bytes": total_size,
|
|
408
|
+
"by_kind": dict(sorted(by_kind.items(), key=lambda item: (-item[1], item[0]))),
|
|
409
|
+
"newest_mtime": newest,
|
|
410
|
+
"oldest_mtime": oldest,
|
|
411
|
+
"summaries": summaries,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def prune_artifacts(keep: int = 20, root: Optional[Path] = None, dry_run: bool = False) -> Dict[str, Any]:
|
|
416
|
+
"""Delete artifact bundles older than the newest `keep` entries."""
|
|
417
|
+
base = root or artifact_root()
|
|
418
|
+
keep = max(0, int(keep))
|
|
419
|
+
if not base.exists():
|
|
420
|
+
return {
|
|
421
|
+
"root": str(base),
|
|
422
|
+
"keep": keep,
|
|
423
|
+
"scanned": 0,
|
|
424
|
+
"removed": 0,
|
|
425
|
+
"dry_run": dry_run,
|
|
426
|
+
"deleted": [],
|
|
427
|
+
}
|
|
428
|
+
entries: list[ArtifactEntry] = []
|
|
429
|
+
for path in base.rglob("*.metadata.json"):
|
|
430
|
+
entry = _artifact_entry_from_metadata(path)
|
|
431
|
+
if entry is not None:
|
|
432
|
+
entries.append(entry)
|
|
433
|
+
entries.sort(key=lambda entry: entry.mtime, reverse=True)
|
|
434
|
+
to_remove = entries[keep:]
|
|
435
|
+
deleted: list[Dict[str, Any]] = []
|
|
436
|
+
for entry in to_remove:
|
|
437
|
+
targets = [entry.path, entry.metadata_path, entry.raw_data_path]
|
|
438
|
+
removed_files: list[str] = []
|
|
439
|
+
if not dry_run:
|
|
440
|
+
for target in targets:
|
|
441
|
+
try:
|
|
442
|
+
if target.exists():
|
|
443
|
+
target.unlink()
|
|
444
|
+
removed_files.append(str(target))
|
|
445
|
+
except Exception:
|
|
446
|
+
continue
|
|
447
|
+
for target in targets:
|
|
448
|
+
if target.exists():
|
|
449
|
+
continue
|
|
450
|
+
_cleanup_empty_dirs(target.parent, base)
|
|
451
|
+
else:
|
|
452
|
+
removed_files = [str(target) for target in targets if target.exists()]
|
|
453
|
+
deleted.append(
|
|
454
|
+
{
|
|
455
|
+
"kind": entry.kind,
|
|
456
|
+
"status": entry.status,
|
|
457
|
+
"topic": entry.topic,
|
|
458
|
+
"metadata_path": str(entry.metadata_path),
|
|
459
|
+
"path": str(entry.path) if str(entry.path) else "",
|
|
460
|
+
"removed_files": removed_files,
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
return {
|
|
464
|
+
"root": str(base),
|
|
465
|
+
"keep": keep,
|
|
466
|
+
"scanned": len(entries),
|
|
467
|
+
"removed": len(deleted),
|
|
468
|
+
"dry_run": dry_run,
|
|
469
|
+
"deleted": deleted,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def prune_artifacts_all(keep: int = 20, dry_run: bool = False) -> Dict[str, Any]:
|
|
474
|
+
"""Prune artifact bundles in every visible artifact root."""
|
|
475
|
+
results = [prune_artifacts(keep=keep, root=root, dry_run=dry_run) for root in artifact_roots(include_user_generated=True)]
|
|
476
|
+
deleted: list[Dict[str, Any]] = []
|
|
477
|
+
for result in results:
|
|
478
|
+
root = result.get("root") or ""
|
|
479
|
+
for item in result.get("deleted") or []:
|
|
480
|
+
item = dict(item)
|
|
481
|
+
item["root"] = root
|
|
482
|
+
deleted.append(item)
|
|
483
|
+
return {
|
|
484
|
+
"roots": [result.get("root") for result in results],
|
|
485
|
+
"keep": max(0, int(keep)),
|
|
486
|
+
"scanned": sum(int(result.get("scanned") or 0) for result in results),
|
|
487
|
+
"removed": sum(int(result.get("removed") or 0) for result in results),
|
|
488
|
+
"dry_run": dry_run,
|
|
489
|
+
"deleted": deleted,
|
|
490
|
+
"results": results,
|
|
491
|
+
}
|