From 103a2247014c93d86fe6b4d31144a2f945074bd5 Mon Sep 17 00:00:00 2001 From: sardor014 Date: Sat, 27 Sep 2025 16:36:32 +0300 Subject: [PATCH 1/5] Implement ASGI application with fibonacci, factorial, and mean endpoints --- hw1/app.py | 116 +++++++++++++++++++++++++++++++++++- hw1/utils/math_functions.py | 55 +++++++++++++++++ hw1/utils/utils.py | 38 ++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 hw1/utils/math_functions.py create mode 100644 hw1/utils/utils.py diff --git a/hw1/app.py b/hw1/app.py index 6107b870..24ad9f25 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,5 +1,33 @@ +from http import HTTPStatus +import json +from utils.utils import ( + check_request_valid, + create_message, + create_start_message +) +from utils.math_functions import ( + factorial, + fibonacci, + mean, + validate_list, + validate_number +) from typing import Any, Awaitable, Callable +import urllib + + +async def read_body( + receive: Callable[[], Awaitable[dict[str, Any]]] +) -> str: + body = b"" + while True: + message = await receive() + body += message.get("body", b"") + if not message.get("more_body", False): + break + return body + async def application( scope: dict[str, Any], @@ -12,7 +40,93 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + + if scope['type'] == 'http': + method = scope["method"] + path = scope["path"] + + if not check_request_valid(method, path): + status_code = HTTPStatus.NOT_FOUND.value + body = { + "result": HTTPStatus.NOT_FOUND.phrase + } + + input_body = await read_body(receive) + + if path == "/factorial": + query_string = scope["query_string"] + parsed_query = urllib.parse.parse_qs(query_string.decode("utf-8")) + if parsed_query.get("n"): + n = parsed_query["n"][0] + else: + n = None + + validation_result = validate_number(n) + if validation_result: + status_code, data = validation_result + else: + data = factorial(n) + status_code = HTTPStatus.OK.value + + body = { + "result": data + } + + elif "/fibonacci" in path: + n = path.split("/")[-1] + + validation_result = validate_number(n) + if validation_result: + status_code, data = validation_result + else: + data = fibonacci(n) + status_code = HTTPStatus.OK.value + + body = { + "result": data + } + + elif path == "/mean": + input_body = input_body.decode() + try: + json_data = json.loads(input_body) + except json.decoder.JSONDecodeError: + json_data = [] + + validation_result = validate_list(json_data) + + if validation_result: + status_code, data = validation_result + else: + data = mean(json_data) + status_code = HTTPStatus.OK.value + + body = { + "result": data + } + + await send( + create_start_message( + status_code=status_code + ) + ) + + await send( + create_message( + body=body + ) + ) + + elif scope["type"] == "lifespan": + while True: + message = await receive() + t = message.get("type") + if t == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif t == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + if __name__ == "__main__": import uvicorn diff --git a/hw1/utils/math_functions.py b/hw1/utils/math_functions.py new file mode 100644 index 00000000..53c1dc49 --- /dev/null +++ b/hw1/utils/math_functions.py @@ -0,0 +1,55 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +from utils.utils import is_digit + + +def validate_number( + n: str +) -> Optional[tuple[int, str]]: + if not n or not is_digit(n): + return HTTPStatus.UNPROCESSABLE_ENTITY.value, HTTPStatus.UNPROCESSABLE_ENTITY.phrase + + n = int(n) + + if n < 0: + return HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.phrase + + +def validate_list( + l: Any +) -> Optional[tuple[int, str]]: + if isinstance(l, list) and l == []: + return HTTPStatus.BAD_REQUEST.value, HTTPStatus.BAD_REQUEST.phrase + + if not l: + return HTTPStatus.UNPROCESSABLE_ENTITY.value, HTTPStatus.UNPROCESSABLE_ENTITY.phrase + + +def factorial( + n: str +) -> int: + n = int(n) + + factorial = 1 + for i in range(1, n + 1): + factorial *= i + + return factorial + + +def fibonacci( + n: str +) -> int: + n = int(n) + + if n <= 1: + return n + else: + return fibonacci(n - 1) + fibonacci(n - 2) + + +def mean( + nums: list[Union[int, float]] +) -> Union[int, float]: + return sum(nums)/len(nums) diff --git a/hw1/utils/utils.py b/hw1/utils/utils.py new file mode 100644 index 00000000..dabf7225 --- /dev/null +++ b/hw1/utils/utils.py @@ -0,0 +1,38 @@ +import json + + +def is_digit( + str_num: str +) -> bool: + if str_num.startswith("-"): + str_num = str_num[1:] + + return str_num.isdigit() + + +def check_request_valid( + method: str, + path: str +) -> bool: + return method == "GET" and path in ["/factorial", "/fibonacci", "/mean"] + + +def create_start_message( + status_code: int = 200 +) -> dict: + return { + 'type': 'http.response.start', + 'status': status_code, + 'headers': [ + (b'content-type', b'text/plain'), + ] + } + + +def create_message( + body: str +) -> dict: + return { + 'type': 'http.response.body', + 'body': json.dumps(body).encode("utf-8"), + } From 56036024327f5d63aa7716f6465ebc5ada981ecf Mon Sep 17 00:00:00 2001 From: sardor014 Date: Sun, 5 Oct 2025 16:56:39 +0300 Subject: [PATCH 2/5] [HW2] create Shop API --- hw2/hw/shop_api/handlers/cart.py | 108 +++++++++++++++++++++ hw2/hw/shop_api/handlers/item.py | 158 +++++++++++++++++++++++++++++++ hw2/hw/shop_api/local_data.py | 59 ++++++++++++ hw2/hw/shop_api/main.py | 6 ++ hw2/hw/shop_api/models/cart.py | 14 +++ hw2/hw/shop_api/models/item.py | 23 +++++ 6 files changed, 368 insertions(+) create mode 100644 hw2/hw/shop_api/handlers/cart.py create mode 100644 hw2/hw/shop_api/handlers/item.py create mode 100644 hw2/hw/shop_api/local_data.py create mode 100644 hw2/hw/shop_api/models/cart.py create mode 100644 hw2/hw/shop_api/models/item.py diff --git a/hw2/hw/shop_api/handlers/cart.py b/hw2/hw/shop_api/handlers/cart.py new file mode 100644 index 00000000..727c31fc --- /dev/null +++ b/hw2/hw/shop_api/handlers/cart.py @@ -0,0 +1,108 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response +from fastapi.responses import JSONResponse +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt + +from shop_api.models.cart import CartOutSchema +from shop_api import local_data + + +router = APIRouter(prefix="/cart") + + +@router.post( + "", + response_model=CartOutSchema, + status_code=HTTPStatus.CREATED +) +async def add_cart(response: Response): + cart_id = str(uuid.uuid4()) + cart_data = {"id": cart_id} + + local_data.add_single_cart(cart_data=cart_data) + + response.headers["Location"] = f"/cart/{cart_id}" + + return cart_data + + +@router.get( + "/{cart_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def get_cart_by_id(cart_id: str): + cart_data = local_data.get_single_cart(cart_id=cart_id) + + return cart_data + + +@router.get( + "", + response_model=List[CartOutSchema], + status_code=HTTPStatus.OK +) +async def get_all_carts( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + min_quantity: Annotated[NonNegativeInt, Query()] = None, + max_quantity: Annotated[NonNegativeInt, Query()] = None, +): + all_carts = local_data.get_all_carts() + + filtered_carts: List[CartOutSchema] = [] + for cart in all_carts: + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + if min_quantity is not None and sum([item.quantity for item in cart.items]) < min_quantity: + continue + if max_quantity is not None and sum([item.quantity for item in cart.items]) > max_quantity: + continue + + filtered_carts.append(cart) + + filtered_carts = filtered_carts[offset: offset + limit] + + return filtered_carts + + +@router.post( + "/{cart_id}/add/{item_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def add_item_to_cart( + cart_id: str, + item_id: str +): + cart = local_data.get_single_cart(cart_id=cart_id) + + if cart is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Cart with {cart_id=!r} wasn't found", + ) + + item_ids = local_data.get_all_item_ids_for_cart( + cart_id=cart_id + ) + + if item_id in item_ids: + for item in cart.items: + if item_id == item.id: + item.quantity += 1 + else: + cart.items.append( + local_data.get_single_item( + item_id=item_id + ) + ) + + return cart diff --git a/hw2/hw/shop_api/handlers/item.py b/hw2/hw/shop_api/handlers/item.py new file mode 100644 index 00000000..8f16b6b4 --- /dev/null +++ b/hw2/hw/shop_api/handlers/item.py @@ -0,0 +1,158 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response +from fastapi.responses import JSONResponse +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt + +from shop_api.models.cart import CartOutSchema +from shop_api import local_data +from shop_api.models.item import ItemPatchSchema, ItemSchema, ItemCreateSchema + + +router = APIRouter(prefix="/item") + + +@router.post( + "", + response_model=ItemSchema, + status_code=HTTPStatus.CREATED, +) +async def add_item( + response: Response, + item: ItemCreateSchema +): + item_id = str(uuid.uuid4()) + item_data = { + "id": item_id, + **item.model_dump() + } + + local_data.add_single_item( + item_id=item_id, + item_data=item_data + ) + + response.headers["Location"] = f"/item/{item_id}" + + return item_data + + +@router.get( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def get_item_by_id(item_id: str): + item_data = local_data.get_single_item(item_id=item_id) + if item_data.deleted: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} was deleted" + ) + return item_data + + +@router.get( + "", + response_model=List[ItemSchema], + status_code=HTTPStatus.OK +) +async def get_all_items( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + show_deleted: Annotated[bool, Query()] = False, +): + all_items = local_data.get_all_items() + + filtered_items: List[ItemSchema] = [] + for item in all_items: + if min_price and item.price < min_price: + continue + if max_price and item.price > max_price: + continue + if not show_deleted and item.deleted: + continue + + filtered_items.append(item) + + filtered_items = filtered_items[offset: offset + limit] + + return filtered_items + + +@router.put( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item( + item_id: str, + item: ItemSchema +): + if local_data.get_single_item(item_id=item_id) is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + local_data.add_single_item( + item_id=item_id, + item_data=item + ) + + return item + + +@router.patch( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item_fields( + item_id: str, + item: ItemPatchSchema +): + old_item = local_data.get_single_item(item_id=item_id) + if old_item is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + if old_item.deleted: + raise HTTPException( + HTTPStatus.NOT_MODIFIED, + f"Item with {item_id=!r} has been deleted" + ) + + update_data = item.model_dump(exclude_unset=True) + updated_item = old_item.model_copy(update=update_data) + + local_data.add_single_item( + item_id=item_id, + item_data=updated_item + ) + + return updated_item + + +@router.delete( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def delete_item(item_id: str): + item_data = local_data.get_single_item(item_id=item_id) + if item_data is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + local_data.delete_item(item_id=item_id) + return item_data + \ No newline at end of file diff --git a/hw2/hw/shop_api/local_data.py b/hw2/hw/shop_api/local_data.py new file mode 100644 index 00000000..4ba9ea0c --- /dev/null +++ b/hw2/hw/shop_api/local_data.py @@ -0,0 +1,59 @@ +from pydantic import validate_call +from typing import Dict, List, Set, Union +from shop_api.models.cart import CartOutSchema +from shop_api.models.item import ItemSchema + + +_local_data_carts: Dict[str, CartOutSchema] = {} +_local_data_items: Dict[str, ItemSchema] = {} + + +@validate_call +def add_single_cart(cart_data: CartOutSchema) -> None: + cart_id = cart_data.id + _local_data_carts[cart_id] = cart_data + + +def get_single_cart( + cart_id: str +) -> Union[CartOutSchema, None]: + if cart_id not in _local_data_carts: + return None + + return _local_data_carts[cart_id] + + +@validate_call +def get_all_carts() -> List[CartOutSchema]: + return list(_local_data_carts.values()) + + +@validate_call +def add_single_item( + item_id: str, + item_data: ItemSchema +) -> None: + item_data.id = item_id + _local_data_items[item_id] = item_data + + +@validate_call +def get_single_item(item_id: str) -> Union[ItemSchema, None]: + if item_id not in _local_data_items: + return None + + return _local_data_items[item_id] + + +@validate_call +def get_all_items() -> List[ItemSchema]: + return list(_local_data_items.values()) + + +def get_all_item_ids_for_cart(cart_id: str) -> Set[str]: + return set(list(_local_data_carts[cart_id].model_fields.keys())) + + +def delete_item(item_id: str) -> None: + item = _local_data_items.get(item_id) + item.deleted = True diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..7b21b4df 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,9 @@ from fastapi import FastAPI +from shop_api.handlers.cart import router as cart_router +from shop_api.handlers.item import router as item_router + app = FastAPI(title="Shop API") + +app.include_router(cart_router) +app.include_router(item_router) diff --git a/hw2/hw/shop_api/models/cart.py b/hw2/hw/shop_api/models/cart.py new file mode 100644 index 00000000..b2e932b0 --- /dev/null +++ b/hw2/hw/shop_api/models/cart.py @@ -0,0 +1,14 @@ +from typing import List +from pydantic import BaseModel, Field, computed_field + +from shop_api.models.item import ItemSchema + + +class CartOutSchema(BaseModel): + id: str + items: List[ItemSchema] = Field(default_factory=list) + + @computed_field + @property + def price(self) -> float: + return sum(item.price * item.quantity for item in self.items) diff --git a/hw2/hw/shop_api/models/item.py b/hw2/hw/shop_api/models/item.py new file mode 100644 index 00000000..a708bc1a --- /dev/null +++ b/hw2/hw/shop_api/models/item.py @@ -0,0 +1,23 @@ +import uuid +from pydantic import BaseModel, ConfigDict + + +class ItemSchema(BaseModel): + id: str = str(uuid.uuid4()) + name: str + price: float + deleted: bool = False + quantity: int = 1 + + +class ItemCreateSchema(BaseModel): + name: str = "" + price: float = 0.0 + + +class ItemPatchSchema(BaseModel): + name: str = "" + price: float = 0.0 + quantity: int = 1 + + model_config = ConfigDict(extra='forbid') From fa9b89143282db216ab8d5a6205b1133eebea446 Mon Sep 17 00:00:00 2001 From: sardor014 Date: Sat, 11 Oct 2025 21:17:58 +0300 Subject: [PATCH 3/5] [HW3] set up prometheus and grafana with docker --- hw3/Dockerfile | 23 ++++ hw3/README.md | 13 ++ hw3/docker-compose.yml | 32 +++++ hw3/grafana_examples/image.png | Bin 0 -> 112567 bytes hw3/requirements.txt | 3 + hw3/settings/prometheus/prometheus.yml | 10 ++ hw3/shop_api/__init__.py | 0 hw3/shop_api/handlers/cart.py | 108 +++++++++++++++++ hw3/shop_api/handlers/item.py | 158 +++++++++++++++++++++++++ hw3/shop_api/local_data.py | 59 +++++++++ hw3/shop_api/main.py | 12 ++ hw3/shop_api/models/cart.py | 14 +++ hw3/shop_api/models/item.py | 23 ++++ 13 files changed, 455 insertions(+) create mode 100644 hw3/Dockerfile create mode 100644 hw3/README.md create mode 100644 hw3/docker-compose.yml create mode 100644 hw3/grafana_examples/image.png create mode 100644 hw3/requirements.txt create mode 100644 hw3/settings/prometheus/prometheus.yml create mode 100644 hw3/shop_api/__init__.py create mode 100644 hw3/shop_api/handlers/cart.py create mode 100644 hw3/shop_api/handlers/item.py create mode 100644 hw3/shop_api/local_data.py create mode 100644 hw3/shop_api/main.py create mode 100644 hw3/shop_api/models/cart.py create mode 100644 hw3/shop_api/models/item.py diff --git a/hw3/Dockerfile b/hw3/Dockerfile new file mode 100644 index 00000000..43812477 --- /dev/null +++ b/hw3/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12 AS base + +ARG PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PIP_NO_CACHE_DIR=on \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=500 + +RUN apt-get update && apt-get install -y gcc +RUN python -m pip install --upgrade pip + +WORKDIR $APP_ROOT/src +COPY . ./ + +ENV VIRTUAL_ENV=$APP_ROOT/src/.venv \ + PATH=$APP_ROOT/src/.venv/bin:$PATH + +RUN pip install -r requirements.txt + +FROM base as local + +CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/hw3/README.md b/hw3/README.md new file mode 100644 index 00000000..aad28c54 --- /dev/null +++ b/hw3/README.md @@ -0,0 +1,13 @@ +# ДЗ + +## Настроить сборку образов Docker и мониторинг с помощью Prometheus и Grafana + +Интегрировать Docker с Prometheus и Grafana в любой уже написанный в ДЗ сервис (по аналогии с тем, как в репе) + +По сути, если вы выполнили вторую домашку, то теперь для неё надо написать Dockerfile и настроить мониторинг. Если вторую домашку вы не делали, то можно взять сервис из [rest_example](../hw2/rest_example/main.py) + +Сдача через PR, так же нужно: + +1) Dockerfile для сборки сервиса +2) docker-compose.yml для локального разворачивания в Docker +3) Приложить скрин с парой Дашбордов в Grafana diff --git a/hw3/docker-compose.yml b/hw3/docker-compose.yml new file mode 100644 index 00000000..a1d7be69 --- /dev/null +++ b/hw3/docker-compose.yml @@ -0,0 +1,32 @@ + +version: "3" + +services: + + local: + build: + context: . + dockerfile: ./Dockerfile + target: local + restart: always + ports: + - 8080:8080 + + grafana: + image: grafana/grafana:latest + ports: + - 3000:3000 + restart: always + + prometheus: + image: prom/prometheus + volumes: + - ./settings/prometheus/:/etc/prometheus/ + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/usr/share/prometheus/console_libraries" + - "--web.console.templates=/usr/share/prometheus/consoles" + ports: + - 9090:9090 + restart: always diff --git a/hw3/grafana_examples/image.png b/hw3/grafana_examples/image.png new file mode 100644 index 0000000000000000000000000000000000000000..18e0adf39808f0e2fb5f9acb6c118b6a9d24ae1f GIT binary patch literal 112567 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 =?K(K)ICTI!vm-+_!Z;1R4ebXJo7L_jHeCHc^w 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®3pm^BY)X2lc-97%##TMwCX zjHy7kLCPsQ+1K` zpjjGHF^N?-u1qbth*6A|C53(JO)(-HnAD9_DN4xIaVw)7_m9m$JXd2op@kn!8j4}dv8;9js#m8vz}8_c+2}0? zIBV@Wwjqb4WrOuMcZdCzIG*if6@6;gJ0&p_eM7`Nyneh=N&|I?_QK*9l)XmXvoANA z)M0jz7zRy=L1?AR+Y257f$=~=W)bz|{aj2C`gGw-;V;r-8_`MXG%DY8SQm_iW4di(LUek0OX1)!cW`aQ~$H{#E+ zJvJhy937!;imNRPuatx>U3v;6E^Y;7Lc8}2Tm`c1qv+Re#l}<(t{;D6n7aF7&VPBD zaKY4G)PG)Lc>-Qp+l;?2b*9FExgj*Hu(|fTNHop8eO_IA%tU}vsN!fgle3iiLbR?@ zZx86BS;@yTtZRPy9wD8>33Je<5>@Z`xZbUqxY|{bJkjhI$6sfyTNE1J>H)4%;xpAQ zB-$s18k2Eq0W)@| zAMuGxwbffg-0oJcFOcLnXtk`5jtaN?E>N~PA|0`y#7eAUU-6~C;8pJ;J`ZXY#(*?i zz8zp!^mF;5YMYvv9X#zy^1rQ~3coMUv2rX}UVDz-PZG5}xZcL%fR@yR8VjPK6;|x( z@PL|gwl}QE?Q0#O92!Yca(-Y<*=0TZ7ma*AcId81eABq}2?zYO48eYxrPGA>oSE}2 zox3FEVbl2s!#zr&Cx%~j6>5MuZhI;8l^QrnJPN%vAhL3;wB<&vZRD_2$2j_^zV*-% zs-w%>;63;%%)1=@b?Sx9?GA;-X6eDLN0X+w6x+DziF&g<|r|_t6!sXGpFsK8&#MTPFEDL zCOWR*MUqS`vh{ZU26s7Pk-8DPF6H0*O!(Uv+GWtH6*CGlsjY7e7=!QxuG{&&Cf}~i zii+->}j z@sk41*3Ro*f&ZpL25+9XB399AnE@rjk9QSw6@1RJtO|l{IAAkHSuOSS4dzYK&K->- z4}F+{7M+@` z!O6Nsb&#aT)p<%7#?hkYNphkum;$I%4KoOeoS%npoqS6lU~w+BDn|6p>`#GXfcyJ> z0t`DUAD@#YvfqB8)auYtJo3U(?Y{GfCy+2x)eyF&{hLk!aNREa@ez}=JaV;uqH!AS z(NrIZbn-8tUy>WzlQwc~2sQweIlMjAg4}_?NgeSG|G>FJHFf$V5#h3t1)Jng@{iYT zy@U4HWmc^1y@;p1pF{XinDK3Bzzt{T%7v%^6PaguqdBqIls$t;m@XZC$c@gb)?>>= zzSmyDNU82*X?Vk$%VfjK2uVLdLxYIZFtjtaa_Yi*y_ zq@_;2bz1RUU_ws}($>072MiaDd0-0Ez(6#sdf6H~x!7lj%JArWgF~CGHO+DpNrQeO zG6~#GDIfuOZ%G2@gsHN#=={QsfrvgT%_NRFBksYvHQI#P_h&;a}PaJ4KFh* z+S}SQbD-zXpjT9|=q}HKZRH>;@RMZsZYjtOuP2<9ntrm$0dvKj?tGho9h#95bSZPC zV)sW#UVm|9FM&>?w2ES+GCvrH;5}HhTYgb$VtpJCwbqHip(73fs@wNkxUNHwCuqO!{}fkhaN@&4)SP|1sPh z|0{Y%KR?`y;hgXy$7$In-}<^T?q&l&8_%D1(EkVesdeTo=96UxcXPRT+v|cf6%H(+ z_Pw2jvNYjUX><-DS$e9zbV`hiioS~6eaxBF0tqEF;`;*@))U9e-z%kUCBu0s-+&Uy z%95Um_0!3gv_eyFpAlTI0c485{mWSS{BAbQ+?|kske#r8^h;Q)?3T4Xko0kcc7)il zCm;yWS~A{h7vO9mQ+&w}2#1vTbo<3^-{p4kHOY*;h0Vh~Xw(9ww<1B-#K!IIm0_>b zZbT2nxVPy%afJ#DS>LHL|fO49Dy0(?u?$ygP4IFKpM(wv}M zYfI^^l&xu#5tTWIocKO49UGu3Kf8erk(q!qp=Py(+)`LZy%1Koh^1S zpfmoXhnryRV<&!3W@w9z6L7p5oQM*v{GeX zd`x$G>pM&>>g-xf*l3{F#^F;xQOO+64dkS^O@P|GmK|WYX&H80O=cEr1;nMIea2fm zkz1Tj%%jAvQw_0uT6hDs#L5NAee>pTCBv2S61<$j6aYd;{*4 zG$UUODR6yt6O##T`cI8!rmglTJA(L=G4^x6a4-L|6+p#k0QXb0)R5HcDv~IrBu&T- zdJCh3T=u@=n{|EjlTLut5{5xiIw4>H=b4dg8oD%+?NPWC7`aZ{j;SZ^8fT5yr>O>G zItGGjah<`ZjlIwFg5Bo5G8VPK4o$7KKC{~fnuD3L6EwKi<3KRIWV-9R^zy)WK;XNM z_DA}hL@WG8Y;5c-OMj2?`r#HbhS zeQaKZBXy=zY2rO5BPcMT#52vQjaJOLUpD8k&ib>CwKN@5`PN%}>ZaGaOaXW}Ay#Lv zcstL9)d4XwyyaF3r2^UD@{8(Q2{!?R?0;5-Kd%4)SSF8-2uG#XwX5OdxBC7vgA=D*PF--xBK;p~ zYy6hciBTc+t!v8U`fA-+dZv0<1l>Bnq{PjNoxBxF6 z$TP+;tcXR6IvT1OjJ5=V?*EG<|L{!iUXaV zD&95Y-t3QpARi2g^tdWL}{{)|~S4(5MiVZWUJqY?AST2;{YwexR}E`O8j zB;;%=5c=*;g_owhl;zS8`zgidaix5wVUmh zmOO0p=OYzO{(C^qBtp@4=pWeW%U3#OBG3RRa~?v_;x$Uow!XlpyH67@b$FqQ{XfQj zBKRve@UBAzzT95W<-M^D4{r5RQR&jTUyy$H?m$<278Cv-gHZI+Al3<6rzj-svtSm3 zXI+T=FIx_NdG-{a&wiNTd^aOF=2vF<_tE%gg7lr&$;P;E2iNZ&{XK{N$JHh6Uo@s} zwfXfpJ9BW+3%9R zP$?ARt~}E$+bR0nbep+!z7{isH@fA+Imphfm%o4820#h?y=eI|3{?Md6N%yW@nx|3 zeV(2NraW*bZ)P;FZlGfSKi2r)&Ha}V+#CfKwfDQiPY!-JsDB!SJODV+EBqrqsq=sP z&i}IwwclnT&vXK?Z(~gmNu)B=q$RK<@VM6h*zw?%V<#X0d$*PcX+E2kbB;nYt35?=@uhkDFi~ zT|i<{5=1T|AY;o9ZT-IS>h1(n3R$Bn)W@r>Jz5M3Z}>Qwr$v6$FAXghQk?q;oSq)K zu((=xoAYgm)34a7MrE=Q^pYhB0KikW_9Y8n0yknfKf%^3s*)2NbQHqZ>xVo+i^P>3`;)+5BrZ0Qlu4H8-u958~h$A8gvHy}lm*@HyQeGF=>r082!3Hpdr< zOb^bo1NxDo9WC^=enx)+jOkWVUicM*`yJdfrMG;=xlL+|d3j{6{=!WDN~Aj)*Fqb1 z6Q!&t>&h*7b+25xGCvzGkOXAp=V4&z^O~8i`o1IjNq37%*$kvh9Qg(#pNaYbk=3P# zKN8h`)%;euDLf$(;KXaB30WOPaBqKmF*V;po=~Zr9(R+P+y=;_|6Yb~!p>|L2w5MN zqI#4$khUH_b`I5?95b=UMmOU)wLbOKKE5-QV*+Q{yHwM1q$5jSy@Q_dUdPNF+$iOs znCyhs!W!UqyhZW~Z46qmD5i?P`;#~;RiQ%rvbaCI>CU~%pg zBb{G4a>OP8neLZbPuQ@5-C-3gqmjsbh53}foRN*f8BZ$P^sR_(ok`_iTY$-%H350t$F`$xsaR0E4U%Ji{iWe=J@$Uc?(vG{`Ov=CrU1eGf%nYf#$+c^251>4FEB08r9*Pv`x1rsk%I5tsY)_ z1i-7W44clX0B{aSq(~ENFb96WS!}JCkRt=GXsSpFbm6J^j+Fim?_p&q3OA{1m@`T9 zMu)X=U@C*lC+j*5!9o0xX3#Wze2vd!{1q5LVl4d3d0p+R8FdQY&#*lv`IhJ zltOUyuVj>4yk=bQ{XsqTu#vA=qN%BPsn;EnH$vDunnhWm+DoR>C>egQ^-CEo*Se$0G+xT&#PC= zCDTM7ubNOgw($#<{_w@yC+;Ofiy(NGd7;3MUC>sH@$_F!TQXwXQ-SzWDS7hvw zpsOBjYjx95yYWXooG|{wKDtK*s1=h!cOKmd4E75hs=)fe2agpg$OQGJ4=BIPBkZwk zj2eH7&d&8Cida2PLa!_Ky^Y<+yW>cy;SpVnfJ(;&`8)=#ss*Vcp>}kYh)%iu%8x4L zx)i@@aLe3e;~YfGxqldlrzohEmD7;ZJy2+uWd zg}?M7SsPC7O70}GE7T8$2J-!|_i-j(deX9Ei9nXjb7iIS0133Z@?jCwX{v}??Pa%- z`&~OigFnCd9eLG{i7cNET2R%`pHhgL+?6{zMDi^yT`hl-BDKM(d+EV_0H*YUSBLpQ zGH)sRkat(p?xuOxiX(ArPr(%KC&`fo-D}YpM&QhsCgAU{v#8@LNRVBgC?;-1Ww1_H z!P?X36V&Q$35I4bOzW70Lku!JY{)>gAXhDvp;PUeq;DCY-o`Z2)FAb7DziIBKy7&W zr=flKU-PG=CnG!LiOkiGWW13=E{OBss3MU#&Mr?@@A>-c$S6ki`h@oy^KoVG|4R$t zTsh;=sBX&KR)U!e~FqvSD-)K$W*TA8b5qQ!{rXiFzXm(xuinG}<6G{85-k!wqhLO45d) zaD8Z@?(4lmUrP*^ag)o3qxB6V2QjP7YmRk5?qZT)!797EE;F`XiKl9+#D21H7%k>& z3zga6p#l9l4HjB$-^xOwa&ULscGg>WVoL)PX?B0Y`vUK=c(PvuKk9kOQtR`+KAL>P z4#H0BgMmgZKN^v2x@9pcd!0CRZIGIo6PV2kal$As=B?khZ;rEJ3v*S0?5f~+yxKzO z1iZSuVCR(#I|P+3U#`X))Y+BttUX5lMElh-#qD8S>06H| z;X!VdNIsFJ(+zd!hpA(P`z0nLG^8FkdI$Unx=J<q=Pv7*(`U~_oFy#c4p^&qYdo z*@?bpLnD6h3QQBq+L!c+;q@1~xv+Cn*ijhgV51MtwCAAlN{xwx+##~^bA z%Ry?U(qEarU(~9zwYhl!>}iZT>fEkYY;CP4IgmW%1D>=EgrP_Wy_GX5wg)K}CR>G> zXzy9g;x{HNGf!zu(W0>qyj-%!hj&bKvdOd@d#}o$mYCy>jH}#JoXZMOX-Kj(cS@mQ z2MK3x#j9F-Yjo+M!a@k{erKxaF(Q*e%Q3J}<}k|U3MrHejRf{{H2G98$9%E^zt8J1T@1^u7#ai=#+^N=_ z^YR$&+O+`6^qK=-YXcP+Cm)c~DRG3+g|Ptd?5 zW%iAF6BB)!IHdY4ce$CLJ;t<-7~)Mp9X$t6BtSu*_||OsWp7h z-m$Dx(XsE>PplKnDUXF7_|@^U6X_+Z@)>Gx<}tBK_D%qa=Zy9Wtfc2SK*qT{rT0`_ zOn~T>R;h9FCjcbza-Lg$UFqpKqt;RAGP)|J`;O&#fYs9GDJ)cqP+Tl+e%i#Y&#{imbbh$&bhCmWXYdh z3eoF}8fi|!ve8Flzl4SVwW;3pJ;`S(fo%7J1SPUtvWPDW!)~HTl81@wd4MLH=h!5y zm#|e;n0s?zARDUCv}|mf!C91%Ii;%*Dm! z4CLmumd{A_xfTqmtj)-N;mv`CHP3TRC*SI|h2zzwEpNnLCYmsdH=?CWs`0G$;*?0Z ze=r4y9>nYltD|Vk)d&4lF}%9XGQVd0Mb_P@P*gdTG$!(fV3?v)X7q5}+5e|6wMo|; zd+k>8Ubh?lbboNo2Ql~Ia{=Pwj+SqqNud&_YB{wW92(6lCd*A*0mLZnnG1iN{Z5=B z30z`I;8uA*-ln^petZ3qDaFo}L&-iXMc~^7lag$pGD_&%`e~@f%8hmCnp4e4#4!Gh z?KJaIh4~LJyP`%fd|KvtPBziSGTq=B6l1!#lbn!o?w-}a<8TH#HW7{J<}*d)y_PmY z(UMI-Z?t9RtI9xoiaQ6VBe?E^aKy_ZiK!?!4CXBk)tUtL0C-~i&{L(h)X<4s^<&Y8 zA-C47XZiM3i2xv%vp%h2#bjVuMNX7MvM8eH=NgME)}Z5J%U-HY-HAd%TCeFrT&dt{ zi}}qg273BFI9l(fANBOm+;Xb=6A2-`bmnU5v(ENQZ(dU!WzpE00$k|7LeOUfvfDJ5 z(;2M0<71{}s3xb`VX36EqGz_#v!40CnyfhI61pdHx!GlrPok}5Mf$d6S=qTFvlsyF z0o@qdy)4z6Xi%{fT{9tU8hXvqF9~Pa4;y$?C1b%t#Q7+T@11;WX7~U&R^Dc4} z;sR$~(Yw7HSIS`q9qW6%c5URhPlMvzskbXf{fd^B!WJHwb#y6hT;?m z#n15WxAq{TeZfNlE(cYUZ;utbUP2$3d5?=nd@qJ(xE`uhjGet2_ya+T_0le&yt3_U zL?N8w#oYGu=#%kEXc~-3a?(RcH=zBw*Th@P#(@S|) zhJmm3KJRyaDiBlhBBlESR}N9h0zo9XwT+M=EWyyk(c8GQXR53P#j}P$%liG%)YO#G z#Dy@64VapXg6UgeK5(u%a4wzIPFC9c!zcWM9)fk+uVQkVr6bLOiubZd;|M*Ik16g2Q#(dv(lmv=WSy z*aNU22By`CNku0S8hb7YSmLzsn)K?%Z0H~6a?APXhP}*Ao*wGm!K@_xNX&J7NM+XH zPUyO)L`4CL731*0ot}ANZx8ez;FwWDpx&2t`R&CW%6GKu=l4Enwl{J$HP1 zWckCIMYvS77vhLtKRx?GLt!-i)BIlkvc%wXSDpZz?m2xYh7?=aMkK z1$K2{w#GRqZdALPOAmB@7vFAQKooc@mkbr60O(uJIbgwYCHnMNWHMY!QKq14(rdHWVh_VR=!3xettuJspMvS zMn;B`oZu{!do&5dx;Cj27I%w=8W5ts>&ks5cdQ{_owN&a@shi*>9&=AvJ&jU2Vw<*HFQXYHtP)mbmvQh&&qqt14&h`=a-MEi)%stu_tl!)k;ATTn4oTfh!vd@HFFq{X z#Z#}XjVcRWICbgINe-B7w)?4>jkk{I@`pf>A8Z!l@27}X3qqlZT0xcgYl5GRA@tR) zr+0*O1iP-xkK;1E`QKY-l=+5y*(j`zAWfhJgxwXq8uY(E*}83LS*;Vp%7qv_==W)z zslcZ`_cr4EzWA24QWU>NbUk{zKb!@H@G%@n-iu%!S~#^*QiMx!Yg%)e4mh~J-hLCw zxWmSpCCF3KqN6e@dB7&-gf{Op#ctEtUeCp3Fw{`|g}&b4LncxX-F9BWdOulN_d|CTD6sbjteOe(9P>-H+{ zjBK!QFfS;9RA(pMRuORM;z3>$I6jqC-(bj~O==|`0aTg|D4IMa#uL?aX`h&JOnDo} zfgs0LD+R^IY6zUP*n($bUmW!1K&SCAk25RJuKqTllrskr*Fc3k;;Xlh#-IkMc?lDI> zI`%9OKC}E2F(e$*dinpLd#QI6-1V*wXU zQSkwF2CJw;M9uUdz4Vp8y%5e*dF2UTW)<7Zc^!_L6axP=3o~QEPPRnMo8^#-Qrkd2 zR7l)&FD_ZjstwnJijw`{>K~D+?wrz}d0ghF#nX4p^#WgE^}SqTk7R0nw_aJa3VdDY z0gd*6Y1X9QFzGD(xP@nFhxt)LY*%R%(rB3ZMiMRTCR`Az*O?PAUZqCGi{il#4(pB1 z8e*_At^1_P4;K9ar~s^)?m9G(;*$Wy^I4>O5fYL6)Vu*#jjlJNOjLu8x_g2y*Zs%B zPn@#8^p4P!XXN$Xjj$k9BCu|$7}mO2uk%Xp;}dN*p(+8+nk^MA1Q|M z1J&7y8_lnnZp0t3F&C+F%TaU7zgkd(>p$6=$8&j2;dtv3nx@=irO!35Id4?+BPfCQ~0@}{minT(ICLSA`YzLZj6m&)N zzUr^w$5pIN?$OHr{lzx~XAT}E@A;?lh$%I10QfBV*n;QY(-DY3QJbL9S)wt(K=4Li{t|Az)uM)2Rx z{eS)9uV=bxf97f7U1y~w`u}}U-rs&HbWeNuk?nsR;QwQ@|Nqea&++-c9=c$P3jQgQ zJX{Y03qP;Su^IdJ#isA;uFXh|V_&4R6pw>!ZSuOq zIa+LcS*^#->w;2q34HOTkXBh23!f>E;$@4L!7^WWuP=x-7U53Mi>rpkA1!cb{`JU2 zEnbGU@AP7Mdy^wh7WM6A{y-50i>eYsKg~ZFirYQ1kNJ;!e^$NDWql{nWZfnRk=S*t_EGlgVvw_NDje!@F^1E3#erV0>4e=KQaZ8 zm-E1M&^??hj9B$@?t8VnxDJtfF4oeFK#Tf(H3-_A+!G_Wx<#2u$<8r1HSB$+&k|JQ zFkRNF;(II~{)5v@WMoy+{)q*DAL)*`oOLFN@U@xXMpql{y^0w0XmU}hC!t4b0=Ok2 zLF8wpjI1n0{z*9cEJQ~cJ=b#c4J`@(h-GvP#OS_XBQCWgjA!?`c=}L8CYUetv`%N{ z{`j32aVA0mAt|H`Wtzo};i4$e)=*yWvr<=G6ImFX)Pd&UNrqu8?|hgqdGVC;!c6FB ztQ?hGMA>})rFso&xe@n0tz=`e678YiJpzMz)agEt@9FF@5G5{7b7~3dPRk(BNaPnD z)#KgraT}!mH|2XW;e|o&Vb?u=ZdxQINmqZx5Rn~yd*um8k%7{$%rY~$BH#^O0v`Gk zS$*94$zlj~M$=kvQ|)rX$C!V=DT~wde>j~Q>3qDZ)Or4Yys0|_kf*k2>yaO_R!f4z zQ|9x|y$(E;+Q!I1zpo*>efxKmlt+uj50yCtF%O!L@72_NGi^5cEZfbaP3ModPzukI zUG7`NQCh}08p|ITYEr%q`i0}Fm9$~;=jd3U?Mb>d8y|FbTZH?V+>oNYDzS;0^XY-- zDe7Pg<3-{MKX^dCR+y$cQCvQ09HK$)*`S5(7pu3uY%49^;TjZ_Q=#6P7O7c> z-52smou)Ahi7}mn@7daJ_f+33RT3+AOW+X5_{uj8o$dYlg%R{}R_gQnH$=a8vfNL< zt3@7bA6p<8B-Oq=pX3}mXCh1Wt<>Y2d<~A*JL-cb%?!=Dse#}z1v1uVcWau{xT@Ke zcmz1FF)`U)r2iE-bbTfM(wpBaJ`qwxoX2aPWE=Z zZomU?5T_69$*cft6ZcLyYS%|)5Q+l=KL4ci*uEMi)<#_QV^(pS#mDH+x#%i`#!G)t z3wIVX5gmgzw4i-|2(K~dmWdj*B+OvIa%dVY(QO0gR{YjD+VN%jJB@4><9Aa}!fjVq zSeyWnHC<1}^3A5ar4tSRe4io-^66N%cFE*pRgbE4zQDE5;u|ReFASOQ)20?TyhRb>41_vzr@hh@*_1Vhl7ZASL1lQlctbJrGg_1ac*KFq6}?i2$u4$*HiwxtV63`^NXeU_HL&`iYq1BaltsZG3EZ;S@c` zTABHe^(x#AC#cunc6FJCCoC42 zQq1J$$ZSKrrz{&K_$XsmxnrtSEQJb^j57;U;fEIsfRfaojQike@uQ#$%<*ebKhylcmbr~q>C1#zJ>uR&4w;6eUKQqM(P3ej zu^sy{4AoF&uda)2%8Fqsn14)zh%(`A_Ev{C6Rl??_I_Tb%Y!Lh)rmIbg^cVqv1bLX zamm52dBCfmhRZ&s`ftZWPwVE3+uA`g@5>+2JdFcO4en}%>mBb;D1cfQ@rh1?$JPAi z^N$tnvkbL5jXyLn>qH*T$Zhl`&{}(iHcg%tx`(hInoX`39GLv#0V6aU zKf5}Uuye;WeDg~!ZtJ>_m8rOkFpTo`NTy1rjI+9(o==?q$f~8MVyg=$$DR6;`A+EM zHIOO|hi{kk;V?;aIY&{jU5tb`v{_)(23B>gq{<(1#;1Mjo!zH)K6`n%KXf|8eB`^w zz4FSVbc`yt?J?{Q@%jq0hyGrP+=I?=q!2egSM#CH`oYkH(tYEF*%B^;ZiZI>QE7iQ zrOU-3p2nY}{m=i{2K{s#F}s?o>;v7ksoB5_q1b_HEnYI?dt z5!)R)TgcTVS-_@Sz7bUUB!;&`N-ikM_;54ql~L(m%l50&3y)QPE!$n)UH`*bro9v0 z`U?BWh7~>V=Ee7UGdJn2R@5SMY9RfN_jdYZmRqRF-7Mc#BN(@PsiK5&U8jx}Iq~bs z`GvjqR=zUDjpCa)lS7@M-1nz$QN=y5oA+KFOv~)%+oP%!pl`^3oO0!ETx%<~B&A(9 z<3qLN(5vJJYVlSV?$QM7l;oARJxGOP`cTbS8IF{xKEFLxUza;HOZlR%l0UwyJ&jH8 zBe&wFS|}xS8$-)oQdf7}X^N!7VrJ%UFKKw|&cIc)30J zrEj-4=CQuYR)E-e$kB5ZYRRnJ2PO0Dy}VXR>yvTlBztr@<7M~dqPyZ2VNq8!qoyWa zq%>rCH8FGQ2S6GXH_DnmgX-nd(wd%4(jN!51WYdpKN^jeT^9b{(d*}CqjY(`l2i^} zHJqlLCQRv&r0u6i=cbd+4rIx~z}h}xBi*L(qlyk&aOy&S=8IAT=9O{rmAC)pk_T^} z&$mbsTFmY>ZTgxUQe0fSpTh(SX-_UAM-~T`g3W9x`{Mz9G-sL1Us(e%ycE;2@UW4b zW{dIM5W?oy%m-ZSWXjwk38vVgeob18A>U-T>{IYZmBBO3r6OA+w>|XyDOoaAE=n6& z#y=vvaVY0%W~x-yqjG;S!e~8k%nj4c&PSN1d7&07nI<$P{GCvGUAtmqWG?7vT_7rxz<4qGdk* zY?oqWYW%pwqQHIj1mYAo1;=E-GD|pKLf~F2OxOB{$J{pUl9PIc&f6R)!ZiER;_` z14!iyw&1EQ_j18t2kRWyKC@pZsc8078RjGb=0aPmh%=|w6ki4)kc(-9g-Iuf?pp{A zq)@vlOBr}{s)wgNFDB&uY%G`8Ip)kmuhx;Y1^00#alX_9?deWF)Oskc$!Ku0b9$|< zy2CKREcrrR$YVJtVz?`=EsQTl&B1!9 z2ODJEgUESm!R}XlX8lctL!>+>^~`6NoVW)f4+lbDTLOUqcE z@eR!8J8bl6;uE!0TsVCuY(p>_Jp3J#P}f|)9??pXj`Sec@6U&1?|fB3!4Rj0v-a9l zs%OgitV1~O!I}z8u;dWNCCiFG&DG_*EjamP$MsVq-KTE+I-l2`4({*Iyn1%hvN_`N zDf@e$k?Er;)@Ex;z@Y9&6v>`If}_Xmu&aEDjL~LY0!~alti%am!?98Vyt^ZAuAOe% z(FR|>*WU1Tn?fs0-_qJBNNieh(CX^o9z2}jKN;BRVy@(Nsd*$A^YxqYOFk2rs({0- zL5~T$auw=(eZiFY`dAXydg;W~yg$~xfoA*z)oDgb#w6TgnWYz~|4<2w-<%C_-7@qG zQcrv~Ix;i3q{ekg7+f(=5cSt%cU&LQ|*c@Y>B%=r{u<#y>2wuG1fGUcfq)m6zo;vkGqX z3mJT2wB8OpYG!6JDQ`Wk2kf#tP1;#OAx7@9S@Q7eXfUhte+LT&)K8Td7Vg&zi%*7p zd~QFS+WVZ$ui$?;c(sQEH<3td+J{6-+h`tOv~4!WG_OWTU)zum_lF@?gDv=5zIqYY z(Be2DE??(GA9hjdmzrVse_l=Sg8w*#Sk#ZS-{!K}y*w3Gb#gs4`L))h)JFAlgHAT} z8*2C4(J9;OFV%|Qwfi8w`0?EBwqr-{1{Yt7Z`dy$?UBsVbgDCebBjmE#8y~-bDXpP zKqu~V{)>(UMi%!CCW8oj>iIDIm0k(K3oug=ZkGf=B zCafrM`C8Kw2;J=JZmHW?QKPr5=2r$L3)z{(hVZ8=}kA%-JJr0O2?+V zK}s5=k?!u0?(Y8H%{k|ubG-NW{l^%LvDaGjo$<_P&NWwd5QAj}%6E4OnMvlG2)Q0x zPc5@ox0BZ;GoLOvhwEp<5?zCa`vp=NT`wk71Vp{e2`UMCL*_`%3d_sfXUDTzaObXAzB>tIGBy`PoLN8ax((2w2u-IEu{j}sK;(CoXW zsLrIr62TW@)dP^t;iB_WUZ>e^NzmKTnfTk-RMwF>4Xrr+SqJ^aiuD;FO<3cl)cZiP z@`YBS?13h25RY93$D$8H6)j|YP8WM`C+?c?($MWSTKO%SOhK)}@ZNj(Q*52i3pbc} zJ*$lR{^_y8U)pQZ)~Bcs&uBfbcLYrmjGHw zuIZEVYiv{CvE{HDU&ImN#2Ov7U?OTL2zXkE4lmW^^QM+IJK7gD)iQLq>iXf>0at7Gh;`k4BQ66=}7W4D1+rT zIKR`8aRMoy>fST|ke_C)QPRi3LqO|3?`Dv{O4-(NEr5b-T9V7-3d>7*4|oy9OE$_ZMZUn(7A${%l42Cayfzc$UGrVPz)t@&PHEX zQ4@KE&R#Ph+NSt8q=*mN?ea_Qv}@{Lmgye&ESncKeRJ#(5mOM2V$lz}fPH9A&Yea|Y)HgkzDnd5u z@XFq)9d`I@M^@ixtK)#=kTX-43ThhexJk~qBizbdXA<7eT*)5E-iy<~?NpfYm%1O| zh98Lh&J&BGd5x%Lof8#nq(8xbj2wd8QBYZZf>Q}`d>n-&JIrA-uVI_5RTeNOD;8j> zdM$sIe!AC56v^f$rHBA90S2eM)^2Te@=Bj&192d{4_njM2^k0Ka__!>)vSsVCrqxb zJzoM>#;K`Pi|+bKZtr-DpMbq=*K1C#C97J`xHJPt6INAhiyEx(4e64gi!CNd(UM2S z$-q172&e2G>V3UYw1H_ji`b8TcKbk`EaMLRH7DwHEX&*}5>C@2W=^>1V>i@00fLV- zn=s6jpDykcJa)O`>q*B*UG%m~wS>-t-6gDEX%Z`WpDE>aa6D5N)vkm*el+R>t*%H# z`AP6@uf2GUEu%w)2Mz~9vBG+F=DWDX zKiN-R^eIG3`x=g0zU{AQw);B*ELM8ycis|9R#u_ci6&23X zYbtSfxc;x&vpW?HZoA#puMzexX2fbpam`d~75%SNWOnNCwxiYUgvndy!xN~4dD1iV zxGqpcRE*w>Av@Q`l!9hx!~R0w;e|w-mB}0i71g2RTZit78{}8{U;C7 zaemu)X(Yv1-Enc`nASDUxR1|VMf64wJ@kSewL}r+b1hFPA~j7Ic`94gNyXkdsmS!N zy_Hnew_D`%pC+?Eq7*SApgJmIc2%u=y&~`H?k}`PaZw%N0vHL~0HbQ;o#3{GNPe1R z6*;@tyvl{Mr^%N>{b?0CVxQJz{qwBln4&+=1^CeB%b^kK)*gyK{< zCU2(J*-=11W&fh=jmtV0WsQ(ZCj8aL5qza7P9fXPlVf}OCbV)!MTg=siKdLoQEVb1 zu@h{UEnelU#M7-C!ilBxS(kjsZPE7{R+XnbPD?Xso^!(SNou@BeA(>CyoDi)g62$X3>54PQdJPV_`yXSpgC8~FuxD>GgO z0?@4g#yqT)Q`TK1*6;?>~)=s zJ9BQHzV*2zD5s2#d+W}2hI3gFG~GMlBf>81Pb&unV5IZR)u)BYB^MsR>Tdl5vQ+hz z_#F4#aOqx|# zPAx2dfZ8)SZC4}21HjFG5}S+sT>a3#Y)i2#zfKx|&yVk5qS%6WS=Cg@^dfcbpraG7 z8@;;SSs@_Ea zg47apMd#c1GJEb8a}FC$eNE;WI(NNWSsRNZFI8ekAyAq8bay=`wq@NQJ9O zdfGK(Y3q5T{%Xo(SrS`e$}+NGwN!dVtejk?fOe#Ut+SvY&-A-WrG?}mT1E3-aN46^ zBtUbJSW0Q$a3RP}M01G@o_K&0zA9SHuy7p&@Bdu%s*Z5YQG}7#*BuL+W>jZ9>GlY& z<^@Z~w(TMO*=gb2&{N9CUy?oBQJk}V=qvp87+dIa+U>J5AOC3Z(2sIn$qum!NN}y! zVM}kw$px<%B=QNXG)g-?b2SzzY)_H~9;nug)A7{7|-<#14zgy!FocQ@?B1(SP zSWw@wwA4nV3XpFRZby|X%Np$TSMT5{Nuj8FHY~ojZElsDouOv+4UvAHuZ&z%Q{<7M z^uUO&-qnD=9(&nOUTFu%C~$j1VzDUlcBc5KYXpMNW6Y$@WKR zLjuY6rli*tJfwxNVg}nA(!1uPQXpJU|ws9r2rmW%Kn~kshZPhi0ZQ zH=Uy)o%4?>`X3Ja+v@?GC=guP@nB?MiO?9u%TrX(WT3`0*;9Ey{i=qgRA#r|bh^$Y zVo+_dL?^U_eEsLl+VmP3$cVG2O(DMb=&qwu9oRmcfH+=S>zMX0ZMv#LH zY5_&6{>jVZ(~FenHOnU|0mrITqs_`ylJg7|Jn$J0To<&&6Q>Ib2YqJTF7r%rE#Ex5 zrsAw`j6$96d3P06MJw>C;lwz-6+M}ZO=q4w0M`_h;(RD(32&NulC;NdqBzOc`qMbP z{O#d^YZ+o*W$zMMZ;f}~S$S7X)7WU}%+)#Bxb#sz#>&?_;jqQncjj9MgzO8=KWFOb zT4Qh)GR^WRBr9-aT{Hns#ay%2(0K2g*+~iSgr2^P1>(tdSm0#_TM9tY>l`)OOS5;Q zcqWBkEj8Jb zjP&gDXfxZHTb)AuK{`^tc30g3D-KZP_lzI)@QF9y1aT@7#*8wQWn=5|?7B0)a_tZ$ zbqoc6=*CLSlb@4U%nkQ21gmyOA@g0iIamD01oP?o2y~RQ$ZU7lvEjdwhn})&=dB1i zBbFgQ++9(O1zjR}Ey53ZFtlIH$_C0LKPPRMFF&ljIu|pzIMi>Q5}jcI$^FRp*7eb_ zqg^?C7XW>!apPKT{7p|r^*XBI#v;#YvX*seh{l3PRXxH!Xr{ra7%I=<AD!Adaw1-{Wr6y!~(H=p-3lbI2e0W;Q@4pK{hKZ}%Cc@YMGSd*O{1x&oS)D;D zMdi$+`Vi-pFvksA^r$sQvb*dGhY}YoZQF4P&yRZ_^2!v4Ve_j!?r4qT_3U#Nv_2r0 zyMTWm^h#fmRx8XioD}HZSV6R6sGNZgen5PO@ux2$i`nk10G^b3)XefP};2z`zvu&iP!>e*z;>y0(6quC=w_}4GJ!fz-%hPx@LN|?zM7)`@es=73k*3aI(RJwYYeZVNNW4n(cW`#NK$kWBI6I z;#~d|Tf3&5M{xo-SS_kOxPYtoCtti_(%o7(Spbo{&ja|W&_<%$bI(%FBvkjE3LYT;Dy3Bw*>!HhRV}c<5 znJlK`F$``8;p$Q24ZFSXap=xw9V@xmK3u=*OIsOJ+kJC$dBU8hRK!#QQ7N&rE_}-M zZ_&FT9ZT*FyuiD!E|JCZH{70*isDd#&E#5BEkzo7oX%kxLy9occKJB(>QBQ&Lj%6U z6zA30>=~-nUeD&9c)oVGyB~O~ai$IWz+|hpO6rX1!Xl!T|304$(%Tj$VW1P!?kQEJ z<+9~NIJHU(Yme+DJ8X_GLb>)w#3%(^j^*3V95%nEdQ~!<#Gh8=E@B8(otu4mnCn{# zJ(A$ZQFTfax2q%MppsP(q7NbD7;@XmV^(*IEV>@gtY+Q&u_cwxm+YWw8Xc1CSd7Q1 zmjm1xEZ|F=c5twd?dub$)^z|RX9P#;7ZOhUe2VwQn1F@mjz zLJ~4`aRmv6a3uq25rpZ}D;)xXo11a^11Vmsn|U7E6&BM5^tJ1RxTLLYPD5Xd7#|^^ zGC9s15ICCLT-sZxi8`E+7RB-%yERM-reGuHE7zxC{}3a3{lE(Y03YHH7Mb8xKLOnN z!q?_rr4Ow49Qy+=7t9X)G<4EdwPii8HChMN>#Dg!p&Qr-{zhhX*jdCDt#yj36tI}C z?mlWi7N__9qR+A30&q0C%e$sdrv(>}p0T&ME$4?2v_b|iY4sW#}#R^)j05mrzL&ev9~%e@WK9O3(=!LVo?oK8bqx!#PN1W#7}&Xs&ST_SzI>7 z02i|11*mV?6}Pcz4n$daq4Lzj`?1Ms6v! z{v75yDu`38Ky5d}719RLOFc8$A4Lp^uRp`;A8v4v3}^(DkNTGzhkfWjn@{VIdepzu zJ!@28UTV$gqiGTN33xwab{qUpu)35Np_f?>x=xm!Kt_;=McU;5XiICvtTJp;#FWob zcXq67Xkl%FwWc}QtlvAc5loqUEb@wt znP%H0UXv#SO>oQo&v{^52-Sr1iUC=K5*n^@(h&~PR0lr&H?_q-jgE*Q95r4kta5PmL=x$L{mP3!f5k92BJom@N2+L?HDEt{%=U^q-d5++LeUU8&c0h?O!Zyrt8cDxtW4Hh zR36lO_T!_NS|EB`#B-DWhtFZk{0|#^;1xw7)0Ng-Q}E;F_yZkJpNst*O&oVNWT%5u zn9(>(jm^YZB0M59I?zl7*P2Rhh$=D=2G#lV&^uJL^(5ul`+C$>*~|P1hopa#Kl-|G zN{j;dO)1vnGiCAz$dSa6Jsu!^`8|xb4*!+KC9ZV(@?(z92rOan;*kHLH&bS&aA+TF z!$#I{UN2r-kO3pgN7Nk3I74u7rGNZJbfqLBLDLcWQ-{dB2)z0K*ikn;wdShBQ{i4cm&?TPfk5JNu?7$EoYl;eMBtsyBbRBKC(f$%Un;k1__7nNRn@q~_U@ z`I<;1ThOY+?u&FJ9CI(sTvY2qpZB z184nkzCycroGs}0l3es>MP>(z)N}T#5GbJT$i0MX_f9wE7TyNug$gEiFmumvbLuU| zJCy%K(?9Z6Vi{7nIhU08qbmyO!Zt)!+^A7F&OY6*oP7N&G(qT*MG3aXfUocmt&)IB z7t1eG_o|(X;zhvgtKZpBgw+1=YhG~}GJ)tDvU-EWkKx)k7G&iFQh#@`=s7^;K>6z9 zuM?wghOBvyOPRGNJw)T_1@Ff|`d(PVC-@7cP7GV6F&6zQ4D}H9X_6MbuMJ zITJSYoQd`?7d7?uBNWbK@JHCvsYMEiI^-kQ>+uY-7fq{Zyavx z7a-v6LRN7xrGAdr58Qb+AZ+)xiABlZ1AUsD;46irq#7L1&#MI6M;V!yZ~#tB0R2Tw zEb9)u?bNiCWhdi+@m~ie{)I&Lp=p}o@`~3tjbce(xJFW2Zy7Nhc5wXo84+!6bVrYznLlTmGa(|@J# zicL#ao*m&{BZ6LsmNs_8>h4=w0MFX3jo3z(HQ zH-v}l`gbH6^s$*tj&rim7TYL;29p_IuGZ#q^%zPMn)Fo-BPg1ijH@qgMhvl(Ua2Z3 zS~Lvac?T}`ME83hPn>g`S~CQUmX|t?31|JNVT;>ECBP5j9e>pYB}D!M6sIh}U!PSb zkiV}$AT;DJ<4pEL!9s;eT@>x%O^4~~v%2Nb6iRd|N$u7{>`dmcN5Fr$0Y;@oTDH)K z8TYvAd`PVNqiS(2mt)-|SM*)y3l%T*vrRODWp~NpA4?kgovl$2<|_#5C$1=hqc9=A z*qbK~@$A-`-|C|K6FHfznG{ePv{h!@9vgrEBi06y69@dC(34mIe)(G$4Pu#O8W+Wh z*o|2fN=XV$nKpMcyriy*#zoeObFqTVDA!jtW}U7PSM_=iC@2a3kNbp1R1Xd9!b;B1b_GCx*AM3v8w9Yg|-O0o_DP98Il?F&{H z4rK5HZ<8q_C7GVquWB4=Njqv~LZx~QWXu5&`;A=G`QF-d5*`#on6`(yQf)d@0Gj)s zDDNTSNa#nb=MeO=Z=)Y)!dM`wBq+gEg#*ATevC`d)tpUfu7z7Q_j?u;$dPI3tZqp+ zD{l>czCFo-a>CpU7U`Q0Q%1JyuBVLdSFXwLTWxF$6cD#3Dt>Z14BM`5G}NOvbkBKv znfs|svA0CS+rSj}KJ4DxIDi)u5sgx(+r@I_Fi~?AyHFozj5}>s8x>tHsVaBWMdw3m z58qvF;r5vzhJXnUn$}HUGEmZ&g0B)k*#J2G4a)IDIP>?7AP8svhXkD9OzE&ZvbxxQ zYw@G?>4qYX<2u*qY>5xI!!&p1r|P;@|5_Tji6q+CF2=F5S}wM&Iq4ow^6PZ?Gv`XE zceClkhA4k*AXfp8f=r>-ll({0^A1r8Dcrxh=&BiX!_s0vHIvM@6B-g7e0!^sB@1UX zJ8@98ID|}VrNaEOxF`^C?J6pk8kh}#jBbSks%G0SU0P&+l-8>6{DV6&`Hic<4$`0}sJ+C72c&JsFS(c)eht;kSnJ){B$#E%d zqLxXj-|0ypA)ol1shCSb6G?@u-Q?Cgbv6bXEn{enNJbcy>vqn`(mJ6HV9HFg%O!wA zr;;EpU?h2=w7?Pa1Fq<4WSPyEI9nL~_%F%u3n3a6*F-L5V0n}IioAJ@d+f8;J10A)ZlW&u}$Kx%q*X(hd)O}8TD3|m;BchpGli4?M4 z>SU!zd<`dq9qfcsd%ndHEwxsO3ZMx{{oonza3KCGe13?bkNeWut)y5;}?=h_2zP+xMya*Y}!OS;C}W-0;}4DA+tdx#Eh@}eI(>uRKd)|&5V?)C+vm!6`SXw<4%Y8 zN@yK_gu8!8f`v4+f*mBQU1nAEaSf8cE}c6R^AghHl=*QpjEeyz-Q0-~`z)EkerEj~ zBztq_pK7I52S?U5M3$hG5T(g9R2__I3)1yILhnm%(*+`Tf9gHX8XoL((F3rFF^s1LPR6bD2%` zZNZH5B^m%r-gp7m1yqLNgP9{@anP)Vndz4F2?q}e{+ArEF+&9#5k8zv`_q^%c&jes z224v6ZPH452i$(BRI;hdv>Tj@j=@agncn5m))OeXtw5M@zLCRI$q_Av&`!7&@`t^Q zoTpPWB)7-jY@n4I;cGPjjd|o|ox*|-K zFix&E$Uqmxd06+g=&@02U?{Ke((yi($WYAPahE5D4Zr4pVAP+^G$-aGPK{6QEH<^y zLaftc{t5GFD7vMFONAT;xBFSFfLpaYh=*F%ZJ8QzCx>1oL^Ky-Q73z&^fWr>-e2q5)#O zo?PYV`zR{@G^`2|8FfI?@PCSNUtq%hanm``tvZ)^CVv2LBqe0OYadMW`7pz4<{yfD|EJ7DFNrt%=7kwpG;{dr-rpdy?~uPl z2VZzy3{7~do*CZsTwQ9dU{<$Y@;7L)`bH?3Q5A}%E@M;idIVs-DWOI>ZBXyV zN!GF#A-C+rtX-Y;eB57t#cNSNr2+SX4?a#9=45JJSLW8&*cG^SIu!nNF}hzKsQec1 z_*34)FHhwq&M)U8T3}+jD_gnWmLssO>(d{}|8rYU;4^4NyA~Z${p3pgt=>3THl-48L-@Jf zr(>Pzzt?64E8!T?;A;0uL9VGlul$L8Dg}{3QgPf*ESg#v67*)5^{GH(k_*oCt68+% zNiHvSV<8MBBPc+OrVK3=?KD_oe?&sZ_|CuIqkpt45ceV|YCb8};Msrqz<2!qUamNA z`-jq`eo`x&v%jce0$<3`It%;vWAdJ^5tJ9`JuP34XBV?BwHVCj_Knz#8ro>1BE=LkXJ`~?D~A)-@j>Wiqg1K zEt9N33xH&-q{j$wJAyES48*XpJhn}se#N-VuCOGWz}!{fbM1@NF{^~(Ge|i| zEsXUw7NQ(g1!Sco5rCyNgkXWZV(wrq=130nQZ>$%| zjz){zrtR!eD_&1K#32yX_Pd>!YORmf5zRa00hJ`>qN6?6yXl^f!DzNisFkZ4YIO$X z97p+%D~U^gf8Dpb4pCyKt#Sp9KQ;VwU9opk7|3;}Lv z4TJ8Qxa}&PLR=7Ot|Dj=U@9hu1g_AHl;Ai5ptKJFNRUwNH*0SH%6f&9Zpr-Q;+xXl zQrE)^nA;kOZCr_VA2yq-opf6*O=kN1$|mNb<1?Ry)K%qh?@q%Hj-nN5&sEH<(o!17 zUEMWxBZ{5kXd2hXTZA7+(H^^krN&3^E9Zg2Xms?;|AedoBe*lNw~GE)R83^9{_0}Y zzE{6|Nv^0M{*Ac@B{0T8Ybsr-7gqTr++oC@DE$R z48YM(A6tKk+4ar_qGE4DLlKG;CoXK)Z=)LPBc8pfL7N_R+daaK6%Np}^Q26^R<+&S znjaF6Eis!MQeRufFx#u6jOW_DE}tyct%+qfmaduo04uH9Dbqw2s8y~Wmi#?$0AG@y zz8>HxO9p$}Oe_KD0nBs89w=59L-n{ivCtZ$P_24Kt(3tXHY6-CSRPYo4xuI2LH*In z%;P?-!usOo!~*N3q+YUK9#)e;?9igO)Yv!Fj z>Kw#GFuH~lW5O`mJ<+s?a+NpARDZ6PvS&;4s%^%4q_4fNuF3I)8VHB*9k}d z%NXE+zMricDs?WAVUbEMlq@ueM~HsFz-FQqNLMQVZ(}#zHoFATS>g3}DD6MN)Uh|l_H6t5YlFEK+gNMqs{5yUFN&y$-Qh`o;|V9BeDDO@$%MJcvz!ox6@5o`a) ziN>Q#+htKRWiZ6sGZGco5Dv%x1g4EAl;3q)#j}9gzd=t-@oHvtRP__ez0wASa5K=v zd9_L&l)&@aM?I-#_s8&he|F-S$;#52hS{UBXFxH$;3V628sKOMJoROEV*u?Yj%)m~ zK$^~A1vF?IfdGOgD6udgYwzvn>#7humLH|XsL@8|$9WCd`sGJ8x1oXK(p1mKgOLewJk1=jGAf5f(jV;yFIE)c~<=`^#8P*ub$aN4uwA@d-^?+ zkiI+jiS)CnRhTid&BR!<91MndbhUz!R5NqZ5nfq+;aGvn_4d8w>b<4}MhaD055F(9 z!47CdjTOcJi_#-5hA%@eOgS8F;gFp)>7vAuLa~>))>k0&^>b{(TXukfE^Qh@8 z+5DBu`NN~UySmnLKJzJ>9$+Y!-Bn_u#%fG@c5K7VjQ zb~JJvUnN7`W#h54GyC7f#R-Mg;IY7JS51U&B#E#;hJUC_znkt>G#v1ZL2GH4PG%Ai z5JyUbR7ZpUV!mZwc_j#gD_*qYp=avIly#j{4GmB)3GmWH^jF#N^TG_mo?;)??$Xe3 zXKnxsk2S9D^f|S0Hv_SIedTkrbxpNP8b$pt5gaqrtJfE86EXDvC}VqHO#aOql~aUT zACwo5jVh`q<22Q53jzZ)5>>2UVoA_@r3hvw;ZPUoM5e_y1NVUs!TrRc7I6NNsV%ID zgiPtwH^~>$Yu8uKbaW4wVTT=%fioSeO~@9NU4jk!_q66pYdn#CxV98U33HKuuoXc4 zzwGPFNSp#^4lEFhNL&Z94|1f7a!y&ef=$7NPuXqt`j z7?!?7&Hr`wzhEE?5mJe11GU+&RtLT$nalE-)IvPi)FohJa#*1eLO%8M2^`(K#)j%7 z7MYD=na*IZEj;vsF3LhGSW?~ZbcpxAl1X8tk*U`4zevF_9WtrIi}F!8_QJYF8FA?( z7bQ318oOhMa{b+A*%VzoGpH-i~*&vzF#$&U5Xe^3Jy2K!f!y7RHfbXg5q|sSirPOjk zgvX{Hh0nAU&co^@>4F}2Fp+xkn?C=JQpf~ek8n*-UXnlf@;WFNpW$$3%Om(ToZeq{ z2g5Y{;^+Z-D zIN=Mi9dY6unXt%ky>#ePXH@}}I38VpzF8)<8MpEt`9WF4jKGiOqE-t)jACZ5uuZ^u z#n=*gAvNeGGV>n{_?P|wY;|jHD2cF`MO23JYk`zOcrsU|xmgX_Qb-Cg%^->rl0jRy zreinVoEk+!?CY=BOf%VKkDJ{TWy28tju_}Lv{qclgrD-d zCtZ{Qgr!%QIllumWS_ZP$x7k|L<*9Ksk9%=-q+JbgZrs@*Gosbdx&hY&hnAlCygR*DqiZ9sB`^`huTZ@pZ>EnuW4U&i4oZ!`GAu)8j&Um4js8W%|- zI^k-0RB48-h5S@2+CN&WE05r_@TIe;SN#yRP6|IHN_r2I5sRsRxnQBuq{C^y{74^J94 z&!P8$e@=l)(Ewb-az9$k5)k`R(HP}Qh=-cfHCAdgk_-be|C1l`6SDvra`nw|s-j8> zE7Pw^D78)Xa)Ba*+3HC+6iv);(M&GeMcEo)Cx=DQ|(6p-enjRU`-D zp-L82u_Wc^o*eL<;q5TTormUs>A?#))$d6Lv#vokHksv5CRD#`QOP5xEeHl7c9bi< zac%|gt6OO?%0UhKV-{yz9~Tt^cxfaUXsekj&Y%F+BZNDZbT_mMb=CdD>QMf-)rmfM z2ttREb?cJn4w5!oN)WP z_Z?*jCV=SB?gJM(5V(xLKF3hP8Di30W>fX}D@*xvtvJCHbtOT`4ZR?O$YxRch~umV zbr%qb7tC0x+4>_z*kTlvDUthlPv@lUu>9$8VwCX}Q_jh?SQ1&fwH}zh{yRALWE=EJm+yDd3pukj`U}&6qQW+g=x5XPD0fQfejrwkp09e_=sdxYE;i8m1??%Oqq-c#sCG3h}lus znam>ji(QC{jOjqCj^l4IOgtbDrg{StIOCC846cKWAO!ozmzjF zjFGK@O?{#_H4>lE8FY13BG2ab%NgibBx$ur9PxW#QE&1yOclUUHc0EJ9(4RPR({7I z!tBo?93_p{*-E#6LHEBt*r$*Avh;nb((oE;kLN$0T}^>ENHYDkCPu3R#j^wBX_G5o zCl-~2QHc}s6;kK*Vc%sfF^{O^Iw|QCv{12y#&F3>D9GDHtXj9X$jP!6M?}iKF#zZK zTOBCCiqtY(aLQRD!Q?D}7*^0>XQylVE6Dl&&miaZ19N|js)YON))W|Ju|!dJ2(AcV z12L#NNm3QmIWLOEofomGti4@oyrK@IcbnxsX_%;WW9Us^XW=*K35|=bV4`zb0>kv) zdaW(VkUWDo5qZ%4x4m$PV_Bv)v+CwXz@c>rG!9WeKH8w|pnf6pZ=%)f$8##ZAc&OD zTo->ARQnL@^Q)R{2N%DFNSfy|&Y@DtH)if!FI%P9El7yE9+Kshm*sL;*iOirC&d`S zL`}~C*Ux5}WmK66=+{tRY5&A);Lf*$*BBo8@?W%};{T=%H~IO;NK{+Kn)7e52r>yx zp*kAjEFy1wLwv^L>a@0s+h0m!VfKE#9Nx?&%z_|xL?(oHVcQ^$ZAzF--hC%2l%Iu9 zVi5kag`UF$IHPhya1fx|gm7a_t@T?;Utl~g0He%b6Hnj(XLa7M}frTFpLH@y88pX{s(Fq|C^I#O^vK1oTeH;juuhU2kHq4HFNe_Jt&T>~N_$?s2HXq4NSLktMKUo( z|C?k=`3f(^dBCzdW6SXN=IVr*%VGNs-^GUgDp2~bQws4zeQ97(yD(OJlsBHwYM69V zSZhlNxP~2J{(w^m6|@vlRa#KivcgoLCQ2+|$-_(i#3e7_SG8(|(zn82gj7W@-n#NO z2XYgbmmBTQYUMU?;u8Aw=b83`n{IQSjWUhKH0wr}(1@K$l`3C3v`(~B9!GQu zuw?R85>L0=9RhRD)!fnRqg3WT#!`b>(B}mSVfS*lkZBoGhjl3a6 zM}Ud6)n8gHG~I>i#GfQ+IGZeR@+?zDjgjF@GcurPk~!;bUUwOaR|WaU_@atLmnUdm z6u>y`?-JJukdBN;AH_%-2C$WE;vA zT?eBr+@h^nB9J_^jB2I>E9p^rqMk4~G-$QW7A48Df^an67%IW)+9Pn=JAr#dMelnl z`0GMY4~P<%nEbI2&$cok6L|MLRM;EhutZD1F7@mgTQ%b?;~k!;aUGG)pB((#h{jJy z5k1{`N-E@?r|N7`^cp%21Z=#H+l~~0^FKW_HgqlOR?vVh`#VF6^TA%$`Q2M4SOo7y z#Dd^san2)h*PGJ~bZf|r2)grJE#UgN^KDK>6;0CwUt3pOo?Y4W&iJ*X^IF)=H}8!5 z7*_@DTc@|r^qkr?K)Yiak9rdrr_eCZJ?HUEaBkhJs`rI>a10l?jXB@PhG$Lc(V`oX z!`=4U>lo-6(3x{Dc99$<_g$PF68@P?q=>;kaYSWNt6Xl!nD}9zx3RswIhyW#l>Fvs zwV!dm!NRY4b;>eUz=yLJTd8L~G6Y7H{XjBW7_QL|Z*FGrHesA-=Q4U9YJDP3pI$4N zu2TAII(oXug)&G52epY~e`maAM|F%LQ_1RS3B8HmQ)7uBowy9faU1&X0`Xd!Vz4Y1 z;6|c>iAw_f$j~LUt+j9U`+;6Sn?V_+PF9AyA082WIQXM7UW-JS83{s%03=!V7zN-$ zP=Fk+kQI|cqK9BBhEca?k^JHS0$nJq*|qcJJ(42G*E~?fa@61XQGRP@F+Dk|AIY?H z;HPOZu+5(W2Q<;2wFKm(IQ{fbV6*?s;b=l+yR)6R8cNh%`(ro|f7aN-ZK#S*mvj3d zXXe0J4q@u?fdKs-OX=3ttF1v@$BD<2ysJWv+jj0oFQ9O!U&Rd9(2i=(Lf#tAY)B5G z=oRjltuC!wyW7~NvfdENE9=VzJYEr98YtFV4=BPj+N9(2_%+Hzccv-T9CI@fUq0%d zX6T>v(t0?hP^$E_I2o!yVKKYx^AQnh%b?}-KE%GV3tS8d=FXF-&izl9Kb2fpBI6;@ zz)}8MdtNlK30M2Z;&UNz%$;4g=U+TW7N*ld>srtA&qO=4vRD%J05g@9XZi(r zz3f9#eWKl>X-}hoPtQxo>oyIw1`puAf!)*fNF~bS`;LARu2wM^V+%_$VT3R(&^%Ex z7pGb5CcK>#IqgSFR(m2ynCENdakqM|z|5x79kDxcQ+qAlwl2Lw#HSlt$}MxXaH*@L zOtutc7-)Agf8ca|i=A6Y5$&xTJu1avEA#D|*`+EG@r{$d%lYyR7~&r|(s3=t2S zm!<_VQML>sC*@(f!6pr1Yc1C^xjr^H=GN0mR_h2z+(fuI7t|Co+nPxpPMAQA)dit*3%Igu3V&QpFUN2I6L)Xbs z<0CXiB95)4)srIE(n-@I_Z|)+o_VvGvcY_HY|rOa8;`Oc0o>)o$M2$mOz`$P^Ek(z zx)PO6@v?EuCR48KM4sJqAW8SJ%Xw6{r)3VC{%*U&!dLFK?iiDNyZzZ?YZ=QcDz|u$+SVG)5+x2 zJXkmrdLY6N8y-g09 zZaxh7O=147@Flcq3v4o<1pTjARKQaOP;y9$lm6E|pT5ThGS~GL)HIp?N>%IXSwSAV z3H@7JCcfsd>wD*!`TmzRe_r4d?nWbwxqF;tWI$g*=p}g?RLhP8yAf1=)Dr1iId4_s zPC8#74wTy~a+j>8Ki^`Ahlcq8wyDnXtMv8Q&d^Zoq6aH63LQ@`IhPg~?T<@zF2zPX z)*g;n1t~49vgvIx*fg45-Q>8Mz@4KFdy7BFu|p;sApIu&g-z-)0W=aZ5tJZ9>GROT z7D+eN*&7b2orS-4QRUKWg>Z%4DD`B<;Q0FloMqx9b@vi&izM7w-IPq_$TNn%HT4!; zdKRK`@pjiHlK)%w43qEW$mZHl37W`sRc3{RPdbHO-xf6U7?p-Ut45uGi*2w{AyVS8 zOs;KERR`Z6#G$0bt)IO+5F)UsAW1V^u4)YA;5S;i?xQ<1KKeeRSJ&U}UM6q9bREzt zYG>`+3)a~=B^a5sS1Ep)S%`|q+0)TrRXozr;Fc3M1l9&#qc8UJBW!6N0p!xvEMPgC zm)S117JwOZwYBDN*r6$mrt`sMQo?%WW4~Wi;^M^1kgLg$v@P$a!tumA1(e3MOD#04 z=g$D)$v>jpWa|xnc_F}}(9mZ-S5h69MTO!ek1?UYzU!dw-t(X)5^i~8y6^3lewfak zV((44ipzYtLHCbViirLvRXpnj&V=$7K6I5PyO!#)xOd0SO7Hbg*0#c;f})ep z%jLdTaqft#s*Tb2je_)lwtTqhVOTS(K#oMK{OEc6a?5r+-NC}ytKOWuXM>#8kXaBx#qDYejq*z_tHp{Gm9kQ z*m&oecRAN#A1+r8!mt#GK5=l#)|krSBTmZBZa9HOY3wlLdHdQ}SG`^Rnh?mr7qa(@5W zv=|Y3X}wMJA*>}rbdR-=wo2GeC4)+ztxwQ*Nq+xJ&6lLGN0g88P{>#Lu#ff(M&{=4 zzr{9B=%VCI{}a8}t%BO06~So0&CT3OCh}S4#&u#DEx6mD4N40)cd3UMqiN8WnoT6W zUjUPHM!{S`q80|mrYSgs47`zX=$NEjcA{scQ8dN{4+Lwz)2@7lV$GLXgq7tGQAWn| zNhLOMH(lP`9z<{k-rA{Ciw&mxt_)jfnj+IBb%Uuo(&ZCR`u5s4*7)0R6WVqnsgt}` zrFFM;8{{_Quyj^xo3u=73q0H}Gaf3+PGG`Kwq_O+wn~iGmQYg>1!=1?==&#O7jf$4 zj5o^Mu=`=QVI`|`ud0bUx1YLFS?;Jiak!FKSNCSZRV82}6H<4S zcVjb6HM;g*t4QfipXIP`{8@B0>+LowMXdpD+Xb8|ig1Zyk-QAtw z?(PsALV(Z^+$FfX1xRoW3GVLhye8+|d*3&{H^%$5|8(!Y_S&_oX3d(bR+|U~dLao* zmsHSNBE4$<2dfWJFTLUeyuA(SU(dd1BjP=iwTTX6nlevPxA%R+tzW8rr)UJTokK95T3Px~b8^#=6%wv|Kr z+Btm&G!1Rh>?2TFYaeuouWfW^*{`5dKG#1uh+?R1Hk{hamJ|sx`8D##63okkTK%;&RR_!#<@$D2tDiX6b(&+=u9Poni`$Z@1R5ga3)CQ5C}Nc z8LCOKte?AGEOfN0Cjac`=rvO!bRtmY%W%Bu7?2lNV5vGEi!4C`?Y^KU_}Kj9AI|W% z2!&PU&@x1wqj(hS@xOwP%l`^Ke-&(aRHWij!97ER`KdqYR5o}lwt{`^G>8 zJuJBR#OSh!62*Ncl)v`gvOVuf<|r2@&7Z#h$MHUG!=-aRpR`iG<%I4R_|f2Q#*X_9 zK(yjv778%M4TEKVE^&*7MtYlUdfsW=_zv zyeog-yZS0*(wX6X$-{U)F*HxHjVYw|LfdAd@VFeUbVA&e8W{OP#?WyOA(+bNs;*{Q za@%8Mq#XRdc-IWG*|^l)r{~!B>NQ+%Uw2g}O0uR5Oa{}Jy$nOU@cD7i=|=02ve$%~ z43#v$B21*M_HpPqx!U3d*MbKIBIL>XW@oa%w(J(K{mXe-4W#R@oheLzEy&gSO&ON2 z*M;6=Lru6z?gan6p)kRLrWnjM7}r?VET^evWr4TgGfW6p(EMl{9UgFl6tD2h2#zMY zP%&}3H&2EMKxap_^vZ76c`@?leNX$0#JCPw z4DXFbV}%zTvP0in@h+Y+7+?$N%#7BLPGBM2IxbJoK)%E3xa~pCU*7 zJ=1X==~)Ww&u{?-=M~V?ku7s{t$y6(IlDDu{|Tsf4THA(812v#3scQ;y7S#!LWC{? z-fmW&ihcs|%^_(6JU&GJf#AoIt7Z)aL*&iav_|tm<)r8m4?kT0_JB&i;pWuGNW})SNI>NcGh1Irn;Ep)YgG!8B%t9yX@q%<1 z(^G0K*B8CKbOdf11mouJ#8ny65Em1yPIXb1ogX)o>1dDjg{%biB(@0>rr$Rox0qM% zj~M%HZKlz0dQ+!VOY>&BVc{qX!>{_^uAe-3rz-u)cJ3RBsGOoRay6i#7E~DyJ*QJ- zgLrZy57ynS-BpYpAu=#jtENmVP`z4UX-9 zBg!1Wf7R|hFuREQMrB^0LSo!3Wt6I$Kj}`{&iebBU%lTn5%nv1z0G72RnO7scrLf3 z^Jcxn-?@YLPqVT=abMhE=L7f4E{XXlq$(*DmX>MU7&;U!@_hTf^H(^t?_jxq^nPgx zX>#rYc<&W<%m)5N%I)I?dty-Y`8!jkV!_>Y%&-jOv_HJcmp(CCOi&K0}bV4s?e(Fl^jq&l?2cNIKzE)M^jtWNTPo%|H5gcch718cIS6_L456RFVK) zDN}mh2(n2OYE4Vl@hCr)s|1A$avc;m^YzruqR0cvmFFcEZIsug({LjKh^0r_Nn$mQ z;Bl#;d{aoGO@i(hG#<^$u%qJJlIvz~rX|h!0PPettk)ESY19f`cg_MSWnkT$v&-~E zsC6J;%5MAQdgBiD{cU?uPif@GO@NS<5~A}uGJSm-s)W(_zjV1e9|P$r>$+x(*%$jo zx&1$1=Gix{stv7d=zwN}%}yBn!LvNKjU$hM0MAq22ltL(qt_^*`uk5axdJrGb)J<7 z*0EEax5oo|rLoL;&gVXTL33s9K?bI$kJpb8Z0N~Ds+4A_QkZXw+8oNpIfsjrh%WaO zi`QZvEWU{hf-8@sD_KWxatCkDMt*-h_KVHYh6baDT2jS0>xGhR6lxl|9+{*n+>o;! zCcB^U(kM6nf;6UTS+vkj5YLPigT{}}qNf!d&Qw@!>)XP}Zw2G>wL~!k1-gT$X!YrE zm8RoEx@t1Tyc?a&x)F#KnLaj{K9f z*qtdF!kE%}pk;#h{#@T}^mldi9Hnes8fEh~;|R1KfgcUy1HMzVxHvdE-5zf8UkD0< z#Zu<$p;AyndbU(j)Hj5UMgW?bXKgNdee zstja+0sAQaNOeRr?|7i<9fkcf-OnQNJ0l@4FdcO>UbP?SRs(6+;O9-R8O}wUGVFM- z-D{p-=~CSVx5eZ%#jTIy>{EqT9>|eRmpFHN75Nx5NfYUEf+m5dM}zF>;8rJfWv7>h zC#3@;nk+ne(c;-j^REn%j(GY0e-TtOzkSCf16d5?Eqq-dod?@Y$1Vwx6*Hvm8&f`c z^UaMy>*4WBLJZyP&!Lr1b5GE!QtuE!HIl4EW+V~Yyma_WMPh$OlOgRM!oP$D`Id{U z?MB_Hf(5tWBO&dGwsEg=sBzs!@0zQZtFG&ja~7+XUyPC55=cA!T$f%sU7%4uU}&G7 zq|=a<@6$}|Oz_f_DzoX97Mg=V8OBGT)@#Gj))YNI8mDwd;#6&hyR6$rrpSD)r)UL% z*Z)ygT5?0%6iiQE#+n34S>?JywJS%8zbLP@Hl-PJ<$a%X zZNF}NdECg$w#W~D(qCj#ol!5i&+MxEG~RnK=lPyz{LkC|G@1%#msb*`2SDn6PDrEC z5f4kfXbBMkb?wmwOIgfy*idR)^{`{szI6#TY?MhTR3qO+g6d*KfJSN8bf1-SCUWSK zf4*ISkpZldqS`HT!a33%4ctiQ z6DJX5H!QPJf#fD9vQfMPu%i@kqMdGl56eL zMSp*w5)&rjW(H^gsdEZsRO|99us87?P=7nFe!PtXF&fuY>sMpte^*g5Itbv)qGQvQ zVSA@DI+mQ3lPzuIUloZWZ0E=fSIs+TfBP#RB5cA5guw%qKmWQ00Q}pPi+< z4j>Ltj18!`Jy3vMIue{}WJGWjkO;&yg2#u2ULsWCV+zB^#VobB5TGLlYMjhJseQZ% z3HCAWK$)Xz^mi3{A{7!2^r{rY%+GE&D(ztk$I2%~_Z04h*=oxU3wE_c?MlZ+l;}c& z9Y=zv27+`Xf~}6#1dF)Ivin7m)YDz|i)u*4Ny@Y~LcJJ(N=fG~mJ-zGh6&af@La`I z8U!>6dnvPnW?3`_gcF`e@!jtOZ7B`!C5CLcb|$lD??xVx3M~pljQBn$3%KqEp0faA zV92_F%zzl7g|{$;70Q9Rkal;puw`u1z+XShNgmgCgopu2Kg*^puPfmCb+KV>7N-Bx z3t(#@BHr4y4Wg%VxN2%b3iGkuWR6NAIU$mPTEM8GO~Q>m%~B z@qslXSy#&%^(F$rDh8dF>t&FOo}B|Bv!i-yi<-$OXS=qzSiaCqhW zqi2KN%GrJ-q3hKljlFbJuv4%*VY`95q)Bce=g^7y&*xVI!K`EG;I#}3+{g+CG9`H;Yf$QR z!AzkU**9-zDR{h3KVo7PZ?qg8co|g`Oz7}9y|O@J74-P^r(hbUP%QN{*({mOnJ$_c@1Do?*zQrOX+ z7qk6m_)ZZp@HD@<(Gd4|V1sdPWP?m^JyCv3+kQ~^!_uhMii<6$AD*^6NFGBQ9S{A> z3K~4=hmSA_`{aN)(6=u3cX2?_7izY^!4J90fmjTQf%H-dy_nU4K*b~_aqf7{WMN1= z7vu;Ts1*81oSzEha2ca=ea_;KX~brD%FDTAllnUddVmz6?cI4jzB$G!Ce{Qizo(CY zB?xXTz+oid+|&9KjD_+4jDbrGCPr9a!m!Lbtbi0DZfl)I=uCs%#K6x6SfUID3vqpTybl7wvfEQvAAyR++q^K>-be3eR^&s{zU%lMC@92)S^ zePa(uVkX(W>6XJk`)LvTOk6ONdme*auAa`T0<1EZ$^W$J>_P?_mpY|7g)TW;3&|5? z;*O=JXP<&j1Hw+n%Yh7lFL#eR$&{+^T8S)Tdi;xH3!)}fDph;K4P2WY0Ol=g~ z_5cS2aijOdF8xd-1j=HWToM2+Rl>7MN+*58aHDTAof&Uy)AWmRDmWV&7YvjiMraz%(|*0Uln!n-J0P zc;Pt}9fBSR12y#^4A>`&muV+80jia0WGwkv;K^sQCl~r%knGKGbI-Tv=sB{;<9aQ5 z9*U+3xS_l4&#`@KRQsvIZquJ4+}TJekCK0mMD}S1*wrEEuDj2AZ?uFGoHT(_vsSTh zL@gpquF-?4&SX>a9c(35APmYc0q-(j!g+73Fu>>LSYgqE-<%WHpS6Yg`d(A29rki^=gYQNTUwHX2_Hta5DnNMmxckQ3^Lh-c%GGRHL4*DG*Hn8C9Vuau^@%eTTc-#;0FL&?ZIjnp+`ER=NT=y9~CcniC4yxOX5&K!K7hbX( z_eiyz4esys6WZq-cYIasd}@fXo~@uezusRCi6cHgEuvziE^PD()=sat zZs_i23gH4I3K1P9Lm0vT2GVz-U2oh7m3CDMtPYxt>&%8E((d~@tqzyVc3W&52Gz;f z6*VgEG7Rw9)R5Jd(jH3L&u5w<$B;bg_c%5=&Y(M25}*fO$wI}$W9sOV%z(%Um|>r{ zJC+61;peB+QbFVIz7Wu8m{x{T%g7?=Anz^E3Jy>(0vuWGio$|D$Egq{gkHl!Vpv~6 zVuJrLwq~jOuRr>Z0-N4e*~~m;)9>9iH0ogcgTCR{YpR)BRu#MAZf}yIsJ8L7oc?O; z8nq4ReRQ9um<4^w80bra7izxOqnsW%8nBcmpY+tTDGvv72Eihmx6Uz)Ze`_i!C-!qK~hQz zl#AuSUj`aEnjXB}5LLR}KzL>Wa020fvB80x%k3iVO$J{f$Hid*WTXIwG(b?kFRyj# zv`p$Bt4zKWq_r_>K4k%oPhaQ@yMGU-VBv23~28Wo61+u@H1dKqb_C)85)GIaj&W5#NiMW^6l@F&nOuk$KUaXbXZ!U+=gvD)XauS=AgzMSsBHfzDFxqmv2u8uEsScA z7jUF_a#tY?NDuq#P9w;we6AHr^i?{15$2j*!oOA8XWY52eRrB~VPj>r{9c7r=dnyo zz*=WWy4>dFf+UEmu(>gwx9F|>6N_RuE2Q~Nlhy8S(wBZpeH;Vd7)5v)&gS@&`3GSe z+7#{4>V(C#t?{xU#fgm>t9K4*rb|j#LtlOZDSRs0^ykb}&n3h#Q01HsMJvAM5mj$s zO#EU~)NI+nh`Q&@vbQ_S4C>cOKin5&k2>l7{b)m=oU*Md2JjdEuC-^(zRC9Pp{0(F z(rR{x5NH*3!oMq0(YWh`-ufd3_LB z7=4Y7V>I8;K=*5Gk+ieYj<>W{1ad z_IGBz4`$$;Z@uBYN-9fHM#eHrj_a*w!GX|li$HTyLfRbXd&t2j@(!gC@pu$bUgXn} zx+nk1fp@X=tMu4^Ci~sVY}!oCgUeWR;MVY?TBc^DMxxQ=5N&=TUY?VwK^``hqDp+G zettYo^6}YFMXpIn3A;sHWx8lM`e1%fSadPHu%z!s-=F^g|E@HeTFI#6ZBb*T+b z-7L$^g2r0C9K^8T`q}@tYw){Gs=f;U8T3{%_nY_Snh^V=G~H|~4@LTQha(vkI#^T} zOSZ0ysk1kIfI0MPTf$kpc%hVr<_!Pk1-Gn6v@{w~gEOl1GY5jmD#sLY;72}R zSc~uOzZHCy9F4N*+(}%c`vnf$aF88;P`t@?LmNi{jX>Ly=%JdK0{zuntF#wDa1E*S z6B4QlUBTt^ZFhRUmI-L*^k3bJ=S8@24lNTXEV}uwnu5+n@)v?*iWoiK)z>|D`L~>p zqF)SGK##kpEHB~$jLc>7Zt|q^jW~NlY=B`WrssU8+@GU?PA=U@tbqYJ|TPhLtI^n;QQ=u&n0`v)%mX21np{ifjx=CugnT=8PcJNqdA)nNRPT!H>VEn^2p%uJQ{1z zPs2;Ovl?xWNU{!NboZ^Auu!^wmf1)68#*PDFBndpL4L>zKXWzr%Hy_*wnY%yegyU?840?LahJ&VHD-j{fEv{Y1K%F6T5sOJb zMI#-l9bM-oeAJGUEtx5!H#fJP1h?ti-vX_;t=jyl@ zr*hC{%I5=LVGt#kMJ)RpGKMig`!wYliw-E4r@4K|>~9hhf@OGDpp1i*rn>G>XhpXQtm3$v@8+jP-?l8%cEsF2n-DM?MP+BarwGlV zh1{2~tzQ&mq%Nmm|21NM@Nwydhi34W$CPAL!J-UnzsP zLe>l`YC#u=6GKu-8i(5?5Tzb7TlZ7gwxoKffrJT{k|5^=nkB;AxA!dDG@z%!`}p( zf0+m`+`?jomkMT04LVW)P4_S|WY_sDC-+EGazl|pV|Y?AYOq&Qg46)B*k@i>t}5J=(a$NnQcBLE&J`x0; zOBx{WD9TRE?qbXQa45{on6$Jd$ji zr4do5!$Vbz^QY58^$eU7rX41Hp?P7HPt)jpmRNDVxT+>#^B>zjWLFE*k|t3n zAZZT@tYA;I>Ly&bn?)&vx1KF4g34gB!&!&>E_#^oj=)j0fMubvq9AbCn@ zuO8>w*T-Mk;7Ea#1Ub6XrJLHAvJAUMfzdEl9qFCR4NHCyO5T# z1-$S3CIowW8g6^U>ho$ojOJZBULPU74@nrsHoG9BLf?l;xgi zLxeu^guV-(5JBXWb(CH%l2V-9N8hBV1}DFZBVr@>tbXBya4m?Gis{~fQKQ4S0#&sNALua1SKTa=mVuoAV_RA2fq~= zgN*YtCjD^MTm00~CJj`R^l-9J9RY}8Ka6=v0=KzbEsv!CLrjc%neT06hy*}{D@@_m z3S-%a0CxPgRS>5j%a9iw&Nj{wiepW@vVaeANayXZR=>vIj-BCSSu&;PW!hG7;bz(t zzat*z1~N}C=-%!wivU{;S({jjSV+20&@BQ#sasKG`_koHp9X{}6DPk}wJzy6U_mfWUwxEyOHsaP|HD zp_)|#@~~p47+V0gG*QY2&io^ru*h#-`{EqWS{euvH71mV9>2+Iog_OpN8XC%+PXN> zE=TPmNaUG(P95su7`{TaZV%ZjFrV1IryajXnIc!6ZK7MWJ)=@n6_jvM*hx&FahRcu zi$(>wufJ+xxu4Ry7)m4UCbcv@>R7K^v+(&S5K1ZIXfgv=1%(K_c*?~}M~D^cXz36oU^<_d zN?E&%42td@>&b)**SGX`g?PNV>Ce2Usp#+k;&eng#3iCxrR_3C@Ck3Q*srO8zd<#V z63#g<IS6jc`+TvINq=gN^VN zvy?>oAFK3^=lL~r-T~G*8Whu}@kO}pH}eihcsY_<>pY%mAjY|mMo|o@bv>mwLJM}6 zS5vXux9)l~vix%DaR)gQkF(fI47FE8ITy|)YI*7u)=SMf-}Cyzk^wQC;Fw|8My~-H zZ|5d9MY?e=0Vgq127dVy#LK$HZF_q zG@3i>&(~XLna(d@jcm3~gsR_XY?&9|F3#B`6I+9~9%A9MUi}tD5ns)$pWn{=S=ymB z0PDavH9c43wCY}h8QkMklg(N-i{E*Qwv3v{j@3W;N=4eC<4&Sdl@- zF?x~nZ`gKhp7g@7z^|gp>Ji!;vVG?hyucl<8WD$7yf(Hi4Sr*uDiMm?H7J3T5$BV| z`L|%zCXZt8n!OJE3?BFSD$(TeGdwWa#qF0Aw>+ZJq;X>{caxPn99`Z}0CQQ=Y;(>Yi;Y#Wr<+>qdJcyvr(w}qsPnLc zPpn%>vr){@P_x(3MnR0$GB%^PZI<~c7p&&p+2?Y=R!YnRuXmnzraVi)NyNXw60 zp0!JTz_=ltI-qq(UHjCx&h&Ti8*5Ix51bCU@Yqq9<--{}kaj4v0AK*=TpuS$J1d?E zk_27F5bkcD)-Ogebc+qMz;Dq)lr2r$CM+dT4^ufiJcZUj?%*%(yv|?ABpf`*j)~PR z`S{~CD7%S^QZtyqG{cuz8$L;?M+?PLd!muWU|TPh1>i#HB8o(Fu$88s+N4>J5{w1; z6#R#h2=i0nT)5}?f|{F7axB*jUUJ3Y;EkE+Fc+`k3lSWqf(gMp2O!fC9EU03HDfafM)jfJ-E6lqkAfVIC^%X8oE9Gci-7s& z63qD|-46Au^u2hKVp@rE_0AdoA7uxp>;8Rmea&+e80N));sb95HV~hH?mDy*-LKAm zTjl)_`A#xtGA+^ESisCd8LWTHs{Bymz4AaPf~}r{wMIHRh{Sai@&ySH%D)BEy}%nX zJ7`(f(3Nos3$>qR$~B}uc$bj%b&!4EWX>O)Iu)EqQ-VL)r%Kx7Q{T;8{QP# z3D+ld>)<^&66ZrvO&?!nXMkT2h*N*tDBsc3iEU_Ad@@l~vJNJC$#2mb*Oie~0ckW# zyPdQBY?Y73Jb|x>r|KEO%y5fN%@<<1FG*N1)yMe;Z0PzrSMKe>(e>mG!j!^??+W!U z8DhD!-81;fr5VFW#-_r87YhKakDAo?@}>`0Tf$n_m(Zv4e#On)deX9URt6wgnoF&Q zf-I>S(B9kOtm9`h#w2_39SnwdeOHJuv2Pr$cc!d7%f)cHRzKsbRlR(l8K;1%CH~c% zCsOUcd@BtrLddct80!ml(LSw!&Y{|6YCJNNi!awKipw8^4h>Hez2q)!kC@nf@DakJ z=TE_?nI_&5v$BUsydo$}pQidhQqU9T#9!@dpPp~omYAMajis`HDkz8 ztd6Wqwz0m*jmgNlkVOSeT4ahF1(P3;BzTEVI<~Uf!rs)bV}PNv`an#eF>NSvfy8AV zY81oQ_?1v(j)^7^PY*CDa);ei6?R6rowM-_6%&%fSjxai3nfLMxmmvyvaTV&^v;%M z`-i;y$JeY8!uCC=#>n)#fEBG+|7C@_r0X97Ca9m^i~%!7P-G0s4v-j|Oxore*BJ>dRGysabTi zBYP0w7*UzKi=HRuzZl>hw5z|kwstr;np&;6%9MwSk`q9sm^r;Cl*d_Lcji?1UEFB% zz)dICLT5af#+Z!-OnxvPR=@g}-!Ts3XGEEL;ddg2R#9~G7IsMa+quAeo?X=f&=55M zSff8}-$S$VESQ^Q-B8p<)YpBz`(P=Qzc1BrmWZaxtbpi1EpE3>lvm>>t2(#tW_6u) zUvfK|?3#6w18Wy<^=<@T2pdjDeL52{^>S$3)G+h2+XoBm^2!8}2-z_?TvR`s?aS(X zUZcr2RnfU7M_y>a53X@1DR9!pE$LN65L@7k`o4hJvBeXJ=I<#uWUC}$`GXxB^7_-Z zMLxLDS+#Vcn#P2vMsXrk{C+olv%|rc8QsP}O`Dn2M)`acjfR0t{i>Vmm5cwM57GQ{ zlY8m!8aBxmX+v#>$F~;)juK4+pJ)k?>V4~2rhPuMbfyjMmlgqs36Z>MBwT!$P*?1U z_oYlfWFl_ejm#$4D;96&`9&^;E8P?AK5SV4e^ok2GA7!P{gON%qwUm@9KOh_AX^N# zOsd?f8D&F$jFb2(;$@*>YV0w6dsNl>`V=ZL5ec8F13BJvX@nIoGM~x6Nmw(JcqQQd z3Q>Qn1gG$%$;W(QT2QJ_Nnx5G95P2a2x`4_rBAf(vUMP zbKI5!HEyKew^kG-VK*Z@Rr7dVQrnV!2^D021{FEwx?PXe8__cwdj>))Yq`H#+x{p_ zLbmaJ;+g|z%g?(6Tk@%;b&b3)XOx=Fq-Puw22(t@hIde}%`cR|!%IkL7Q2Pg33m8R z5CaptidF59HE)H5(ij)^gmh0j&~uQ7i_{HOldYdMl)ij!r;Jr7SIAq$&=h3g`{R9MpbpD4qFovir5vcdyol+y_U3h%gYXV!iCWyrpOruv7w z*RW>0GvC_F+vU_SZW-=pT*ACd)-L12u|(JO1FDm5r^rZ_tt&X*HHxzt;RQso{Ek0vqQ=5Yii z+Zv*=M=S=j&_=J9gq-95>S_L!)w~p;@!u&0%qZTL>c0?sfUDHCQ1qg(*RTiEoVRkT zHmmeKD{SvpPk~`3ke5_WBo{8>&oLXt>TcF-NNZE^#aCEEB1g*(n;C=C^ILY7_|wTj z-r~nLG;?R@2~Fp%BOyx6VdlU(#!Lf^q&;LbJlyecVZnzSMi=mi9Wuvr3PaSmOSC_R zkx50DUWaa$=t%sXNITV^^VZmE+(P?nF+$Zk3!N~s&s!4)mQf{MkzR9}!*Kpv->E~<-_}`cKr+1_n=YDRRh4PnYKj#) z7um1u3|odj+@T~>I<_A1TN28tfcFiVG)YeMt*>?6v?F~tpKtn2ut-!j;Zj--HQ$G( zkz9v)E7oi_=GyRjE1hb%u3l6SO3S^a=ALAFuiu+bG+f;+8N>SXBX?jk=;zf&*w&e| zV%z20$wT9qo@e{-5K6>Ce2d2H7Y6Lr*IVa*mN46VHgdDw8SYcYjf`kmd`HI3Qn#`B zK;?`hFL6O?WuSSK$K7%L>99=z1U=iS-?jYj{QTcgBhNm|(!9gAdj(#CR2bBhf6dKa ze2HF~k|v&YTqcCQB%nYiaEkOq%4aoE_i2n|@DEMh&g-lzNff6F06dK;6^U|6!+>+@ zrsXxGPnK}Qdu#G+3?qTEsgUW$?+u47v@A^f&WC`^2W88-V_80Y^!j5Aa9>AU#P5tff znYZP)#XzSGn=ExA1nJ!M)jD3uOH9defXILG#JJLU5QrNdux=Gw1fZTcnJ9q!C80!i zu$cE3W_eh_@Vopa1q~+UPOmIoj@trZhClG%1aME+gpP%3womuEO3WFztheN{Hz*RQ zmnFRe4=QH)q-PQ_%ArD&$$;NVlPW-MQa#}|1u0p@yl>y=Grqmu?jC&rj)fkOb7ABo$k;ogq9zJYW0&)!k-^#K{8;(EyQ0($8b21j!hPULX^?&1} zADQN1FJ<+nB&mM8c$$aBIz3cm_SQ<5`dY%7F?jEN<*DyX$TgJ?7q)CFS8`gh`L)=>2A1j8D}6JE54aTNeFpO4(LEp9IiE@eTlQgX z$hZ`+70Lf022GmmcvqqRi1)h|TOXc*f)RXARN&eBvu?C~=VTu!V6t2tqjK)srs~<} zH98MLTPA!48g;O@m6&I8sAn4k%5hw7+^1^-m$6-R+6>P1;${<8-B|!RZ&xmxzd;Q* zR7;I$T+>cFGi^aV8X1pvXBtmImKokzAv1Sb;Nk19;pOaEXD$ng6B6?EWN0cDPV^yG zun>_%${+)SfSk0}v;J9bxdo@|_J1ytG!CQ`oRs+d7`yXvO_(9%PlY3jN-*sTiUcVv zX5cRG1G7h!wp{8ia-Z$%^uDhn?lg#JNI<0bsef}hMKTbsu}tDseTh-jxQG$qS+El$ zS2%HZj?rpxX!Q(Up=bWG!JE#A3$mom&%52K`@$f87O^V^6(@uX-L=>|)vdZ{& zmxBJY#)pn8Wf8zS2wxYRrm*5ac;s|{xuYJA_bL2zx|dDa$%NSk}U;J@+`ESMl@ zRd*aE&!EtH#`?#vqlM-S@5`fpkz{y$Ui(~Lz;AIws!NiVx29;yGd&;JxBEnJJRYE9 z%?1UM!UJ$j13?JptuZ~=raYs`%>6`Tv;UH)r(r^_2lg}=y}u*lx@P@}`2LXw@;yB{ zU5jq2^DWSH$A?JnWX-bepH9EXUMfe!KNMj=>nXl}??4U%X!pJ3Gb_ zSAptSJ1sJ01HZUnzk#)YCzWb$RmbCB_Ks&?ce`JIuHt60_`#Ko=_uA^n~PG|IqVdN z;P~!8dd_zsurc!d_iIccBdW?Q;x^}U*(HNrrm0#sel~ut^+fuMJbA>Ym^Oi%-F{Qg zo&U%cNnt^jzYh4)-a9Zpw)xVO>s%uGvHI}Ll+8vUpnn1K(tk-Z*!g%?cz5D5@UHI# zfRWCk(uF7>`G@O@pZ9EG7-zk&0EG}y8{*xW}p6BS(5tl1S#$nTkVxcEA4nI&JRsu6uW+gwgmtGJVOKWRhdZmG?P3|AOV~_VXuU zZ|581xjKHT*SHd82%x`2&`7l*tAO70 zhUfHj+zi5j<=VBLakGBN9w3^7Ko0{=j`9)pc`T`a!Bcsg+)3HKsKCs#hi2a@J~y*W zsBfp@;b1NCwM+z!O3KrR(clhhW)oI)rgl8+3D&u6qjO93Md2knMf>XswYo#eE6L-J zA3ao~2{Z=}E?LKNy*Ed5#R)rW+W9AMm7V@(64 z&FNp0)?(vC>@cSEZ?cqQN_Y!n5_l!}QhMVPUNet8;tYbJRp^BKiYXR^LGvWUPTH{? z=j6Q#5{yQ9`@`)tpGPI}UnYd4L>Z6Jxod4PJlK7z$eP8e(cN!ZT?)&Zs`t5z5&0v^cp%8^E zeEg&^wE&jU*~k!=r(gQwT~RwFGuYFc7`oEGx_@RHoC>ht%24~C4aK$i(U15VtBn@x z#;0ZaF_s&4ei=*}$-X+|oiGe2&#m0{!#vBTNi<+Sy_~IBwj7+Sjx!VE5u}ZG1^?6# zFeUJBF_&9pF^gGXL3U#>k$a;$9>^UkBbl=$>6f$Adz-ObfIqbX8QN;Lb}NS#tlYAq zqT^DTAkM2R+VFKFzR-~J<(1k11ZvYi{XXzW9i3{n&lbRO9_EA2k0wUi68E^Lf<=11 zNp;K(bFv@NEJqP<|pECI&hT+aKXpgjWeG@+6oFIOcNeFZx8n z?MA1z#ZhQ+NEXB1)Isk3B!-;dX}w2gO25;V%U>pxFR-iHRC4$92t&!*I_>5gjgYps zSL4h9ELRgsjL>h;kHN|Fa@+>wW>Vp`Fx7&dcg^pm6{^O$vn!FivrTKvG2_Yk~Hz^9d1N6P1MDH+Yem=kPad{Y7 z-%~*P85Hs+qXTIO#GZn+jnQ_$eYd=ejcXV?QLzp6_G$f8oGi zUvL5sppZ$*&J~Z|Y-TDcu~KXEM4G>sO{Nv4p`mHmO@DdJsxWD%OS4fo4y`IB8DH$G z3iO|(hCm!fJ%~&)eR3u1H~Ud)-BH@PbEgR>Atv{1*(rmUd2g8t9a#Rl?m(oeuOlR| z?q`&LUhlCJ0#)F@IoD=Wdx3#o1SY5qP{4fTBieqkN0#L7_egAbN`+^C(GuzP!aBEkbMK34h2|FRCPz$f zgy@t&1&y@*d^#71hHLc zAP$^2&>|@0&~}TJ;d6xWBCvinc^e9yq@IwMNrHU4@w=IrzmRG~hYB|B3D_OgvYpgR z)e-?;G8Oqn_O1uZ)&aY|FWVHL=a#4rRH(JO>!;mJTHe~?w!XF!>U>)h%!uKU5}af= z-!P|PdHKMoolDL?SN{6(ZlBe=c!>XE(c`>i{ZYSIrEjod)05#XPY+JLu==4IbishV z5HJ?CSD_~U$TP+C|6$^ z8SdU6Z%ifsZfffaqz}(jeM0AWHOW~ z(|8ol8qQR>+~dd%MnIW-5o}w%)qBHh4_T1sy^ao;n(Ne@a+_Vkp^jI4StxwK4G%`s zlzXT$+p>Qc!)hSzklpont=cy&mX?(VjhZ0H{qsX@R(5-Xaj&;dqdfzdtBQ#)cKh!t z0|r5lXCFTZ7=t3X2TcLp*H0%5$yZ;O=WEOoL6+_X2!g)Yu48O|nRzaw+=l(y{GIa+ z?Yh=^sDw}ShfD2=lyXEgbxQG5`W}lUYH=w{xny#ubh+tEPJ7|(Ri=rR6oAO6@FP^| z=_i)QI|(h12MDo9?@-xA!3#WYx!6>1YnuzO<2k{gkFR_``#;1E-3Rmk^!3(Zacs@@ zaPUEbyAwhPJ_L7{06~HUA0Rlv-3N!@5Znpw0fO6*K!Q62m*DOWUvtiTPtN__=l%F+ zct*N<@7`TqwQ8-}A3Kt(CBE;EyMMWZBx;nsdkvCK{ISEJx{zH~)+OfMyyTC*RaTR# zGG;muL{jx^RPDzX6A`3iX00aKR8Vg}GuuFXN9|Pjr*@=K&2nuqI{8ElT|jTp0SZKc z-u9c+W;<=Ysx{rqf_7Wu72GMBnthx<66TOtd-aMz;LcogObc-oFHIyuQSj3SbP($~#G|2f{-u+ST&RMfFmRLhqXkG^m`* zlD>6zky^Ff(##J1E(a0@^KR&x@q+zu23BOI!?5|&}aC!Et_toiGO6~1Ney#o3wVy>y8LbxdRx89C zmJ|_j3=4DC+M=h6d8rshK7flcy;;Q(adyl{8^a9xZNiF>2KeDod&9UPVnx&ftCUd_ zlkrg#5g->*-$KtZ)F`wOYPy>|A1}P1Cgu0?19le$6dq78E!eIDBqz_LxJrLo#X^$G z3@&W+z9F0y{5~c>8n#OBKe|lC!oni-a0^TlqQy{)1LUCjB|3RRItB)^mCY7K(@-`+&# z$-T;`sofP(Hu&fV?3le*{>7 z=XQ)N2*b+-QN2t#1^MYV1-8d#t%y$Y6Mp;S=UmpqyfLlzs|9b#ko*(cC(9C#HZmQ> zs9QbiQk|Bv5TI7VfanQ&NE0*F7(uxEt0s*ReE^SO(9$7FP_Jf^C;FgiNBFJ1te@9tv8ZN2U> z5QyC{{5&-!$zt#ir#V6t_UzESUVmH?Bw8@!W{|oBC={YN*NmrVKoa`IXK0=6Xt3k8 zkHyu~89w*!ENe&p=&ZRS54tpCkTLHE=$+F>1_kJ%&cd&hNZG7a2v)C*DgxDpk%}D6E8)TVR&OwBcE*k%WBp4%I-Z z;W@B68a83_#a*pfqqgmAg;|7b&jr(tYS6JGFsS&W?6UMOFXM|HY9!j*hWgAd$6yKu zaMgUrdBv)`Hb=T%PW@h}%K6km(F@|UpQBWD=?Zy?7HM^+Y*wfR);*S?GEoctvXq~}WLVu5*R!1pN`Hz8*6&M1i) z=VuCmuVd5pT^2w9ju`J;)sHr2&?O^G*6?(OwJSKX@Mu*($sXV$1D#Lp9ufS*XEBS7 zx9zs=1*QXoc{;WhGWGKw^b~le?Z_ez3fZx=(n9hQnN;#an8d8BU#;gQB|@s(6P}0y zYd#PM4#b61^~!~J(wO;vlQMiHMT7g8sH^zoM`lXXEq({-;aX1Yb++mIu68x zp)Bwkj(qg=vl=n}lSZxr=x>b#Y;*-^Awhl5eAN4A>40;Mf026hz5s z|IRxKX59N^UgPO^N+opwM6~@7+i!bz5D&ZV9!#Ff?;L>4Xmj+*vHRn&I!~$4SpK6p z`j3>p|M>>+g|sNp0D2&;x2Zf$+L}uAaeghfZtQW`eOsS~Mfz};q4fLTfLAwOVlbbv$5Mi%}HlT znazw=vNHVe0&tw<2bUAE0Aj)F`(I~STRH*o7+S=YpAE1u#yblCPjd8ES|h6aH-IO4 z@ze)`Qj617Fa z3I(j6?1Ss}JO=+WqbXYIk#GtCMoW~&=YtuJi)D9wE zv^F8YvxP(0rNNyg6UcN#laOPEtWJjj@%e+YIrLw$SKzib`5xcbJc18* zyw^F$AWG=jLM%K+m|)he6SiQMqLt-yUT@;ynf-(*1ZafJx`+-9bwRhR()TQl>&8x#CCB84XNPalAD z5Wr)P!>*kw^bhiqi*q9;xyV0pHd={g;Ur8#hi+XT)c?W~E(F69f<9{v80bEE!V&pe zQcSHNf226wt2I$Op;zU|?+K@B)O!;?ZaR({_z&}6M%b}@U3p<$fJ*UiaQdH@C4WBv zZBxZj8Bwsed{X$I(|Aq~5BbW`CB)a0={W#;M`L$8&CaOX5UP&bL&pnGtb_S~jstWA z;D=`@w%7_d9t#7M4k{Xx%eVNQkHxcpYysclgD(t3od)pZ~tEm!+53>sr@=14+CcO#y$Y*3D=M*DT&jC#&1}yzygKC3s@pRX* z*re~H#aO|aJzx8{7q2rTz3IE)3_D#kwG)S$;pgKSSgcY_FVZ$5+*x=p=1%BXGI;Ih z>dNd)K~t&NgWPNR%p7g*47B5(;+?GHU-(4+x(qg;O`xRs35NjXRPpH&=YO*2f4|`3 zK+$#d#5MKYg(&zOo|g@@@6T{MDb}38v`u}`5fEzb^e$9Av^{)2qBXT^jjadXr%G%n z>J=ZQyg?Xwe3oAcQ|d28_K4|BnQk#-EBIR7a=a?6 z|KifP)k5^a@VLe6Dt!Fto>lBZ=Gf@41Lt)2iNXB3M}#-fX7^=X>zz$1c>k&6*JJ~- z4GHGz2ySdk{nU+sLGoTJodL)02ExyUg+Qq#7?k6}qt!C+5jWM8vCm-)8Jw+=F-R4( zD2Z^<#_PY3x|xZv<=uA8L*dzljUclI0MBS*s4Pu~Y1^|P=A{2b%>TOCGo_bu(b@7S zjLdu$6&2szgi#!nK4^br` zxnX#Vxq$vIhssx*SnWN+$P}<+LDn7iTGqnomcJ-nezEY zw$U}y{t!-*sQ3ZHHPv3fLib8$*lRdNV*NaM>$_L^@n36klP4Pm*jG=(v|d|DjZaag z8JN`(Ej+YPKL4fK7rYZVk(?ZD8qUGt(PToK5VHx)sK0^|YD#Q~VR+xmrTpUxfrC6x z{yE&!+)W2E`etfuE8X(#_VZM-;(+N-E|3RdkI%E-tU{rR4)!_s!fuD=*F)*QPS2(s zS(wW8TVLoo*juIY?+G2LIHa2@K8Tm>L=&^eaDP~c*`eGOOat#blk)f=E+Yg52*cZk zQiRAS-O3)ciu`oeJ(XWe2Yd&%5~#@izH*BGTbUNWm2W?34*>^IkjI8U3rhKX(4IvV zx^r?$ozrMXNx-bEoLqRFc8&gGk$ss1*;MWNj7;1_F6q;Y+(Z@~g)V7~n2SitGc*DP z2GLK9m&CK4=X*FWl6yhr!RT8VKRGtrqUuq4qt|{05$Vg#ezkqdx%^e9lPk?k^A z`)h-2BA@Y$u17A|lYa`ag-qiMW-QF(j%3ezxdpURT1Hm*rqDx`%)RvL%bw(5&vrp6}~lXc8;cp$eTp?Sc|WXxJ#a`F+7TL zAQ#Y-|6otycSdve{{5O!yOy|Ir#(1x`!4y?We{`RM0o4ojS4y>3<$2XKI0U&SucEf zy^xuiDSdN6#Y03;W3}oPN|!i-*ul#-#GB^>5mxZ%;;D~tEP5SpI^KFSrd9y-#_+i% zZsk~W209g(1>OW>Wr%)Ex)t5XngUKhK(6b%t5s#@-S0WS4y>K;gBb$1tCWIf*cSTe znFOR7stXyCb(Rx@b(!Qui#-xS8aive>T5GM8Sg45_@Q+?^f%Ad%QXVRQD>xPVrjLE zC>j(;=@9EH`C712rvib@;A2yFXkB^B?#bj{;S^KttmPX~FXjOEH?ZplhY6~;Ov=nG zr`>6ITid0!2}4!!MsG3?S3ueghdRIBH?wD+kC3O>d`3BG60kUCg1heYm}#(A^;iiL zj?))K7%OO3%6ix^)*6(6C9D&OZ|c9KlM)WNMl;+MdDxAnv;JZ;Jal9%Q)D?o`UslO zP%FH}q!9Ky-~B~57CH1o$kTNsG&W$ZMrHsCR9_jgLb6AG84YA`-wC-J0MF?U7ToI@ z>&TB)VoARNuSRTGev&?e>p-AqeM+@9$O;+jwDpo~wbhIAsqPTphF2>1{%K?BKt#U! z-QL0W%bx2zi-XjfkTd>-qiz&5;kN3Ue%5rhev~LM<5(i}$&Rv#ko1k%&6<7foSJi)TyNbMkAOh%6XG>Uwinj{y1Q{wNLDt0%Y9Z7 z$zUK3sZ=@Jt}F6g%z0HV6YaBdSNgK`c$>GA`Z_R`r=7ZLfl!`By3TSu)`wZ2SRkQ0 zii9zF0mb5M`xEE;cMrp)1f6q`CwIG32^Ky!=m69pqg}ni(;49lg*^By`NgiJq>ED= zc*>%^6~@IlpVIeLc|}M?D2{?yS96yQ*>1n>eRsXo_49ONo4|>82V02eH^~e=Hy1$t za;}Dq+9qyX_Wk&gARe$P4$g-a;oz+a9DJ_V|1`sx6n4P98;m@NC3MQWH&+ex<`4*+ zMD6@JzbAmSK(r+PJA#9Bq+KLU%2vT61zF?(FSGyNiNE!HMpE6Y!Z;^E{QebM=)uQP zSi}^Elj!9YTn(3ifXplZNS+LQVW(HUro?%OtGkvovb~AkE|!HlS4wCF#sSEY766ni zxhPWj!XL*1!*oVc+tkz@o?ub#Z@5dpDOoYz!s zp`v{Xi`YPp#C5t1(Zw4LBNGakU?#W=3{aFwFWHcahI?#B=k5z502&6Oyc0w$@kDHdVx| zfe%6GM5z7H$NmeC&04%PI@4(ae}fb`nK%v~;jwSzZ~%3S#DCi;N&NG~&XC{R&|Z@vMSaVs3N%F zr&6Q@m&lc?MiU(qBjtcAmGh)ownUwWGUs9X$0u<|k6UCvTTo}p`K(1fTejt{CIKEL z{m4a0g@pZ?yU6#&Q}Q{1mA#shj))}n%x0w3_g&p*qtAbCWce~r4ewN1(YhA<@3&G$ zoXpK>o}_acq@C#K?NkuBYu%J@ukr$+Hg$=;3LSr;1Onr`A3eJcUC%duGiY<05PY~? zm}hX^>8p&>#u|1I^$Y$UmoGAUL)Ai8`EYGjH(j)xyD@g z@zA!v-nLqdG6ovnm_kP@fFWlUN4Y%&a>I6P+awRrX?`B9(*G7v_G^ z6QtbP^L(+{l}-JnL6{GLNs%&uYG1GP4gEIp&hb8)bIh|~JAAj=h2ReYWg2){aC)Ca z4&hIS6SR+}a+Tg4Sk!}dmC(c#`Ia~DG;=yfl!fD`$}dvjulJiPzbl^es5u`u*xW4w zO;fTZ$E5lQlVrQ6RF$0ZNJ(WjeHVRLxV3HtUQYRbuQ;dAa+9{%-@gAA@T&Yf649t> z`+CEH_h?fFxH(`WLhs71A9E&Udl4})Rz(r*SPh}S_G;H}S?wSq_}IPTxX4&TBM}^1 zE3IF`-f-WPW>Gne?QtMP#gfG@by|h33?+$rQceTrW>1{bcZ{Da;_~iBd9@e3V_R?y zZ_CB{YIJ$1ob8LZ_-ww?L8rJKPiotZO{RoJVSF!lQ%XtQz7o>g@boArUV%^E>D&Wk z;+~pbD=~dZ?=!KsgWnmuYpxn`Zl^jimNlB32UaOg-+b&kT|SUeCnAsTcDLv+;`}jA z-gM2FMF{7zm4(ZByphGUl^+(r^`)-@1N6QV((PSmnIV>)qLP<&G7=9-*Q{8{;}|zg z=g_c%R#P;)9^gB9dI+iPd_$xGUEW<7Y)%;@hFu)j1c|OOUhi^bO?GkT>=l?_Y%|&7 zYZE=>Rc;Ir_^%jjttBlazBtrCgXZbT%FD-#r`fND$Uh}AU+ypne{~XcQ$C4U?-1*w zQ>;L$Tl|9H9R0)GKA%{j`O?=j&J#Z+2^d2=j=?9}dme)k-}Y2mu8SqJt%<4bzO%*> zY-QtgtakRmPrGHFwKfiy@KK*AdF&8qQ*VCQ`a-p3P}w5?Cg`C`$@ab3+amhY4luoIJV zH$6Ch#sfx^;n^5pQfs@yj3l!QF4(iycU48FW)NO5fjOF%Fvg5vx7SesKJ4oL7RLx_$? z#?_8eLFN$`v$jLn%_aq5(OLEH>!$g9n!%-h+!EBsAA{yCy}SZ`S`$HMgpX3-YW z_0Y4G3TOX~@5BaR!R1lcZm7b%WGqQdOehk=BEPYg zyego`&RH@wjY_MGG)W1H(}33kTbK@9gQ+o|O2t@~$!F-)^~hlt9;Zc{w)AhU?+`Hg z5;Z&D43kxQQxB9&!lYXp2j3Z_ZSZCGVP|s_2@H%OmUp&1RX7JK9s0Y6<axOtttT_|A@@jh#PPL&T4x2%xMfwP%M@t57G9MD^3KO=S;MihJbzQc2MJra6z?BE|M-xKKmtn37Y9U zO)NRIMpx^sR;0RtaV3-ksQI=ud{VKk%a70#l_x9PS+Q^nk}dE~gR>9gi7H0^Dz9@#<{}kT#B$8NpZKzT12X-tZo4fXRpzaqVdW*I_w5E*6P=p{( zG?z!Kz&?aBXG@eDwq{Nv%kUYi6~AeT5~vZ(x72R)Ai8EZPCcCID?5B*oIrpjM3V9R z;Ox({rm<5wMXY&k;Zsic9y~(dWN_~5NW07+xedGnUECS*jev^=AvSRMof14v{>rG!B$rBd4S!nfq&p|`1X%#j` zJbU)jW**5U8zGNG9HfSQ*q_3NxDE~jomlAv{t_{_Gs;U<1WCpCA3t8^CRoBA@>tTP zOQmv7u65!S392x>f}(idAyA9^Aqce7)200Mx;c(c4)kEyW|oFG(3^Jo*fP;5+|Mm& z9TM_{(7NT8?fS?wB|r8O+=0$U>^iNktlFZWKfV2y-)j@Q^Xs)bEQ;2;&nPFb=`2N@ zV+q*mFdL}3F_Ex9>^gj2>9y*@pEbuEmO?71j=;YHAmf-Ftj7<{N8CAyEkT4{50UQ^=RB3ns;xXJSai-+$Ggcm z5uD8e*9UZz3vZfbrGII{LeTn;YOU?76*%sfO=|68=bEcTyOfK$&_IzFM~y=8(|4g6 zw2VdeSL?Ht%oa~ATbzSL2~WuV)xnD>nB+9Ou8TA&JIYDOj)doi?YAqA{_|^5;u%+J zgAAP*@2KCrgqWc#P+|Dolc;}qOEuuHynAlVIyrrQQ5(Bnghs@wP-ivq$$!f%YftJO zN);aAddWCu{h86!ma^e1q#aIi{p7WVA?Acy+#if^*JIw4h5ONGU-8qM>#!nRk_8!P zh)=S`K*}f>o3AfW@fah?vRH&aq5U}fute?pfrwUr6Jhg!&(M~F${l+gqQ`*(y2X*Z z9ss3|wYq%Z8e(fJ;<{tzmQLsO(M1zBz>9$1-JGM!{LI3&9#|#8T1HuTdN|jyFUQNS zf7w-!2nU+nE!W|RpVx*PqsH#<5atvntaii|D`)3*^?WFzy~XXA8?4Y1Fh9TDI-W;d zhlfZQdk4J3k#7@2?}f)>!Ln!g53=U>St(Zj>=PdpL(9aePH=K01 zYpR2O-6w!$0Tc@W(_w{f(Sw6B@qKWs$Xd-RWda`}>NrjNOVxO#Y&F4XE#kZ%P!b`j zG@>sj_C})O*wNz!FQcw!7YUOSV5E^x3eL_cbb^Lb*GCsIYPlgUXjZ!)R~Hl9VP`0K zn!uLxpN5oLTqPiJiQAqAn$Ei36HkqjZdpBcj}%{_Et0)E`4j^xs9hrJDao)|>lzA- zo!p+XEh_mW!|?&rry`n@0>Sk6u96wzT3wT9b9LzhG-L*aPT~ZVCIKUvyN2&S^x<)G z^VM$Ryl9r=oz2xrbQk8GCwjx2F%!GUdD7yoyd@x`QQRtL_Nn22;FV&SV8 z)6B=vN0r1f!_9t0_WHcp7DLP1Ur|D`O=+14)Yo|Q7f%Yh0_U_4a4UV8xS99qj(oXLslGk?n3N0 zsm_)tkM#;a4tNhEmyK6~X&x?Pj&#Q2dvOgi=AR)vVi)vOaj&pfIkc!)!9k1-+X~|TD)jzyeBl$q)Dc7Th?LN6)rvWeQ-_VtQ_8BKRSA`o9VLW zB2tduueFx;oN(1U1QG>hkBCS^7K6GaToIyah_FI2kyP_UKJ`m;m~QaPS6@vG03pV_8QyOYJz z_JY_YK1(QqH6xo-A};7w8?~^}p!mr=Al1S*vCX>6(>VSXyi7sZ4&M$@)vAr?Qh5&K zwQ18=U{eECG^_B3aE{ zSOK5QuF-vt_hNXRL7_KK~ z#noD1eY!I6)qBB}pRH~|uoUuhr_&}gAO!`bkyZvPacBo$j7-QwSOHvox$h~wPR^}J zz0?TB5;0HcT;L{WSp5ahB3)W1Ziv>jdApPWQ#>{Q>85tHy?K6bKdH6xqs&e>%r%VO zL-4r*W<>0|jT(tp1O|Nal+w9da<=h|Xh)=ZJXC0I+^NT?j3V+g7jbMs5 zNCYsVuOMbmGlEX1B%2u1wr@f&Q#}8o|6`-RrLcPpBK%_xOX?i6&JA ziEx&@hnka9DmS;BMCJnU8SYL$xDy@Fswb!?8eHabs{LYBy)$33PT6Pr-Ey*<`(srJbP7=Ay`{o}GI%ps8m+ z^r`Hy+t$>DyPUxcA3FLv7l&Hl;Mj~XkhEs#+KsWZA165u@`84W)Kol^(BGD>$fsb)5r!d+ zUcx+SZjMg<(v)v-S&-5n2k;9$hEumWC{#(>!M)P;S}#!nS}vAwsyy|_W3`S}J0q>P zyiAyN-q@c7X#y{k>#>hJDtoN_RiZP>heO?O%D3K{W6kzA!FeMhV{b=ffyEbsu}W<8 zJl7fF#f88qIr+IWnN0C0&vw@vUk!w?ioZiqb?7|>idx>y*kedFszN-ypZXHGzY;v3 z4um_=Q#5S00|-u8E}eUK2w$%siEDg5!?Idmt=Zq;_*0trbXVv`4T-=@tEP!CU+d4{ zh7isZ(x~;_QQC{!^zz>9I{GuZFwwGc5~zvA8Fc6-Mg6Q$Hf&l6w|c&6J(M}N-WZHY zCc4^vE6=Yg?GD)%y6`e+93xCCu3}ii?R*k4iPZ1S8>CeuoGU}=6Iak8^wFk$RlBeZ z%9*?~%KnR>LF@!k(YbS2no6xB7$Ny&o4U4qA5Z&ybL=CG4X)p{_2&M4aqLy6XU$p- zQ{Vf0xlcJ!Zoysjq`N;w=xgm&7DHeg;e3#QG?j)2g?9R&uirm941DLn)rKDWhF;vU zz}&rUJAX(`x0)3;7U@p8QR=&XATvB^lUiyt?+?5{H=btfeOT(MM}?_OH_1F;QQr0> z2gwn#kv!+MU|qoaWQ+U$_G*Lf>z)(S!&T1RRz4B^$S{qt&fTU!Ef02?Io{E5IEM|U zSUMWh_kuW8BZ zbxx2U;85dTl3&ix73e|A0~&wE8r9_5_%*WX#p^YhZ2#sEp&hZg9Ag)uFY84MGc8A3 zdIS?gdEuEAs-eXF?1H?XNV&fl(kp!hx_c%c?;W&cK_m#d$vgd^jJbR-byC(Z-#0`( zdIu$J_-aa_YdNy?%a{IdHlB%>Q*{1Zc3|!%Yu@Sb<3TRvUdcmw@w7_kEg#zVeYeZ& z&VURFa}cM+oC0{E-hA=1uv+I2&NeUw{`qmWK!(I7=pCx~yc~4Z6*1XN4emY~yW`1Q z7`~oqBV*TJ0ene|qRjmH9=^EO*^xgMQQ+cjL3VaZFc+M{tTH^J9LV50<7VYnXW6P( zE*V}#ziM_?8f4xIj4N#e%@gt)KOC)R6jVhJ-B%7Zb_2f{3#d_XTXfXu1)J0$p8duV7&st&Ay7?A zaw%%HVfAsqY!i?>z29UR9 z8}WCDdwY(ztg0qbtL9u({mFr3edecq#hBK}97FP;$cPiM0UEE$XB|*JpXm++16=S? z5Tp&u|7o6`aYu*#E2{f`M`^m`DfrcN?h5+pwP?Eo@A{u-bOJYl$xMY@_Wf42&CNDy z>Oc5`?hPr}Z!pDQlybqD{@8E2c~PA5MF#XCq5Z{gpdULnRUag%5=Xwws+&DHOf)Y= zw3%HxtN#?jOtfXn?`JE0n>}HyYa2s!Py-eKq0@AmJskxyfKvo{yl%T|k`xA>ZzY_L zi#WF5NKQY<)RbWSwP%Pm4nRYAdQW8F)#!Dk*_;ylq4Y2VZlh;9C0~EC`h-Lge_8h& zZ5bUPKcBC_l?WHH;hm53rAt#2Hi!$zB9&g})Is&E15l#k3R1E?P@!lpsA=l4&BCJ+ z*TYcM>UR+rn4PjglX!@w`!`=>-QfX(v2AVbc#GHpl z^Ak9wto^X1d(50mzGOr-?Kv{iR$5e@L}gFnq?>)@>O%YYs6n|G)W4guI5GcmkdNIf zSie*6#LSRMb{P3YHIuOekyt^JG(ejv!d`I$xYzQ7POh)dNJoJ}kO@2ZhHLW|&S>;3 zVzp|z#3@ddXLJ#L_s^H_T`jdc|HiUu0kJsBfbz@7Iy zK>dy}-b#;qmB@S->vJN|%aw$MTuk7Ie$<5Nub#5W#Cg5wc0$Llq==rcem%U5@C|cq zHt0!CS$LdGDaDFx)kmzRUv|)^a>MxCuj(**ljt_^i>YB5_`Od>dQXMLEv#v$e$Ox9%x&1HVbt75Ni1JA&6pMOiN>kwEoyAW8YKyn!77# zCb4DIy6DW#2k_n?3OK@oX$bO71R2kj0&Ow#q3_|ldVNmQQ>M{-A!4E_gQ;MMEhP+p zIw$D)-Gfbwdb0Y=%Z$!#umX3O2Pv2Jvp)ap(j?bdX}qb4Qq7+^Rhcr z@BOz39p8O%ignt*R~v$>wHv7)H6-`KE5hmm)}f}6s}exm+CmZw0C*luK|)m$X-uJS zDot5R`waa{6pb)bQ4aO9>c-k>JmTXURuz_sDH5Im(KN8JR=!{e8G&W z63goypF@K2eZ>*^WN?M(he}(CoZ((E<_elI)W&H;SVv>&a2)y|R{ov;=FZ>tv(i%_ zl`GV(zrbp7opW#$xKi+&-y*AQmm=I*yFjIwGs@`M7J@gGGV|rvYqqfA%-6#oN+4H4(YXvx{7TVqrWMbD3PD;t16P1*9Y$cm93cm8~?;7{}Z zJ!lL~d`Ifn1)I*-vKH1XEtLZLRjN%Cm#R;TDbyIZt40NN=P)%Sa@=UjvsZeIUT>CA z@Q4mg@(>vc%i(V1IA=e9s_r$nRnGtEZb%Q}%7wb?YF!e*FAP8N-*!(+n?b`0F%%(vq)#g`= zN#5yG__MFoF7fDO0P?|6!1%CCeP47BxKf0;fm5d&ZI9J_6`F?s_#un44Kg|zZv z!1tO6t^Oj~busNZBEtr#bASu+gcj8PaD?uC`;*gs&=*Itqa~TFbKy{`Amv7CnZ`1UB$IRL(qBPm-AqT{8 zQJ-a^{JbxSP-k>h7cv?8-K}#9ZvFsQv7VHOfszYq;3T9rB7> z5=d+>AjUl!G}F##`nUUb+id?seEuVY5Ou>D2HIJa!~(8i(+>_AIm1X&=lyb#;3ETa ze%p597}r7^o!gMgcLgeV3O?28fBIVrA7PCP8^WydM@9=1)0szS%#a3@1W= z^ZEJn=V;^#d2df4v@_)vqcE4|DgpVg-rO|9 z-YF^G$e6x9zcOs^Qhbf*#iWh#9q7@W4P5)s=;FCIkiHPz$fFPT5ib_1fREqf-+%s~ zb>N}c5?7yhc2mRM+&lzJn>y%Gs%Z5(Gb(=IJ2(*BtM3+upFsP~U>V3TF&MT0!loAW zdjbcK2x3kX&1|CG$qZu9;*SCv9Q$=dqBEn3my#~!Dj&aq|Fz}0IOBmmv)2qDCnEF* zlES>x?X|umQLeTk5&B|RUbh}i@|ASjPSvZ3Anb-*(ac=wWCtm=LcISmfK3gijVatu zZd`()81^DzQ=$H28@ot@T1kagq1Z+7RQm7J0X~ELLOdR)(7LdOyJn{7`E#b&q0eH< zcqaDt1K$r6EZY(1V@2ZrF;X=HzuL6ARkY8`$JnCbZd}JOFZ4LR{}@w8SMX2hTyi2W zP@8YR|DSh(kKg$~U&kD`?_cU(1_v<&p>q9aULJoZst?_w&Yzm?^GpG%fPZYo=s@^o zxAu}$T|uf2Cjzm5P2^vnL!ZI>Cp`10p!;(a>S)mjYPzkdR0j9c42ge6=_g79t)-^> zWk&nwtA3(d@D^fnTFxQ#s=cXnd;d1c7(@8ki9fmreK<*d`m>8Q{l5msMElo=vpE9V zer*$$L@w9`_%Fu)oE04W3F0~u!|P-RuiNkNrzcCh^nZ*}*LSo~=bs#^@Cx&hPEZhL z4@GWC{+T$$5(4bcYCGjTf8Hv6sVo%}*WmHWguu6Xj^m#bfubQc&1Zm#(}VSGZRd23 zIm7ASGWty+{rLg`uB`qlSB1biy>pMt%agDK)V1rq_3oaZAv<2SHebc_MnGN52Rpj^ zq<^d-CcaxbJcO+8U*A>IT%}r*k)`ES`!L-K3S{ic-Bs+%npDUC87j1{PFZ6$Tl&2ys+ARGEDRf_J91gkNMYuFrY2Y zGSYZCcFA!Q4aWa0r2qFHXcM|y`saNP^V|RTlm9U$ss?8$r=}nIuRG$;>_4stXCmx^ zac|_vtUuQO=eK{q6Us=f5Y}62|F22;_i_}?-iL=JG!11R{Z|tzNP!G0?p%t0mf!!j zHLfR`{Pu!`)91gLpf2$P(=k?d|Lf-cb3XvX0j2?@x-A^gt@PhaND^QdNUlxr{_mmv zIf$|=FirDj!@~ literal 0 HcmV?d00001 diff --git a/hw3/requirements.txt b/hw3/requirements.txt new file mode 100644 index 00000000..bb6b4134 --- /dev/null +++ b/hw3/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn +prometheus-fastapi-instrumentator \ No newline at end of file diff --git a/hw3/settings/prometheus/prometheus.yml b/hw3/settings/prometheus/prometheus.yml new file mode 100644 index 00000000..6bdf88e7 --- /dev/null +++ b/hw3/settings/prometheus/prometheus.yml @@ -0,0 +1,10 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + +scrape_configs: + - job_name: demo-service-local + metrics_path: /metrics + static_configs: + - targets: + - local:8080 diff --git a/hw3/shop_api/__init__.py b/hw3/shop_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw3/shop_api/handlers/cart.py b/hw3/shop_api/handlers/cart.py new file mode 100644 index 00000000..727c31fc --- /dev/null +++ b/hw3/shop_api/handlers/cart.py @@ -0,0 +1,108 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response +from fastapi.responses import JSONResponse +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt + +from shop_api.models.cart import CartOutSchema +from shop_api import local_data + + +router = APIRouter(prefix="/cart") + + +@router.post( + "", + response_model=CartOutSchema, + status_code=HTTPStatus.CREATED +) +async def add_cart(response: Response): + cart_id = str(uuid.uuid4()) + cart_data = {"id": cart_id} + + local_data.add_single_cart(cart_data=cart_data) + + response.headers["Location"] = f"/cart/{cart_id}" + + return cart_data + + +@router.get( + "/{cart_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def get_cart_by_id(cart_id: str): + cart_data = local_data.get_single_cart(cart_id=cart_id) + + return cart_data + + +@router.get( + "", + response_model=List[CartOutSchema], + status_code=HTTPStatus.OK +) +async def get_all_carts( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + min_quantity: Annotated[NonNegativeInt, Query()] = None, + max_quantity: Annotated[NonNegativeInt, Query()] = None, +): + all_carts = local_data.get_all_carts() + + filtered_carts: List[CartOutSchema] = [] + for cart in all_carts: + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + if min_quantity is not None and sum([item.quantity for item in cart.items]) < min_quantity: + continue + if max_quantity is not None and sum([item.quantity for item in cart.items]) > max_quantity: + continue + + filtered_carts.append(cart) + + filtered_carts = filtered_carts[offset: offset + limit] + + return filtered_carts + + +@router.post( + "/{cart_id}/add/{item_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def add_item_to_cart( + cart_id: str, + item_id: str +): + cart = local_data.get_single_cart(cart_id=cart_id) + + if cart is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Cart with {cart_id=!r} wasn't found", + ) + + item_ids = local_data.get_all_item_ids_for_cart( + cart_id=cart_id + ) + + if item_id in item_ids: + for item in cart.items: + if item_id == item.id: + item.quantity += 1 + else: + cart.items.append( + local_data.get_single_item( + item_id=item_id + ) + ) + + return cart diff --git a/hw3/shop_api/handlers/item.py b/hw3/shop_api/handlers/item.py new file mode 100644 index 00000000..8f16b6b4 --- /dev/null +++ b/hw3/shop_api/handlers/item.py @@ -0,0 +1,158 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response +from fastapi.responses import JSONResponse +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt + +from shop_api.models.cart import CartOutSchema +from shop_api import local_data +from shop_api.models.item import ItemPatchSchema, ItemSchema, ItemCreateSchema + + +router = APIRouter(prefix="/item") + + +@router.post( + "", + response_model=ItemSchema, + status_code=HTTPStatus.CREATED, +) +async def add_item( + response: Response, + item: ItemCreateSchema +): + item_id = str(uuid.uuid4()) + item_data = { + "id": item_id, + **item.model_dump() + } + + local_data.add_single_item( + item_id=item_id, + item_data=item_data + ) + + response.headers["Location"] = f"/item/{item_id}" + + return item_data + + +@router.get( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def get_item_by_id(item_id: str): + item_data = local_data.get_single_item(item_id=item_id) + if item_data.deleted: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} was deleted" + ) + return item_data + + +@router.get( + "", + response_model=List[ItemSchema], + status_code=HTTPStatus.OK +) +async def get_all_items( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + show_deleted: Annotated[bool, Query()] = False, +): + all_items = local_data.get_all_items() + + filtered_items: List[ItemSchema] = [] + for item in all_items: + if min_price and item.price < min_price: + continue + if max_price and item.price > max_price: + continue + if not show_deleted and item.deleted: + continue + + filtered_items.append(item) + + filtered_items = filtered_items[offset: offset + limit] + + return filtered_items + + +@router.put( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item( + item_id: str, + item: ItemSchema +): + if local_data.get_single_item(item_id=item_id) is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + local_data.add_single_item( + item_id=item_id, + item_data=item + ) + + return item + + +@router.patch( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item_fields( + item_id: str, + item: ItemPatchSchema +): + old_item = local_data.get_single_item(item_id=item_id) + if old_item is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + if old_item.deleted: + raise HTTPException( + HTTPStatus.NOT_MODIFIED, + f"Item with {item_id=!r} has been deleted" + ) + + update_data = item.model_dump(exclude_unset=True) + updated_item = old_item.model_copy(update=update_data) + + local_data.add_single_item( + item_id=item_id, + item_data=updated_item + ) + + return updated_item + + +@router.delete( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def delete_item(item_id: str): + item_data = local_data.get_single_item(item_id=item_id) + if item_data is None: + raise HTTPException( + HTTPStatus.NOT_FOUND, + f"Item with {item_id=!r} is not found" + ) + + local_data.delete_item(item_id=item_id) + return item_data + \ No newline at end of file diff --git a/hw3/shop_api/local_data.py b/hw3/shop_api/local_data.py new file mode 100644 index 00000000..4ba9ea0c --- /dev/null +++ b/hw3/shop_api/local_data.py @@ -0,0 +1,59 @@ +from pydantic import validate_call +from typing import Dict, List, Set, Union +from shop_api.models.cart import CartOutSchema +from shop_api.models.item import ItemSchema + + +_local_data_carts: Dict[str, CartOutSchema] = {} +_local_data_items: Dict[str, ItemSchema] = {} + + +@validate_call +def add_single_cart(cart_data: CartOutSchema) -> None: + cart_id = cart_data.id + _local_data_carts[cart_id] = cart_data + + +def get_single_cart( + cart_id: str +) -> Union[CartOutSchema, None]: + if cart_id not in _local_data_carts: + return None + + return _local_data_carts[cart_id] + + +@validate_call +def get_all_carts() -> List[CartOutSchema]: + return list(_local_data_carts.values()) + + +@validate_call +def add_single_item( + item_id: str, + item_data: ItemSchema +) -> None: + item_data.id = item_id + _local_data_items[item_id] = item_data + + +@validate_call +def get_single_item(item_id: str) -> Union[ItemSchema, None]: + if item_id not in _local_data_items: + return None + + return _local_data_items[item_id] + + +@validate_call +def get_all_items() -> List[ItemSchema]: + return list(_local_data_items.values()) + + +def get_all_item_ids_for_cart(cart_id: str) -> Set[str]: + return set(list(_local_data_carts[cart_id].model_fields.keys())) + + +def delete_item(item_id: str) -> None: + item = _local_data_items.get(item_id) + item.deleted = True diff --git a/hw3/shop_api/main.py b/hw3/shop_api/main.py new file mode 100644 index 00000000..06998ecf --- /dev/null +++ b/hw3/shop_api/main.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI +from prometheus_fastapi_instrumentator import Instrumentator + +from shop_api.handlers.cart import router as cart_router +from shop_api.handlers.item import router as item_router + + +app = FastAPI(title="Shop API") +Instrumentator().instrument(app).expose(app) + +app.include_router(cart_router) +app.include_router(item_router) diff --git a/hw3/shop_api/models/cart.py b/hw3/shop_api/models/cart.py new file mode 100644 index 00000000..b2e932b0 --- /dev/null +++ b/hw3/shop_api/models/cart.py @@ -0,0 +1,14 @@ +from typing import List +from pydantic import BaseModel, Field, computed_field + +from shop_api.models.item import ItemSchema + + +class CartOutSchema(BaseModel): + id: str + items: List[ItemSchema] = Field(default_factory=list) + + @computed_field + @property + def price(self) -> float: + return sum(item.price * item.quantity for item in self.items) diff --git a/hw3/shop_api/models/item.py b/hw3/shop_api/models/item.py new file mode 100644 index 00000000..a708bc1a --- /dev/null +++ b/hw3/shop_api/models/item.py @@ -0,0 +1,23 @@ +import uuid +from pydantic import BaseModel, ConfigDict + + +class ItemSchema(BaseModel): + id: str = str(uuid.uuid4()) + name: str + price: float + deleted: bool = False + quantity: int = 1 + + +class ItemCreateSchema(BaseModel): + name: str = "" + price: float = 0.0 + + +class ItemPatchSchema(BaseModel): + name: str = "" + price: float = 0.0 + quantity: int = 1 + + model_config = ConfigDict(extra='forbid') From f3fd5fcc4092e14bb644c6eeb99ba57e0dab7b69 Mon Sep 17 00:00:00 2001 From: sardor014 Date: Sun, 26 Oct 2025 07:18:50 +0300 Subject: [PATCH 4/5] [HW4] add integration with db --- hw4/Dockerfile | 23 +++++ hw4/docker-compose.yml | 43 ++++++++ hw4/requirements.txt | 5 + hw4/shop_api/__init__.py | 0 hw4/shop_api/db/config.py | 14 +++ hw4/shop_api/db/session.py | 27 +++++ hw4/shop_api/handlers/cart.py | 168 ++++++++++++++++++++++++++++++ hw4/shop_api/handlers/item.py | 172 +++++++++++++++++++++++++++++++ hw4/shop_api/local_data.py | 59 +++++++++++ hw4/shop_api/main.py | 19 ++++ hw4/shop_api/models/cart.py | 33 ++++++ hw4/shop_api/models/cart_item.py | 30 ++++++ hw4/shop_api/models/item.py | 36 +++++++ hw4/shop_api/models/product.py | 27 +++++ 14 files changed, 656 insertions(+) create mode 100644 hw4/Dockerfile create mode 100644 hw4/docker-compose.yml create mode 100644 hw4/requirements.txt create mode 100644 hw4/shop_api/__init__.py create mode 100644 hw4/shop_api/db/config.py create mode 100644 hw4/shop_api/db/session.py create mode 100644 hw4/shop_api/handlers/cart.py create mode 100644 hw4/shop_api/handlers/item.py create mode 100644 hw4/shop_api/local_data.py create mode 100644 hw4/shop_api/main.py create mode 100644 hw4/shop_api/models/cart.py create mode 100644 hw4/shop_api/models/cart_item.py create mode 100644 hw4/shop_api/models/item.py create mode 100644 hw4/shop_api/models/product.py diff --git a/hw4/Dockerfile b/hw4/Dockerfile new file mode 100644 index 00000000..43812477 --- /dev/null +++ b/hw4/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12 AS base + +ARG PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PIP_NO_CACHE_DIR=on \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=500 + +RUN apt-get update && apt-get install -y gcc +RUN python -m pip install --upgrade pip + +WORKDIR $APP_ROOT/src +COPY . ./ + +ENV VIRTUAL_ENV=$APP_ROOT/src/.venv \ + PATH=$APP_ROOT/src/.venv/bin:$PATH + +RUN pip install -r requirements.txt + +FROM base as local + +CMD ["uvicorn", "shop_api.main:app", "--port", "8080", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/hw4/docker-compose.yml b/hw4/docker-compose.yml new file mode 100644 index 00000000..98c5f8d5 --- /dev/null +++ b/hw4/docker-compose.yml @@ -0,0 +1,43 @@ + +version: "3" + +services: + + local: + build: + context: . + dockerfile: ./Dockerfile + target: local + restart: always + ports: + - 8080:8080 + environment: + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_USER: postgres + DATABASE_PASSWORD: password + DATABASE_NAME: hw4_db + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:15 + container_name: hw4_postgres + environment: + POSTGRES_DB: hw4_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./migrations:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/hw4/requirements.txt b/hw4/requirements.txt new file mode 100644 index 00000000..83ab7e76 --- /dev/null +++ b/hw4/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +sqlalchemy +asyncpg +pydantic-settings \ No newline at end of file diff --git a/hw4/shop_api/__init__.py b/hw4/shop_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hw4/shop_api/db/config.py b/hw4/shop_api/db/config.py new file mode 100644 index 00000000..7969d4a0 --- /dev/null +++ b/hw4/shop_api/db/config.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + DATABASE_HOST: str + DATABASE_PORT: int + DATABASE_USER: str + DATABASE_PASSWORD: str + DATABASE_NAME: str + + @property + def DATABASE_URL(self) -> str: + return f"postgresql+asyncpg://{self.DATABASE_USER}:{self.DATABASE_PASSWORD}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" + +settings = Settings() diff --git a/hw4/shop_api/db/session.py b/hw4/shop_api/db/session.py new file mode 100644 index 00000000..d19e303a --- /dev/null +++ b/hw4/shop_api/db/session.py @@ -0,0 +1,27 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base +from .config import settings + + +engine = create_async_engine(settings.DATABASE_URL, echo=True, future=True) + + +SessionLocal = sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +Base = declarative_base() + + +async def get_db() -> AsyncSession: + async with SessionLocal() as session: + try: + yield session + except Exception: + await session.rollback() + raise + finally: + await session.close() diff --git a/hw4/shop_api/handlers/cart.py b/hw4/shop_api/handlers/cart.py new file mode 100644 index 00000000..fe15c1c0 --- /dev/null +++ b/hw4/shop_api/handlers/cart.py @@ -0,0 +1,168 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response, Depends +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import selectinload + +from shop_api.db.session import get_db +from shop_api.models.cart import Cart as CartModel, CartOutSchema +from shop_api.models.item import Item as ItemModel +from shop_api.models.cart_item import CartItem as CartItemModel + + +router = APIRouter(prefix="/cart") + + +@router.post( + "", + response_model=CartOutSchema, + status_code=HTTPStatus.CREATED +) +async def add_cart( + response: Response, + db: AsyncSession = Depends(get_db) +): + db_cart = CartModel() + + db.add(db_cart) + await db.commit() + await db.refresh(db_cart) + + response.headers["Location"] = f"/cart/{db_cart.id}" + + return db_cart + + +@router.get( + "/{cart_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def get_cart_by_id( + cart_id: str, + db: AsyncSession = Depends(get_db) +): + try: + cart_uuid = uuid.UUID(cart_id) + except ValueError: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Invalid cart_id format") + + query = ( + select(CartModel) + .where(CartModel.id == cart_uuid) + .options( + selectinload(CartModel.cart_items) + .selectinload(CartItemModel.item) + ) + ) + result = await db.execute(query) + db_cart = result.scalar_one_or_none() + + if db_cart is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Cart with {cart_id=} wasn't found") + + return db_cart + + +@router.get( + "", + response_model=List[CartOutSchema], + status_code=HTTPStatus.OK +) +async def get_all_carts( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + min_quantity: Annotated[NonNegativeInt, Query()] = None, + max_quantity: Annotated[NonNegativeInt, Query()] = None, + db: AsyncSession = Depends(get_db) +): + query = ( + select(CartModel) + .options( + selectinload(CartModel.cart_items) + .selectinload(CartItemModel.item) + ) + ) + result = await db.execute(query) + all_carts_db = result.scalars().unique().all() + + all_cart_schemas = [CartOutSchema.model_validate(cart) for cart in all_carts_db] + + filtered_carts: List[CartOutSchema] = [] + for cart in all_cart_schemas: + if min_price is not None and cart.price < min_price: + continue + if max_price is not None and cart.price > max_price: + continue + + total_quantity = sum([cart_item.quantity for cart_item in cart.items]) + if min_quantity is not None and total_quantity < min_quantity: + continue + if max_quantity is not None and total_quantity > max_quantity: + continue + + filtered_carts.append(cart) + + return filtered_carts[offset: offset + limit] + + +@router.post( + "/{cart_id}/add/{item_id}", + response_model=CartOutSchema, + status_code=HTTPStatus.OK +) +async def add_item_to_cart( + cart_id: str, + item_id: str, + db: AsyncSession = Depends(get_db) +): + try: + cart_uuid = uuid.UUID(cart_id) + item_uuid = uuid.UUID(item_id) + except ValueError: + raise HTTPException(HTTPStatus.NOT_FOUND, "Invalid cart_id or item_id format") + + item_query = select(ItemModel).where( + ItemModel.id == item_uuid, + ItemModel.deleted == False + ) + db_item = (await db.execute(item_query)).scalar_one_or_none() + + if db_item is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Item (product) with {item_id=} wasn't found or was deleted") + + cart_query = ( + select(CartModel) + .where(CartModel.id == cart_uuid) + .options(selectinload(CartModel.cart_items)) + ) + db_cart = (await db.execute(cart_query)).scalar_one_or_none() + + if db_cart is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Cart with {cart_id=} wasn't found") + + existing_cart_item: CartItemModel | None = None + for cart_item in db_cart.cart_items: + if cart_item.item_id == item_uuid: + existing_cart_item = cart_item + break + + if existing_cart_item: + existing_cart_item.quantity += 1 + else: + new_cart_item = CartItemModel( + cart_id=cart_uuid, + item_id=item_uuid, + quantity=1 + ) + db.add(new_cart_item) + + await db.commit() + + return await get_cart_by_id(cart_id, db) diff --git a/hw4/shop_api/handlers/item.py b/hw4/shop_api/handlers/item.py new file mode 100644 index 00000000..43edf58c --- /dev/null +++ b/hw4/shop_api/handlers/item.py @@ -0,0 +1,172 @@ +from http import HTTPStatus +from typing import Annotated, List +import uuid + +from fastapi import APIRouter, HTTPException, Query, Response, Depends +from pydantic import NonNegativeFloat, NonNegativeInt, PositiveInt +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from shop_api.db.session import get_db + + +from shop_api.models.item import ( + Item as ItemModel, + ItemSchema, + ItemCreateSchema, + ItemPatchSchema +) + +router = APIRouter(prefix="/item") + + +@router.post( + "", + response_model=ItemSchema, + status_code=HTTPStatus.CREATED, +) +async def add_item( + response: Response, + item: ItemCreateSchema, + db: AsyncSession = Depends(get_db) +): + db_item = ItemModel(**item.model_dump()) + + db.add(db_item) + await db.commit() + await db.refresh(db_item) + + response.headers["Location"] = f"/item/{db_item.id}" + return db_item + + +@router.get( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def get_item_by_id( + item_id: str, + db: AsyncSession = Depends(get_db) +): + try: + item_uuid = uuid.UUID(item_id) + except ValueError: + raise HTTPException(HTTPStatus.NOT_FOUND, "Invalid item_id format") + + query = select(ItemModel).where(ItemModel.id == item_uuid) + result = await db.execute(query) + db_item = result.scalar_one_or_none() + + if db_item is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Item with {item_id=} is not found") + + if db_item.deleted: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Item with {item_id=} was deleted") + + return db_item + + +@router.get( + "", + response_model=List[ItemSchema], + status_code=HTTPStatus.OK +) +async def get_all_items( + offset: Annotated[NonNegativeInt, Query()] = 0, + limit: Annotated[PositiveInt, Query()] = 10, + min_price: Annotated[NonNegativeFloat, Query()] = None, + max_price: Annotated[NonNegativeFloat, Query()] = None, + show_deleted: Annotated[bool, Query()] = False, + db: AsyncSession = Depends(get_db) +): + query = select(ItemModel) + + if min_price: + query = query.where(ItemModel.price >= min_price) + if max_price: + query = query.where(ItemModel.price <= max_price) + if not show_deleted: + query = query.where(ItemModel.deleted == False) + + query = query.offset(offset).limit(limit) + + result = await db.execute(query) + all_items = result.scalars().all() + + return all_items + + +@router.put( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item( + item_id: str, + item: ItemSchema, + db: AsyncSession = Depends(get_db) +): + db_item = await get_item_by_id(item_id, db) + + db_item.name = item.name + db_item.price = item.price + db_item.deleted = item.deleted + + await db.commit() + await db.refresh(db_item) + + return db_item + + +@router.patch( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def change_item_fields( + item_id: str, + item: ItemPatchSchema, + db: AsyncSession = Depends(get_db) +): + db_item = (await db.execute( + select(ItemModel).where(ItemModel.id == uuid.UUID(item_id)) + )).scalar_one_or_none() + + if db_item is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Item with {item_id=} is not found") + + if db_item.deleted: + raise HTTPException(HTTPStatus.NOT_MODIFIED, f"Item with {item_id=} has been deleted") + + update_data = item.model_dump(exclude_unset=True) + + for key, value in update_data.items(): + setattr(db_item, key, value) + + await db.commit() + await db.refresh(db_item) + + return db_item + + +@router.delete( + "/{item_id}", + response_model=ItemSchema, + status_code=HTTPStatus.OK +) +async def delete_item( + item_id: str, + db: AsyncSession = Depends(get_db) +): + db_item = (await db.execute( + select(ItemModel).where(ItemModel.id == uuid.UUID(item_id)) + )).scalar_one_or_none() + + if db_item is None: + raise HTTPException(HTTPStatus.NOT_FOUND, f"Item with {item_id=} is not found") + + db_item.deleted = True + await db.commit() + await db.refresh(db_item) + + return db_item diff --git a/hw4/shop_api/local_data.py b/hw4/shop_api/local_data.py new file mode 100644 index 00000000..4ba9ea0c --- /dev/null +++ b/hw4/shop_api/local_data.py @@ -0,0 +1,59 @@ +from pydantic import validate_call +from typing import Dict, List, Set, Union +from shop_api.models.cart import CartOutSchema +from shop_api.models.item import ItemSchema + + +_local_data_carts: Dict[str, CartOutSchema] = {} +_local_data_items: Dict[str, ItemSchema] = {} + + +@validate_call +def add_single_cart(cart_data: CartOutSchema) -> None: + cart_id = cart_data.id + _local_data_carts[cart_id] = cart_data + + +def get_single_cart( + cart_id: str +) -> Union[CartOutSchema, None]: + if cart_id not in _local_data_carts: + return None + + return _local_data_carts[cart_id] + + +@validate_call +def get_all_carts() -> List[CartOutSchema]: + return list(_local_data_carts.values()) + + +@validate_call +def add_single_item( + item_id: str, + item_data: ItemSchema +) -> None: + item_data.id = item_id + _local_data_items[item_id] = item_data + + +@validate_call +def get_single_item(item_id: str) -> Union[ItemSchema, None]: + if item_id not in _local_data_items: + return None + + return _local_data_items[item_id] + + +@validate_call +def get_all_items() -> List[ItemSchema]: + return list(_local_data_items.values()) + + +def get_all_item_ids_for_cart(cart_id: str) -> Set[str]: + return set(list(_local_data_carts[cart_id].model_fields.keys())) + + +def delete_item(item_id: str) -> None: + item = _local_data_items.get(item_id) + item.deleted = True diff --git a/hw4/shop_api/main.py b/hw4/shop_api/main.py new file mode 100644 index 00000000..d2cc6207 --- /dev/null +++ b/hw4/shop_api/main.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI +from shop_api.db.session import engine, Base + +from shop_api.handlers.cart import router as cart_router +from shop_api.handlers.item import router as item_router +from shop_api.models.cart import Cart +from shop_api.models.item import Item + + +app = FastAPI(title="Shop API") + +@app.on_event("startup") +async def startup_event(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +app.include_router(cart_router) +app.include_router(item_router) diff --git a/hw4/shop_api/models/cart.py b/hw4/shop_api/models/cart.py new file mode 100644 index 00000000..260b9b44 --- /dev/null +++ b/hw4/shop_api/models/cart.py @@ -0,0 +1,33 @@ +import uuid +from typing import List +from pydantic import BaseModel, Field, computed_field + +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.orm import relationship +from shop_api.db.session import Base +from shop_api.models.cart_item import CartItemSchema + + +class Cart(Base): + __tablename__ = "carts" + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + + cart_items = relationship( + "CartItem", + back_populates="cart", + cascade="all, delete-orphan" + ) + + +class CartOutSchema(BaseModel): + id: uuid.UUID + items: List[CartItemSchema] = Field(default_factory=list) + + @computed_field + @property + def price(self) -> float: + return sum(cart_item.item.price * cart_item.quantity for cart_item in self.items) + + class Config: + from_attributes = True diff --git a/hw4/shop_api/models/cart_item.py b/hw4/shop_api/models/cart_item.py new file mode 100644 index 00000000..012a572d --- /dev/null +++ b/hw4/shop_api/models/cart_item.py @@ -0,0 +1,30 @@ +import uuid +from pydantic import BaseModel +from sqlalchemy import Column, Integer, ForeignKey +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.orm import relationship + +from shop_api.db.session import Base +from shop_api.models.item import ItemSchema + + +class CartItem(Base): + __tablename__ = "cart_items" + + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + quantity = Column(Integer, default=1, nullable=False) + + cart_id = Column(PG_UUID(as_uuid=True), ForeignKey("carts.id"), nullable=False) + item_id = Column(PG_UUID(as_uuid=True), ForeignKey("items.id"), nullable=False) + + cart = relationship("Cart", back_populates="cart_items") + item = relationship("Item") + + +class CartItemSchema(BaseModel): + id: uuid.UUID + quantity: int + item: ItemSchema + + class Config: + from_attributes = True diff --git a/hw4/shop_api/models/item.py b/hw4/shop_api/models/item.py new file mode 100644 index 00000000..af362cec --- /dev/null +++ b/hw4/shop_api/models/item.py @@ -0,0 +1,36 @@ +import uuid +from pydantic import BaseModel, ConfigDict + +from sqlalchemy import Column, String, Float, Boolean +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from shop_api.db.session import Base + + +class Item(Base): + __tablename__ = "items" + + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False, index=True) + price = Column(Float, nullable=False) + deleted = Column(Boolean, default=False, nullable=False, index=True) + + +class ItemSchema(BaseModel): + id: uuid.UUID + name: str + price: float + deleted: bool = False + + class Config: + from_attributes = True + + +class ItemCreateSchema(BaseModel): + name: str = "" + price: float = 0.0 + +class ItemPatchSchema(BaseModel): + name: str = "" + price: float = 0.0 + + model_config = ConfigDict(extra='forbid') \ No newline at end of file diff --git a/hw4/shop_api/models/product.py b/hw4/shop_api/models/product.py new file mode 100644 index 00000000..7a156b2a --- /dev/null +++ b/hw4/shop_api/models/product.py @@ -0,0 +1,27 @@ +import uuid +from sqlalchemy import Column, String, Float, Boolean +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from shop_api.db.session import Base +from pydantic import BaseModel + + +class Product(Base): + __tablename__ = "products" + id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False, index=True) + price = Column(Float, nullable=False) + deleted = Column(Boolean, default=False) + + +class ProductSchema(BaseModel): + id: str + name: str + price: float + + class Config: + from_attributes = True + + +class ProductCreateSchema(BaseModel): + name: str + price: float From babcf103b413fc96d3151047ef749cecd5677e21 Mon Sep 17 00:00:00 2001 From: sardor014 Date: Sun, 26 Oct 2025 16:56:22 +0300 Subject: [PATCH 5/5] [HW5] add tests for [HW2] and CI --- hw2/.github/workflows/test.yml | 36 +++++ hw2/hw/tests/handlers/test_cart.py | 204 +++++++++++++++++++++++++++++ hw2/hw/tests/handlers/test_item.py | 186 ++++++++++++++++++++++++++ hw2/pytest.ini | 2 + 4 files changed, 428 insertions(+) create mode 100644 hw2/.github/workflows/test.yml create mode 100644 hw2/hw/tests/handlers/test_cart.py create mode 100644 hw2/hw/tests/handlers/test_item.py create mode 100644 hw2/pytest.ini diff --git a/hw2/.github/workflows/test.yml b/hw2/.github/workflows/test.yml new file mode 100644 index 00000000..fa1beac6 --- /dev/null +++ b/hw2/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: HW2 tests + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + build-and-test: + name: Build and Test + + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-mock pytest-cov + + - name: Run tests + run: | + pytest \ No newline at end of file diff --git a/hw2/hw/tests/handlers/test_cart.py b/hw2/hw/tests/handlers/test_cart.py new file mode 100644 index 00000000..ca27b9d5 --- /dev/null +++ b/hw2/hw/tests/handlers/test_cart.py @@ -0,0 +1,204 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from http import HTTPStatus + +from shop_api.handlers.cart import router +from shop_api.models.item import ItemSchema +from shop_api.models.cart import CartOutSchema + + +@pytest.fixture +def client(): + app = FastAPI() + app.include_router(router) + with TestClient(app) as test_client: + yield test_client + +@pytest.fixture +def mock_data(): + item1 = ItemSchema(name="Test Item", id="item-1", quantity=1, price=10.0) + item2 = ItemSchema(name="Test Item", id="item-2", quantity=2, price=25.0) + + cart1_item = item1.model_copy() + cart1 = CartOutSchema(id="cart-123", items=[cart1_item]) + + cart2 = CartOutSchema( + id="cart-456", + items=[ + item1.model_copy(update={"quantity": 3}), + item2.model_copy() + ] + ) + + cart3 = CartOutSchema(id="cart-789", items=[]) + + return { + "item1": item1, + "item2": item2, + "cart1": cart1, + "cart2": cart2, + "cart3": cart3, + "all_carts": [cart1, cart2, cart3] + } + + +def test_add_cart(client: TestClient, mocker): + test_uuid = "new-cart-uuid" + mocker.patch( + "shop_api.handlers.cart.uuid.uuid4", + return_value=test_uuid + ) + mock_add_db = mocker.patch("shop_api.handlers.cart.local_data.add_single_cart") + + response = client.post("/cart") + assert response.status_code == HTTPStatus.CREATED + + expected_data = {"id": test_uuid} + assert response.json()["id"] == test_uuid + assert response.headers["Location"] == f"/cart/{test_uuid}" + + mock_add_db.assert_called_once_with(cart_data=expected_data) + + +def test_get_cart_by_id(client: TestClient, mocker, mock_data): + test_cart = mock_data["cart1"] + + mocker.patch( + "shop_api.handlers.cart.local_data.get_single_cart", + return_value=test_cart + ) + + response = client.get(f"/cart/{test_cart.id}") + assert response.status_code == HTTPStatus.OK + assert response.json() == test_cart.model_dump() + + +def test_add_item_to_cart_new_item(client: TestClient, mocker, mock_data): + cart = mock_data["cart3"] + item_to_add = mock_data["item1"] + + mocker.patch("shop_api.handlers.cart.local_data.get_single_cart", return_value=cart) + + mocker.patch( + "shop_api.handlers.cart.local_data.get_all_item_ids_for_cart", + return_value=[] + ) + + mocker.patch( + "shop_api.handlers.cart.local_data.get_single_item", + return_value=item_to_add + ) + + response = client.post(f"/cart/{cart.id}/add/{item_to_add.id}") + + assert response.status_code == HTTPStatus.OK + + response_data = response.json() + assert len(response_data["items"]) == 1 + assert response_data["items"][0]["id"] == item_to_add.id + assert response_data["items"][0]["quantity"] == 1 + + +def test_add_item_to_cart_existing_item(client: TestClient, mocker, mock_data): + cart = mock_data["cart1"] + item_id_to_add = "item-1" + + mocker.patch("shop_api.handlers.cart.local_data.get_single_cart", return_value=cart) + + mocker.patch( + "shop_api.handlers.cart.local_data.get_all_item_ids_for_cart", + return_value=[item_id_to_add] + ) + + response = client.post(f"/cart/{cart.id}/add/{item_id_to_add}") + assert response.status_code == HTTPStatus.OK + + response_data = response.json() + assert len(response_data["items"]) == 1 + assert response_data["items"][0]["id"] == item_id_to_add + assert response_data["items"][0]["quantity"] == 2 + + +def test_add_item_to_cart_cart_not_found(client: TestClient, mocker): + mocker.patch("shop_api.handlers.cart.local_data.get_single_cart", return_value=None) + + response = client.post("/cart/fake-cart-id/add/fake-item-id") + + assert response.status_code == HTTPStatus.NOT_FOUND + assert "wasn't found" in response.json()["detail"] + + +@pytest.mark.parametrize( + "params, expected_ids", + [ + ( + {"offset": 0, "limit": 2}, + ["cart-123", "cart-456"] + ), + ( + {"offset": 1, "limit": 2}, + ["cart-456", "cart-789"] + ), + ( + {"offset": 10, "limit": 10}, + [] + ), + ( + {"min_price": 50.0}, + ["cart-456"] + ), + ( + {"max_price": 50.0}, + ["cart-123", "cart-789"] + ), + ( + {"min_price": 5.0, "max_price": 20.0}, + ["cart-123"] + ), + ( + {"min_quantity": 2}, + ["cart-456"] + ), + ( + {"max_quantity": 2}, + ["cart-123", "cart-789"] + ), + ( + {"min_quantity": 1, "max_quantity": 1}, + ["cart-123"] + ), + ( + {"min_price": 5.0, "min_quantity": 2}, + ["cart-456"] + ), + ( + {"max_price": 90.0, "max_quantity": 1}, + ["cart-123", "cart-789"] + ), + ( + {"min_price": 1000.0}, + [] + ), + ( + {}, + ["cart-123", "cart-456", "cart-789"] + ), + ], +) +def test_get_all_carts_filtering( + client: TestClient, mocker, mock_data, params, expected_ids +): + mocker.patch( + "shop_api.handlers.cart.local_data.get_all_carts", + return_value=mock_data["all_carts"] + ) + + response = client.get("/cart", params=params) + + assert response.status_code == HTTPStatus.OK + + response_data = response.json() + response_ids = [cart["id"] for cart in response_data] + + assert response_ids == expected_ids \ No newline at end of file diff --git a/hw2/hw/tests/handlers/test_item.py b/hw2/hw/tests/handlers/test_item.py new file mode 100644 index 00000000..7a2f7f6d --- /dev/null +++ b/hw2/hw/tests/handlers/test_item.py @@ -0,0 +1,186 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from http import HTTPStatus + +from shop_api.handlers.item import router +from shop_api.models.item import ItemSchema + + +@pytest.fixture +def client(): + app = FastAPI() + app.include_router(router) + with TestClient(app) as test_client: + yield test_client + +@pytest.fixture +def mock_data(): + item1 = ItemSchema(id="item-1", name="Apple", price=10.0, quantity=5) + item2 = ItemSchema(id="item-2", name="Banana", price=100.0, quantity=1) + item3_deleted = ItemSchema(id="item-3", name="Cherry", price=50.0, quantity=10, deleted=True) + return { + "item1": item1, + "item2": item2, + "item3_deleted": item3_deleted, + "all_items": [item1, item2, item3_deleted] + } + +def test_add_item(client: TestClient, mocker): + test_uuid = "new-item-uuid" + + item_create_data = {"name": "Test Item", "price": 9.99, "quantity": 1} + + mocker.patch("shop_api.handlers.item.uuid.uuid4", return_value=test_uuid) + mock_add_db = mocker.patch("shop_api.handlers.item.local_data.add_single_item") + + response = client.post("/item", json=item_create_data) + + assert response.status_code == HTTPStatus.CREATED + assert response.headers["Location"] == f"/item/{test_uuid}" + + data_sent_to_db = { + "id": test_uuid, + "name": "Test Item", + "price": 9.99 + } + + data_returned_to_client = { + "id": test_uuid, + "name": "Test Item", + "price": 9.99, + "quantity": 1, + "deleted": False + } + + assert response.json() == data_returned_to_client + + mock_add_db.assert_called_once_with( + item_id=test_uuid, + item_data=data_sent_to_db + ) + +def test_get_item_by_id_success(client: TestClient, mocker, mock_data): + test_item = mock_data["item1"] + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=test_item) + + response = client.get(f"/item/{test_item.id}") + + assert response.status_code == HTTPStatus.OK + assert response.json() == test_item.model_dump() + +def test_get_item_by_id_deleted(client: TestClient, mocker, mock_data): + deleted_item = mock_data["item3_deleted"] + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=deleted_item) + + response = client.get(f"/item/{deleted_item.id}") + + assert response.status_code == HTTPStatus.NOT_FOUND + assert "was deleted" in response.json()["detail"] + +@pytest.mark.parametrize( + "params, expected_ids", + [ + ({}, ["item-1", "item-2"]), + ({"show_deleted": True}, ["item-1", "item-2", "item-3"]), + ({"limit": 1}, ["item-1"]), + ({"offset": 1}, ["item-2"]), + ({"offset": 1, "limit": 1, "show_deleted": True}, ["item-2"]), + ({"min_price": 20.0}, ["item-2"]), + ({"min_price": 20.0, "show_deleted": False}, ["item-2"]), + ({"max_price": 20.0}, ["item-1"]), + ({"min_price": 5.0, "max_price": 60.0, "show_deleted": True}, ["item-1", "item-3"]), + ({"min_price": 1000.0}, []), + ], +) +def test_get_all_items(client: TestClient, mocker, mock_data, params, expected_ids): + mocker.patch("shop_api.handlers.item.local_data.get_all_items", return_value=mock_data["all_items"]) + + response = client.get("/item", params=params) + + assert response.status_code == HTTPStatus.OK + response_ids = [item["id"] for item in response.json()] + assert response_ids == expected_ids + +def test_change_item(client: TestClient, mocker, mock_data): + existing_item = mock_data["item1"] + + updated_item_data = { + "id": existing_item.id, + "name": "New Apple", + "price": 15.0, + "quantity": 2, + "deleted": False + } + + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=existing_item) + mock_add_db = mocker.patch("shop_api.handlers.item.local_data.add_single_item") + + response = client.put(f"/item/{existing_item.id}", json=updated_item_data) + + assert response.status_code == HTTPStatus.OK + assert response.json() == updated_item_data + + updated_model = ItemSchema(**updated_item_data) + mock_add_db.assert_called_once_with( + item_id=existing_item.id, + item_data=updated_model + ) + +def test_change_item_not_found(client: TestClient, mocker): + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=None) + + invalid_data = {"id": "fake-id", "name": "Fake", "price": 1, "quantity": 1, "deleted": False} + response = client.put("/item/fake-id", json=invalid_data) + + assert response.status_code == HTTPStatus.NOT_FOUND + +def test_change_item_fields(client: TestClient, mocker, mock_data): + old_item = mock_data["item1"].model_copy() + patch_data = {"name": "Gala Apple", "quantity": 50} + + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=old_item) + mock_add_db = mocker.patch("shop_api.handlers.item.local_data.add_single_item") + + response = client.patch(f"/item/{old_item.id}", json=patch_data) + + assert response.status_code == HTTPStatus.OK + + expected_item = old_item.model_copy(update=patch_data) + assert response.json() == expected_item.model_dump() + + mock_add_db.assert_called_once_with( + item_id=old_item.id, + item_data=expected_item + ) + +def test_change_item_fields_not_found(client: TestClient, mocker): + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=None) + response = client.patch("/item/fake-id", json={"name": "test"}) + assert response.status_code == HTTPStatus.NOT_FOUND + +def test_change_item_fields_deleted(client: TestClient, mocker, mock_data): + deleted_item = mock_data["item3_deleted"] + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=deleted_item) + + response = client.patch(f"/item/{deleted_item.id}", json={"name": "test"}) + + assert response.status_code == HTTPStatus.NOT_MODIFIED + +def test_delete_item(client: TestClient, mocker, mock_data): + item_to_delete = mock_data["item1"] + + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=item_to_delete) + mock_delete_db = mocker.patch("shop_api.handlers.item.local_data.delete_item") + + response = client.delete(f"/item/{item_to_delete.id}") + + assert response.status_code == HTTPStatus.OK + assert response.json() == item_to_delete.model_dump() + + mock_delete_db.assert_called_once_with(item_id=item_to_delete.id) + +def test_delete_item_not_found(client: TestClient, mocker): + mocker.patch("shop_api.handlers.item.local_data.get_single_item", return_value=None) + response = client.delete("/item/fake-id") + assert response.status_code == HTTPStatus.NOT_FOUND \ No newline at end of file diff --git a/hw2/pytest.ini b/hw2/pytest.ini new file mode 100644 index 00000000..171cd2de --- /dev/null +++ b/hw2/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = hw \ No newline at end of file