feat(standards): codify codegen, ship generic Tier-B infra, add get_app_template + prompts; fix docs:// regressions

P0: fix doc:// -> docs:// across standards src/server/registry and regenerate stale mongo-ops bundle. P1: build recipe + spec-first routing now document the openapi-first codegen fast path (models/routes with --inject-request/--package). P4: CRUD template documents the model-vs-document id rule (BaseDocument owns id). P5: authless guidance (omit security keys). P6: template gains generic infra (http_cache, keyed SSE bus, KV port+in-memory+Redis), interfaces/openapi.py + .dockerignore + generic build test registered as components, and a get_app_template tool; genericity matrix marks LLM pool + human-in-loop as reference-app patterns. P7: intake/build/start_app/trial prompts. P8: template ships tests/integration/test_openapi_app_builds.py. Rebuilt standards + openapi-first bundles and re-collected.
This commit is contained in:
2026-10-13 03:26:51 +05:30
parent ef951a1913
commit fced40af59
109 changed files with 4082 additions and 5885 deletions

View File

@@ -879,7 +879,7 @@
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="w"> </span><span class="nt">get</span><span class="p">:</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a><span class="w"> </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">get_health</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a><span class="w"> </span><span class="nt">responses</span><span class="p">:</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a><span class="w"> </span><span class="s">"200"</span><span class="p p-Indicator">:</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a><span class="w"> </span><span class="s">&quot;200&quot;</span><span class="p p-Indicator">:</span>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">OK</span>
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8"></a><span class="w"> </span><span class="nt">content</span><span class="p">:</span>
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9"></a><span class="w"> </span><span class="nt">application/json</span><span class="p">:</span>
@@ -894,8 +894,8 @@
<span class="normal"><a href="#__codelineno-1-3">3</a></span>
<span class="normal"><a href="#__codelineno-1-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="c1"># routes.py</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_health</span><span class="p">():</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a><span class="w"> </span><span class="sd">"""Health check operation handler."""</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a> <span class="k">return</span> <span class="p">{</span><span class="s2">"status"</span><span class="p">:</span> <span class="s2">"ok"</span><span class="p">}</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Health check operation handler.&quot;&quot;&quot;</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a> <span class="k">return</span> <span class="p">{</span><span class="s2">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;ok&quot;</span><span class="p">}</span>
</span></code></pre></div></td></tr></table></div>
<p><code>openapi_first</code> resolves <code>operationId: get_health</code> → <code>routes.get_health</code> and registers the route. <strong>No decorators, no <code>@app.get</code>, no routing metadata in code.</strong></p>
<p>Guarantees this binding provides:</p>
@@ -959,9 +959,9 @@
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3"></a><span class="kn">import</span><span class="w"> </span><span class="nn">routes</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4"></a>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5"></a><span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">"openapi.yaml"</span><span class="p">,</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">,</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7"></a> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">,</span>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8"></a> <span class="n">title</span><span class="o">=</span><span class="s2">"My Service"</span><span class="p">,</span>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8"></a> <span class="n">title</span><span class="o">=</span><span class="s2">&quot;My Service&quot;</span><span class="p">,</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>Run with your FastAPI-compatible server (ASGI):</p>
@@ -982,12 +982,12 @@
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.client</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIClient</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4"></a>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7"></a>
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_health</span><span class="p">()</span> <span class="c1"># operationId-driven call</span>
</span><span id="__span-5-9"><a id="__codelineno-5-9" name="__codelineno-5-9"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span> <span class="c1"># 200</span>
</span><span id="__span-5-10"><a id="__codelineno-5-10" name="__codelineno-5-10"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="c1"># {"status": "ok"}</span>
</span><span id="__span-5-10"><a id="__codelineno-5-10" name="__codelineno-5-10"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="c1"># {&quot;status&quot;: &quot;ok&quot;}</span>
</span></code></pre></div></td></tr></table></div>
<p><code>OpenAPIClient</code> builds one callable per <code>operationId</code> from the <em>same</em> spec, so the client can never drift from the server.</p>
<hr />

View File

@@ -934,7 +934,7 @@
<span class="normal"><a href="#__codelineno-0-2">2</a></span>
<span class="normal"><a href="#__codelineno-0-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>Behavior:</p>
<ul>
@@ -958,9 +958,9 @@
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="kn">import</span><span class="w"> </span><span class="nn">routes</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a><span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">"openapi.yaml"</span><span class="p">,</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">,</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6"></a> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">,</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7"></a> <span class="n">title</span><span class="o">=</span><span class="s2">"My Service"</span><span class="p">,</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7"></a> <span class="n">title</span><span class="o">=</span><span class="s2">&quot;My Service&quot;</span><span class="p">,</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p><strong>Startup pipeline (fail-fast, in this order):</strong></p>
@@ -1027,12 +1027,12 @@
<span class="normal"><a href="#__codelineno-2-9">9</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.client</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIClient</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_health</span><span class="p">()</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"user_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">create_user</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"Ada"</span><span class="p">})</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;user_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">create_user</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Ada&quot;</span><span class="p">})</span>
</span></code></pre></div></td></tr></table></div>
<p>How operations become methods:</p>
<ul>

View File

@@ -858,7 +858,7 @@
</span><span id="__span-0-15"><a id="__codelineno-0-15" name="__codelineno-0-15"></a><span class="w"> </span><span class="nt">schema</span><span class="p">:</span>
</span><span id="__span-0-16"><a id="__codelineno-0-16" name="__codelineno-0-16"></a><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">string</span>
</span><span id="__span-0-17"><a id="__codelineno-0-17" name="__codelineno-0-17"></a><span class="w"> </span><span class="nt">responses</span><span class="p">:</span>
</span><span id="__span-0-18"><a id="__codelineno-0-18" name="__codelineno-0-18"></a><span class="w"> </span><span class="s">"200"</span><span class="p p-Indicator">:</span>
</span><span id="__span-0-18"><a id="__codelineno-0-18" name="__codelineno-0-18"></a><span class="w"> </span><span class="s">&quot;200&quot;</span><span class="p p-Indicator">:</span>
</span><span id="__span-0-19"><a id="__codelineno-0-19" name="__codelineno-0-19"></a><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">A greeting</span>
</span><span id="__span-0-20"><a id="__codelineno-0-20" name="__codelineno-0-20"></a><span class="w"> </span><span class="nt">content</span><span class="p">:</span>
</span><span id="__span-0-21"><a id="__codelineno-0-21" name="__codelineno-0-21"></a><span class="w"> </span><span class="nt">application/json</span><span class="p">:</span>
@@ -877,8 +877,8 @@
<span class="normal"><a href="#__codelineno-1-3">3</a></span>
<span class="normal"><a href="#__codelineno-1-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="c1"># routes.py</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_greeting</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a><span class="w"> </span><span class="sd">"""Return a greeting for the given name."""</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a> <span class="k">return</span> <span class="p">{</span><span class="s2">"greeting"</span><span class="p">:</span> <span class="sa">f</span><span class="s2">"Hello, </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">!"</span><span class="p">}</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Return a greeting for the given name.&quot;&quot;&quot;</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a> <span class="k">return</span> <span class="p">{</span><span class="s2">&quot;greeting&quot;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&quot;Hello, </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">!&quot;</span><span class="p">}</span>
</span></code></pre></div></td></tr></table></div>
<p>If a handler is missing at startup, the app <strong>refuses to boot</strong> (<code>MissingOperationHandler</code>) — the fail-fast guarantee catches contract drift immediately.</p>
<hr />
@@ -897,9 +897,9 @@
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a><span class="kn">import</span><span class="w"> </span><span class="nn">routes</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a><span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">"openapi.yaml"</span><span class="p">,</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">,</span>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">,</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a> <span class="n">title</span><span class="o">=</span><span class="s2">"Greeting Service"</span><span class="p">,</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a> <span class="n">title</span><span class="o">=</span><span class="s2">&quot;Greeting Service&quot;</span><span class="p">,</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>Run it:</p>
@@ -922,12 +922,12 @@
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.client</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIClient</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7"></a>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_greeting</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"Ada"</span><span class="p">})</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_greeting</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Ada&quot;</span><span class="p">})</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">)</span> <span class="c1"># 200</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="c1"># {"greeting": "Hello, Ada!"}</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="c1"># {&quot;greeting&quot;: &quot;Hello, Ada!&quot;}</span>
</span></code></pre></div></td></tr></table></div>
<hr />
<h2 id="6-next-steps">💡 6. Next Steps</h2>

View File

@@ -899,7 +899,7 @@
<span class="normal"><a href="#__codelineno-2-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a>pip<span class="w"> </span>install<span class="w"> </span>-e<span class="w"> </span>.
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>uvicorn<span class="w"> </span>main:app
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>curl<span class="w"> </span>http://localhost:8000/health
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="c1"># → {"status": "ok"}</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a><span class="c1"># → {&quot;status&quot;: &quot;ok&quot;}</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="22-crud_app-dict-based-crud">2.2 <code>crud_app</code> — dict-based CRUD</h3>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a>openapi-first<span class="w"> </span>scaffold<span class="w"> </span>crud_app<span class="w"> </span>my-crud-service

View File

@@ -832,11 +832,11 @@
<span class="normal"><a href="#__codelineno-0-5">5</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">load_openapi</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.client</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIClient</span>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>The base URL comes from the spec's <code>servers</code> list (first entry) unless you pass <code>base_url</code> explicitly:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">,</span> <span class="n">base_url</span><span class="o">=</span><span class="s2">"https://api.internal.myco/v1"</span><span class="p">)</span>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span><span class="n">spec</span><span class="p">,</span> <span class="n">base_url</span><span class="o">=</span><span class="s2">&quot;https://api.internal.myco/v1&quot;</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>You can also hand over a preconfigured <code>httpx.Client</code> (custom transport, TLS, retries):</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
@@ -930,18 +930,18 @@
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a><span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a><span class="c1"># Path parameter</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"item_id"</span><span class="p">:</span> <span class="mi">3</span><span class="p">})</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;item_id&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">})</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7"></a>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8"></a><span class="c1"># Query parameters</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">list_items</span><span class="p">(</span><span class="n">query</span><span class="o">=</span><span class="p">{</span><span class="s2">"limit"</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="s2">"offset"</span><span class="p">:</span> <span class="mi">20</span><span class="p">})</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">list_items</span><span class="p">(</span><span class="n">query</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;limit&quot;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="s2">&quot;offset&quot;</span><span class="p">:</span> <span class="mi">20</span><span class="p">})</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11"></a><span class="c1"># JSON request body</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">create_item</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"Orange"</span><span class="p">,</span> <span class="s2">"price"</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">})</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">create_item</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Orange&quot;</span><span class="p">,</span> <span class="s2">&quot;price&quot;</span><span class="p">:</span> <span class="mf">0.8</span><span class="p">})</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13"></a>
</span><span id="__span-4-14"><a id="__codelineno-4-14" name="__codelineno-4-14"></a><span class="c1"># Custom headers / timeout</span>
</span><span id="__span-4-15"><a id="__codelineno-4-15" name="__codelineno-4-15"></a><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16"></a> <span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"user_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17"></a> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">"X-Internal-Key"</span><span class="p">:</span> <span class="s2">"..."</span><span class="p">},</span>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16"></a> <span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;user_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17"></a> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;X-Internal-Key&quot;</span><span class="p">:</span> <span class="s2">&quot;...&quot;</span><span class="p">},</span>
</span><span id="__span-4-18"><a id="__codelineno-4-18" name="__codelineno-4-18"></a> <span class="n">timeout</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>
</span><span id="__span-4-19"><a id="__codelineno-4-19" name="__codelineno-4-19"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
@@ -982,7 +982,7 @@
<span class="normal"><a href="#__codelineno-5-2">2</a></span>
<span class="normal"><a href="#__codelineno-5-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1"></a><span class="c1"># One directory, two processes</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2"></a>uvicorn<span class="w"> </span>main:app<span class="w"> </span>--port<span class="w"> </span><span class="m">8000</span><span class="w"> </span><span class="c1"># server</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a>python<span class="w"> </span>-c<span class="w"> </span><span class="s2">"import asyncio; from client_script import run; asyncio.run(run())"</span><span class="w"> </span><span class="c1"># client</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a>python<span class="w"> </span>-c<span class="w"> </span><span class="s2">&quot;import asyncio; from client_script import run; asyncio.run(run())&quot;</span><span class="w"> </span><span class="c1"># client</span>
</span></code></pre></div></td></tr></table></div>
<p>Or the same client against a remote environment:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-6-1">1</a></span>
@@ -990,7 +990,7 @@
<span class="normal"><a href="#__codelineno-6-3">3</a></span>
<span class="normal"><a href="#__codelineno-6-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1"></a><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAPIClient</span><span class="p">(</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2"></a> <span class="n">spec</span><span class="p">,</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3"></a> <span class="n">base_url</span><span class="o">=</span><span class="s2">"https://staging.internal.myco"</span><span class="p">,</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3"></a> <span class="n">base_url</span><span class="o">=</span><span class="s2">&quot;https://staging.internal.myco&quot;</span><span class="p">,</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<p>The URL is the <em>only</em> thing that changes between environments — the contract never does.</p>

View File

@@ -655,12 +655,27 @@
</li>
<li class="md-nav__item">
<a href="#3-route-generation-verification" class="md-nav__link">
<a href="#3-route-generation" class="md-nav__link">
<span class="md-ellipsis">
🔁 3. Route Generation / Verification
🔁 3. Route Generation
</span>
</a>
<nav class="md-nav" aria-label="🔁 3. Route Generation">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#resource-names-and-extra-media-types" class="md-nav__link">
<span class="md-ellipsis">
Resource names and extra media types
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -803,21 +818,21 @@
<h1 id="codegen-generate-models-routes-from-your-spec">Codegen — Generate Models &amp; Routes From Your Spec</h1>
<blockquote>
<p><strong>TL;DR</strong> — point <code>openapi-first codegen</code> at your spec and get <code>models</code> + <code>routes</code> scaffolds you can bind with one line. The codegen output is deterministic, and it's a <em>starting point</em> — not a maintained artifact.</p>
<p><strong>TL;DR</strong> — point <code>openapi-first models</code> / <code>openapi-first routes</code> at your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a <em>starting point</em> — not a maintained artifact.</p>
</blockquote>
<hr />
<h2 id="1-why-celebrate-codegen">🧭 1. Why Celebrate Codegen?</h2>
<p>Two pain points kill OpenAPI projects:</p>
<ol>
<li><strong>The "write the spec, then write the same thing as Pydantic models" step</strong> — exactly where server/client param types drift (your route says <code>item_id: int</code>, your client sends a string…).</li>
<li><strong>Writing the docs/spec catalog by hand</strong>, once you have more than a handful of operations.</li>
<li><strong>Typing handler names by hand</strong> when <code>bind_routes</code> requires each <code>operationId</code> to resolve to a function of the exact same name.</li>
</ol>
<p>Codegen collapses both. One command, one source file, same generated shapes everywhere.</p>
<p>Codegen collapses both. One command, one source file, the same shapes everywhere.</p>
<hr />
<h2 id="2-model-generation">🚀 2. Model Generation</h2>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>openapi-first<span class="w"> </span>codegen<span class="w"> </span>models<span class="w"> </span>--module<span class="w"> </span>my_project.models<span class="w"> </span>--input<span class="w"> </span>openapi.yaml
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>openapi-first<span class="w"> </span>models<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>app/_gen/models.py
</span></code></pre></div></td></tr></table></div>
<p>The generated model module mirrors the spec's <code>components.schemas</code> <strong>by name</strong>:</p>
<p>The generated module mirrors the spec's <code>components.schemas</code> <strong>by name</strong>:</p>
<table>
<thead>
<tr>
@@ -835,33 +850,61 @@
<td><code>class Item(BaseModel)</code></td>
</tr>
<tr>
<td>every <code>$ref</code> (schema)</td>
<td>a <code>type: ClassVar</code> alias ✓</td>
</tr>
<tr>
<td><code>required + type</code> from schema</td>
<td>Pydantic <code>Field(...)</code> / type hints ✓</td>
</tr>
</tbody>
</table>
<p><strong>Never hand-edit those files.</strong> If the spec changes, regenerate — just like you'd re-run <code>cargo build</code> after editing <code>Cargo.toml</code>.</p>
<p><strong>Never hand-edit those files.</strong> If the spec changes, regenerate — just like re-running a compiler after editing source.</p>
<h3 id="why-constructor-time-validation-still-applies">Why constructor-time validation still applies</h3>
<p>Codegen doesn't change the design: the generated models are plain Pydantic, and the <strong>client/server still validate against the spec</strong> at startup. Codegen is a <em>convenience accelerator</em> on top of the fail-fast guarantees in <a href="../../04_design/">04 – Design</a> and <a href="../../06_error_handling/">06 – Error Handling</a>.</p>
<blockquote>
<p><strong>Wire model vs. domain model.</strong> Generated models are the <strong>wire contract</strong>. Keep them separate from your persisted/domain model (see the standards <em>CRUD Template</em>): give the wire schema an <code>id: str</code>, but model the Mongo document so the id is optional.</p>
</blockquote>
<hr />
<h2 id="3-route-generation-verification">🔁 3. Route Generation / Verification</h2>
<h2 id="3-route-generation">🔁 3. Route Generation</h2>
<div class="language-bash highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span>
<span class="normal"><a href="#__codelineno-1-3">3</a></span>
<span class="normal"><a href="#__codelineno-1-4">4</a></span>
<span class="normal"><a href="#__codelineno-1-5">5</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a><span class="c1"># dry-run verification against your routes module</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a>openapi-first<span class="w"> </span>codegen<span class="w"> </span>routes<span class="w"> </span>--module<span class="w"> </span>my_routes<span class="w"> </span>--input<span class="w"> </span>openapi.yaml<span class="w"> </span>--check
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4"></a><span class="c1"># scaffold an operation skeleton (generates the handler with a TODO)</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5"></a>openapi-first<span class="w"> </span>codegen<span class="w"> </span>routes<span class="w"> </span>--module<span class="w"> </span>my_routes<span class="w"> </span>--input<span class="w"> </span>openapi.yaml
<span class="normal"><a href="#__codelineno-1-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a>openapi-first<span class="w"> </span>routes<span class="w"> </span>openapi.yaml<span class="w"> </span>-o<span class="w"> </span>app/interfaces<span class="w"> </span><span class="se">\</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a><span class="w"> </span>--use-models<span class="w"> </span>--models-module<span class="w"> </span>app._gen.models<span class="w"> </span><span class="se">\</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3"></a><span class="w"> </span>--inject-request<span class="w"> </span>--package
</span></code></pre></div></td></tr></table></div>
<p>Why bother? Because <code>bind_routes</code> (in <a href="../../02_components/">02 – Components</a>) needs an <code>operationId</code> → handler <strong>exactly</strong> matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same <code>operationId</code> as the spec says.</p>
<p>One file per resource (derived from the first path segment). Flags:</p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--use-models</code></td>
<td>Import request-body <code>$ref</code> models from <code>--models-module</code></td>
</tr>
<tr>
<td><code>--models-module MOD</code></td>
<td>Dotted module for those imports (default <code>models</code>)</td>
</tr>
<tr>
<td><code>--inject-request</code></td>
<td>Prepend <code>request: Request</code> to every handler (DI convention)</td>
</tr>
<tr>
<td><code>--package</code></td>
<td>Emit <code>&lt;resource&gt;/routes.py</code> packages (with <code>__init__.py</code>) instead of flat <code>&lt;resource&gt;.py</code></td>
</tr>
</tbody>
</table>
<p>Why bother? Because <code>bind_routes</code> (in <a href="../../02_components/">02 – Components</a>) needs an <code>operationId</code> → handler <strong>exactly</strong> matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same <code>operationId</code> the spec declares.</p>
<h3 id="resource-names-and-extra-media-types">Resource names and extra media types</h3>
<ul>
<li>A path such as <code>/fetch-requests</code> yields a <strong>sanitized</strong> module name <code>fetch_requests.py</code> (a raw hyphen could never be imported).</li>
<li><code>multipart/form-data</code> and <code>application/x-www-form-urlencoded</code> bodies expand to one parameter per schema property: <code>UploadFile = File(...)</code> for <code>format: binary</code>, <code>Form(...)</code> otherwise — not an opaque <code>payload: dict</code>. Fields are required only when listed in the schema's <code>required</code>.</li>
</ul>
<p>The stubs <code>raise NotImplementedError</code>; fill each body in (typically by resolving a service from <code>request.app.state</code>).</p>
<blockquote>
<p><strong>Note:</strong> <code>codegen</code> is build-time tooling. It does <strong>not</strong> run at runtime, and templates (the Bake-your-stuff section of <a href="../02_templates/">02 – Templates</a>) make scaffolding a server out-of-the-box even simpler for greenfield projects.</p>
<p><strong>Note:</strong> <code>models</code> / <code>routes</code> are build-time tooling. They do <strong>not</strong> run at runtime, and templates (the Bake-your-stuff section of <a href="../02_templates/">02 – Templates</a>) scaffold a working server out-of-the-box for greenfield projects.</p>
</blockquote>
<hr />
<h2 id="4-idempotent-output">🧪 4. Idempotent Output</h2>

View File

@@ -868,8 +868,8 @@
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">http</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a><span class="w"> </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">bearer</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a><span class="w"> </span><span class="nt">bearerFormat</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">JWT</span>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a><span class="w"> </span><span class="nt">x-server-url</span><span class="p">:</span><span class="w"> </span><span class="s">"{AUTH_SERVER_URL}"</span>
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8"></a><span class="w"> </span><span class="nt">x-introspect-path</span><span class="p">:</span><span class="w"> </span><span class="s">"/introspect"</span>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a><span class="w"> </span><span class="nt">x-server-url</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;{AUTH_SERVER_URL}&quot;</span>
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8"></a><span class="w"> </span><span class="nt">x-introspect-path</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;/introspect&quot;</span>
</span></code></pre></div></td></tr></table></div>
<p><code>x-server-url</code> and <code>x-introspect-path</code> are library extensions that point at a JWT introspection endpoint (see <a href="#-5-introspection">Introspection</a>).</p>
<h3 id="12-security-per-operation-or-global-requirements">1.2 <code>security</code> — per-operation (or global) requirements</h3>
@@ -965,12 +965,12 @@
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a><span class="kn">from</span><span class="w"> </span><span class="nn">openapi_first.app</span><span class="w"> </span><span class="kn">import</span> <span class="n">OpenAPIFirstApp</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a><span class="kn">import</span><span class="w"> </span><span class="nn">routes</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9"></a>
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"openapi.yaml"</span><span class="p">)</span>
</span><span id="__span-2-10"><a id="__codelineno-2-10" name="__codelineno-2-10"></a><span class="n">spec</span> <span class="o">=</span> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">)</span>
</span><span id="__span-2-11"><a id="__codelineno-2-11" name="__codelineno-2-11"></a><span class="n">schemes</span> <span class="o">=</span> <span class="n">parse_security_schemes</span><span class="p">(</span><span class="n">spec</span><span class="p">)</span>
</span><span id="__span-2-12"><a id="__codelineno-2-12" name="__codelineno-2-12"></a><span class="n">security_deps</span> <span class="o">=</span> <span class="n">make_security_dependencies</span><span class="p">(</span><span class="n">spec</span><span class="p">,</span> <span class="n">schemes</span><span class="p">)</span>
</span><span id="__span-2-13"><a id="__codelineno-2-13" name="__codelineno-2-13"></a>
</span><span id="__span-2-14"><a id="__codelineno-2-14" name="__codelineno-2-14"></a><span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span>
</span><span id="__span-2-15"><a id="__codelineno-2-15" name="__codelineno-2-15"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">"openapi.yaml"</span><span class="p">,</span>
</span><span id="__span-2-15"><a id="__codelineno-2-15" name="__codelineno-2-15"></a> <span class="n">openapi_path</span><span class="o">=</span><span class="s2">&quot;openapi.yaml&quot;</span><span class="p">,</span>
</span><span id="__span-2-16"><a id="__codelineno-2-16" name="__codelineno-2-16"></a> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">,</span>
</span><span id="__span-2-17"><a id="__codelineno-2-17" name="__codelineno-2-17"></a><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
@@ -993,13 +993,13 @@
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3"></a><span class="k">def</span><span class="w"> </span><span class="nf">test_unauthenticated_is_401</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">overrides</span><span class="p">):</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4"></a> <span class="k">with</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/pets"</span><span class="p">)</span>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;/pets&quot;</span><span class="p">)</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6"></a> <span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">401</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7"></a>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8"></a><span class="k">def</span><span class="w"> </span><span class="nf">test_invalid_token_using_fake_introspector</span><span class="p">(</span><span class="n">app</span><span class="p">):</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9"></a> <span class="c1"># Point x-introspect-path at a stub uvicorn/TestServer returning active:false</span>
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10"></a> <span class="k">with</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/pets"</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">"Authorization"</span><span class="p">:</span> <span class="s2">"Bearer nope"</span><span class="p">})</span>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11"></a> <span class="n">r</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;/pets&quot;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;Authorization&quot;</span><span class="p">:</span> <span class="s2">&quot;Bearer nope&quot;</span><span class="p">})</span>
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12"></a> <span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">401</span>
</span></code></pre></div></td></tr></table></div>
<p>See <a href="../07_testing/">Testing</a> for the full recipe, including how tests stub the introspection server.</p>

View File

@@ -908,7 +908,7 @@
<h3 id="23-at-call-time-client">2.3 At call time (client)</h3>
<p>Runtime transport errors surface as <code>httpx</code> exceptions (<code>httpx.RequestError</code> family), not swallowed or remapped. Missing required args fail before any HTTP request is made:</p>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-3-1">1</a></span>
<span class="normal"><a href="#__codelineno-3-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"user_id"</span><span class="p">:</span> <span class="o">...</span><span class="p">})</span> <span class="c1"># OK</span>
<span class="normal"><a href="#__codelineno-3-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;user_id&quot;</span><span class="p">:</span> <span class="o">...</span><span class="p">})</span> <span class="c1"># OK</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a><span class="n">client</span><span class="o">.</span><span class="n">get_user</span><span class="p">()</span> <span class="c1"># ValueError — user_id required</span>
</span></code></pre></div></td></tr></table></div>
<hr />
@@ -934,7 +934,7 @@
<span class="normal"><a href="#__codelineno-4-17">17</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">fastapi</span><span class="w"> </span><span class="kn">import</span> <span class="n">HTTPException</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2"></a>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_item</span><span class="p">(</span><span class="n">item_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a><span class="w"> </span><span class="sd">"""Retrieve an item by ID.</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;Retrieve an item by ID.</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5"></a>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6"></a><span class="sd"> Implements the OpenAPI operation ``get_item``.</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7"></a>
@@ -943,11 +943,11 @@
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10"></a>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11"></a><span class="sd"> Raises:</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12"></a><span class="sd"> HTTPException: If the item does not exist (404).</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13"></a><span class="sd"> """</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13"></a><span class="sd"> &quot;&quot;&quot;</span>
</span><span id="__span-4-14"><a id="__codelineno-4-14" name="__codelineno-4-14"></a> <span class="k">try</span><span class="p">:</span>
</span><span id="__span-4-15"><a id="__codelineno-4-15" name="__codelineno-4-15"></a> <span class="k">return</span> <span class="n">_get_item</span><span class="p">(</span><span class="n">item_id</span><span class="p">)</span>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16"></a> <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17"></a> <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">404</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s2">"Item not found"</span><span class="p">)</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17"></a> <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">404</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s2">&quot;Item not found&quot;</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="32-client-side">3.2 Client-side</h3>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-5-1">1</a></span>
@@ -958,7 +958,7 @@
<span class="normal"><a href="#__codelineno-5-6">6</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1"></a><span class="kn">import</span><span class="w"> </span><span class="nn">httpx</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2"></a>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a><span class="k">try</span><span class="p">:</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4"></a> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">"item_id"</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4"></a> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">path_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;item_id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5"></a><span class="k">except</span> <span class="n">httpx</span><span class="o">.</span><span class="n">HTTPStatusError</span> <span class="k">as</span> <span class="n">exc</span><span class="p">:</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6"></a> <span class="o">...</span> <span class="c1"># 4xx/5xx from the server</span>
</span></code></pre></div></td></tr></table></div>

View File

@@ -885,8 +885,8 @@
<span class="normal"><a href="#__codelineno-2-17">17</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a><span class="c1"># conftest.py (abridged)</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a><span class="k">def</span><span class="w"> </span><span class="nf">spec_file</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a> <span class="n">path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">"openapi.json"</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a> <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">SPEC</span><span class="p">),</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">"utf-8"</span><span class="p">)</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a> <span class="n">path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&quot;openapi.json&quot;</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5"></a> <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">SPEC</span><span class="p">),</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&quot;utf-8&quot;</span><span class="p">)</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6"></a> <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7"></a>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
@@ -909,7 +909,7 @@
<span class="normal"><a href="#__codelineno-3-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">test_app_routes_served</span><span class="p">(</span><span class="n">spec_file</span><span class="p">):</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2"></a> <span class="n">app</span> <span class="o">=</span> <span class="n">OpenAPIFirstApp</span><span class="p">(</span><span class="n">openapi_path</span><span class="o">=</span><span class="n">spec_file</span><span class="p">,</span> <span class="n">routes_module</span><span class="o">=</span><span class="n">routes</span><span class="p">)</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3"></a> <span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4"></a> <span class="k">assert</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/health"</span><span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">"status"</span><span class="p">:</span> <span class="s2">"ok"</span><span class="p">}</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4"></a> <span class="k">assert</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;/health&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;ok&quot;</span><span class="p">}</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="32-fail-fast-the-heart">3.2 Fail-fast (the heart)</h3>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-4-1">1</a></span>
@@ -923,7 +923,7 @@
<span class="normal"><a href="#__codelineno-5-2">2</a></span>
<span class="normal"><a href="#__codelineno-5-3">3</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">test_loader_invalid_spec_fails</span><span class="p">():</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2"></a> <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="n">OpenAPISpecLoadError</span><span class="p">):</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">"broken.yaml"</span><span class="p">)</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3"></a> <span class="n">load_openapi</span><span class="p">(</span><span class="s2">&quot;broken.yaml&quot;</span><span class="p">)</span>
</span></code></pre></div></td></tr></table></div>
<h3 id="34-client-contract-drift">3.4 Client contract drift</h3>
<div class="language-python highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-6-1">1</a></span>

File diff suppressed because one or more lines are too long

Binary file not shown.