From c4d4a89cf119f69a296718223b5d7fd6524f3947 Mon Sep 17 00:00:00 2001
From: Rusty Conover
+ π Documentation: vgi-python.query.farm +
+ --- ## See It in Action diff --git a/docs/api/arguments.md b/docs/api/arguments.md new file mode 100644 index 0000000..664ad9b --- /dev/null +++ b/docs/api/arguments.md @@ -0,0 +1,17 @@ +# Arguments & Schema + +Function signatures are declared with the argument types below. The framework derives Arrow +schemas from these declarations and validates inputs before your function runs. See the +[Argument Serialization](../argument-serialization.md) guide for how these map to the wire. + +## Arguments + +::: vgi.arguments + +## Argument specifications + +::: vgi.argument_spec + +## Schema helpers + +::: vgi.schema_utils diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..b96ca27 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,21 @@ +# Auth & Secrets + +HTTP workers authenticate requests via a pluggable callback that populates `CallContext.auth` with +an `AuthContext`. Bearer-token and JWT/JWKS authenticators ship in `vgi.auth`. Secrets (credentials) +flow through the secret protocol so workers never see raw values unless explicitly resolved. See the +[Authentication](../authentication.md) guide. + +`AuthContext` and `CallContext` are re-exported from `vgi-rpc`. The bearer/chain authenticators +require `pip install vgi-python[http]`; JWT authentication additionally requires `[oauth]`. + +## Auth + +::: vgi.auth + +## Secret protocol + +::: vgi.secret_protocol + +## Secret service + +::: vgi.secret_service diff --git a/docs/api/catalogs.md b/docs/api/catalogs.md new file mode 100644 index 0000000..6bb5c7c --- /dev/null +++ b/docs/api/catalogs.md @@ -0,0 +1,7 @@ +# Catalogs + +A worker can expose a database-like catalog β schemas, tables, views, macros, indexes, secrets, +and settings β so DuckDB can `ATTACH` it. See the [Catalog Interface](../catalog-interface.md) +guide for the conceptual overview. + +::: vgi.catalog diff --git a/docs/api/client.md b/docs/api/client.md new file mode 100644 index 0000000..d3e5f51 --- /dev/null +++ b/docs/api/client.md @@ -0,0 +1,6 @@ +# Client + +The Python `Client` spawns or connects to a worker, streams Arrow data to and from its functions, +and surfaces errors as `ClientError`. It is the pure-Python counterpart to the DuckDB extension. + +::: vgi.client diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md new file mode 100644 index 0000000..93ef46b --- /dev/null +++ b/docs/api/exceptions.md @@ -0,0 +1,8 @@ +# Exceptions + +VGI raises typed exceptions for binding, catalog, schema, and execution errors. Several +validation errors also live alongside the features they guard (see +[Arguments](arguments.md#vgi.arguments.ArgumentValidationError) and +[Metadata](metadata.md)). + +::: vgi.exceptions diff --git a/docs/api/filters.md b/docs/api/filters.md new file mode 100644 index 0000000..59f9bc0 --- /dev/null +++ b/docs/api/filters.md @@ -0,0 +1,8 @@ +# Filter Pushdown + +Table functions can receive SQL `WHERE` predicates pushed down from DuckDB, letting the worker +prune data at the source. Filters arrive as a `PushdownFilters` tree of typed nodes; use +`deserialize_filters` to decode them. See the [Filter Pushdown](../filter-pushdown.md) guide for +the protocol and a worked example. + +::: vgi.table_filter_pushdown diff --git a/docs/api/functions.md b/docs/api/functions.md new file mode 100644 index 0000000..70dddc7 --- /dev/null +++ b/docs/api/functions.md @@ -0,0 +1,32 @@ +# Functions + +VGI exposes four function patterns. Pick the one that matches how your data flows: + +| Pattern | Shape | Base class | +|---|---|---| +| **Scalar** | 1 row in β 1 row out | [`ScalarFunction`](#vgi.scalar_function.ScalarFunction) / `ScalarFunctionGenerator` | +| **Table** | no input β rows out | [`TableFunctionGenerator`](#vgi.table_function.TableFunctionGenerator) | +| **Table-in-out** | rows in β rows out (streaming) | [`TableInOutFunction`](#vgi.table_in_out_function.TableInOutFunction) / `TableInOutGenerator` | +| **Aggregate** | grouped rows β one row per group | [`AggregateFunction`](#vgi.aggregate_function.AggregateFunction) | + +All four ultimately derive from the shared [`Function`](#vgi.function.Function) base. + +## Scalar functions + +::: vgi.scalar_function + +## Table functions + +::: vgi.table_function + +## Table-in-out functions + +::: vgi.table_in_out_function + +## Aggregate functions + +::: vgi.aggregate_function + +## Function base + +::: vgi.function diff --git a/docs/api/http.md b/docs/api/http.md new file mode 100644 index 0000000..3a8146f --- /dev/null +++ b/docs/api/http.md @@ -0,0 +1,6 @@ +# HTTP + +HTTP-transport utilities: a human-facing worker info page, demo blob storage for externalized +payloads, and request-size middleware. Requires `pip install vgi-python[http]`. + +::: vgi.http diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..b894dfc --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,47 @@ +--- +description: "vgi-python API reference β functions, arguments, worker, client, catalogs, state storage, filter pushdown, auth, and observability." +--- + +# API Reference + +## Where to Start + +New to VGI? Follow this path: + +1. **Pick a function pattern** β `ScalarFunction`, `TableFunctionGenerator`, + `TableInOutFunction`, or `AggregateFunction` ([Functions](functions.md)) +2. **Declare arguments** β `Param`, `ConstParam`, `Returns`, `TableInput` ([Arguments & Schema](arguments.md)) +3. **Host them in a worker** β subclass `Worker`, then `vgi-serve` it over stdio or HTTP ([Worker & Serving](worker.md)) +4. **Connect from DuckDB or Python** β the DuckDB extension or the Python `Client` ([Client](client.md)) + +Everything else β catalogs, state storage, filter pushdown, auth, observability β is optional and +added incrementally. + +## Modules + +| Module | Description | Required? | +|---|---|---| +| [Functions](functions.md) | `ScalarFunction`, `TableFunctionGenerator`, `TableInOutFunction`, `AggregateFunction`, `Function` | Yes | +| [Arguments & Schema](arguments.md) | `Param`, `ConstParam`, `Returns`, `TableInput`, `ArgumentSpec`, `schema` | Yes | +| [Worker & Serving](worker.md) | `Worker`, the `vgi-serve` entry point | Yes | +| [Client](client.md) | `Client`, `ClientError`, catalog client helpers | If calling workers from Python | +| [Catalogs](catalogs.md) | `Catalog`, `Schema`, `Table`, `View`, `CatalogStorage` | If exposing a catalog | +| [State Storage](storage.md) | `FunctionStorage`, SQLite / Azure SQL / Cloudflare DO backends | If functions keep state | +| [Metadata & Protocol](metadata.md) | `ResolvedMetadata`, `FunctionExample`, `FunctionStability`, protocol types | For introspection | +| [Filter Pushdown](filters.md) | `PushdownFilters`, filter node types, `deserialize_filters` | If accepting pushed-down filters | +| [Auth & Secrets](auth.md) | `AuthContext`, `CallContext`, bearer/JWT authenticators, secret protocol | HTTP: `[http]`; JWT: `[oauth]` | +| [Observability](observability.md) | OpenTelemetry tracing, worker logging configuration | `[otel]` for tracing | +| [HTTP](http.md) | Worker page, blob storage, request-size middleware | `pip install vgi-python[http]` | +| [Transactor](transactor.md) | `TransactorClient`, `TransactorProtocol` | `pip install vgi-python[transactor]` | +| [Exceptions](exceptions.md) | VGI exception types | β | + +## Import Convention + +The most common symbols are re-exported from the top-level `vgi` package: + +```python +from vgi import ScalarFunction, TableInOutFunction, AggregateFunction, Param, Returns, Worker +``` + +Subpackages (`vgi.catalog`, `vgi.client`, `vgi.http`, `vgi.transactor`) are imported explicitly. +Optional modules require their corresponding extras to be installed. diff --git a/docs/api/metadata.md b/docs/api/metadata.md new file mode 100644 index 0000000..a77a10b --- /dev/null +++ b/docs/api/metadata.md @@ -0,0 +1,18 @@ +# Metadata & Protocol + +Functions describe themselves through metadata β stability, examples, parameter info, ordering and +null semantics β which DuckDB reads for introspection and the query optimizer. The protocol and +invocation types model the request/response lifecycle on the wire. See the +[Metadata](../metadata.md) guide for authoring metadata via nested `Meta` classes. + +## Metadata + +::: vgi.metadata + +## Invocation lifecycle + +::: vgi.invocation + +## Protocol + +::: vgi.protocol diff --git a/docs/api/observability.md b/docs/api/observability.md new file mode 100644 index 0000000..449c2b3 --- /dev/null +++ b/docs/api/observability.md @@ -0,0 +1,15 @@ +# Observability + +Workers can emit OpenTelemetry traces and structured logs. Tracing is a no-op unless the `[otel]` +extra is installed and a tracer provider is configured; logging configuration helpers shape worker +log output for the CLIs. + +## OpenTelemetry + +Requires `pip install vgi-python[otel]` for live tracing; otherwise a no-op tracer is used. + +::: vgi.otel + +## Logging configuration + +::: vgi.logging_config diff --git a/docs/api/storage.md b/docs/api/storage.md new file mode 100644 index 0000000..d7e08ce --- /dev/null +++ b/docs/api/storage.md @@ -0,0 +1,20 @@ +# State Storage + +Stateful functions (notably distributed aggregates) persist per-group state through a +`FunctionStorage` backend so it survives across worker invocations and processes. The default is +SQLite; Azure SQL and Cloudflare Durable Object backends are available for multi-worker +deployments. + +## Function storage + +::: vgi.function_storage + +## Azure SQL backend + +Requires `pip install vgi-python[azure]`. + +::: vgi.function_storage_azure_sql + +## Cloudflare Durable Object backend + +::: vgi.function_storage_cf_do diff --git a/docs/api/transactor.md b/docs/api/transactor.md new file mode 100644 index 0000000..3150bec --- /dev/null +++ b/docs/api/transactor.md @@ -0,0 +1,7 @@ +# Transactor + +The transactor is a long-lived subprocess that gives worker functions transactional access to a +database, mediated by `TransactorClient` over the `TransactorProtocol`. Requires +`pip install vgi-python[transactor]`. + +::: vgi.transactor diff --git a/docs/api/worker.md b/docs/api/worker.md new file mode 100644 index 0000000..bef01d8 --- /dev/null +++ b/docs/api/worker.md @@ -0,0 +1,23 @@ +# Worker & Serving + +A `Worker` hosts your functions (and optional catalog) in a separate process. It speaks the VGI +protocol over Arrow IPC β either stdin/stdout (subprocess transport) or HTTP. The `vgi-serve` +CLI (`vgi.serve`) is the zero-boilerplate entry point for running one. + +```python +from vgi import Worker, ScalarFunction + +class MyWorker(Worker): + functions = [MyScalarFunction()] + +# vgi-serve my_module:MyWorker # stdio +# vgi-serve my_module:MyWorker --http # HTTP +``` + +## Worker + +::: vgi.worker + +## Serving + +::: vgi.serve diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f17e82c192b0a3ab23658ee395fad170a4e4c14e GIT binary patch literal 42039 zcmcF~WmH^2(`U@!8fL@btO>X(Z@uj&l4$$>b>!z-J`uvVU)uk*tFdg4XsplRA^c zq6TG0aj<8M;h`eFr%5C%CQq%WuFggS2M&zU+ndMp0$N()Or2hb?+WyU#*%+cQ~#zC zEWhD?5ZM7Sdv=a?+E2RvUF2dIml|< zBCD!>tTtt-Ds$zkx)K%E#%oBUb`n%CDh&fSpC>(U7L!jtj_r5d&Fu5eU}WQbL9ld6j_Z&r+j^KAMCVEtCb z_FX==>?<>MqtBe#CV@AK_C2BzgU-$rEiEyG5Dbkt9N0fbxg54BtItH3IyFNy7FzTJ z2gbr+Q9*M{oT*dlFr;8$%)#}IBibnZCHb4lNrqw6D4(xA<1}7-#%Tcj^tL @XE9%c1q{lrF{HPa<|bPP;T| zmmUyW;}i5K2fH6bC?5=iNw}3%G@Ro0v1o8GG*@Wqv|qE7vO #$~)eg|60)jW>b>FeE|A!Dy&w*$3anzK0)Y%Z<0N >`8VVuy}HVKpK)ovjs5%md}HZ z29u}OdOWi!!mMeM*^|qZO2ed5QO-Cn#xb*O)@+n)-W70QFvYAcpRS-xu`q&RnKU-U zx!{Z_$1aSqev84DodJ7#1x<|^rgb`;yevU|oup9m=sjGZ|8SwY-)U}1(A8CsX+qiY z*|&F$e9lD!O-(VTOsPjo7#%5dpeGMBg>4~T2yFMaDl|~5%&3h?1KxAdYnj^F#bsak zDvsy!no~~Xinm^Z>$+gu{OYcI_|y-6LQ7*4samx)m0CK5S$X(fy1)#-5LQQ2QrkXZ ziPB`hz%)?^l`jZKECdyG$@pa&gs%D9fu{Deab>8nb}KG{9zVXv!H5YO{p>f6m%!@E zj(E;ui}9pOsNhwsT0o!(ER&P2{4j%CcGCU(Khn_D#I_rM&+<=vjO8Ey2&=CCE|JQ* zY7r83p3_~Znzhu`T!0`F37FF6Vn~-{LeSaj^YkWodZWe2s3smY=;~0kHES+B(`3_b z!MYtLgJTxQOfTcQ1#(%3#+DRkpJp>}cEIXQnoZqN_V-1Y)#cJsSEQ65#g1AuG{-sn z^cYJQS!~>{*s@Eqa=l=1$l 2xk=*iavX~M{vtw6?>*W z%9N?~2m$$m$G&}|AnaPT{R`5`zek~~A7fOycUBdlHq8n_u@qKA*p=3)KfnDh(y263 zhD@_SDFPLYTQfpGS0ix|xEdTS@M_aY1OXC9hd*<5_;od4Q$pek178b#9lobF>Z>{s zXae^3?55qdpG?TLs>*#;H-)G$!NU0OSrJGzqgA<{6cR%~P%CiNl$ #_Nl zza|=wp@4WS#`>T93_G1*-oL(?e7+E#EQS#lOz3eOcD%Jfr3W4=M44px`zZv03aj!L zOfS*Z #&Vj&2S*qgDbn5wuR2q5 z)`_9p^UNlbr? Wkv3GyKZ~ox%)Veq&KaFV! zrccYz(Uw9fO`+(~y=xfP34f<_+Ggss3;>>|>DfDq@^#I0j4$k8mX~%AM&$zwrBbDf zlBD2z{Pw{|_~XNm;RK5P1H(jYt1^+QirrR-GZmCUiQ2EgMBj9xNIgzgd6p`U_rf^h z)+*)yV!Rltn(&zD+v?}L9;ycSjsF$89|k(41jsP>RH}0Q%KA$}CY~lhNXTWgEWP~g zq}rN!>MP&D7NS WKY}swGVyn&cPM5jUTy)^Df8PkHWSOR>1g| ala@FuF2bITACNGA%
eGS^J8V{N zjWTzNL1$xuL2r!uh7_rU$vLN(OzBiSy {YgF%HqKO1L@GHMVWeX^c6_4WW>tVxBu4Ccl@5D9@lphkMOf(T7LI_bA zfY2cy+NkU~qY}7Q&l^!W@v8NQbElP<+yN^$n>_lAM>=89+$iuopA!~av@{EzSRJr# zdz8_3gX!&gd~Xz5(nO;sv!_8vi@_6XeRl0PSh+btSCeM$WQfRs!Tv1el0#!ll11|@ zrcW_gyV+y)Cb<7`XlYC_b5fDINQsWlB#ESnMo}(14D@BO>`I%Ts(V*`_Aj-|QB~%{ zL&H3}W-Yd9W12=qk;BUDSTw?8Yt~^~W(A%SR6G5Ii(9+c#4Fc+;29<*)51q$=+H`6 zEm7^&6DugH*Q^C?m6KzF1=k3Cq3}!#B;=*Vuo0ms6?v>$R 2S6{GA1IPoMauDgZd&D|L3M#g*} zWTiQI4Y3G=JTo1qZ~b d7vagZ0egEb#tLcXbQSPa znWmN$hAA+m=FH=5GHKYbEnwf_IHO~dg)@EX(vU3#ln#|i*9qb=i{* =~DV zA%oSMVOobt*UU8e5@>C5pr=U5wUHIJqeSER#)J)jq;Y#*^P{xZ f||s9sTMfeF;%B9(_F!v-*UVd6@I z1U^!QaH3&^2@O?gomIW1A_j(Sl8_p2EElegs1y(=750&?WEjVPxo~Mb*TYPvD0%@h zP$Xq#poba{1eFda-j;=hG7KVSWrTzlM~wWaW-oPxiYtIXNfeT(Eovx=QVPouxPDmv z6eutag>6?hh^X?D11v*QD1~;^55P16q7hNa`i3~VDFiA|*p|c#OiE4&*ZK;gwnECv z_>9U8DyW6|p(_$$Xk?7fzWW_~^wKvSQL6WBc~KU+y4;QR^Ta+_e}tc;SYUmF3Y` z1f)fI!X(wOKr|)YFqK{5JIhLTuX>YOgByhN1bq!)kVJq2PlqMhz7mvt2m(>zLDxW= za7(&Ac?B|-0v-0Nvu%wbE7bmqJ{wXXq^{hDAYoVt?UONerP@a$s{Ji$>l>!iGs1;x zw ;h$0H=SLBqSD`Dbar`m! zh(~OSC6CVLB8z7i@q-C%TXjvpp1UKeNe%R~eqK#QQ?tqACb_Zz&)Eo!Zxmg%NEP2& z%g|?*q4g4mL<(8^xz+*|2m)W=`rvpH*Aw`G#PtPUAn}8+CeHH!CGdn1jxbiC;s(_~ zMeE9Q2z_)@jHlCUcPb`yKB{2fS_y(mQSo?Fae-8oVL>ufV$+hCrouGBkrb8`*rtzd zDlAJAwZM*mX=+R}z%;^+{Yojcsy4Z)u>Qn2R5|{)uHD14pV!3K)MR{rO#JVJXCo$@ z$m8k$s*%zYK0s7I8^XhMf~pv%&668UMgo&eI>oUI=7vA}3svdn3svUog|yZzm_C)` z7R=@0H5=I1Ycr+OBNZz>r cyhlIl&7q=;+J+xr)!JGiE6I(p zHEy`m10^Urp@O=uLD@4XIZ$>4Wmn=>l-Tt`cAytP5E%FsWk3@){;oBU9M7&ODxW7R z!I%^k%U&rYu4L~-<#po^l!}YN5u2gf<+S=) mb|=5-d| xWnvi;(~uZONclgq!C jb zpR%hcmR(9^mxAL_EV&fQ4*61 {4=k950|)Rs<6WQBoL~(j@Gh zCb6WzG{LqMQCktQ17eYYcvO>!Y2wj DB*M0RaeS?4sY~&H-ubrY)s??Xp&^9klv131+_Aj&j8nMn z;V0-5DYoqPSTJ4Tc`vLZ{`1L1)U7&_*EFc?W{DpNO0Ge%EGZNXvPFY@Dg4^;jL_l+ z5cpxwIIR?#07Ga@Gn@cWpE9XWCdi}`G}gsw$s}oONYhxKpedasos5!-*(9T8*hauI zu}uThkhMh25#tytJFeDgv=*06v| ixUP<3*4$BZMnpI%tq!O+dJdaX8KZ>F! zC~(#Kh7qvD@g(_@!C2lPn>Wc9CB?FV>lvY&;D=OAUj>x{Q$aFn(U3~emPydwn5Ltt zj?ShuZJ8vknPk{uG#(`uF|jS9GS8b@L&zHPpmfcOhnu5D+N|Dic>FVw+3~F3QdBZ{ zm9Y?2(o(p7vNk24HiQR_AL!bNEaiA%auw$ZV*(Kp`ej(FC@VulAuK|6y?~ pee9TLBvwTA_2)*Kq~1` zp9)CD0wNI=`so#SDG0{h$v>Z=u0c=FuTx54+LHd^IBT{faDynLW5fLWyWY+x-|?2e z>T3L53Jv2;09S6^#+yF*S+p=PMZl6dC8l jXYp2bhK+5iw~@Cz#Y+$CS1VQ#%@&(w3pCIZaD_ic}&>%rY>I z5V{M3@jpt1a8gk59btTGlTwvIM b5L_l52r!F3lh$(C 8G27&n1(RuMNT8HvV6;rh3#f}lh}bgp+pUViN{QpRxSmfU z76}PoL4aRr)f;YJg&{*!MpQaV8HRyn7-3br5V)RCw&>D7l4pP42s;lBv3-9(yL*Q@ zG&sgc*1_>Yl-M*4EK3r#LLWSp2*{*;8qyxActFHb2vis|RCp-Qw%%H6EE8Nm!p5Bu zdiuhuZ>@a3@PT*puGhY5Li@NE2weO<3O!M&Z+rMLKK#{hV_Fe})XeHCGG}rb%LwZT zgTSOvG8oC3438O%=4}e4O3>y9_ rRwjvv&HeXT=0 zx#%h0^R?gd*rxqNtgywG;|F-I&wDRCj{kc1ITTA}wAQ2(G45Wmled5AMv|!nquD%P zd*7>g->Z+u4>VVP{}0@?Vkhm5DI%6ZDrS>O#b~HYkce7bc)}bOO>g7T4SUF>Q#7R_ zq~lTIF`KAmV3?9(p@eN)2z0o1L4|syL$;$K1(s=qe+&a@NCE} 3Xb#rHpbC1)&MT wBPGQZbXM9ckuGZDG;OPL7${%G9 #OJckBGIM6rB{=qQ@$4U(3%M?o` ze)zH1GpDPW|N7 GPs9)iHtz04Dewb@e;b>kjuXETY86b6dfm=^ vg_YU%(KlvjC$K(7H=Wy!6X>9L4#PZo) z93IStX@0|?>}f{&hS|KkNWt-GN=JC#8_(kVx81{^R_^3&XD#CD555|ye75fGWBa~- z=1gs8dRu+18D@PdhSHj{>*4r5j_23toDtJt;j~tco!!CP&s$2a=&-MEl#RO&vvT`> z*6%t*Pv0nm1)rguN#9VMy%9k?8jz_|jOAf?#KJHoskp^Q-uyQH?O)!CsGa(M8?*Uu z9$r7O 9*tU)1dXzmEfu=bXXKrVP;}%Te_yyCLGpUJ&bPU6g zc%Dbu^~e ~iAVPTqFbLh2JS=1*zi#s}8p2AWjV=CTWpXL@U# zcf4{LcRjt8r?!@OX3IVbV@2vyaSUk?D8=Atf#PVM=9I~#h9n!i`@%WidK`V%gq1vn zlFQ+d0!qW=mIk7>!O%#SB{N&N<%|D95NO&O66A{x@o0n &6q2`)Nq2^)7F zq^EzB{rw}%o!mq^uE{hf$re3)KfrNZPB~^O?| ^MyKgmJO=)iU+@&m>*2>^mftw!O zKznmN^QSb^Tp!2teG<_KmT6FO9g1a#Vp-REO3#_p$iiuDy!D(V3};L1*gwF^ZTncU z?EqW%4KOfPVzeL_7`2E+OyY4#zEI>fXP(T5-+D=fTcT>6X#Y-y{_EtVXDjoXZ9Dkl zPp@U$o_!3Aj^PJkS~U@|nA}>&adYQ#(t;^0o7F*Aa~;u$tXN==T(Lx;6qaFNn R^h?o+0EYC {AbXDtL^ zQ+F@@V+9sWZ>KRCtz z`v-I6ih@|g=8gv+ (~<;mTZnH9)Rd;-OXRXllsNl&Pb= zKEVm|yEtvpG#1ZjqcxL62u-={Qglib%AxNoMHrbU<1unYm;L>tv}DqJ>!wF}YV!g1 z_m9#$oFiXy5K`d#9$WYJapKY$-2V7x4v#rZZcg*f55Ahk3nufmU);|(fBP7ldj@!L zZ8z^Ye;L_q0o#<+Ct@5J$kEe3#@KL XZ@OlmoX33QtUa@2vpS<)e9H)H5u$?f|Hs12UkOJGb`1~*LXVtF5 z)Fq;fj%NALMJMsLbC;1VIy|%O08&b3wAIns7^Nv4Wyb- !BGUoG63z3G2YldpG!`k&!OQQtG6HIk@b6dYD*8Jd6%JVmIH&sp?+!of~E*O zze3koa4%e{VE^$8=d*bB47Th!z=bEx<$E7~4KX`Z_>$u?Rwy6|yW3}yaoixF?6^SK z`SRPpd6?Uu*hWizl$-wR-8{K@KaXzOPjg*@8Izir)!D@SX>H8vY+%8ZCj3H~{rzKT zt!PZe=x9umA0FXVC(P!t^}Fb7uA`$dNudzVO-V#!v@|4PZ$E>hdCIPjWf~wL9tlZ7 zj_;E%mN8@or2<+qNlYU=N&1F!%<62$FeSbZ!?{wWv3fYPW*}Q&drv>7F6hGb+zDAe zYW+BB20#jivjz6{4v|bG7#__rQgpE+7Tvr0*l}=}Sj6On1yhJM)G=#vGf!?k#J2rI z -Qj}LA-KGS*F3@Xpt3Ldk6xb6Xs2(vndVQ=dP!>vwrts>g!SrZ9oRG}pzr{{9VIf8RPzoZrRe7aqs7_IgULM ab0{rgk}sWFhvOa z93CsNa_bHzHP^9xP6x%Zi(yC}TECY~Jwu$HnMy2bv3y<^D|YlEQ2f_>F5t9dI`RFm zk)Y%Fl?E;$qs?)B96!Le3_6<<-0+#V5|7%ro`>VQNFj)rg7!=j1dQfNjO5F-H^fj% z(_EjxGz6}%8OnyuzJowfpNwJ|2BpAbBv&M28N?zML)k2EKW7P_zwBJL?d#{tAKb~7 zeFJ>ww~ukcRc}9nSf{ndloHbngDb}mD3{87{e2g(?Z7Y}|DQh)wJj{e0Ndi}t@|0y z7HLSwx$BvoZ0#AOr$0|R5o7P+VGa-HX{(Q+wI-V{qZC98Nh%%*sjyPu1&YD3LM>}p za$L5L zYT$!$xIvP@OgoKfT!*I@_A(15TeYEn)6@6^W#55(2=OHAt zH>MFluI$l2Qebj(5 !KF*$rwTk4h?2;{D6*z1i5k$&MnkBOy#;hkFM{= zG!4q0&ux!y!Vfg@XoTiWiotA=9s38F(mV~N1!DylrNOpL(y_4BnIQ#^<1 LA! 8m{Lf5 zWzc r{*19 %<^n!&(TSvD5tLFSqjpI5l#<6b?j;Bmr!Slg zfrn*Vj1&S^Z9Rb2iqjTPqoqDd(G9ra{ >q?;^g_0DZAltqhi@3P@2|Eg2q$~ z#}DWo97V?C{Ljsgv$bcKc+}=Kr!C~nrL!rP!WLl>%M7!T2L=&Zaq5yqND (vt{*0KB&1^zG=f~oWjI&DG>mbg6=5Lz z+EeCZih#VM_|KpGi3>jY3%>vRC#g>+D3n}G)8whm`@&!@P`v4kMZE9glla6NPv%SS zIiL2%6hcV0>^Y3%g;aoSv5X-NQWb(#pfsi-$rc>)WfuWqU>l2KRB(I8^=hoZc*Mjs zP5OrNJhFBtQ`;LjZuTTf#WL||g!Q`*v*++A4aqoXEuDdBn>@U(n`gH45Q|7&cgg}1 z5d(q5a|4P_*mbr(5u>xYK2&n2%oD43bN&76kcP>Et_H69mviwwhj1niL~N6FyALy% zD-y9RBDP7ORpo>XTYm*XNMTUV+g{@N 0|%E7*m>(IG&FoCD}rmQ;(U#dCO;W=ZYO{=swJSt9J3`bC* R1X_?u+8iG7*tn-JB*h1yw4kSd z7}wVfX3I4NY|SJ{Mr}&2Pv3B^!nxJh23&g1QXXI5&B5U!@45PB+8XNE*nNmxvCOMZ zn8UHNJMe;_G7zc2;7ATH2;+xYoek8-LN3Mno%=cMn91YS$x=`#xtus}5?{Og0)Bkw zGYsV%w)Pz6(({%xy|an$-Shydn91552WUt}X-dVIGpUj6;3y@>Bb`jqkxBB@hHlnu z-p%2mF*;l7IW#cBNWO?=nnWWOp66f~7Q@*Bg_28MD$bLed-%eS|3p(dMrTWg)=ZqK zEurfbjab;0LEmVOk!+q<9XE@Xrn+#brI0+hW(Q+Mhht~AvuIiyg}lpk_pGAecr2UI z#zm(uWXIlqENM`1J@Q2d0Yq$rDQyi{QnF?55TE(kpYXJ#K4$W@54@IC#2{aCF$_T< zLf2-+wgY%dkx3`{!S8P4mir&!#6=4@?}X)?u;>^X)2W&+24VF dB zvz2(;zWqG#%qkvSxtdM8_cD~t5hyUELBukNMI#L5O03)2%j}K?lw3b#(;-m4$NOJ* z0*|cUO~FxIckgO0dBq}PQL8pI2O%&F&76)5cLo8z 1QVi_iZ3P{*tn!^tine_kH-kFEVRabfcbJyCJ-n-M? zNhj$noqZ<+0t6CvPy}TWVMJU;amLXZg;5lB1a;JRMif!T4RsVm5r_+G2w}+{vUieB zC%x}oU3=AC-#_lH>dwNdv%IhMeIA~NbkcQi{qDKvcYf!b@AvpA3;F1crD^LOK{HHb zQ6!nka{0=c41M52e)xxN^bIHI>W@)X7UZJkGx*H)=OG9dhH3HP%U5vK>iN`_2NAHy z=1i8)p2Y9&z8Q}qBa4v9 `b3%TfwSqzS)@u~{-m0>hp=bfu&B8#wm zRy{Xgyvz|W40y0CP-O+vF!=aYE4g6tR5Dp7{bm{l=PjB_DClL* $2977- DyzF)h!&HpBPfc@@u7s%du^F~ z_NGf%G_9J^L<(7wuyDx4@r=fvqrG?(6+_p_WOWXw207f-&qGgdU{XZ|XUuQm!j&sH zd+`D)BjIu5HXVtB)Af1C#U5R2yy7FeP1J`vyLe{vHlE$$81ZN_g=K>%ON0Y{X4F@( zdVUiNn(MjeXU{PjPw~ou4&Ht4yrRPwMS*lqW8w5Vt~hHBzj)?VcDD8M%&rb9A`uMJ zEOz!=mc{JGN>oWCqZ#Z!+QaJk^_ZqJx6Veur$}7AW+5*g=wdjL;|o7}ign9o@bHG+ zWDT3BUuET-$>j68)AZx_Qd8!mZ&W4ZmC3m&FekN`=hn-Xarvqix(3G(1c9n(h=%Gg zx~8EUP6yt3i<{io+hLSgINf`VRiWbiKGT>;vmZZX$CDXkNn~=hGju(bNh1mZs^n;4 zdsPv?#|hC4v!F04kT-PZPOoOkta=b^(%C$kuJaGqt_B2j!$jAdvqC1Xv2<1g4}b9{ z2ICn9V@Za_()5oe=ow5Pi8hgtAHy9#oJeQL85UJRAFJj~1uc&vi^YE9@{pGcmP|*_ z89eyZPFJYLVo!TNTMl*;33w?BJ9+DPI`4*zI ~0^SYcP&w=)C{3Rm^Fs=8vx&VE3^BdPmb78yKOzZzI3m@FJ5cD_AjS z78k8r!MRHpQ$EpiyE|whZ!M1t$2bq5YjB7sU)jo^Ufj&yBS(oPQ<%1ZC`yC_0cJLo zbN2iu&RaB%IZd^M{4$Cv^Se!ZY3&(dPkSGu$s8V8EFSm-Q6R77_`u~Wcxu~W2F5b{ z=d-(LtgdhlV(y!!=?2Y{YbXo(=^sn6uVaYg14(ur?xL+{l+6dbxN_ATZoYUqd)oW? z>7TZ<{*_jK`@&wlszPmffG^&15zP(dq|#X@lreO^e9MI>ib8qFS7b~V2%YhCmat!D zQFDzW8(^5klPT8&IR7r0&bohg=vCtjXd}9XB2jh62Nfww<4oQHgGB%WUbRRoa|Ts1 z9tJ6P(Ye^RfTkH_bNQk|lMTcZX_sUxx*p0I(-sMNS-oJgTexzZly2HCyG6sRNaXSw z&6CRb ^G6q#fu&&rlYZolz-(rLGXS|CQ-pehPGk8~5y Q}a~5YW zo{!(vOgzCukHq6_+_{G*U*5{b9j`JHPk`tcWZ36tMtvD)FPO%fCC$uhswNyzFbsoi zUS~9(r92X3MN0!uY&k^7V4OqU!>pKDLprM!WdX8zotX_4y!XPTeB+@P*nX&!uAw*q zzatGM3IciEpspgsq{=8`8J!Kg+j(YJ8$)AhG|eQH%@Of?xZ#3@eD=okIA?wn+YfbN z+X4+$Va{IA#MIg-sZ4f4!@fmBW!O Ty4OE3(xTYKA^M> %e%eDjsjC*$MvhufmPQns>}S`dDD65x<%eF zF-(KJ4%e<(Ky7)1eaHK`_PhmHrp31(-a!9on(By`4_~pI#j_@H%f*X0)R*M?yMDn) z!gf-$HYl>f{^P@>^Ex1K-P$FDeKMno+_-nfsbT1*&Gy!A3=5`IN0?e$#^`7Q+lK0} zhj*XT!n; 2LexGCg!!*{`5DtYX4|=)stT~)Hr=G>l4U`2v=!Q- lIClXc6ca`73>Zp31f$!WyX86?4(vF((F z9aB mjl!yIPMEpcUg`YX(T~(HmUBkCrV>c~_@Sqze!?C18=aNKMuFc@g`AwX+ zq?x?t^rnxDCAs<1<; %G<%N?U<(5CraC<#@l*y$6wnQeyq-ZoaE$xhIb40tJURyA>^j`T z`YlJ;wC^|`MW(%Pkk5bbhvae@md|Z*3R;$BF}rCBFFtrbAN$HTd1}i}B9Sn8!(hqG zde)xV!q{+(k$AqiolWF+2?A(&o#~UxnN}NN>){NW4|b8$tzyGFI1f$NOzNURZoX(K zU- L1+8Y*e13KRCL&KK4VGC4;aJC@2K6uh43 z9Dt&s0FpbaMbk}ES&h+TnxU~21F;nSu>^f1Df(k+hT~~QlNrX+Inp_Y@u3^`gfvk> zct$9mt_zIwLTOtNB_v5Mo~{J}UcX3nqRP|6+>&jBVcTe$g`GEDo@2Twjyc{RVi&)+ zEQ@#)iJ(^@8t_mS4p0>dQd<_Fwmih7iZC^0A*!N&%7Q+EJ`bv21ksrZZkQ&zX_C(7 zNTe)8obM`&0#}_qk9VCjAI&hx=XDfW#ugm1aZ6)4BV!3h6KNz#WN0kIzV<#uQDRDU zl*a1FNrY(u+p_TaWHue^qJJcTM;2K=yU~@Y7#G5k+)ANtSmcwA^O{~4raTnj_gju& zTaeD?P$iL{f8|cDUAJaD |go;3@2__ _3D z#Lt|`<$%+0lgj3~ )ZucmgQ{cx=t*aM!-gvoUK`r9YUvN+hp@5W2qc{qj9F0G-Q$<(PT5>5&qA26>$auXc^LHl9Rk0_?q!f&? zIQzu?@kxwun^U{SY}+~^Q9AJ@5Co@lZ6Y6UB2)b2yDR8;#Iq(N=@eF9!Xf)wmdo1| z@T$&y?edVHx{3&st3yn!EoX8~IgQm3s-u1)eh(hi(UCI^6U{J*r!!8Mo;cnBWm+~t zkHiE2bPdP)Vq`KJx?wVs$Pr7Vi6>G_ubV|QpfZ+BO>lnB+KHmfw$?5(d4t-rAWNDj zITW_nXhssSsf>o%w5O9#-nX8fkqowNFtx6d`~LYKSu$&e^IhHh6Kr?CnAqR^t0$l3 zj_>>c17|xrZ$T4Z|H!3GDhm)#W!#vk@DLoLX(Zs|@y$p0_`g2}*xdVfS8(N-vxuj2 zCwO$r6?ELz+C%ry7-!6zLRHv@ ainXA zBi+Ls8yus5G|6Z(M<(aY^%h)Xm1POp{r~aN#^dj$=}J!(3}swYa{Lf8Q9f&enY%p^ z<4Zp-gkzt^h^?JfLmbT#h6BNZ1b0|8!4`K|AUFhf2rkP)a19m+5L^}s?k>SygS!Pb zxVyXi+3!D`xtPo8o}Qkrs;8^#WdP;F@}Dtg-OOHL6Zp6cd)tJk@lNk*&mRFDM&f|% zPdGD5waG9o`q*rh0_KgP9FU@h-OrTxVe3qTo~;8eX1XBKWo15b7^Q>JVCcOjp*YUa zw%Z#Nd{U#OWn~Q|QV46Z#K)p2*wj*AEMY?0iErrcEd}IyiRtOdW!5z1feGV?pb9C# z{|)wvS{?tJH`M>+rNlKw>m%4-w@5L?Kc 02`jNlv%r z#9TY}@(f~a5&>fIBg9~AdNs%1FBYT-{;uQMVu>486F8_s!B)p4`R!sdt5$K 4A_C2wtLLys6Pzlow?VOPOGfYb5 z?Xcsr&C_(%C0yI2YxruCy1tZTl7`fVP8j> SzFT%jUWW8=z-Kjz-_r^I{hY|^ z))4FBK4tt}TBYb{QKfK_KD)j$TapXvY^zh9DMtyHqKhP9hNL{)=EMzd#nPJ^m}oL; zn%Jq&Q z**DQB(28e;6x-#X z6TPFC)vTh1^flcI8HQTS^YP+}nRa~MRYlYzu&yYfhLE^I^Wg!{P7BvXF#a%)RxkxC zmKx({l*oc~acM16sut5G &tF<&)VQ|2st!x9}UixXmn1ZW#`iM@je{WpKNG{%1=! z8Cdg1v_4O%Q+?n0F6$3g1x@+2^G`BYCe&Q1QzYToHQTD#xo>=-65zRse+$k#O673n zwl7}_HPwu9yHB;MfbnI8++^RTJZcyT>!s3pzYQ;VM`Lls4X$et!h`WhFeyWuQz94_ z>tUZn8Lq|w8XrORR?{LSjd%FIbJ}cq>{+2vt^E6w*TRjOULuELyj 5$^GO+ z({0Xbz`=He*!RR9!$6;%VBR}IhVr1_sOuWY+N1Vs_aXz Z^`c6!yQCnt z=r!}J>nr|GNrXDvMB!C$QGUa=^tOteVLUMpGmLM~SETm3wV|!{qFJ_VA!UO<5fNTC z!&sMII#0ub15Z-cuW+g+ZDWg5HobUd+ViU;a1QIyN^`UIJ-gT6=qb@9jBzH8JKIwC zUwSXZO8H-8L-*alz|8s1i+QZk6!o$EaGX-wmXcA#$r&}adSw5M@{BF42&QS=9X_o* zYcI8h?iyPVAR)@T^=|aJqsWFnigAud` #6xm 0Wq>G%%m?}CO|Zyx~{)LavAuwLV^Gb z?vGG&kW5@$bZxe^RR-FHv 09+50_yfMtI^*UL$&%iZy1<;Bu?-jOxcT6&O%DhLny^rF>3d5{C5 zu<*9QE~Up&m7Z}B>~8Ov?_D*jPqkq)>`G{bUjB6ves^!-CEMS0%g;_T!sWS48#`S4 z)&8v;F81 Pxi1J4b@BrjN8Y z`o+EYi{CWV*FFxaHPizvP3VKE!}GD??T!Q6oQsLKu@{S@QIfd}QVCh3%4}1*kNVMb zX{cK4<6y4v*q}A~#%z(Js3g249B1BOCMC{h{;r)F9`rYFXBwL-F#Bp*T;~S)Jl?3_ zU?C}oC9TGZ2MRZgvC0|z@gmG+JYTPTVnN7C3Qk3*Z~n{0)D-eIP%r+sRT+A1rFo29 zd4|tbsa9`r9H@4aWa88s@|a`P65Rh66M}J3%oS_W>9#hWyUUYtFwD{MbU=KrWb(?V zt#e+kvfMw=apNGD!vQ^1ivf<~ZnQ~K_WVWWOEdN9llt?0cpbyBuYb852f@%qsJ}l> zkWH{Q$Qaa$G3NMwDDa#|%=attVWQ$Dm3`BXo_0>(rz1ccAc%Lw@(1 B|0z z2A&!2c(a6pl+B*F(`+YOQU=^ijYz_~QP54q|9uPHzI&l+pbO*jg$4i>%95u)300n_ z?S8xH`ps_g#gNo6&>Wbih278x6NXMLq-|j9LpkdIBwBS!vL(IMS}zC`0o^?w-*qD( zB2cYoV~^CSq=^$CBP hiK%Ox~%EIJNv67Pa}w5)Qi%u}h%h$Cv7HHzYt zbY1tAqy|{!>P`ocrj4%_ci!-Vxh^=5(2Ap&StpKW3 z)T!0r`koKJy@+-Qvu|LJlq9?P4`A6*=oYxRk$O4tA>p&jV`B$Y3~&7%T{!(*IupG| zBc3F$tr1U8aokTUVia?DjtqW+A}*5cVoFmfGvMWJN_5 *3qsn_j#1=1@%gp<}u~>g8(q4SAX}Y&*SzyMS+aD#h*Tq(J*YHkeeI zz63L5 a =!6PTRg7 za~V>0ecY#jq1G*u!+e2YQxh7Z&Dt>bQK=$Bt391|BX`Q =Y7 z^!^Ihau f-3@$w_;I`eT5C3#zs%(qguFHbB@3xaxCR)c+CN|UDYwtfXbp60y zu4BURXu2r#G^n+tQGoEOQl4}9d9ez=cdIs?<$RFAD@&dINxwnBV=FsPDDJ4o_wJ%4 zkG*&w 4p^9Y%f(tH0zs{A~M)BoghgzHbR28uTc>PUUPqD z)6k&pA`;)~IG52(?;};m2^ny4$JqCdTOyVbnzl`Xmd^UISt=a2dkyMEprkE0cG2np z#b*x7MT(Q7{e8KW_}0MhMS_P_!I|-G0QWnST#$X&s(xY6#_iekBf$K5hfb;oK?a|6 zN9lLprkcl5L7vY!gNpNj_pIZl^vM13i0m9}1!_zvI43wfX6#J@l?C&}S{`p$N*E=; zol{Z2OCeiB$V(jxGhMWM2sPSZT%NkyOKyrmy_e|g%b*OLuYVn!;6E{*pd-IkC%TBQ zl@9QAIRCVFBtt9MlN&^! e$E{Kn z!E-7Tasd!Nhs})L%;& T)`HDndU&dxe(l;(|4zFa;G#GHn916pR!fn|Cx+)Pv{=_P9_yaFFc`h1h&t%# zcv&tE?>1b0<$Q5WW*BweNF0<_Pv ^IccCa6MD{KjO#W} zyE$8D(asJAQJg4Hqeo7l))|!Ox8hU?AcwDeBu3{547K@Nl0+YLl*AA3QyfHGUUUKM z|CkZNzUCf*{sn^P_ga*hAtvZtg3D(2M_!mP6KBH@8suMbY$H-b;qKaY-EHI#T3jp- zQUD2f|KH~dIGD$FnA4j(CRG6MhZPKw@#WIvqCIpeZKj0I1$WJx#YOxQQ0rRDKHi*d zW~BQZ>ciakPo5X_bj?1wHteXR$;M2O)PfSSw?{6eHEo8P0oE&y8%!^W8ifls!HF f~)YP3+1B~cs-^oDo5w(B4xmEja zl-W2Uw}*GCi4Vk``sJ rq|s9=4N8YsJ6a!G7|Bj=quXL0Q-c8(#vHJ|9mz=LP7*U zUA`CbNzN5Hjt7+Va+A$|zZVk}#o0Zm-E~|QGag9ogLzy9wQs>ZMV+_NpSar0uR$_i zsPnLyfj*YhF0SVN?hpSNXUEQgu5nZNvahWPMIA%QDsd_ygCzX=gyK#{1Mhn5HPR40 z9?&*(eB$&^{6Wp$KDY_J90P4MgD ?`V_}PRKnq*Yg;3(h0|}7S1vB!FB{3tx2IKi-R-ZRJV-c8#bN^}S;Gy5ab#~9D zR`_n}v7Fn)qJbl>Sbf$u ?b{>$b;G>@cXaJ}OjCmK%c!kkGTrHVT0s+eCaH*U z`DSjyuTeawXNAe^mK=8e#ARrQ^*DtGG=K=Fu8R*(R|>C@tDBx >7~nwJd3?4ng?{fS<^cD=j=;WJ{f63;NU-BW=ebmCm2rrXerVpw GegRis4u@2Dx050hb!V88aS0tE `~KNKCDa8FjCbxL1kKZ;K#ep)@lO;Q^G{eFnDJmmgw(jVnpD@$vgS_q} z?ih}mOKqAaM6l&!2Qe$BA0a7pF_iV`Z*Ry`-HQx}-`bEuQ0$31{Q$QtzFHsg&SC}y zYPFw4u^Yev;Ff9K#&s%Og&%$H0dCH5U*zechhbi!W9}qszvQE%oO%ju?i>9cyk)7+ z-7H_DyeD7|_lALzM>aN)! EaYwmM5Vhn+0K1&VfW^W7XD?5e7 M;W<{P2Y(VS)5M@==w)S z#^0Cd@qzaYazE