From 1963ba3bac1d09b3de5bf3b24ce5997aeb688a80 Mon Sep 17 00:00:00 2001 From: pc Date: Thu, 23 Apr 2026 14:10:56 +0800 Subject: [PATCH] init wifi template project --- .gitignore | 5 + .pre-commit-config.yaml | 24 ++ .well-known/schema.json | 78 +++++ README | 1 + conftest.py | 120 +++++++ core/metric_manager.py | 82 +++++ glrocky_sdk-0.6.1-py3-none-any.whl | Bin 0 -> 58534 bytes justfile | 62 ++++ pyproject.toml | 80 +++++ scenarios/tc_generator.py | 477 ++++++++++++++++++++++++++ scenarios/test_cases.yaml | 15 + uv.lock | 523 +++++++++++++++++++++++++++++ 12 files changed, 1467 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .well-known/schema.json create mode 100644 README create mode 100644 conftest.py create mode 100644 core/metric_manager.py create mode 100644 glrocky_sdk-0.6.1-py3-none-any.whl create mode 100644 justfile create mode 100644 pyproject.toml create mode 100644 scenarios/tc_generator.py create mode 100644 scenarios/test_cases.yaml create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6950c51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +doc.xlsx +.env +logs/ +.config.json \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..10772c9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +exclude: "^assets|^test/" +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.11 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + - repo: https://github.com/asottile/pyupgrade + rev: v3.20.0 + hooks: + - id: pyupgrade + args: [--py312-plus] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + exclude: "^helm/" + args: [--unsafe] + - id: check-merge-conflict diff --git a/.well-known/schema.json b/.well-known/schema.json new file mode 100644 index 0000000..095358b --- /dev/null +++ b/.well-known/schema.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": "2.0.0", + "metadata": { + "name": "gztel-wifi-e2e", + "uuid": "157f58111-aaa9-4636-b476-20251201gztele2e", + "description": "广州电信WIFI测试-e2e", + "remoteUrl": "https://gitea.glrocky.cn/aitest-scripts/gztel-wifi-e2e.git", + "branch": "master", + "tags": { + "osName": "android", + "osVersion": "16", + "brand": "Xiaomi", + "model": "2509FPN0BC", + "manufacturer": "Xiaomi" + } + }, + "requirements": { + "devices": [] + }, + "cases": [ + { + "name": "TC-0102-audio", + "description": "通话实时翻译", + "supportedMetrics": [], + "materialParams": [ + { + "name": "paramGroupUuid", + "description": "UUID", + "type": "text", + "isArray": true + }, + { + "name": "inputTextList", + "description": "对话列表,支持多轮对话", + "type": "text", + "isArray": true + }, + { + "name": "prompt", + "description": "提示词", + "type": "text", + "isArray": true + } + ], + "parameters": [] + }, + { + "name": "TC-0201-img", + "description": "日程管理", + "supportedMetrics": [], + "materialParams": [ + { + "name": "paramGroupUuid", + "description": "UUID", + "type": "text", + "isArray": true + }, + { + "name": "inputTextList", + "description": "对话列表,支持多轮对话", + "type": "text", + "isArray": true + }, + { + "name": "prompt", + "description": "提示词", + "type": "text", + "isArray": true + } + ], + "parameters": [] + } + ], + "commands": { + "generateSchema": "uv run --reinstall-package glrocky-sdk pytest --co --generate-schema --continue-on-collection-errors", + "startTest": "uv run pytest" + } +} \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..eb509a8 --- /dev/null +++ b/README @@ -0,0 +1 @@ +Honor \ No newline at end of file diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..ca4ccb2 --- /dev/null +++ b/conftest.py @@ -0,0 +1,120 @@ +from typing import Callable, Literal +from glrocky.framework.schemas import ( + TestSessionRuntime, + Device as _Device, + TestCaseExecutionState, + TestCaseRuntime, +) +from glrocky.framework.const_stash_keys import CASE_RUNTIME_CONFIG_KEY +import pytest +from glrocky.framework.schemas import TestCaseMertic as MetricItem +from glrocky.framework.schemas import TestCaseMetrics as MetricPacked +from glrocky.core.logger import core_logger + + +@pytest.fixture(scope="function") +def device_info( + request: pytest.FixtureRequest, +) -> _Device: + runtime: TestSessionRuntime | None = getattr(request.config, "project", None) + if runtime is None: + raise RuntimeError("unreachable!!!!!") + ret = runtime.devices[0] if runtime.devices else None + if not ret: + raise RuntimeError("no device found") + return ret + + +@pytest.fixture(scope="function", autouse=True) +def metric(record_metric: Callable[[MetricPacked], None]): + """usage: + def test_g(metric): + metric.span() # optional + metric.add() + metric.add() + metric.send_all() # optianl + """ + + class M: + def __init__(self): + self.data: list[tuple[str, str, int, list[MetricItem]]] = [] + self.span() + + def add( + self, + name: str, + value: float | int | str, + label: str | None = None, + type: Literal["number", "image", "video", "text"] = "text", + ) -> None: + metric = MetricItem( + name=name, value=value, label=label or name, unit=None, type=type + ) + self.data[-1][-1].append(metric) + + def span( + self, + material_type: str = "default", + material_id: str = "default", + material_iteration: int = 1, + ): + # DO NOT ADD DUPLICATE ITEM!! + if len(self.data): + last_span = self.data[-1] + if all( + ( + last_span[0] == material_type, + last_span[1] == material_id, + last_span[2] == material_iteration, + ) + ): + core_logger.debug( + f"SKIP DUPLICATE SPAN:{material_type=},{material_id=},{material_iteration=}" + ) + return + + self.data.append((material_type, material_id, material_iteration, [])) + core_logger.info( + f"added new span for metric:{material_type=},{material_id=},{material_iteration=}" + ) + + def send_all( + self, + ): + if len(self.data) < 1: + core_logger.debug("no materics to send") + return # no span at all,skip + + data = self.data[::-1] + self.data.clear() # TODO: not safe,to be improved + core_logger.info(f"{len(data)} metrics for send.") + while True: + if len(data) == 0: + break + item = data.pop() + material_type, material_id, material_iteration, metrics = ( + item[0], + item[1], + item[2], + item[3], + ) + if len(metrics) == 0: + core_logger.info("skip empty metrics") + continue + for_send = MetricPacked( + type=material_type, + material=material_id, + iteration=material_iteration, + metrics=metrics, + ) + core_logger.info(f"rebuild metric:{for_send=}") + core_logger.info("sending metrics to executor...") + record_metric(for_send) + core_logger.info("sent") + + _m = M() + yield _m + try: + _m.send_all() + except Exception as e: + raise RuntimeError(f"Cloud not send metric to server,Detail: {e}") diff --git a/core/metric_manager.py b/core/metric_manager.py new file mode 100644 index 0000000..751692d --- /dev/null +++ b/core/metric_manager.py @@ -0,0 +1,82 @@ +from pathlib import Path + + +class MetricManager: + def __init__(self, metric): + self.metric = metric + + def new_span(self, m_type: str = "default", m_id: str = "default", m_iter: int = 1): + self.metric.span(m_type, m_id, m_iter) + + def _smart_name_label(self, name: str, label: str | None) -> tuple[str, str]: + """retruns: (name,label)""" + if not name: + raise ValueError() + if not name.strip(): + raise ValueError("Empty name") + + if label: + return name, label + for sep in ("|", ":"): + if sep in name: + _n, _l = name.split(sep, maxsplit=1) + return (_n, _l) + return name, name + + def add_text_metric(self, name: str, value: str, label: str | None = None): + _n, _l = self._smart_name_label(name, label) + self.metric.add( + name=_n, + label=_l, + value=value, + type="text", + ) + + def add_number_metric(self, name: str, value: float, label: str | None = None): + _n, _l = self._smart_name_label(name, label) + self.metric.add( + name=_n, + label=_l, + value=value, + type="number", + ) + + def add_image_metric(self, name: str, value: Path | str, label: str | None = None): + _n, _l = self._smart_name_label(name, label) + self.metric.add( + name=_n, + label=_l, + value=str(value), + type="image", + ) + + def add_video_metric(self, name: str, value: Path | str, label: str | None = None): + _n, _l = self._smart_name_label(name, label) + self.metric.add( + name=_n, + label=_l, + value=value, + type="video", + ) + + def send(self): + self.metric.send_all() + + def from_dict(self, the_dict: dict): + if not the_dict: + raise ValueError("dict is null") + for k, v in the_dict.items(): + if isinstance(v, (int, float)): + self.add_number_metric(name=k, value=v) + elif isinstance(v, str): + self.add_text_metric(name=k, value=v) + elif isinstance(v, Path): + if v.suffix.lower() in (".jpg", ".png", ".gif", ".bmp"): + self.add_image_metric(name=k, value=str(v.resolve())) + elif v.suffix.lower() in (".mp4", ".mkv"): + self.add_video_metric(name=k, value=str(v.resolve())) + else: + self.add_text_metric(name=k, value=str(v.resolve())) + + else: + raise NotImplementedError(f"{type(v)} not supported") diff --git a/glrocky_sdk-0.6.1-py3-none-any.whl b/glrocky_sdk-0.6.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..9348d957747bf72631a3a4dff99a62f53d77d1c6 GIT binary patch literal 58534 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°S8*}=R=+n{L&LgIR&P}z7_a}SEVpR0$IhEf;z7)6#_5 zXAVn@_6=O4XFFx7V4Oj8%HgpR!qaf*Ju7#8h|R^ZpECcSGxB`93!2jxOsN zwDb^se_XhwB~5MNQzWMfm~J~pC>}w zqpvpI&pEPk?Yg(Q>Tuc-uJdfGcEn=7Xqs)zW^oVL9M9ec=Tf~udd;0~uJ$kPk{etX z{j5VTkk>3IejCpzrZ}E%Z$)oW_NR`>$+eSI3if1swcjo`Qz_;;!3G zhu)Jl{u%^v)iUmWT+jRoVN7WL`C*OChRq@cnCGEoiTDg%p8}a#Hstm-Uy!!%j_b#( z-RfNu#a*HpleyImS9Rws7k-U4D3yMqG+kfC?~jw3gN?4lkW(0HYpHGUT$zPApy28K zK@EH-4YDcUs#S>cv+jK5y%=JfLVL?~+z}fRJah=~?FXR-J~GbW!jc7R_807bcO_CH zl0fDk2;%rNTEhD8uH@)sYo}}M^1q98?}SO4z<&e-UctjqMJe!QNmmV*1=HIOQvfPq z;L*~BU*4EPZXJ&^vF*p(n3>VT(k{L3h+Uy;Spp4_hz>#Q^Sz+na!%zHb;2Dr3yjxT}m^C^Jhc z&4RG+p^QG;may5OjOmrU$@aTws5+^LcE1EQCPx=g;r_Twky5`oR|UMT9dhghgzaH# zrGRy!DvZqwyhdft^t)=uqX8SR`HOLV#nMM&k&!)>QRwUpqt4iG<2De-O0OBY6@!CCWU==h0_21eNM@3`@+7TdDu&-xlNFOWZ5J!7>- zW0sAkfq30KKhrruN9^nR^t^-q+t;AiQ4q&JKK}ag_ka4@$k@f)(Abgg|9$?01LFIC z#2GOEh!*A*fimGa&{!diLI+OrJOK`dCg_V?>JM|?Zk&(UC9f8T8;W$_f-WA9fCI@0 ztz4QPkcV(q z+rvP6?4W}EK5E^85i*&&lJ1{$%vmp{9aB0AbS-k>Hqi^2Y=}M%eaG7RU9T3UAcWy@@M;S0iU2XeU#q~A&zV9CSO zkWFb;Jq~K|F#FP5C;Gge3Tx|WQ9x)68TK;r0Wrf3=Lg2&>UhNEK`w7ig8p9Eh$Av< zV=9aGE(e^IjO(hOPg*Sz_UqZ_GF+>Lc1X8h`ON4YB=cuwYs0Tf34di|N#N3<; zg7=RX$ud2d4N7WdeM2E;9&*NJ*kmH5-G4SS#YtrS7V^?}oHT;po#W%tLQFdoGYGKL zFk0RWSDQNGb)B>>fxvMGNl91;eD~X&dL8A$rCSlWfhiQRPr=UE(?PUP5&@0#JxNC8 z98%gCv26((0j3Ox11tde{FXN(2z=WSx872KRphDmnlBWED2HX{pGcFFmF(1EmmW*4v;gHhZ8SH<*R1hrnHX zf>F}jEtLtqTQMRfyr4ky5ncvz3G^`8(8+IiaPVdDF(MJP#)i=A^4Nj-oRD&|PYT2A zjRi0bqQTJOp37MxkxcL;1Bn<>GOr16g3Q-2W^cSMVSeKZ{hA_1DgbV3?evA;vi7;U zj=_UB47xHqjubt)ejKhnpL`r%d3+sxT>krZ-cczzvv*C!r8R;NSr~jpY)3(Fnpc>M%M5?`dIUL|gh8vhA5=eW0deb;4~F*r z1&cJMtt!t!NBj`~-|4|1mjFNd!yhhxv`$3-M|wCKIv5+<{G`VZq4?hf8CKh}Ib=rZ z$t|rL7&aMV?u4*sGKiDd2$q}iAs;cUCU_^DsOV-hFICGb;}jmr(qwR ziA6Rw=@bi-=uBT7&7J+0UF|?oK+gZ&j$*0s%R{}6L*L~UM@Mwowt3S`h1r%!TYjs{ z`luL%Lb*a`UQ#?U>Na?I*-F|{PFYUlZStj)*IPC*x#)FFfpiV0R)yuSJ}Y0mf>>uK z`R>s&t3+^-JRur1rKO-&L}jrs;(H~MQzofaW7DrsPWcDV_;~|ntYVP@ly)i7Vl4%L z@feWVH6p99kwrxR-YnE4GD}WU3CTMu4&NPrLwCWSB;ccHw-b!KC9`4g!MOOF)b8(B+aF{+2c=JRdV83CtFh zDv|QZ#8m?CTgD4kJ07Kk2+V)g(|&IetvDAmJ4_fJH-M2*NDe{5!NooF#esPzbr5u} zU+t%xSwN1;r;ANZohy$RndvUUP~8WmVZ;nibh!*dPJ!7&RNUU&e0L)q5(GHUtrZ6U zh!(Oa`IvH``S1_@Z!s5M*xPILZ5V>^T*X%pgG=>9P0E%|0GHaW&KMReN!rUF~mZUSzV+f1OSlq|z}+NYMa0Zu_`1)J^WEvDK#h-Oc` z0xwisGI)Ck=n8T{5A+XeYvKtsVU6dI3Tskxynz3NA=@4!mjU$cObxU1@ZCcCOIbZM zo|GlwmjpJ^2l{^f_Q;)fc~PX>mHInucQ3#!+!I}vvjba3Z^})vN;g+Kdqzl5v2rHG zW4`bZfhqzI)=0^TW2E9%CG?(_mw(_jxClr#6C4ziDYURVl<>~{qAC<51_LR{c*69q zfH65FNpGhgWOkHyGVyWu*$;L-rN6F97+03=+qA5Sxt94^7;u8M741Wi<-;X zG-JIW0@CeY|5aaEu$H*dI~SUil981L4rBMyZ%`(CUB@$r+jYfwRgSQ1k;zlFD82Cv zaxveXEueXjF^jWB!{+Kq;)0Ub()yU8m$vH3Q^%#tzp*oXW`SpVN((GB+`a~q&H~ev zU+7tzlbRRb5nN;9;@ZjP<$(Kg^;0h|nwD2(km+5}_nh;Pn+F2Wq{MFSJ{~Y~*b#`R z4m5W@DShqdVgN!-k{8njEKPCGkNW=@-}or(#fx@z>||!dKx?9k0cC>*jt4QzK7Au_HyuaVm%8P)|LV9ErciOeA{f=#{#RN56V)-U~dYU zKrtX2+&{Lb`ff5Y-A$K`FJKf1KiZ}L$EIRS; z`I$|#EzDF|M~eKe3LqQVWQHD*QR2K~2R2HSb7k8AE-%BCryr2ake+hOOW-=Uc2UCY zF!_iw{|nsR>&n4?_g_-Y23+zH(VzAw_jBX^M;kP8(6=^rwRN!k&$a#J|8D5X%c#X^ zXyxVPRK_Ohq?#1$m>B2}_a**kr>{5`R5mV6Jwbc4{~v346(DGeQ*v^F2dZHQ!2jCB ztEE-~68_Mopr1`V{{Q&}`afzsCtX8*2csV>pRJ-Txgm$qGg}L>79D`nI>}F$W#}4H z4{vKUX6~@W&)boOEU+r#Y~j4#JUB5?mKnScwUIG_gUofFYtH*d=aP`+BAr>(AxO7= z=sxq_GkecF9#KPKKhxqbDj1)eGs)jfTrT@~a@GAmFgfc*+)l2*D%-WR=eL7P(2k zmpdBO4!<^3aj>F;j+}6bP2DTjy)?&v3;^&BkPj`&8Hz0Jx9u`~ko~LdR6U76;_vJ4@j2j7~RPYl3IXmfd*L z&zVYMGuW`5XZo6tZmlqWtPWPRsT*~_V7;6NFw3GvkG^#_aSb*)w&5)js zp`CNYKe@PGTfSDgZB-5D6U$)#aDy0TZodEHsf3!*L_bjoPXmkJEoo0E?_e34&juwy z5!Ws(f!#X)m3N87V9CuA~d0wa!=>OWdX?KU8NZ$+7G@CEd5 zm@B25chvQhRS-W+ko5n|F2f%=wv(=-lfI*wuBEa2Kho^zcme1DdW?|kcT`|H=UaRy z1Z@TqRJQyu460vHGvV)?FV~aQ>mrvQytAXgy475Zz*k^$Xb6;^h4r@A$qNhn9$2rI zP{qoxg~1wBLEPMv@^YLB1gRJ$Rn^k(BM7CU320hFhf%J%aJl|X%2gqg(`iQ?Yec^> z3eAbkn1MA;L}v!G=xGc5z?t%}IWTuNs1g(}(P*AiqvLn?50mDzs^-aIFc7JU_XOka z6LJ&L$JIy%nAa8uzn_r*ZGNfxh8^3_d?5bE{QsO0{Qu?6THnF)ADg}9FaMbleB_oQ zZ0DkNaQEre@$~6^kJ8r^G8274v@FiI<{GsIrqua`&#SwZBEOQB08JL_(Q2=#n>vKR zCmww)0QyRlyQ%?42La2Gr~NiWlmIcLxzjw@Y{uIVa#%5MYd_0WK*CiG*)=5!jcR1Zhbol_(|??eF$p4;Ig(=N9}8X0p+EsnqxTc_ z9ZDk8=#w!!-rV_RQFAvncD^h~hvq@?sn2Ya$E>nx$nY~rqD=X717Ke)&THRfoe+(9 z2EC`qau?c|P_2~@sctVy|ccaazpW}J`Zz>xtXRX%DD^gV@dX3JO+hs$e zy$&GWE4X@2-I9wrwHj4B4fq%qWFeb5upu+8{+=oL5{Oau3o|+YKSz1?yU|F0 zI{&GD?GuACDvq|*AZK84IOw(<=K1_1t!@^Fx2032^U|5BTwWow91(6o1Z<&^F?h9E z{BnlDCNLhLSpL>eJCyX1O8Qw8oWZkt0%NSA(U$vqbFCOkdd7&mIpAHLSwrIeDD@)} z_meiQ-zB;Au;R z9EMl`RuIex0?3Q9{T+-B98iCM-;O7kKQ>S{9zoMFk8O0mGh>#jh)3bJQQHJ508yI0 zMJQ~;lupOjBVG|gF$$6p6Zdf)Zommh^sr&a39$7H5N6XRF{CJW`gV!Vm8U#Xp ztZ4U#pDTh7Y5BGC@nf9(Ra7F(K)zU|nq>F=_KdKYR`8`ENwGrYk(!jKXfYL{F-{Hg z`s+@Ls3+s{lMAFZ!y2$mcJ?VcY?lSw%FTi8Lr7h8>(Mcp5BCQnVxh=OyDA8<~~Q2B;Lq zt}tP>OQs(+Ra&YvgiCqVva$|&EwMn&G5y+%vC#I)(`^;kO3BIl976S;L(k4w$E?sz zOy10JL;z#3K(c_cWS()UAFMyFLrawJ0lk!*tneEEdAAJvo9pjmv-{fSNmn^$>*4Y_?oT|w&i3-O{TkiD;dy(WUvZ7S=Hvc4`ZJ#E z!}7f{J^IYY-TwG@-j%I#Ns!4EJcX*;MiH8r))b(lE$)P{+(8MVq(l#4wIrl z0~Tj3yIR%t!l+Sd?6RZ9BKwFljJNlo2NtuImNO{~406Y0);};BRRqSr@T_D#~nEg{_PgD|uddxSA zQ(n*#PZDIG9VW`KDGh&7Zn%%DHH4bV#oA=gG}g~7`vTy-Vbm~eSd$PnwK5na6!j>I zIuroD`VQvONE;CRiU5jjO&Mt&p^aS4$mtK!+Wo)r?EI}Mar;vYh~yYlS%&l_wY1P= zOtH!<);X;vNwAc2l5Ys0+Ly+yMAzVc6-KnQPnfoK@kRjT-d#ZDA zV+gLtCw+?1UAQI?8&xRk8q?E5L9~B6s{3bgbt_iy=43e5F~iL4%+$eV(R%(-Pk0%{X3S{ii06vhr?OOGP4U!kpoSPGYc>}tD( zVfhB6{)iULr@ zrU$8G>TG2%cYvp55n6(Bmj_w5D9VvQO;81gw!2+OHw4}$?ax~SwRN_Et;Tr@ z<_&Aw4DwI+3LKr(Qu0oEwo}-RER>i8K-Fa@eQ}}g$_tkUw z=4GG&)(M0OA(qvUUzQTW1F8KpU3oXEJ+Z#P;RL91&Vw5J7#ZIVyI_@6p7+Z!We^Jk z|H>UNSRM;Duf*RBG9M``MXT1B*E=Vh2{r!8euRI#ptmF@TXGYT2Pkmu9OnSVLS zM}_y#7N4lqCa|y%$;%N1yX3)I+Q7PG&8u4Pr!*C&?ZoI2_f;7*+cLB_2DA20Jlfa? z;dHz~15yF~wwBNQC8IE24B1X?>uS)V6{nS)4PCy9(h5oA9p%w%Sjr`j=hlW)}!I zutvQ{+~dfPGVX{xrJ(z8#k4E0y}~L1$}->4{uheQ?i>D4AyH&~A8JnInKCTo^`=04 z)FZ{3_Y*DEO7@kuw1>u7R04m=Oh)6a9RR-Bor|oaZRV={FKr8BlgNPOZ%_y?q+0e} zv+yV9kYetQD>KzLRu%WFFs3c7NXIoe=E|pj|EIM*b@KP&zvbW;W$tw}pXO92e+Ebw zG+in;J(7D*kaTtY1?-0+o!U)?dTR#Gk>)>y_348nINvr`)W5lG^sv;Xcu{W2 z&nDswGy6kHGwNfEd{l~#_I^IR1lq>pd4Ih;b;@q{+#Ws&VI1EdkW3hUK!p?U^}K|c zhjpWPR@RWcHfdL@*ffhEC{q`F$XJ*aVfrw|0p3rLTm%TRqv$p(4RTS3g@&|wp@Ceo zEs&$IQU1OqUH$&Afms$7!6Dhtu*~&mT88s~lC1yzb@d-sJFIRkyQzZq#oIgJz?T#b zHZ*sc8gD|%Dy}Lf9i-8*07W<%1?zlXZ+dBowWLG@X$uHOP6$1nV11I{#~MtSX*FO1 z_QK?7JzM$_%(Jrc^x_;s63r<{-~Qb7y6KpEk8@1?n^IK?mLb`k{u!Jgs z@zffYG9to_1d8Lx5i%T$uG1jd++Q_L=?>(3=zO0=8R75YprqCZKC- zctDxIRBUY~jSH%ce6%qC_{r|i*Dvmmv*}KgEGNX<0LNY23$;geZoW|6 z+P!AgxODOQG*{yMSS>vxanZuP1}Rxaw*J((3TgCkm3MBw17q!|LtHxCG^W-SxdHj53fdWEtycfh;@&{=TGf&}azZ|XY4$shq0&^XSeQ|pxw6Xsm z#@;E)wr<3+X@EyY;|m5NlnqYL!j+JiRBcCx}Y?dqpS)FFTM9JCyT-p9&ybL zNngwJ3jEG5;<$eXTW1pQ^quGuN%F$*-C?tQrJV^r*bVtKR!gLc7Gt~kdpT{oeI-|v^(tRMvD$ku+9Amhz)7K52zSAxfnf20l zqKVDF-dfM4p6k?APLL|UE^YGaa(2Tjdb(w0yBo73-c2+sQOXelIm?tT;>Rj}Yz+`k zaatcYb(16BUN0z(nl6r9t|nJEL>*KjB@{idS-~dnUU}?V@#VadvlSyhs{jG4wLzqc zZSKz(IgjVgh+#jeS~XAH>4rUb8vLr=Bn7c-nS>}n3uuye6^|$YMA2nQlTm>k?{s6| zt$ok`z)s02zBCj^B}zw1RQS0WyEs1PzD~y zZatZV*}CIq%NOPjw}U^Ky_cj+vj$1eDK*-zVIb%rRPlwH0->7nCLwS6tUB2*Kpd3tEsA^!(Wu`>d$9Dg zkuv>VQQ)YG(Su77G>RKA38Z0V0{6sKEM5$M?dw;|?r;d)V#3UAyW+P>iD_9zJB7VE zvFyl`926J{Q>RF7;L2Pcwy2U2*Dx!`=tXtGM;rSd5Tk>FegDdxwv6{|Rb=XdT(Kl{ zV(!DxUM?!XW$SoF`|I>^55Me2KNFP$HH6*J4P}r^PYxBvGwx)e20NHPJg#eXOr1Yf zVDV+T$^ygJR3LE2J?hVG|Q&$7+J=$|qvz)sM6ovPJ>Vc+ncMIMBS$ldlZ1|#= zg71{b?|9ohHe+Xw<=6)oOs2;HS&Rii+;PSOFL809A^py-rakno%Er83wVh`3934;i zLM~ppf{l1R6>6F{v~V2X;)Uq5H*GH{+YToMkwGQIU@R0!JsT73Gyw>3+iG_PkcYAm zKbc5cXFzUutX#UGcQ;7*N_q;`Y52mxx7%$)J~4WEK;O{l4_iMlKkk9Ag)cL0^K=>! z0frNR8Iah?hzChjl27b>Ir=-RS{(MVXE9HFYo5NvPe!2R&`dDHz$NxaLe&{PKN@3O zWxFWcOG6#Y>+?jT8DS)28q zs0Iy!2HDfPe5t5lX1I?i`GQM61ytCncENHH#b&j(%lFX_VYTvgb{qCj9|>V#1W5J2 z6iFxm0P_E~!7{V-aB+3|VGNB89sZB8MD5=>A?NC~ccOY6l??*k3~Eu$Wl6AWB?4@m zD8C39=0?nDBdG9KTem)*;Uq^>Xgch1ZPJ;yC)nk=QVU4RD4--*(US`5gR6f??P>jSH-0@b(uvdEv~+~HgJCzC32udD6t z6Y-C_vzQC#Sby8Z`2iHyqy>2^Tk7@k3;2Q*GlVDgG$CM%N~_hOj-aWe3cfclS`a`4 z*?(Y9zDBVj5hX`r?J=+?ODk{?PFNVC3@leFFc6|5MfM9-&e(ClF^x!$S$%UuLGU5@ zF2z)%`^=tvQK+d`HlM*-aXPc<+JrOBHTHU>f++7zBl{z8 z_#P@JN#tg(M))M8RSGaynJzFigHF9#3H513>emS~ym2e`uB0El)~WU-CwWKGxeTGk z5B2Iu5f;v%>r%hw(G`eoAzIMJ3#nF#34D;{89`evVU$*)?n#@a+! z-K#h>#Y#m3a2xsL>D(_IVdAneQ4yjtX@}etkV2 z?Ot#gFF+yX$iAInY#bG}qrxm&6Gn>n;{sH$-vky~8vynh2;`ynegf-33)L26d~l$SX@gqRN&qEEimXN$rJ|vYI}y$8^dw zF;&djIJtbdR7X!uMW3&EKXUXznbUrdyKKPD^6>Te1q3*3yFZ<}Ko>b`X#p(guOR-?_QDsj12S6|hZV-y+0( z#?Q*kAUC*Y zx3hlD_1_2n)DR!Cs#3}sy%ol2_J;Tm_i+58Qx^FtXq>!OgG1$^aQchrtPN$ISDn>e zh)k$=TUH|PUIK99V=-G)x-}S+q`=v3*e7~p-0bnI6CJEh0IE*piJK38GK&R0)TZ8U zwr1!XqtE$YmRCu_5f0yuYB195CLi6X56H{s#IYWM>#HoWk|y?YIfGqM3%Rlq%az2- zZAdLRkryZWp%v=WsUb={!Y&wfx1Pmvdc(c{paI8LFk9|_bV8CJ>loqx@XDK+|6HtG zm1V8h1rWN<)nU$hm8?>7U;~?bDf0(FpMfKdQ$(>iGo?DtrrHO73uHRy|H<)|a6Toa zMR|)d+VEGjT&}0Zz7{ny+sb@4GiNS@rZ6v4P$MIVQo@F|7ok%&ELQV1ZiGzPbuvq^ zK`>$cRVPASpH-RX`~_#ekQbNJd2zpc-dmA0&&Th@$ie41l>595CU~>LTq~ouR@NJi zq`POBCabE%4!ut>2$N`o1gd}=eEmn&(|$D#*L!6qtV3|)-gI8dJJ4YTAZ?D4i9A|% zM$8+;)i4irC|1}kuhF)YrzTawRi&E20;*8;NhiK`Nkn#gNCI7GDzqG96CvWBK`$^w zim;OB7Xhjb@haL>{$5i~fm6yo3AzZj-*dir^|VK5vsrUV`E;;spI!slMHvB46>^nP zFw3Ovq8o7k$2WL41ICY-C1ofgc#F0*IX7R)*cwAV4=jxR0GE^wOUjg^={%Y#@{Q5O~J(#b`&J80tGe)n`M$Atb^UQP|M5<{Fin?) z>?WussO=7OWm=tz>fGnsku)}7SL2Br1qatmfBO$`#$plT5b{S=R{KeiBKgl1^Kat% zr)VaV`0W=MFovFep!VTXlY~_AKDa3%NDNFUHp|DHvf0rK2jH56Pr^e61Xj~9xEO<1 z;^SXf8V`*PhM#olxNgn+rRlC;KY8PFJCAJ1lLa>;0BsYCl<^qEp&YwS zA%DG$S*b(Pa{%M!IvxWnQjLy>39|~zKmks~>){W}Oip}co7bcT{Z z?hQ(?A7)r|S8*tGiLM9T%VI3n(sADRuk%PIzJFPG4(ey<36`f{Ou5^hz#OOF9IKl#aI zDiFSHeHTagVG4F~)-?(L zi63T^OrDN&g%|Tu0r+c3DO-t!pvW;`8yXt3KsfnA6w}3BC?O3w^Rcpm)-7ewXjws7 zauJRBOjzf;SkQ3|rNMgH{$jJ!^?hn{a}~g*%&^HhNgl)6{R)o&C)`69A9nbJt%NuQ zy$Rh6Sd=WBHthTMadYr}8vRcV9e?N7>*@RJFng;0-|XD3!N8_{Bbk2r7OQf0GXl8$ zY#^c1+6d+6a!5_k(-dBmzAYLtojgl#K1;Z&-D)o}YH14y>(V7Vk}m;{XeV)E5eY|; zwUj;j|Eikf6TuQ&{ge{bPbrc8=TfpYbTM_ZG_?6Crcf0@>jeRfu48qdqA*zpTps#L zjj?JJ%TO)Q2tt_A)QbfZvZ;v_FzwnLQ288Q3wphBf|n?*CI4NMl2JC@$TpG*pKIUi zE_*UPI#E#9B*kh?sLJxtg`3C}n!*f7z9c1yYqK^x7OG3la67A^C^&^Yxl)=1k4$#E z+;|m-V&vj7EfV`WMcvD(rzlOQ8>epvcl(qJp6%RgKG~c}J#S0NOnfiQBL&5n#95|Vfr#rAFy=Ny=_5Gros?m{mm9~?-?2<7 zXUc_aIRG~4Mx&9Tf^|XA5eWloXL!nEX*@Po8!3bJLe<90CYE#M2qn7QeWJexyhzES zcJShiSI}{U$9tW2gxpo8nwSk9oK8f+3^x|ReP@HH%9~(*j722IH%TE*!Zv^YBo4rP zBOGGB=xXsXz`5@pz^w@NH#Lv7(jc>!TJk@@qPklaxGr-<;|;cSmYbFt8clAI@!MvL zON~DX4BoOtUgyry@!Q2&a~HS8FJ|bDo6&apHupGWG6v}x_}ic;M4Y$o0}`Rv(>DgT zStj7|*wj9WSm+W@2Wgjtzw^1dLkGta!_ZD)C$0$Zw7%i;NY-t&W)Gq4D1bMgONL6# z0<9~GVlz2^pWP3i)AJiXY^jMCDisy3$Q;{%`1|{4>E`JBemA}OG6q)XDGgHa-LscP zgK5u^g$($W6W}bs8Ylmz3;{+jVMVm5-8#XR@a*^X0Ql>fK-}xW@POIgwRo6&ZjWEx zttapfJ*Fub`+J9B0XOJVx@mc~+PLh?8T|Vn(H&SCCpck0)mrg0f~5WbSFJx3y@S2e zzmhyI)nudA84$YO)l2zSNy8tkAR0q1kffAX;)LXdtmAN5n~Tm0Hx=Htx$7@3$Q%vw zAAZu$5Uh_TJ3Id*B#|^J;yM-p6LD2VpLY-IcGa}H6yOl&dQekcg(OlN|Can^+vRSc z8={~^Wr^DmyH2~_yA`{0*s+e<(YC!&#b3E{w|`{iJGoWEM@~GIqJ8p0_z5RX*A|LR z51g;*@fXy(?zI**P}W+dZGFB>li-*geprLrKR>GQo{6=~X zb9al<-q3MS8`@JzP?qbe2V#o}I3FQxqDMW>G;()&u0rFA%yTz-j!Pn4}4kRODMD6=HMTaEc)&9Zh_`puKF0mt{bz_SZ~b zMeUqMwTNWgDK9c+TTE6_dB78yQc)(ZmaYRM(^UgpV{XeE?-XrI3QcZpZ$7w1$OKK} zW97q8fQyWCKlc*GA?v-K3TWNbf#?8E&mSrUkC3^rLIk)fV6QTqa%V=DEuiBN;=SQn zGmSF4HXU_K$1`eJK(g{xP7vP}Ss@_jbT}iumluJ_ewB=n+=DSHGD*7Ed#b zh6Mr&?iRizE@8)fBSN%k_6A;ubUnGx{t22Ep;^BPIb3n0u`qRO9e}X8JfhMX`E5=` zq*{Z7f@ouA?5K!qhlH|+SO|5|avHKa$?T8rUn4&F<8oY<^{rf5cYjrc?~=}9G?H+i zvtpsL=w-FKO;t`q;ScBYaAGmXqrTH^Jb@;$MR_qcQRCf&9}ME-wte@bL4Wx6m)tgo zQC)8P>_-p<)R^J0(`mtcN(Wm2e49zsPu%JM*Y9r%OhWh6PbG8wG=N0^Lq(gq{3{aD zHLh3g|2CA(D^ut5^b|?vl?fvkNPxm+70hta1aS>Ik~}@ROu7vW>*w-+!_DJ}I`UDH zg5DsJvzUcvEs(jMT6CZMWs`BRLX>xnsxP*s)s`CX8=rCg7`>;YQpGPoX zQ{%hOBXnBqk-#HYQiuLL0NvDy&G6McmEN!cNKKf)BI~2obLe*@X;5RzEqdfvwX2Jg zfvrs3Nhs9Q7400;l?d^T&`zLmP1YQN6@R{!Wk1%$s-d9Wd?{x*1{eDS_w^5Xp=?)9 zyZ-0>CIklnp#INK$C9nGZneRH;QOpz>US4Z0ngCd7$OU4t0XZe zL^)`2cw3Dn?IiA`k#w|O5!DsTZi#jV{_1w*b~M3KO1j%_8>pS1oNv<~5uYt4J*BZ; zFAFWXFX(m>vL^S<2Ms;4M)I}ZT>82x;I6feNtJG--2=HcYT|`OgTcL~Fs93+jX6G1 z-5%VFv**{UslKdVV@FT7yttZLaUT`8_~-F?bm9hA!cAbCE7g3WhcplMgJh&&=LqDi zU0qGW1Jo6Gh@+j%Ah9#9X;$pQHZC5snww(2L_Khsg}kz*on2we2)?4|*DnHx7*|qZ zIn2K6El&!uloQVf7hL|Dk#+a5Unr1{=pOmD?f%L2fwF3@by zbRUoK&G!cQ3Kb5KzbI@|72)OLA%RVzyTlgKio1jie2$U*_G?^{K{_KgXqEMmxXiDW zOqceuu2f=?<%yd)CAk;6vpnWgk@%oQLm>0M$JbjX0?Hd!7?g1;s2(g%{Yhjoc8jJ^ z!bUm#&3LdUBYcf*`AZgy;O&42x!Js7q9H=o$hp$pnI?#E!5f5b=Bhet#k$gM5bcf1 z%~F!Z8xNsP5Cj5vllzdEe}&iHhDwq5G?#m?npR+bh3G7~#%@{S!9fgmN<W~ z6v03PHwazjq)PLAqJ*oAE#-fZ@WqtnNzw&F?uaTHa@_VHj)y_}S$EUwRb7S*GeZ&Q z7+~hT;(S7*8J5{VWT)IO*_@$5p&n2w4g_@v5I0{$+&Wp&S4-~ykE2b|JE4p8XYk|s zW10LPj<)}|J*-$o+J0W(Uoo*Xe4I*WdK{vWBB)9#giu!FfP{@}Uc;kE>(*||wgnX6 zet>`U5XeEq;gUJ}rYZM^^bMBIUlNK?;qmu8@V0sWrbi^pNKQ?#ARdbpwW<8#>6f35 zsxnKYz??)gBtZ>{$dVDLsv&Xt)Cq@pcRDP&%w{d<;9$zo!{>-6o0sV z8MI~K$`Bf}x@MmF@wjE7q$5&(gf3HB(3&*6RkFPHz)nDUq1JgZv*Rxl!s<0_@Vuf>CxTG=}D$R>Dj}XFoADB+|flau5KhQAKhzUix zC8J8wIny0sawKDkr|Qw5ECcH9!8DxxolWjA&G$FB0|CcGwPl3tH}tx52aX^t`uliH zd_Ikg9)^{>Rg=Ea$tP_gk0BffAX!B@Bg`r}nFMp?hOp9FDDMQA5XF2NZk17@{i?-+ zwf6RJwh#kJYX{>C{z;9q6OARw>XUkGJW$pxDw+Ftzrj{T7=eINq^$T}bx!F=0{fcd8X(kC%9EQ*Ob{z7yEG6{JD%+_0za(Khln(!a3m z$ErWKwr%ukpaVi^C^}3U&Pv>VZ^puQQq$(r@zR~eYw8+{+WFHSyo zWXdn|cl&)kUUum+f;S+<-*7EuYmR*QCwaW@4yVR(E5E}2*MpZ+)y$L`8UWzxXUfX_ z-ySvp#)X~fe;gpDPCs`a{eMf_`u`6`+=QhYhcbNY>py7Xk?xwpc&pqvCrTKiApw?+ zWJIY|Bj(H8r8Ebhs{9TA;;lfd79g)9F7s{S}E z^Xx<8K7t8p_ot)0z5Q={xtEwEPhI`$Z&BkY*%HS@%|C^j(_T@T~H7rx8ss)+nw3ZK}w`9KKRRuZ z905oloLwb}Kqd`4r2%$>E`u=b(TgNRD^@-;&yK}UWNGpne10Q~u@I`~(1drMk?uC` zMV?7G^rSILKjx%_73W3JV=>yVP8$Xf(`B@(8a*8&u^|p^>z%=YKQTRGpOQ&>L2@`D zb~Fpt#OLGpxhn#2Mznby4V%W}4H*ThB?(@tUSWRvY&XqRk+NYc zM{qcW^Uyf6sN6{+Wp)P0wFQG7QpVyWi^(x1$96-PHB}kzBhS~&5gD7+`5nf5hLz_s z{AYfGYy3M8B@nAh8JR6o$9!r;@9Xydu=^U%?{)dmyk(O1xk^`|T)4xzLr1*6j(gc9 z7*W*Lj3~QYkaHT(_vf)9#zq;zxC8!8*n+S|*ZIMl3V)|B8 zJ(_lOeaBptne~7G9D0+wR8Y{+qK8@z%nc-AP%Kl8J8+ZRsIkHrAr#;KIdY66YU8o;|+5Etj1AOdoqB{tfOL;U5)dKj^)6J`4>_H z2b}HSz46ACR=9$WJCagK>DcF}wB$Cg=sB1`XGWB>WDPfXnC^=ENExccnlY`(4RALh zaz`=$>=2!>GnM_ya7y~HHZ1ZOkl685?dm*@gEUzmI}(yR_H!8_4Fb98x+Gx4CF-qT0 zEF8A5cX@PupO=rX_i=x}JaFI9Sj;M!Hup;jR5ARvYvFHgx1jNOm}IjV?>d+9Jhw;Gn zb`~9}I+oQiag$Y{uru_&(UkWGY;;vfEtpdBf8o0r%ti;QL}4zBLUupq}C@yA68 zO(DzA(Bo50y8++D=uG~<_u41BKO35pjJ!x8P zSG~Vt1Pr7h<=~U3oab?0-LzwLLoDtFYX)|cY+=vcSC>Kwux@bqtV++OV z+Z$Tu?!%fy&^akQnEJY1Z}OkC=2u|REPU6S>{H4d9haUGF9V=7! zJ6uqV0T4?Zwih;2Sy`Z%lL?P`3Kgfz$V7PK`e(GnT`|ahge?LcDb&O8Lz z5E<5^N-Y_%{lyOS2$GU7k`1zk-`h!*13yA{;%}4CmFUie{q|d)j#aEh;*gPLzG{Nw zrrDu~BFbOQGN`gU1$z&XXtf-yZ{?hhQ#?XY8V+k`ym8T#emH@lhki#39pqafB-)*L z$GupZH~k!-LQX?cWky4pieE+Up-`Q)_9S8 zGuEcPKh+GYPc<77O)TG!jmhOvKw)gn883XVu!(=f1^s+Z!V1ql1uwjF+SZ_;tnZ#? zEJ@~F1$S-EI-|+aI(dj|F8x)CIwbmox;@A)oWzI}@S%@*p@F#M_Fl)um`4I&qnqX0 z9%6%}cX9ZWQ@SySwP9kRz$dH*5?S`kFVWygdUzbe!d}J4lktmWfm9xDbAIsrw(po6 z>9*?izf3qURnTFChh723JVvJ_CIVX{s&{|w`M5sLuI{G#kDse)pX_OBwyWn-&f{$?FD2;Qw?$$?UD}aQKVKIFi~L@`fCfS798K}&(~4! z@vtrSP1)E}pcuVe$og0Hs+77k2SJ6Y9LeLSt^4ze(ruRKp&rkGzg`(^2%JUr=?NRG z0IOtS5#waZ5>G7{pz>-Yl<^v4Ck?ncGAu{fY$sZLW(}EKX&vB|VGIViyDCP(fTI&f zPUmN){EZ_hv-H~{>bAu&Scvlp`-<{erP$S=!lwYiZr|2NG^5$^diMLPMxE-J3P8m$ zKw7J6Lcbi~)&wYh=sUC{$St&?&2wu5_}aO}5+i3La_eZ_|lNzT0ZaeyIL>FQllAPWn+kZVU}+wW{#KgQ5F8{RpAzPKCt&x>AFGg zVmIhhS3YZ7ipc4UTd862b~I0RcptA&COjVc)-L07jrX|!wFl2p-|toVx$j>6>aAY_MUW=kXYPLBrzkBHU1aD$D^m8It4YXGt(tO9Ua$7~ zzN8{e77TRJEXOr-zY)z5r`Jo&-L- zSctf%n@TAbCnG&HI0zGbRaOI~mTWeKO;Ty7LZ2W zh^&OPlTP@BUS>sIy>HPJiPS>AJxePvm(i;x?DRhVtWw`xz!_B-H=8J8`TK?5vSZjD zuBVkkf=7(wD+Hm*!=TE9)IXB=HzP2XFiH9qNfy_dn$kgs?;Fn8EwL|N{#Upc>_Ou0 z>u$PR^3;p~CKqfHxXbR=Y$}*=78XM7-{3m8W`=`=b8jbcRI!1TyTFYhR%MbX&pNn; z4w8)X$SQh~{*-_kr_mVfR{9LP#kVK#xm@tyz;8yFZ^F&qMNSoaI|>yFe$F{T$6g#G z#l=rsA~Yav>&RLtC*$H^ui+ZhGO@Qf>5JhXGH%p&0z0iHEg|4r@dA+x1tlJGsO0=! zySut(dv%WMzZrYyDCaPyIQjSX)CX9{pu#WUK5i3j0?U?|hFEVHLIC827fDUJ;1YN- zzn-#GLp9y%>%?61)Q+*tt|$1#5~~@kQVxROLykjRz80wr9A^sIe&Krj*1hnJ;B^EA zgxIaR#;jX&blY9?(z*^pc*RvN1x^iB!loeaVOL1t?1v5D6nhD@5G3F}>WB-l<=}kM zf(a>1(lf}5bn09zn;7Lm_ywhzAf3^u=^pZ~DU< z_pRM|A@u%G&qWj~e=;$3dN3E+ghl`#m~1$@kkW6FH>r10*&$@>`7)OT=H1a@0`5@^ z<)KZQ(RLnh)rwz9WVY_Z> z)efL|J+_E>6h=ByzVrr^A0t5sbJTwb=?RXE)7#)gdMQe;bf@+R3yQdq_58Y#lIlq3 zz7<@kg+*7#(6l+qzu^!Uy)GmEetN7bEi!al9Ce%=x~1xR6Q&*6-UTkN)+R=4#7Y)y zQX{)0uf?Ni*>rkdrPk!AA25fv?p(G9=ibkB$Lw>KdD`X`q`<>q1~?&iWO8|0zc)EA z`2ZAT6n7rWd$KthXa#$bJ0?L3rSqVzE+WtgS6pElPW+Vs0R`VDwnQ){A7|-Ua3xn~ zJyR%}#UP+H+V0oZ-74;(_=@Q-*iV zX^Sjkp)Q!rd?tUayU2}icnCjkgAXG{mYVdFEz#>{FRiA1Td*!7w%ME`)o4gagX~!; z#y<$4%wRv(-JGpp(Bddb>X>}E3{I9+Y+gO4VoAaVuJ@s4!F|v?Z|yY=#|eStE$Fal zaG(AnH2)U_28pA0W$VR~gLM}Mh5#8}Vo@&)_$`4OFtO-#_fB|IBO(E>)v0S?XE|UB zV|njCG{M4o7|Pce41IcN%Avzd^1VC^SNibR*ro$t=HMRJ!Iu2A#hPm2JmW|sM*s-q z%u1D*;GUk`f^j#HuWq1GtE5UDpfWwlq3J8i?#KTzZ=3B{;15Mb3cfP?*?x@Ce4QCw zhpybVR~boQkfuu2*qa6PKQWZVF~tl z`6XR-T15I-wu>D*?<)j>Rku@awh#^LNnZQeD@jZP<4I0>uqRVcY&N*jc83Y@-gerE}AlbCiMXl{`7 z>~!aor04zguy#m8vE4#}yDr|(s3zU2`)n_G)D;|^j~tMC zOH`=tuzvETv#3O`BpH)xUE)%IQX=3X8h0Hm!bG48NMq1Lj+BYgT`?$#FQm~@u^ti% zlmr0iFb`W4d;zGy#Fl@Pl0?q&EGiyQ4kZC|sOA(kI35PumQAeL~PGHT?L>S1E7fl*!({aORz7&;qPx7Yjo zw*PETr~h@g3@@)-+wBR`JK)&&&7qpk@5pZ;1Ihvq$qvbu3t= zLFN^kO|ljJqb7|mY78%=IXdC#OaPxwB?q$STffH;YXRaXCQVOa(9qa>2E8nv{jci>TrPCD@SI-tJ)owT$!-_wLn0*fJk6;us@m{3|T87s6sd3rjY{0 z#mgrlmH_X(ClO|eMvYCf3^!i+#3AC9%_q-*t;dK-ikfyBVJ}a9u|7$S9{U-fj+CUo zf>2DiR71j*0)Oou!=xFkXo}_ZilUA;&TOh~z@nId3e4$vb3FOn!L|8%c&s?hiq3m$ zPjJ0es6@`78_LtQE@7a2Y0BHn^G}v$Mc#Mv1J@*Irx;`%09CiVcdIiJSb>xr{S4|! zX`#saJAPMCxqDqqoCK|Q@rF59j>5JGQJ4s{{=R43>@!Ft-y<}eG8bEMu)n^mKA7*B z8@NErn%0!-LjVGKwc&8H1Hn~z#@t3IyUrtSA}8mpU#TXc1i$1i)YY_xc4I#(2X+na zs=XXB0BgsN(yV%C&?K&-&&2FKXVxP2L#{OYU6nq0bW{uOOxmT%e=Xs-LYFnGrxIq% z-`>9@?78xxwnTYkrVg0j0Zg4e>cmay$P>6rA}#7R!vJ4$QI>9{c*bPvb9NJ!>_v_1 z0^Q>-?Bu;eAVz|iD58;eb$Fk!|LB61QMI%mjOSu3zEW2KZ!^|ct4ic1_(i7g=Bt-( zEXCg9WBZ}2`oWvr)L(EF28f`bTV_Ye5VLuHoOkvaX3bF87}ivIMa#O<1l5vR7763w z{ppGSDyXGSM_ze^1=UN$MM3JGvd$wcBXe87Ecg*wjdoH z7tU(`VfnQZuHRUGf%up7a{7jQle|!O*?9fC3Bxw)x_sFv=ISq6wzvUo=U#CwLAN)A z&5Pd0KlF|5LN@!$AE?0K2kavLk9R94Q@fwMt)K5#jLNvxCIiCeJ9UT}+eVt3wFIS$ zzSO*~M5s{=uA}Lx8kVr^NVhuuyVg`NRCIAVX9hF<=&2D660|Dm{8K#vM`d}r`UdA6 zGE43b9IZG;YBiCcuj`276Y@?Hq9Tz21b!|m6ECjD;ijG<=Fs@1pk z=7)y%8~2OJgGlK&QoSoWX#YbZIo<2G-KcX9TuHRnKOvj-CHJ+Ye*s&76AhvED7+_pf z)^GQ7uG830p}f>Hu8{V6Cyaph!Ke90{0lr%o3?>WWKAMNR^V0~30o(EvAS2OPue^~ zD?&rnMZvC_(WACv$x`4`l7(GtN9_xTL-%9#$mbjIftm$9cf5b4(3nSQ)-<+?@4Z=S zWbWwD`3iHRIo~N2S=R>1q3r!vF*Ar`wkSY(y;w4%>AR0rsMqVD~Ai64`On;we&@UN!;XVA`mO1Ir=C#TS&8;IoakRIK1%81d+97x}M z7#6R1`^QEDQ8-Y_e{GUA{sF7#|6{c{|D1I{TXcU`l$rk@C9GIYI&S`7TV~ZYQmBy1 z(xAarC?Rh70E9Zm0qZF0_au!oR(jo@^2@)o9WPxnhp(VU@~g~`GBN${B~0h7_QQAJ zvTO_p`(INRt?DRZ>_$r1+E=#fg|H2pwrqOhEt&vR!6oCY?Qu2G6QWlOf_|-UeF3>1 z8&oZvRVs&hM2RXoTT7z&&t=xQwj@|}5xhW9NajeB>gJH6ojHj@RY|pJ4oh52?xrRF zp&v!BBIx}rB<;~?e7SXIRE4%wNX9|^f~-!PU2ED<1>R&9LNq&- z(3sPeXW;%)c;k{xcP)7sJ{Y}wuh)z`96mfgd`)ukKPJwejAGUPkkPUe;LOxbgHr>s zD{^hy!i!bTqStj(Y7$x70NX1BM)Akl z{0PH5qc_22r<2n}!5yZFJ)X!)XBat8-{pG{D)q(5SUS;HXl)xZQA*~bv~&@KTMf|A zr>c))tmox5`yJiu2CD%4ww3 z?w?}UFK76q@EV7`9FIG!V9rsW^R%nZJI~>69a2e#ak_LO9|%`;X7Y_9x8y~UGIiQ6 zUKN-lVajKO%4Nm4)%kn{)%X2;P`Rt-yrOENxEznrwqdSJl4Z4<2h@YBzS_;r*RGUA zIUxB2B2AFHnobgJOI=Um=r+S{}c_FFuQ+{()-OO+>Qw{4AL)lKTHb zwf}b`>0)VXYVZ0lP;ryTI&NJoar=?_trbGXl(jMY-RuJ6I6EP!cMY$fdCD%H8AHlS zazcTmm+Xf=`Mdr1@dRBzaVT{s06JP?{-V6Py!z)yMFkNwdMU5YzW2D8+1A?X=g-=W zQp2y)je00dtLm%vhg*~7Q3N8t2&JPSk-phgw;WwQ*KW>houa=HPRlZD(Fo zjV9+Rk!2a)=#zj7pDaoHi*8Aj^d=w4l~6_Zw^8OnCd>AW#o_@~(CE5K1e3La&c!?p za)p_Cq!%?Hk>*weiGJ^cRqBx>ttCz%F(HIQ`hgK4c}(It8S;D81nAD>B$gCPo5Vo< z$T^FBq*w$?&0&eu844bS=H@t*MXMI_9+R|Ho+r7Aa*8PE47)o2U$6UJTAt9R9sJ4$ zA+vH4RU`@IyL+XS5SmHyU#;mBJJwCZoS~6Mubfr=$I8J386dU%Nv^6F0rfOV(jova z?yaRGN!dD!vgxA{c#_VleI)nh`4jBPc6THV{=6&TkIjjrP(=pd2GSSLRGMR;BW^}w z9`LM^h&H}(+)#V!>o0PA8e?`x?Ck2x*L$zsTl&6f!{2G&PE-Szi|enC!`eXU1CEKN zjxv7~!(5g^Y!7=bC7PuGC zC9$Nj%u>_>J@ceomP}e~J)IU{oIiA{PlB{@)`M`?P(UQ&fV9MvBbOPNiR5kz2~H}< zEyuG!F!1l2;OXVB%9t8JF42c4Z&YC26$}94wUgvbwc6}yiSgPS(bD$q%}0A@u*1j?Qn6j zqPsgCc-Q~?Wk2p7{^rol&oL@;V}B3=nL|>we#F33%|f#`HDlV11dX7qqaNTKS0jlgU1$nXxmPJ^}wW~&e_ zK*vD-^gFzUCSVHGhW|7xsO=E=r64)|>uc92Q>7#Q<@KmQW7IIuzq8}(n7b=k{n{^j zTTkE5hq)V2&=(Q|cMX>Sh~I-iPu(EcxQa&<)h8RH1H@p8FU&+FYceB3ADEU&q&exa znc$+diIIxwEB}#%%?e4I6_QU@fGFwP$Qj2rY7g$hf-R7_K-MLzQVsMqvsQ-cMqioD zgkVmcnxmW48;OJM%~dCNV;swpB2v7Y_pG?J&FR7&Qx=h6oky-tG5KAV?)!r zEvby>T(J$PEF0XP3^`}plcq^4c{-XZms1sQe<$E6&H+@d{#KK?ATI4h8FMz}rn2V5 zILOqFMQ0;SyH1o>-1$t57q{8CHg(ijDI1*qKI+^(m0cGtY@%a2m9M!+BjZoLxyReF zu}AqL*=|{nAi#EP(`N@?NpAs#!g4f<9yx+FZ2rY++C0jBk!=gm^1{Q@uTn08v8}78 zJ&lwFlllpy;%N5|$cv`>r_zbFF3O;T0dZ&Xn+YAS5&!1Gfus&cY#i6arE zAb4Q|qvh}wVJ1wnirXGwBETMkQ72tW`OSl9@vzMetJ2vZh#cbgmn7C3(SiE-eEW$O z%&>V$I7Bh}QkCKFc<%C9 zSPz{dwZ>gXSuGk}dy~O3+?Yn#RU<4zCDK^%!MpAw;TnjE#I6J$IwZwHdLC+Ig0-zB(5Q8fbTalWHBaJuo&Lx!{MfDm=IoaDnv4}|I_~%p!k<&{ z95@O6R@Egn$E3tTa*%_hDScB^7176u?uh|!$WKO2PMAeqbj6tSdP4x`{nW|BcyISS z=~rA%qM^)v=Y7&y(_9yCkum(#_;F1p5J-+q?ouKE5KT14NrFDR*(llECT&YnJX)GD z*(Z^F6NkZO6VPXg?A~3oW}E&OU+)wqT99q&rfpk0ZQHhO+qSjSxzo07+qP|6JKb5Q zPIp&TpL-u-y{`BcB4*4v=J=5q(jh?{TmuJ(EQ{bU!1WeJ$DT+#7vBQJdJQaPTwDQb zoIM+nFJPG354fpRv~Q{%V`8MQw5P#yy(ozO4@K1J+9CyCeAODbHl&2yw_uUF&g zwf~aGMbF~@&G4)5nq9ftyJPt@y0A`#2fNcus9Kgt2kdv5Z4>H2(0gpD!HAL%5w7l) z@p)hnjpn<9XbA%4q-<>%k-ZnMo#QL@&cub|!{s&f{_4qA*{uFvDEN`I6p%I5{C0Wb zCw%e8lBD(@)Jm%akb2P!vQQJMaGl7FeA^zSe}*#}-8Wb*(&LkXnE>vSyZNx}g_x1* zc2vVB8p7+jNqoE6j2l$IgllJ@62XDfRPRQ%sr&!IgNsbsgPG!zKA=noJ=d5`>2p1=;QxKdQi&2Cei#_K~Y{EY{#IMCZ9o@ZhMbZGI z#@0zzCn=8);EGPpmN7o>lC*CEaKIpkA(yDmY$3`f$ltFyBqG}wm&$TeRZun$_XW1k zz1Cg&o8}D0ghsjjk1}w;MR9Tu?eHYRUHNE^zlr#Dk?6_P`xRT_x?NAeUg<7n859UW zS^m-ReIR1vlqTRqv|uBa8ZARtQi2lF`(c2ihrO2U%IOaM&5;GfyMrUNeE|r1l#AX$ zDXT`EIHSYE-RCH)K6s zAF&n2H#4Hf&e#M!)i9aYHtMCx`PAxpx$GPmALLoVc9HG%Y1DCcZMVG>DC9oQ%A~M< z>kW1l?3<3+Pc27sxO|;PBZRpT58$buyL^`egkEktU{^FrUN%7Rz=WODKUe_9T9I`t zH2(3Y(K)aIPTm;EgG3xjtldNAy!>4t9N=#OhK5c*$3A!Z&HowHR@ zI|HtNHVm+~O8ffe@BI^YEJSxwySc2`Ix{^7Y)kTHBikpz>z3A;G8swOi?p4U*U6pk z5z0|=%6V>9n>bX?jIbMDu}vbP2A8y}H-qb4#a}_ieh4-lA6VHnfnk$uSnjtj@rT>G z_ugD%RhIeWrVp?T233(FC`w+-koaNs-L9_UH}Y@>$34cEv8^JLvMMH)nQC@xiFOO* z>CAKt{sIVO6G@#3VF|bs4*h~|9a98oe?(f4M0*DNbD{%WkO6233c}x6mbt?GUUSNv zY8}*Z{t-BcAScSHfHS#ym{C5!QehWVhlJ{GfzQ04Oh1tCr+cLmcp4gWJB;g5^{Zj3 zlKU(q5kt#rT4V#%ZgdW|_LgL;!2?x!F}&~wkK>Y!wiZvg*=OfhrDHqmCzh~579$e-#!Z9q%Y^JAl6xnsq8W- zF(f)d+!6Pze;B~1X<~fwu!z4QIM6gY#?srKD*}F@bm;z~DGMsnr>dvM>HT1* zz}K;sz6bYk>R{ll%4xo^(;#a&*1}upU%yJcO(!-556`r$VEL1iZQ-;j0ceiAtdn2| zA~C&PW9L?B_$uiv@BgI(b0A(ktfX=+I=AM2Xpn{q)rH81NpFK?T)TWuc3IXinTshD zi#XJ#W#suM>vDv-9e7x&=?-`cay`u7=Z@2z3+uVzUP%4x2e^}WcvI>gymq5J3a0qu zaNEHE;_&XfSZ>m&@cIXTX!$zJy%3eFm8w7r&%EI;vbDs;>8bm5>s9WscMJ`tXc)iK z^gIp45o7?txKN8`T6T$KdkVUwK3GP(N*@D$K$&JwUL84WbUwSpwiI0MGlH{q=~W{x zU6ZtIqB3^q!|?Sp`#A~f=~lf~FLO;fP}EFS*-`D(n!yCdaV!IL`j{|VT-q1C&vVS` zgxg9Bo6{sUO5~2fH7A_NfLBXSo-jSVH6liyCi%A{aOKw3D^an`iaIyQ-%I57Negz| zq$puRBY9dzbKtj+CWFsEU%BBmj`hODH4{(`OGjl}U}#f=Zv0VAlxdSfc0nckM{VxY z9MQ5*Nrk@p6+5gNS@}R#S&u0#%%~S|5&>>9c!egT(;f-FKF?JGGYj_y}pZk9MRL>6JdN^SJ<2gRYsc< zgxSVkue#+`)^ydiabDDdX9vEcF6Tuw6fRG*Ug9;p7>k|>W%(4oqEERxr?# zEXu8T$c#^V;j1+QIW)L87!1y-SWd?}*_+DG2!^L!=?NIK=H0huvEFtLxU|lgj)18x zMYzpygJ{5+h*A-jhOdfm=69dJ&m^qdP|rFe>0EaaB56a<96bo|xKUnR4sTQAPFwZi zXuPErXW{`4&2%kd1gx-v|+Mt)tKB%)sk8F#LL%J?60W&wBHx)-UBL5dvI?~ z@=9sd<=uK-h8xRLvAMPTmp}h>{%SrC=OeBCqf#^hrryIq)vYM0+Dh~r7n>G)tFg#W zu-7$uL8~Vn)o(zq)nK;0iqWlxRM-!%66P>;!O2mT)`Z6CmjCaySgBoL`eP+>x>A}1 zU&IS4ce&t29`$ny%}q^-m}IiSt(tcmV--`Y*LL%D%U?#b$qgi|I6kn3=|#hv+(;|E z_2zWj`<|+}unF~p^HCfBV>gWAZrXKIA6c4|zqbC@E_F3chE8MS&H3^BB3MB&pG{z7 zZzTZ4J@<33N8o%RA17s-1ujKLRRTc0pMA~3y@S98v&m{)14F6n3T&|=FmrQ)PmTC_v(Jg5jZ3mE}6( z^ayp;`Jh>Xe|miF7`wIT60#;jnu{X(9O}+eRyUCT;*$kH>l%=(Q$OyCR|%#r8YYD7 zv|-@sMC5T+xTx-PYz^s$^%yYtbGBnl-py+kklrJ%7YgmR-TleBMtHd+Dth=s&S48&X$AU#1Zvc%AD%dc5=RA~(?x6GM@ol)A>x!2Flgy1AX5 zmD9fxPNe<|Vlp8!DJ3mOqd5BCypz!*N#ICy^g_}BkOilR`-BFn0|7&A?Yq0*92|Fd zP|Z+tlAvvpoKTXZ_mXwm|Its`EKQFm@lymc{^_aw(b)YTQ1L(SZDU~jgE#pHm)a&? zMhch#A@t@Il~5NcK3q-Uwva4uwO};PG=bT=d(<{g@ogvT>bhBZl7tI?CL_)5=G0Z$ zrEqP&52QdW2l=kfcycZIG1UoUdC(iQ=U4NwcqG;f2DL?dU)*F;oKA%ejsBsuj)EV9$H}gKgg7^357VK$a0tPVWUUz zqp#40Kjcutn*M#ksqTsC%k+a}AFLCFmbb%G2QcDHMDI);2IgAYs>o~Lt~s*9VcbV5 z?azLqQUn0|bvjj|cha7JfA!5+w*tODk6-jhNr3wQJpTVJW>lJz9Qf(oJE2I?hf}L*&mq(=_;vt`x;yITFw5_TPWrPR=0H?Uu zyGI2fW?ngg$MbkQ^T+`Kdq7--W7j)C?5p+{<>!A7I{#+^1SgbO7(O?jF^0WZ~) zvzMc?N_2bBnfarO0~2P~sG^3c?*1OJ#0(6(QxV>1D*^0gV_ol(9)k-v7Qa;%VFN(S2gF&3xH$Sy5(tZx6}E z{@%Cy_ZJ4BX7dU81Lgxl1OTA;e_oh{jlJ~`%iX|O???3UUwYIZeu=5YKP-8=)VA!9 z#Sr{n^y=EV4`mrwi0=HUHOd#~!+7~s$chUElwRdnrI8ursyApI zBEZNj^cPL(c5I=uw3$Ednws6vbfO3XGjSuAk~79Nhj#O8t_Lf-P-5C3&u}LGIC#u} zKb(Le^P1wUhAh^Wl(;;IKhRl%0TohO!18)e0_{Xv^&SG8q88AFnn|?|&tl$za~!JL zuSZP&dWkzjrSWXV{PgH~F{JXW{{vU$f1dct$%>WUSkqk12Jl()XKj|SIpRP`wR0DI z6`}0+P9#iHxSB(;^Q&4o&tFYxRAGQU6jRn9+#-x#eqjhryetwtF#b+Q(cwfS0!^Z7 z=osXgEZ|o=o0NT!=kbTVb=Hc)D4lEkV64Pc+r!{_kaL_%;fik*`I0AcZS~7W`1T6A z__AhHlJTR%&2o0y&_D${kFs#2KuZbBN2{S<)MXs`>b$GAn0gYcU;6KkvAzxIOwBhO z+bD}dJb6FZJWAr*@mGLT(0fa8TkDO*sKl;VK#fGoLvqZ!UhI(y1pAN_IQF7up+F?X zYYR%PvgLb*BVM%FBXpi44n4#y;U{8i-q3jy_vEuxD#U9^1!(o7L5nF2;cCNp8#0(A z1_&XPoh&}LXY4!g;g{bzx3jJQ(x5T!Y{4BuppUHWh8?mRt0MhmA+HdPK%BkhaUl}R z&M@ZfGYf~{z0t-?(y)|=34FSB#IUjY9@vVyAfDY%`7f8U zt#VK3=jpPvQGS-KB8jV$;)Ti4Bg*Vi_} zk?{E=D{w2Sg^)B&7XYn!K@oeFG{yN20zhOc%nj3aSft+^MFVvV>k%9WTrd;EKMhov zc$n*aRk-fe2Qn8bVfWUW#+taC5@(kb?AA4=N^cBdruFHy<{FtMeAx{Ar^t?^GrOm; zRo4w!x5JvPwKR65V<^wjl3H^Grmgqd<^JmKFBX3H~`=-)A-16dzLR`D7WW<^N_Gs0JA-BA!cPPwz#?E%Vpz5`?}_Q-*@2cSSi@8uB#cA0~Cg&inxx%`d|azLflJIy;+zyC^dUirrpKl;I_!?se{fg8gj%96xT3|0*N= zXZ-bF`^i7;X?_TEPA31*RQ&8EMXFj>o1!SbD>bR*bei=lSI0I6QqWsyLbQu({E-ZT zb9hxtOO}gih|`8Jw84WGPE^FL6p)0Aq`u^qWb8)MMsP=7|)|go5e6!331^9 z;t5_xdgt=Xt7kSDDwSC4+giYkQclo(cK5OD@n+3Aek&(hhXI-ng3jcxv&QyKOfmMr zb%%xqAmS$Ro>^i%uiUR#VumsRSgVMMZ4 zl(FMMuoA&u)ksmN3EUcZl2Xz8waXCQdKoM|5<`KmDx*M~W_2*^mMOXgt0Zkm&-G?) ztd(J7C}~cf;FmEqT>smGsJ+Q^Y3Zkgmu(8ycifK(>Bh~;&#rSn)-Q-#GfXX4wHla# zr(frF4i8Om0je@FpSj+PVUlcD4DZLg_GZxDkkY*-(E?xMrZ=U#EtPX^r=P^Su*GNU zca?=_QxY~~1}?{XkKDzN#>Us}?&^Wff`bluI_=7c{W>*vSe!g2MoNIcRxvk>+a3`b&DpIB} zsb$RIlt#C7SA=tZ68#yU2hkchw}}L*2rGZz$`*eRL`h?l>_ z$V0`X;)@$68^<@cDc>z>#W7pJ4Q!0f0cDM zy(11f604WV?aRrz|+I}>a(g15j?8H%BRSrE?Bdvcn z@f$Q=t}647vg62iH^=rjn*^U_jpBhd8TWXZP ziQAuYkCMFuF(*V_x&SiG!veW6*pJXzlR=}TFpXz@v+KI=_N097d{SPTgk*bc}rZ9@njvfOe zo$grEe6Bpjyd(-lAo%?(*xhV?ywyy*X%~~JsWftxsw9d0#w@(Nib$hH1{9Gyzsg@E zag?VS{hQ-5zW^>HJ8oUB49Q6$!yT{dET@}Ij&1W)dfND?(87f?CstvD$uz!I$M;R_ zKUPuj+IxB=LUOsKy|xI1J5L~Oaf-OlZwlzY5fBR+dglkmPAJ$#+IlY9c@GAoH87BP zq18yWQ%&jP5&Vv6TSjiN+&HuR9~H#O_p*sokwN+wdp~=8s<0FFdEh6#JV31nCnkxNOGgVI5K2ij*AM`I*tpo+8p z&Y`42Ly}#7QksTLN^M9?$LyUr5@i;1;?*pu<$a<|oZVgI?`7^cjbmibpVy7s{i`#h zlGXNuuxkX|CwK&_f73-t=yjxK2uFI-r{B-Ki`|v)vUgyi1jmb*4L2#1R`h_11dvyO zi3`|hOKk&UH}|mcFtIT4z&z>zUz^zScKbHBd*A7Cz0V4Y5JnNRJ2NKDH zmLw0Fh%)+2K~8flcL6K}KY*Xs8@#EksydLH)>GH>f^XdSp+h66$!f7;ED|YaPQ)S+ zaM;x-tZQ_+{w`3Xz3wNxOabQbs(vZFBB{_DQ)zcI$+Mv zvpMSFop`au^>Hz~pLy8NZ0H)eylZCqd}ukFzz(svfBF0v({+8I?|C|ST)VpL$kDFQ zn%G)ih0R8SC7Ruhh>-Y|?_;Xf)uxhInh_S^`-=wrfjvm*m&FcNn$1NtY%QbySS>&m zKz505WRz3opHh!OOT0EQA;X@vDmJbAs`fk#(Rg|&m8Y1VirnLToRU|U{yMIz-5HRA zu6U9bBocDX;t7o=9OE(+eO74vmJ@{r$n`lb3pcV%8&Tn2S+yeRMOn%&Pqeop5~~)2 z0EaA>d=%-+0Tr<>ywXv{1{+HjxK`96)g8*kVtINe^M{NbV=lR@v4WG=uV}+by)Jil z&b%3;3YSPz4ojWALF`3t;{~$c!d7qig<`i>k<3<17xQ`{&;7Erl<#{fXE)$Ax|_aR z^_fIOKX^E=#u|TP?=;J3tfk`u4c`W57MkjCNQujaYb}|c8U9Oi3gCw$+Rl&O+@tpI z|4?(v>wu_J{HzwtKWa|m|M?jE-(kl;?X&*TywIdFVY|SOvUNfwYI>lYtioR#ZVU-Y zI2Z~Vi%KxDx>EL=$H;WJQ3a)pT7ILnGkG?@840zr{60=!VQ^-bS?38o6>}pRmrEXP zlIZsg&&wGTn*a*(I3hfed6Ty!(IPB`gV#i{Towcx zt=UUEY!oO6dQ4kL|A8=uM)nS~Oo}|c(EJq;%_fPlnY@4zLu&|!oL%Hd?UJR(ZUL=+ zE*yA9VBu=^0B%>vpn(P^)EP61DS=v}8e>X3i#$js812NA?(V&6N5Q@%h(>7*-r?N- zY#$qUm6C*omt3xn}J@W7Ms7N>4cE}?C zHW`V)2}DD&(}#Bcp8#a%7dd>WQFkqL2RRj(%qe3|WX^^!?ziYAMFI@+(d9d8Pz1!wBvk%0b8ciY~-^%SEP6(qoDM(uJD4+{tCe z?ynhTVWFj0SVW0hQT>VNj0VkAw)FMX&IZ2Sylftpf6R@?4y?{5Rz|4gF3xeQx+KFk zhUbpEnaR6yaJsK%Fdq4>q!sMMvcpg^b%%hYXa{k0l=O(Wk^DyGZ>l!YdC#v_v{Eb?G$0TzPNpVYI-~}%1|UD) z%MnnSsl)#xyF0Nadt3gKU6uLC8vXxA`2W%<6s|q#zgGlaC1d4MQ_A=GT|z3cY2+&?#lM z-T8X#bK_G(OR_LZDgeOx0%;n$R3FcWi!?qb)5}jITRQJ#?}wG>Ewm>962&CqKoV+z z!J4NJGWg(sj4*FvTFo65V*e3{E^jE;BvS{AGQ?~b9hI??KBe&RJi2QBz-gI_t=c=7 zF}`G2$|^Cs2-DP$U~UvBno9-OI%1AO+={8Vub?lj6h}|08kl>gzFGduMX-=JyKR_= z`ga*iVirgD6e@03hohoD7mZ+`*AvPpwD<&+JjL2$?Um3j`PsZ1B5CK1PC<%>a1cxd zf?7YJTRLIxv+OCz}p=(ywZv6L!fpCj|TyS<#Opq#A22HcvPVLP<1FWsxJzw29i zoU0p=Tv}V7O@CM3sdIT#ijM(TLNQvnG3HN}c+I=))Rsurf|wzMTV#>wNU!**?wOky z^fg4}x2cF3bG_-sm%f3|$|(X~1>>a18RKISn`*(=MY?*DGn+4p)_<=E<@q64JDOk< z8gyzO39z8HYgWK?&p_BtmUZF=QWmB-p#~})`RqwSD(fZB6C>Jbm-C|OH}KK}@{M_r z?~zn$H#jQ=l&5eD26FfaG5d>6Hvax4ImeG48aC1NN7XtwR!32X%%U2L&&Rj%mj3mt z^!@B;Z#(kv%CoaGqtg4-wraSBmvmxge_Hg)-iB;Vd;I9l^Ewd;-u%H?ab6RpvaB09 zwTbYsG|d-;yB;Huae#;`H*wa|lcUMHe0*`=EvI9rdFVDPNt>nvk#8-0h1G;$a%bqw zu8i!S$yteVSRatTG>eTkWyXf+%dM5IHpqB}z1Tl1g7EV{_LXY4)wzGb+5>*Zzm^lM zEe!vylJVcHc5!J5N}91r3RQ7xusT$fL{uosNX7*w$x0N)g@^k{#ezsh0ZO3<$w(z7 z?!I=zHb%mBDJEnose316sr$$%e$qrNL@NMv^#3SoB#H={R{ppK5EK48gUZIpp3d09 z)Z<@kDn~!0-wpQr4nNR7-*`YZ2|gTq`rM(S*scqmITRVUfDi+SDB%(7N6q-@1IO1J zwt+C=-_EQeUcG=9@O8q|Fef|%4rm?SRgfLj;iVnTYKNCk86mnAk+eyjJ$qdnZ>yAb z$e>F3Hq$?L46y;ar!B9xnk^(Xg}|OW5G3j(U$~0aR=z_K&1)R1PTKCk(9j*dhdxT- z%U!-nHV4^Vz(tf|7}{ztxGDb9k3kz2VI|q$zPpNJLxu|XxNZBCW;!X6rYocqPl3JQ zYe;GyBa7L_{^>vncD6`w#~%kXH{aQQobUJW?Tfi7()S;;YD%pwH-29ox)TtOYbA28Lp`+AwW~6z2!EJq(o6P2g+83S%$4g4@HJHm}*shN~f{6d5D-TioOCH zxJz*|6m%%i85Ac-FXkjW+3r`Uj#_ z1HwI0ahVn90c5t#YBjyAs^(Ykijm%$Q$dMqTk@C?Zi}sBj+GWk)92@;+EACJU_Ifv zkzbmG^20=LabZ_jU~Y>C_b7G1Fv`uMH}CL_Om>n9w_ykbfd(N$a_k)&`WIJd|e#VGp>$GkiP8s}cF0dh&@6>@ji&7gxV+r6=4|gDPU*A@ozTtbJ zQLjceEN3rf#cz1fgkAUuR6;Lbu#$txzp90VXqfcAv3WdGa?C6qG{$j$e_^VW-vsf= zriowALz+fW^< z&ImMVyi`);ExvXf9?5xTH_R8V%|8Ag|pC zUV<#D<`ElwF6D(L()7_*?+~C5Wv@hS)=DO>kDnc)$hdZL>|NN8xKXBVeTnt1DGL_H z-vxtZ$`KEGtMB3ZmI77>KIpSZiqotUAQ`Yqwo9tof)bYT2p}Qy(=Dhv-;CA zI<;cs{}SlU;pf#&YGwktu5U60*yuD&?l1kTILFb`yf6tV1l}Fdgve)py*Ba>RJm=1S@f&u} z)A8g!VOv&2>Aqty)eDHCreH+CWs6iw=F$|iQ7t(_H+!1g*-F&cWT{VD=nw8^QJd|$ z)&Y#wy+7+P=1Bj;UHb1~hqpj)UVz(dSHuEB(@4c8b5SYadO~ z*vzWZe?Xu`ZEw{Mo54xe*yI#0lOauF<=%yup0INIjR#6tw|;+_ViYMRY_umkN zKt~a)%EUl(Vtq`8Q-AJiQG zymC6aUbbFhR&s(~b(}iPagcH!sLTHthk*qjC!v)Ty9dHXf@Yw?Eg>apYNsm!F;0P+ zBbAe54s)Cs7atp7fFqL3v&(>jRx{hv7n&s(lJ*Do;!YAyL(u+5khbow^BoBf0FcWG z0D${1DTx0w{r`&8{&D%4{}#4JK+zY6mP7=8Zwk-H<0bpEO!t za=BTef|Ly|7Z)b{YCznCz9T;bI|v!b$q)ii9ieL`KJv`E@3`LR%TiIfVp)mVRcf5? zY4Z8s^N#b5a!&;`DI|0a(F0DJ`|;Rg28oo4{K~N>@`-#8QE+ISL}3>cGX<47NjP~K zZ(*NK$p@dcwha7O)NlrmFLAZ2~&f zA7pULe3a_vmYaDK5U+w$u<)E7(S8!07%Uj@FoEMQ9h@C+j0IFjkJ4<>2EYJ0L3Co6 z1@5W=d-XYM2Cy_@XVs-6>{e@kJ)`X1nw8Y&KO4&} zAg#a;RTJ~3nM{dz2(7>2ZVe)t-@CHs$n8{(=ME>q5Md7$H1WfurOpEhq>=h;m3uLQ z-v|P)bOOCEm?y;74mq%hijm2mnZeB~{`9j6$S2tm#L-!>90^0o=o%ISeHd6>yvmo+jQBYQJwdbi?nDsK`gTDC34&02TW) z;FS_!-kPj}<}5oy-mXo2ysoJhr}>V%?UbotDgD7X%^7%{36>wQ{E-vGw6-K zPfH{r6=G<|l}gJ;DyjjgBylkuW>yl4rr;8aVo*7&57mvtC6GiM3P38V%JiP1-^J~| z#CWctAnPwXcAFS(y!5tuT)#I@3e5$Y)QlO!`OsQT#Pn*qYUD*xMXEW*00w~hb2C|u zxA=u@7ftKz0jO?4-M?CXTF!4E zxOW)}rfT%mT=*mqZ*Qn2GMbZwUK_x%nosLx-F>Y?L`U}Im$Br2$cg|7t zSR`J;Y4I8A7xyohhVfH8eJ0U`TmkHB>)5fRtE($Sg9cIKd))bUZ_qOUB52M013m`l zwGA8pXU0!~;PLCJ(SEPcKm$rpr4V+xt~!n@Pe3%v8Zr#UUTQkcs(NNq{^C(0n~yVo zp8Qc@^?sz)--l0SC7ATGfq3Q4gR4+>QMDt|oPfU3Avi!^Ovx}1gE#8-yt{lxW9R%I z|(?~s(1t(27d?`f+m+H>#%~V@Iv9Pu|Ae$)?Tsp_q*0uZ8Vq>smm)FCSx4tw1J*eNQgTys3scG zgrt&{&l7@0g!Ez&^948{f%fkJ5i;FD6nQPZuw`};vTcbhF^YpX543Cu0Tad#o|g{0 z0vvaz8i;s>oi`92;`#h}MqWJAL5kUIn!;OrM05;W1xrGhE-qHGBF%T(Ji#0r#aWFM zh^=NCLTjT&EH6^$N0fCCUcb}l8F?Yp^yO*p8gUueIAm3TBog(dmaLL#7dXv%ZeDqf zjpiHxG9cxGC=i{26S`m`yyr@o0ymvB&@)D}S?4g0;IxuXT|%4Poxx$FVm7V8-7T^g z2A^SVpMfcX5SYWstc?<<*wx)n2=W5>v`6o-i7N?P3OPU^z=q1@O(EQ*?FmkTs9<_^ z`IF$=%{%v_K+w=o7m%Tg7AUlwH*L>g-JCB3ubJDZ2S)i(gEsQYlb{rp@DLiz(#AqP z3shT-ZV)mH%8ntEsT~c$Cq|y$@a(4YRYVdH4m@ic5Z*8n&humHYiZ#ubo0>doduD) zkbD5wOmR2AN#ZOUTy+5gK-xKHu~zK!2S7zU)(*j*%Bu;n-ScM$wgPm1Yn=?LjPD$@ zMPvC}vM9bbuc2j~?NyamOk}!_#~Hrze>AIv^Ycc*41T)i`O`!!$9yuj?1^~9BaP-aLZ^cOJkGOCErSa5$= z$OP>Zz(T!U%W{YU>V46rdWF~xE1gZW=9Chdqt_2^Jrl$KCiay9ufO&C={&m_Gd9ly z!e$xGrn?<356o>`wQ7{}$(dBWn13&yT&UV|Kz2{-%7~Js@;;}->jhPJi?;=%8ma*Z zPD40uow$CpYNk`z^&*{C?J^UjpG+SL&!=>LzEeC#X`rhMhW!`bhrCg>{uV;t+f2{fy%Za{t-)|TgQ7<9L-FxV0d2mw z#yBC>J!-`zMbzp$nGB|s4qj>_=-3&9G9yJ z3y7n4rn)R7N6+AyZiA+-{P-ozQp{1q>xQ&4P88`zcKajiIW4QMgSaD40XZj-Eu&~A zT1nEQ(4}C;7s8IW&J!1m3S$eS_mAePhuqZwY74PW57i+iNF`)w!Uu~Z+y@63r}2;J zmKBWq*d!q~f=h;B&;pVqa&N`qQMP?zJd;2YYe`T~U;#u?QaBEczKMseyZSjYdpeNW zW0#FnoumZobarl*W;WF}Z#)60_A=ZWzVSxE(kAe#bVNZE?AsLdUX#vSXVNn=3tAoo zdNUMKwMrtvu82vG#q3iJ{2C|l=s)#bN`2DLt>^YXY0CmAbH@1*N-Pf26%qJ^u1tr^ zl5GjbX)&;yzlGT#or(Rg4{MNLn;^kkY#kC!9>{r_>62b+o}VXiT%|YX@SqrCQ;DQfw%&A`7m)Wv>_VJY?)C-d%ay|CPpp3WEy+ws1*ta8Kmxec57 zdfi^*!vB0vXyJSRd(e>6<@PT64GM22T)UP9U;O7}=Y19-4fnq#*EZ@a~T;^p%W@~9JIwAEi6 zW)a+vqb)3^T+YFF%O((qWZCdWdvcqU8%JEetc`GJQbkFvv=A0h28nK+&TBbPp=~jGX9wHh ze1$iS+b{AUyo51@gSi8u!_805R#V0JK8VXQwoPacTlMjvnSA9v7#1%D_S5f*SI4p2 zh{qG6uPA+I*#$k*@1;f__ag8g3Ct;@42geDi6l@^MM1k(5r_T7I$BOzVX&NXZ8?77 z_KL)}b?Shzzk()3ZPuO9F`nv`csfR_9)m4si07PpwX)8XGG5{Lmfi=3ddD_KqM(dq zx0S4op;5u+O5FXX%;<5yo@CA#o1HC( z#--p$vn^ZA+mz}hiWb>$3*^X>A!}g52Uz!}P+pDqkeh-U9&_3$X~haH2YgsrQ1Gj> z?s8;kp3BCmOGZ(9=S2l*;w!)RsF48S{7}6Tm0^`0F9F9$dS0(CD*)F`F+3!AXcvB*TElv71oQCZSnt=4$ zTCUYXbs9+Y!Zch2$-QmaqrSohxxsO{N)ls>2NlXGF8(S_2aOE)!e_Hi&z-TUp(=gr z-nH$Jb#XL?VL=}i%d!8J2vy7-ETu{J2CiCc>PuiNl%rg6krNnjgB@BE-dXM4DE0pH z+zzk!3k8GB-}?32pYk>&EmtG>7vn(Ey2)u?p6Skn7pe^KyqtB4(&1WR(BAg~cA+p|y4Y0NKfL{{Yn*HNOqT zpC6LCx^Mt>0`xz!29y+?tS7owJzOQM#O+n1G(PRVKtEyFb{t%|=>U9U7_k}duwn$(c6A4rBa+kjX#7pYU6xN4gwjfu3Xkjc1t|>3rS9E-v(gN~x5Kq&% zq}P3eHNO0>b)xYFgC9nYFz(WHtz*7cGs&7{q#69sb$D7xHby7Zl2b@%d$IJml+gBO z@T_B!%nhJTCeFA{MR22oJtEOU1 z#+c&~X)YUDk5=z6!Q+52VqHq3-ZqEWr^$^uGxvia77eSWwNld( zyyAV}Bb6BY+-ptcEM*YMISuU>I(pl5*QSSZI_0dc zg_Z;Iv)C%VjO%chPO72woMA!f^1{`1Kh8HntpYZze7C`bWRU3yE>&!XJ2{k5U3W(MSL_usShk5?aQ-X>l@|DL zU8^Hquj#AUU#|1)R@}%Rp;v;p9wXVQXqfXJYH@=%HtCXJPB?MCyv66quuYvt*KNLrqJ&AFtPEUom^N>xwa{b?{C|J{f6Nr#8>B($ zXNHPD;ZqAVaRpe$gcAn*fBMCg1+{qmQ|lFvnq;eUh@^l#Pr zt)%+bu6Ao1B)gQHX{;83;g1D9-t2xZHzl$5nBS+fgS zQcBG<7_!co8Ci-NMM@=0RQ8%M$(G27gcy1&dr4%;zLi2E`9JgZjp3Nr_kXU-b&SjT zo%^|;WzISGrtcc#{8LcpAm4kzJ3`ueXS;um*Pr|8A^E)f#EhekvO&Ttti%RVVM*L0 zM~8$GHf0;eNp8&(k-gb(VmFWT!>nTSxmXu-yAD={aqUREo7TFU*fOVfM(Tx9_%ICoe7fiAZf3+(Q0IMe~pWXI`wb^ zar>nbjeX+8VTMCls?s{dxC&>Ecc;T!eIjmmBw7t=O9a-msjeLHnv<24HNb032;{w8 zw&By&uZ^MpVH<^YSVJ$oS!hY7B_XDr* zKsSfDehXuktw4yD!!}NYz)bE`d1i6)j~B$OVo_CUg4CPKV3jUI8-ElL(mP@!j*!vs<)ivIh1e zhDkxG64%0XdYXzw@5|-94D$=R$-ue2QIYr?FKTcJ$*YH&_1IE#WZ=c)vv3O=U;7#6 z%ThakY_P|de$HdD*4?eY`RVR?U9!Hb^QCFYs6* z{STW{`n|u^SMJrc|Jjsns$S|ar&x~@WfkISH&~Y@U=|D()xI0za$i2}i7eU|r7I{p zl%aOx#KUAE_VEqX2NVKYdyQwWU(2yzwc%IdaXQFkDVdUwj7j$?ugGT}3b9?}_IOYf zzgbjpdWiinh45cgK6x9{c?)p_1{P2wlmvXvqU@^xhL zc$3CpGS~5N-R|;mP(ML|C5Xd2MTs<~pp$8F`pQ-QH$OFbeuU=DXy*N%yzln|ed=U@ zH!;dof=}XG`Ahv4vQJsa2ha0HA2yn8P<-E{Y0DVORm+YQf1)qGH>f(qboAase@i2# z83lueK2)c)R%WkpGyhi&(Rk;pJrj8rgplvst^^j!{Y?I}ZA2!}L!dZx_E_4?kr(*= zy?@>^Wz~?y85g|twU~C{N54sR4r8{> z!rAHjS*qB= zhc-@C$7Qz0s+IAu_LS^EYK%IP;^UfPgYCi(PdC2z~&Hnl>9d#OK6c}$dr z#~Mw)v!DMt(j?{{i9D&=-%Z-RH}KYIQ_tp}4*#@CJ6nO9rTAd~!Tk+AY8dAxte&7~ z6|c*vXF!t4NSwn=zGg^ML45q1noeLb@0Sc$ZIHfj)3&_m1XFu?;!Eq>#_I2J$FIQJ z)K1+{MKeB1a1qP67PDpli^$ZS0X|6{JL^*6-(zqIj&gowZ7q`ra*_q%V;<6jZC0it zi5r>y>Jv6Zzi#k{N^Mh+8sWV_|zmTTRdO+6%o+=$DW#d-pB-pxW5Llzzr zJ|q9MADt{tB9*;O5ox|KWi;kgLps{)+Z`w%UrmJi@Yi{%#R)V%oX z5sLbF71G{t3OA`I$*Y)bl2pgoW9?|-LaVUQ34CC>%F=N9wSAtd<_A;r)Lfo zx_>{&KCOh~sp*Tie@dD=;$P8ISRYp_>b$km?RQddaMOG-Y_UDhP?C=%RUxK$DX?)z zHuozZqKe}8{9T0?NmT)pTkJwVE^u#~wnXcEtP+|osmMvNo%7>-!t6Bs=d%p#zUj4r z`i;?D(d)Rgx)4e6BgH%|We4s@l(;k2yzqB@lU01T=5W;o;w-p714 znQ*JU6S73aWcrN$mQ(NJkhk7`j@f6SSSp;+p2ZhJY@Us(Q)+tnE0N9UiFBk_O)T&E zGnPRPM6W0~OSsFQZo5>LlyQ69k<&Bb)&vDR>xcm-MV=^UM7GnMiDc7+_(XO3bb)WY zq-x1R_d=uBKLQRX0?nLXndQ#%wun>x+60@&8~t z&%NAZe{MX*8)l4)MrkFVPtApOACZiVos<;*m2WDGA%$I0JRM6mcRx^V>m_C(#6}K( zCV#%Ix5SD=CvNyKZo$(k?LhJonC^J~P)|UHlv!n-ZdpYEi)#Scl`+fjn??fn$YSF$ zl6ilT*no)RfdhBAl$y(GIbG_SN!HdMNun9Ib{2xvp}XxgWhNNXrUo9(&*Qny{l?uc zGm1ly1MlA9{jjcJojK3`ib&G2gycWr($Jgk;|i& zX_HsdM>mC3O<9WXv2SUsy5$yKh%PKlntz}}*;1Uu{bg*X{hIc74}=SFT5{`h(|-?GQ)okymxDVyV|qip_P zILh)*CwY#+AABLv2-?`DLN*qN~<&ralI7hMgm zvtv%^EZ9HK+i7Ym$!vHDma970$KHO>IA#R)CPK8+ra<9PT3qDCVui}U;h?1Wf|sL% zT1*S0AZ&3RBOe1FxW{K>aKCW-t<$2?gwv z2~Ot+H|cfE3=CVBP6v5#@%_sHNbj?57?igcc+3Hh^H9d3t$%AF`0qVHh%GA^3LvDF#Eb za;{{6Fr2I=0jZ_~+n7=4u$o;DH5o z8Mp^BfZiUGar_fn6%1w+K&~9n5=DAQF9gPe9%r$+u0am0v3)=g;U&Q+6Kt0)!xWe-!Mslvh5~ zM!@rd$+vn$mt_3AH0Th;Bsrzr0Z|qZcTjQQNWjCvo31s8UZ4s%#ti|AGOvP#9HW!u4Q3IIFSN*xS2tO@?#+YPi@J5_7~ z-X2a5O--u*ueIBuvkY2_l6HX(_q8tIwxTd4lnN~)MWdd@u0>rID+WbE>$=d8YW`~> zm#Vx#fzZ+)G@wi1T0pvmK%j7F`2!lfJZLRAtr!B72u-I(BUVJwh=1eam$}G7G0@Ct zG>ld>6hq6Cwu%Bt@I|AP#y~0mP4@*wKvPc95MH;Th&2*WL9x)xBQ$JT0$uFtJS0#g zGzS3<$&&g%kxLl~pj2qAGL5Q2Sc^)FT!s>%A(=E{O2%5mH5MH-@QsGN{)mQzhQUDv zg9fhAC_Gs-3cWBkC<*EuP9x10&`3*G+BMbz)K8m6dIp~VsNaQ4q~%`QP@$m4Vj3#w z6&=(n(=n6>H3rjoHh#k2Pzr> literal 0 HcmV?d00001 diff --git a/justfile b/justfile new file mode 100644 index 0000000..2890b79 --- /dev/null +++ b/justfile @@ -0,0 +1,62 @@ +set shell := ["powershell.exe","-NoLogo","-NoProfile", "-c"] # windows +set dotenv-load +set quiet :=true + +@_: + just --list + +[group("build")] +generate-schema: + rm {{quote(".well-known"/"*.json")}} + @fd -e json -H "schema" + uv run --reinstall-package glrocky-sdk \ + python -m \ + pytest --co --generate-schema --continue-on-collection-errors -q + mv {{ quote (".well-known" / "schema_autogenerated.json")}} {{ quote (".well-known" / "schema.json")}} + @fd -e json -H "schema" + +[group("qa")] +lint: + uvx ruff check --fix --unsafe-fixes . + uvx ruff format + +[group("run")] +[doc("run a specific test,eg: j run 0101_02")] +run TEST: + uv run python -m pytest -sqv -k {{TEST}} + +[group("clean")] +clean-pycache: + @rm.exe -rf .pytest_cache .mypy_cache .ruff_cache .coverage + rm.exe -rf log out + @fd -td -I "__pycache__" -x rm -rf '{}' + @echo "✅ Done" + +[group("clean")] +clean-cache: + @rm.exe -rf .pytest_cache .mypy_cache .ruff_cache .coverage + +[group("clean")] +clean-out-and-log: + rm.exe -rf logs/ out/ + +[group("clean")] +clean-all: + just clean-cache + just clean-out-and-log + just clean-pycache + +[group("dev")] +[doc("invoke ipython with u2")] +debug-u2: + uvx --with uiautomator2 --with .packages/glrocky_sdk-0.1.2-py3-none-any.whl --directory . ipython + +[group("dev")] +[doc("show android ui")] +debug-ui: + uvx uiviewer + +[group("dev")] +[doc("start test executor server,from glrocky framework")] +debug-srv: + uv run api-test-server diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..48ffbfd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[project] +name = "gztel-wifi-e2e" +version = "0.3.1" +description = "广州电信WIFI测试-e2e" +readme = "README.md" +authors = [{ name = "liumin", email = "liumin@glrocy.com" }] +requires-python = "==3.12.6" + +dependencies = [ + "glrocky-sdk", + "pyyaml>=6.0.3", + "uvicorn>=0.34.2", +] + +[tool.pytest.ini_options] +minversion = "8.3.5" +#addopts = "--no-summary --no-header -rfEX --strict-markers --capture=tee-sys " +addopts = "--capture=tee-sys " +testpaths = ["scenarios"] +python_files = "tc_*.py" +markers = [] + +[tool.pyright] +venv = ".venv" +venvPath = "." +reportMissingTypeStubs = false +reportUnknownMemberType = false +[tool.uv.sources] +#glrocky-sdk = { path = "../../packages/glrocky-sdk" } +glrocky-sdk = { path = "./glrocky_sdk-0.6.1-py3-none-any.whl" } + +[[tool.uv.index]] +url = "https://mirrors.aliyun.com/pypi/simple/" +default = true + +[tool.glrocky] +remote_url = "https://gitea.glrocky.cn/aitest-scripts/gztel-wifi-e2e.git" +branch = "master" +uuid = "157f58111-aaa9-4636-b476-20251201gztele2e" + +[tool.glrocky.tags] +osName="android" #adb -s 513a205d shell "getprop ro.build.version.release" +osVersion="16" +brand="Xiaomi" #adb -s 513a205d shell "getprop ro.product.brand" +model="2509FPN0BC" #adb -s 513a205d shell "getprop ro.product.model" +manufacturer="Xiaomi" #adb -s 513a205d shell "getprop ro.product.manufacturer" + +[tool.glrocky.commands] +generateSchema = "uv run --reinstall-package glrocky-sdk pytest --co --generate-schema --continue-on-collection-errors" +startTest = "uv run pytest" + +[tool.glrocky.requirements] +devices = [] + +[tool.glrocky.project_settings] +screen-record-extra-commands = [ + "--window-x=1380", + "--window-y=50", + "--always-on-top", + "--disable-screensaver", + "--window-borderless", + "-m", + "600", + "--video-codec", + "h264", + "--video-bit-rate", + "5M", + "--max-fps", + "60", + "--time-limit", + "3600", + "--print-fps", + "--video-buffer", + "100", + "--no-audio-playback", + "--no-audio", +] + +[dependency-groups] +dev = ["ruff>=0.12.2"] diff --git a/scenarios/tc_generator.py b/scenarios/tc_generator.py new file mode 100644 index 0000000..5723dd9 --- /dev/null +++ b/scenarios/tc_generator.py @@ -0,0 +1,477 @@ +import os +import re +import sys +import json +import time +import concurrent.futures +from dataclasses import dataclass +from pathlib import Path + +from typing import Callable, Any + +import pytest +import yaml +from glrocky.core.logger import logger +from glrocky.framework.marks import Marks as M + +from glrocky.services.dify.dify import run_workflow +from loguru._logger import Logger +from pydantic import BaseModel, ConfigDict, Field, field_serializer, create_model +import socket + +PRODUCTION_ENV_NAME = "GLROCKY_PRODUCTION_MODE" +PRODUCTION_EXECUTOR = "192.168.0.139" +YAML_FILE = Path(__file__).parent / "test_cases.yaml" + +def _get_local_ip() -> str: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 80)) + ip: str = s.getsockname()[0] + return str(ip) + finally: + s.close() + + +def _is_production_mode() -> bool: + env_val = os.getenv(PRODUCTION_ENV_NAME) + if env_val is not None: + return env_val.lower() in ("true", "yes", "1") + return _get_local_ip() == PRODUCTION_EXECUTOR + + +def pre_process_material(input: list[str]) -> str: + + assert isinstance(input, list) + if not input: + return json.dumps([], ensure_ascii=False) + result = [] + for text in input: + splited = _Q_PATTERN.split(text, maxsplit=50) + for part in splited: + if not part.strip(): + continue + result.append(part.strip()) + return json.dumps(result, ensure_ascii=False) + + +IS_PRODUCTION = _is_production_mode() + +METRIC_FIELD_MAPPING: dict[str, str] = { + "result": "result|结果", + "_dify_result": "_dify_result|业务完成", + "_dify_message": "_dify_message|业务错误信息", + "resultText": "resultText|文本结果", + "recordVideo": "recordVideo|录像", + "recordAudio": "recordAudio|录音", + "screenshotList": "screenshotList|截图", + "firstToken": "firstToken|首字符时长", + "timeSeries": "timeSeries|时间序列", + "fileList": "fileList|文件", +} + +IMAGE_EXTENSIONS = {".jpg", ".png", ".gif", ".bmp"} +VIDEO_EXTENSIONS = {".mp4", ".mkv"} +_Q_PATTERN = re.compile(r"Q\d+\s*[::\.]+\s*", flags=re.MULTILINE) + + +@dataclass +class _TimeoutCtx: + client: Any = None + task_id: str | None = None + + def stop(self): + if self.client and self.task_id: + try: + logger.info(f"Stopping workflow: {self.task_id}") + self.client.stop_workflow(self.task_id) + logger.info(f"Stopped workflow: {self.task_id}") + except Exception as e: + logger.debug(f"Stop workflow failed: {e}") + + +class MetricManager: + def __init__(self, metric): + self.metric = metric + + def new_span(self, m_type: str = "default", m_id: str = "default", m_iter: int = 1): + self.metric.span(m_type, m_id, m_iter) + + def _smart_name_label(self, name: str, label: str | None) -> tuple[str, str]: + """retruns: (name,label)""" + if not name or not name.strip(): + raise ValueError("Empty name") + + if label: + return name, label + sep = next((s for s in ("|", ":") if s in name), None) + if sep: + _n, _l = name.split(sep, maxsplit=1) + return (_n, _l) + return name, name + + def _add_metric(self, name: str, value: Any, m_type: str, label: str | None = None): + _n, _l = self._smart_name_label(name, label) + self.metric.add(name=_n, label=_l, value=value, type=m_type) + + def add_text_metric(self, name: str, value: str, label: str | None = None): + self._add_metric(name, value, "text", label) + + def add_number_metric(self, name: str, value: float, label: str | None = None): + self._add_metric(name, value, "number", label) + + def add_image_metric(self, name: str, value: Path | str, label: str | None = None): + self._add_metric(name, str(value), "image", label) + + def add_video_metric(self, name: str, value: Path | str, label: str | None = None): + self._add_metric(name, value, "video", label) + + def send(self): + self.metric.send_all() + + def from_dict(self, the_dict: dict[str, Any]): + if not the_dict: + raise ValueError("dict is null") + for _k, v in the_dict.items(): + k = METRIC_FIELD_MAPPING.get(_k, _k) + logger.info(k) + + match v: + case None: + self.add_text_metric(name=k, value="") + case int() | float(): + self.add_number_metric(name=k, value=v) + case str(): + self.add_text_metric(name=k, value=v) + case Path(): + match v.suffix.lower(): + case s if s in IMAGE_EXTENSIONS: + self.add_image_metric(name=k, value=str(v.resolve())) + case s if s in VIDEO_EXTENSIONS: + self.add_video_metric(name=k, value=str(v.resolve())) + case _: + self.add_text_metric(name=k, value=str(v.resolve())) + case _: + raise TypeError(f"{type(v)} not supported") + + +class DifySettings(BaseModel): + difyUrl: str = Field( + title="Dify服务地址", + ) + difyWorkflowId: str = Field( + title="Dify工作流ID", + ) + difyApiKey: str = Field( + title="Dify API密钥", + ) + + +class MaterialForDify(BaseModel): + paramGroupUuid: list[str] = Field( + default_factory=list, title="UUID", description="UUID" + ) + inputTextList: list[str] = Field( + default_factory=list, + title="对话列表,支持多轮对话", + description="对话列表,支持多轮对话", + ) + prompt: list[str] = Field( + default_factory=list, title="提示词", description="提示词" + ) + model_config = ConfigDict(arbitrary_types_allowed=True) # allows uuid extra... + + +class DifyPayload(BaseModel): + inputTextList: list[str] | str + prompt: list[str] | str + address: str + caseId: str + parameters: dict[str, Any] |str| None = None + + @field_serializer("inputTextList") + def serialize_input_text(self, value: list[str] | str) -> str: + if isinstance(value, list): + return pre_process_material(value) + return value + + @field_serializer("prompt") + def serialize_prompt_as_single_str(self, value: list[str] | str) -> str: + if isinstance(value, list): + return value[0] if len(value) > 0 else "" + return value + + +def create_dynamic_model(model_name: str, parameters: list[dict]) -> type[BaseModel]: + field_definitions = {} + for idx, param in enumerate(parameters): + name: str = ( + param.get("name", "").strip() if isinstance(param.get("name"), str) else "" + ) + if not name: + raise ValueError(f"参数定义错误: 第 {idx + 1} 个参数缺少 'name' 字段或为空") + + label = ( + param.get("label", "").strip() + if isinstance(param.get("label"), str) + else name + ) + description = ( + param.get("description", label).strip() + if isinstance(param.get("description"), str) + else label + ) + param_type = ( + param.get("type", "string").strip() + if isinstance(param.get("type"), str) + else "string" + ) + + if param_type not in ("string",): + raise ValueError( + f"参数 '{name}' 不支持的类型 '{param_type}',目前仅支持 'string'" + ) + + default = param.get("defaultValue", "") + if default and isinstance(default, str): + default = default.strip() + + field_definitions[name] = ( + str, + Field(default=default, title=label, description=description), + ) + + try: + model = create_model(model_name, __base__=BaseModel, **field_definitions) + model.model_config["extra"] = "ignore" + + model() + return model + except Exception as e: + raise ValueError(f"创建参数模型 '{model_name}' 失败: {e}") + + +def call_dify( + case_meta_info: dict[str, str], + logger: Logger, + material: list[MaterialForDify], + dify_cfg: DifySettings, + metric, + material_reporter, + parameters: dict[str, Any] | None = None, + timeout_sec: int | None = None, +): + # logger.info(device_info.device_serial) + logger.info(dify_cfg) + + event_callbacks: dict[str, Callable[..., None]] = {} + + def on_node_started(_, __, d: dict[str, Any]): + logger.info(f"开始执行:{d.get('title', '')}") + logger.info(f"输入节点参数:{d.get('inputs')}") + + def on_node_finished(_, __, d): + logger.info(f"结束执行:{d.get('title', '')}") + logger.info(f"节点输出:{d.get('outputs')}") + + event_callbacks["on_node_started"] = on_node_started + event_callbacks["on_node_finished"] = on_node_finished + + if timeout_sec: + ctx = _TimeoutCtx() + + def on_workflow_started(client, event_name, event_data): + logger.info("开始执行dify任务") + ctx.client = client + ctx.task_id = event_data.get("task_id") + + event_callbacks["on_workflow_started"] = on_workflow_started + if not material: + raise RuntimeError("缺少素材") + else: + logger.info(f"下发素材:{material}") + metric_manager = MetricManager(metric) + local_ip = _get_local_ip() + + for material_index, item in enumerate(material, 1): + paramGroupUuid = item.paramGroupUuid[0] + assert paramGroupUuid, "The param Group Uuid Value must be set." + + material_reporter.begin(paramGroupUuid) + dify_final_status = False + try: + # IMPORTANT: the group uuid for params + payload_data = { + **item.model_dump(), + "address": local_ip, + "caseId": case_meta_info.get("id", "Unknown"), + } + if parameters: + payload_data["parameters"] = json.dumps(parameters) + payload = DifyPayload(**payload_data) + logger.info(f"payload send to dify:\n{payload.model_dump_json(indent=2)}\n") + metric_manager.new_span( + "default", f"default-{material_index}", material_index + ) + + def _run_workflow(): + return run_workflow( + api_key=dify_cfg.difyApiKey, + base_url=dify_cfg.difyUrl, + workflow_id=dify_cfg.difyWorkflowId, + inputs=payload.model_dump(exclude_none=True), + event_callbacks=event_callbacks, + ) + + if timeout_sec: + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(_run_workflow) + try: + result = future.result(timeout=timeout_sec) + except concurrent.futures.TimeoutError: + logger.error(f"Workflow timeout after {timeout_sec}s,(yaml config)") + time.sleep(0.1) + logger.info("尝试停止工作流...") + ctx.stop() + raise TimeoutError(f"Workflow timeout after {timeout_sec}s") + else: + result = _run_workflow() + logger.info(f"工作流返回结果:{result.outputs}") + logger.info(f"工作流最终状态:{result.success}") + + if result.outputs: + logger.info("提交结果到执行器") + metric_manager.from_dict(result.outputs) + metric.send_all() + logger.info("提交结果到执行器完成") + else: + logger.warning("dify 工作流无返回") + if not result.success: + logger.error(f"工作流执行失败:{result.error}") + dify_final_status = result.success + except Exception as e: + logger.error(e) + dify_final_status = False + continue # do not break next round + finally: + material_reporter.end_with(paramGroupUuid, dify_final_status) + + +def make_dify_test( + meta: dict[str, str], + dify_settings: DifySettings | None = None, + param_model: type[BaseModel] | None = None, + timeout: int | None = None, +) -> Callable[..., None]: + if param_model is None: + param_model = create_model("EmptyParams", __base__=BaseModel) + + @pytest.mark.usefixtures("cfg") + def _func_impl( + logger: Logger, + material: list[MaterialForDify], + metric, + material_reporter, + cfg: param_model, + ) -> None: + param_dict = cfg.model_dump() if cfg else {} + call_dify( + meta, + logger, + material, + dify_cfg=dify_settings, + metric=metric, + material_reporter=material_reporter, + parameters=param_dict, + timeout_sec=timeout, + ) + + # always use cfg fixture + # _func_impl = pytest.mark.usefixtures("cfg")(_func_impl) + return M.meta(**meta)(_func_impl) + + +def make_skip_test(meta: dict[str, str]) -> Callable[..., None]: + @M.skip(reason=f"{meta['id']} 未实现") + @M.meta(**meta) + def _func( + logger: Logger, + material: list[MaterialForDify], + metric, + ) -> None: + + assert False, "not implemented" + + return _func + + +def generate_cases_from_yaml(module_name: str, yaml_path: Path): + if not yaml_path.exists(): + logger.warning(f"Test case definition file not found: {yaml_path}") + return + + try: + with open(file=yaml_path, mode="r", encoding="utf-8") as f: + all_cases: list[dict[str, Any]] = yaml.safe_load( # pyright: ignore[reportAny] + f + ) + except yaml.YAMLError as e: + logger.error(f"Failed to parse YAML file {yaml_path}: {e}") + pytest.fail(f"YAML解析失败: {yaml_path}") + return + if not all_cases: + return + for case_info in all_cases.get("cases", []): + if not isinstance(case_info, dict): + logger.debug(f"Skipping non-dictionary item in YAML file: {case_info}") + continue + case_id: str = case_info.get("id", "") + if not case_id: + logger.warning(f"用例 缺少 case_id 字段。{case_info=}") + pytest.fail(f"用例 缺少 case_id 字段。{case_info=}") + description: str = case_info.get("description", "") + if not description: + logger.warning(f"用例{case_id} 缺少 description 字段。") + pytest.fail(f"用例{case_id} 缺少 description 字段。") + action: str = case_info.get("action", "skipped") + + meta = {"id": case_id, "description": description} + fn_name = f"test_{case_id.lower().replace('-', '_')}" + + if action == "dify": + dify_settings: DifySettings | None = None + param_model: type[BaseModel] | None = None + timeout = case_info.get("timeout") + + if parameters_def := case_info.get("parameters"): + model_name = f"{case_id.replace('-', '_')}_Params" + param_model = create_dynamic_model(model_name, parameters_def) + + if dify_config_block := case_info.get("dify_config"): + env_key = "production" if IS_PRODUCTION else "testing" + if env_config := dify_config_block.get(env_key): + dify_settings = DifySettings( + difyUrl=env_config.get("url"), + difyWorkflowId=env_config.get("workflow_id"), + difyApiKey=env_config.get("api_key"), + ) + else: + logger.warning( + f"No '{env_key}' config found for {case_id}, will use default." + ) + fn = make_dify_test(meta, dify_settings, param_model, timeout) + elif action == "skipped": + fn = make_skip_test(meta) + elif action == "custom": + logger.info(f"Case {case_id} is a custom test.") + continue + else: + logger.warning(f"Unknown action '{action}' for case {case_id}. Skipping.") + continue + + setattr(sys.modules[module_name], fn_name, fn) + logger.info(f"Generated {fn_name} for {case_id} with action '{action}'") + + + +generate_cases_from_yaml(__name__, YAML_FILE) diff --git a/scenarios/test_cases.yaml b/scenarios/test_cases.yaml new file mode 100644 index 0000000..9878e1d --- /dev/null +++ b/scenarios/test_cases.yaml @@ -0,0 +1,15 @@ +dify_templates: + tianjin_dify_config: &testing_config + url: "http://192.168.0.213:8090/v1" + workflow_id: "2ca605b8-3354-4a8a-92b8-b4bbb37cce94" + api_key: "app-xTY5Wsl9QdJaVULU6C5IpXOz" +cases: + - id: TC-0201-img + description: 日程管理 + category: AI办公.日程管理 + action: dify + dify_config: + testing: + url: "http://192.168.0.213:8090/v1" + workflow_id: "2ca605b8-3354-4a8a-92b8-b4bbb37cce94" + api_key: "app-xTY5Wsl9QdJaVULU6C5IpXOz" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f94cc5d --- /dev/null +++ b/uv.lock @@ -0,0 +1,523 @@ +version = 1 +revision = 2 +requires-python = "==3.12.6" + +[[package]] +name = "adbutils" +version = "2.9.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "deprecation" }, + { name = "pillow" }, + { name = "requests" }, + { name = "retry2" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/89/26/d522ca7dd18e51e48952046e5f26b0436532f1f433f9d1376f3b4700f99e/adbutils-2.9.3.tar.gz", hash = "sha256:9adb922796f1096cb5bfebc6754f419ec781136b3f38a5588ca58a07bec26147" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/66/3b/616c81dc14b8cf03ffb3202365d2593a3e5aad5c91356b6e41090e3b56cb/adbutils-2.9.3-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:db11a3273f755f50bd7d80d14feb470ad5f02fe73d3d6b8442a1408374a5174b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/27/5da73f56315694de3bbbbb3ded61199b4ea4a714e19df5ad5285f9b2534f/adbutils-2.9.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:fe91d59324dfcdebf01cd76e843467d8efb1ab78bd697bc81ec9b83918dc9c8d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/9c/fe66312844bd048190fc3cbb803c19eb57a2679f1ea0aea5e2f6dd2ef4a1/adbutils-2.9.3-py3-none-win32.whl", hash = "sha256:82cf82ad5b4ff74cbe44c54c9a92364c906c6fd93eba25b1f47941862140edfa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/56/ecc00bcb16dc7358573f9886571281ed4dfdb14d20c47783301b046d96f7/adbutils-2.9.3-py3-none-win_amd64.whl", hash = "sha256:73f6ca710bdfb9bc72d9b7e5c9c51325751c1e43d4bdc98b9cee910e2e97588c" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981" }, + { url = "https://mirrors.aliyun.com/pypi/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a" }, +] + +[[package]] +name = "fastapi" +version = "0.115.14" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca" }, +] + +[[package]] +name = "glrocky-sdk" +version = "0.6.1" +source = { path = "glrocky_sdk-0.6.1-py3-none-any.whl" } +dependencies = [ + { name = "fastapi" }, + { name = "loguru" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "uiautomator2" }, + { name = "uvicorn" }, +] +wheels = [ + { filename = "glrocky_sdk-0.6.1-py3-none-any.whl", hash = "sha256:3a4ef036cbc30adea62ed01914a7bf9d81bf33848e1fe5534dc3252d8de23ba1" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "loguru", specifier = "==0.7.3" }, + { name = "pillow", specifier = ">=11.2.1" }, + { name = "pydantic", specifier = "==2.11.3" }, + { name = "pytest", specifier = "==8.3.5" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "tenacity", specifier = ">=9.1.2" }, + { name = "uiautomator2", specifier = ">=3.2.9" }, + { name = "uvicorn" }, +] + +[[package]] +name = "gztel-wifi-e2e" +version = "0.3.1" +source = { virtual = "." } +dependencies = [ + { name = "glrocky-sdk" }, + { name = "pyyaml" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "glrocky-sdk", path = "glrocky_sdk-0.6.1-py3-none-any.whl" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "uvicorn", specifier = ">=0.34.2" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.12.2" }] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c" }, +] + +[[package]] +name = "lxml" +version = "5.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779" }, + { url = "https://mirrors.aliyun.com/pypi/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20" }, + { url = "https://mirrors.aliyun.com/pypi/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941" }, + { url = "https://mirrors.aliyun.com/pypi/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" }, + { url = "https://mirrors.aliyun.com/pypi/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" }, + { url = "https://mirrors.aliyun.com/pypi/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c" }, +] + +[[package]] +name = "retry2" +version = "0.9.5" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "decorator" }, +] +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/97/49/1cae6d9b932378cc75f902fa70648945b7ea7190cb0d09ff83b47de3e60a/retry2-0.9.5-py2.py3-none-any.whl", hash = "sha256:f7fee13b1e15d0611c462910a6aa72a8919823988dd0412152bc3719c89a4e55" }, +] + +[[package]] +name = "ruff" +version = "0.12.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce" }, + { url = "https://mirrors.aliyun.com/pypi/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342" }, + { url = "https://mirrors.aliyun.com/pypi/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51" }, +] + +[[package]] +name = "uiautomator2" +version = "3.3.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "adbutils" }, + { name = "lxml" }, + { name = "pillow" }, + { name = "requests" }, + { name = "retry2" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6c/9c/83c3c7a6071133deced5d1ca4b3dc432b3f658d172f4908dd45beb6c2138/uiautomator2-3.3.3.tar.gz", hash = "sha256:15e5425dba73913979a234dae917f576f91a6862784af816042ab0ba4d94fa1e" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/0b/50/d41b28eb876ad7049215a0bf1e28725d9b900ca797c6bc78b72bb712307f/uiautomator2-3.3.3-py3-none-any.whl", hash = "sha256:8e1b0bdf788f15e1c81cf53817d066a908301c64f040aa4d7a8407e999f24328" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" }, +]