doorstop-vscode-server 0.1.0__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.
@@ -0,0 +1 @@
1
+ """FastAPI server wrapping the Doorstop Python API."""
@@ -0,0 +1,4 @@
1
+ from doorstop_server.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
doorstop_server/app.py ADDED
@@ -0,0 +1,22 @@
1
+ from fastapi import FastAPI
2
+
3
+ from doorstop_server.config import Settings
4
+ from doorstop_server.errors import register_exception_handlers
5
+ from doorstop_server.lock import SerializeRequestsMiddleware
6
+ from doorstop_server.routers import documents, health, items, review, tree
7
+
8
+
9
+ def create_app(settings: Settings) -> FastAPI:
10
+ app = FastAPI(title="doorstop-vscode-server")
11
+ app.state.settings = settings
12
+
13
+ app.add_middleware(SerializeRequestsMiddleware)
14
+ register_exception_handlers(app)
15
+
16
+ app.include_router(health.router)
17
+ app.include_router(documents.router)
18
+ app.include_router(items.router)
19
+ app.include_router(review.router)
20
+ app.include_router(tree.router)
21
+
22
+ return app
doorstop_server/cli.py ADDED
@@ -0,0 +1,27 @@
1
+ import argparse
2
+
3
+ import uvicorn
4
+
5
+ from doorstop_server.app import create_app
6
+ from doorstop_server.config import Settings
7
+
8
+
9
+ def parse_args(argv=None) -> Settings:
10
+ parser = argparse.ArgumentParser(prog="doorstop-vscode-server")
11
+ parser.add_argument("--project", required=True, help="path to the root of the Doorstop project")
12
+ parser.add_argument("--host", default="127.0.0.1")
13
+ parser.add_argument("--port", type=int, default=7867)
14
+ args = parser.parse_args(argv)
15
+ return Settings(project_root=args.project, host=args.host, port=args.port)
16
+
17
+
18
+ def main(argv=None) -> None:
19
+ settings = parse_args(argv)
20
+ app = create_app(settings)
21
+ # workers=1, no reload: exactly one process, one event loop -- required for
22
+ # the request-serialization guarantee in lock.py to hold.
23
+ uvicorn.run(app, host=settings.host, port=settings.port, workers=1, reload=False)
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class Settings:
6
+ project_root: str
7
+ host: str
8
+ port: int
@@ -0,0 +1,13 @@
1
+ import doorstop
2
+ from fastapi import Request
3
+
4
+ from doorstop_server.config import Settings
5
+ from doorstop_server.doorstop_tree import load_tree
6
+
7
+
8
+ def get_settings(request: Request) -> Settings:
9
+ return request.app.state.settings
10
+
11
+
12
+ def get_tree(request: Request) -> doorstop.Tree:
13
+ return load_tree(get_settings(request))
@@ -0,0 +1,20 @@
1
+ import doorstop
2
+
3
+ from doorstop_server.config import Settings
4
+
5
+
6
+ def load_tree(settings: Settings) -> doorstop.Tree:
7
+ """Build a fresh Tree from disk.
8
+
9
+ Rebuilt on every request rather than cached: the user can hand-edit item
10
+ files in VS Code between two server calls, and a cached tree would
11
+ silently miss or clobber those edits. ``request_next_number=None`` is the
12
+ ``--force`` equivalent the CLI uses to skip Doorstop's own numbering
13
+ server -- safe here because this server is the only process ever
14
+ mutating the project and every request is fully serialized.
15
+ """
16
+ return doorstop.build(
17
+ cwd=settings.project_root,
18
+ root=settings.project_root,
19
+ request_next_number=None,
20
+ )
@@ -0,0 +1,34 @@
1
+ from doorstop.common import DoorstopError
2
+ from fastapi import FastAPI, Request
3
+ from fastapi.responses import JSONResponse
4
+
5
+
6
+ class DoorstopApiError(Exception):
7
+ """A request-level error raised by a route handler (bad input, not found, ...)."""
8
+
9
+ def __init__(self, status_code: int, code: str, message: str):
10
+ super().__init__(message)
11
+ self.status_code = status_code
12
+ self.code = code
13
+ self.message = message
14
+
15
+
16
+ def _error_response(status_code: int, code: str, message: str) -> JSONResponse:
17
+ return JSONResponse(
18
+ status_code=status_code,
19
+ content={"error": {"code": code, "message": message}},
20
+ )
21
+
22
+
23
+ def register_exception_handlers(app: FastAPI) -> None:
24
+ @app.exception_handler(DoorstopApiError)
25
+ async def handle_api_error(_request: Request, exc: DoorstopApiError) -> JSONResponse:
26
+ return _error_response(exc.status_code, exc.code, exc.message)
27
+
28
+ @app.exception_handler(DoorstopError)
29
+ async def handle_doorstop_error(_request: Request, exc: DoorstopError) -> JSONResponse:
30
+ return _error_response(400, "DOORSTOP_ERROR", str(exc))
31
+
32
+ @app.exception_handler(Exception)
33
+ async def handle_unexpected_error(_request: Request, exc: Exception) -> JSONResponse:
34
+ return _error_response(500, "INTERNAL_ERROR", str(exc))
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+
3
+ from starlette.types import ASGIApp, Receive, Scope, Send
4
+
5
+ _UNLOCKED_PATHS = {"/health"}
6
+
7
+
8
+ class SerializeRequestsMiddleware:
9
+ """Ensures the app answers exactly one request at a time (except /health).
10
+
11
+ Implemented as a bare ASGI middleware -- not `@app.middleware("http")`
12
+ (Starlette's BaseHTTPMiddleware) -- because BaseHTTPMiddleware runs the
13
+ downstream app inside a separate anyio task/stream that our
14
+ @app.exception_handler() registrations cannot see through: an unexpected
15
+ exception raised in a route would bypass the structured {"error": ...}
16
+ response entirely and blow past this middleware as a raw exception. A plain
17
+ ASGI middleware just awaits the inner app directly, so by the time control
18
+ returns here any exception has already been turned into a normal response.
19
+
20
+ The lock is created per middleware instance (i.e. per app / per
21
+ create_app() call), not as a module-level global, so it's always bound to
22
+ whatever event loop first serves that app.
23
+ """
24
+
25
+ def __init__(self, app: ASGIApp) -> None:
26
+ self.app = app
27
+ self.lock = asyncio.Lock()
28
+
29
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
30
+ if scope["type"] != "http" or scope["path"] in _UNLOCKED_PATHS:
31
+ await self.app(scope, receive, send)
32
+ return
33
+ async with self.lock:
34
+ await self.app(scope, receive, send)
File without changes
@@ -0,0 +1,100 @@
1
+ from pathlib import Path
2
+
3
+ from doorstop.core import exporter, importer, publisher
4
+ from fastapi import APIRouter, Depends
5
+
6
+ from doorstop_server.deps import get_tree
7
+ from doorstop_server.errors import DoorstopApiError
8
+ from doorstop_server.schemas import (
9
+ AddItemRequest,
10
+ CreateDocumentRequest,
11
+ DocumentResponse,
12
+ ExportRequest,
13
+ ExportResponse,
14
+ ImportRequest,
15
+ ItemResponse,
16
+ PublishRequest,
17
+ PublishResponse,
18
+ ReorderIndexResponse,
19
+ ReorderRequest,
20
+ ReorderResponse,
21
+ )
22
+
23
+ router = APIRouter(prefix="/documents")
24
+
25
+ _EXPORT_EXTENSIONS = {"yaml": ".yml", "csv": ".csv", "tsv": ".tsv", "xlsx": ".xlsx"}
26
+ _PUBLISH_EXTENSIONS = {"markdown": ".md", "html": ".html", "latex": ".tex"}
27
+
28
+
29
+ @router.post("", response_model=DocumentResponse)
30
+ async def create_document(body: CreateDocumentRequest, tree=Depends(get_tree)) -> DocumentResponse:
31
+ document = tree.create_document(
32
+ body.path,
33
+ body.prefix,
34
+ sep=body.separator,
35
+ digits=body.digits,
36
+ parent=body.parentPrefix,
37
+ itemformat=body.itemFormat,
38
+ )
39
+ return DocumentResponse(prefix=str(document.prefix), path=document.path)
40
+
41
+
42
+ @router.post("/{prefix}/items", response_model=ItemResponse)
43
+ async def add_item(prefix: str, body: AddItemRequest, tree=Depends(get_tree)) -> ItemResponse:
44
+ document = tree.find_document(prefix)
45
+ item = document.add_item(level=body.level)
46
+ return ItemResponse(uid=str(item.uid), path=item.path, level=str(item.level))
47
+
48
+
49
+ @router.post("/{prefix}/reorder/index", response_model=ReorderIndexResponse)
50
+ async def ensure_reorder_index(prefix: str, tree=Depends(get_tree)) -> ReorderIndexResponse:
51
+ document = tree.find_document(prefix)
52
+ if not document.index:
53
+ document.index = True
54
+ return ReorderIndexResponse(prefix=prefix, indexPath=document.index)
55
+
56
+
57
+ @router.delete("/{prefix}/reorder/index", status_code=204)
58
+ async def discard_reorder_index(prefix: str, tree=Depends(get_tree)) -> None:
59
+ document = tree.find_document(prefix)
60
+ del document.index
61
+
62
+
63
+ @router.post("/{prefix}/reorder", response_model=ReorderResponse)
64
+ async def reorder_document(prefix: str, body: ReorderRequest, tree=Depends(get_tree)) -> ReorderResponse:
65
+ document = tree.find_document(prefix)
66
+ if body.mode == "auto":
67
+ document.reorder(manual=False)
68
+ else:
69
+ if not document.index:
70
+ raise DoorstopApiError(
71
+ 409,
72
+ "NO_REORDER_INDEX",
73
+ "No index file to reorder from -- call POST /documents/{prefix}/reorder/index first.",
74
+ )
75
+ document.reorder(manual=True, automatic=False)
76
+ return ReorderResponse(prefix=prefix, mode=body.mode)
77
+
78
+
79
+ @router.post("/{prefix}/import", response_model=DocumentResponse)
80
+ async def import_into_document(prefix: str, body: ImportRequest, tree=Depends(get_tree)) -> DocumentResponse:
81
+ document = tree.find_document(prefix)
82
+ ext = Path(body.sourcePath).suffix
83
+ importer.import_file(body.sourcePath, document, ext)
84
+ return DocumentResponse(prefix=str(document.prefix), path=document.path)
85
+
86
+
87
+ @router.post("/{prefix}/export", response_model=ExportResponse)
88
+ async def export_document(prefix: str, body: ExportRequest, tree=Depends(get_tree)) -> ExportResponse:
89
+ document = tree.find_document(prefix)
90
+ ext = _EXPORT_EXTENSIONS[body.format]
91
+ path = exporter.export(document, body.destinationPath, ext=ext)
92
+ return ExportResponse(path=path or body.destinationPath)
93
+
94
+
95
+ @router.post("/{prefix}/publish", response_model=PublishResponse)
96
+ async def publish_document(prefix: str, body: PublishRequest, tree=Depends(get_tree)) -> PublishResponse:
97
+ document = tree.find_document(prefix)
98
+ ext = _PUBLISH_EXTENSIONS[body.format]
99
+ path = publisher.publish(document, body.destinationPath, ext=ext)
100
+ return PublishResponse(path=path or body.destinationPath)
@@ -0,0 +1,11 @@
1
+ from fastapi import APIRouter, Request
2
+
3
+ from doorstop_server.schemas import HealthResponse
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/health", response_model=HealthResponse)
9
+ async def health(request: Request) -> HealthResponse:
10
+ settings = request.app.state.settings
11
+ return HealthResponse(status="ok", projectRoot=settings.project_root)
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter, Depends, Response
2
+
3
+ from doorstop_server.deps import get_tree
4
+ from doorstop_server.schemas import LinkRequest, LinkResponse
5
+
6
+ router = APIRouter(prefix="/items")
7
+
8
+
9
+ @router.post("/{child_uid}/links", response_model=LinkResponse)
10
+ async def link_item(child_uid: str, body: LinkRequest, tree=Depends(get_tree)) -> LinkResponse:
11
+ child, parent = tree.link_items(child_uid, body.parentUid)
12
+ return LinkResponse(child=str(child.uid), parent=str(parent.uid))
13
+
14
+
15
+ @router.delete("/{child_uid}/links/{parent_uid}", status_code=204)
16
+ async def unlink_item(child_uid: str, parent_uid: str, tree=Depends(get_tree)) -> Response:
17
+ tree.unlink_items(child_uid, parent_uid)
18
+ return Response(status_code=204)
@@ -0,0 +1,41 @@
1
+ from typing import Iterator, Optional
2
+
3
+ from doorstop.core.item import Item
4
+ from doorstop.core.tree import Tree
5
+ from fastapi import APIRouter, Depends
6
+
7
+ from doorstop_server.deps import get_tree
8
+ from doorstop_server.errors import DoorstopApiError
9
+ from doorstop_server.schemas import ReviewClearRequest
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ def _iter_items(tree: Tree, scope: str, target: Optional[str]) -> Iterator[Item]:
15
+ """Resolve {scope, target} to items, mirroring doorstop's own CLI disambiguation."""
16
+ if scope == "all":
17
+ for document in tree:
18
+ yield from document
19
+ return
20
+ if not target:
21
+ raise DoorstopApiError(422, "TARGET_REQUIRED", "target is required unless scope is 'all'.")
22
+ if scope == "document":
23
+ document = tree.find_document(target)
24
+ yield from document
25
+ return
26
+ yield tree.find_item(target)
27
+
28
+
29
+ @router.post("/review", status_code=204)
30
+ async def review_items(body: ReviewClearRequest, tree=Depends(get_tree)) -> None:
31
+ for item in _iter_items(tree, body.scope, body.target):
32
+ item.review()
33
+
34
+
35
+ @router.post("/clear", status_code=204)
36
+ async def clear_items(body: ReviewClearRequest, tree=Depends(get_tree)) -> None:
37
+ if body.parents:
38
+ for parent_uid in body.parents:
39
+ tree.find_item(parent_uid)
40
+ for item in _iter_items(tree, body.scope, body.target):
41
+ item.clear(parents=body.parents)
@@ -0,0 +1,53 @@
1
+ from doorstop.common import DoorstopError
2
+ from fastapi import APIRouter, Depends
3
+
4
+ from doorstop_server.deps import get_tree
5
+ from doorstop_server.schemas import DocumentNode, ItemNode, LinkInfo, TreeResponse
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ def _item_links(item, tree) -> list[LinkInfo]:
11
+ links = []
12
+ for uid in item.links:
13
+ try:
14
+ parent = tree.find_item(uid)
15
+ suspect = uid.stamp != parent.stamp()
16
+ except DoorstopError:
17
+ # Dangling/renamed parent UID - treat as suspect rather than failing the request.
18
+ suspect = True
19
+ links.append(LinkInfo(uid=str(uid), suspect=suspect))
20
+ return links
21
+
22
+
23
+ @router.get("/tree", response_model=TreeResponse)
24
+ async def get_tree_structure(tree=Depends(get_tree)) -> TreeResponse:
25
+ tree.load()
26
+ documents = [
27
+ DocumentNode(
28
+ prefix=str(document.prefix),
29
+ markerPath=document.config,
30
+ parentPrefix=document.parent or None,
31
+ digits=document.digits,
32
+ separator=document.sep,
33
+ itemFormat=str(document.itemformat) if document.itemformat else None,
34
+ items=[
35
+ ItemNode(
36
+ uid=str(item.uid),
37
+ path=item.path,
38
+ level=str(item.level),
39
+ header=str(item.header) if item.header else None,
40
+ text=str(item.text) if item.text else None,
41
+ active=item.active,
42
+ normative=item.normative,
43
+ derived=item.derived,
44
+ reviewed=item.reviewed,
45
+ cleared=item.cleared,
46
+ links=_item_links(item, tree),
47
+ )
48
+ for item in document
49
+ ],
50
+ )
51
+ for document in tree
52
+ ]
53
+ return TreeResponse(documents=documents)
@@ -0,0 +1,116 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class HealthResponse(BaseModel):
7
+ status: Literal["ok"]
8
+ projectRoot: str
9
+
10
+
11
+ class CreateDocumentRequest(BaseModel):
12
+ prefix: str
13
+ path: str
14
+ parentPrefix: Optional[str] = None
15
+ itemFormat: Optional[Literal["yaml", "markdown"]] = None
16
+ digits: Optional[int] = None
17
+ separator: Optional[str] = None
18
+
19
+
20
+ class DocumentResponse(BaseModel):
21
+ prefix: str
22
+ path: str
23
+
24
+
25
+ class AddItemRequest(BaseModel):
26
+ level: Optional[str] = None
27
+
28
+
29
+ class ItemResponse(BaseModel):
30
+ uid: str
31
+ path: str
32
+ level: str
33
+
34
+
35
+ class ReviewClearRequest(BaseModel):
36
+ scope: Literal["item", "document", "all"]
37
+ target: Optional[str] = None
38
+ parents: Optional[List[str]] = None
39
+
40
+
41
+ class LinkRequest(BaseModel):
42
+ parentUid: str
43
+
44
+
45
+ class LinkResponse(BaseModel):
46
+ child: str
47
+ parent: str
48
+
49
+
50
+ class ReorderRequest(BaseModel):
51
+ mode: Literal["auto", "manual"]
52
+
53
+
54
+ class ReorderResponse(BaseModel):
55
+ prefix: str
56
+ mode: Literal["auto", "manual"]
57
+
58
+
59
+ class ReorderIndexResponse(BaseModel):
60
+ prefix: str
61
+ indexPath: str
62
+
63
+
64
+ class ImportRequest(BaseModel):
65
+ sourcePath: str
66
+
67
+
68
+ class ExportRequest(BaseModel):
69
+ format: Literal["yaml", "csv", "tsv", "xlsx"]
70
+ destinationPath: str
71
+
72
+
73
+ class ExportResponse(BaseModel):
74
+ path: str
75
+
76
+
77
+ class PublishRequest(BaseModel):
78
+ format: Literal["markdown", "html", "latex"]
79
+ destinationPath: str
80
+
81
+
82
+ class PublishResponse(BaseModel):
83
+ path: str
84
+
85
+
86
+ class LinkInfo(BaseModel):
87
+ uid: str
88
+ suspect: bool
89
+
90
+
91
+ class ItemNode(BaseModel):
92
+ uid: str
93
+ path: str
94
+ level: str
95
+ header: Optional[str] = None
96
+ text: Optional[str] = None
97
+ active: bool
98
+ normative: bool
99
+ derived: bool
100
+ reviewed: bool
101
+ cleared: bool
102
+ links: List[LinkInfo]
103
+
104
+
105
+ class DocumentNode(BaseModel):
106
+ prefix: str
107
+ markerPath: str
108
+ parentPrefix: Optional[str] = None
109
+ digits: Optional[int] = None
110
+ separator: Optional[str] = None
111
+ itemFormat: Optional[str] = None
112
+ items: List[ItemNode]
113
+
114
+
115
+ class TreeResponse(BaseModel):
116
+ documents: List[DocumentNode]
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: doorstop-vscode-server
3
+ Version: 0.1.0
4
+ Summary: FastAPI server wrapping the Doorstop Python API for the Doorstop VS Code extension
5
+ Author: Simon Walbrun
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/simonwalbrun-lab/doorstop-vscode-server
8
+ Project-URL: Issues, https://github.com/simonwalbrun-lab/doorstop-vscode-server/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: doorstop>=3.0
15
+ Requires-Dist: fastapi>=0.115
16
+ Requires-Dist: uvicorn[standard]>=0.30
17
+ Requires-Dist: pydantic>=2
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: httpx>=0.27; extra == "dev"
21
+ Provides-Extra: build
22
+ Requires-Dist: build; extra == "build"
23
+ Requires-Dist: twine; extra == "build"
24
+ Dynamic: license-file
25
+
26
+ # doorstop-vscode-server
27
+
28
+ A small FastAPI server that wraps the [Doorstop](https://pypi.org/project/doorstop/) Python API directly (instead of shelling out to the `doorstop` CLI). It is designed to serve exactly one client — the Doorstop VS Code extension — and answers requests strictly one at a time; there is no concurrent request handling.
29
+
30
+ ## Install (editable, for development)
31
+
32
+ ```bash
33
+ pip install -e .[dev]
34
+ ```
35
+
36
+ ## Run
37
+
38
+ ```bash
39
+ python -m doorstop_server --project <path-to-doorstop-project-root> --host 127.0.0.1 --port 7867
40
+ ```
41
+
42
+ or, after installing:
43
+
44
+ ```bash
45
+ doorstop-vscode-server --project <path-to-doorstop-project-root> --host 127.0.0.1 --port 7867
46
+ ```
47
+
48
+ ## API
49
+
50
+ See `src/doorstop_server/routers/` for the endpoint implementations. `GET /health` is the only route that is not serialized behind the request lock.
51
+
52
+ ## Tests
53
+
54
+ The test suite exercises the real FastAPI app against a temporary Doorstop project (no mocking of Doorstop itself), so it doubles as a pinned-down contract for the HTTP interface — every request/response shape, status code, and error format a test asserts on is something the extension can rely on.
55
+
56
+ ```bash
57
+ pip install -e .[dev]
58
+ pytest
59
+ ```
60
+
61
+ - `tests/conftest.py` — shared fixtures (`client`, `project_root`, `document`) and a `set_item_text` helper for mutating an item directly via the Doorstop API (bypassing the server) to simulate out-of-band edits.
62
+ - `tests/test_serialization.py` — proves the one-request-at-a-time guarantee black-box: fires concurrent `/items` calls and checks for zero UID collisions/gaps, rather than inspecting the lock directly.
63
+ - `tests/test_errors.py` — pins down the structured `{"error": {"code", "message"}}` shape for both expected (`DoorstopError`) and unexpected exceptions.
@@ -0,0 +1,22 @@
1
+ doorstop_server/__init__.py,sha256=o9e0w1q7dfd8GEy4hFQ2r9V8BbsLm697L9_4XESdJiY,55
2
+ doorstop_server/__main__.py,sha256=6SadmjAXtqdWnebwvhTypQtN8MZQlovqu-a8EkEUSZU,76
3
+ doorstop_server/app.py,sha256=CvRgKD7ODgLUrOoo4_QEcFb9sPoyq0kU0pbZYXIdrsc,702
4
+ doorstop_server/cli.py,sha256=SXztnfvTpqSqNa6AmV3VIuDVRyR6EQyNTDalAGB7WV8,919
5
+ doorstop_server/config.py,sha256=G2hewSdb4x6u19cEZaQTkS7wMRzvyUK_fC-fqO0DFSE,126
6
+ doorstop_server/deps.py,sha256=MVU6yky26g5gjg-pYSOKyOLwOyQ1JJ9Hdxsx347LCjk,324
7
+ doorstop_server/doorstop_tree.py,sha256=byLaQ0a4qsRhg74NHVxNNDU2sSwDOYgSgb36kT3EAkw,731
8
+ doorstop_server/errors.py,sha256=FYkBEQM_GhBHZqCnLfA9Ur_0PKbj0BQFFehuAM88q5Y,1307
9
+ doorstop_server/lock.py,sha256=n4qHhwxVFNSDfK8U-VIpjWb7SgEPtAdOkgcwidN69OY,1436
10
+ doorstop_server/schemas.py,sha256=ILWiYhbjRWeGYMOP1wHnNn9gN1RwfGhxpmmebRZxfD8,2106
11
+ doorstop_server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ doorstop_server/routers/documents.py,sha256=01-hi2RNKHTzHfkyzrFlnog_71CKSLjES_o-orfFqqE,3840
13
+ doorstop_server/routers/health.py,sha256=ie80xHT6AWYZ3guy19BD12gnJgjYl00uQ5GmxKyEZq4,339
14
+ doorstop_server/routers/items.py,sha256=mUEkomsNi2XjWCCFw2DJUVtZE49pP4aX0WslYGsFu-g,729
15
+ doorstop_server/routers/review.py,sha256=L8WKuYAI-2ZPcwnKD-agitY9a7iGoZ6Z928A5XJApds,1395
16
+ doorstop_server/routers/tree.py,sha256=WhFSClzLlgDQ56EZpAodmO4nQ9LER3k6zklLg8T5xvg,1840
17
+ doorstop_vscode_server-0.1.0.dist-info/licenses/LICENSE,sha256=6BiafIi_4eCDkLL5CrmU4yWbYImREooyzPvJEgbZJbQ,1093
18
+ doorstop_vscode_server-0.1.0.dist-info/METADATA,sha256=mC5wbqx0xJm0f0pwxfX3oBe7HI32goS-rJxQzHs4phI,2728
19
+ doorstop_vscode_server-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ doorstop_vscode_server-0.1.0.dist-info/entry_points.txt,sha256=5Q21gr9I-psi6KvV3nkF7UlPwJe-XBY1OVbqj9xhcvY,68
21
+ doorstop_vscode_server-0.1.0.dist-info/top_level.txt,sha256=f67Y3oDxOwc06FFlD_pfzqHQZEK36x1DKM2VzhKIGAk,16
22
+ doorstop_vscode_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ doorstop-vscode-server = doorstop_server.cli:main
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 Simon Walbrun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ doorstop_server