corio 2.3.4__tar.gz → 2.3.6__tar.gz
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.
- {corio-2.3.4 → corio-2.3.6}/PKG-INFO +13 -1
- corio-2.3.6/corio/api/__init__.py +1 -0
- {corio-2.3.4/corio → corio-2.3.6/corio/api}/api.py +118 -5
- corio-2.3.6/corio/api/mcp.py +1 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/releaser.py +3 -7
- {corio-2.3.4 → corio-2.3.6}/corio/pyproject.package.toml +18 -6
- corio-2.3.6/corio/sandbox/__init__.py +1 -0
- corio-2.3.6/corio/sandbox/api.py +64 -0
- corio-2.3.6/corio/shell/__init__.py +3 -0
- corio-2.3.6/corio/shell/dsl.py +230 -0
- corio-2.3.6/corio/tests/test_api.py +96 -0
- corio-2.3.6/corio/tests/test_shell.py +277 -0
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/PKG-INFO +13 -1
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/SOURCES.txt +9 -1
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/requires.txt +13 -0
- {corio-2.3.4 → corio-2.3.6}/pyproject.toml +18 -6
- {corio-2.3.4 → corio-2.3.6}/LICENSE +0 -0
- {corio-2.3.4 → corio-2.3.6}/README.md +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ai/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ai/agentic.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ai/infer.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/aio.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/augmentation.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/av.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/caching.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/constants.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/context.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dataclass.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/datatype.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/db/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/db/document.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/debug.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dm.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dns/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dns/client.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dns/dm.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dns/proxy.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dns/server.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/docker/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/dt.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/encrypt.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/entrypoint.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/env.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/function.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/google_api.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ha/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ha/constants.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ha/core.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ha/supervisor.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/ha/utils.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/hash.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/hfh.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/hook.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/https.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/api.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/incrementor_pyproject.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/project.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/repository.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/infra/stack.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/inherit.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/inspection.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/interface/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/interface/context.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/interface/controls.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/interface/interface.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/iterator.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/jsn.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/json_fix.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/logs.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/markup.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/merging.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/metric.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/mqtt.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/name.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/net.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/netrc.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/openai.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/parallel.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/path/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/path/app.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/path/path.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/path/type.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/paths.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/patterns.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/pdf.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/plat.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/process.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/profiling.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/rand.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/sec.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/semantic.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/sets.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/setup/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/spaces.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/strings.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tabular.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/conftest.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/helpers.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_caching.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_datatype.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_dns.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_dt.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_encrypt.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_env.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_hash.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_hook.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_infra.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_iterator.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_jsn.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_name.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_path.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_patterns.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_rand.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_strings.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_toml.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_tools.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tests/test_yml.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tokenization.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/toml.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/tools.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/unicode.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/version/__init__.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/version/version.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/webhook.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/yml.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio/youtube.py +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/dependency_links.txt +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/entry_points.txt +0 -0
- {corio-2.3.4 → corio-2.3.6}/corio.egg-info/top_level.txt +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/add-service +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/add-user-path +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/add-ve-shell +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/apt-essentials +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/apt-headless +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/auth-token +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/compose-update +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/compress-dir +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/cru +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-build-bases +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-build-bases-dev +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-install-deps +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-install-prod +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-prune +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-sandbox +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docker-sandbox-init +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/docs-deploy +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/download +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/encrypt-secrets +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/fmtr-test-script +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/git-clone +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/ha-addon-launch +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/infra +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/install-browser +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/install-docker +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/install-ts +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/install-ys +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/mirror-dir +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/opt-dev-init +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/parse-args +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/pypi-check +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/pypi-reserve +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/run-script +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/run-tests +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/set-password +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/set-secure-path +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/set-user-sudo +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/snips-install +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/ssh-auth +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/ssh-serve +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/tasmota-config +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/tasmota-flash +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/tasmota-terminal +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/vlc-tn +0 -0
- {corio-2.3.4 → corio-2.3.6}/scripts/vm-launch +0 -0
- {corio-2.3.4 → corio-2.3.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: corio
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.6
|
|
4
4
|
Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
|
|
5
5
|
Author-email: Frontmatter AI <innovative.fowler@mask.pro.fmtr.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -90,6 +90,17 @@ Requires-Dist: pydantic; extra == "api"
|
|
|
90
90
|
Requires-Dist: pydantic-extra-types; extra == "api"
|
|
91
91
|
Requires-Dist: pycountry; extra == "api"
|
|
92
92
|
Requires-Dist: logfire[fastapi]; extra == "api"
|
|
93
|
+
Requires-Dist: httpx; extra == "api"
|
|
94
|
+
Provides-Extra: api-mcp
|
|
95
|
+
Requires-Dist: fastapi; extra == "api-mcp"
|
|
96
|
+
Requires-Dist: uvicorn[standard]; extra == "api-mcp"
|
|
97
|
+
Requires-Dist: logfire; extra == "api-mcp"
|
|
98
|
+
Requires-Dist: pydantic; extra == "api-mcp"
|
|
99
|
+
Requires-Dist: pydantic-extra-types; extra == "api-mcp"
|
|
100
|
+
Requires-Dist: pycountry; extra == "api-mcp"
|
|
101
|
+
Requires-Dist: logfire[fastapi]; extra == "api-mcp"
|
|
102
|
+
Requires-Dist: httpx; extra == "api-mcp"
|
|
103
|
+
Requires-Dist: fastmcp; extra == "api-mcp"
|
|
93
104
|
Provides-Extra: dm
|
|
94
105
|
Requires-Dist: pydantic; extra == "dm"
|
|
95
106
|
Requires-Dist: pydantic-extra-types; extra == "dm"
|
|
@@ -342,6 +353,7 @@ Requires-Dist: contexttimer; extra == "all"
|
|
|
342
353
|
Requires-Dist: Unidecode; extra == "all"
|
|
343
354
|
Requires-Dist: tinynetrc; extra == "all"
|
|
344
355
|
Requires-Dist: huggingface_hub; extra == "all"
|
|
356
|
+
Requires-Dist: fastmcp; extra == "all"
|
|
345
357
|
Requires-Dist: peft; extra == "all"
|
|
346
358
|
Requires-Dist: transformers[sentencepiece]; extra == "all"
|
|
347
359
|
Requires-Dist: torchvision; extra == "all"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from corio.api.api import *
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
4
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
2
5
|
from dataclasses import dataclass
|
|
3
6
|
from functools import cached_property
|
|
4
|
-
from typing import Callable, List, Optional, Union, Self
|
|
7
|
+
from typing import Callable, List, Optional, Union, Self, TYPE_CHECKING
|
|
5
8
|
|
|
9
|
+
import httpx
|
|
6
10
|
import uvicorn
|
|
7
11
|
from fastapi import FastAPI, Request
|
|
8
12
|
|
|
9
|
-
from corio import env
|
|
13
|
+
from corio import env, strings
|
|
10
14
|
from corio.iterator import enlist
|
|
11
15
|
from corio.logs import logger
|
|
12
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from corio.api.mcp import FastMCP
|
|
19
|
+
|
|
13
20
|
for name in ["uvicorn.access", "uvicorn.error", "uvicorn"]:
|
|
14
21
|
logger_uvicorn = logging.getLogger(name)
|
|
15
22
|
logger_uvicorn.handlers.clear()
|
|
@@ -44,6 +51,9 @@ class Base:
|
|
|
44
51
|
URL = None
|
|
45
52
|
URL_DOCS = '/'
|
|
46
53
|
URL_PREFIX = None
|
|
54
|
+
IS_MCP = False
|
|
55
|
+
MCP_PATH = '/mcp'
|
|
56
|
+
|
|
47
57
|
|
|
48
58
|
def add_endpoint(self, endpoint: Endpoint):
|
|
49
59
|
"""
|
|
@@ -63,7 +73,13 @@ class Base:
|
|
|
63
73
|
)(endpoint.method)
|
|
64
74
|
|
|
65
75
|
def __init__(self):
|
|
66
|
-
self.app = FastAPI(
|
|
76
|
+
self.app = FastAPI(
|
|
77
|
+
title=self.TITLE,
|
|
78
|
+
swagger_ui_parameters=self.SWAGGER_PARAMS,
|
|
79
|
+
docs_url=self.URL_DOCS,
|
|
80
|
+
lifespan=self.lifespan,
|
|
81
|
+
description=self.description,
|
|
82
|
+
)
|
|
67
83
|
logger.instrument_fastapi(self.app)
|
|
68
84
|
|
|
69
85
|
for endpoint in self.get_endpoints():
|
|
@@ -75,6 +91,10 @@ class Base:
|
|
|
75
91
|
for child in self.children:
|
|
76
92
|
self.app.mount(child.url_prefix, child.app)
|
|
77
93
|
|
|
94
|
+
if self.IS_MCP:
|
|
95
|
+
self.app.mount(self.MCP_PATH, self.mcp_app)
|
|
96
|
+
self.app.router.lifespan_context = self.lifespan_mcp
|
|
97
|
+
|
|
78
98
|
@cached_property
|
|
79
99
|
def children(self) -> List[Self]:
|
|
80
100
|
"""
|
|
@@ -92,7 +112,7 @@ class Base:
|
|
|
92
112
|
|
|
93
113
|
"""
|
|
94
114
|
if self.URL_PREFIX:
|
|
95
|
-
self.URL_PREFIX
|
|
115
|
+
return self.URL_PREFIX
|
|
96
116
|
|
|
97
117
|
return f'/{self.__class__.__name__.lower()}'
|
|
98
118
|
|
|
@@ -137,7 +157,29 @@ class Base:
|
|
|
137
157
|
Launch message.
|
|
138
158
|
|
|
139
159
|
"""
|
|
140
|
-
|
|
160
|
+
message = f"Launching {self.TITLE} at {self.url}"
|
|
161
|
+
if self.IS_MCP:
|
|
162
|
+
message = f"{message} (with MCP at {self.MCP_PATH})"
|
|
163
|
+
return message
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def description(self) -> str|None:
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
Optional OpenAPI description with immediate child API links.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
if not self.children:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
lines = [f"- [{child.TITLE}]({child.url_prefix})" for child in self.children]
|
|
176
|
+
lines= "\n".join(lines)
|
|
177
|
+
description=strings.trim(f"""
|
|
178
|
+
### APIs
|
|
179
|
+
|
|
180
|
+
{lines}
|
|
181
|
+
""")
|
|
182
|
+
return description
|
|
141
183
|
|
|
142
184
|
@property
|
|
143
185
|
def config(self) -> uvicorn.Config:
|
|
@@ -178,3 +220,74 @@ class Base:
|
|
|
178
220
|
"""
|
|
179
221
|
import asyncio
|
|
180
222
|
return asyncio.run(cls.launch_async(*args, **kwargs))
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
@asynccontextmanager
|
|
226
|
+
async def lifespan(app: FastAPI):
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
Default app lifespan.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
yield
|
|
233
|
+
logger.info(f"Closing {app.title}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# MCP integration methods
|
|
237
|
+
|
|
238
|
+
@cached_property
|
|
239
|
+
def mcp(self) -> FastMCP:
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
Build an MCP server from this API's OpenAPI schema.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
from corio.api.mcp import FastMCP
|
|
246
|
+
client = httpx.AsyncClient(transport=httpx.ASGITransport(app=self.app), base_url=self.url)
|
|
247
|
+
return FastMCP.from_openapi(openapi_spec=self.app.openapi(), client=client, name=self.TITLE)
|
|
248
|
+
|
|
249
|
+
@cached_property
|
|
250
|
+
def mcp_app(self):
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
HTTP ASGI app for this API's MCP server.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
return self.mcp.http_app(path='/')
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@cached_property
|
|
260
|
+
def mcp_lifespans_flat(self) -> list[tuple[Callable, FastAPI]]:
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
Flattened MCP lifespans for this API and immediate descendants.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if self.IS_MCP:
|
|
268
|
+
lifespans = [(self.mcp_app.lifespan, self.mcp_app)]
|
|
269
|
+
else:
|
|
270
|
+
lifespans = []
|
|
271
|
+
|
|
272
|
+
for child in self.children:
|
|
273
|
+
lifespans.extend(child.mcp_lifespans_flat)
|
|
274
|
+
return lifespans
|
|
275
|
+
|
|
276
|
+
@cached_property
|
|
277
|
+
def lifespan_mcp(self):
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
Combined lifespan wrapper for FastAPI and mounted MCP apps.
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
original_lifespan = self.app.router.lifespan_context
|
|
284
|
+
|
|
285
|
+
@asynccontextmanager
|
|
286
|
+
async def lifespan(app):
|
|
287
|
+
async with AsyncExitStack() as stack:
|
|
288
|
+
await stack.enter_async_context(original_lifespan(app))
|
|
289
|
+
for mcp_lifespan, mcp_owner_app in self.mcp_lifespans_flat:
|
|
290
|
+
await stack.enter_async_context(mcp_lifespan(mcp_owner_app))
|
|
291
|
+
yield
|
|
292
|
+
|
|
293
|
+
return lifespan
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
@@ -228,13 +228,9 @@ class IncrementorVersion(Incrementor):
|
|
|
228
228
|
def old(self):
|
|
229
229
|
return self.paths.metadata.version_obj
|
|
230
230
|
|
|
231
|
-
@cached_property
|
|
232
|
-
def old_tag(self):
|
|
233
|
-
return self.old.tag
|
|
234
|
-
|
|
235
231
|
@cached_property
|
|
236
232
|
def has_old_tag(self):
|
|
237
|
-
return self.
|
|
233
|
+
return self.old.tag in self.repo.tags.all
|
|
238
234
|
|
|
239
235
|
@cached_property
|
|
240
236
|
def pinned_tag(self):
|
|
@@ -250,7 +246,7 @@ class IncrementorVersion(Incrementor):
|
|
|
250
246
|
|
|
251
247
|
if not self.has_old_tag:
|
|
252
248
|
raise RuntimeError(
|
|
253
|
-
f'Current version tag "{self.
|
|
249
|
+
f'Current version tag "{self.old.tag}" was not found. '
|
|
254
250
|
f'Refusing pinned release "{new}" until the previous release state is resolved.'
|
|
255
251
|
)
|
|
256
252
|
|
|
@@ -267,7 +263,7 @@ class IncrementorVersion(Incrementor):
|
|
|
267
263
|
|
|
268
264
|
if not self.has_old_tag:
|
|
269
265
|
logger.warning(
|
|
270
|
-
f'Current version tag "{self.
|
|
266
|
+
f'Current version tag "{self.old.tag}" was not found. '
|
|
271
267
|
f'Assuming previous release failed and reusing version "{self.old}".'
|
|
272
268
|
)
|
|
273
269
|
return self.old
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "corio"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.6"
|
|
4
4
|
description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
dependencies = []
|
|
@@ -27,7 +27,8 @@ spaces = ["tinynetrc"]
|
|
|
27
27
|
netrc = ["tinynetrc"]
|
|
28
28
|
hfh = ["huggingface_hub"]
|
|
29
29
|
merging = ["deepmerge"]
|
|
30
|
-
api = ["fastapi", "uvicorn[standard]", "logfire", "pydantic", "pydantic-extra-types", "pycountry", "logfire[fastapi]"]
|
|
30
|
+
api = ["fastapi", "uvicorn[standard]", "logfire", "pydantic", "pydantic-extra-types", "pycountry", "logfire[fastapi]", "httpx"]
|
|
31
|
+
"api.mcp" = ["fastapi", "uvicorn[standard]", "logfire", "pydantic", "pydantic-extra-types", "pycountry", "logfire[fastapi]", "httpx", "fastmcp"]
|
|
31
32
|
dm = ["pydantic", "pydantic-extra-types", "pycountry"]
|
|
32
33
|
ai = ["peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "pydantic", "pydantic-extra-types", "pycountry"]
|
|
33
34
|
"ai.compute" = ["cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0"]
|
|
@@ -70,7 +71,7 @@ tasmota = ["decode-config", "esptool"]
|
|
|
70
71
|
encrypt = ["pyrage"]
|
|
71
72
|
secrets = ["pyrage", "dotenv", "yamlscript", "pyyaml", "logfire", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "pygit2"]
|
|
72
73
|
cli = ["pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "logfire"]
|
|
73
|
-
all = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov", "pytest-asyncio", "dask[bag]", "distributed", "bokeh", "tokenizers", "faker", "sre_yield", "contexttimer", "Unidecode", "tinynetrc", "huggingface_hub", "peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0", "openai", "pydantic-ai-slim[logfire,openai]", "ollama", "json_repair", "sentence_transformers", "pandas", "tabulate", "openpyxl", "odfpy", "deepdiff", "html2text", "flet[all] <0.80.0", "flet-video", "flet-webview", "google-auth", "google-auth-oauthlib", "google-auth-httplib2", "google-api-python-client", "diskcache", "cachetools", "pymupdf", "pymupdf4llm", "appdirs", "filetype", "dnspython[doh]", "regex", "playwright", "aiomqtt", "av", "homeassistant_api", "aiohasupervisor", "pytubefix", "decode-config", "esptool"]
|
|
74
|
+
all = ["logfire", "semver", "pydevd-pycharm~=251.25410.159", "pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml", "beanie", "setuptools", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav", "build", "twine", "packaging", "pygit2", "python-on-whales", "deepmerge", "httpx", "httpx_retries", "logfire[httpx]", "fastapi", "uvicorn[standard]", "logfire[fastapi]", "tomlkit", "pyrage", "dotenv", "pytest", "pytest-cov", "pytest-asyncio", "dask[bag]", "distributed", "bokeh", "tokenizers", "faker", "sre_yield", "contexttimer", "Unidecode", "tinynetrc", "huggingface_hub", "fastmcp", "peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0", "openai", "pydantic-ai-slim[logfire,openai]", "ollama", "json_repair", "sentence_transformers", "pandas", "tabulate", "openpyxl", "odfpy", "deepdiff", "html2text", "flet[all] <0.80.0", "flet-video", "flet-webview", "google-auth", "google-auth-oauthlib", "google-auth-httplib2", "google-api-python-client", "diskcache", "cachetools", "pymupdf", "pymupdf4llm", "appdirs", "filetype", "dnspython[doh]", "regex", "playwright", "aiomqtt", "av", "homeassistant_api", "aiohasupervisor", "pytubefix", "decode-config", "esptool"]
|
|
74
75
|
|
|
75
76
|
[[project.authors]]
|
|
76
77
|
name = "Frontmatter AI"
|
|
@@ -83,7 +84,7 @@ Homepage = "https://github.com/fmtr/corio"
|
|
|
83
84
|
corio = "corio.entrypoint:main"
|
|
84
85
|
|
|
85
86
|
[tool.corio.metadata]
|
|
86
|
-
version = "2.3.
|
|
87
|
+
version = "2.3.6"
|
|
87
88
|
port = 0
|
|
88
89
|
base = "python"
|
|
89
90
|
description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
|
|
@@ -120,7 +121,8 @@ spaces = ["netrc"]
|
|
|
120
121
|
netrc = ["tinynetrc"]
|
|
121
122
|
hfh = ["huggingface_hub"]
|
|
122
123
|
merging = ["deepmerge"]
|
|
123
|
-
api = ["fastapi", "uvicorn[standard]", "logging", "dm", "logfire[fastapi]"]
|
|
124
|
+
api = ["fastapi", "uvicorn[standard]", "logging", "dm", "logfire[fastapi]", "httpx"]
|
|
125
|
+
"api.mcp" = ["api", "fastmcp"]
|
|
124
126
|
dm = ["pydantic", "pydantic-extra-types", "pycountry"]
|
|
125
127
|
ai = ["peft", "transformers[sentencepiece]", "torchvision", "torchaudio", "dm"]
|
|
126
128
|
"ai.compute" = ["cuda-bindings==13.2.0", "cuda-pathfinder==1.5.4", "cuda-toolkit==13.0.2", "nvidia-cublas==13.1.0.3", "nvidia-cuda-cupti==13.0.85", "nvidia-cuda-nvrtc==13.0.88", "nvidia-cuda-runtime==13.0.96", "nvidia-cudnn-cu13==9.19.0.56", "nvidia-cufft==12.0.0.61", "nvidia-cufile==1.15.1.6", "nvidia-curand==10.4.0.35", "nvidia-cusolver==12.0.4.66", "nvidia-cusparse==12.6.3.3", "nvidia-cusparselt-cu13==0.8.0", "nvidia-nccl-cu13==2.28.9", "nvidia-nvjitlink==13.0.88", "nvidia-nvshmem-cu13==3.4.5", "nvidia-nvtx==13.0.85", "torch==2.11.0", "triton==3.6.0"]
|
|
@@ -177,7 +179,12 @@ corio = ["pyproject.package.toml"]
|
|
|
177
179
|
|
|
178
180
|
[tool.tox]
|
|
179
181
|
requires = ["tox>=4.22", "tox-uv>=1"]
|
|
180
|
-
env_list = ["caching", "datatype", "dns", "dt", "encrypt", "env", "hash", "hook", "infra", "iterator", "jsn", "name", "path", "patterns", "rand", "strings", "toml", "tools", "yml"]
|
|
182
|
+
env_list = ["api", "caching", "datatype", "dns", "dt", "encrypt", "env", "hash", "hook", "infra", "iterator", "jsn", "name", "path", "patterns", "rand", "shell", "strings", "toml", "tools", "yml"]
|
|
183
|
+
|
|
184
|
+
[tool.tox.env.api]
|
|
185
|
+
description = "Run api tests."
|
|
186
|
+
deps = ["fastapi", "fastmcp", "httpx", "logfire", "logfire[fastapi]", "pycountry", "pydantic", "pydantic-extra-types", "pytest", "pytest-asyncio", "pytest-cov", "uvicorn[standard]"]
|
|
187
|
+
commands = [["python", "-m", "pytest", "-q", "corio/tests/test_api.py"]]
|
|
181
188
|
|
|
182
189
|
[tool.tox.env.caching]
|
|
183
190
|
description = "Run caching tests."
|
|
@@ -254,6 +261,11 @@ description = "Run rand tests."
|
|
|
254
261
|
deps = ["pytest", "pytest-asyncio", "pytest-cov"]
|
|
255
262
|
commands = [["python", "-m", "pytest", "-q", "corio/tests/test_rand.py"]]
|
|
256
263
|
|
|
264
|
+
[tool.tox.env.shell]
|
|
265
|
+
description = "Run shell tests."
|
|
266
|
+
deps = ["pytest", "pytest-asyncio", "pytest-cov"]
|
|
267
|
+
commands = [["python", "-m", "pytest", "-q", "corio/tests/test_shell.py"]]
|
|
268
|
+
|
|
257
269
|
[tool.tox.env.strings]
|
|
258
270
|
description = "Run strings tests."
|
|
259
271
|
deps = ["pytest", "pytest-asyncio", "pytest-cov"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from corio.sandbox.api import SandboxApi
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from corio import api
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Child(api.Base):
|
|
6
|
+
TITLE = 'Sandbox Child API'
|
|
7
|
+
IS_MCP = True
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_endpoints(self):
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
Sandbox child endpoints.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
endpoints = [
|
|
17
|
+
api.Endpoint(method_http=self.app.get, path='/pong', method=self.pong, tags='sandbox'),
|
|
18
|
+
]
|
|
19
|
+
return endpoints
|
|
20
|
+
|
|
21
|
+
async def pong(self, value: str = 'ok'):
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Child ping variant.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
return dict(child=value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SandboxApi(api.Base):
|
|
31
|
+
TITLE = 'Sandbox API'
|
|
32
|
+
PORT = 8001
|
|
33
|
+
IS_MCP = True
|
|
34
|
+
|
|
35
|
+
def get_endpoints(self):
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
Sandbox parent endpoints.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
endpoints = [
|
|
42
|
+
api.Endpoint(method_http=self.app.get, path='/ping', method=self.ping, tags='sandbox'),
|
|
43
|
+
]
|
|
44
|
+
return endpoints
|
|
45
|
+
|
|
46
|
+
async def ping(self, name: str = 'world'):
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
Parent ping endpoint.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
return dict(message=f'hello {name}')
|
|
53
|
+
|
|
54
|
+
@cached_property
|
|
55
|
+
def children(self):
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
Immediate sandbox child APIs.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
return [Child()]
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
SandboxApi.launch()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _normalize(name: str) -> str:
|
|
9
|
+
return name.strip("_").replace("_", "-")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _to_value(item: object) -> Value:
|
|
13
|
+
if isinstance(item, Value):
|
|
14
|
+
return item
|
|
15
|
+
if isinstance(item, Argument):
|
|
16
|
+
return Value(name=item.name)
|
|
17
|
+
return Value(name=str(item))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _to_subcommand_part(item: object) -> Argument:
|
|
21
|
+
if isinstance(item, Argument):
|
|
22
|
+
return item
|
|
23
|
+
if isinstance(item, Value):
|
|
24
|
+
return Argument(name=item.name)
|
|
25
|
+
return Argument(name=str(item))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_expression_part(item: object) -> Subcommand | Argument:
|
|
29
|
+
if isinstance(item, Subcommand):
|
|
30
|
+
return item
|
|
31
|
+
if isinstance(item, Argument):
|
|
32
|
+
return item
|
|
33
|
+
if isinstance(item, Value):
|
|
34
|
+
return Argument(name=item.name)
|
|
35
|
+
return Argument(name=str(item))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _to_argument_child(item: object) -> Argument | Value:
|
|
39
|
+
if isinstance(item, Argument):
|
|
40
|
+
return item
|
|
41
|
+
if isinstance(item, Value):
|
|
42
|
+
return item
|
|
43
|
+
return Argument(name=str(item))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _flatten_parts(parts: list[object]) -> list[str]:
|
|
47
|
+
return [token for part in parts for token in part.tokens()]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Value:
|
|
52
|
+
name: str = ""
|
|
53
|
+
children: list[object] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
def __getattr__(self, name: str) -> Value:
|
|
56
|
+
return Value(name=_normalize(name))
|
|
57
|
+
|
|
58
|
+
def tokens(self) -> list[str]:
|
|
59
|
+
return [shlex.quote(self.name)]
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return " ".join(self.tokens())
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return f"{self.__class__.__name__}('{self}')"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Argument:
|
|
70
|
+
name: str = ""
|
|
71
|
+
prefixes: str = ""
|
|
72
|
+
children: list[Argument | Value] = field(default_factory=list)
|
|
73
|
+
sep: str = " "
|
|
74
|
+
|
|
75
|
+
def __getattr__(self, name: str) -> Argument:
|
|
76
|
+
return Argument(name=_normalize(name), prefixes=self.prefixes)
|
|
77
|
+
|
|
78
|
+
def __neg__(self) -> Argument:
|
|
79
|
+
return Argument(name=self.name, prefixes=f"-{self.prefixes}", children=list(self.children), sep=self.sep)
|
|
80
|
+
|
|
81
|
+
def __pos__(self) -> Argument:
|
|
82
|
+
return Argument(name=self.name, prefixes=f"+{self.prefixes}", children=list(self.children), sep=self.sep)
|
|
83
|
+
|
|
84
|
+
def __add__(self, other: object) -> Argument:
|
|
85
|
+
child = _to_value(other)
|
|
86
|
+
return Argument(
|
|
87
|
+
name=self.name,
|
|
88
|
+
prefixes=self.prefixes,
|
|
89
|
+
children=[*self.children, child],
|
|
90
|
+
sep=self.sep,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __call__(self, *parts: object) -> Argument:
|
|
94
|
+
return Argument(
|
|
95
|
+
name=self.name,
|
|
96
|
+
prefixes=self.prefixes,
|
|
97
|
+
children=[*self.children, *[_to_argument_child(part) for part in parts]],
|
|
98
|
+
sep=self.sep,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def __eq__(self, other: object) -> Argument: # type: ignore[override]
|
|
102
|
+
return Argument(
|
|
103
|
+
name=self.name,
|
|
104
|
+
prefixes=self.prefixes,
|
|
105
|
+
children=[_to_value(other)],
|
|
106
|
+
sep="=",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def token(self) -> str:
|
|
111
|
+
return f"{self.prefixes}{shlex.quote(self.name)}"
|
|
112
|
+
|
|
113
|
+
def tokens(self) -> list[str]:
|
|
114
|
+
if not self.children:
|
|
115
|
+
return [self.token]
|
|
116
|
+
if self.sep != " ":
|
|
117
|
+
first, rest = self.children[0], self.children[1:]
|
|
118
|
+
return [f"{self.token}{self.sep}{first.tokens()[0]}", *_flatten_parts(rest)]
|
|
119
|
+
return [self.token, *_flatten_parts(self.children)]
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
return " ".join(self.tokens())
|
|
123
|
+
|
|
124
|
+
def __repr__(self) -> str:
|
|
125
|
+
return f"{self.__class__.__name__}('{self}')"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class Subcommand:
|
|
130
|
+
name: str
|
|
131
|
+
children: list[Subcommand | Argument] = field(default_factory=list)
|
|
132
|
+
active: Subcommand = field(init=False)
|
|
133
|
+
|
|
134
|
+
def __post_init__(self) -> None:
|
|
135
|
+
self.active = self
|
|
136
|
+
|
|
137
|
+
def add_subcommand(self, name: str) -> Subcommand:
|
|
138
|
+
child = Subcommand(name=name)
|
|
139
|
+
self.active.children.append(child)
|
|
140
|
+
self.active = child
|
|
141
|
+
return child
|
|
142
|
+
|
|
143
|
+
def __call__(self, *parts: object) -> Subcommand:
|
|
144
|
+
self.active.children.extend(_to_subcommand_part(part) for part in parts)
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def tokens(self) -> list[str]:
|
|
148
|
+
return [self.name, *_flatten_parts(self.children)]
|
|
149
|
+
|
|
150
|
+
def __str__(self) -> str:
|
|
151
|
+
return " ".join(self.tokens())
|
|
152
|
+
|
|
153
|
+
def __repr__(self) -> str:
|
|
154
|
+
return f"{self.__class__.__name__}('{self}')"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class Expression:
|
|
159
|
+
name: str
|
|
160
|
+
shell: Shell
|
|
161
|
+
children: list[Subcommand | Argument] = field(default_factory=list)
|
|
162
|
+
active: Expression | Subcommand = field(init=False)
|
|
163
|
+
|
|
164
|
+
def __post_init__(self) -> None:
|
|
165
|
+
self.active = self
|
|
166
|
+
|
|
167
|
+
def __getattr__(self, name: str) -> Expression:
|
|
168
|
+
command = _normalize(name)
|
|
169
|
+
if self.active is self:
|
|
170
|
+
child = Subcommand(name=command)
|
|
171
|
+
self.children.append(child)
|
|
172
|
+
self.active = child
|
|
173
|
+
return self
|
|
174
|
+
self.active = self.active.add_subcommand(command)
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def __call__(self, *parts: object) -> Expression:
|
|
178
|
+
if self.active is self:
|
|
179
|
+
self.children.extend(_to_expression_part(part) for part in parts)
|
|
180
|
+
return self
|
|
181
|
+
self.active(*parts)
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def tokens(self) -> list[str]:
|
|
185
|
+
return [self.name, *_flatten_parts(self.children)]
|
|
186
|
+
|
|
187
|
+
def popen(self) -> subprocess.Popen[str]:
|
|
188
|
+
return self.shell.popen(self)
|
|
189
|
+
|
|
190
|
+
def iterate_lines(self):
|
|
191
|
+
return self.shell.iterate_lines(self)
|
|
192
|
+
|
|
193
|
+
def __iter__(self):
|
|
194
|
+
return self.iterate_lines()
|
|
195
|
+
|
|
196
|
+
def __pos__(self):
|
|
197
|
+
return self.iterate_lines()
|
|
198
|
+
|
|
199
|
+
def __str__(self) -> str:
|
|
200
|
+
return " ".join(self.tokens())
|
|
201
|
+
|
|
202
|
+
def __repr__(self) -> str:
|
|
203
|
+
return f"{self.__class__.__name__}('{self}')"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class Shell:
|
|
208
|
+
context: str = ""
|
|
209
|
+
|
|
210
|
+
def __getattr__(self, name: str) -> Expression:
|
|
211
|
+
return Expression(name=_normalize(name), shell=self)
|
|
212
|
+
|
|
213
|
+
def popen(self, expression: Expression) -> subprocess.Popen[str]:
|
|
214
|
+
return subprocess.Popen(
|
|
215
|
+
expression.tokens(),
|
|
216
|
+
stdout=subprocess.PIPE,
|
|
217
|
+
stderr=subprocess.PIPE,
|
|
218
|
+
text=True,
|
|
219
|
+
bufsize=1,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def iterate_lines(self, expression: Expression):
|
|
223
|
+
process = self.popen(expression)
|
|
224
|
+
if process.stdout is not None:
|
|
225
|
+
for line in process.stdout:
|
|
226
|
+
yield line.rstrip("\n")
|
|
227
|
+
process.wait()
|
|
228
|
+
|
|
229
|
+
def __repr__(self) -> str:
|
|
230
|
+
return f"{self.__class__.__name__}('{self.context}')"
|