exactdocs 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.
- exactdocs/__init__.py +5 -0
- exactdocs/__main__.py +3 -0
- exactdocs/_version.py +6 -0
- exactdocs/_web/assets/index-B350HwjP.js +10 -0
- exactdocs/_web/assets/index-CG8rwfz9.css +2 -0
- exactdocs/_web/assets/routes-CCGZxbYl.js +1 -0
- exactdocs/_web/index.html +13 -0
- exactdocs/bootstrap/__init__.py +5 -0
- exactdocs/bootstrap/bridges.py +27 -0
- exactdocs/bootstrap/cli.py +48 -0
- exactdocs/bootstrap/container.py +28 -0
- exactdocs/bootstrap/http.py +90 -0
- exactdocs/bootstrap/mcp.py +21 -0
- exactdocs/bootstrap/settings.py +15 -0
- exactdocs/catalog/__init__.py +9 -0
- exactdocs/catalog/adapters/__init__.py +0 -0
- exactdocs/catalog/adapters/driven/__init__.py +0 -0
- exactdocs/catalog/adapters/driven/memory.py +35 -0
- exactdocs/catalog/adapters/driving/__init__.py +0 -0
- exactdocs/catalog/adapters/driving/http.py +21 -0
- exactdocs/catalog/adapters/driving/mcp.py +18 -0
- exactdocs/catalog/dtos.py +42 -0
- exactdocs/catalog/errors.py +16 -0
- exactdocs/catalog/models.py +34 -0
- exactdocs/catalog/module.py +27 -0
- exactdocs/catalog/ports/__init__.py +4 -0
- exactdocs/catalog/ports/driven.py +18 -0
- exactdocs/catalog/ports/driving.py +21 -0
- exactdocs/catalog/use_cases.py +33 -0
- exactdocs/py.typed +0 -0
- exactdocs/retrieval/__init__.py +8 -0
- exactdocs/retrieval/adapters/__init__.py +0 -0
- exactdocs/retrieval/adapters/driven/__init__.py +0 -0
- exactdocs/retrieval/adapters/driven/memory.py +26 -0
- exactdocs/retrieval/adapters/driving/__init__.py +0 -0
- exactdocs/retrieval/adapters/driving/http.py +17 -0
- exactdocs/retrieval/adapters/driving/mcp.py +28 -0
- exactdocs/retrieval/dtos.py +53 -0
- exactdocs/retrieval/errors.py +17 -0
- exactdocs/retrieval/models.py +34 -0
- exactdocs/retrieval/module.py +18 -0
- exactdocs/retrieval/ports/__init__.py +4 -0
- exactdocs/retrieval/ports/driven.py +26 -0
- exactdocs/retrieval/ports/driving.py +13 -0
- exactdocs/retrieval/use_cases.py +22 -0
- exactdocs-0.1.0.dist-info/METADATA +35 -0
- exactdocs-0.1.0.dist-info/RECORD +49 -0
- exactdocs-0.1.0.dist-info/WHEEL +4 -0
- exactdocs-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.4 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:block))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab, var(--ring) 50%, transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.relative{position:relative}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.mx-auto{margin-inline:auto}.mt-4{margin-top:calc(var(--spacing) * 4)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-row{display:table-row}.h-10{height:calc(var(--spacing) * 10)}.min-h-dvh{min-height:100dvh}.w-fit{width:fit-content}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.auto-rows-min{grid-auto-rows:min-content}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-6{gap:calc(var(--spacing) * 6)}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:2147483647px}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-border{border-color:var(--border)}.border-transparent{border-color:#0000}.bg-card{background-color:var(--card)}.bg-destructive{background-color:var(--destructive)}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.p-2{padding:calc(var(--spacing) * 2)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-left{text-align:left}.align-middle{vertical-align:middle}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-card-foreground{color:var(--card-foreground)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.underline-offset-4{text-underline-offset:4px}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.has-aria-expanded\:bg-muted\/50:has([aria-expanded=true]){background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.has-aria-expanded\:bg-muted\/50:has([aria-expanded=true]){background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab, var(--destructive) 60%, transparent)}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing) * 6)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing) * 6)}.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\]>[role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x) var(--tw-translate-y)}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){a.\[a\&\]\:hover\:bg-accent:hover{background-color:var(--accent)}a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab, var(--destructive) 90%, transparent)}}a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--primary) 90%, transparent)}}a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab, var(--secondary) 90%, transparent)}}a.\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}a.\[a\&\]\:hover\:underline:hover{text-decoration-line:underline}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0)}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./index-B350HwjP.js";var t=e;export{t as component};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ExactDocs</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-B350HwjP.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CG8rwfz9.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Adapters that connect one domain's driven port to another domain's driving port.
|
|
2
|
+
|
|
3
|
+
This is how domains talk to each other without importing each other: the consumer declares a
|
|
4
|
+
port in its own language, and a bridge here translates to the provider's public use case.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from exactdocs.catalog.dtos import GetLibraryQuery
|
|
10
|
+
from exactdocs.catalog.ports.driving import GetLibrary
|
|
11
|
+
from exactdocs.retrieval.models import DocTarget
|
|
12
|
+
from exactdocs_kernel import NotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class CatalogIndexedVersions:
|
|
17
|
+
"""Implements ``retrieval.ports.driven.IndexedVersions`` using the catalog."""
|
|
18
|
+
|
|
19
|
+
get_library: GetLibrary
|
|
20
|
+
|
|
21
|
+
async def is_indexed(self, target: DocTarget) -> bool:
|
|
22
|
+
"""Ask the catalog whether this exact version has an index."""
|
|
23
|
+
try:
|
|
24
|
+
library = await self.get_library(GetLibraryQuery(name=target.library))
|
|
25
|
+
except NotFoundError:
|
|
26
|
+
return False
|
|
27
|
+
return target.version in library.versions
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""``exactdocs`` command line."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from exactdocs import __version__
|
|
10
|
+
from exactdocs.bootstrap.container import Container
|
|
11
|
+
from exactdocs.bootstrap.http import create_app
|
|
12
|
+
from exactdocs.bootstrap.mcp import build_mcp
|
|
13
|
+
from exactdocs.bootstrap.settings import Settings
|
|
14
|
+
|
|
15
|
+
app = App(name="exactdocs", version=__version__, help="Version-accurate docs for AI agents.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command
|
|
19
|
+
def serve(
|
|
20
|
+
*,
|
|
21
|
+
stdio: Annotated[bool, Parameter(help="Serve MCP over stdio instead of HTTP.")] = False,
|
|
22
|
+
host: str | None = None,
|
|
23
|
+
port: int | None = None,
|
|
24
|
+
reload: bool = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Run the server: web UI + REST API + MCP over HTTP, or MCP only over stdio."""
|
|
27
|
+
settings = Settings()
|
|
28
|
+
if stdio:
|
|
29
|
+
mcp = build_mcp(Container.build(settings))
|
|
30
|
+
asyncio.run(mcp.run_stdio_async(show_banner=False))
|
|
31
|
+
return
|
|
32
|
+
uvicorn.run(
|
|
33
|
+
"exactdocs.bootstrap.cli:asgi_app",
|
|
34
|
+
factory=True,
|
|
35
|
+
host=host or settings.host,
|
|
36
|
+
port=port or settings.port,
|
|
37
|
+
reload=reload,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def asgi_app() -> object:
|
|
42
|
+
"""ASGI factory used by uvicorn (also with ``--reload``)."""
|
|
43
|
+
return create_app(Container.build(Settings()))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
"""Console-script entry point."""
|
|
48
|
+
app()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from exactdocs.bootstrap.bridges import CatalogIndexedVersions
|
|
5
|
+
from exactdocs.bootstrap.settings import Settings
|
|
6
|
+
from exactdocs.catalog import CatalogModule
|
|
7
|
+
from exactdocs.catalog.adapters.driven.memory import InMemoryLibraryRepository
|
|
8
|
+
from exactdocs.retrieval import RetrievalModule
|
|
9
|
+
from exactdocs.retrieval.adapters.driven.memory import InMemoryExcerptIndex
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
13
|
+
class Container:
|
|
14
|
+
"""Every domain module, wired. Built once per process."""
|
|
15
|
+
|
|
16
|
+
settings: Settings
|
|
17
|
+
catalog: CatalogModule
|
|
18
|
+
retrieval: RetrievalModule
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def build(cls, settings: Settings) -> Self:
|
|
22
|
+
"""Choose driven adapters and connect domains through bridges."""
|
|
23
|
+
catalog = CatalogModule.build(repository=InMemoryLibraryRepository.demo())
|
|
24
|
+
retrieval = RetrievalModule.build(
|
|
25
|
+
versions=CatalogIndexedVersions(catalog.get_library),
|
|
26
|
+
index=InMemoryExcerptIndex(),
|
|
27
|
+
)
|
|
28
|
+
return cls(settings=settings, catalog=catalog, retrieval=retrieval)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, FastAPI, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from starlette.exceptions import HTTPException
|
|
10
|
+
from starlette.responses import Response
|
|
11
|
+
from starlette.routing import Route
|
|
12
|
+
from starlette.types import Scope
|
|
13
|
+
|
|
14
|
+
from exactdocs import __version__
|
|
15
|
+
from exactdocs.bootstrap.container import Container
|
|
16
|
+
from exactdocs.bootstrap.mcp import build_mcp
|
|
17
|
+
from exactdocs.catalog.adapters.driving import http as catalog_http
|
|
18
|
+
from exactdocs.retrieval.adapters.driving import http as retrieval_http
|
|
19
|
+
from exactdocs_kernel import ConflictError, DomainError, NotFoundError, ValidationError
|
|
20
|
+
|
|
21
|
+
# Built web UI, copied into the package by CI / the Docker build (see web/README.md).
|
|
22
|
+
WEB_DIST = Path(__file__).parent.parent / "_web"
|
|
23
|
+
|
|
24
|
+
_STATUS_BY_ERROR: dict[type[DomainError], int] = {
|
|
25
|
+
NotFoundError: 404,
|
|
26
|
+
ConflictError: 409,
|
|
27
|
+
ValidationError: 422,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Health(BaseModel):
|
|
32
|
+
"""Liveness payload."""
|
|
33
|
+
|
|
34
|
+
status: str
|
|
35
|
+
version: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_app(container: Container) -> FastAPI:
|
|
39
|
+
"""Build the single HTTP service: REST API, MCP (Streamable HTTP) and the web UI."""
|
|
40
|
+
mcp_app = build_mcp(container).http_app(path="/mcp")
|
|
41
|
+
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
|
|
44
|
+
async with mcp_app.lifespan(mcp_app):
|
|
45
|
+
yield
|
|
46
|
+
|
|
47
|
+
app = FastAPI(title="ExactDocs", version=__version__, lifespan=lifespan)
|
|
48
|
+
app.add_exception_handler(DomainError, _domain_error_handler)
|
|
49
|
+
|
|
50
|
+
api = APIRouter(prefix="/api")
|
|
51
|
+
|
|
52
|
+
@api.get("/health", tags=["system"])
|
|
53
|
+
async def health() -> Health:
|
|
54
|
+
return Health(status="ok", version=__version__)
|
|
55
|
+
|
|
56
|
+
v1 = APIRouter(prefix="/v1")
|
|
57
|
+
v1.include_router(
|
|
58
|
+
catalog_http.build_router(
|
|
59
|
+
list_libraries=container.catalog.list_libraries,
|
|
60
|
+
get_library=container.catalog.get_library,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
v1.include_router(retrieval_http.build_router(search_docs=container.retrieval.search_docs))
|
|
64
|
+
api.include_router(v1)
|
|
65
|
+
app.include_router(api)
|
|
66
|
+
|
|
67
|
+
# Exact route (no /mcp -> /mcp/ redirect); the MCP app keeps its own middleware.
|
|
68
|
+
app.router.routes.append(Route("/mcp", endpoint=mcp_app))
|
|
69
|
+
if WEB_DIST.is_dir():
|
|
70
|
+
app.mount("/", SpaStaticFiles(directory=WEB_DIST, html=True), name="web")
|
|
71
|
+
return app
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _domain_error_handler(_: Request, exc: Exception) -> Response:
|
|
75
|
+
status = next((s for t, s in _STATUS_BY_ERROR.items() if isinstance(exc, t)), 400)
|
|
76
|
+
code = exc.code if isinstance(exc, DomainError) else "error"
|
|
77
|
+
return JSONResponse({"error": code, "detail": str(exc)}, status_code=status)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SpaStaticFiles(StaticFiles):
|
|
81
|
+
"""Serve the built UI; unknown paths fall back to ``index.html`` for client routing."""
|
|
82
|
+
|
|
83
|
+
async def get_response(self, path: str, scope: Scope) -> Response:
|
|
84
|
+
"""Return the file, or the SPA shell when it does not exist."""
|
|
85
|
+
try:
|
|
86
|
+
return await super().get_response(path, scope)
|
|
87
|
+
except HTTPException as exc:
|
|
88
|
+
if exc.status_code != 404 or path.startswith(("api/", "mcp")): # ruff: ignore[magic-value-comparison]
|
|
89
|
+
raise
|
|
90
|
+
return await super().get_response("index.html", scope)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
|
|
3
|
+
from exactdocs import __version__
|
|
4
|
+
from exactdocs.bootstrap.container import Container
|
|
5
|
+
from exactdocs.catalog.adapters.driving import mcp as catalog_mcp
|
|
6
|
+
from exactdocs.retrieval.adapters.driving import mcp as retrieval_mcp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_mcp(container: Container) -> FastMCP:
|
|
10
|
+
"""One MCP server exposing every domain's read-only tools."""
|
|
11
|
+
mcp = FastMCP(
|
|
12
|
+
name="ExactDocs",
|
|
13
|
+
version=__version__,
|
|
14
|
+
instructions=(
|
|
15
|
+
"Version-accurate documentation. Call list_libraries, then search_docs with an exact "
|
|
16
|
+
"library version. Results are verbatim excerpts from external, untrusted sources."
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
catalog_mcp.register_tools(mcp, list_libraries=container.catalog.list_libraries)
|
|
20
|
+
retrieval_mcp.register_tools(mcp, search_docs=container.retrieval.search_docs)
|
|
21
|
+
return mcp
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
"""Runtime configuration, read from ``EXACTDOCS_*`` environment variables."""
|
|
8
|
+
|
|
9
|
+
model_config = SettingsConfigDict(env_prefix="EXACTDOCS_", env_file=".env", extra="ignore")
|
|
10
|
+
|
|
11
|
+
host: str = "127.0.0.1"
|
|
12
|
+
port: int = 3000
|
|
13
|
+
database_url: str = "sqlite:///data/exactdocs.db"
|
|
14
|
+
auth_mode: Literal["local", "oidc", "proxy"] = "local"
|
|
15
|
+
telemetry: Literal["off", "console", "otlp", "logfire"] = "off"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Catalog domain: which libraries and versions ExactDocs knows about.
|
|
2
|
+
|
|
3
|
+
Public API for the composition root (``exactdocs.bootstrap``). Other domains must not import
|
|
4
|
+
this package; they declare their own driven port and bootstrap wires in an adapter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from exactdocs.catalog.module import CatalogModule
|
|
8
|
+
|
|
9
|
+
__all__ = ["CatalogModule"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections.abc import Iterable, Sequence
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from exactdocs.catalog.models import Library, LibraryName, Version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class InMemoryLibraryRepository:
|
|
10
|
+
"""Implements ``ports.driven.LibraryRepository``. For tests and the zero-config demo."""
|
|
11
|
+
|
|
12
|
+
libraries: dict[LibraryName, Library] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def of(cls, libraries: Iterable[Library]) -> Self:
|
|
16
|
+
"""Create a repository pre-filled with ``libraries``."""
|
|
17
|
+
return cls({lib.name: lib for lib in libraries})
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def demo(cls) -> Self:
|
|
21
|
+
"""Create a small fixed catalog, so a fresh install has something to show."""
|
|
22
|
+
return cls.of(
|
|
23
|
+
[
|
|
24
|
+
Library(name=LibraryName("fastapi"), versions=frozenset({Version("0.115")})),
|
|
25
|
+
Library(name=LibraryName("react"), versions=frozenset({Version("19.1")})),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def list_all(self) -> Sequence[Library]:
|
|
30
|
+
"""Return every library."""
|
|
31
|
+
return tuple(self.libraries.values())
|
|
32
|
+
|
|
33
|
+
async def get(self, name: LibraryName) -> Library | None:
|
|
34
|
+
"""Return the library with this exact name, or ``None``."""
|
|
35
|
+
return self.libraries.get(name)
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""REST adapter for the catalog (consumed by the web UI)."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
|
|
6
|
+
from exactdocs.catalog.ports.driving import GetLibrary, ListLibraries
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_router(*, list_libraries: ListLibraries, get_library: GetLibrary) -> APIRouter:
|
|
10
|
+
"""Expose the catalog's driving ports under ``/libraries``."""
|
|
11
|
+
router = APIRouter(prefix="/libraries", tags=["catalog"])
|
|
12
|
+
|
|
13
|
+
@router.get("")
|
|
14
|
+
async def list_all() -> LibraryList:
|
|
15
|
+
return await list_libraries()
|
|
16
|
+
|
|
17
|
+
@router.get("/{name}")
|
|
18
|
+
async def get_one(name: str) -> LibrarySummary:
|
|
19
|
+
return await get_library(GetLibraryQuery(name=name))
|
|
20
|
+
|
|
21
|
+
return router
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""MCP adapter for the catalog. All tools are read-only."""
|
|
2
|
+
|
|
3
|
+
from fastmcp import FastMCP
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
6
|
+
from exactdocs.catalog.dtos import LibraryList
|
|
7
|
+
from exactdocs.catalog.ports.driving import ListLibraries
|
|
8
|
+
|
|
9
|
+
READ_ONLY = ToolAnnotations(read_only_hint=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_tools(mcp: FastMCP, *, list_libraries: ListLibraries) -> None:
|
|
13
|
+
"""Register the catalog's MCP tools on ``mcp``."""
|
|
14
|
+
|
|
15
|
+
@mcp.tool(name="list_libraries", annotations=READ_ONLY)
|
|
16
|
+
async def list_libraries_tool() -> LibraryList:
|
|
17
|
+
"""List indexed libraries and their versions."""
|
|
18
|
+
return await list_libraries()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Data crossing the catalog boundary (into and out of driving ports). Immutable, validated."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from exactdocs.catalog.models import Library
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Dto(BaseModel):
|
|
12
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LibrarySummary(_Dto):
|
|
16
|
+
"""A library and its indexed versions, sorted ascending."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
versions: list[str]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_model(cls, library: Library) -> Self:
|
|
23
|
+
"""Map a domain entity to its boundary representation."""
|
|
24
|
+
return cls(name=library.name, versions=[str(v) for v in sorted(library.versions)])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LibraryList(_Dto):
|
|
28
|
+
"""All libraries visible to the caller, sorted by name."""
|
|
29
|
+
|
|
30
|
+
libraries: list[LibrarySummary]
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_models(cls, libraries: Iterable[Library]) -> Self:
|
|
34
|
+
"""Map domain entities."""
|
|
35
|
+
ordered = sorted(libraries, key=lambda lib: lib.name)
|
|
36
|
+
return cls(libraries=[LibrarySummary.from_model(lib) for lib in ordered])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GetLibraryQuery(_Dto):
|
|
40
|
+
"""Look up one library by its exact name."""
|
|
41
|
+
|
|
42
|
+
name: str = Field(min_length=1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from exactdocs_kernel import NotFoundError, ValidationError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EmptyVersionError(ValidationError):
|
|
5
|
+
"""A version label is blank."""
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
super().__init__("version must not be empty")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LibraryNotFoundError(NotFoundError):
|
|
12
|
+
"""No library with this name is in the catalog."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str) -> None:
|
|
15
|
+
super().__init__(f"library {name!r} is not in the catalog")
|
|
16
|
+
self.name = name
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Catalog entities and value objects. Pure Python: no frameworks, no I/O."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import NewType
|
|
5
|
+
|
|
6
|
+
from exactdocs.catalog.errors import EmptyVersionError
|
|
7
|
+
|
|
8
|
+
LibraryName = NewType("LibraryName", str)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
12
|
+
class Version:
|
|
13
|
+
"""A documentation version label exactly as published by the library (e.g. ``19.1``)."""
|
|
14
|
+
|
|
15
|
+
value: str
|
|
16
|
+
|
|
17
|
+
def __post_init__(self) -> None:
|
|
18
|
+
if not self.value.strip():
|
|
19
|
+
raise EmptyVersionError
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return self.value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
26
|
+
class Library:
|
|
27
|
+
"""A documented library and the versions that have an index."""
|
|
28
|
+
|
|
29
|
+
name: LibraryName
|
|
30
|
+
versions: frozenset[Version] = field(default_factory=frozenset)
|
|
31
|
+
|
|
32
|
+
def has_version(self, version: Version) -> bool:
|
|
33
|
+
"""Version is a hard filter: only an exact match counts."""
|
|
34
|
+
return version in self.versions
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Wiring of the catalog domain: driven adapters in, driving ports out.
|
|
2
|
+
|
|
3
|
+
It does not pick adapters itself; the composition root (``exactdocs.bootstrap``) passes them in.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
from exactdocs.catalog.ports.driven import LibraryRepository
|
|
10
|
+
from exactdocs.catalog.ports.driving import GetLibrary, ListLibraries
|
|
11
|
+
from exactdocs.catalog.use_cases import GetLibraryUseCase, ListLibrariesUseCase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
15
|
+
class CatalogModule:
|
|
16
|
+
"""The catalog's driving ports, ready to be exposed by driving adapters."""
|
|
17
|
+
|
|
18
|
+
list_libraries: ListLibraries
|
|
19
|
+
get_library: GetLibrary
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def build(cls, *, repository: LibraryRepository) -> Self:
|
|
23
|
+
"""Assemble the use cases around the given driven adapters."""
|
|
24
|
+
return cls(
|
|
25
|
+
list_libraries=ListLibrariesUseCase(repository),
|
|
26
|
+
get_library=GetLibraryUseCase(repository),
|
|
27
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Driven (secondary) ports: what the catalog needs from infrastructure."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from exactdocs.catalog.models import Library, LibraryName
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LibraryRepository(Protocol):
|
|
10
|
+
"""Persistence for catalog entries."""
|
|
11
|
+
|
|
12
|
+
async def list_all(self) -> Sequence[Library]:
|
|
13
|
+
"""Return every library."""
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
async def get(self, name: LibraryName) -> Library | None:
|
|
17
|
+
"""Return the library with this exact name, or ``None``."""
|
|
18
|
+
...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Driving (primary) ports: what the outside world may ask the catalog to do."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ListLibraries(Protocol):
|
|
9
|
+
"""List every library and its indexed versions."""
|
|
10
|
+
|
|
11
|
+
async def __call__(self) -> LibraryList:
|
|
12
|
+
"""Run the use case."""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GetLibrary(Protocol):
|
|
17
|
+
"""Fetch one library; raises ``LibraryNotFoundError`` when it is missing."""
|
|
18
|
+
|
|
19
|
+
async def __call__(self, query: GetLibraryQuery) -> LibrarySummary:
|
|
20
|
+
"""Run the use case."""
|
|
21
|
+
...
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Catalog use cases. Each one satisfies a driving port structurally, without inheriting it."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from exactdocs.catalog.dtos import GetLibraryQuery, LibraryList, LibrarySummary
|
|
6
|
+
from exactdocs.catalog.errors import LibraryNotFoundError
|
|
7
|
+
from exactdocs.catalog.models import LibraryName
|
|
8
|
+
from exactdocs.catalog.ports.driven import LibraryRepository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ListLibrariesUseCase:
|
|
13
|
+
"""Implements ``ports.driving.ListLibraries``."""
|
|
14
|
+
|
|
15
|
+
repository: LibraryRepository
|
|
16
|
+
|
|
17
|
+
async def __call__(self) -> LibraryList:
|
|
18
|
+
"""List every library."""
|
|
19
|
+
return LibraryList.from_models(await self.repository.list_all())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class GetLibraryUseCase:
|
|
24
|
+
"""Implements ``ports.driving.GetLibrary``."""
|
|
25
|
+
|
|
26
|
+
repository: LibraryRepository
|
|
27
|
+
|
|
28
|
+
async def __call__(self, query: GetLibraryQuery) -> LibrarySummary:
|
|
29
|
+
"""Fetch one library by its exact name."""
|
|
30
|
+
library = await self.repository.get(LibraryName(query.name))
|
|
31
|
+
if library is None:
|
|
32
|
+
raise LibraryNotFoundError(query.name)
|
|
33
|
+
return LibrarySummary.from_model(library)
|
exactdocs/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from collections.abc import Iterable, Sequence
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from exactdocs.retrieval.models import DocTarget, Excerpt, SearchText
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class InMemoryExcerptIndex:
|
|
10
|
+
"""Implements ``ports.driven.ExcerptIndex`` with case-insensitive substring matching.
|
|
11
|
+
|
|
12
|
+
Placeholder until the SQLite (FTS5 + sqlite-vec) and pgvector adapters land.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
excerpts: list[Excerpt] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def of(cls, excerpts: Iterable[Excerpt]) -> Self:
|
|
19
|
+
"""Create an index pre-filled with ``excerpts``."""
|
|
20
|
+
return cls(list(excerpts))
|
|
21
|
+
|
|
22
|
+
async def search(self, target: DocTarget, text: SearchText, limit: int) -> Sequence[Excerpt]:
|
|
23
|
+
"""Return excerpts of ``target`` that contain ``text``."""
|
|
24
|
+
needle = text.value.casefold()
|
|
25
|
+
hits = (e for e in self.excerpts if e.target == target and needle in e.text.casefold())
|
|
26
|
+
return [hit for _, hit in zip(range(limit), hits, strict=False)]
|
|
File without changes
|