llm-api-scope 0.5.1__tar.gz → 0.6.0__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.
- {llm_api_scope-0.5.1/llm_api_scope.egg-info → llm_api_scope-0.6.0}/PKG-INFO +7 -1
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/README.md +6 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/config.py +19 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/main.py +3 -1
- llm_api_scope-0.6.0/apiscope/repo/__init__.py +6 -0
- llm_api_scope-0.6.0/apiscope/repo/app.py +152 -0
- llm_api_scope-0.6.0/apiscope/repo/fetch.py +132 -0
- llm_api_scope-0.6.0/apiscope/repo/schema.py +53 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/schema.py +2 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0/llm_api_scope.egg-info}/PKG-INFO +7 -1
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/llm_api_scope.egg-info/SOURCES.txt +4 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/pyproject.toml +1 -1
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/LICENSE +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/__init__.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/__init__.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/app.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/fetch.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/reader.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/schema.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/spec/__init__.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/spec/app.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/openapi/spec/schema.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/__init__.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/app.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/fetch.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/parse_txt.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/parse_xml.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/schema.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/apiscope/rfc/search.py +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/llm_api_scope.egg-info/dependency_links.txt +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/llm_api_scope.egg-info/entry_points.txt +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/llm_api_scope.egg-info/requires.txt +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/llm_api_scope.egg-info/top_level.txt +0 -0
- {llm_api_scope-0.5.1 → llm_api_scope-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llm-api-scope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: read and cache structured documents from remote for LLM agents
|
|
5
5
|
Author-email: D7x7z49 <85430783+D7x7z49@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -79,6 +79,12 @@ read, search, and navigate RFC documents from the IETF.
|
|
|
79
79
|
|
|
80
80
|
the metadata index is mirrored once via rsync. individual text files are fetched on demand and cached locally. you can browse the table of contents, jump to a section (XML) or page (TXT), filter the index by status or source, and run keyword searches against fulltext content.
|
|
81
81
|
|
|
82
|
+
### repo
|
|
83
|
+
|
|
84
|
+
sync documentation directories from any git repository to a local cache.
|
|
85
|
+
|
|
86
|
+
register repositories by URL with a target subdirectory and a ref — a branch, tag, or commit. the sync command clones with shallow depth, blobless filter, and sparse checkout so only the needed tree and files come over the wire. extracted docs land in the cache under a stable hash path, and a configurable TTL avoids redundant re-fetches. provider-agnostic, zero auth.
|
|
87
|
+
|
|
82
88
|
## future
|
|
83
89
|
|
|
84
90
|
- read academic papers from arxiv
|
|
@@ -30,6 +30,12 @@ read, search, and navigate RFC documents from the IETF.
|
|
|
30
30
|
|
|
31
31
|
the metadata index is mirrored once via rsync. individual text files are fetched on demand and cached locally. you can browse the table of contents, jump to a section (XML) or page (TXT), filter the index by status or source, and run keyword searches against fulltext content.
|
|
32
32
|
|
|
33
|
+
### repo
|
|
34
|
+
|
|
35
|
+
sync documentation directories from any git repository to a local cache.
|
|
36
|
+
|
|
37
|
+
register repositories by URL with a target subdirectory and a ref — a branch, tag, or commit. the sync command clones with shallow depth, blobless filter, and sparse checkout so only the needed tree and files come over the wire. extracted docs land in the cache under a stable hash path, and a configurable TTL avoids redundant re-fetches. provider-agnostic, zero auth.
|
|
38
|
+
|
|
33
39
|
## future
|
|
34
40
|
|
|
35
41
|
- read academic papers from arxiv
|
|
@@ -23,6 +23,8 @@ DEFAULT_CONFIG_SCHEMA_PATH = DEFAULT_ROOT / "config.schema.json"
|
|
|
23
23
|
|
|
24
24
|
CACHE_ROOT = DEFAULT_ROOT / "cache"
|
|
25
25
|
|
|
26
|
+
TMP_ROOT = DEFAULT_ROOT / "tmp"
|
|
27
|
+
|
|
26
28
|
# ==============================================================================
|
|
27
29
|
# config model
|
|
28
30
|
# ==============================================================================
|
|
@@ -51,6 +53,21 @@ class OpenapiConfig(BaseConfig):
|
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
class RfcConfig(BaseConfig):
|
|
56
|
+
# override
|
|
57
|
+
cache_ttl: int = Field(default=60 * 60 * 24 * 30) # 1 month
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RepoEntryConfig(BaseModel):
|
|
61
|
+
dir: str
|
|
62
|
+
|
|
63
|
+
# format - branch:<branch>, tag:<tag>, commit:<short-hash>
|
|
64
|
+
# e.g. branch:main, tag:v1.0, commit:abc1234
|
|
65
|
+
target: str = Field(default="branch:main")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RepoConfig(BaseConfig):
|
|
69
|
+
entries: dict[str, RepoEntryConfig] = Field(default_factory=dict)
|
|
70
|
+
|
|
54
71
|
# override
|
|
55
72
|
cache_ttl: int = Field(default=60 * 60 * 24 * 7) # 1 week
|
|
56
73
|
|
|
@@ -58,6 +75,7 @@ class RfcConfig(BaseConfig):
|
|
|
58
75
|
class Config(BaseModel):
|
|
59
76
|
openapi: OpenapiConfig = Field(default_factory=OpenapiConfig)
|
|
60
77
|
rfc: RfcConfig = Field(default_factory=RfcConfig)
|
|
78
|
+
repo: RepoConfig = Field(default_factory=RepoConfig)
|
|
61
79
|
|
|
62
80
|
@classmethod
|
|
63
81
|
def read(cls, path: Path) -> "Config":
|
|
@@ -85,6 +103,7 @@ class Config(BaseModel):
|
|
|
85
103
|
merged.openapi.alias |= other.openapi.alias
|
|
86
104
|
if other.openapi.proxy is not None:
|
|
87
105
|
merged.openapi.proxy = other.openapi.proxy
|
|
106
|
+
merged.repo.entries = self.repo.entries | other.repo.entries
|
|
88
107
|
return merged
|
|
89
108
|
|
|
90
109
|
|
|
@@ -25,6 +25,7 @@ from apiscope.config import (
|
|
|
25
25
|
get_project_config_path,
|
|
26
26
|
)
|
|
27
27
|
from apiscope.openapi import openapi_app
|
|
28
|
+
from apiscope.repo import check_repo_deps, repo_app
|
|
28
29
|
from apiscope.rfc import check_rfc_deps, rfc_app
|
|
29
30
|
from apiscope.schema import CommandContext
|
|
30
31
|
|
|
@@ -57,6 +58,7 @@ def callback(ctx: typer.Context) -> None:
|
|
|
57
58
|
|
|
58
59
|
app.add_typer(openapi_app, name="openapi")
|
|
59
60
|
app.add_typer(rfc_app, name="rfc")
|
|
61
|
+
app.add_typer(repo_app, name="repo")
|
|
60
62
|
|
|
61
63
|
# ==============================================================================
|
|
62
64
|
# commands
|
|
@@ -74,7 +76,7 @@ def health(
|
|
|
74
76
|
|
|
75
77
|
# gather issues from all modules
|
|
76
78
|
issues: list[str] = []
|
|
77
|
-
for label, check in [("rfc", check_rfc_deps)]:
|
|
79
|
+
for label, check in [("rfc", check_rfc_deps), ("repo", check_repo_deps)]:
|
|
78
80
|
err = check()
|
|
79
81
|
if err is not None:
|
|
80
82
|
issues.append(f"[{label}] {err}")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# apiscope/repo/app.py
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from apiscope.config import (
|
|
9
|
+
CACHE_ROOT,
|
|
10
|
+
DEFAULT_CONFIG_PATH,
|
|
11
|
+
TMP_ROOT,
|
|
12
|
+
Config,
|
|
13
|
+
RepoEntryConfig,
|
|
14
|
+
get_project_config_path,
|
|
15
|
+
)
|
|
16
|
+
from apiscope.repo.fetch import sync_entry
|
|
17
|
+
from apiscope.repo.schema import RepoCommandContext, RepoEntry
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="sync documentation from git repositories")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ==============================================================================
|
|
23
|
+
# public helpers
|
|
24
|
+
# ==============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_deps() -> str | None:
|
|
28
|
+
if shutil.which("git") is None:
|
|
29
|
+
return "git is required but not found in PATH"
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ==============================================================================
|
|
34
|
+
# helpers
|
|
35
|
+
# ==============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve_target(global_flag: bool) -> Path:
|
|
39
|
+
if global_flag:
|
|
40
|
+
return DEFAULT_CONFIG_PATH
|
|
41
|
+
project = get_project_config_path()
|
|
42
|
+
if project is not None and project.exists():
|
|
43
|
+
return project
|
|
44
|
+
return DEFAULT_CONFIG_PATH
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ==============================================================================
|
|
48
|
+
# callback
|
|
49
|
+
# ==============================================================================
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.callback()
|
|
53
|
+
def repo_callback(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
global_flag: bool = typer.Option(
|
|
56
|
+
False, "--global", "-g", help="edit global config instead of project config"
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
# check required external tools
|
|
60
|
+
err = check_deps()
|
|
61
|
+
if err is not None:
|
|
62
|
+
typer.echo(err, err=True)
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
|
|
65
|
+
# prepare cache directories
|
|
66
|
+
repo_cache_dir = CACHE_ROOT / "repo"
|
|
67
|
+
repo_tmp_dir = TMP_ROOT
|
|
68
|
+
repo_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
repo_tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
# inject context for subcommands
|
|
72
|
+
ctx.obj.repo_command_context = RepoCommandContext(
|
|
73
|
+
cache_dir=repo_cache_dir,
|
|
74
|
+
tmp_dir=repo_tmp_dir,
|
|
75
|
+
global_flag=global_flag,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ==============================================================================
|
|
80
|
+
# commands
|
|
81
|
+
# ==============================================================================
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command(name="add", help="register a repo for syncing")
|
|
85
|
+
def add_repo(
|
|
86
|
+
ctx: typer.Context,
|
|
87
|
+
url: str = typer.Argument(help="git clone URL"),
|
|
88
|
+
dir: str = typer.Argument(help="directory within repo to extract"),
|
|
89
|
+
target: str = typer.Option(
|
|
90
|
+
"branch:main", "--target", help="ref target (branch:, tag:, commit:)"
|
|
91
|
+
),
|
|
92
|
+
) -> None:
|
|
93
|
+
# duplicate check against merged entries
|
|
94
|
+
if url in ctx.obj.config.repo.entries:
|
|
95
|
+
typer.echo(f"<{url}> already registered", err=True)
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
|
|
98
|
+
target_path = _resolve_target(ctx.obj.repo_command_context.global_flag)
|
|
99
|
+
with Config.edit(target_path) as cfg:
|
|
100
|
+
cfg.repo.entries[url] = RepoEntryConfig(dir=dir, target=target)
|
|
101
|
+
|
|
102
|
+
typer.echo(f"registered <{url}>")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command(name="remove", help="remove a registered repo")
|
|
106
|
+
def remove_repo(
|
|
107
|
+
ctx: typer.Context,
|
|
108
|
+
url: str = typer.Argument(help="git clone URL"),
|
|
109
|
+
) -> None:
|
|
110
|
+
target_path = _resolve_target(ctx.obj.repo_command_context.global_flag)
|
|
111
|
+
with Config.edit(target_path) as cfg:
|
|
112
|
+
if url not in cfg.repo.entries:
|
|
113
|
+
typer.echo(f"<{url}> not registered", err=True)
|
|
114
|
+
raise typer.Exit(code=1)
|
|
115
|
+
del cfg.repo.entries[url]
|
|
116
|
+
|
|
117
|
+
typer.echo(f"removed <{url}>")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command(name="list", help="list registered repos")
|
|
121
|
+
def list_repos(ctx: typer.Context) -> None:
|
|
122
|
+
entries = RepoEntry.from_config(ctx.obj.config.repo)
|
|
123
|
+
if not entries:
|
|
124
|
+
typer.echo("no repos registered")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
cache_dir = ctx.obj.repo_command_context.cache_dir
|
|
128
|
+
|
|
129
|
+
for entry in entries:
|
|
130
|
+
doc_path = cache_dir / entry.sha256_id
|
|
131
|
+
typer.echo(f"- [{entry.url}] [{entry.target}@{entry.dir}] <{doc_path}>")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command(name="sync", help="sync all registered repos to cache")
|
|
135
|
+
def sync_repos(
|
|
136
|
+
ctx: typer.Context,
|
|
137
|
+
force: bool = typer.Option(False, "--force", help="force re-sync ignoring cache TTL"),
|
|
138
|
+
) -> None:
|
|
139
|
+
entries = RepoEntry.from_config(ctx.obj.config.repo)
|
|
140
|
+
if not entries:
|
|
141
|
+
typer.echo("no repos registered")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
rc = ctx.obj.repo_command_context
|
|
145
|
+
ttl = ctx.obj.config.repo.cache_ttl
|
|
146
|
+
|
|
147
|
+
for entry in entries:
|
|
148
|
+
err = sync_entry(entry, rc.tmp_dir, rc.cache_dir, force=force, ttl=ttl)
|
|
149
|
+
if err is not None:
|
|
150
|
+
typer.echo(f"sync failed for <{entry.url}>: {err}", err=True)
|
|
151
|
+
else:
|
|
152
|
+
typer.echo(f"synced <{entry.url}>")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# apiscope/repo/fetch.py
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from apiscope.repo.schema import RepoEntry
|
|
9
|
+
|
|
10
|
+
# ==============================================================================
|
|
11
|
+
# git helpers
|
|
12
|
+
# ==============================================================================
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _run_git(args: list[str], cwd: Path | None = None) -> str | None:
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
["git", *args],
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
cwd=cwd,
|
|
21
|
+
)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
return result.stderr.strip()
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _clone_repo(url: str, work_dir: Path, branch: str | None = None) -> str | None:
|
|
28
|
+
args = [
|
|
29
|
+
"clone",
|
|
30
|
+
"--depth=1",
|
|
31
|
+
"--filter=blob:none",
|
|
32
|
+
"--sparse",
|
|
33
|
+
]
|
|
34
|
+
if branch is not None:
|
|
35
|
+
args.extend(["--branch", branch])
|
|
36
|
+
args.extend([url, str(work_dir)])
|
|
37
|
+
return _run_git(args)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fetch_commit(work_dir: Path, commit: str) -> str | None:
|
|
41
|
+
err = _run_git(["fetch", "origin", "--depth=1", commit], cwd=work_dir)
|
|
42
|
+
if err is not None:
|
|
43
|
+
return err
|
|
44
|
+
return _run_git(["checkout", "FETCH_HEAD"], cwd=work_dir)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sparse_set(work_dir: Path, directory: str) -> str | None:
|
|
48
|
+
return _run_git(["sparse-checkout", "set", directory], cwd=work_dir)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ==============================================================================
|
|
52
|
+
# sync
|
|
53
|
+
# ==============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _copy_docs(work_dir: Path, directory: str, dst: Path) -> str | None:
|
|
57
|
+
src = work_dir / directory
|
|
58
|
+
if not src.exists():
|
|
59
|
+
return f"directory [{directory}] not found in <{work_dir}>"
|
|
60
|
+
shutil.rmtree(dst, ignore_errors=True)
|
|
61
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
try:
|
|
63
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
64
|
+
except shutil.Error as e:
|
|
65
|
+
return str(e)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def sync_entry(
|
|
70
|
+
entry: RepoEntry,
|
|
71
|
+
tmp_dir: Path,
|
|
72
|
+
cache_dir: Path,
|
|
73
|
+
*,
|
|
74
|
+
force: bool = False,
|
|
75
|
+
ttl: int,
|
|
76
|
+
) -> str | None:
|
|
77
|
+
work_dir = tmp_dir / entry.sha256_id
|
|
78
|
+
dst_dir = cache_dir / entry.sha256_id
|
|
79
|
+
|
|
80
|
+
# skip if cache is fresh (not stale) and not forced
|
|
81
|
+
if not force and not _is_stale(dst_dir, ttl):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
# cleanup previous state
|
|
85
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
86
|
+
shutil.rmtree(dst_dir, ignore_errors=True)
|
|
87
|
+
|
|
88
|
+
ref = entry.reference
|
|
89
|
+
|
|
90
|
+
if ref is not None and ref[0] == "commit":
|
|
91
|
+
# commit ref: clone default branch first, then fetch + checkout specific commit
|
|
92
|
+
ref_name = ref[1]
|
|
93
|
+
err = _clone_repo(entry.url, work_dir)
|
|
94
|
+
if err is not None:
|
|
95
|
+
return err
|
|
96
|
+
err = _fetch_commit(work_dir, ref_name)
|
|
97
|
+
if err is not None:
|
|
98
|
+
return err
|
|
99
|
+
else:
|
|
100
|
+
# branch or tag: --branch accepts both (tags checkout in detached HEAD)
|
|
101
|
+
branch = ref[1] if ref is not None else "main"
|
|
102
|
+
err = _clone_repo(entry.url, work_dir, branch=branch)
|
|
103
|
+
if err is not None:
|
|
104
|
+
return err
|
|
105
|
+
|
|
106
|
+
# sparse checkout target directory
|
|
107
|
+
err = _sparse_set(work_dir, entry.dir)
|
|
108
|
+
if err is not None:
|
|
109
|
+
return err
|
|
110
|
+
|
|
111
|
+
# copy extracted docs to cache
|
|
112
|
+
err = _copy_docs(work_dir, entry.dir, dst_dir)
|
|
113
|
+
if err is not None:
|
|
114
|
+
return err
|
|
115
|
+
|
|
116
|
+
# cleanup working directory
|
|
117
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ==============================================================================
|
|
123
|
+
# freshness
|
|
124
|
+
# ==============================================================================
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_stale(path: Path, ttl: int) -> bool:
|
|
128
|
+
if not path.exists():
|
|
129
|
+
return True
|
|
130
|
+
stat = path.stat()
|
|
131
|
+
latest = max(stat.st_mtime, stat.st_ctime)
|
|
132
|
+
return time.time() - latest > ttl
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# apiscope/repo/schema.py
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from apiscope.config import RepoConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RepoEntry(BaseModel):
|
|
14
|
+
url: str
|
|
15
|
+
dir: str
|
|
16
|
+
target: str
|
|
17
|
+
|
|
18
|
+
def __hash__(self) -> int:
|
|
19
|
+
return hash(self.sha256_id)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def sha256_id(self) -> str:
|
|
23
|
+
# first 8 chars of sha256 hex digest; sufficient for collision resistance
|
|
24
|
+
return hashlib.sha256(self.url.encode()).hexdigest()[:8]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def reference(self) -> tuple[str, str] | None:
|
|
28
|
+
if self.target.startswith("branch:"):
|
|
29
|
+
return "branch", self.target[len("branch:") :]
|
|
30
|
+
elif self.target.startswith("tag:"):
|
|
31
|
+
return "tag", self.target[len("tag:") :]
|
|
32
|
+
elif self.target.startswith("commit:"):
|
|
33
|
+
return "commit", self.target[len("commit:") :]
|
|
34
|
+
else:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_config(cls, config: "RepoConfig") -> set["RepoEntry"]:
|
|
39
|
+
return {cls(url=url, dir=ec.dir, target=ec.target) for url, ec in config.entries.items()}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ==============================================================================
|
|
43
|
+
# context
|
|
44
|
+
# ==============================================================================
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RepoCommandContext(BaseModel):
|
|
48
|
+
# computed properties
|
|
49
|
+
cache_dir: Path
|
|
50
|
+
tmp_dir: Path
|
|
51
|
+
|
|
52
|
+
# sub command options
|
|
53
|
+
global_flag: bool
|
|
@@ -4,6 +4,7 @@ from pydantic import BaseModel
|
|
|
4
4
|
|
|
5
5
|
from apiscope.config import Config
|
|
6
6
|
from apiscope.openapi.schema import OpenapiCommandContext
|
|
7
|
+
from apiscope.repo.schema import RepoCommandContext
|
|
7
8
|
from apiscope.rfc.schema import RfcCommandContext
|
|
8
9
|
|
|
9
10
|
|
|
@@ -11,3 +12,4 @@ class CommandContext(BaseModel):
|
|
|
11
12
|
config: Config
|
|
12
13
|
openapi_command_context: OpenapiCommandContext | None = None
|
|
13
14
|
rfc_command_context: RfcCommandContext | None = None
|
|
15
|
+
repo_command_context: RepoCommandContext | None = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llm-api-scope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: read and cache structured documents from remote for LLM agents
|
|
5
5
|
Author-email: D7x7z49 <85430783+D7x7z49@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -79,6 +79,12 @@ read, search, and navigate RFC documents from the IETF.
|
|
|
79
79
|
|
|
80
80
|
the metadata index is mirrored once via rsync. individual text files are fetched on demand and cached locally. you can browse the table of contents, jump to a section (XML) or page (TXT), filter the index by status or source, and run keyword searches against fulltext content.
|
|
81
81
|
|
|
82
|
+
### repo
|
|
83
|
+
|
|
84
|
+
sync documentation directories from any git repository to a local cache.
|
|
85
|
+
|
|
86
|
+
register repositories by URL with a target subdirectory and a ref — a branch, tag, or commit. the sync command clones with shallow depth, blobless filter, and sparse checkout so only the needed tree and files come over the wire. extracted docs land in the cache under a stable hash path, and a configurable TTL avoids redundant re-fetches. provider-agnostic, zero auth.
|
|
87
|
+
|
|
82
88
|
## future
|
|
83
89
|
|
|
84
90
|
- read academic papers from arxiv
|
|
@@ -13,6 +13,10 @@ apiscope/openapi/schema.py
|
|
|
13
13
|
apiscope/openapi/spec/__init__.py
|
|
14
14
|
apiscope/openapi/spec/app.py
|
|
15
15
|
apiscope/openapi/spec/schema.py
|
|
16
|
+
apiscope/repo/__init__.py
|
|
17
|
+
apiscope/repo/app.py
|
|
18
|
+
apiscope/repo/fetch.py
|
|
19
|
+
apiscope/repo/schema.py
|
|
16
20
|
apiscope/rfc/__init__.py
|
|
17
21
|
apiscope/rfc/app.py
|
|
18
22
|
apiscope/rfc/fetch.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|