codealmanac 0.1.0.dev0__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.
- codealmanac/__init__.py +13 -0
- codealmanac/app.py +175 -0
- codealmanac/cli/__init__.py +1 -0
- codealmanac/cli/dispatch/__init__.py +0 -0
- codealmanac/cli/dispatch/admin.py +124 -0
- codealmanac/cli/dispatch/config.py +50 -0
- codealmanac/cli/dispatch/root.py +328 -0
- codealmanac/cli/main.py +28 -0
- codealmanac/cli/parser/__init__.py +0 -0
- codealmanac/cli/parser/admin.py +81 -0
- codealmanac/cli/parser/lifecycle.py +57 -0
- codealmanac/cli/parser/root.py +19 -0
- codealmanac/cli/parser/wiki.py +87 -0
- codealmanac/cli/render/__init__.py +0 -0
- codealmanac/cli/render/admin.py +191 -0
- codealmanac/cli/render/root.py +290 -0
- codealmanac/core/__init__.py +1 -0
- codealmanac/core/errors.py +45 -0
- codealmanac/core/models.py +14 -0
- codealmanac/core/paths.py +25 -0
- codealmanac/core/slug.py +7 -0
- codealmanac/core/text.py +5 -0
- codealmanac/database/__init__.py +15 -0
- codealmanac/database/sqlite.py +54 -0
- codealmanac/integrations/__init__.py +1 -0
- codealmanac/integrations/automation/__init__.py +3 -0
- codealmanac/integrations/automation/scheduler/__init__.py +5 -0
- codealmanac/integrations/automation/scheduler/launchd.py +163 -0
- codealmanac/integrations/command.py +56 -0
- codealmanac/integrations/harnesses/__init__.py +7 -0
- codealmanac/integrations/harnesses/claude/__init__.py +1 -0
- codealmanac/integrations/harnesses/claude/adapter.py +217 -0
- codealmanac/integrations/harnesses/codex/__init__.py +3 -0
- codealmanac/integrations/harnesses/codex/adapter.py +221 -0
- codealmanac/integrations/harnesses/git_status.py +49 -0
- codealmanac/integrations/sources/__init__.py +29 -0
- codealmanac/integrations/sources/filesystem/__init__.py +5 -0
- codealmanac/integrations/sources/filesystem/adapter.py +685 -0
- codealmanac/integrations/sources/filesystem/selection.py +209 -0
- codealmanac/integrations/sources/git/__init__.py +3 -0
- codealmanac/integrations/sources/git/adapter.py +132 -0
- codealmanac/integrations/sources/github/__init__.py +3 -0
- codealmanac/integrations/sources/github/adapter.py +413 -0
- codealmanac/integrations/sources/runtime.py +22 -0
- codealmanac/integrations/sources/transcripts/__init__.py +33 -0
- codealmanac/integrations/sources/transcripts/claude.py +61 -0
- codealmanac/integrations/sources/transcripts/codex.py +69 -0
- codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
- codealmanac/integrations/sources/transcripts/runtime.py +387 -0
- codealmanac/integrations/sources/web/__init__.py +3 -0
- codealmanac/integrations/sources/web/adapter.py +303 -0
- codealmanac/integrations/updates/__init__.py +7 -0
- codealmanac/integrations/updates/package.py +85 -0
- codealmanac/integrations/workspaces/__init__.py +1 -0
- codealmanac/integrations/workspaces/git/__init__.py +3 -0
- codealmanac/integrations/workspaces/git/probe.py +128 -0
- codealmanac/manual/README.md +24 -0
- codealmanac/manual/__init__.py +19 -0
- codealmanac/manual/build.md +20 -0
- codealmanac/manual/evidence.md +23 -0
- codealmanac/manual/garden.md +20 -0
- codealmanac/manual/ingest.md +17 -0
- codealmanac/manual/library.py +84 -0
- codealmanac/manual/models.py +83 -0
- codealmanac/manual/pages.md +28 -0
- codealmanac/manual/requests.py +6 -0
- codealmanac/manual/sources.md +18 -0
- codealmanac/manual/style.md +19 -0
- codealmanac/prompts/__init__.py +5 -0
- codealmanac/prompts/base/notability.md +14 -0
- codealmanac/prompts/base/purpose.md +23 -0
- codealmanac/prompts/base/syntax.md +19 -0
- codealmanac/prompts/models.py +9 -0
- codealmanac/prompts/operations/garden.md +26 -0
- codealmanac/prompts/operations/ingest.md +18 -0
- codealmanac/prompts/renderer.py +24 -0
- codealmanac/prompts/requests.py +22 -0
- codealmanac/server/__init__.py +1 -0
- codealmanac/server/app.py +202 -0
- codealmanac/server/assets/__init__.py +1 -0
- codealmanac/server/assets/app.css +865 -0
- codealmanac/server/assets/app.js +3 -0
- codealmanac/server/assets/index.html +80 -0
- codealmanac/server/assets/viewer/api.js +30 -0
- codealmanac/server/assets/viewer/components.js +197 -0
- codealmanac/server/assets/viewer/main.js +126 -0
- codealmanac/server/assets/viewer/renderers.js +122 -0
- codealmanac/server/assets/viewer/routes.js +36 -0
- codealmanac/services/__init__.py +1 -0
- codealmanac/services/automation/__init__.py +3 -0
- codealmanac/services/automation/models.py +83 -0
- codealmanac/services/automation/ports.py +14 -0
- codealmanac/services/automation/requests.py +40 -0
- codealmanac/services/automation/service.py +294 -0
- codealmanac/services/config/__init__.py +17 -0
- codealmanac/services/config/models.py +61 -0
- codealmanac/services/config/requests.py +21 -0
- codealmanac/services/config/service.py +55 -0
- codealmanac/services/config/store.py +26 -0
- codealmanac/services/diagnostics/__init__.py +1 -0
- codealmanac/services/diagnostics/models.py +22 -0
- codealmanac/services/diagnostics/requests.py +8 -0
- codealmanac/services/diagnostics/service.py +283 -0
- codealmanac/services/harnesses/__init__.py +1 -0
- codealmanac/services/harnesses/models.py +104 -0
- codealmanac/services/harnesses/ports.py +18 -0
- codealmanac/services/harnesses/requests.py +19 -0
- codealmanac/services/harnesses/service.py +38 -0
- codealmanac/services/health/__init__.py +1 -0
- codealmanac/services/health/requests.py +8 -0
- codealmanac/services/health/service.py +20 -0
- codealmanac/services/index/__init__.py +1 -0
- codealmanac/services/index/models.py +135 -0
- codealmanac/services/index/requests.py +26 -0
- codealmanac/services/index/service.py +86 -0
- codealmanac/services/index/store.py +411 -0
- codealmanac/services/index/views.py +524 -0
- codealmanac/services/pages/__init__.py +1 -0
- codealmanac/services/pages/requests.py +17 -0
- codealmanac/services/pages/service.py +26 -0
- codealmanac/services/runs/__init__.py +1 -0
- codealmanac/services/runs/models.py +91 -0
- codealmanac/services/runs/requests.py +76 -0
- codealmanac/services/runs/service.py +86 -0
- codealmanac/services/runs/store.py +256 -0
- codealmanac/services/search/__init__.py +1 -0
- codealmanac/services/search/requests.py +23 -0
- codealmanac/services/search/service.py +31 -0
- codealmanac/services/sources/__init__.py +1 -0
- codealmanac/services/sources/models.py +126 -0
- codealmanac/services/sources/ports.py +30 -0
- codealmanac/services/sources/requests.py +76 -0
- codealmanac/services/sources/service.py +351 -0
- codealmanac/services/tagging/__init__.py +1 -0
- codealmanac/services/tagging/models.py +9 -0
- codealmanac/services/tagging/requests.py +35 -0
- codealmanac/services/tagging/service.py +43 -0
- codealmanac/services/topics/__init__.py +1 -0
- codealmanac/services/topics/models.py +36 -0
- codealmanac/services/topics/requests.py +115 -0
- codealmanac/services/topics/service.py +297 -0
- codealmanac/services/updates/__init__.py +4 -0
- codealmanac/services/updates/models.py +83 -0
- codealmanac/services/updates/ports.py +17 -0
- codealmanac/services/updates/requests.py +10 -0
- codealmanac/services/updates/service.py +113 -0
- codealmanac/services/viewer/__init__.py +1 -0
- codealmanac/services/viewer/models.py +80 -0
- codealmanac/services/viewer/renderer.py +89 -0
- codealmanac/services/viewer/requests.py +86 -0
- codealmanac/services/viewer/service.py +211 -0
- codealmanac/services/wiki/__init__.py +1 -0
- codealmanac/services/wiki/documents.py +83 -0
- codealmanac/services/wiki/frontmatter.py +94 -0
- codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
- codealmanac/services/wiki/models.py +69 -0
- codealmanac/services/wiki/paths.py +42 -0
- codealmanac/services/wiki/service.py +57 -0
- codealmanac/services/wiki/templates.py +73 -0
- codealmanac/services/wiki/topics.py +266 -0
- codealmanac/services/wiki/wikilinks.py +58 -0
- codealmanac/services/workspaces/__init__.py +1 -0
- codealmanac/services/workspaces/models.py +124 -0
- codealmanac/services/workspaces/ports.py +9 -0
- codealmanac/services/workspaces/requests.py +82 -0
- codealmanac/services/workspaces/roots.py +74 -0
- codealmanac/services/workspaces/service.py +303 -0
- codealmanac/services/workspaces/store.py +127 -0
- codealmanac/workflows/__init__.py +1 -0
- codealmanac/workflows/build/__init__.py +1 -0
- codealmanac/workflows/build/models.py +8 -0
- codealmanac/workflows/build/service.py +45 -0
- codealmanac/workflows/garden/__init__.py +3 -0
- codealmanac/workflows/garden/models.py +30 -0
- codealmanac/workflows/garden/requests.py +22 -0
- codealmanac/workflows/garden/service.py +239 -0
- codealmanac/workflows/ingest/__init__.py +1 -0
- codealmanac/workflows/ingest/models.py +26 -0
- codealmanac/workflows/ingest/requests.py +39 -0
- codealmanac/workflows/ingest/service.py +302 -0
- codealmanac/workflows/lifecycle.py +197 -0
- codealmanac/workflows/sync/__init__.py +3 -0
- codealmanac/workflows/sync/models.py +157 -0
- codealmanac/workflows/sync/requests.py +63 -0
- codealmanac/workflows/sync/service.py +651 -0
- codealmanac/workflows/sync/store.py +51 -0
- codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
- codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
- codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
- codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
- codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Garden Operation
|
|
2
|
+
|
|
3
|
+
Garden improves the existing configured Almanac wiki as a whole graph.
|
|
4
|
+
|
|
5
|
+
Before editing, read `manual/README.md`, `manual/pages.md`,
|
|
6
|
+
`manual/evidence.md`, `manual/style.md`, `manual/sources.md`, and
|
|
7
|
+
`manual/garden.md` under the configured Almanac root.
|
|
8
|
+
|
|
9
|
+
Garden is cultivation. The goal is not to add activity; the goal is to make the
|
|
10
|
+
project memory more coherent, navigable, current, and trustworthy.
|
|
11
|
+
|
|
12
|
+
Inspect pages, topics, links, referenced files, and health issues where useful.
|
|
13
|
+
Look for duplicate pages, stale claims, missing anchors, missing links, bloated
|
|
14
|
+
pages, confusing topics, unsupported claims, disconnected temporal notes, and
|
|
15
|
+
clusters that need hubs.
|
|
16
|
+
|
|
17
|
+
Prefer synthesis over logs. Fold fragments into evolving pages when chronology
|
|
18
|
+
is not part of the meaning. Split pages that contain independent concepts.
|
|
19
|
+
Merge overlapping pages when one page is the better home. Archive or supersede
|
|
20
|
+
stale pages only when preserving history helps future agents.
|
|
21
|
+
|
|
22
|
+
Improve topic neighborhoods, leads, links, source notes, and page boundaries.
|
|
23
|
+
Create or revise hub pages when a dense cluster needs reading order.
|
|
24
|
+
|
|
25
|
+
No-op is valid when the wiki is already coherent enough for the current pass.
|
|
26
|
+
Do not churn unrelated pages to show activity.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Ingest Operation
|
|
2
|
+
|
|
3
|
+
Ingest starts from bounded selected material and distills reusable project
|
|
4
|
+
understanding into the existing configured Almanac graph.
|
|
5
|
+
|
|
6
|
+
Before editing, read `manual/README.md`, `manual/pages.md`,
|
|
7
|
+
`manual/evidence.md`, `manual/style.md`, `manual/sources.md`, and
|
|
8
|
+
`manual/ingest.md` under the configured Almanac root.
|
|
9
|
+
|
|
10
|
+
Use the source briefs and source runtime snapshots in the runtime context as
|
|
11
|
+
operation input. The brief identifies the selected source and its provenance
|
|
12
|
+
hint. The runtime snapshot is readable source material gathered before the
|
|
13
|
+
agent run. Update existing synthesis pages when they are the right home. Create
|
|
14
|
+
new pages only when the material names a durable concept, decision, flow,
|
|
15
|
+
invariant, incident, gotcha, or project-world fact that deserves its own page.
|
|
16
|
+
|
|
17
|
+
Preserve the source-selection boundary. The selected material is raw material,
|
|
18
|
+
not the source of truth for every claim.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
|
|
3
|
+
from codealmanac.prompts.models import PromptName
|
|
4
|
+
from codealmanac.prompts.requests import RenderPromptRequest
|
|
5
|
+
|
|
6
|
+
PROMPTS_PACKAGE = "codealmanac.prompts"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PromptRenderer:
|
|
10
|
+
def render(self, request: RenderPromptRequest) -> str:
|
|
11
|
+
sections = [prompt_text(section) for section in request.sections]
|
|
12
|
+
sections.extend(request.context)
|
|
13
|
+
return join_prompt_sections(tuple(sections))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prompt_text(name: PromptName) -> str:
|
|
17
|
+
resource = files(PROMPTS_PACKAGE).joinpath(*name.value.split("/"))
|
|
18
|
+
return resource.read_text(encoding="utf-8").strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def join_prompt_sections(sections: tuple[str, ...]) -> str:
|
|
22
|
+
return "\n\n---\n\n".join(
|
|
23
|
+
section.strip() for section in sections if section.strip()
|
|
24
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pydantic import field_validator
|
|
2
|
+
|
|
3
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
4
|
+
from codealmanac.core.text import required_text
|
|
5
|
+
from codealmanac.prompts.models import PromptName
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RenderPromptRequest(CodeAlmanacModel):
|
|
9
|
+
sections: tuple[PromptName, ...]
|
|
10
|
+
context: tuple[str, ...] = ()
|
|
11
|
+
|
|
12
|
+
@field_validator("sections")
|
|
13
|
+
@classmethod
|
|
14
|
+
def require_sections(cls, value: tuple[PromptName, ...]) -> tuple[PromptName, ...]:
|
|
15
|
+
if len(value) == 0:
|
|
16
|
+
raise ValueError("at least one prompt section is required")
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
@field_validator("context")
|
|
20
|
+
@classmethod
|
|
21
|
+
def require_context_text(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
22
|
+
return tuple(required_text(item, "prompt context") for item in value)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local read-only web server adapter."""
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from importlib import resources
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException
|
|
6
|
+
from fastapi.responses import HTMLResponse, Response
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
|
|
8
|
+
|
|
9
|
+
from codealmanac.app import CodeAlmanac
|
|
10
|
+
from codealmanac.core.errors import CodeAlmanacError, ConflictError, NotFoundError
|
|
11
|
+
from codealmanac.services.viewer.models import (
|
|
12
|
+
ViewerFile,
|
|
13
|
+
ViewerOverview,
|
|
14
|
+
ViewerPage,
|
|
15
|
+
ViewerSearch,
|
|
16
|
+
ViewerTopic,
|
|
17
|
+
)
|
|
18
|
+
from codealmanac.services.viewer.requests import (
|
|
19
|
+
ViewerFileRequest,
|
|
20
|
+
ViewerOverviewRequest,
|
|
21
|
+
ViewerPageRequest,
|
|
22
|
+
ViewerSearchRequest,
|
|
23
|
+
ViewerTopicRequest,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ServerAssetSuffix(StrEnum):
|
|
28
|
+
HTML = ".html"
|
|
29
|
+
CSS = ".css"
|
|
30
|
+
JAVASCRIPT = ".js"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ASSET_MEDIA_TYPES: dict[ServerAssetSuffix, str] = {
|
|
34
|
+
ServerAssetSuffix.HTML: "text/html",
|
|
35
|
+
ServerAssetSuffix.CSS: "text/css",
|
|
36
|
+
ServerAssetSuffix.JAVASCRIPT: "text/javascript",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServerAssetRequest(BaseModel):
|
|
41
|
+
model_config = ConfigDict(frozen=True)
|
|
42
|
+
|
|
43
|
+
path: str
|
|
44
|
+
|
|
45
|
+
@field_validator("path")
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_path(cls, value: str) -> str:
|
|
48
|
+
if value != value.strip():
|
|
49
|
+
raise ValueError("asset path must not contain surrounding whitespace")
|
|
50
|
+
if not value:
|
|
51
|
+
raise ValueError("asset path is required")
|
|
52
|
+
if value.startswith("/") or "\\" in value:
|
|
53
|
+
raise ValueError("asset path must be relative")
|
|
54
|
+
parts = value.split("/")
|
|
55
|
+
if any(part in {"", ".", ".."} for part in parts):
|
|
56
|
+
raise ValueError("asset path contains an invalid segment")
|
|
57
|
+
suffix = Path(value).suffix
|
|
58
|
+
if suffix not in {item.value for item in ServerAssetSuffix}:
|
|
59
|
+
raise ValueError("asset path has an unsupported extension")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def media_type(self) -> str:
|
|
64
|
+
return ASSET_MEDIA_TYPES[ServerAssetSuffix(Path(self.path).suffix)]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def parts(self) -> list[str]:
|
|
68
|
+
return self.path.split("/")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_server_app(
|
|
72
|
+
codealmanac: CodeAlmanac,
|
|
73
|
+
cwd: Path,
|
|
74
|
+
wiki: str | None = None,
|
|
75
|
+
) -> FastAPI:
|
|
76
|
+
server = FastAPI(title="CodeAlmanac Local Viewer")
|
|
77
|
+
|
|
78
|
+
@server.get("/api/overview", response_model=ViewerOverview)
|
|
79
|
+
def overview() -> ViewerOverview:
|
|
80
|
+
try:
|
|
81
|
+
return codealmanac.viewer.overview(
|
|
82
|
+
ViewerOverviewRequest(cwd=cwd, wiki=wiki)
|
|
83
|
+
)
|
|
84
|
+
except ValidationError as error:
|
|
85
|
+
raise validation_error(error) from error
|
|
86
|
+
except CodeAlmanacError as error:
|
|
87
|
+
raise http_error(error) from error
|
|
88
|
+
|
|
89
|
+
@server.get("/api/page/{slug}", response_model=ViewerPage)
|
|
90
|
+
def page(slug: str) -> ViewerPage:
|
|
91
|
+
try:
|
|
92
|
+
return codealmanac.viewer.page(
|
|
93
|
+
ViewerPageRequest(cwd=cwd, wiki=wiki, slug=slug)
|
|
94
|
+
)
|
|
95
|
+
except ValidationError as error:
|
|
96
|
+
raise validation_error(error) from error
|
|
97
|
+
except CodeAlmanacError as error:
|
|
98
|
+
raise http_error(error) from error
|
|
99
|
+
|
|
100
|
+
@server.get("/api/search", response_model=ViewerSearch)
|
|
101
|
+
def search(q: str | None = None, limit: int = 50) -> ViewerSearch:
|
|
102
|
+
try:
|
|
103
|
+
return codealmanac.viewer.search(
|
|
104
|
+
ViewerSearchRequest(cwd=cwd, wiki=wiki, query=q, limit=limit)
|
|
105
|
+
)
|
|
106
|
+
except ValidationError as error:
|
|
107
|
+
raise validation_error(error) from error
|
|
108
|
+
except CodeAlmanacError as error:
|
|
109
|
+
raise http_error(error) from error
|
|
110
|
+
|
|
111
|
+
@server.get("/api/file", response_model=ViewerFile)
|
|
112
|
+
def file(path: str, limit: int = 50) -> ViewerFile:
|
|
113
|
+
try:
|
|
114
|
+
return codealmanac.viewer.file(
|
|
115
|
+
ViewerFileRequest(cwd=cwd, wiki=wiki, path=path, limit=limit)
|
|
116
|
+
)
|
|
117
|
+
except ValidationError as error:
|
|
118
|
+
raise validation_error(error) from error
|
|
119
|
+
except CodeAlmanacError as error:
|
|
120
|
+
raise http_error(error) from error
|
|
121
|
+
|
|
122
|
+
@server.get("/api/topic/{slug}", response_model=ViewerTopic)
|
|
123
|
+
def topic(slug: str, descendants: bool = False) -> ViewerTopic:
|
|
124
|
+
try:
|
|
125
|
+
return codealmanac.viewer.topic(
|
|
126
|
+
ViewerTopicRequest(
|
|
127
|
+
cwd=cwd,
|
|
128
|
+
wiki=wiki,
|
|
129
|
+
slug=slug,
|
|
130
|
+
include_descendants=descendants,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
except ValidationError as error:
|
|
134
|
+
raise validation_error(error) from error
|
|
135
|
+
except CodeAlmanacError as error:
|
|
136
|
+
raise http_error(error) from error
|
|
137
|
+
|
|
138
|
+
@server.get("/", include_in_schema=False)
|
|
139
|
+
def index() -> HTMLResponse:
|
|
140
|
+
return HTMLResponse(read_asset_text(ServerAssetRequest(path="index.html")))
|
|
141
|
+
|
|
142
|
+
@server.get("/app.js", include_in_schema=False)
|
|
143
|
+
def app_js() -> Response:
|
|
144
|
+
return asset_response("app.js")
|
|
145
|
+
|
|
146
|
+
@server.get("/app.css", include_in_schema=False)
|
|
147
|
+
def app_css() -> Response:
|
|
148
|
+
return asset_response("app.css")
|
|
149
|
+
|
|
150
|
+
@server.get("/assets/{asset_path:path}", include_in_schema=False)
|
|
151
|
+
def static_asset(asset_path: str) -> Response:
|
|
152
|
+
return asset_response(asset_path)
|
|
153
|
+
|
|
154
|
+
@server.get("/{path:path}", include_in_schema=False)
|
|
155
|
+
def fallback(path: str) -> HTMLResponse:
|
|
156
|
+
if path.startswith("api/"):
|
|
157
|
+
raise HTTPException(status_code=404, detail="not found")
|
|
158
|
+
return HTMLResponse(read_asset_text(ServerAssetRequest(path="index.html")))
|
|
159
|
+
|
|
160
|
+
return server
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def asset_response(asset_path: str) -> Response:
|
|
164
|
+
try:
|
|
165
|
+
request = ServerAssetRequest(path=asset_path)
|
|
166
|
+
return Response(
|
|
167
|
+
read_asset_text(request),
|
|
168
|
+
media_type=request.media_type,
|
|
169
|
+
)
|
|
170
|
+
except ValidationError as error:
|
|
171
|
+
raise validation_error(error) from error
|
|
172
|
+
except FileNotFoundError as error:
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=404,
|
|
175
|
+
detail={"code": "not_found", "message": f"asset not found: {asset_path}"},
|
|
176
|
+
) from error
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def read_asset_text(request: ServerAssetRequest) -> str:
|
|
180
|
+
asset = resources.files("codealmanac.server.assets").joinpath(*request.parts)
|
|
181
|
+
if not asset.is_file():
|
|
182
|
+
raise FileNotFoundError(request.path)
|
|
183
|
+
return asset.read_text(encoding="utf-8")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def http_error(error: CodeAlmanacError) -> HTTPException:
|
|
187
|
+
status_code = 400
|
|
188
|
+
if isinstance(error, NotFoundError):
|
|
189
|
+
status_code = 404
|
|
190
|
+
elif isinstance(error, ConflictError):
|
|
191
|
+
status_code = 409
|
|
192
|
+
return HTTPException(
|
|
193
|
+
status_code=status_code,
|
|
194
|
+
detail={"code": error.code, "message": str(error)},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validation_error(error: ValidationError) -> HTTPException:
|
|
199
|
+
return HTTPException(
|
|
200
|
+
status_code=422,
|
|
201
|
+
detail={"code": "validation_failed", "message": str(error)},
|
|
202
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Static assets bundled with the local viewer."""
|