From 9bd22bf3d9ca4de0673e30dbd93e1e96a4c9118d Mon Sep 17 00:00:00 2001 From: dv Date: Wed, 8 Apr 2026 21:37:04 +0300 Subject: [PATCH] =?UTF-8?q?=D1=8D=D1=8D=D1=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 8 +- backend/app/routers/objects.py | 194 +++++++++++++++++++++++++- backend/app/schemas/object.py | 38 ++++++ backend/pyproject.toml | 1 + create_objects_template.py | 129 ++++++++++++++++++ frontend/src/api/objects.ts | 63 +++++++++ frontend/src/pages/Objects.tsx | 225 ++++++++++++++++++++++++++++++- objects_import_template (2).xlsx | Bin 0 -> 21777 bytes 8 files changed, 651 insertions(+), 7 deletions(-) create mode 100644 create_objects_template.py create mode 100644 objects_import_template (2).xlsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cec2c47..26b8dfc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,13 @@ "Bash(powershell -Command \"Get-Content ''C:\\\\Users\\\\meme\\\\Downloads\\\\racks.csv'' -Raw | Write-Output\")", "Bash(powershell -Command \"[System.IO.File]::ReadAllText\\(''C:\\\\Users\\\\meme\\\\Downloads\\\\racks.csv''\\)\")", "Bash(powershell -Command \"Get-Item ''C:\\\\Users\\\\meme\\\\Downloads\\\\racks.csv'' | Select-Object Length, LastWriteTime\")", - "Bash(npx tsc:*)" + "Bash(npx tsc:*)", + "Bash(python -c \"import openpyxl; print\\(''openpyxl available:'', openpyxl.__version__\\)\")", + "Bash(pip install:*)", + "Bash(python create_objects_template.py)", + "Bash(python -c \":*)", + "Bash(python -c \"import sys; data = sys.stdin.buffer.read\\(\\); print\\(data.decode\\(''''utf-8'''', errors=''''replace''''\\)\\)\")", + "Bash(docker compose:*)" ] } } diff --git a/backend/app/routers/objects.py b/backend/app/routers/objects.py index 5a9d1ff..3b12aaf 100644 --- a/backend/app/routers/objects.py +++ b/backend/app/routers/objects.py @@ -1,6 +1,8 @@ import asyncio +import re +from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from sqlalchemy import select, distinct from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -11,7 +13,19 @@ from app.models.object import Object from app.models.tag import Tag from app.models.user import User from app.schemas.admin import SheetsSyncRequest -from app.schemas.object import ObjectCreate, ObjectMetaResponse, ObjectRead, ObjectUpdate, SyncResult, DiscoveryStarted +from app.schemas.object import ( + ObjectCreate, + ObjectImportError, + ObjectImportPreview, + ObjectImportResult, + ObjectImportRow, + ObjectImportSkipped, + ObjectMetaResponse, + ObjectRead, + ObjectUpdate, + SyncResult, + DiscoveryStarted, +) from app.services import crypto from app.services.database import AsyncSessionLocal from app.services.device_discovery import discover_via_ssh @@ -215,6 +229,182 @@ async def discover_devices( return DiscoveryStarted(task_id=task_id) +@router.post("/import/preview", response_model=ObjectImportPreview) +async def preview_objects_import( + file: Annotated[UploadFile, File()], + db: AsyncSession = Depends(get_db), + _: User = Depends(get_current_user), +): + """Dry-run: parse xlsx and return what would be created/skipped.""" + return await _parse_objects_xlsx(db, file, dry_run=True) + + +@router.post("/import", response_model=ObjectImportResult, status_code=status.HTTP_201_CREATED) +async def import_objects( + file: Annotated[UploadFile, File()], + db: AsyncSession = Depends(get_db), + _: User = Depends(get_current_user), +): + """Import objects from xlsx. Deduplication by server_ip (skip + warn on duplicate).""" + preview = await _parse_objects_xlsx(db, file, dry_run=False) + return ObjectImportResult( + created=len(preview.would_create), + skipped=len(preview.would_skip), + errors=preview.errors, + ) + + +def _norm_col(name: str) -> str: + """Normalize xlsx header: 'server_ip *' → 'server_ip', 'ssh_user (root)' → 'ssh_user'.""" + name = re.sub(r"\s*\*", "", name) + name = re.sub(r"\s*\(.*?\)", "", name) + return name.strip().lower() + + +async def _parse_objects_xlsx( + db: AsyncSession, file: UploadFile, dry_run: bool +) -> ObjectImportPreview: + import io + try: + from openpyxl import load_workbook + except ImportError: + raise HTTPException(status_code=500, detail="openpyxl not installed") + + content = await file.read() + try: + wb = load_workbook(io.BytesIO(content), read_only=True, data_only=True) + except Exception: + raise HTTPException(status_code=400, detail="Не удалось открыть файл. Убедитесь, что файл в формате .xlsx") + + # Use first sheet (the data sheet) + ws = wb.worksheets[0] + rows = list(ws.iter_rows(values_only=True)) + if not rows: + raise HTTPException(status_code=400, detail="Файл пустой") + + # Build column index from header row + header = [_norm_col(str(c)) if c is not None else "" for c in rows[0]] + col = {name: idx for idx, name in enumerate(header) if name} + + required = {"name", "city", "server_ip"} + missing = required - col.keys() + if missing: + raise HTTPException( + status_code=400, + detail=f"Отсутствуют обязательные колонки: {', '.join(sorted(missing))}", + ) + + def _cell(row: tuple, key: str) -> str | None: + idx = col.get(key) + if idx is None: + return None + val = row[idx] + if val is None: + return None + s = str(val).strip() + return s if s else None + + would_create: list[ObjectImportRow] = [] + would_skip: list[ObjectImportSkipped] = [] + errors: list[ObjectImportError] = [] + + # Track IPs seen within this file to catch intra-file duplicates + seen_ips: dict[str, int] = {} + + # Pre-load existing server_ips from DB for fast lookup + existing_ips_result = await db.execute( + select(Object.server_ip, Object.name).where(Object.server_ip.isnot(None)) + ) + existing_ips: dict[str, str] = {ip: name for ip, name in existing_ips_result.all()} + + for row_num, row in enumerate(rows[1:], start=2): + # Skip entirely blank rows + if all(c is None or str(c).strip() == "" for c in row): + continue + + name = _cell(row, "name") + city = _cell(row, "city") + server_ip = _cell(row, "server_ip") + + # Validate required fields + missing_fields = [] + if not name: + missing_fields.append("name") + if not city: + missing_fields.append("city") + if not server_ip: + missing_fields.append("server_ip") + if missing_fields: + errors.append(ObjectImportError( + row=row_num, + reason=f"Обязательные поля не заполнены: {', '.join(missing_fields)}", + )) + continue + + # Deduplicate within file + if server_ip in seen_ips: + errors.append(ObjectImportError( + row=row_num, + reason=f"Дублирующийся IP {server_ip} в файле (строка {seen_ips[server_ip]})", + )) + continue + seen_ips[server_ip] = row_num + + # Deduplicate against DB + if server_ip in existing_ips: + would_skip.append(ObjectImportSkipped( + row=row_num, + name=name, + server_ip=server_ip, + reason=f"Объект с IP {server_ip} уже существует: «{existing_ips[server_ip]}»", + )) + continue + + ssh_user = _cell(row, "ssh_user") or "root" + ssh_password = _cell(row, "ssh_password") or "123123" + db_via_tunnel_raw = _cell(row, "db_via_tunnel") + db_via_tunnel = bool(int(db_via_tunnel_raw)) if db_via_tunnel_raw and db_via_tunnel_raw.isdigit() else False + + import_row = ObjectImportRow( + row=row_num, + name=name, + city=city, + server_ip=server_ip, + client=_cell(row, "client"), + description=_cell(row, "description"), + notes=_cell(row, "notes"), + ssh_user=ssh_user, + db_via_tunnel=db_via_tunnel, + ) + would_create.append(import_row) + + if not dry_run: + obj = Object( + name=name, + city=city, + server_ip=server_ip, + client=import_row.client, + description=import_row.description, + notes=import_row.notes, + ssh_user=ssh_user, + db_via_tunnel=db_via_tunnel, + is_active=True, + ) + obj.ssh_pass_enc = crypto.encrypt(ssh_password) + db.add(obj) + # update local cache so next rows in this file see the newly added IP + existing_ips[server_ip] = name + + if not dry_run: + await db.flush() + + return ObjectImportPreview( + would_create=would_create, + would_skip=would_skip, + errors=errors, + ) + + @router.post("/{obj_id}/sync/sheets", response_model=SyncResult) async def sync_devices_from_sheets( obj_id: int, diff --git a/backend/app/schemas/object.py b/backend/app/schemas/object.py index 4af65b7..a07ef5e 100644 --- a/backend/app/schemas/object.py +++ b/backend/app/schemas/object.py @@ -85,6 +85,44 @@ class ObjectMetaResponse(BaseModel): clients: list[str] +# --- Import from xlsx --- + +class ObjectImportRow(BaseModel): + row: int + name: str + city: str + server_ip: str + client: Optional[str] = None + description: Optional[str] = None + notes: Optional[str] = None + ssh_user: str + db_via_tunnel: bool + + +class ObjectImportSkipped(BaseModel): + row: int + name: str + server_ip: str + reason: str + + +class ObjectImportError(BaseModel): + row: int + reason: str + + +class ObjectImportPreview(BaseModel): + would_create: list[ObjectImportRow] + would_skip: list[ObjectImportSkipped] + errors: list[ObjectImportError] + + +class ObjectImportResult(BaseModel): + created: int + skipped: int + errors: list[ObjectImportError] + + class SyncResult(BaseModel): created: int updated: int diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6207f71..e668bc0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ # Google Sheets sync (optional — requires GOOGLE_SERVICE_ACCOUNT_JSON env var) "gspread>=6.0", "google-auth>=2.0", + "openpyxl>=3.1", ] [project.optional-dependencies] diff --git a/create_objects_template.py b/create_objects_template.py new file mode 100644 index 0000000..dd95dc9 --- /dev/null +++ b/create_objects_template.py @@ -0,0 +1,129 @@ +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +wb = Workbook() +ws = wb.active +ws.title = "Импорт объектов" + +DARK_BLUE = "1F3864" +LIGHT_GRAY = "F2F2F2" +LIGHT_BLUE = "EBF3FB" +WHITE = "FFFFFF" + +columns = [ + ("name *", 30), + ("city *", 20), + ("server_ip *", 18), + ("client", 25), + ("description", 35), + ("notes", 35), + ("ssh_user (root)", 15), + ("ssh_password (123123)", 18), + ("db_via_tunnel (0)", 20), +] + +sample_row = [ + "Парковка ТЦ Мега", + "Москва", + "192.168.1.10", + "ООО Рога и Копыта", + "Въезд с ул. Ленина", + "Контакт: +7 999 000-00-00", + "admin", + "qwerty123", + 0, +] + +header_font = Font(name="Arial", bold=True, color=WHITE, size=11) +header_fill = PatternFill("solid", start_color=DARK_BLUE, fgColor=DARK_BLUE) +header_align = Alignment(horizontal="center", vertical="center", wrap_text=True) + +sample_fill = PatternFill("solid", start_color=LIGHT_GRAY, fgColor=LIGHT_GRAY) +blue_fill = PatternFill("solid", start_color=LIGHT_BLUE, fgColor=LIGHT_BLUE) +white_fill = PatternFill("solid", start_color=WHITE, fgColor=WHITE) +data_font = Font(name="Arial", size=10) +data_align = Alignment(vertical="center") + +# Header row +for col_idx, (header, width) in enumerate(columns, start=1): + cell = ws.cell(row=1, column=col_idx, value=header) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + ws.column_dimensions[get_column_letter(col_idx)].width = width + +ws.row_dimensions[1].height = 30 + +# Sample data row (row 2) +for col_idx, value in enumerate(sample_row, start=1): + cell = ws.cell(row=2, column=col_idx, value=value) + cell.font = data_font + cell.fill = sample_fill + cell.alignment = data_align + +# Empty data rows 3-52, alternating white / light blue +for row in range(3, 53): + fill = white_fill if (row % 2 == 1) else blue_fill + for col_idx in range(1, len(columns) + 1): + cell = ws.cell(row=row, column=col_idx) + cell.font = data_font + cell.fill = fill + cell.alignment = data_align + +ws.freeze_panes = "A2" + +# --- Справка sheet --- +ws2 = wb.create_sheet("Справка") + +ws2.column_dimensions["A"].width = 22 +ws2.column_dimensions["B"].width = 40 +ws2.column_dimensions["C"].width = 20 +ws2.column_dimensions["D"].width = 20 + +title_cell = ws2.cell(row=1, column=1, value="Инструкция по заполнению") +title_cell.font = Font(name="Arial", bold=True, size=13) +ws2.row_dimensions[1].height = 24 + +# Sub-header +headers_ref = ["Колонка", "Описание", "Обязательность", "Значение по умолчанию"] +for col_idx, h in enumerate(headers_ref, start=1): + cell = ws2.cell(row=3, column=col_idx, value=h) + cell.font = Font(name="Arial", bold=True, color=WHITE, size=11) + cell.fill = PatternFill("solid", start_color=DARK_BLUE, fgColor=DARK_BLUE) + cell.alignment = Alignment(horizontal="center", vertical="center") +ws2.row_dimensions[3].height = 22 + +ref_rows = [ + ("name", "Название объекта (парковки, площадки)", "Обязательно", "—"), + ("city", "Город расположения объекта", "Обязательно", "—"), + ("server_ip", "IP-адрес основного сервера CAPS", "Обязательно", "—"), + ("client", "Клиент / заказчик", "Необязательно","—"), + ("description", "Описание объекта", "Необязательно","—"), + ("notes", "Заметки (контакты, особенности и т.п.)", "Необязательно","—"), + ("ssh_user", "Логин для SSH-подключения к серверу объекта", "Необязательно","root"), + ("ssh_password", "Пароль для SSH-подключения к серверу объекта", "Необязательно","123123"), + ("db_via_tunnel", "Подключаться к БД CAPS через SSH-туннель (0 — нет, 1 — да)","Необязательно","0"), +] + +for i, (col_name, desc, required, default) in enumerate(ref_rows, start=4): + fill = white_fill if (i % 2 == 0) else blue_fill + values = [col_name, desc, required, default] + for col_idx, val in enumerate(values, start=1): + cell = ws2.cell(row=i, column=col_idx, value=val) + cell.font = Font(name="Arial", size=10) + cell.fill = fill + cell.alignment = Alignment(vertical="center", wrap_text=True) + ws2.row_dimensions[i].height = 18 + +note_row = len(ref_rows) + 5 +note_cell = ws2.cell(row=note_row, column=1, + value="⚠ Дедупликация по полю server_ip: если объект с таким IP уже существует в системе, строка будет пропущена с предупреждением.") +note_cell.font = Font(name="Arial", size=10, bold=True, color="C00000") +note_cell.alignment = Alignment(wrap_text=True) +ws2.merge_cells(start_row=note_row, start_column=1, end_row=note_row, end_column=4) +ws2.row_dimensions[note_row].height = 32 + +out_path = r"C:\Users\Professional\Documents\VSCode\utp_service\objects_import_template.xlsx" +wb.save(out_path) +print(f"Saved: {out_path}") diff --git a/frontend/src/api/objects.ts b/frontend/src/api/objects.ts index 8d36fb5..d755acb 100644 --- a/frontend/src/api/objects.ts +++ b/frontend/src/api/objects.ts @@ -261,6 +261,69 @@ export const useObjectsMeta = () => staleTime: 30_000, }) +// --- Object xlsx import --- + +export interface ObjectImportRow { + row: number + name: string + city: string + server_ip: string + client: string | null + description: string | null + notes: string | null + ssh_user: string + db_via_tunnel: boolean +} + +export interface ObjectImportSkipped { + row: number + name: string + server_ip: string + reason: string +} + +export interface ObjectImportError { + row: number + reason: string +} + +export interface ObjectImportPreview { + would_create: ObjectImportRow[] + would_skip: ObjectImportSkipped[] + errors: ObjectImportError[] +} + +export interface ObjectImportResult { + created: number + skipped: number + errors: ObjectImportError[] +} + +export const usePreviewObjectsXLSX = () => + useMutation({ + mutationFn: (file: File) => { + const form = new FormData() + form.append('file', file) + return api + .post('/api/v1/objects/import/preview', form) + .then((r) => r.data) + }, + }) + +export const useImportObjectsXLSX = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (file: File) => { + const form = new FormData() + form.append('file', file) + return api + .post('/api/v1/objects/import', form) + .then((r) => r.data) + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['objects'] }), + }) +} + export const useSyncSheets = (objId: number) => { const qc = useQueryClient() return useMutation({ diff --git a/frontend/src/pages/Objects.tsx b/frontend/src/pages/Objects.tsx index 594fc3f..9d1c4e2 100644 --- a/frontend/src/pages/Objects.tsx +++ b/frontend/src/pages/Objects.tsx @@ -1,5 +1,13 @@ -import { DeleteOutlined, EditOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons' import { + DeleteOutlined, + EditOutlined, + PlusOutlined, + RightOutlined, + UploadOutlined, + WarningOutlined, +} from '@ant-design/icons' +import { + Alert, AutoComplete, Button, Checkbox, @@ -13,8 +21,10 @@ import { Popconfirm, Row, Space, + Table, Tag, Typography, + Upload, message, } from 'antd' import { useMemo, useState } from 'react' @@ -25,7 +35,10 @@ import { useObjectsMeta, useUpdateObject, useObjects, + usePreviewObjectsXLSX, + useImportObjectsXLSX, type ObjectItem, + type ObjectImportPreview, } from '../api/objects' import { stripEmpty } from '../utils/form' @@ -48,6 +61,15 @@ export function ObjectsPage({ mode }: Props) { const [searchText, setSearchText] = useState('') const [form] = Form.useForm() + // Import modal state + const [importOpen, setImportOpen] = useState(false) + const [importFile, setImportFile] = useState(null) + const [importPreview, setImportPreview] = useState(null) + const [importStep, setImportStep] = useState<'upload' | 'preview'>('upload') + const previewMutation = usePreviewObjectsXLSX() + const importMutation = useImportObjectsXLSX() + const fileInputRef = useRef(null) + const objectUrl = (id: number) => mode === 'inventory' ? `/inventory/objects/${id}` : `/work/objects/${id}` @@ -133,6 +155,39 @@ export function ObjectsPage({ mode }: Props) { const isSubmitting = createObject.isPending || updateObject.isPending + const openImport = () => { + setImportFile(null) + setImportPreview(null) + setImportStep('upload') + setImportOpen(true) + } + + const handleImportFileChange = async (file: File) => { + setImportFile(file) + setImportPreview(null) + try { + const preview = await previewMutation.mutateAsync(file) + setImportPreview(preview) + setImportStep('preview') + } catch { + message.error('Не удалось прочитать файл') + } + } + + const handleImportConfirm = async () => { + if (!importFile) return + try { + const result = await importMutation.mutateAsync(importFile) + setImportOpen(false) + message.success( + `Импорт завершён: создано ${result.created}, пропущено ${result.skipped}` + + (result.errors.length ? `, ошибок ${result.errors.length}` : '') + ) + } catch { + message.error('Ошибка при импорте') + } + } + const renderObjectRow = (obj: ObjectItem) => (
setSearchText(e.target.value)} /> {mode === 'inventory' && ( - + + + + )}
@@ -230,6 +290,163 @@ export function ObjectsPage({ mode }: Props) { /> )} + {/* Import xlsx modal */} + setImportOpen(false)} + width={900} + footer={ + importStep === 'preview' && importPreview + ? [ + , + , + ] + : null + } + > + {importStep === 'upload' && ( +
+ { + handleImportFileChange(file) + return false + }} + style={{ padding: '16px 32px' }} + > +

+ +

+

Перетащите .xlsx файл или нажмите для выбора

+

+ Поддерживается шаблон objects_import_template.xlsx +

+
+ {previewMutation.isPending && ( + + Анализируем файл... + + )} +
+ )} + + {importStep === 'preview' && importPreview && ( +
+ {/* Summary */} + + + Будет создано: {importPreview.would_create.length} + + {importPreview.would_skip.length > 0 && ( + + Пропущено (дубли): {importPreview.would_skip.length} + + )} + {importPreview.errors.length > 0 && ( + + Ошибок: {importPreview.errors.length} + + )} + + + {/* Would create table */} + {importPreview.would_create.length > 0 && ( + <> + Будет создано + {v}, + }, + { title: 'Клиент', dataIndex: 'client', width: 130, render: (v) => v ?? '—' }, + { + title: 'SSH', + dataIndex: 'ssh_user', + width: 80, + render: (v: string) => {v}, + }, + ]} + /> + + )} + + {/* Skipped */} + {importPreview.would_skip.length > 0 && ( + <> + + + Пропущены (дубликаты по IP) + +
{v}, + }, + { title: 'Причина', dataIndex: 'reason' }, + ]} + /> + + )} + + {/* Errors */} + {importPreview.errors.length > 0 && ( + <> + +
+ + )} + + )} + + {/* Create / Edit modal — only relevant in inventory mode */} =!(%sS}DcvdENJuwGmvpy;ba$sngLFxE$AX2#g1Z*z-tK+= z|D1ErJ@-E6dG0?IChJ?{d&l>FG3FX`j5TE?pkXi`z{A5opjHoj`{1_&2mH5_IlY06 zuA`}?vF)!v80nlW%u~N$EJoLsvd8lwl_=F!m$@NQJI@!uj zJ9HBCD&R(H%D0B~$+#*mI+$qI|d#IUd>ThhIItnS-GMUd2(>?NZK& zy#{eVX1{rMHs2%P(l?>qGmp{z@P$k#O}(H?Pe3`DR!g*@h~t4*tqJrCul}Ao_8+VB zS*74fHt<`lau=o zwVE4le)J#gZlCps^v_@HFS@nVUAnl~UUe6yYinw2?Mw5`w{GpmtALKJJ!%yQeeGKt2WODt{%*Sb!R}0VVaRUCFdIFe zmYaK#-4zmV%Pi=^+VR$Zr;_dTjM!OJTc*?lw0}ZhOGoFU4eBu0N@}Z-4OB2WC`OQ|>c6V~+ zq9b400_f|lowcp)@!7%Q;VtR+@8apDbT^KU9(GrNQb%nEf!bAenxUJ}#pdE;Prl~!^lTSRlPu zko-fT_cw*4>FuV)Z5r3{IY64aL2fD)@Kfr(v<8?J)9_)HemR##f0_XpKvhlG!Y>rF z##t}7-y=8VAX07cHEqS1G-lv|G<{Hjc;CA-A}!qGR?69<}-07iq`t#gpFhgD4(yPW&13(l8!sUKf!$ z+CIWQo6_gg*2DU>Z>iF`sj{T74N3ddV?rtB%1_)jMSIJlUm!$1oyg)k94|NS6&-fk zEM^L<_)Z<*uyEWnG}lKS-Uk*{pc&HkiNx%yI=288p$v1K8K-S?R!34Dccv?i$n)i6 z1Y*{;IH~}IYU1nB(ks~%)N+%V=0Y=Jv721Cw2BT%a6UW%ocQz3xnVpZ4rnyOxI$_U zgc>gRGtO_~Q@=>aDQ6Z5&jeQN=P((a(;7G=A^Y&;{%l|9fW`u8e-JCkh1`*&$h%Eq z8T=Q6UK@iA>cPeOB3&AmqD$%&3}&#$4?N|r07UU}1*K;e38{rtpc>9e8Gbv^fZu1~ z6XEFe#@U&OWG;e{lY1l=_o62=P1Hea)P&qxF(#Jco!qNT^TvG1S$BlbsT;i%#;Pb1 z&73y=u{u;>hgLw(j01XxBslj;(ta6Y!4@mmXhl>#2LEgw5kap&MRcePP4S*);T_Gw z)GtcE(R_DKTjh|%mX)F_V1BmKgRAUaNZ~`#k7S@SOw2{UT^;>{s!u$ongUO*W?s>M zztwPBtJ1j2ShYJV9hJj=54R>Bks4wP9PT8bhg>*H*R{9o51ulH(>KRH!*tCwrkByf zTDGPY#@gLkn$|l#qK;8k5UYTMi!S)v!E19`C&j;l1ys1GIaE_&sU&kXa^RlD1jmZ` z{4%J(BfB6yKMGl?H@6_l-4#Tk#3wO{47u!DIt#}CllC+W_Uo&oubY{=`M z#P!#mRnT^WHHA>MJTew zL}bXO#*rJSo+~>1<8`WeslyKGyAVi52uuRGM=^V6n++`f2t;m$gSM&)3*V+rLA&TLQ&+`N4Z0e{^*WTqn{kneCQc+@3Gt}$Qbu6ZW&uzHZTSFZ+a_`YB zSFlG7PPLdC{`4qrvsDpwQlo9TrZu?lZUqY!lGb};JkBA;95$~x{%OpKE4zs1Kim{oV|(SPg`5y$X^&tbuF3 z8yXnk&+=NpjNA7yI?)>jhKV+=fX9fdPuL2(6XPx%to3P0Z)J;B6n9A{T%@YC6pK}q zb`2*KW_ZiY30R`BUBOYfOQJcrUSK9-Ce3+YM#%?r`dAqVqn`!;f_k{IvE$`bxa-XDnvj*+)WFT$jEsH# zPc^r^YYx678oSpi6n_S3Rk0?VW=?c)ZSXEg zUHSUMKshiRcyNHPXoi$jP55saAZ>WnQ{g`TePM5A;rM3;+@piX@CK-gW<}nhKbt!g zaUVYE2g)%!R~oziELis#b9!)5F?pLGLK1#I>(A(ff`eIMPQV(?<_ab@0Z#g9nLx<< z;CyslmsR1fiSHwa829IJucyq9(7asT`x~7ZTpcFnN>Htz7A`u5X;itl*?lE@s zzY5%%YF;$%Aj=W9shMPe(Nw$XH881+;^Ho(Fc;vEYLhIA_MZQ$KfK%%cY_OFm#OZ# zkdEikKnVPkkf@`3_#+5Bnn#T9Z}7{aaYtE>XHDPB1f^!NOC!El3Np3axss`6GqOeO zYk1W0AH^hXc-_O~aavMbi|x-rwjZJ*Hgpwrau0q20hjlPiTMqDojmR&+IGFU8OLfl zX)k6k$vj&U?G)0U4CRz3{JW>Hw@1{cKNL)dAR;9mEC)<4OzW-69uVT%h^7t-fj}1JAiHpl)WrQvWe}%x53P2ur zp>PTu8(mv_TU=UvEIu=*(k$_cuoW^YB=R4DGy0jvg-{IguP|^ar`7ty-o`Y6s>C=r z^0hQp>sITXgd7H{6iCVZ48dTHo+6GTBE^z-uaN($9yAW(pp40NJd zqg^6#FdA!72kAkDiM-D65n(%^XJ2@Km=AT`=eJ@2=@Ak+=7F1EuGgi>&YUwz7Tfjo{^Fz6o(|0q92?A87 z+~~VBH1Vt*d7rpfrnZOfyawTr07aF=DW;)dx$^2Y#d(o28of(X~5U*&ru-=a{!D8&5qp(uZ{WDm8Aa zna$Y%rQmPQE+C`4;ZgbC`Hop3Ac8r+M|!+#f}?-rSx z6)a@yALf?lnV6ltn^~2G{RLjY);Z0kNJ9nb6UeUE&b{WGGugY5u`|d3OhXO{-XLm? z<3UnlKz4@s*E!FH`D1fPM`osTPpM(JAm)JJ9VPsN1w{QriLyKs{Cjvn#GUN{5O}Lg zda9&=J4(Nc4~PDt*edHEiVq!siC@Z$tpa2X0kUUCI9H+D!oq-vI|tyOfP-poj&BAw zrPZg&^Fy&yDq}SH5_%|@jou?;$ z-~Ms)GIL1tUTgSYelZ(D9P7u%!uuQlBnSv>+a1XBfIm$cU}$#BD9Hdjm$aMccT221 z4>Es>Pt3J>k?p(92^-G08>4d_USvg@WG3-o@ysC6O|nXr#Dxl<^SXPUWEbhP;7BP} zie{6~4VYb+kH4r~-de~#lu0k!n@uX3tehz&qfIwwE?h^KcN`GOc1>^LyLwY+Wvjfa zT$%32H)EvSznOiI@KhWE2L!xd+$fqXHsV9|tQahclaZKb#EU<0zr5MYU1=FqE7lm) zN(UGutI?(>Ht4@%nZG&(&AhX7ORiL}O==e3?0r?K&dUKd+YW5@Ri(<#k5|UkX;Ljg zCwDsOU>$yl&bD0Ni!i%tOI9B7$Zjo{oZg8F#vO>&;qnK8*@gCn+j7C&W~0nf#Wg;a ziknh=z$<8k#hk*k!d|%o;>sA~q_?ywpE<2A%T*TX?|Xm#nUhJv8I07SRshH$aNWP` zsO2a;Gq(X5#`7^o8~oab%8bk-a6RHV_)kU>a^|W!q+&$wvk!6ncMjK|IALGi_I0BIU&7 z{H65WoyQ>t6@DT&Z&05y;@jNm$+FcPTQggxnQ$Eoe~Yj`mom{Xv6rj)!kMJ_PX+4C z-@T_+dglvBw;_M=MINs#J&Ubko%#Aki6DHBa`>pl3fL=={4Cf9=N`99n0Wg+nj9y&!|duBehQ9N1vlfi71+WxZI z&P0#16A~_mG5!-~2YlY!(A0q87y6sas|VB0KN46S@Ndp}!M|#mz`uC#xU9pCEd9A{ zhQ9kd?tS4&sz2MB4svC&ZR{#XKR4M>w$mp+7#Z0A_Eq|OR-%SYXlk7(Gn?D9J2g%g zhZX$6?>~(06fgSj6rm&T6v;5|6hl$&6zNyPA&T1Y1Q5lR7;cE78>0h6@xo>Uq6mO( zzf+Vj(}B)p7`KEoIE><-F;BJMAx>%EpFm*{ z{Hf&o6UY5&>dx9J`}-66{i*!pT}zp}Q~KPUdOG&~N#RZ}J^Zd^djEYBxVzJH7l^IP z=@3?kj(2Wz8V1|#%OM#4KlOiG3X#qb3WG6x0yE42F4P9%LtFn39D@H6-}^*p0Qcnv zBC4%_Acx?W6e7JL6h`B~1m=HNSq^htY4hR%>IgE2({GsXb!lMO^zTYqm3!GDSG zeWEykOSOTBY3m=%A=s6Yp*JMUXq=b8{4eoOpLC{V=nj2jFg8hGW*NYJwt@K6*598) z@L%G4pV)N+J}pn2(KpTy-me|gmkteC)x@(IV{cB?dfGc*0xaEK&HytPXCdiymX_Mi zS4r`;n0(rAzwcS@5-ft2rHHx#Z-3}Tf4K)+yJ(#?yiZ_bE_Zw zw+|t^Vz7t!^ZbY&JYW`l`0wnBA->;W^xIYrZ5u=L-#2v7L0*RrOFZddKzX!;?i9e_ z8a?S3$g(fuYxx;pM=S@~9G~QPWG*>0bE#u}VA!own-!V463>sFgF0gkmZ73=yB7&< zXx_RR8Vg-Lw6c;!Hv?X(xj>plzmLs2#mxY>T&^P_Y7qf-LhKQ-W16|N3pQd7x4(ds zBKC7J^I$#dj@*Z5@xAJ_L{$wileB3!<(i8!!AR1YPiIb7{k}astXCytM2sPMe>2XSv@QG(TsFH--6ViF}I-z9IaJ z`@&p=BFQUnV5Z5pRaWAWSExZ4Vwc})>FvwV6tXsK>CHpNle$;|fCrvSdmLA*o5{Ob z;UR+#^;)R}0nszZBYO7rE$Sr;sRXwG0zQwa@bH?(iybqMW-Z#G@Y~91{M+LdWaIS7 zWbK+m?2!49mUc5&k86+GMt6IchGw_cxgR%B;&VfW<3p~zE>)oC-}VWy0S4&_EL-Plo91PX0Vgi?SHqyU zG8VjP1fJ7HT0FoX2c{nzEwuItZYR2vcj>#*1#9ZlUkq4twj|xyQrv3iGH^Xz{-l(~ z$A`r`d3};U7Fz8rzIIG$W9_hbk~`QnvwuCmau_ntXXQDYqLq@$JMVeh*%}ujGq-r{ zW_Qj4a&Ep^KlqwnIuvf)ErSQD0d`;Ud(J-B^q4-FWb;&~S!`b1*GB3VRSBsDAWt<; z?e2QgmpUAtpRY%!q#fMOU5+VObJQPt)VC}w+=8a}`W?DO+4?6sTLHu8w;nD(q&~{v zNn4>2um#B^ugx5qN8z*ew(|BJTFAF>i@VR}q*uN7Y{Lg`-^ey69&TV8C!ghJ4T5SM z8}R}2S6`RQ=lCWy2hXoEL05v)GOewyjjdO!!cUI(PFFQoLFw+Lx&GVby6mRw>tWj& z`j-XzE;~-zr=13JHw{~7267h-TZ0C2sS94$WzZS7rNmJL&o)Y*)%aYHhg`N_M+>j& z+~jAnC#SXcT(4*7UKHrECwNv+;1BRAotoeDO4AQ$8{Tl*^CPA6MGFTEr{`xH17sXr zbZ^K2wL#D}3?Rif+)jF}X*30JV=`FH4DRvUSAb6I_o|)=6mO1~U{72sw%%X%W@6_M zFoB#r*pZNSkhLe9?UqaL6~N6kx?4tu%bDTcP5jOa5|SD24M&0KM;aLFd%5!2}>RVehPCQ`PC3 zP-1z_(+wq{J3Q`EssGhHlgHy)Nsqep8+Dv&22s7M^h$Aju-@!DX=7F_F_oo zm@wUmCJYM56j&1rdKIN|%$Ss-^OGbYqA`}loL&~G98)H#XwxLgF@vOgk)2e|J0{KO z?Ig)5g9frO*2I$DJCR^CZqmRv1Aj7Rmc)!+L@6T^CWuHf!IVWeXT~?tAKfTPI>D4% zH|N4P@ezhml5B#hknUGp*ddDdB3{f-QskCCLK$PQXSLmnBANAY;U%HHEC?BpU?#oV zxKXCzpr8eGh~On*yg~^X&|s9k+ayt@A43TW=upB-!g>`GGGM_>dbhnrnMQ!}5zt|T zmwe4n zecL{wOrt^xzSemIFZtMumWY82X41DU1!Wo?%ICF?9K7TcFDW7hDi~$Iwjz{iOsLM+ zI%@Ee2woOM40JG)er-)C)7VhZf;zhJl89cRL<~$Y%KmKwDATwvxdaW21NRUkQ;8~= zg&h2QhEQhy6T&Z6KOR9DuPq83(G&fsMY(2}1qk>t{!M|@vb_SPEqTIUu}sF zda~oED$z?HuU*hQ1Rnpl8(YrI;^6;v=-V{zGcG}WW0O6^(A13o=|TXt%H?Nj&}^*7 zv3RS?XYH1#)C~7dSQ-xER$tQp8Vx%K|DmC8Gq{{wg2u*Mdx)v28I{cc-w8W)jB)l5 zeNuJHm=PWP+lLsYaUXI0|0ndFITY;N27>g;U6~gL>4z+_k{v^)LUwoeN7gSpcUsoi z&hagQ-?f z6P6z5A2z$E0232H8;{1i!ur_(K3bK-uvevVDx2NOeA+yF+qq{O@mI~wKj1-(cyf{x=ge1W<8BtL9!h$&l0^Qr8>jAcF zLr@C2137`nJCZeSXb}-Lo+Pb~&pa0AZLfv0tSkIUXfQsNyjo8r`?yVpn1w(?L?(u# zX9q{*{mkTRTQPBT)X*ia{xf&_=LOV2l3<#-$F~;z)l6zxq(4M9+h)5)_-ghW{afqDUJ-%9fg6aW%fwhm zMu6pK#RkF5h%sE~)K&iZFB6y^pH*EWB)oW?x*~^dCH6_b93~m z`j=styw~5{y-1WqLP}>Exja|JniJcaJMdpcYS$$De%h9rba`{t@!+i;(o2 zDix9W5$Vt+;ewH*h6yKw;f;=%9<4ht$Y%=Ah>U>QFE7AcCPrBi^_CsRG7w z%SA^$lJ~J?}Wqfc@I3eJp$^pGfe`(7GldsW0GuhQ+EOVE`qrrRj<^|FUqQ;^eGsDvsB zh=^)GrsKMJ6Um!B0?j^(ej64ed(%M)`!O&gGzoS2>xQM$o~Fu~+;ivs?QTR`c|{cr z08{Bw!~8eK$&5sxiecy*CrMy@Uc)fg8QowIR7FBl*Fs4TUXjsi(e+3po$9QNO3)NG zjL)o1%B*?Htm1y%l22QLC2vMDSK^?Gvd>LP^+(6g2WK~JskF(Skm2UZh7Kzw_^WR9 z;34Oq-_*jmuQXc|T^mCK1v?wa*9ReAbt~4`mDZTS^zd%!#A-D$Bye*hfBCT>vLLJ= z^5tjH3%M0(S#-g2=Kbr{C|}7p=b(=A?3&dxDwgH{(b&)jwEtIAZHc$Zy+9xG7{(JyFGcsRPR$ar3Ba`R<~ zuh*~a*EfcAb({qeJoy|{YUIhPKoyJ0txltvUZ+4uubG@gHMmImJ(=1$M8fZ76Cl28 z-PP9chy6uk{^+QU?t#O$bcSq;qa#(Km_hErAT0*@`1J}%@8VL34!Q6B)b$MQQ5 z(%3$kfuR^IBjdixu6kG4v*g75lf40qmX6XP`WVkLZCcDYUFKS>IA6E}I*j1;b`>jZ zaX>$U1bE;7l15jTO@;xjY=iACZ&4;$JT`WJYutiX5_RlNOP!mB)X*?(thj_NV6xlt z_G0oJ!M3*V9Lz|QH4*3fK@#v8?_(^uxsehwqq7X&Z{6YqFuM);^!6H4b{gB7?l~Im zNhJmChD%-rmfQV0v`8{(?Z!jinC{PuIv2@(2!QBXul6srn3j=S{4}ZuOo3!Y&Yx;y z7ra>CA1i!5Um}RFYbrCKJ8WOGRgI@6{kPW|xqPeuN zJ=oSO+tn*!bok9MaziL^WKc&bF3Rx<$0H(od*N-1Z+e=r$j1Q`Ux%G$GW2TBL}7!u z+n#ooHnyXmmY0m=6t**~RaeCia0ugP^sqJf09QZBZGVZe;%Up2?-a7|VlpX2uX^R= zAg{uY{{zSUX_ylfu+Z-&=n9wmpcjUc9@*LmS66kQbCzD`vjn_raK6q&u>IIV1d7-Sg7*+saNX{Tom-V%Ks%h+FUtIzd_@0su7$1)ih z)WoWIpA;PBN9x%M2sR&&Hb*nWHkdo@xK2?+igy$OblB`Vk@z}>IFGcUKTf?DT`rR! z*I=f`pqF{y8dc69PeVz>G!9Shz#mz|yK&6`Bmf3hZKsgC+9MML>#CWeO)uBbUw#W* zb7&*u=Z&%27Iz9JdqL0JdFGH+=c}IAqiGz2SaBlHic{V}^oXKRYJHSc{ZX_4a#dmt zp^L)K7OmUE6Zncr6gIc1uhx7#CW&-98*4AI4<5XP+Y!#v@W|Z5bbw-|*fjj!k%qpw zL-<`9^;4uFj2s929-=%4AICV2+({JnNDIx28&5GyP2vX)uNwm8vQJyLExx~Tr$Fz^ zaDH(19CdCJSKKk$Le3^)pFM4mvtpOjE%8N%f@x_2@pF~JDx}ueG0(4&Q;3}=73MzM z37v12!Cw(0Ho?90%7|>7MN&>V9pwl}MT$g-@T%c3Gob16h1w4z$;)&PJNT zR4b6kPuq8gp{m;y50ezpi6cv4j6pg@$g`1U=eT+98lCUqm(WxGSbJN}iGBYuY||8# zjymEQR?(t$e0fSCbpcMa9?e8Ui=`esHHhFE)jYULW5cTQl3bR?j0Y+221QqpjKrhP zYzw<1sWJYvat0LvpKvjA@DxQ&2##c3$^8I^pP-1C}0k0HN(pe#&a z$(Ig`e%1w|Ff$~!SGlNs&wUZIb9Yf#EebsNKfT6KWO=f*vLYkF^MZ_q3 zGzblnVdl&^5gUCGuR-2n{NwLH3$#MOu1^uQIQ_0dWXS_)Z6hixkC?T@k?P_V;2l_Z z%w!#+d%ebv z8p80su^c6uihKOnLWE_UQoKW6AAyTRf)Edn$y?mhzwQ~)5~V$P*pn1ojps5NE_%<^ zbFiRSsdRmpl}D-Q3{X;F__*k7U-xpb*#>1yAiRD*YX~F`oEH^u&2m`MXP=C5!aMb# z=!rT?dTpFKZ{UI~*bHQUt|R(1^98GbCDq|K#9UdtFQJRRgN>u54;*R*7AOz7jQmo_ z!(?P#QQw-l)rsR?f4ssf`9S;JcL$v+M#&*NGYzZo&}I5D8mBnEbVG+tI6h}CKZZ>T zixhkJ*D}LBuUGY+kCiXG;h^5VJhg7`u%!b%@4~1;3S~k{IthX^82$Lv>5#MV3y;yE zT6;Xh`?#m(mEN>Y*F>s$ywfmNzKMG0LOuD)));fIo+Lkd`^+Shl|i{rtTcysrVne=a~;88suQI8e9I2`;OdV)!&Y7mDlC!nUoHS- zy1S&7dE(^!dUL7oXN*j1S0wI`!V@6Ix|OU2d%oyR{-`qPErgWsyzOlwL$heK>f=}a{0~5 zhWNJKYje;Q0`Zve8cVm)%OHde=U)$WappOaf!U_SbL| zx|%Rsj85}sHWp$WRqlAPG3w9fv2*pSl*_{O1K$r+F22aa$N6T4xGd!{snqv!1r3c6 z9e4ga&ag7l``SkVdfP&U#R^cCLb;J{fx5^>bwz#K&B%Jgy(`!mL7CRdh%48aBwsRA zw*~U_oorVgJ~?))A5uj8T;6#l|3r&d!B;|2grwOJxWl=*m7yAV$*=x_?UOTpIe>J{ zCof8>6J71Kryd~uXlctYMwpIf+u^NKj${}YCi(bC&nC0TMZkBMm9SWj+3&h~!Apqk z-kakODx(0Sc74W5cxD{Q1G-1o$>Bj5u`U3#r&i^f@n(V4`YtC5)*_au7a5T$6GasJ9`!$YDshO~>sU9i^{tFO%ar7XLdF;_;==#vxH_QhnP8@pkxkVy2lt_qx_ z>xtUMx9kk&`Psmk-9C}Q9_4*X_rs5{WxSehkzpO(In>lC7Rn|S@W^vJjRN{jc}`h; zZ?@v?7eF=SZeI(CpXOa?*@`*udkiBbPmteHB#v5fMeoYf9LYH*6 z-wCc!ujl)SixT3D8^*IPR>@l?vvsd(q?Z}Qcr;3${+`?$(!A1evnM~kO<<~>BtQvf zgzAUzV*op>6B0V|7gS%@2JNKae7}qgjH4Ore)dZ12xYyko@vNQ(P|zebT6FRFoh9~szrKBBsX47Ym)QAO#P{{`~jwn5;L@UaX+y>f-y72*z#WES?FGJ#l^@If@%y!t5{hmCHP) zp=*w`+ddejcZQ5IKI>_0eE7mk0lV(RT#}qD!|4w%D|Lt9g?n*Iw#4yV8q_Q=>-U_z_zvAzYwZe6tC^&|O&2714EmVUY?B<0OUbs6hKkNeWz zmmIm%DFR_F?}atGizM6>yUq+Ee#3Xx*QhP^cG3@LpZO^!E|*|`Vk6dkNv1~Z<{_5b zg!Azwv%q6wy{6Zw3s3OTtvE5Scs6OpYcXP@8R(K7Qyf;07Qc`AD1Sd1K_YvbD)K3T zhBn$p#!vaPl{hb+q~X+@t0VntI5Q@t;T#|M*J!=p7A@Q)fX@HD610@Y9tosb-r zhq3d0kBL-XbAG&8k+|x`xW;v>KBU(07HOs)bJT4ssd^QYf{noxERI@{!kHhk8L|e& ztCTKDAqLwuWv17+x%gm(Hb9m8FuqTt3(h9jNufO@OX(^iB&b6H)3BXY*snaO<**!X zT0C)1>8bnYmU^!bP?Y}U*1OZr@=fDQUCevRW+oafXX3eOjF|JJS-2p^v%}L7q;n1$ zKT8Y!8r%hAOVt!PC1$6bQkMf%$yo9AnOkVc#Mff0U9uKD=`Cgd_a?qie@=YuoXrjI zX1raha#phpC^z7nOOXV?NwSBtu}JdO6Y~4IsZ~s-rPA-)3iOS`wm-FcY~qMLfF|<8 zBGF49UUYZl)h^kemsHqjLP=C;zv|&Tj}UaC@sA0=+&U-ZZhS3W!a$%$#=f0}+dCq4 zlpkM+ODv=RItCh^`!K6qex`bN<2WIkoevv#xhx7kQ4dyu!v-LU#yxPB6H`w++-RpD z$-x9arO@iL`ZOixtNhwaxy^Dh%}5d!vScyM+T9f=uNcb?)vat*UYQE!4})cysD_Gz zPn0RPNWBZc2X*;U2L`jn!!%z{IJYATamrhM3xP!+mD7RNTYdMqo05_9JYhsko|Z;+ zpo?<-wOPhH!a=JwOX&Elyzq_-;wybTHPkl^L_Y6hikM**^UdMx@pUcvMp7{ zhB%xbU5=QWfW;tLZ315$#uaYeG`w~J-b7l=;b%H5eESY#`sxD5G{+6+r^alG$2||D zm}&-oAQ-?1AeZ>Rj0#||-dJs?f%bpi=jBCt3N&Hnd{zh-WPDb;<~@?J!PuZZ=mnfG z6zvq6!}|VY1Ftx?mTtD@z0kYGgANBL<8oy>)nZI!_D-=LqL`B%%4d;{I50dSXs`XuFyWgX`7pk>1*hW;B***@nyA5ays1C*kn} zRKuEGg*KtZ@)8(;*#b8t>vJfda)R{jT9B{c)gPsLh7iR>Uqr}^GH6P^k1L;{9e%FLQWF!Ay*Q|Q`oqoMS~4~!_#L!yn&ET_vO zbL+Vvw|+%9rl-cmEH$}xNjk6mW{NN7AeA9I?)0H;Ewet+Hgyl{91e`o!HIE(+~w*V zs63$6KI+KoTP2g38g6yrN6vXNBCLst?l;d+aj{a@N@RDbe3BY$I+j}e_>IZE`ZO2( zC+N!QkQ|Jxa!TRmcGTu}X4bik-gKsHxv^VokeY6N1Yz**l(dW_lEr`4rQ~kAiAS#S zoXAs6*~*UtuWa^Yb+F8C?CEXPVa zQPNUwwN_tk$xS`?PceC&nEAb^n5F&$M@k&1SdI3!6zdueQkTz+(ik+sH>6U)cX$8! z4Jpj~x_obFV{2t;OMicc+>v_mkr4^}9IFS~Z3Hh5$7Avfk{tB)eBp$GBwn&QC~4$f zr**hAH0sXot}IlUq5{9Y!hZinPJTH)U~|>spe-L(S{IcJnLs|_>sVQBYL^Gk>t$Ct z+wOqsTYcmi^L(|lKJIw2yj>9@Q?iPNrL$27lZya~7s{lsV4t@_ zn6Q{Yrv!lQ%mDxVb0*05Od$Kk^sKDj-911miU<5GE*yWEZ(KqP?Jy6$1KZ@4^zu2Y z;24Im%L4a%Ua^=5mXmMI;>l*y<#|)HvlU!~d_oN6yf}Bgp7v-2eI#E%j4_h;?J1p% zfb;GP43Z1fN+Y1MSThwA5<;US6tex6x7fJbBF>5{&T;&<`5VzQyy!HYx+bQJ` zeix^~B>(z!0)AKVVbCc~+Y!vvyK1Zr-^X?&Fa?N$Um3IB({IQ4Fo*_v`{EdvcGFB4 zKO44upCU(E{ZT4b+~Xw&bTCoYdlWP6$JXj!1JPL%b1(xWQ@bKa2L_O}V2!Xc^U>Oa zjF$t)E%MTrEYZu!+FRIzv}@%F9ve z6=3%JQrD$jg+`fN>-H4JLI1~uGAIZBzu%fb_Y9VGR=E94E2j7|LN9Fb#g9AMgZ3mAk>8q z622GtjUT+hTm5{%$@tI1_v}7=_xl`w58~bWOLMf8bSDFfpG@0}q8W7ZhfiMHHwcK$ zzvlR`F6l8otv|CtUfYc_o0BJ`DO6O%HraG;%B@wxYuCN-?IP~u2bwot8FGp>Iao&( z&P`3w8$K+b_j*2kz~?Cn4p~QcVETf;ST$K`qSy3+490c1UgfFTHhP80ij=T)Kg#FA z`dBmScWcf`bai;H{MKu|lb;{d(lP5KFRXAZC@K|qz{!_BlVV407u@SIq0kH!Xsdl5 ztcEuDA>F8F)8HuMa0T%k#V0MeHHoz)5~+W4X$3l*^6NTf!Y-eVUEy=*g4da1x9`7P z@5LE!)5GA^h7J(9ervo`{4tmk?L=OC_Ee8OVoyTbHn@9DndSzUY_a_)t5wAf3gTNn zWWQ$^VBdEByS_yL`}W?kKNqtEkh~dCV84O4jT!;v-|K$NC}Siue~SO9Mv#a<*R-%> ze_uWnAwK#7;E_`l@2rP>s!K@ysmDgg1arIn*~2Md6VC$#Idxj{3JRUB2lmyJ^z6L} zmaqBh+R*fTKJbb-u5st#8{;zBn2hBR^Ytf)Sk*g8Eu78p4_U<;Ix5*&$h}StN;_C z0Mq^FX#`oy(h#(=v@^7{(^hnTZ)mFt840=+CBP#A3Xi}cinB)7BP=B)RMjfO8D%P- z4?*N49emD+zH^Qz8E6zC9{lqL=B>v~Y|O`!o7E$|NKnc{M&<*eG!K)xlxFO|^2UXn zrjg=&R+8sJz~y>Saj|FA!0puLOQ$iQiksz$0tG<(D0INB`uYj>$B$J{-z^)qM7~3b z9@&y!8xkpF43$EBmCP$ZRLe;gKJSdqsXywfxy}?tC(y2X@)mu%*BrQlckZB^8uUg} zPLYEm-s-95vZa!FWRn0bMbG0P1xBpa&)b0YhjzO@hU>>bZsgbHx=x=T`HHt|GYJsz zQ_(!k9C17B$kB96v10myZAy`sSN#J@uF7cO%KN4=toO`LhY~vP9A~TbD-kU@Qv7&^ z_jiQ>q9Oc_)s;;59}f?acP$zGIN=6)LZ2E3;BdmYx^~rk_3>Z!sOce1B!0?If2KMK{37$59gZ6J$#Qu)(`^Oo`Wa7_*S@a&^kEzAqRe%5d0QtQ0=g}{I zr}}R{{rnx`_iGsBgT|jnti&C}?^mq9EB`)7LoRcF9*REb|YCOIZonM-X%*@V^G|rA0sv^6Gy9y;w4) literal 0 HcmV?d00001