From e4d3170769ac652193acc28c41f22cf3e10cd8ab Mon Sep 17 00:00:00 2001 From: Lostecho <31339626+IHHII@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:36:07 +0800 Subject: [PATCH] update --- fastapi-best-practices/README.md | 995 ++++++++++++++++++ .../images/custom_bad_response.png | Bin 0 -> 165737 bytes .../images/custom_responses.png | Bin 0 -> 700599 bytes fastapi-best-practices/images/type_hints.png | Bin 0 -> 399736 bytes .../images/type_hintsless.png | Bin 0 -> 337464 bytes 5 files changed, 995 insertions(+) create mode 100644 fastapi-best-practices/README.md create mode 100644 fastapi-best-practices/images/custom_bad_response.png create mode 100644 fastapi-best-practices/images/custom_responses.png create mode 100644 fastapi-best-practices/images/type_hints.png create mode 100644 fastapi-best-practices/images/type_hintsless.png diff --git a/fastapi-best-practices/README.md b/fastapi-best-practices/README.md new file mode 100644 index 0000000..439f218 --- /dev/null +++ b/fastapi-best-practices/README.md @@ -0,0 +1,995 @@ +## FastAPI Best Practices +Opinionated list of best practices and conventions we used at our startup. + +For the last 1.5 years in production, +we have been making good and bad decisions that impacted our developer experience dramatically. +Some of them are worth sharing. + +### Contents +1. [Project Structure. Consistent & predictable.](https://github.com/zhanymkanov/fastapi-best-practices#1-project-structure-consistent--predictable) +2. [Excessively use Pydantic for data validation.](https://github.com/zhanymkanov/fastapi-best-practices#2-excessively-use-pydantic-for-data-validation) +3. [Use dependencies for data validation vs DB.](https://github.com/zhanymkanov/fastapi-best-practices#3-use-dependencies-for-data-validation-vs-db) +4. [Chain dependencies.](https://github.com/zhanymkanov/fastapi-best-practices#4-chain-dependencies) +5. [Decouple & Reuse dependencies. Dependency calls are cached.](https://github.com/zhanymkanov/fastapi-best-practices#5-decouple--reuse-dependencies-dependency-calls-are-cached) +6. [Follow the REST.](https://github.com/zhanymkanov/fastapi-best-practices#6-follow-the-rest) +7. [Don't make your routes async, if you have only blocking I/O operations.](https://github.com/zhanymkanov/fastapi-best-practices#7-dont-make-your-routes-async-if-you-have-only-blocking-io-operations) +8. [Custom base model from day 0.](https://github.com/zhanymkanov/fastapi-best-practices#8-custom-base-model-from-day-0) +9. [Docs.](https://github.com/zhanymkanov/fastapi-best-practices#9-docs) +10. [Use Pydantic's BaseSettings for configs.](https://github.com/zhanymkanov/fastapi-best-practices#10-use-pydantics-basesettings-for-configs) +11. [SQLAlchemy: Set DB keys naming convention.](https://github.com/zhanymkanov/fastapi-best-practices#11-sqlalchemy-set-db-keys-naming-convention) +12. [Migrations. Alembic.](https://github.com/zhanymkanov/fastapi-best-practices#12-migrations-alembic) +13. [Set DB naming convention.](https://github.com/zhanymkanov/fastapi-best-practices#13-set-db-naming-convention) +14. [Set tests client async from day 0.](https://github.com/zhanymkanov/fastapi-best-practices#14-set-tests-client-async-from-day-0) +15. [BackgroundTasks > asyncio.create_task.](https://github.com/zhanymkanov/fastapi-best-practices#15-backgroundtasks--asynciocreate_task) +16. [Typing is important.](https://github.com/zhanymkanov/fastapi-best-practices#16-typing-is-important) +17. [Save files in chunks.](https://github.com/zhanymkanov/fastapi-best-practices#17-save-files-in-chunks) +18. [Be careful with dynamic pydantic fields.](https://github.com/zhanymkanov/fastapi-best-practices#18-be-careful-with-dynamic-pydantic-fields) +19. [SQL-first, Pydantic-second.](https://github.com/zhanymkanov/fastapi-best-practices#19-sql-first-pydantic-second) +20. [Validate hosts, if users can send publicly available URLs.](https://github.com/zhanymkanov/fastapi-best-practices#20-validate-hosts-if-users-can-send-publicly-available-urls) +21. [Raise a ValueError in custom pydantic validators, if schema directly faces the client.](https://github.com/zhanymkanov/fastapi-best-practices#21-raise-a-valueerror-in-custom-pydantic-validators-if-schema-directly-faces-the-client) +22. [Don't forget FastAPI converts Response Pydantic Object...](https://github.com/zhanymkanov/fastapi-best-practices#22-dont-forget-fastapi-converts-response-pydantic-object-to-dict-then-to-an-instance-of-responsemodel-then-to-dict-then-to-json) +23. [If you must use sync SDK, then run it in a thread pool.](https://github.com/zhanymkanov/fastapi-best-practices#23-if-you-must-use-sync-sdk-then-run-it-in-a-thread-pool) +24. [Use linters (black, isort, autoflake).](https://github.com/zhanymkanov/fastapi-best-practices#24-use-linters-black-isort-autoflake) +25. [Bonus Section.](https://github.com/zhanymkanov/fastapi-best-practices#bonus-section) +
Project sample built with these best-practices in mind.
+ +### 1. Project Structure. Consistent & predictable +There are many ways to structure the project, but the best structure is a structure that is consistent, straightforward, and has no surprises. +- If looking at the project structure doesn't give you an idea of what the project is about, then the structure might be unclear. +- If you have to open packages to understand what modules are located in them, then your structure is unclear. +- If the frequency and location of the files feels random, then your project structure is bad. +- If looking at the module's location and its name doesn't give you an idea of what's inside it, then your structure is very bad. + +Although the project structure, where we separate files by their type (e.g. api, crud, models, schemas) +presented by [@tiangolo](https://github.com/tiangolo) is good for microservices or projects with fewer scopes, +we couldn't fit it into our monolith with a lot of domains and modules. +Structure that I found more scalable and evolvable is inspired by Netflix's [Dispatch](https://github.com/Netflix/dispatch) with some little modifications. +``` +fastapi-project +├── alembic/ +├── src +│ ├── auth +│ │ ├── router.py +│ │ ├── schemas.py # pydantic models +│ │ ├── models.py # db models +│ │ ├── dependencies.py +│ │ ├── config.py # local configs +│ │ ├── constants.py +│ │ ├── exceptions.py +│ │ ├── service.py +│ │ └── utils.py +│ ├── aws +│ │ ├── client.py # client model for external service communication +│ │ ├── schemas.py +│ │ ├── config.py +│ │ ├── constants.py +│ │ ├── exceptions.py +│ │ └── utils.py +│ └── posts +│ │ ├── router.py +│ │ ├── schemas.py +│ │ ├── models.py +│ │ ├── dependencies.py +│ │ ├── constants.py +│ │ ├── exceptions.py +│ │ ├── service.py +│ │ └── utils.py +│ ├── config.py # global configs +│ ├── models.py # global models +│ ├── exceptions.py # global exceptions +│ ├── pagination.py # global module e.g. pagination +│ ├── database.py # db connection related stuff +│ └── main.py +├── tests/ +│ ├── auth +│ ├── aws +│ └── posts +├── templates/ +│ └── index.html +├── requirements +│ ├── base.txt +│ ├── dev.txt +│ └── prod.txt +├── .env +├── .gitignore +├── logging.ini +└── alembic.ini +``` +1. Store all domain directories inside `src` folder + 1. `src/` - highest level of an app, contains common models, configs, and constants, etc. + 2. `src/main.py` - root of the project, which inits the FastAPI app +2. Each package has its own router, schemas, models, etc. + 1. `router.py` - is a core of each module with all the endpoints + 2. `schemas.py` - for pydantic models + 3. `models.py` - for db models + 4. `service.py` - module specific business logic + 5. `dependencies.py` - router dependencies + 6. `constants.py` - module specific constants and error codes + 7. `config.py` - e.g. env vars + 8. `utils.py` - non-business logic functions, e.g. response normalization, data enrichment, etc. + 9. `exceptions.py` - module specific exceptions, e.g. `PostNotFound`, `InvalidUserData` +3. When package requires services or dependencies or constants from other packages - import them with an explicit module name +```python +from src.auth import constants as auth_constants +from src.notifications import service as notification_service +from src.posts.constants import ErrorCode as PostsErrorCode # in case we have Standard ErrorCode in constants module of each package +``` + +### 2. Excessively use Pydantic for data validation +Pydantic has a rich set of features to validate and transform data. + +In addition to regular features like required & non-required fields with default values, +Pydantic has built-in comprehensive data processing tools like regex, enums for limited allowed options, length validation, email validation, etc. +```python3 +from enum import Enum +from pydantic import AnyUrl, BaseModel, EmailStr, Field, constr + +class MusicBand(str, Enum): + AEROSMITH = "AEROSMITH" + QUEEN = "QUEEN" + ACDC = "AC/DC" + + +class UserBase(BaseModel): + first_name: str = Field(min_length=1, max_length=128) + username: constr(regex="^[A-Za-z0-9-_]+$", to_lower=True, strip_whitespace=True) + email: EmailStr + age: int = Field(ge=18, default=None) # must be greater or equal to 18 + favorite_band: MusicBand = None # only "AEROSMITH", "QUEEN", "AC/DC" values are allowed to be inputted + website: AnyUrl = None + +``` +### 3. Use dependencies for data validation vs DB +Pydantic can only validate the values from client input. +Use dependencies to validate data against database constraints like email already exists, user not found, etc. +```python3 +# dependencies.py +async def valid_post_id(post_id: UUID4) -> Mapping: + post = await service.get_by_id(post_id) + if not post: + raise PostNotFound() + + return post + + +# router.py +@router.get("/posts/{post_id}", response_model=PostResponse) +async def get_post_by_id(post: Mapping = Depends(valid_post_id)): + return post + + +@router.put("/posts/{post_id}", response_model=PostResponse) +async def update_post( + update_data: PostUpdate, + post: Mapping = Depends(valid_post_id), +): + updated_post: Mapping = await service.update(id=post["id"], data=update_data) + return updated_post + + +@router.get("/posts/{post_id}/reviews", response_model=list[ReviewsResponse]) +async def get_post_reviews(post: Mapping = Depends(valid_post_id)): + post_reviews: list[Mapping] = await reviews_service.get_by_post_id(post["id"]) + return post_reviews +``` +If we didn't put data validation to dependency, we would have to add post_id validation +for every endpoint and write the same tests for each of them. + +### 4. Chain dependencies +Dependencies can use other dependencies and avoid code repetition for similar logic. +```python3 +# dependencies.py +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt + +async def valid_post_id(post_id: UUID4) -> Mapping: + post = await service.get_by_id(post_id) + if not post: + raise PostNotFound() + + return post + + +async def parse_jwt_data( + token: str = Depends(OAuth2PasswordBearer(tokenUrl="/auth/token")) +) -> dict: + try: + payload = jwt.decode(token, "JWT_SECRET", algorithms=["HS256"]) + except JWTError: + raise InvalidCredentials() + + return {"user_id": payload["id"]} + + +async def valid_owned_post( + post: Mapping = Depends(valid_post_id), + token_data: dict = Depends(parse_jwt_data), +) -> Mapping: + if post["creator_id"] != token_data["user_id"]: + raise UserNotOwner() + + return post + +# router.py +@router.get("/users/{user_id}/posts/{post_id}", response_model=PostResponse) +async def get_user_post(post: Mapping = Depends(valid_owned_post)): + return post + +``` +### 5. Decouple & Reuse dependencies. Dependency calls are cached. +Dependencies can be reused multiple times, and they won't be recalculated - FastAPI caches dependency's result within a request's scope by default, +i.e. if we have a dependency that calls service `get_post_by_id`, we won't be visiting DB each time we call this dependency - only the first function call. + +Knowing this, we can easily decouple dependencies onto multiple smaller functions that operate on a smaller domain and are easier to reuse in other routes. +For example, in the code below we are using `parse_jwt_data` three times: +1. `valid_owned_post` +2. `valid_active_creator` +3. `get_user_post`, + +but `parse_jwt_data` is called only once, in the very first call. + +```python3 +# dependencies.py +from fastapi import BackgroundTasks +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt + +async def valid_post_id(post_id: UUID4) -> Mapping: + post = await service.get_by_id(post_id) + if not post: + raise PostNotFound() + + return post + + +async def parse_jwt_data( + token: str = Depends(OAuth2PasswordBearer(tokenUrl="/auth/token")) +) -> dict: + try: + payload = jwt.decode(token, "JWT_SECRET", algorithms=["HS256"]) + except JWTError: + raise InvalidCredentials() + + return {"user_id": payload["id"]} + + +async def valid_owned_post( + post: Mapping = Depends(valid_post_id), + token_data: dict = Depends(parse_jwt_data), +) -> Mapping: + if post["creator_id"] != token_data["user_id"]: + raise UserNotOwner() + + return post + + +async def valid_active_creator( + token_data: dict = Depends(parse_jwt_data), +): + user = await users_service.get_by_id(token_data["user_id"]) + if not user["is_active"]: + raise UserIsBanned() + + if not user["is_creator"]: + raise UserNotCreator() + + return user + + +# router.py +@router.get("/users/{user_id}/posts/{post_id}", response_model=PostResponse) +async def get_user_post( + worker: BackgroundTasks, + post: Mapping = Depends(valid_owned_post), + user: Mapping = Depends(valid_active_creator), +): + """Get post that belong the active user.""" + worker.add_task(notifications_service.send_email, user["id"]) + return post + +``` + +### 6. Follow the REST +Developing RESTful API makes it easier to reuse dependencies in routes like these: + 1. `GET /courses/:course_id` + 2. `GET /courses/:course_id/chapters/:chapter_id/lessons` + 3. `GET /chapters/:chapter_id` + +The only caveat is to use the same variable names in the path: +- If you have two endpoints `GET /profiles/:profile_id` and `GET /creators/:creator_id` +that both validate whether the given `profile_id` exists, but `GET /creators/:creator_id` +also checks if the profile is creator, then it's better to rename `creator_id` path variable to `profile_id` and chain those two dependencies. +```python3 +# src.profiles.dependencies +async def valid_profile_id(profile_id: UUID4) -> Mapping: + profile = await service.get_by_id(profile_id) + if not profile: + raise ProfileNotFound() + + return profile + +# src.creators.dependencies +async def valid_creator_id(profile: Mapping = Depends(valid_profile_id)) -> Mapping: + if not profile["is_creator"]: + raise ProfileNotCreator() + + return profile + +# src.profiles.router.py +@router.get("/profiles/{profile_id}", response_model=ProfileResponse) +async def get_user_profile_by_id(profile: Mapping = Depends(valid_profile_id)): + """Get profile by id.""" + return profile + +# src.creators.router.py +@router.get("/creators/{profile_id}", response_model=ProfileResponse) +async def get_user_profile_by_id( + creator_profile: Mapping = Depends(valid_creator_id) +): + """Get creator's profile by id.""" + return creator_profile + +``` + +Use /me endpoints for users resources (e.g. `GET /profiles/me`, `GET /users/me/posts`) + 1. No need to validate that user id exists - it's already checked via auth method + 2. No need to check whether the user id belongs to the requester + +### 7. Don't make your routes async, if you have only blocking I/O operations +Under the hood, FastAPI can [effectively handle](https://fastapi.tiangolo.com/async/#path-operation-functions) both async and sync I/O operations. +- FastAPI runs `sync` routes in the [threadpool](https://en.wikipedia.org/wiki/Thread_pool) +and blocking I/O operations won't stop the [event loop](https://docs.python.org/3/library/asyncio-eventloop.html) +from executing the tasks. +- Otherwise, if the route is defined `async` then it's called regularly via `await` +and FastAPI trusts you to do only non-blocking I/O operations. + +The caveat is if you fail that trust and execute blocking operations within async routes, +the event loop will not be able to run the next tasks until that blocking operation is done. +```python +import asyncio +import time + +@router.get("/terrible-ping") +async def terrible_catastrophic_ping(): + time.sleep(10) # I/O blocking operation for 10 seconds + pong = service.get_pong() # I/O blocking operation to get pong from DB + + return {"pong": pong} + +@router.get("/good-ping") +def good_ping(): + time.sleep(10) # I/O blocking operation for 10 seconds, but in another thread + pong = service.get_pong() # I/O blocking operation to get pong from DB, but in another thread + + return {"pong": pong} + +@router.get("/perfect-ping") +async def perfect_ping(): + await asyncio.sleep(10) # non-blocking I/O operation + pong = await service.async_get_pong() # non-blocking I/O db call + + return {"pong": pong} + +``` +**What happens when we call:** +1. `GET /terrible-ping` + 1. FastAPI server receives a request and starts handling it + 2. Server's event loop and all the tasks in the queue will be waiting until `time.sleep()` is finished + 1. Server thinks `time.sleep()` is not an I/O task, so it waits until it is finished + 2. Server won't accept any new requests while waiting + 3. Then, event loop and all the tasks in the queue will be waiting until `service.get_pong` is finished + 1. Server thinks `service.get_pong()` is not an I/O task, so it waits until it is finished + 2. Server won't accept any new requests while waiting + 4. Server returns the response. + 1. After a response, server starts accepting new requests +2. `GET /good-ping` + 1. FastAPI server receives a request and starts handling it + 2. FastAPI sends the whole route `good_ping` to the threadpool, where a worker thread will run the function + 3. While `good_ping` is being executed, event loop selects next tasks from the queue and works on them (e.g. accept new request, call db) + - Independently of main thread (i.e. our FastAPI app), + worker thread will be waiting for `time.sleep` to finish and then for `service.get_pong` to finish + - Sync operation blocks only the side thread, not the main one. + 4. When `good_ping` finishes its work, server returns a response to the client +3. `GET /perfect-ping` + 1. FastAPI server receives a request and starts handling it + 2. FastAPI awaits `asyncio.sleep(10)` + 3. Event loop selects next tasks from the queue and works on them (e.g. accept new request, call db) + 4. When `asyncio.sleep(10)` is done, servers goes to the next lines and awaits `service.async_get_pong` + 5. Event loop selects next tasks from the queue and works on them (e.g. accept new request, call db) + 6. When `service.async_get_pong` is done, server returns a response to the client + +The second caveat is that operations that are non-blocking awaitables or are sent to the thread pool must be I/O intensive tasks (e.g. open file, db call, external API call). +- Awaiting CPU-intensive tasks (e.g. heavy calculations, data processing, video transcoding) is worthless since the CPU has to work to finish the tasks, +while I/O operations are external and server does nothing while waiting for that operations to finish, thus it can go to the next tasks. +- Running CPU-intensive tasks in other threads also isn't effective, because of [GIL](https://realpython.com/python-gil/). +In short, GIL allows only one thread to work at a time, which makes it useless for CPU tasks. +- If you want to optimize CPU intensive tasks you should send them to workers in another process. + +**Related StackOverflow questions of confused users** +1. https://stackoverflow.com/questions/62976648/architecture-flask-vs-fastapi/70309597#70309597 + - Here you can also check [my answer](https://stackoverflow.com/a/70309597/6927498) +2. https://stackoverflow.com/questions/65342833/fastapi-uploadfile-is-slow-compared-to-flask +3. https://stackoverflow.com/questions/71516140/fastapi-runs-api-calls-in-serial-instead-of-parallel-fashion + +### 8. Custom base model from day 0. +Having a controllable global base model allows us to customize all the models within the app. +For example, we could have a standard datetime format or add a super method for all subclasses of the base model. +```python +from datetime import datetime +from zoneinfo import ZoneInfo + +import orjson +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel, root_validator + + +def orjson_dumps(v, *, default): + # orjson.dumps returns bytes, to match standard json.dumps we need to decode + return orjson.dumps(v, default=default).decode() + + +def convert_datetime_to_gmt(dt: datetime) -> str: + if not dt.tzinfo: + dt = dt.replace(tzinfo=ZoneInfo("UTC")) + + return dt.strftime("%Y-%m-%dT%H:%M:%S%z") + + +class ORJSONModel(BaseModel): + class Config: + json_loads = orjson.loads + json_dumps = orjson_dumps + json_encoders = {datetime: convert_datetime_to_gmt} # method for custom JSON encoding of datetime fields + + @root_validator() + def set_null_microseconds(cls, data: dict) -> dict: + """Drops microseconds in all the datetime field values.""" + datetime_fields = { + k: v.replace(microsecond=0) + for k, v in data.items() + if isinstance(k, datetime) + } + + return {**data, **datetime_fields} + + def serializable_dict(self, **kwargs): + """Return a dict which contains only serializable fields.""" + default_dict = super().dict(**kwargs) + + return jsonable_encoder(default_dict) +``` +In the example above we have decided to make a global base model which: +- uses [orjson](https://github.com/ijl/orjson) to serialize data +- drops microseconds to 0 in all date formats +- serializes all datetime fields to standard format with explicit timezone +### 9. Docs +1. Unless your API is public, hide docs by default. Show it explicitly on the selected envs only. +```python +from fastapi import FastAPI +from starlette.config import Config + +config = Config(".env") # parse .env file for env variables + +ENVIRONMENT = config("ENVIRONMENT") # get current env name +SHOW_DOCS_ENVIRONMENT = ("local", "staging") # explicit list of allowed envs + +app_configs = {"title": "My Cool API"} +if ENVIRONMENT not in SHOW_DOCS_ENVIRONMENT: + app_configs["openapi_url"] = None # set url for docs as null + +app = FastAPI(**app_configs) +``` +2. Help FastAPI to generate an easy-to-understand docs + 1. Set `response_model`, `status_code`, `description`, etc. + 2. If models and statuses vary, use `responses` route attribute to add docs for different responses +```python +from fastapi import APIRouter, status + +router = APIRouter() + +@router.post( + "/endpoints", + response_model=DefaultResponseModel, # default response pydantic model + status_code=status.HTTP_201_CREATED, # default status code + description="Description of the well documented endpoint", + tags=["Endpoint Category"], + summary="Summary of the Endpoint", + responses={ + status.HTTP_200_OK: { + "model": OkResponse, # custom pydantic model for 200 response + "description": "Ok Response", + }, + status.HTTP_201_CREATED: { + "model": CreatedResponse, # custom pydantic model for 201 response + "description": "Creates something from user request ", + }, + status.HTTP_202_ACCEPTED: { + "model": AcceptedResponse, # custom pydantic model for 202 response + "description": "Accepts request and handles it later", + }, + }, +) +async def documented_route(): + pass +``` +Will generate docs like this: + + +### 10. Use Pydantic's BaseSettings for configs +Pydantic gives a [powerful tool](https://pydantic-docs.helpmanual.io/usage/settings/) to parse environment variables and process them with its validators. +```python +from pydantic import AnyUrl, BaseSettings, PostgresDsn + +class AppSettings(BaseSettings): + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + env_prefix = "app_" + + DATABASE_URL: PostgresDsn + IS_GOOD_ENV: bool = True + ALLOWED_CORS_ORIGINS: set[AnyUrl] +``` +### 11. SQLAlchemy: Set DB keys naming convention +Explicitly setting the indexes' namings according to your database's convention is preferable over sqlalchemy's. +```python +from sqlalchemy import MetaData + +POSTGRES_INDEXES_NAMING_CONVENTION = { + "ix": "%(column_0_label)s_idx", + "uq": "%(table_name)s_%(column_0_name)s_key", + "ck": "%(table_name)s_%(constraint_name)s_check", + "fk": "%(table_name)s_%(column_0_name)s_fkey", + "pk": "%(table_name)s_pkey", +} +metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION) +``` +### 12. Migrations. Alembic. +1. Migrations must be static and revertable. +If your migrations depend on dynamically generated data, then +make sure the only thing that is dynamic is the data itself, not its structure. +2. Generate migrations with descriptive names & slugs. Slug is required and should explain the changes. +3. Set human-readable file template for new migrations. We use `*date*_*slug*.py` pattern, e.g. `2022-08-24_post_content_idx.py` +``` +# alembic.ini +file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s +``` +### 13. Set DB naming convention +Being consistent with names is important. Some rules we followed: +1. lower_case_snake +2. singular form (e.g. `post`, `post_like`, `user_playlist`) +3. group similar tables with module prefix, e.g. `payment_account`, `payment_bill`, `post`, `post_like` +4. stay consistent across tables, but concrete namings are ok, e.g. + 1. use `profile_id` in all tables, but if some of them need only profiles that are creators, use `creator_id` + 2. use `post_id` for all abstract tables like `post_like`, `post_view`, but use concrete naming in relevant modules like `course_id` in `chapters.course_id` +5. `_at` suffix for datetime +6. `_date` suffix for date + +### 14. Set tests client async from day 0 +Writing integration tests with DB will most likely lead to messed up event loop errors in the future. +Set the async test client immediately, e.g. [async_asgi_testclient](https://github.com/vinissimus/async-asgi-testclient) or [httpx](https://github.com/encode/starlette/issues/652) +```python +import pytest +from async_asgi_testclient import TestClient + +from src.main import app # inited FastAPI app + + +@pytest.fixture +async def client(): + host, port = "127.0.0.1", "5555" + scope = {"client": (host, port)} + + async with TestClient( + app, scope=scope, headers={"X-User-Fingerprint": "Test"} + ) as client: + yield client + + +@pytest.mark.asyncio +async def test_create_post(client: TestClient): + resp = await client.post("/posts") + + assert resp.status_code == 201 +``` +Unless you have sync db connections (excuse me?) or aren't planning to write integration tests. +### 15. BackgroundTasks > asyncio.create_task +BackgroundTasks can [effectively run](https://github.com/encode/starlette/blob/31164e346b9bd1ce17d968e1301c3bb2c23bb418/starlette/background.py#L25) +both blocking and non-blocking I/O operations the same way FastAPI handles blocking routes (`sync` tasks are run in a threadpool, while `async` tasks are awaited later) +- Don't lie to the worker and don't mark blocking I/O operations as `async` +- Don't use it for heavy CPU intensive tasks. +```python +from fastapi import APIRouter, BackgroundTasks +from pydantic import UUID4 + +from src.notifications import service as notifications_service + + +router = APIRouter() + + +@router.post("/users/{user_id}/email") +async def send_user_email(worker: BackgroundTasks, user_id: UUID4): + """Send email to user""" + worker.add_task(notifications_service.send_email, user_id) # send email after responding client + return {"status": "ok"} +``` +### 16. Typing is important +FastAPI, Pydantic, and modern IDEs encourage to take use of type hints. + +**Without Type Hints** + +
+
+**With Type Hints**
+
+
+
+### 17. Save files in chunks.
+Don't hope your clients will send small files.
+```python
+import aiofiles
+from fastapi import UploadFile
+
+DEFAULT_CHUNK_SIZE = 1024 * 1024 * 50 # 50 megabytes
+
+async def save_video(video_file: UploadFile):
+ async with aiofiles.open("/file/path/name.mp4", "wb") as f:
+ while chunk := await video_file.read(DEFAULT_CHUNK_SIZE):
+ await f.write(chunk)
+```
+### 18. Be careful with dynamic pydantic fields
+If you have a pydantic field that can accept a union of types, be sure the validator explicitly knows the difference between those types.
+```python
+from pydantic import BaseModel
+
+
+class Article(BaseModel):
+ text: str | None
+ extra: str | None
+
+
+class Video(BaseModel):
+ video_id: int
+ text: str | None
+ extra: str | None
+
+
+class Post(BaseModel):
+ content: Article | Video
+
+
+post = Post(content={"video_id": 1, "text": "text"})
+print(type(post.content))
+# OUTPUT: Article
+# Article is very inclusive and all fields are optional, allowing any dict to become valid
+```
+**Solutions:**
+1. Validate input has only allowed valid fields and raise error if unknowns are provided
+```python
+from pydantic import BaseModel, Extra
+
+class Article(BaseModel):
+ text: str | None
+ extra: str | None
+
+ class Config:
+ extra = Extra.forbid
+
+
+class Video(BaseModel):
+ video_id: int
+ text: str | None
+ extra: str | None
+
+ class Config:
+ extra = Extra.forbid
+
+
+class Post(BaseModel):
+ content: Article | Video
+```
+2. Use Pydantic's Smart Union (>v1.9) if fields are simple
+
+It's a good solution if the fields are simple like `int` or `bool`,
+but it doesn't work for complex fields like classes.
+
+Without Smart Union
+```python
+from pydantic import BaseModel
+
+
+class Post(BaseModel):
+ field_1: bool | int
+ field_2: int | str
+ content: Article | Video
+
+p = Post(field_1=1, field_2="1", content={"video_id": 1})
+print(p.field_1)
+# OUTPUT: True
+print(type(p.field_2))
+# OUTPUT: int
+print(type(p.content))
+# OUTPUT: Article
+```
+With Smart Union
+```python
+class Post(BaseModel):
+ field_1: bool | int
+ field_2: int | str
+ content: Article | Video
+
+ class Config:
+ smart_union = True
+
+
+p = Post(field_1=1, field_2="1", content={"video_id": 1})
+print(p.field_1)
+# OUTPUT: 1
+print(type(p.field_2))
+# OUTPUT: str
+print(type(p.content))
+# OUTPUT: Article, because smart_union doesn't work for complex fields like classes
+```
+
+3. Fast Workaround
+
+Order field types properly: from the most strict ones to loose ones.
+
+```python
+class Post(BaseModel):
+ content: Video | Article
+```
+
+### 19. SQL-first, Pydantic-second
+- Usually, database handles data processing much faster and cleaner than CPython will ever do.
+- It's preferable to do all the complex joins and simple data manipulations with SQL.
+- It's preferable to aggregate JSONs in DB for responses with nested objects.
+```python
+# src.posts.service
+from typing import Mapping
+
+from pydantic import UUID4
+from sqlalchemy import desc, func, select, text
+from sqlalchemy.sql.functions import coalesce
+
+from src.database import database, posts, profiles, post_review, products
+
+async def get_posts(
+ creator_id: UUID4, *, limit: int = 10, offset: int = 0
+) -> list[Mapping]:
+ select_query = (
+ select(
+ (
+ posts.c.id,
+ posts.c.type,
+ posts.c.slug,
+ posts.c.title,
+ func.json_build_object(
+ text("'id', profiles.id"),
+ text("'first_name', profiles.first_name"),
+ text("'last_name', profiles.last_name"),
+ text("'username', profiles.username"),
+ ).label("creator"),
+ )
+ )
+ .select_from(posts.join(profiles, posts.c.owner_id == profiles.c.id))
+ .where(posts.c.owner_id == creator_id)
+ .limit(limit)
+ .offset(offset)
+ .group_by(
+ posts.c.id,
+ posts.c.type,
+ posts.c.slug,
+ posts.c.title,
+ profiles.c.id,
+ profiles.c.first_name,
+ profiles.c.last_name,
+ profiles.c.username,
+ profiles.c.avatar,
+ )
+ .order_by(
+ desc(coalesce(posts.c.updated_at, posts.c.published_at, posts.c.created_at))
+ )
+ )
+
+ return await database.fetch_all(select_query)
+
+# src.posts.schemas
+import orjson
+from enum import Enum
+
+from pydantic import BaseModel, UUID4, validator
+
+
+class PostType(str, Enum):
+ ARTICLE = "ARTICLE"
+ COURSE = "COURSE"
+
+
+class Creator(BaseModel):
+ id: UUID4
+ first_name: str
+ last_name: str
+ username: str
+
+
+class Post(BaseModel):
+ id: UUID4
+ type: PostType
+ slug: str
+ title: str
+ creator: Creator
+
+ @validator("creator", pre=True) # before default validation
+ def parse_json(cls, creator: str | dict | Creator) -> dict | Creator:
+ if isinstance(creator, str): # i.e. json
+ return orjson.loads(creator)
+
+ return creator
+
+# src.posts.router
+from fastapi import APIRouter, Depends
+
+router = APIRouter()
+
+
+@router.get("/creators/{creator_id}/posts", response_model=list[Post])
+async def get_creator_posts(creator: Mapping = Depends(valid_creator_id)):
+ posts = await service.get_posts(creator["id"])
+
+ return posts
+```
+
+If an aggregated data form DB is a simple JSON, then take a look at Pydantic's `Json` field type,
+which will load raw JSON first.
+```python
+from pydantic import BaseModel, Json
+
+class A(BaseModel):
+ numbers: Json[list[int]]
+ dicts: Json[dict[str, int]]
+
+valid_a = A(numbers="[1, 2, 3]", dicts='{"key": 1000}') # becomes A(numbers=[1,2,3], dicts={"key": 1000})
+invalid_a = A(numbers='["a", "b", "c"]', dicts='{"key": "str instead of int"}') # raises ValueError
+```
+
+### 20. Validate hosts, if users can send publicly available URLs
+For example, we have a specific endpoint which:
+1. accepts media file from the user,
+2. generates unique url for this file,
+3. returns url to user,
+ 1. which they will use in other endpoints like `PUT /profiles/me`, `POST /posts`
+ 2. these endpoints accept files only from whitelisted hosts
+4. uploads file to AWS with this name and matching URL.
+
+If we don't whitelist URL hosts, then bad users will have a chance to upload dangerous links.
+```python
+from pydantic import AnyUrl, BaseModel
+
+ALLOWED_MEDIA_URLS = {"mysite.com", "mysite.org"}
+
+class CompanyMediaUrl(AnyUrl):
+ @classmethod
+ def validate_host(cls, parts: dict) -> tuple[str, str, str, bool]:
+ """Extend pydantic's AnyUrl validation to whitelist URL hosts."""
+ host, tld, host_type, rebuild = super().validate_host(parts)
+ if host not in ALLOWED_MEDIA_URLS:
+ raise ValueError(
+ "Forbidden host url. Upload files only to internal services."
+ )
+
+ return host, tld, host_type, rebuild
+
+
+class Profile(BaseModel):
+ avatar_url: CompanyMediaUrl # only whitelisted urls for avatar
+
+```
+### 21. Raise a ValueError in custom pydantic validators, if schema directly faces the client
+It will return a nice detailed response to users.
+```python
+# src.profiles.schemas
+from pydantic import BaseModel, validator
+
+class ProfileCreate(BaseModel):
+ username: str
+
+ @validator("username")
+ def validate_bad_words(cls, username: str):
+ if username == "me":
+ raise ValueError("bad username, choose another")
+
+ return username
+
+
+# src.profiles.routes
+from fastapi import APIRouter
+
+router = APIRouter()
+
+
+@router.post("/profiles")
+async def get_creator_posts(profile_data: ProfileCreate):
+ pass
+```
+**Response Example:**
+
+
+
+### 22. Don't forget FastAPI converts Response Pydantic Object to Dict then to an instance of ResponseModel then to Dict then to JSON
+```python
+from fastapi import FastAPI
+from pydantic import BaseModel, root_validator
+
+app = FastAPI()
+
+
+class ProfileResponse(BaseModel):
+ @root_validator
+ def debug_usage(cls, data: dict):
+ print("created pydantic model")
+
+ return data
+
+ def dict(self, *args, **kwargs):
+ print("called dict")
+ return super().dict(*args, **kwargs)
+
+
+@app.get("/", response_model=ProfileResponse)
+async def root():
+ return ProfileResponse()
+```
+**Logs Output:**
+```
+[INFO] [2022-08-28 12:00:00.000000] created pydantic model
+[INFO] [2022-08-28 12:00:00.000010] called dict
+[INFO] [2022-08-28 12:00:00.000020] created pydantic model
+[INFO] [2022-08-28 12:00:00.000030] called dict
+```
+### 23. If you must use sync SDK, then run it in a thread pool.
+If you must use a library to interact with external services, and it's not `async`,
+then make the HTTP calls in an external worker thread.
+
+For a simple example, we could use our well-known `run_in_threadpool` from starlette.
+```python
+from fastapi import FastAPI
+from fastapi.concurrency import run_in_threadpool
+from my_sync_library import SyncAPIClient
+
+app = FastAPI()
+
+
+@app.get("/")
+async def call_my_sync_library():
+ my_data = await service.get_my_data()
+
+ client = SyncAPIClient()
+ await run_in_threadpool(client.make_request, data=my_data)
+```
+### 24. Use linters (black, isort, autoflake)
+With linters, you can forget about formatting the code and focus on writing the business logic.
+
+Black is the uncompromising code formatter that eliminates so many small decisions you have to make during development.
+Other linters help you write cleaner code and follow the PEP8.
+
+It's a popular good practice to use pre-commit hooks, but just using the script was ok for us.
+```shell
+#!/bin/sh -e
+set -x
+
+autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place src tests --exclude=__init__.py
+isort src tests --profile black
+black src tests
+```
+### Bonus Section
+Some very kind people shared their own experience and best practices that are definitely worth reading.
+Check them out at [issues](https://github.com/zhanymkanov/fastapi-best-practices/issues) section of the project.
+
+For instance, [lowercase00](https://github.com/zhanymkanov/fastapi-best-practices/issues/4)
+has described in details their best practices working with permissions & auth, class-based services & views,
+task queues, custom response serializers, configuration with dynaconf, etc.
+
+If you have something to share about your experience working with FastAPI, whether it's good or bad,
+you are very welcome to create a new issue. It is our pleasure to read it.
diff --git a/fastapi-best-practices/images/custom_bad_response.png b/fastapi-best-practices/images/custom_bad_response.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec73d8683779ca5166bc7876217d8d4b33093a44
GIT binary patch
literal 165737
zcmb5V1yq#Z*EURdOGu{@(%pk}r$~2q4UG;+H%LoLBM1^ohzy~02uMkHcf-Ut=wcrND6+#ez zel{cD#poAT|48ME2L5!FObHE@6k8X2l{Hc3msy_{rMzkO%=P?=0>grsx?||-`~Teo z^mzqPx0D9?4A>do0(~BwfmXkH<-nUD;sS4jAecR3t=5T6kv5-{r>#$%3|p2gdoi{A zaFRU940o5xUx^F8YH&1X;}+kX%Ny0y$_*2>rzoP+cHd=GNF(Dt @g-)Jt%d($X({eVocS+B)81c zPGt@}U9 $m|wtzKVzJ7W mOGAL0(Tqx;f z=L34K+*lzGyl0Z$Huk)5kP?Ax2p{su*i2o2GbgRkRx=1af?2_qN}=D-wzKDdQ&uux z7CRLyFh8j`J}=er6;R;0u$24cO;a{1T%osZ!JJ7Teva1nZo3S8F#)jaFB-p)D(mgS zRGrK;U|w8NxM^NfKhJn(U~_jnI&uA!E-~}Trr(?bL3iM$(@o*4yNm^)6S~{oXmmqW zcbCws-xcqg+$Xh@q>d1 w2Vdi~+6d4@7IIlbAKJ=> {1Z`lQ`B`ySc)0LSY~4i*KNT-u2#3cei-7n+{*Z=k^w^qGWKAX`CizdaXUd$ z-~pMQK&0Gaul=5~oYCeU<@T1VOWeRyT7^;jHgX1&voe`H#>Lm3`jMC~%aJ?bE&0-i zMwEc9=da33YD>;)@)kzlwEs-iT1neMpB%qVTP?wG6h_$L2WaqTmyK-?hWJLSE_qUL zNx8uwIz>Sl!Iusb g8Ao_T=9-G7 zB#9;Itp|_Z-RSKuLjC4KZAJo|@32|vxM(FrvdsJ!_0QdLq$N<@cO?v$g3e#P$bI%T zorn43Z^5^XV^VSws9*f==QwijlKseLUw`*d`a3Z#Qfx*+5~hd}x`+C&zRhIuenb?9 z9a+K=+S`8NS`Day+=@fO%$#
z^)T|fU5WXY?f17&hHRyUopGbJ7?Lughmm0;P>l7KrYE}PKqpF1C6A9y!`yau%ipS< zq|C%G)3gA$I0`<#;a~h{-#O8tKmVB<3Ll-PSA{S6vq8?XYo#3x0dxNJ32%>!uFe(2 zs4evl4L0ie;%-hF6Hku+L15gVnxSR}==zz#=g$wNGrc(TKVd$Iyz`$D8qfO@%X?Hr z@zf4}b%|Qx3#iTA8z2p_no=H&pVhnib+;DY-nHU3MQT-PcBH~eh_g cJOIwxbzKuOx=pXIW}nK7%Vyk5A$=AG|ocI34`p@H9ayD34hbtl*+>u|HcY z >Z6yI3V!M=Bz(lfXk=-DH5lwPZSRJ zdo(|j!rNhJgZ-j$YlEMf5!hXBa9km3*oAZ!=JL*z(O|kd(e=wULt``(2Cums^=r5L z&)#24R}Am{(GZjczy|iblXxfw%7>f5vei9r>zqyPY`wjp6_uV)!lN6qtHODD7UpIE z6S;cLLsDi21cpZbD?I~UFOl((-pA#vJr^{@NRnpN3@(5e_p9nYF427ILnKM%_41K5 zww6+#QDA_*3OQC?ckc8^T~@2lw#w!pncZN|wPO?(7_=<0c?hyv|H1-z_Nnul8|!tI zZkg?!-lL89v5w4r@k{jf#8nd| )2f$#0L7Ku_o^ z05;W~T47~P^Fz0U(4Yr?hq3e^>QR)gTt(w^$mq8~%ZkG&ngJPf7jL_w=a-v!^c`E^ z>o9}*Q!p?w@B}HtZe$prTr*}|=aA;RlOXr3RsQSd#0Y_c2-=***NF>(g#<5yh2ty- znlo(5ZafGiJ)G>znFMJs5n2@M%AW*YCIhYl%oC>Pn$5uYQvEFhKba{^yA6T&9)5`R zb4 2)Y49uJp$g_JJe34%yaNb#iN3Qyq|#Pe1Vw{dTg96tQB9xtZlUIU&2RA1 zf0ye*lxRi!+(8VcG>#}A8(6EPWAX+9lQ$pA$LueM*Z=q+OAOwmhCK5GgU1f+?@9@S zbhf(8I6PK_{#6_Q&oY>PWT7%E9ykU>i Dd3XV%?AK4#HU6xoW;8 z{6&oC`E&t}^%NB$S9`&6W3-uNRpgI7S6yX`6~{~1S9KF0W0FZ?v?%*_o$*mu-$mWf zEb_!sdb_^T4BKw=4B0w~*s*1~*9ZgD1C`8e3Osv4A|A;`lqC~bu(PhAJ)YeCAMp48 zt|}5uuoCjSU_Hz#nDiKfFF2lyTt-jfmNGjg3C^$^^@WQmxbo7<2PL(0NXt@|ZxT)~ zYn{GllK&%8%f0efWH^Z)j1oZz U^6 z*k-`je^Sv#G6Mqu*Tap60^K7}S%|@Bj<_&5<-cWPRo5@bd!;*KhAzZCqk;QXKQ1_Y ze@{LMppTxAYlnT8th5-abi`un{!@HdDVUF_hn82tw$=`MNb>(ly*WSE6a7=^vx8*v z?oS;yx6o4|Lv3Vao#!a4Qfrs23zJ|U_)z9|7P*5TV-NLX!yTq)i=HJt=`quUF3TD8 zQm=!Uk%H~{a6P$ByIOym>t&0vI5$!z(^3Yg<2osxJ5HzQ98)GzBg)Y|A!*u7lwweJ zVg4-Ac*cppDfk({^VIT)@O_JZ#s8IWDUK@@dH|Say!+WLs0QCibvOX?1OG&FH94DV}E;Fn0pJ^PxOD{(1Y<>XAU0vs`So$>WeaL zqh_ALqy5&MNVe7cqziM<{j0(IegzG38L>`@zC1HTxzA!Y3?*lUm1l~y6l4xM)G%tW z&9Rr-{Iy=@{R7A3BV0Mrqa9Z3s*TELpecI#nBB;`g6mCPXtOblR1 Q+PM zc^foBp6gp6v@!Ve2XE~})C=f(b(za=b4c@RXi(sO;`K%K_=Mzcsf}$#zUt0-Qm9tx zAr@4wYV!`fbAeEAAF{i*Ji8~!s_qg)Am8CB6$}{x#S`KH`k>6p4RwWTql!^ L*BuO(g7a-J z{_kAv=eNRc)I)_lQWU;QDi6@__{$`~&G;hacneYl0a$8liS6wyhfNEQ!JVMZ&X-iJ zkclm9XmY-w{MCXFVcM;PA#5!BDj|XXo=AM4i!Tf=JPjCE;4y^ddu^#qd}9sJPK@dd zk_d%w@fb3U)b8f3z+usfsu>&^iI@IJA 1Nobd^0 zMFd$H$v-Y;5Q|AgE9`L`E_6h@55idHf}^}1`mqEq;M^5(xH!HGMuJt?yq+t}t#q{V zmJ;2(q@G@6x^2Wt3M_FEXR(e*y8V#_(OIO9dy)l^c|*e4I=FdUHwE};0?azzhA0od zHNm{fL==g6e1w^N^G-C?`AX@`t0LRiZv1)i(cN #|_-1Kgx@5*L#1`HImb$)i81=sR=1;IuTSA6I7B~L}jSZ=enaVKB$4H(` ;N)vnL>y=CayhnY-hNv=v>9uV>p@&jX+%p@?U**0Bb%2xbQxt4w zqafRdn8_`0)It8ljSD5o0~wgfpAtmu^oSU*=Z1|Rl1>Q{Mz&%^xJ+T8+8I_k-NBjG zN(5+9g#kmI0p|ZHL3F1yWIv*jCR)AWiMkLuuXnIp$Yy7-Cu!<8pSoJfnsE3XxCPm) z08KQG3+ZW!U5YwQG<5uq%zVq+50`}2nS4b~3be^U7VGqaM){U9!d@I8Y2>~IfPeDU z5%x*;7rs2?wSGO&3Q@m9l;x%9n!((kK_}bnL~ap8U0;cyrKFkW6(`O4$6p{CKCrCP zFuRtw-NE ZAg{rozS&C$kPRQqgllUkrlNG{rRE zeg2@tF<*WM8s1?)`<}3!4%e#lExy=UWK4$g(I*9-D|!y=7i|r0U@EC@F7jF@$-wC2 z&$Ki_ U0NdU~lrxrB#J_ ?I!kNktBNM>ZnI4Du=i zQzipkCZ(R&+o>IMCQI>2^P(_KbnU_cYj9uc?iW`Kpgobbm_XVdS0*`<*wi*+qZ@&W zhG=4+6|hFI_!dasXpkI72=bw3^&wpX25Z7i=ZkZPBW*O#sCYkR(fHya?*0@B0S8?g ztJ`@*$(HJ*dZk78(*++Tu}Y`e+eF@II2tw@>uTp$nfiT=Ji!mu=U=aPu _R@{ `Yh0H=!kQ2CEJHIs)?>@0b#;zxbMC<@Vn$7eJxL2MN;dkp;!F8V@dhu z7?0;Lbr)X++FG;YWG(S@c474J=}-$q2TvZ-3elGp8vGpu^>L%&Q_>ONQu_Z`I_tQm z-v9lpAfS{;NsFj-N#`s;T0{i_Ndb}WW`H0nFhn{hl1g_odUQ*}7~LC;*kIfF?)~}w z{ymTVakj_Kec!L^dS2Jz@r;;Qo<;+IWgOSFVp6MVZ2M+4yCjmh_%_w<0+WVwgZpM$ z+<_iXe>erz4`VFWt4bX}#Y?yGagC@a{vH$>Eoz^v;vg=@aJ|WU!$3wj=&+5_t2;{+ z$<1S;+OnP89_RW;?d7k#xN1RcWppTd&QcVGW@d$eJyglc6+tq*6*MmUBRll>8F7>_ z(*a-vyqO}HrbfC|+;AV1 7)&H~ELYPMpaQ2@}662m$Wpf@I9$maz(|YCDgtx|MldZaunPmrP_8 zHH&$lMj8;mW#w6K+%#F}jEr8-rB_YDVOR$$;oQ8GTrIE=KrNa@bL7HoUp`Zm*|)UF zsSdqmLHZad1&7%jtTYC_Tl+=!Qp22RTJ_=2P_N6eMaWUCNRQsczw>!ArEHWiC1*oi z;&P)`4!-{5`0e(=_}dSv^Izp_nTX51yTs<|+b)Mgk6d7Iu_3>D=Wf7prc`@6wfe?s zCC-#Ukz%_!`KapFg++s9j&xgr&CBU~GuA?vk?ulD%RkCrY-On*={?YXp|=0Kbk+7* zSrD+Mf84`w(f>1lFppy1bF72%1zQZe2?f)Oekkpp+%LKpLQ_T_{D;3ptGL~PkS((R z48SV8f9wh?zX*8`eIm Y*{ zz{m3#O#IK29CT#{gi}g??gpX+FAre#`_%~ZD$hT!G9p2MMk16Di$`}|d)MZawqL97 zJ?9Dw{s0Xw{#@p}ED|K^dRPzYlS2&0Uk8aM);i8;k>qsEq`%DrIZ;zT-VYp9F%WGo z6_SDv>)pFs tm3JtOY!!agE@;}TB2!@^I{ 5ZH}BCq5E%I z(~(~r-*Kf^JI&pE7ya%!y-tXZIergRlg+3nZ=&c$G4iAEwqFCekA;1h_ukB|IM@15 zUY8pmt+}@GVVVNKy@A{jMm=SddEYA9bD<~8W!;yh9loQzu%G!pKaZIQ9-vPj@(@!* z-;3={ml sP)uZ)1eK=lI z2< ZDc<3Fulb5+eRqLXRYTR{KjzXDV+x`MgiUpoUY z-sw !(|aFelabVhm=)Ux#SpBU9}Pg zc= BSL>hL*k`_>b`Js4_DH6snw1O-y({`M3Mz} zom#)=o*sAF`%jotC&Kd1aMwHeTh5m+UvT?>EC@Ow+H-WoVL`N$1sqrDy-L>3XXtN4 z)F5A w05tAJo8Ly`iDsfVuc1qIW-{xjO}9P3NZEiB$*Nq1Cxklq)s!6kG~10`n% zUMuNNpa n3F8&Hz>9}-xr~G9A&8A{wUcZ&pp_s zs6SBzeMU9Mob`;$ QwgmR;bv-fNn4qz)hE3$CG6%Riy>6ix`NiuAh#sj^(wV@~! zmNJsO)AK^Aa)sp67dG@;cn j)`6F@W~rHPD!XNUx}nX i>VT9Pde zV*tfWji5O--GsB5nfG&~9kZxQNZ6P->+WC`tMJI98-Fh*RjQ&Dvq^B`c#dpa6ti8A zF6$-LPyD|wfO8Fk9uaj~pXP*O4a-{}6?7X3JlheNyAx)^MN?|((^Fsp5={S;6;g%A z`~zF%Wkg~qHtn5GcLQGlrcKt;4T`(J2CYd|Y_f3(n}!uq_eUTqNC2}5I8`xY#otur z>3~1|E+~gL9ww|Ls&vqnbe?ZHJ{ i)CSx}=p_omuj)%BS`F z{>VlcpJ nfJcfGHTkTYG(Q0 zZLAaU({?al`?BpgS5fYZTyVnIR3_rw7njy~s{`k{>5hODF^h*Y^?OkUL|jJ1sG>gg zhW4+Z?Sip7JZ)~dFFjkK6qB*N6*{dKCFe#8{GQ0$kn?VXmhhr6sh(2-Wrt%f@1!}z zrWK>t`p~g*@5`kGX)y1$g9Zly8&s4HWNl*NA|=ZbZ|C0aXNUlHFKY9j@MT)J$L}%W zsl;``I2hnnBFPhHt9XC^ku3>czO;|H1&UR5i6rs_%-r#hhbVM9*k=m367~@De^?Rx z)-Rw}KWt6puNRm$4Kir({M9@#IghYWH9G23rr9|=-ywtJPZ%Xkx9z=}yz6KhJdDnk zBm#4t1$Y=~T)yxGAZ87e6xCx>nG|)NA97Au&?)^^50VjOUjQ~(Ewl^i 6E#w!=g^Kdz)iH#R~iQ@!VYDec@#=8a$)I?yjV zs>*T7?=?CSOWRm}pL?YOK6O%rp6ZZqz=T^UcZR~DQOBtK0!c|)NunKlnaSGzUD)Vv zk;a$ze&Y&1RWYgPof5ZQsRjK_ >NY*41pZ_bcz z-qgSE%-gX0!v2>B=NoX;R%OkxMV2D?=VmGUw$xsXlDfu4ZY2-b%Qg!&c_FV%@s{oV z2w6&mI}}gIH|jF#pt%yW>q-mkDQwD%#hxZCJICT|{ea;~m<#cXLA3yQ-p%MN8ktIo z>*7oL*ivm?Cr+F=QpUs;_PfZR`yKj%`e4M<>=C`MJMRN(;;TngbuqcAki=b=#Ui;0 zp2A+98ydh@1dkYEG)oxOLdbDrdra=J9v?N}?7Q5)Dye19`XgH!g7|uc0c%K6E$K&x zNxk8VR4~eJFZjGx5pnN}%6XAc;Y#3iQ6o=VrKqUi D$0wzlFV#HZf7fQ6-|J@AsrJ!+ znElcQ@%7O$V`dAu1>Y3(C|ZiuSWQfpL76^~MS5DuA6frtsY)r+J=2E!_{U$ZB$3GB z9=&cad~oET!Lt8ea7><5oI68GFb$VKHQEk4GzuIwNEc6TOZ)g-&7<&P%Je#RZ*mG9 zG*k@&CyQYHc_&`pJVu36J8E8z+=;Ze S-er@OjZl#s}B{5KRbc3DW=G?Ch{-34&sLra@$1jC8J*3~3^~Ihnt&9rzl0O!F zr`jxg+INZV7(BptE>K=2MvI$wO>$x*o%-MDcRPe&DL#;50O)Tza3C2?qT<)Z{xcT} z3cttV-~Y!rxutce*)qmG=j%Uy;Q#N5hnr+zgNWD?71a}G-QRy}5|i>g;-2jb6nnq? zXyQNE7r>=TYYqn=<-*t|vy&=G#SPMQ;iZP>Pal|u2 =UJ%^ zZr-nouDRMm(u>r!YyxE0?yN3M`s4Wvsyu~g3#H?r$~u$&yoH5G)^31vyfl^p^8z@P zxR?ob9I9=3Pn`gA$J7fLOi9x7ZHn~&+1KUp*7@|?J9s-sX5+%exnh%B t%;3&d?u`8MGO(+)R0a;5RdZi<5Ne7z42XOK9It1S&xQt1S~_E z>>^FnF02QHkL>cvGpJMrCMA=W!>!OC^5mSyAD?xaxQD#xwD{&-Cz@^+B{L;lvmZiv zC|tiZZm48Dfd#gNUHg75pvKQvSrz^hy!8=70;|XQ!g>A)dTzgrg!}$B8zo!uQEn@~ ze` Er*0bUsbw9O!!JDYN(D0HQHtK0TKEafdr`Q zpmY6VDY~&N6;gGO(7s;)PW2pX4_s#+d$a@PqW9(FxtT`M)m-mFr%08%?zH=4*65cK z1i8BHkELp|3tlPZtErB{>to*s*8KAo`wGCVV{zGcB;!s11@({3UJa5twVtEJ4@%wU zA3MZ|Xk4tRUY9#z7qGDWLW9U9$NEhq=2QtQDo*g$L7gr8M#0lN*t@$uudbZL{QKgx zz)k?qB4wU<@(Ve4pV5=rS%SptU?2Y6o+#>1@(@(KI$mB>A!{kyCrY3gTyQ-KWFx}$ z8W^Q?{ysoA3STCWHWO3`J49)3yg7Ix w!X!MM89pFxh8^5`M{ZgK-67ZGa`Pb--0vc%lo;U*I0e#24X~D2>RBjVB!aM z_LbN2cph&C8_m0t<%Cs9{X~@tJ1(v++Wo6snxy0ku&M=i-?GjJ=U+bE236>M*^VUw zSh!T4XZ>6C{gvB_uqI*8U%;z7{#+p_=O+I*A;ihp-<{)H5f<0>+@cV^!s;<+BV8ou zXQArI+eD60tdS-++qta2<$G235#p7O@ZoGhJ-%gybptjjxm3ei;KB$5Bu+jUHW*>= zGC-0Tp1!uw`c^D7_if7iDPtri(*Lyy!ErlvEf_cO0;&6i#OEsb*v=&LW^kqWm<#aD zzM!9cUmlh_K%?;PDg31E#^-$L!cUZV&-)4Va;L4@%iT4m;Y#ZjWVYxfIezp)&9)fA z=4SPPd<#`!6O?XC1%_W!59TUseZpchAlGFDEB@wCMJVB>YQC|(;?K#qX+X^SCRq7t zJNLIVK|fRC4L4lTugzC8=<_#vnv?-%VHN=}4|v@n<}1hv6^=iLG(J1)QU64<$Pvb+ zU1DFjpY>sEj3a-Om+}ER7azPe4Aw?)P~?!i^FH!8xk9qL2wu8pv8^sMeycEj*xvfO zVVs)Sy3N~l+xAjr{0if21-14gbPaVftuh*|(xkcMd*^ih#CjF#B@;B|>~Twx-B`AB zi_G5hWY4p(v%JRfdfG9%)#S+6Se&%Mi(e9p0?D_bhKHE0!h{*|nlO$a-K#;tlI{Nc z (Tsqu@qo@Baw+(0ivQDpvZUngN5tMGW83?t8Hk zAnt{(AUF` NzN?s?@Rg%J?pi&y2B-EjaQzb4W&AYLADYT_xIvQXm4jE<3ar zK}W^DPCgEPEH{)VFBID4aypw7{P1kqlALRgTV*A+aG(Cz$M9J8T5+nhEx9bZm^0g$ z8(vzeyt`SpA7Ey3Q}w*3fgoEZs==_{xOYhO=*yA0jV(DXE^mw1JGl6m JQ?N|P9yD;GTe(&F5GOmkzL z`?#rJ7Ch$xkF)cmn6#siT{6`}EDgP@xV)49$b^TV_^if^L~dSa>)Rskv>8;yD{U*& z6V`^Uh6qP3llg`-(|^TgSzIc7{15DI5N66eb80@m2gyp&6W|WzIv*~@8}Uya`db=i z;(2WUn)x5p)_+(Lo%IrJ=TRYw?Z1sRXnBfty*DD34bknxE_*ehxRhQZF-v9b=|0Ez zcTkUFJZe936*Tc-dJpU?E#SbS{9YAE?k%=OdPQNAcGo2ykF({K4V_q M-3B996IJvnN_T=7;edT^7lP}0pUIP^Xoz`J+*TNq?U%BQ$!geP zEv%|0z&=;2d~`!7W9 V zCAzi|^pF7YJZhl5+38a-@odZ5|9PBP{dSGV>K|I!1I3-Gwe{>~_@L)#ONqFT0Oy60 z#$t?bXY(=Wqo^nVgG#ab?OMe?4uBG~HWCdXDQeNInq9@En22v0Hm;w|jkiqcAmUZL zx9<7#%MIOXFYaOxHKmi%_)$fb#W`aZn_KlG?Etiy+BF_Or8)3Bh+UN>qOU4n_#JPX zTHStlY|lLcm^9}Y4@jC$(mgJ0e_~Ac1GtnrYP2_hn!&$9=U6u0h~D;6++?AGG0R@X zb5Bm;R#^!+ pXxQa21m1U zmBSdog?sFMAwf#C4ipPKsi#db0y<(@B?G`K%iAXss?$^}LUo`C(B63Hz--9Gy~3(O zGIws|LTf$?IbhZ&tGMZKF~i__SU|T|E|kn3(-jly |5kj`n2^? zxsXWrQ~6^H9barWTg8X%SNW@0@qml9q6uupbjr!{FZcKPbE5eGyA1v7HN^?wZ>^8n z&tUtQspXq%ml3&%tjA!m%e6Q4vYZ?3mU2t;%>Hq2S+u*h*!#X2Q}?$EHgu^^gb8H= z%fT3e#M1$rS)U17cvX7}r=$@)Q=FRQe!Gc4*u#5r%QH4H(s-|yHga=6L$n~vJs`to z`Mv` W7T1FOeVp8G%Cm E`=osGI$skFKo e0x*x>%afoixs7n>>MU zu!DYMVt5aQe}$rzHv6Of8dN=7C&@y)v6D^47#3Lz!32 zpGEG@PD+kaf@gn7%0IZ0MMc$&GEjS{)3(4qK(zw%mU6oIx_0Xwq?u6P#8Um^>DQBT z@7}(3U(5;Xh6Q
Hg?_45^zW_MGGlBXkQn=c%0^e`a zLXiREja6reWF#w3=SndX5V(DpIq&TDpDWb=1Nzx!&EAqxPh!<01oetD#?x;7*PV&N zibF^)u;F)pNXB2UVVUX4R~fMJ)Yvb){eIfp78*ldujl=2X*BBHlRclDjQ-j87I(43 zM_nk;2D)aO#s=DETmg4W#5k#5R&{QbO{utqRyW-suCYyVl^r;H|GIZb^S*VSJM|`v zUC&CY8{_<3BbE)Of`d}HIme`KS)NM}*QCz^Xe$S$S1?tAxGvqX-!^(dW;6NBb^@7J zozw>wp`J6&O~psk-Tlw3LKn90OEGW<-|9pv;=)qrwT9kyyg(4>>GW6XNqVX>d$ixq zcyZ}eCzTmHAdokTCo1bj)0uSxx4bB(l(Bd7Nbt10H6*AcSJ;<_9=&$HB@*h`ki#Y1 zLyG(OTJqJl$_j!h7o}gDg9%&&q8WH8f2-Cyi=|F1g}D~BsN)-MU(0e_2kNvp@q4gR zrvk}0NgRWYM D$Q7yxaB;*gzZS<8e5_eB{bOwep}EzVLK3({*c?YP%#(b<&ou zD1Z-Ned@%2?y>UvOqx*Z;=;EX0ixnq Qt!j?$Xe*fesV}Epr#a^3WZ;)+??xeZ;}=l zyAiZZw(}iG)mSs`9QZ+uTK@Ka4mhLl0UEcUyjkrDCBqd1nUWvE^!?_qDmuN?FWfU3 zXk?w5c;3mwTJW2Zn6{GS68vJ24I8j}4I&wh-HNw=gG{X!+2aTlpDr2=(wkIZHut@l znO}RL9jCK5c_2GHnvRS8w8~KlDX;7|*U+(vJKzZ)86hp&6Un~ umbq8(-Xbw1SFF9p?EWD0a&KL3`iWz$mS7XHLKK4{3J;;Yp?p-IL)W7DE*KiR^q zH)_;^oJ-A%3%*#z3f`gMz2b)TcY9{WAQiyt^3)CQ8hEkB{$A;o0BiHnge3He6R?G~ zAls@RdGMbaLr+F3rkZ1HzP$$@(g2G88Mx%!{MphmE~7YrL!R%1>B1|bC1F61cSBoC zd>gNZ?aft~EAm&3bEG$n>OM5Em$ozUY}YMwI~5}!w+436c@vASlLBNK{$`1oSCJ17 z^j!EI{%}H-%k!e2u8iW&C>vQOqh)^mA-HsI9}8w0SRUJ7^>u06Z0dd-xPDZ5r&Wwh zqAydQ*m5xd^mEC7g0#zD_q`{Z@SQZEb-xhwp|zhMHnR`Fqo_AvZyZCPl}z%3Z%5bD zH(hNuSg> M>L7@d+shr5EXBVighT*T1&=+aq(wQ{MlVjU zEz)pGaB@+*{w@ 0C&Yinf9}!YBE+**GKmnslY7w9*}P*;<{2_^-sE~sajIfH48=k=s7BDh z|9e2ZMnttAM|FXHsbsxGGJ(0PeXZ9^xa)p^uiRR}d;xBD0bP^oHW<~h0hV0^8!6N` zC`^@40|^hJ*-F*8a~4^DEkr=!r0lYy7yqUd^J?Jt+t{1`0OwA-fQX5s3l@)MT rbzGyA9ctfBmj0 z!16}6;blD1meh!(E9LX&^G~0<+Om_iS2&*MlSi`7JAg`}XbYHnRe-hW0SUTuIz&MJ zSDJ&}-ne_cM;22ja%+-0=bXUZuhQBYx!h~ 824}x*Q?2yAUPuGVdtTg+%ekf{`6(d$q=k1s zuZ<0dK~h3rpj^DH$EhMZx-Lk(23KYu^twDn@gMF#-Ra16>raoe7Q@#^9gT~X&~@?Y z`h`_C qRkAyd}%U;rv1Q|~{yN4wMq@>(x_mD@`+L6OUw zo3WG52iELfDTmIoKfxBk3Z{y+u&cQ7hgVXSuRGfNMkRb#SSb+s(Yg5>LIqnmt?w&| z18ICS`{TLMGrK|fl<5G}n$2)G2Rf=WdYr4we&ZbE#2UZ6#7y$2`DB6Q5>6dwcFsXG ze*eZlw!KruUhfC^;*`x&+@51EIt8#&cH}Lwx@UeLrZrIDaJsboyIo_#aZPEc(9dT* zn}KpjfvPNKz~Lg7@pQygfS#R*?$WmvgcL<1pa=G3=l1^l18pP_CS7)Ox&QdS+_kmn zs52A^f>F3ALRj&mmrh%7>FvXuoE$TZC*8_TI1WXRiQ_1~mdt^XPhRiHAy +``FLEujz^Yj`_go+7F57Zw`#fJ#@=n)}FgE45k*z8_ba>ty7+ zx=BOIr$d5wFFA%SSq8Cx9?^0L{3M=7IN93YM^rKwR00ydZJn-Tc@VSrFfo&MA7h!8 zSiHS=BVmn{VS&X1iAx>A100cPSU 0CBO%njAt<4FMlO2;#@V7|4YBi+f~uTN?j zW#m5#8JhhVxYIoO;<*8@K^8?GQnivs;#GoBI}>fr+mk&hwtP~<3CRn$sKRk i7DDOM#wwJe z&L)?ZsJlRC&Iu2V?E#E<^vf$8aQDy6)T13Ws3Z@yrQ~VyDNh>^_mWS^reF~P8cfU= z>1o^yf~L!yB@849+sK_B9&^d+*);6#YFA>;nVsG|<`)wEMZmK#0FmPs?8N;!&vjid zA(iv}Xk$$43uFS(g=^LjLLut-@@0v?&Bf(KvBELSNDg>YTp%ui;vSwvFSnnZ@qJ*B zeZ7_#wE7JhN_G~%Jo`R7D3M6)225S?rWfa2;dbsuJ{bdLk=KG0f0|7^zN1nh1vu2| zP1PMMz=)UIpWn@D28I4Eni??%XbV%S$z^+hast^U<4TcCXJM}V!JivV+1Xd9FNxjW zPtZv#vbkXB*f64^mlHjZ80i#f{WKg*7ziVhV$w^>Vh82r*Z5Kiug hJi}+GD$hDXQXdi@L$Ch90 zjvNc$roN?+QidhpVHyc-3HrUYSHAV)a{5Qg6M&}KJ~Z*lVdE5BsQR6*qNl)h3!D$f z7&wHuRo!nv*mlyzGC92~AkcwUwv7I}%)j(se{m)do=Biy2PeW3- ll> zqxk fJ<0DbwL3-TOtXqopK)u`;4D9-_gifRPg$2pUF*(bxd5d~F~-|Ely zwiOFnx1xYd#d*e4E`{soB!u1)Bb4c0rt@{e?_3% ewp1rY _pUqRqxbDC=*nmKXlO!o^L5M`fCM$a`Ew1<={Q6dq-T}QG2NMcWvdP} z!Vk~*v?4y-_?1sDY(sA(0tSR#_I)*|=LqV9eIuPKo2&SMcO)2$dFv1BeW0xIuZj;J zd@5wU7)&eIRqR;)^idx_*naq;v!I0Wp7|P!d=0!k=K-GLwBq?n%7dH0gs|9;WKoXY zj7?YIcKfFy*iuc}|kZUU}$VBv%E$b>uVi1%h8p;B{`-i0urS-zf-l!AD9QBKk5_NH|{ zU8be-4H@=h05>rX0o)j(V2>U~yb!j%EAAzvBt?n6I4%*Xu>c>B^LX&hm*LrD??7 3SHh~7l!DN9Qf6&NI z5~-lhucWV8T9Kxov=ovMfGF>Pb@4xnrQrk~NBozn`gqqF*hm43fdwSKr{H_O;vQ9% z{dv25L=c#}1veJDCj`>V%NyGYrosREhF%NSc|mv-r;Dz9@vph$tDMPIZH1toYX;Xc z(b=TeNU*99bm8$zYi=iWfisPCm~sBzy_rh!n$wffr8)&6=T1pQSm3stQSysmQs&07 z{>te1p9+S3mGit@rzdliDWj@5ttOvc`<8F^kXP@o<)()DPvFln$}!SN{95KBZTd2& zlCy;mPVFWX<#XL9OzvW^1dLe51GMU;^GK)DSz>U-+}wfhS-cjrFq072iB^ykdP}S$ zee EXKK7itJ@c(Stt9(ho-+hw(M%vbKam}$n`}-b) z7>+A+&0Et$xPQ+ja4ev~O4UY(T)irf9--I pS~+zl_TGq-*zf&`r7b&(!luc!SGy=SRfnT3r=UnvIFfBbN*Ye1Oz*E zk<0A`ZcbpFM|0o$AHI{+I{X1tp2&b4Ax4a18Vglb*=x)x>=X1;1h*x42}xH%<8vuj zV@_IYUCgPZ1_0LV%jQKDa>qo5{ F1U@1n%z`B&N%rw+`kz zbM=_PCu}P?_n}eyc*|?<%t!B)oL>No{pO=7aFu85s;I!<_$Vt@f5@T5x8udepzT{a znlrcW>8nVwIc9&;ems7YD%Np#Mr*51?!LB=d=kMxe$# ;(e+Z_^ur* zI^f-R{N?B6y;JdfY+HicBaYg+sSB)K%0SOkQFu# zJtvX;=I8UP#qd+o)^mLv#84_v|CJsTGs1WWg_Sd7kR)9*%p%gr!QK)#cvV+9NKx`H zl&ifvamQS*b2_=H3?l0!fgN`Tp{c&Knh+h~mEQ1t-bXJ*m>Jgg(?^ELm8G<(9SYEW zR61wRxQ2d$KKk^MiYmaW7ZUT2KQN5*6b4R^)`_3SFa;KkJ4ac!?`>UvifH>&u$~W? zl_ZHiU=~_kw3~X;rL3pV?>P(EmKXbWx}FP$lv$8SWv>`2@cwfbCVRs R@evF$ta;2^nta6Yn+p?lXY(RBStUPigo z-d0ky6MT}BcD$K^%4riRaUJgccpfXMX;tw~T2c-U93WG{gi~KFAfgf=2HI$a&~sfU zDd8hW=W9X8LTC_!_i^M)5L!Jd7GnCbf;i#lI>6xB2{+q}g0^qEx(#6{+m=8?;+IAD z%NL^djzbF8F1k53s;qqZm4rnB*FLmqO|H5WvdUxQ6Pw@O5|)C89fCH`gal7Xa< RbmCQO#EPZOWny5A{Py_iMxm)6^?vxe2U zV`85%sm2X7F`h8Y{B@)-GoGwR3?=O;ohSU8_0K8~T;OV5FUkcMX0)Edka{1U-fv%$ zT|0xJL#(bsHd@W?fiMYY2eG2Eh37!dV^H$Jb!?C^`>CA6I?Kkvk2f;v3w4bvY9S z67`B@E7eB-TI6kP90Z-YQgKx3PsB>>^Zv@fNb)CgAQ2PbuP(hW?wAoD9fdKx;?AgY zz5#&F#u6Ug*c-Z$GC8MYbsv}Msjojj>T3#fX^rV^^3QJO-U%MNwSOD@e0W_UDE-IK zyzDeAPtC=t|220;-`bpY@Yjj<8fkz*mGpNmmj0tH^Yk)`R8YfO(VH2N4e_V;(owq@ zEeHH#vdiW)oVgTjF!j`<(pW#ppej}L$Z?Uo? Fo>A?mYYa5})=T-(`BQ9@EFlVvPtdpjBjeW}@0@xiy&h=X_f|G@fW};sh=by4O3v z*mjK$6j=*O{ WJ$yn!0Y&S8=EO%iFH}HN;1cz)9%g03?1ynsZqLzc6#tDCE~`b;7H=+kcf= zIWqht=OEgj?ARD2A9B7a7z(X&xY$xRSbUHb_HTaERawm0hL~$Jd3D5@j&<_4IV8wD zw6@plt#-1AAfi( s_sr*>*rl7KuPN8n7RHq)9scD+aTk{JJ zWm2~oDXMtFb>n;ore}aX8=3wkiUeHSh;J2r8yNK9U&vLt4qMO4#G=}H*DF6k!IGw< zT?I~BYcR3Q@dDA^IVJSKCa*~7(Q=Jsj4yfJjuj-;GuHp=v4*d0+<)Rx;i7Zo%SYK= zTz~&HHN>-yx~Y=VIUo90^WF9V{kB3(Ssx>bE0oG#^A WnUJ(yh@j3sof8rb2>x&oJ7wZF}ZA zlwi@e Ar?Uw z{8yD3 z0J4D6Nw~$oI9K_1y@VIOMpYA1^f|2P2C6XlOrFy|5>@~w;pfasDDZ>Qnu+fYPgZb^ zTNY!uC9_8$pGEoFN(oi%2K1`oq|+S<_PF7#v$?g3f(rz8(`d5rE%AP~?N#{`@jN(p zu$J~A9P%VsdApN3hlrl6z;Vb)2gx1Oklvk=ieL<)(<%vjaO7BW)F@3cxCKCTG2DUc zH}-o~>vok|h0#ry=1P`pZTS;g#U(}O;#;XwurUi)d314*of=oQ3 Uh6r4a;FamfrozHpRlI0$(iIDY!$WiLZUuo|s0EHs5I|4BZciLiXdY?PGn~sB! zGdpqUQ =Zqs3HUr^dRlFU00*1S;RXbn)fIzmtkYOG@METV*#>w1S)=-B4^%l@52*gCD zK%+HSS+-tYHE`WoLWdUc#$J~&=z57QZ-F7HpSA0Zy~=@6m`Bt#+yOd2@6b;fOv3I{ zk83A`HbH16114hz4-s$`Rayx#L-LrixynXRDKIMMWtj{5%JzfVu3gy&IiVy*fPU$< zsup@6K-n{4j+0#MskF&cA*AH9gZ02gc2&0@&EX7du*)6P#eDqJyzQMF0EZ0SHglTV zj6v1QMOA0*fM+-4f6{Pd3rZg`X0kgG>(ty64OXm!FiWoEYV7K!hW_$BFsyD9_O~$d zzHX)v;9Pk+Gb!daWZI~%fC1TG)^)!+yW7Ds^eZcDcy@qB1zLf*gt`X|OKN;G4GG@x z!hl9^gZ74Ua7r6?7jZ@hG~i&T-#%AHv^HZbZm1E_FY=eHT3B(pxo|XXh|%$ui-;|V zS$po=_sD@)XClDa^usr1^Yf5@*Q? fsduOL_Zy<<8S9 zNjAi!Q)=`H9fg9=d_cK0!--K8czvAG8X7*j!+Ejf)A{Pm&$w@fcOD-*+P)RCmF4ab z7{t;}Sq=jjG*HlK;wr1+IN%x*%=+ ja=UX<#LFhHm@tR^OmhvaMf^BC! z=u|ak>WD{_gP6FkzeBc$IIAbne~P~4>5bzLlV>A9BZKVt2BC+EuvAH&D3{+J+f&i+ zcz=KX!MwIKZZGmzQKGxS)F~-NM&KW%h1L_00pD#u=fz?XRNCu$Yj=)e(o>EIKJlH5 zh3T={L|Oc_zwxBC;V02uugcC+`(a4ZsGd!`E~1S(54o61W2M|?yL40ImwS7(6?VhE ztJ36`_60Pw%5>}-Ttq(F!lUP%1G0@;{j$ZU1FZaT?=|P;>G5Fs-(cnunbdaOY(G1+ z;$GPwydn?_{c68((k$^|T$U)-L$m)hm^J+#01cj(5P9FK?L02n#D+kJFcEpax%w$o z yZaVCFjT*HBf)Eyy3Lx$R-RV_lx6*OoQ|kD-+>&r<%hMJ^N*rKxvNeaD&LE@n zP@f8DPbqusDQqoxM#`jreZxJ7mdRKFedU{Y&C^`sdmXybXfVTVtk}B9pK?HA9|#+$ z2mnh2nOJhQ0k^eJv?;ljB}AkCJ%F{Hj(8m^bk#;HnTv~l_WU>c#YXSuqVI~8iO1-C ziuuu+*HSG^$9FNnC_VAq(K3*xy1gIa66IRpjCOA1mIX;-3X%Z06)YSRW%iR8+VQt+ z8>~7uGb0f=#P?4%Y~%Q5!Vcg^WtV&lHR{?R-gKonY@PF7xgJ=c^Bl?vFkhH{Kt@jV zv`1bWpzly6t49zi|1DV;m#<;IAH@G__hpI+T*WlnYuqG;QPS#ggk0A?<9M5Q#^12} zH{-DwEfI7Lu~>czhU&Xo;cz2LhZaYOBqM0bW)>{&G4ll=cqWHsJChsR_biZ`w%~wz zk$YFlA4H!?(?Om0YY1%)_)Esbf2 7Y*nT^gsx zlwjKBdl~<3C+b_~bB53&uyO?5WZ $d z05 {8^a6k{;Ba9U$jDyONEyr*o@&3g| zt+bph!qmOTw%uMk6mHj(fPS}&?An9PSOij!9_s|K6GqRWl&I$I(6$|F3# kW^V%X7-ov)em5jKCz4TjAI?OTImq_YxU+YX0~N49kG{ zaj1E*E#$(4W8Y3cgX<}|vXKc76jljtpdBxt6;%^7CV1b6s*)miIpOgUNk6<_6h+dD z15^J;)mw)}^+j#NN_Q&V-60(^q#%t!i6RXGlF~2=2vUNQN-8aibO{XIjkG#63?U6O z!_1uT;O}|9_kAz^<~p;_-sfKTUhBTs5;$_l4R6 -cXP zSt!iK7}kX ah}J;hC(7%Q9=?h!D$hX(w^aNWH0Zi!znish!cV-K>wrFCt!7C$Yir{jL4&;a!S z7I1`*4r{rVfOF^`5bh5sWYk!EB)TI|6)&1bbBCrZYTeaOAjdXar~Ozc@!eyD@Gm{3 zTC(XQnAuC=hkhSv9Q7X>HycWYt-()SaJyE=F3W+`Gsi9wo*_aM=Py9as2MsoIerwW z7Tvr7o%K BdZ|AOt01jf|zIaSb*l(E~Fbo UT8_@ZK41k6%jh-ithPkO8L!EQ_0A-TIcS#6)xUGS6Np&!e z =7v`Oz&Ow~KRQ3Kf>}xlrbzN|rP)*1~ZA%%+U=7*CMmD=@YZP9> zpuUT=_D{7`3P_b5CL5f@xd~(Dg%bUx>SxQIGD~9*S&wjg?2+SwazV=gqfEF r3z9JXi*R@|^zcX(`1HeI&$Chm!Z(n+3 z(34zPGOwiFPo8{C^)Lf_WD^_Aj{hTF<@WU8Y?&xomAslbW8(H%S+O~#Ey=B`-GxkC zBm7LDbup0NcA+Sn59CeRp;-NfAusd>e1`mn0n=xn`aV|9Lcv$+w}3{Vf_kQ`ehnP< zOec42#wS<;&PyZc(#tcGz+Fi9!IR5wBmnP*=1G?hvl+|C-K$ONWs_L-7zy!<)gEJ7 zNke$Z&bdoHDVg$XiO^@Rlhn$Nd5l};_{Q3PWP(2n?y=~59*WJsnGi -aecz{Mr+2wbFuy@iv~N J5 zWfm=?p9D6M<7?GTLMh r2Gxm4ers%g?mGNqq 1;67T9d%Qp+bB#xlx&SUo_m+~86`_Oq{2ZbwT1LvE*EHT<^9iyE& zuVJBd=2WSX`}OQGf~J>vRhG<$*WgXMx4~}NbDoV4WN$}qcE|DWfE{jw$in*|QnXD{ zSf`ItfECUXXAKN|tHHfq0lSOpO%UC#`|JT-!13Lm$29;N$_0i(c??eYtI9tXaTc9q z(hsO_PXZnzS!?BT_~n9|6i#AER9@L}>2u$j5}m**%Bn#}cz9*o*Y|NPLVDMHo9usn zc`*Hbg%v0&HKZtkoN!vi--v%>`$r}klj8XdMK45JXNilHAF}4%<)nFS8FO#ue^*LF zTGicc -~!mp`)i56ss3?F?VXXH!qb$L(95_q}2$_tT+SyG`Nrg2|7` zMVx9qsxY`;2!A>_u4X_(mGOS|P%@s!4e4&dh9u~8K5G0p)`2numi8uxDY~H`c9e6M zbJunP55%x4td7dmxuB=#68s#p2gNF&^2G}U6Ft3Be@{!j?AW1oq%p(1epkfwtyu3~ z$cs-=qXLe`(sV;I&uXq0)eqd>qQq5rcUh|63bQX<{@UeA8GEzu**j~cJ6F%O{BoFq zG9lo$qQ}^w98j!76^A5$ze2x4g+;zWYun6fMMYREodXt(-i0{y+d<(C*GK!G>Y#=JC)XTInu2-VCM19hEyb#tin?5tZb9aNEw(6 zCorZGTxYIG&Fmu+5!%5UXfT x&AqP_M z*VpR1^~oK(beh!M%5{sqy6m`_^p|r8{XSvKhgfDNGCzN9#~!Mt*84((t-?OI&( +0v*yyf=ZM@sAwH{X8ndouI(*1ORKd*%P_ z1z 6Py%wgSGqm-)=4L$P$rSu_cSAU*2zAz!AKpgaNaMc zkLS?+Xhxf-m%Q2nMiL7)X{imcGEoocB5qdwEtdMyHih_OZwB*1E#|xTNcrHGHHE`8 zlJwDGR<*WP*BlNzKd^ z6MOezSvQEgg(Fp%PyRwR_kP)-+_4VW>}EVRyQ|5Q;QKaNg7*pvN9dx8|I`7r^7S4h zHt|MwK!QjSG19iMFS(O!Cqpsn2?6=1%JRK!u_tnOuaCbR+*6FvTn`~U6S)6TOVPq9 z9{ {FCn{}JF%aA z23f7Vwi|~A2zm1Pze1w|JfQR7J7$jjc^;UNiyrieH(Y|><2#3_lYYFOC4DZKQM0u+ zr58maeQ9b-k~XbT;h8nN!SoeZF!O*xA*bGL!&mv4GSQS=>B|@D!RuRWPJWM76`nq= zwl~*~0dcQ@VB(Ggx49`%rtg2DB;()A$yrER(k!e2a<#*C|IT)1E1`0ROfz5=?(n2} zyf-T5{%^ct-9mz_k56yG8woO!C;K)}2Qt5C$1UCxah@ok>)L) WU%B#g*G$oZ00>!ErQHhI-_;9C_SH;n`VPK` #mtX_KB%_1mTp8d_$~23VwoW5MG8u5f+La2xuT z1Hyoos8GHeECO5Qb7nMKpmpKs&Htm|Vj#0jcc!S{J1L^LY7I?%aEpZ8sdor8I{&-| zc{X|&$A+Ob==prad4mB-!sE6a)3ll^!B@a$2rXj$|9=7)oLvXymuwp#-4AN8x#I3% zT!uCIf|Djo(co$YI;16QS3?Y`dw9M@rv^$YYq$$1w4A0-I z_!0g-rYzu8Hu4YI=zn7-p;u!j_q~RwczEdJu*t0=W{TASx9s6m63)v5S3+Af;m=`F0QLc> zNn0V|z}Jr6Fd>;t{E(8(vQmfPcm4{Ew0J|l3tvv(S!xM0_$lRB4gz@7HQ)cB#O)HC zF^NsiTY=WKCKd19+hj8;dO6mYCu>>Ul@)sfS74KPPeH#*g;ypbt6RQS^1HxN2lCra zkn^2jWv<|p;36 rW&)cBSNM-&`0yuN^|vzvbNPMq)Y$g z^>#Im63yida_9DI59e`1no)SY=cQHZv>p?4j2;r4myTZ2?jt> 0`b<74LZ`G`mln7f4=f~d{G8KnvIa>k*<8_aKm=!xPUNU-)**})bdjXO}F3plm z2|}$2(f{HH1_bu`basOU1iQ?QVI761>UzQ+Ssa)T go=q`nXOC=h zJqV6-B`uFGofK|JJ+Ml>d0Z%EkL-gXzW34jR$md}v~2bbenMA*A){g2UCuA}cWptc z0*j{_)to*r-S@iWJZyB;U4~&{QaNQ(p>KdV<`bV4&A{2!g>%uhI58UTJWdPoq|evW zPU3$J8`tZLiatw2zo2d%*({557VSEGRn ^!o@^zE1y5dS_Wut1yrwQUI5_$TFv%Y#9r% z*sv*mpk>xwnpMFa?Zcf7Q>W2wOoUEzTJ~dfGX_8c$BSV8_|meR(()nC{Bh>g44U|X zJ(e}(;c--mTvpH3vzKgd$=1#epl8lu`(Ez|CC(wZHJt8n8MU_!3(fC8mG$hDtEnZ= zq+A0&iPB4w#kiX$Y9w*hasa0F?7JzC6u%@!%O^?HD$86;zVOI%qPR*#y<+ZAv{!l@ zX_3XCG&GeYVb9U?%t_|^e{l2- rU!PDd$O+0(!*&1hmVqLb^d^Ai$jpN+D3_r zJfU@vx~*B1{SWoNLCZdV`+pwvebr|y5$7W2^j$2R36=7U&8}NkK9m7= jxOEetM7) zBTRnh@Dbtch{2x)jvX>0UJ6v~Bwm6~ajhzk*tog>{OtqqeG+T@chllF5r=-daXe=^ z#X0l(V>deX;{&LA>Y1d^JIZ8?malL_j9x54^TXAaHb8`RJfTYfNW>g4 m-h#Pb9dabiZUeO$VEevU*A-Y*WcMMLD`~%*AIisYm zR=g2?#Q?Xjy&}T98GA}{;I15vI(rIoGWW} wosr5p2$F2`u8Su|G8sy;|Q{xo|mmB!PC&oKxLB zqSq2Nf*#Q6$vR-t(%u>PUW$TwkMzIP?5w*9zct2(VRX=C_>fm*L*439PZA_YayP>D z`}=ys7b?@$y!gp(8r4FDpPQdn3S|AGzE|);_l+)-Tl$5qgdA;!%@u;|!aekUW_6zv zd&pcp|LX06%;Y{Yzus%a{UgQShC2@Rdh7`uJ5+%_1$JceVWoOvR-@v*x0EFUw{mnv zMSP59 ~hH?3q8|Wmaxsd_MfXzaDE51RwsXd_ANUwl*imH@$%ytRROx;78 zOT+WJNyEmabO8{u `;Hh?NCNzHMAgYL zE?BKL(vMZRbaoAod0fF6q5pcqzN9{xDG`nRrA+qHYc)Oo23YxnS74e%JsO+Lu?{pO z=1DcQ83jl($+32%?UJ=*W-%f}?y$TqvJnDOIu^8rt!A7E30UUE>5^MJp*M{?bij*s z6w*|e<`Jz3n>ka3j{c{6vq+!`aga3kM^6EM>dxK76RNE}#JvCQ+c&q$=1oH^@u@*Y zWy8Il-=P8PY3)7VU2`FCvT^tdx8DE =kCe?K`;3-lWFT zMmP)MN4LA=Vp6t?>#Vi{4?h5+cf@iZpofkR)-s@ r@DiW;Z&8d<`3 zL#9Y60fdcjT3N}@FbT_+I~xuW6Cm pT1H<6lh!)Jyff`w@EP#lLD2t#K*ugXWj?g@m8$P(~~iXSIf@M z3Ewzh9nG!;dddAlYbrsqRSan$UT?|byE6E>oB1w~Hvanz_d5yNeGq|i2N|9fPMz*1 zHqLe!s{cC0i1JdjNX~B$nqo-?a;$Uz%lr9RDbH1qP$RHlO~yM}Yieaup?|Y2;k@w2 zAls?AIsqHygN^y?9|FnvKPd2z** RE?*llz z==S3?UI?GsmT~qZ!KR^MhSqky8!WP3Xs_kZu}6Rp1Ov(@gDQi!_ERfc(HM?SXy`s^ ze^}3OQBK`DT|)6mOy1YKjB!@Q&f3;sLdz0} j4hzP+uB7#fRzM~#$eHzus9f>D z2H+i` S1;cnn!02q> pNN#EP2OkYwXn~r@YNMzJ#ai)@&k?JErR5okl;p>tqOY>5Q)eYssG+ zaBtSl^MkkULYg;QIW(X-(ws`G{DW}*OtzG ;7+@i(uNy>5|?i z b!ZP2n@wa4PI(T?A2}VF%ai2 w2;tyx?!3SGCUfVDw@s{l_H-uA$VSZM)d=-I+;U=~} zDOBM$WVQEnXu#s3M<;4?Zn>lNBe}e^`^WFx*g&Gz(%M5EklppD^ <`_^SxQ zuZty1Vj;~J(EW6UX_;}g1QYH}li_9dCa+F) v5wAy6A4xRHb_L+v-|-wd%jg* zFCVGBtXs^3!czAMo|EDyj7s Lz^g*#$a@BTsUbPpPUG7qKU@~i^BDq8z zN|{6M@XFi ~Xk~;nou5OSpyXlei3W?q5 zERMpJk7mW1#(mM|zWaq%Q(pJEjUPwi7Y2`KG i#x?nmG37$m&~f5ZQ$8U$Ren>wsUu#h 9`<}F5qLK8$J6(95HNb1=(+(Gxiqf_L0B<2c`K2jCoL<)g6M2 z$`ha~K`gCdY2}eyq2%|aQyk|t&hA*O>~E^DDPr;CSgL(N3rhh?<>Szft*{V*F0VS) zeNoDn^QBSHpgS#`@GQT_ruiszkrvT@?ilyjlVGhg%K&2v #2x5GhbN40_)q; z_U-(N?=8}8DeNcwnLYOc!W<%K)GC6=g2WY-kJ5OjT+DkTh^%nFy_10rza`slKa(A* zD_z*i;(hGbZMv5<@p0Eb0;)!?Rc93;4po_gopHL!mlaLKlL!vOmdT3VEc_SmL{+&n zguZf@kfOXU6?pL>Iwd_<2_chOEDBNz)lK5W52ccMyi7ut;>$;E6=T>{AyEF3c|Kf8 zeAjk8{&kPh*WYZBO%EP1D?{GKvjrwidCY-9q986$8!GLfrWli@UYG3=!|bQnO;H3d zaF~$c!4JGmGQ4PO_Ghma%JVoJJ0ZI(R$El<&|iXo0TF16W bQFCjxO#|2%>k|&4nX%v*Y>dh$;$M3kp$#)%P1;7C2`+ z&7O85sej(#TP^Q&r}(ko?zy WaiUIX%BkXJ0snpM}CMm zg|Ot2 >h5gha^eQp|e$wtN2 z2GK$Jb#R9kQ!3y-{{eniAK>Nd;?I3I-}g@wA{Up15Rp(B87wdDDPt EesCZKV#}0&Gzs?8REn~ourIJ70E!MRG z?sWc|`NMAcH`hv8Rw@nk$vv9*DP32mY%n%4YSNNADf2XMoc=9<2}-L354`G+Pa%b- zj!HYU!;OtjAuMnAybhW~ABP;tEdXwJE1=JvlwuqcKQUtXu#viz^(!&m5lE{51pm~@ zc(UQzN}3t>$xWp~<-HnCQ8oJ>a<}~3)Z_d0r8*3V>{&~PdfA=*z^Qxyqs{_!5^TH! zsQM@lGNn=ta4O(IS J= zva6`f&m2>iXOH+OStDZB$0x`oO<=7@R2$Wl@ySlN)Et z`7b{BM`qMO#E_Fhwx8G!q}*|0*9eb{_W-M-UyQasL>Z;yrJs{kF+s|{Gyw)KCuU;e zZ#&b~!Z#yRF`6tYR0ny}#HR 6)cP8#gmsXYaWMz4dfkRKI9`33x;&!RxU+Di_@0s3RkzujMvqw&r z0IA$7PWu5|=7aV sc@*81SNYP7o7EujRW^RW8LJ2&iJ2r_&xP;*DhHM)k|Do zZ*<8Lo~f-W7pNZl8LxLAt| gRVQ~kBE ztTgK4|7Mb-sFuWfk+ V zAL=cxG_uo4-Xp`8)J#>b=t-;olIG11qDkE<9R3~B2*NOxCrIEIxrx4kL|YaTP+5YS zz|-jO*or;kj6fxe;vj3M 9hIZp_A@ys^aLt%{I%4CP#2|HbME6kD$# zieB1TvNib;5vbA$ISkb;E(Oj!W`NMWv)nqA0*nWf+^HW2HU_?pUE?&dI7E?lTEN;h zTGr+QOSoOAEN(|`aN!*p>0Tbe0N&j2zN%9?bhE0T)zBCYa(pFxPFCec?g0rBDB5#M z%Liab%uc~#P&|I`@Nz`Kq-FIBcln&yuV9N3n+Pq;&p(_BdR$#RmN-1>0`Xl~iq*}> zFSVWPFyg1 ^(L}~DVj>7j3o4AXw63<2-O|ON& 1v!D7N?)qA=UV7QwlrM{Q2<|l(~LGYg!nB($y`#38@z|dVT65W}$|&9Es$udKB2F0Dt5O zPzog$0K}X@eAvS>PT~Ahpx#LS=LNj+7iQOgrP;y}XTU^}MPECZZ<3GNE1fR2Ko?oN zyXW)avME_#j^4hoc~)4Rb{lyReV-V?>gY^|YYT*=7^@z6bh>nHsloKGL@U?HYbRi> z_; %*^<@f> zfF9^FE><+G22*Kc-44!z(n0RxL-Ey2EZn1k8KYT~%fG;5Ck7<@hKW*t>p=eflVuX( zo5BD`FrRa;49W7zeU_l$tz2oh*l!n==zRElcfp986d1_aPuP?O9MTX8J73H zL#xMP{TZ?k3U=+ztl^~S*FUBHLOJ 9WvA<%@^q<=C>#=7R z2z$j6N0KRsOZrw;p~7EgDbYE3VILQtV{!x_laIaO{F2(l#6hbL|3ZaKZXzNW+V}7a zyGn%Q$JxuNMYj{La2 2DLnqAO2D@Jy?`&jcUd&Pa zeW@dY1A2`DixS3wxW&!;Y|tq?L~X_QuP##_XW`Tj4>*qn`3M!_F1tSF&rqCvsfe&f zwy!KMP4P3@K3vH+SaTuHV GUa7Ra4j)cd-Ecm2r+?bSd!s!6X_*B@R(S9A0cb za!cTa0tQd9`$vVC1jb`|Df=KpKqG|~2(NyGDTXMC-hMl}Lu7}7GG~D|S+A0Vz1D}S zx-llG#u vgFN0a^ONh!tJT1z-ktJ zq-{uf?=(%G1J!5N*bhkd#y`4LiU_Zbhid7@%KIb~c(>DxRnJGr)GWxuSn%00V0%Z~ z?q_Z~*OZc?qbPRsJBTM!&3IaG;7VI-anvH*?4-I~q|yu*X@H^N?FHO*mX|cc`CM-% zI1)3TqIh#Gar#fLa)>|oVHuNa{hDy+hrGi2bi#UHngk5y2MW>e@P-yB&ALOX-pD;7 zlF2X^%=~pvz1B2fuQ{|z^%{nxW`$k}$(xY~ G{uCvX zcP93;XNk4M6}hYt5$@>0jk%q#r$RpqD5uB{no=)GOF8n;4k*QL;r{&8zwKKQGY3Bg zy4)1oWB(37;R^xzr<;!o37{Ne3c$h(##Pr!t?2~G)$Hf0j4tr+IMl=r``Oc=0TpPd ziSk+<`HcJuG|=SlL^~-F$8n+(#~W 9L{&mw&iWJxKwY zPf( >b7dz>{*gzo;>CX =2cZU#09b?~0_n4=nrsnBn%1 z*uxBPJMJNXlIg`(*^gagxXE|BU{(BNnIC^QSKeGVPXaEO6-nv{Td#GzR}4tXzt+Qw z7mJj@sZqHWkP(|!w)~CsM;*FXk+KJr_g_vAaz^x8p-W&6fI$sCmG09+-nvJ)fG+5Y zkDZ<#Ud>B!9PesSDY=&)NlPuR7Ps|`dkNA2aPfNp#%6#upq^w2Xhua^JUbr;T5z7s za^Q$OIZ<8tl?Mvqls4OX_*VPM4&rG^zCA`meMj#1G(^TFPb`xQcYFLWRqP+74b^j? zdr>*VoZVZ7{6@}P-AL*#T04J9>}?cs8Zm+YcJwsaZ$trZshjrD_r`A>^w#r*9A+2l z 1ygLa*e^2{z7b4Z|M_qjp0w>C3A9H`=L&uo9gd}1K6 zuCUptt86`k+HZ3ayryXkYegv_x=ti?6G_FhIyutJA9*@;x-9a2WkJ$s%) ;1{@wxd!* 1-puWMFz@m#1dnxt zJ(Br^2WaR`M1gwe@{k_L%85&b*WY)4x$=YDS+h3oXCuPfzG>``>u>@W^ubI#|4$?^VasvXK?;BTTxqza zZ?U3y(kuCTPXF|6@2A1$%@iE#<5S88EgUMjekeJZG|VC!M%eKw)6PL^&-KYNK&n!3 z^7UjH77#{`L;0^u6R)zK3{QzSJ{4eT9SI=jO!gXqz`iV> 7=;<9T`dV! z!t5qWJw{um{^FZB7_KDY v$*RSUWnwhW>EzH?SvLwFW^-VJ%fP zq_nki30>Bg92j%9_gCgg5<-~xJE9()L5}ROnQ<}4N!L%iM|tjwQ~KWHdH)rN`@jP* zNn*Tk^n;c-g58u`H{yjF{#E%1{pNAoj{hTa5nv L$kS(0TE)X)u!au zI=OXgEmp}xX4hA4fvCIjCh0RXG6+RM7?Mzg2~6qlY-^k{8Z0D}KkF;>L? &?KWR?kl6h3t}C*pC;=V8in>%3G{_k0@ZO?HUr8i?-Xu#amU;?a}#gLzkd)|Kq1Q z4!Ba{v=RBtegS`7;E>p@UG1bOurJ97qPkjlIu2EhVq0zI->;iyaC28|y?|fL`FHua za4Q{xb;aOHWd$1tWcR^=5kz?dPpt+C&Nw;|s