patx/veela
Its the Wred framework but magical, and also slower ;)
Commit e9b7459 · patx · 2026-10-13T20:43:19-04:00
Comments
No comments yet.
Diff
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..da34f9d
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+branch = True
+source =
+ wred
+
+[report]
+fail_under = 100
+show_missing = True
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..536cf1b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Python caches
+__pycache__/
+*.py[cod]
+
+# Virtual environments
+.venv/
+venv/
+
+# Build artifacts
+build/
+dist/
+*.egg-info/
+
+# Test and coverage output
+.pytest_cache/
+.coverage
+.coverage.*
+htmlcov/
+coverage.xml
+
+# Local secrets
+.env
+.env.*
+!.env.example
+.aws/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..97e8f2c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Harrison Erd
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0bc0699
--- /dev/null
+++ b/README.md
@@ -0,0 +1,179 @@
+# Wred
+
+A tiny Python/ASGI backend for HTML and JavaScript apps. One file. Standard-library
+JSON and embedded Uvicorn. Pronounced "red".
+
+This is the local `experiment/embedded-uvicorn` branch, not the published PyPI
+release. [Build instructions and experiment results](experiments/embedded-uvicorn/README.md).
+
+Your API needs to return some JSON. It probably doesn't need a goddamn cathedral.
+
+## What you get
+
+- Async handlers, explicit HTTP methods, and fixed or parameterized routes.
+- Headers, query parameters, cached body reads, and JSON parsing.
+- JSON responses, custom response tuples, and a small-file helper.
+- Callable ASGI response support and routing relative to mounted paths.
+
+## What you don't get
+
+Auth, sessions, databases, validation schemas, templates, form or multipart
+parsing, CORS, CSRF protection, rate limiting, dependency injection, generated
+docs, background jobs, WebSockets, or built-in streaming.
+
+Use existing ASGI middleware for CORS, authentication, sessions, or compression.
+[Starlette's middleware](https://starlette.dev/middleware/#using-middleware-in-other-frameworks)
+can wrap Wred directly. Your app chooses its dependencies; Wred doesn't need to
+know they exist.
+
+## Install and run
+
+Python 3.11+. Copy the generated `wred.py` beside your application. No pip install
+and no neighboring files are required at runtime.
+
+Save as `app.py`:
+
+```python
+from wred import App
+
+app = App()
+
[email protected]('/')
+async def hello(request):
+ return {'hello': 'world'}
+
+if __name__ == '__main__':
+ app.run()
+```
+
+And smoke it:
+
+```bash
+python app.py
+```
+
+Open <http://127.0.0.1:8000>. Use `app.run(host="0.0.0.0", port=8080, workers=4)` to change
+the binding and worker count. Multiple workers require a module-level app and
+the `if __name__ == '__main__':` guard. The default is one worker, with h11,
+asyncio, and no WebSockets
+or lifespan. Wred can still run under independently installed ASGI servers,
+including Granian with `--interface asginl`.
+Unsupported protocols raise `RuntimeError` without sending HTTP messages,
+following [ASGI guidance](https://asgi.readthedocs.io/en/latest/specs/main.html#protocol-specifications).
+You can also export a middleware wrapper as `app`.
+
+## Routes
+
+Handlers are async and receive a request. Methods default to `['GET']`;
+path parameters are strings passed by keyword.
+
+```python
[email protected]('/users/{user_id}')
+async def get_user(request, user_id):
+ return {'id': user_id}
+
[email protected]('/users', methods=['POST'])
+async def create_user(request):
+ return await request.json(), 'application/json', 201
+
[email protected]('/users/{user_id}', methods=['PUT', 'PATCH'])
+async def update_user(request, user_id):
+ return {'id': user_id, 'method': request.method}
+```
+
+Different methods can have separate handlers on the same path. Fixed routes
+win over dynamic routes for that method; dynamic routes follow registration
+order. Registering the same path and method again replaces its handler.
+Unmatched paths or methods return JSON 404. No automatic HEAD, OPTIONS, or 405.
+
+Mounting at `/api` makes a `/users` route handle `/api/users`. Routing respects
+`root_path`; `request.path` and the original scope keep the full incoming path.
+
+## Requests
+
+| API | Returns |
+| --- | --- |
+| `request.path` | Path without the query string. |
+| `request.method` | HTTP method. |
+| `request.headers` | Decoded header dict; use lowercase keys. |
+| `request.scope` | Original ASGI scope, including client, scheme, and middleware state. |
+| `request.query()` | Cached dict of string keys and values. No `await`. |
+| `await request.body()` | Cached raw bytes. |
+| `await request.json()` | Parsed JSON; an empty body becomes `{}`. |
+
+```python
[email protected]('/search')
+async def search(request):
+ return {'q': request.query().get('q', '')}
+```
+
+Query parameters preserve blank values; repeated keys keep the last value.
+JSON can be an object, array, or scalar. Validate inputs and convert types yourself.
+
+Authentication middleware can populate the scope for handlers to check.
+Starlette's SessionMiddleware uses `request.scope['session']`; its cookies are
+signed, not encrypted. Wred doesn't supply a session property.
+
+## Responses
+
+Dicts and lists become JSON with HTTP 200. Bytes default to
+`application/octet-stream`. Custom responses use
+`(body, content_type, status[, headers])`:
+
+| Handler return | Response |
+| --- | --- |
+| `{'hello': 'world'}` | JSON object, 200. |
+| `[{'id': 1}, {'id': 2}]` | JSON array, 200. |
+| `b'hello', 'text/plain', 200` | Text response. |
+| `{'id': 3}, 'application/json', 201, [('Location', '/items/3')]` | Created resource with a custom header. |
+| `b'', 'text/plain', 303, [('Location', '/')]` | Redirect. |
+| `b'', 'application/json', 204` | No content. |
+
+Dicts and lists always use `application/json`, including inside tuples.
+Other tuple bodies must be bytes. Headers are string pairs; repeated
+`Set-Cookie` headers are supported.
+
+Return a callable ASGI response object to let it manage status, headers, and
+body through `(scope, receive, send)`. Reuse external response classes for
+streaming, downloads, or SSE. Use ASGI middleware for behavior across routes.
+
+## Static files
+
+```python
+from pathlib import Path
+
+static_root = Path(__file__).parent / 'static'
+
[email protected]('/')
+async def index(request):
+ return await app.static('index.html', root=static_root)
+
[email protected]('/static/{filename}')
+async def asset(request, filename):
+ return await app.static(filename, root=static_root)
+```
+
+`static(filename, root=...)` returns `(bytes, content_type, status)`.
+The root is required and accepts a string or `Path`. MIME types are guessed;
+missing, unreadable, or outside-root files return plain-text 404, including
+symlink escapes. Files are read in a worker thread, entirely into memory.
+Small files. Don't feed it a movie.
+
+## Errors
+
+Malformed JSON, invalid UTF-8 query strings, and `WredRequestError` produce
+JSON 400 responses: `{'error': message}`. Raise `WredRequestError` from a handler
+for application validation failures. Unsupported response types produce JSON
+500 errors; other exceptions propagate.
+
+## Example app
+
+The [notebook demo](https://gitman.io/patx/wred/src/example_app) includes signup,
+login, refresh cookies, logout, protected note CRUD, and rate limiting.
+Those features belong to the app, not Wred.
+
+```bash
+python -m example_app.app
+```
+
+Open <http://127.0.0.1:8000>. Data lives in memory and disappears on restart.
diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt
new file mode 100644
index 0000000..c05cf61
--- /dev/null
+++ b/THIRD_PARTY_LICENSES.txt
@@ -0,0 +1,87 @@
+uvicorn 0.54.0: uvicorn-0.54.0.dist-info/licenses/LICENSE.md
+
+Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+h11 0.16.0: h11-0.16.0.dist-info/licenses/LICENSE.txt
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Nathaniel J. Smith <[email protected]> and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+click 8.5.0: click-8.5.0.dist-info/licenses/LICENSE.txt
+
+Copyright 2014 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..e38e901
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="Wred is a tiny Python/ASGI backend for HTML and JavaScript apps.">
+ <title>Wred-embedded | Single file Python/ASGI framework and webserver</title>
+ <style>
+ :root { color-scheme: light; font-family: Arial, Helvetica, sans-serif; color: #702a36; background: #fff8f8; }
+ * { box-sizing: border-box; }
+ body { margin: 0; }
+ main { width: 100%; }
+ .intro { text-align: center; padding: clamp(12px, 4dvh, 40px) 32px clamp(24px, 6dvh, 64px); }
+ h1 { margin: 0; }
+ h1 img { display: block; width: min(680px, 100%, 70dvh); height: auto; margin: auto; }
+ .intro p { margin: 16px auto 0; max-width: 800px; font-size: clamp(22px, 2.3vw, 30px); line-height: 1.5; }
+ a { color: inherit; text-underline-offset: 4px; }
+ a:hover { color: #c92432; }
+ a:focus-visible { outline: 2px solid #c92432; outline-offset: 5px; }
+ footer { display: flex; justify-content: center; flex-wrap: wrap; gap: 16px 32px; padding: 40px 24px; font-size: 18px; }
+ .examples { max-width: 960px; margin: 0 auto; padding: 0 24px; }
+ .examples pre { margin-bottom: 24px; background: #fff0f0; border: 1px solid #dc8994; border-radius: 6px; }
+ pre { margin: 0; padding: clamp(12px, 3vw, 32px); overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: #702a36; font: 20px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; }
+ code { font: inherit; }
+ @media (max-width: 700px) {
+ .intro { padding-left: 12px; padding-right: 12px; }
+ .intro p { font-size: clamp(18px, 5vw, 24px); }
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <header class="intro">
+ <h1><a href="https://harrisonerd.com/wred-embedded"><img src="https://patx.gitman.io/wred/wred-logo.png" alt="Wred" width="1774" height="887"></a></h1>
+ <p>Wred is a <a target="_blank" href="https://gitman.io/patx/wred-embedded">magical</a>, single-file Python framework for simple JavaScript web apps. Just grab <a target="_blank" href="https://gitman.io/patx/wred-embedded/src/wred.py">wred.py</a> and go!</p>
+ </header>
+ <section class="examples" aria-label="Installation and example">
+ <pre aria-label="Install"><code>$ wget https://gitman.io/patx/wred-embedded/src/wred.py</code></pre>
+ <pre aria-label="app.py"><code>from wred import App
+
+app = App()
+
[email protected]('/')
+async def index(request):
+ return {"hello": "world"}
+
+app.run()
+</code></pre>
+ </section>
+ </main>
+ <footer>
+ <a href="https://gitman.io/patx/wred-embedded/src">Source on Gitman</a>
+ <a href="https://harrisonerd.com">© 2026 Harrison Erd</a>
+ </footer>
+</body>
+</html>
diff --git a/docs/social-card.png b/docs/social-card.png
new file mode 100644
index 0000000..b1edbe8
Binary files /dev/null and b/docs/social-card.png differ
diff --git a/docs/wred-logo.png b/docs/wred-logo.png
new file mode 100644
index 0000000..bec2a6f
Binary files /dev/null and b/docs/wred-logo.png differ
diff --git a/example_app/app.py b/example_app/app.py
new file mode 100644
index 0000000..fe00cca
--- /dev/null
+++ b/example_app/app.py
@@ -0,0 +1,20 @@
+"""Run the notebook demo from the repository root:
+
+ python -m example_app.app
+
+Open http://127.0.0.1:8000. Users, tokens, and notes disappear on restart.
+Login is restored with an HttpOnly refresh cookie; API calls use bearer tokens.
+Storage and rate limits are process-local. Production needs shared storage and HTTPS.
+"""
+
+from wred import App
+
+from . import auth, notes, pages
+
+app = App()
+pages.register_routes(app)
+auth.register_routes(app)
+notes.register_routes(app)
+
+if __name__ == '__main__':
+ app.run()
diff --git a/example_app/auth.py b/example_app/auth.py
new file mode 100644
index 0000000..a8df158
--- /dev/null
+++ b/example_app/auth.py
@@ -0,0 +1,220 @@
+"""Signup, login, refresh cookies, and bearer authentication for the demo."""
+
+from asyncio import to_thread
+from functools import wraps
+from hashlib import pbkdf2_hmac, sha256
+from http.cookies import CookieError, SimpleCookie
+from secrets import compare_digest, token_bytes, token_urlsafe
+from time import monotonic
+
+from wred import WredRequestError
+from .rate_limit import rate_limit
+from .storage import users, tokens, refresh_tokens
+
+token_lifetime = 3600
+refresh_lifetime = 7 * 24 * 3600
+refresh_cookie_name = 'notebook_refresh'
+
+
+def same_origin_request(request):
+ # HTML forms cannot supply this custom header; cross-origin fetch requires a preflight.
+ if request.headers.get('x-notebook-request') != '1':
+ return False
+ if request.headers.get('sec-fetch-site', 'same-origin') != 'same-origin':
+ return False
+ # Browsers send Origin on POST; API clients may omit it but still need the custom header.
+ origin = request.headers.get('origin')
+ expected = f'{request.scope.get("scheme", "http")}://{request.headers.get("host", "")}'
+ return origin is None or origin == expected
+
+
+def refresh_cookie(request, value, max_age):
+ cookie = SimpleCookie()
+ cookie[refresh_cookie_name] = value
+ morsel = cookie[refresh_cookie_name]
+ morsel['path'] = '/api'
+ morsel['httponly'] = True
+ morsel['samesite'] = 'Strict'
+ morsel['max-age'] = max_age
+ # HTTPS deployments get Secure; plain HTTP remains usable for the localhost demo.
+ if request.scope.get('scheme') == 'https':
+ morsel['secure'] = True
+ return 'Set-Cookie', morsel.OutputString()
+
+
+def read_refresh_cookie(request):
+ cookie = SimpleCookie()
+ try:
+ cookie.load(request.headers.get('cookie', ''))
+ except CookieError:
+ return ''
+ morsel = cookie.get(refresh_cookie_name)
+ return morsel.value if morsel else ''
+
+
+def issue_access_token(username, login_id):
+ token = token_urlsafe(32)
+ # Store a digest so the server does not retain the raw bearer credential.
+ digest = sha256(token.encode('utf-8')).hexdigest()
+ # Tag tokens with their login ID so logout can revoke all tokens from that login.
+ tokens[digest] = {
+ 'user_id': username,
+ 'login_id': login_id,
+ 'expires_at': monotonic() + token_lifetime,
+ }
+ return {
+ 'access_token': token,
+ 'token_type': 'bearer',
+ 'expires_in': token_lifetime,
+ 'user': {'id': username},
+ }
+
+
+def revoke_login(login_id):
+ for store in (tokens, refresh_tokens):
+ for digest, record in list(store.items()):
+ if record['login_id'] == login_id:
+ del store[digest]
+
+
+async def read_credentials(request):
+ data = await request.json()
+ if not isinstance(data, dict):
+ raise WredRequestError('Send a JSON object with username and password')
+ username, password = data.get('username'), data.get('password')
+ if not isinstance(username, str) or not isinstance(password, str):
+ raise WredRequestError('Username and password must be strings')
+ username = username.strip().casefold()
+ if not 1 <= len(username) <= 64 or not 15 <= len(password) <= 128:
+ raise WredRequestError('Use a username of 1–64 and a password of 15–128 characters')
+ return username, password
+
+
+async def hash_password(password, salt):
+ return await to_thread(pbkdf2_hmac, 'sha256', password.encode('utf-8'), salt, 600_000)
+
+
+async def validate_access_token(token):
+ digest = sha256(token.encode('utf-8')).hexdigest()
+ record = tokens.get(digest)
+ if record is None:
+ return None
+ if record['expires_at'] <= monotonic():
+ tokens.pop(digest, None)
+ return None
+ return {'id': record['user_id']}
+
+
+def protected(handler):
+ @wraps(handler)
+ async def wrapper(request, *args, **kwargs):
+ header = request.headers.get('authorization', '')
+ scheme, _, token = header.partition(' ')
+ user = None
+ if scheme.lower() == 'bearer' and token:
+ user = await validate_access_token(token)
+ if user is None:
+ return (
+ {'error': 'Unauthorized'},
+ 'application/json',
+ 401,
+ [('WWW-Authenticate', 'Bearer')],
+ )
+ request.user = user
+ request.access_token = token
+ response = await handler(request, *args, **kwargs)
+ if isinstance(response, tuple):
+ body, content_type, status = response[:3]
+ headers = list(response[3]) if len(response) == 4 else []
+ else:
+ body, content_type, status, headers = response, 'application/json', 200, []
+ return body, content_type, status, headers + [('Cache-Control', 'no-store')]
+
+ return wrapper
+
+
+def register_routes(app):
+ """Register authentication endpoints on the supplied Wred app."""
+
+ @app.route('/api/signup', methods=['POST'])
+ @rate_limit(requests=10, seconds=60)
+ async def signup(request):
+ username, password = await read_credentials(request)
+ salt = token_bytes(16)
+ password_hash = await hash_password(password, salt)
+ # Check after the await so simultaneous signups cannot overwrite the same account.
+ if username in users:
+ return {'error': 'Username already exists'}, 'application/json', 409
+ users[username] = {'salt': salt, 'password_hash': password_hash}
+ return {'id': username}, 'application/json', 201
+
+ @app.route('/api/login', methods=['POST'])
+ @rate_limit(requests=5, seconds=60)
+ async def login(request):
+ if not same_origin_request(request):
+ return (
+ {'error': 'A same-origin request with X-Notebook-Request: 1 is required'},
+ 'application/json',
+ 403,
+ )
+ username, password = await read_credentials(request)
+ # Use a dummy record for unknown users so failed logins still perform password hashing.
+ record = users.get(username, {'salt': bytes(16), 'password_hash': bytes(32)})
+ password_hash = await hash_password(password, record['salt'])
+ if username not in users or not compare_digest(password_hash, record['password_hash']):
+ return {'error': 'Invalid credentials'}, 'application/json', 401
+ login_id = token_urlsafe(16)
+ refresh = token_urlsafe(32)
+ # Store only its digest and a fixed absolute expiry; refreshes do not extend this deadline.
+ refresh_tokens[sha256(refresh.encode()).hexdigest()] = {
+ 'user_id': username,
+ 'login_id': login_id,
+ 'expires_at': monotonic() + refresh_lifetime,
+ }
+ return (
+ issue_access_token(username, login_id),
+ 'application/json',
+ 200,
+ [('Cache-Control', 'no-store'), refresh_cookie(request, refresh, refresh_lifetime)],
+ )
+
+ @app.route('/api/refresh', methods=['POST'])
+ @rate_limit(requests=30, seconds=60)
+ async def refresh(request):
+ if not same_origin_request(request):
+ return (
+ {'error': 'A same-origin request with X-Notebook-Request: 1 is required'},
+ 'application/json',
+ 403,
+ )
+ digest = sha256(read_refresh_cookie(request).encode()).hexdigest()
+ record = refresh_tokens.get(digest)
+ if record is None or record['expires_at'] <= monotonic():
+ if record is not None:
+ revoke_login(record['login_id'])
+ return (
+ {'error': 'Unauthorized'},
+ 'application/json',
+ 401,
+ [('Cache-Control', 'no-store'), refresh_cookie(request, '', 0)],
+ )
+ # Reuse the cookie while issuing a fresh memory-only access token; concurrent tabs work.
+ return (
+ issue_access_token(record['user_id'], record['login_id']),
+ 'application/json',
+ 200,
+ [('Cache-Control', 'no-store')],
+ )
+
+ @app.route('/api/me')
+ @protected
+ async def me(request):
+ return request.user
+
+ @app.route('/api/logout', methods=['POST'])
+ @protected
+ async def logout(request):
+ digest = sha256(request.access_token.encode('utf-8')).hexdigest()
+ login_id = tokens[digest]['login_id']
+ revoke_login(login_id)
+ return {'logged_out': True}, 'application/json', 200, [refresh_cookie(request, '', 0)]
diff --git a/example_app/notes.py b/example_app/notes.py
new file mode 100644
index 0000000..1f8efaa
--- /dev/null
+++ b/example_app/notes.py
@@ -0,0 +1,127 @@
+"""Private note CRUD, validation, filtering, pagination, and JSON export."""
+
+from datetime import UTC, datetime
+from secrets import token_urlsafe
+
+from wred import WredRequestError
+from .auth import protected
+from .storage import notes
+
+
+async def read_note(request, partial=False):
+ data = await request.json()
+ if not isinstance(data, dict) or not data or data.keys() - {'title', 'body', 'pinned'}:
+ raise WredRequestError('Send an object containing title, body, or pinned')
+ if not partial and 'title' not in data:
+ raise WredRequestError('A title is required')
+ if 'title' in data and (
+ not isinstance(data['title'], str) or not 1 <= len(data['title'].strip()) <= 100
+ ):
+ raise WredRequestError('Title must contain 1–100 characters')
+ if 'body' in data and (not isinstance(data['body'], str) or len(data['body']) > 10_000):
+ raise WredRequestError('Body must be a string of at most 10,000 characters')
+ if 'pinned' in data and not isinstance(data['pinned'], bool):
+ raise WredRequestError('Pinned must be a boolean')
+ if 'title' in data:
+ data['title'] = data['title'].strip()
+ return data if partial else {'body': '', 'pinned': False, **data}
+
+
+def owned_note(note_id, user):
+ note = notes.get(note_id)
+ if note is None:
+ return {'error': 'Note not found'}, 'application/json', 404
+ if note['owner'] != user['id']:
+ return {'error': 'This note belongs to another user'}, 'application/json', 403
+ return note
+
+
+def register_routes(app):
+ """Register note endpoints; each handler requires a valid bearer token."""
+
+ @app.route('/api/notes', methods=['GET'])
+ @protected
+ async def list_notes(request):
+ # Keep this demo's query budget at 20 fields, including repeated keys.
+ if request.scope.get('query_string', b'').count(b'&') >= 20:
+ raise WredRequestError('Invalid query string')
+ query = request.query()
+ try:
+ limit = int(query.get('limit') or '5')
+ offset = int(query.get('offset') or '0')
+ except ValueError:
+ raise WredRequestError('Limit and offset must be integers')
+ if not 1 <= limit <= 50 or offset < 0:
+ raise WredRequestError('Limit must be 1–50; offset must be zero or greater')
+ search = query.get('q', '').strip().casefold()
+ pinned = query.get('pinned', '')
+ if pinned not in ('', 'true', 'false'):
+ raise WredRequestError('Pinned filter must be true or false')
+ # Apply ownership before searching or counting so other users' data never leaks.
+ items = [note for note in notes.values() if note['owner'] == request.user['id']]
+ items = [
+ note for note in items if search in (note['title'] + '\n' + note['body']).casefold()
+ ]
+ if pinned:
+ items = [note for note in items if note['pinned'] == (pinned == 'true')]
+ # Show pinned notes first, then most recently edited notes within each group.
+ items.sort(key=lambda note: (note['pinned'], note['updated_at'], note['id']), reverse=True)
+ return {
+ 'items': items[offset : offset + limit],
+ 'total': len(items),
+ 'limit': limit,
+ 'offset': offset,
+ }
+
+ @app.route('/api/notes', methods=['POST'])
+ @protected
+ async def create_note(request):
+ data = await read_note(request)
+ note_id = token_urlsafe(9)
+ now = datetime.now(UTC).isoformat(timespec='microseconds')
+ note = {
+ 'id': note_id,
+ 'owner': request.user['id'],
+ **data,
+ 'created_at': now,
+ 'updated_at': now,
+ }
+ notes[note_id] = note
+ return note, 'application/json', 201, [('Location', f'/api/notes/{note_id}')]
+
+ @app.route('/api/notes/{note_id}', methods=['GET'])
+ @protected
+ async def get_note(request, note_id):
+ return owned_note(note_id, request.user)
+
+ @app.route('/api/notes/{note_id}', methods=['PUT', 'PATCH'])
+ @protected
+ async def update_note(request, note_id):
+ note = owned_note(note_id, request.user)
+ if isinstance(note, tuple):
+ return note
+ data = await read_note(request, partial=request.method == 'PATCH')
+ note.update(data)
+ note['updated_at'] = datetime.now(UTC).isoformat(timespec='microseconds')
+ return note
+
+ @app.route('/api/notes/{note_id}', methods=['DELETE'])
+ @protected
+ async def delete_note(request, note_id):
+ note = owned_note(note_id, request.user)
+ if isinstance(note, tuple):
+ return note
+ del notes[note_id]
+ return b'', 'application/json', 204
+
+ # This fixed path wins over /api/notes/{note_id}, even though it is registered afterward.
+ @app.route('/api/notes/export', methods=['GET'])
+ @protected
+ async def export_notes(request):
+ items = [note for note in notes.values() if note['owner'] == request.user['id']]
+ return (
+ items,
+ 'application/json',
+ 200,
+ [('Content-Disposition', 'attachment; filename="notes.json"')],
+ )
diff --git a/example_app/pages.py b/example_app/pages.py
new file mode 100644
index 0000000..128b826
--- /dev/null
+++ b/example_app/pages.py
@@ -0,0 +1,34 @@
+"""Public page/assets, redirect, health check, and a rate-limit demonstration."""
+
+from pathlib import Path
+
+from .rate_limit import rate_limit
+
+static_root = Path(__file__).parent / 'static'
+
+
+def register_routes(app):
+ """Register the browser UI and public demonstration endpoints."""
+
+ @app.route('/')
+ async def home(request):
+ body, content_type, status = await app.static('index.html', root=static_root)
+ return body, content_type, status, [('Cache-Control', 'no-cache')]
+
+ @app.route('/static/{filename}')
+ async def assets(request, filename):
+ body, content_type, status = await app.static(filename, root=static_root)
+ return body, content_type, status, [('Cache-Control', 'no-cache')]
+
+ @app.route('/ui')
+ async def old_ui(request):
+ return b'', 'text/plain', 303, [('Location', '/')]
+
+ @app.route('/api/health')
+ async def health(request):
+ return {'status': 'ok'}
+
+ @app.route('/api/rate-limit-demo')
+ @rate_limit(requests=3, seconds=10)
+ async def rate_limit_demo(request):
+ return {'message': 'Request accepted'}
diff --git a/example_app/rate_limit.py b/example_app/rate_limit.py
new file mode 100644
index 0000000..22ecccb
--- /dev/null
+++ b/example_app/rate_limit.py
@@ -0,0 +1,40 @@
+"""Application-owned sliding-window rate limiting, independent of Wred."""
+
+from collections import deque
+from functools import wraps
+from math import ceil
+from time import monotonic
+
+rate_buckets = {} # (handler, client IP, window) -> accepted request timestamps.
+
+
+def rate_limit(requests, seconds):
+ def decorator(handler):
+ @wraps(handler)
+ async def wrapper(request, *args, **kwargs):
+ now = monotonic()
+ # Drop inactive clients so their buckets do not remain in memory forever.
+ for key, timestamps in list(rate_buckets.items()):
+ if timestamps[-1] <= now - key[2]:
+ del rate_buckets[key]
+ # Use ASGI's client address, not an untrusted X-Forwarded-For header.
+ client = (request.scope.get('client') or ('unknown', 0))[0]
+ key = (handler.__name__, client, seconds)
+ timestamps = rate_buckets.setdefault(key, deque())
+ while timestamps and timestamps[0] <= now - seconds:
+ timestamps.popleft()
+ if len(timestamps) >= requests:
+ retry_after = max(1, ceil(timestamps[0] + seconds - now))
+ return (
+ {'error': 'Too many requests', 'retry_after': retry_after},
+ 'application/json',
+ 429,
+ [('Retry-After', str(retry_after)), ('Cache-Control', 'no-store')],
+ )
+ # Reserve the slot before any await, so concurrent requests cannot bypass it.
+ timestamps.append(now)
+ return await handler(request, *args, **kwargs)
+
+ return wrapper
+
+ return decorator
diff --git a/example_app/static/app.css b/example_app/static/app.css
new file mode 100644
index 0000000..787478b
--- /dev/null
+++ b/example_app/static/app.css
@@ -0,0 +1,126 @@
+:root {
+ color-scheme: light;
+ --ink: #172746;
+ --muted: #64748b;
+ --blue: #2457d6;
+ --paper: #fff;
+ --canvas: #f5f7fc;
+ --line: #d7e0ee;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ color: var(--ink);
+ background: var(--canvas);
+ font-synthesis: none;
+}
+* { box-sizing: border-box; }
+[hidden] { display: none !important; }
+body { margin: 0; }
+button, input, textarea { font: inherit; }
+button { cursor: pointer; }
+button:disabled { cursor: default; opacity: .45; }
+a { color: inherit; }
+button:focus-visible, a:focus-visible { outline: 3px solid #9bb9ff; outline-offset: 4px; }
+input:focus, textarea:focus { outline: 2px solid #9bb9ff; outline-offset: 2px; border-color: var(--blue); }
+button { border: 0; border-radius: 6px; padding: 11px 16px; font-size: 14px; font-weight: 600; }
+.primary { color: #fff; background: var(--blue); }
+.primary:hover:not(:disabled) { background: #1944b1; }
+.quiet { color: var(--ink); background: transparent; }
+.quiet:hover:not(:disabled) { background: #e9eef9; }
+.danger { color: #a1353d; background: transparent; }
+.danger:hover:not(:disabled) { background: #fcebee; }
+.small { padding: 9px 12px; white-space: nowrap; }
+.shell { max-width: 1260px; margin: auto; padding: 0 38px; }
+.app-header { display: flex; justify-content: space-between; align-items: center; min-height: 100px; gap: 24px; }
+.brand { display: inline-flex; gap: 12px; align-items: center; font-size: 19px; font-weight: 700; text-decoration: none; letter-spacing: -.5px; }
+.book-mark { width: 24px; height: 28px; border: 2px solid var(--blue); border-left: 7px solid var(--blue); border-radius: 2px 5px 5px 2px; transform: rotate(-5deg); }
+.account { display: flex; align-items: center; gap: 8px; font-size: 14px; }
+#username { margin-right: 12px; color: var(--muted); overflow-wrap: anywhere; }
+.notice { margin: 0 0 20px; padding: 14px 18px; color: #244f48; background: #e5f2ee; border-radius: 6px; line-height: 1.5; }
+.notice.error { color: #902c39; background: #fcebee; }
+.welcome { display: grid; grid-template-columns: 1.15fr 1fr; align-items: center; gap: 90px; min-height: 660px; padding: 40px 35px 65px; }
+.introduction h1 { font-size: clamp(44px, 5.1vw, 68px); line-height: 1.06; font-weight: 650; letter-spacing: -3px; margin: 0 0 25px; }
+.introduction > p { color: var(--muted); max-width: 360px; font-size: 18px; line-height: 1.65; margin-bottom: 40px; }
+.paper-preview { max-width: 385px; padding: 28px 32px 24px 40px; background: var(--paper); border: 1px solid var(--line); border-left: 4px solid #9bb9ff; transform: rotate(-3deg); box-shadow: 0 12px 30px #17274608; }
+.preview-title { display: block; margin-bottom: 17px; font-family: Georgia, serif; font-size: 23px; }
+.preview-line { display: block; min-height: 34px; line-height: 33px; border-bottom: 1px solid #e8edf5; font-family: Georgia, serif; font-size: 14px; color: var(--muted); }
+.auth-panel { padding: 32px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; }
+.auth-switch { display: flex; gap: 4px; background: #eef2f9; border-radius: 6px; padding: 4px; margin-bottom: 28px; }
+.auth-switch button { width: 50%; color: var(--muted); background: transparent; }
+.auth-switch button[aria-pressed="true"] { background: var(--paper); color: var(--ink); box-shadow: 0 1px 3px #17274612; }
+.auth-panel h2 { margin: 0 0 9px; font-size: 25px; letter-spacing: -.7px; }
+.auth-panel > p { margin: 0 0 26px; color: var(--muted); line-height: 1.5; }
+label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 9px; }
+input:not([type="checkbox"]), textarea { width: 100%; color: var(--ink); background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 12px 14px; }
+.auth-panel input { margin-bottom: 21px; }
+.auth-panel input[type="password"], .auth-panel input[name="password"] { margin-bottom: 8px; }
+.field-help { font-size: 12px; color: var(--muted); margin: 0 0 25px; line-height: 1.5; }
+.auth-panel .primary { width: 100%; }
+.workspace { display: grid; grid-template-columns: 350px minmax(0, 1fr); min-height: 680px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
+.notes-pane { border-right: 1px solid var(--line); padding: 26px 23px; background: #fafbfe; }
+.pane-heading, .editor-heading { display: flex; justify-content: space-between; align-items: center; gap: 14px; }
+.pane-heading { margin-bottom: 24px; }
+.pane-heading h1 { font-size: 22px; margin: 0; letter-spacing: -.6px; }
+.notes-pane input[type="search"] { font-size: 14px; }
+.check { display: flex; align-items: center; gap: 9px; font-weight: 400; font-size: 13px; margin: 0; }
+.check input { accent-color: var(--blue); width: 15px; height: 15px; margin: 0; }
+.filter { margin: 16px 0 23px; }
+.list-summary { font-size: 12px; color: var(--muted); margin: 0 0 12px; }
+.note-list { display: grid; gap: 8px; }
+.note-item { display: block; width: 100%; padding: 16px; text-align: left; color: var(--ink); border: 1px solid transparent; background: transparent; }
+.note-item:hover { background: #eef2fa; }
+.note-item[aria-current="true"] { border-color: #b1c6f7; background: #eef3ff; }
+.note-item strong { display: block; font-size: 15px; font-weight: 650; overflow-wrap: anywhere; }
+.note-item .excerpt { display: block; font-family: Georgia, serif; color: var(--muted); font-size: 13px; font-weight: 400; line-height: 1.6; margin-top: 7px; overflow-wrap: anywhere; }
+.note-item .note-meta { display: block; color: var(--muted); font-size: 11px; font-weight: 400; margin-top: 12px; }
+.empty-list { margin-top: 45px; color: var(--muted); font-family: Georgia, serif; line-height: 1.7; font-size: 17px; }
+.pagination { display: flex; justify-content: space-between; align-items: center; gap: 3px; margin-top: 20px; }
+.pagination button { padding: 9px 6px; font-size: 12px; }
+.pagination span { color: var(--muted); font-size: 12px; }
+.editor { position: relative; padding: 30px 38px 25px 48px; }
+.editor::before { content: ""; position: absolute; top: 104px; bottom: 105px; left: 25px; border-left: 1px solid #c7d7f7; }
+.editor-heading { margin-bottom: 34px; }
+.editor-heading h2 { font-size: 16px; font-weight: 600; margin: 0; }
+.save-state { color: var(--muted); font-size: 12px; }
+.editor label { color: var(--muted); }
+#note-title { font-family: Georgia, serif; font-size: 28px; padding: 10px 0 14px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; margin-bottom: 25px; }
+#note-body { font-family: Georgia, serif; font-size: 17px; line-height: 1.75; border: 0; padding: 8px 0; resize: vertical; height: 300px; min-height: 295px; }
+#note-title::placeholder { color: #a0aabc; }
+#note-body::placeholder { color: #a0aabc; }
+.editor-details { display: flex; justify-content: space-between; align-items: center; color: var(--muted); font-size: 12px; gap: 14px; padding: 20px 0; border-top: 1px solid var(--line); }
+.editor-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
+.editor-actions .danger { margin-left: auto; }
+.note-date { margin: 24px 0 0; color: var(--muted); font-size: 11px; }
+.app-footer { padding: 25px 0; text-align: center; color: var(--muted); font-size: 12px; line-height: 1.6; }
+.demo-tools { margin-top: 20px; padding: 16px 20px; color: var(--muted); border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
+.demo-tools summary { cursor: pointer; color: var(--ink); font-weight: 600; }
+.demo-tools p { line-height: 1.6; }
+.demo-tools button { background: #e9eef9; }
+.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
+@media (max-width: 1000px) {
+ .shell { padding: 0 24px; }
+ .welcome { gap: 40px; padding: 30px 0 60px; }
+ .workspace { grid-template-columns: 300px minmax(0, 1fr); }
+ .editor { padding-right: 26px; padding-left: 35px; }
+ .editor::before { left: 15px; }
+}
+@media (max-width: 760px) {
+ .shell { padding: 0 18px; }
+ .app-header { min-height: 85px; gap: 14px; flex-wrap: wrap; padding: 18px 0; }
+ .account { width: 100%; justify-content: flex-end; gap: 3px; }
+ #username { margin-right: auto; }
+ .welcome { grid-template-columns: 1fr; gap: 28px; min-height: auto; padding: 25px 0; }
+ .introduction h1 { font-size: 44px; letter-spacing: -1.8px; }
+ .introduction > p { max-width: 100%; margin-bottom: 0; font-size: 16px; }
+ .paper-preview { display: none; }
+ .auth-panel { padding: 25px; }
+ .workspace { grid-template-columns: 1fr; }
+ .notes-pane { border-right: 0; border-bottom: 1px solid var(--line); padding: 22px; }
+ .note-list { max-height: 270px; overflow-y: auto; }
+ .editor { padding: 26px 24px 26px 35px; }
+ .editor::before { left: 17px; }
+ .editor-heading { margin-bottom: 25px; }
+ #note-title { font-size: 25px; }
+ #note-body { min-height: 260px; }
+ .editor-actions .danger { margin-left: 0; }
+ .empty-list { margin-top: 20px; font-size: 15px; }
+ .app-footer { text-align: left; }
+}
diff --git a/example_app/static/app.js b/example_app/static/app.js
new file mode 100644
index 0000000..c7a4405
--- /dev/null
+++ b/example_app/static/app.js
@@ -0,0 +1,330 @@
+// Bearer tokens stay in memory. The server's HttpOnly refresh cookie restores login.
+const state = { token: null, user: null, selected: null, notes: [], total: 0, offset: 0, limit: 5, mode: 'login' };
+const $ = id => document.getElementById(id);
+let listSequence = 0;
+let selectionSequence = 0;
+let searchTimer;
+let sessionGeneration = 0;
+let refreshPromise = null;
+
+function notice(message, error = false) {
+ $('notice').textContent = message;
+ $('notice').classList.toggle('error', error);
+ $('notice').hidden = !message;
+}
+
+// This one client helper handles JSON, bearer auth, errors, and empty 204 responses.
+async function api(path, { method = 'GET', data, authenticated = true, retry = true } = {}) {
+ const token = state.token;
+ const generation = sessionGeneration;
+ // Cookie-using endpoints require this header; cross-origin HTML forms cannot set it.
+ const headers = { 'X-Notebook-Request': '1' };
+ if (data !== undefined) headers['Content-Type'] = 'application/json';
+ if (authenticated && token) headers.Authorization = `Bearer ${token}`;
+ let response;
+ try {
+ response = await fetch(path, { method, headers, credentials: 'same-origin', cache: 'no-store',
+ body: data === undefined ? undefined : JSON.stringify(data) });
+ } catch {
+ throw new Error('Could not reach the notebook. Check that the server is running and try again.');
+ }
+ const result = response.status === 204 ? null : await response.json();
+ // Ignore a response from a previous login rather than exposing it in another account.
+ if (authenticated && generation !== sessionGeneration) throw new DOMException('Login changed', 'AbortError');
+ // Share one refresh across concurrent requests, then retry the original request once.
+ if (response.status === 401 && authenticated && retry) {
+ try { await renewLogin(); }
+ catch (error) {
+ if (error.status === 401) {
+ leaveNotebook();
+ throw new Error('Your login has ended. Log in again to continue.');
+ }
+ throw error;
+ }
+ return api(path, { method, data, authenticated, retry: false });
+ }
+ if (response.status === 401 && authenticated) {
+ leaveNotebook();
+ throw new Error('Your login has ended. Log in again to continue.');
+ }
+ if (!response.ok) {
+ const wait = Number(response.headers.get('Retry-After')) || result.retry_after || 0;
+ const error = new Error((result.error || `Request failed (${response.status}).`) + (wait ? `. Try again in ${wait} seconds.` : ''));
+ error.status = response.status;
+ error.retryAfter = wait;
+ throw error;
+ }
+ return { data: result, headers: response.headers };
+}
+
+function renewLogin() {
+ if (!refreshPromise) {
+ const generation = sessionGeneration;
+ const previousUser = state.user?.id;
+ refreshPromise = api('/api/refresh', { method: 'POST', authenticated: false })
+ .then(({ data }) => {
+ if (generation !== sessionGeneration) throw new DOMException('Login changed', 'AbortError');
+ // Another tab may have logged into a different account using the shared cookie.
+ // Never retry an old account's pending write using that new account's identity.
+ if (previousUser && previousUser !== data.user.id) {
+ leaveNotebook();
+ throw new Error('This browser changed accounts. Reload to open the current notebook.');
+ }
+ state.token = data.access_token;
+ return data;
+ }).finally(() => { refreshPromise = null; });
+ }
+ return refreshPromise;
+}
+
+async function enterNotebook(login, message) {
+ sessionGeneration++;
+ state.token = login.access_token;
+ const { data: user } = await api('/api/me');
+ state.user = user;
+ $('auth-password').value = '';
+ $('username').textContent = user.id;
+ $('welcome').hidden = true;
+ $('workspace').hidden = false;
+ $('account').hidden = false;
+ showNote(null);
+ await loadNotes();
+ notice(message);
+}
+
+async function restoreNotebook() {
+ const buttons = [$('auth-submit'), $('login-mode'), $('signup-mode')];
+ buttons.forEach(button => { button.disabled = true; });
+ notice('Checking your login…');
+ try { await enterNotebook(await renewLogin(), 'Welcome back.'); }
+ catch (error) {
+ leaveNotebook();
+ // A new visitor simply has no refresh cookie; other failures deserve a visible message.
+ notice(error.status === 401 ? '' : error.message, error.status !== 401);
+ }
+ finally { buttons.forEach(button => { button.disabled = false; }); }
+}
+
+async function action(callback, button) {
+ if (button) { button.disabled = true; button.setAttribute('aria-busy', 'true'); }
+ try { await callback(); }
+ catch (error) { if (error.name !== 'AbortError') notice(error.message, true); }
+ finally { if (button) { button.disabled = false; button.removeAttribute('aria-busy'); } }
+}
+
+function authMode(mode) {
+ state.mode = mode;
+ const signup = mode === 'signup';
+ $('signup-mode').setAttribute('aria-pressed', String(signup));
+ $('login-mode').setAttribute('aria-pressed', String(!signup));
+ $('auth-title').textContent = signup ? 'Start your notebook' : 'Open your notebook';
+ $('auth-description').textContent = signup ? 'Choose a username and a long password. Your notes belong to you.' : 'Log in to pick up where you left off.';
+ $('auth-submit').textContent = signup ? 'Create account' : 'Log in';
+ $('auth-password').autocomplete = signup ? 'new-password' : 'current-password';
+ notice('');
+}
+
+function leaveNotebook() {
+ sessionGeneration++;
+ state.token = null;
+ state.user = null;
+ state.notes = [];
+ state.total = 0;
+ state.offset = 0;
+ listSequence++;
+ selectionSequence++;
+ $('note-list').replaceChildren();
+ $('search').value = '';
+ $('pinned-filter').checked = false;
+ showNote(null);
+ $('workspace').hidden = true;
+ $('account').hidden = true;
+ $('welcome').hidden = false;
+ $('username').textContent = '';
+ $('auth-password').value = '';
+}
+
+function dirty() {
+ const original = state.selected || { title: '', body: '', pinned: false };
+ return $('note-title').value !== original.title || $('note-body').value !== original.body || $('note-pinned').checked !== original.pinned;
+}
+
+function mayDiscard() {
+ return !dirty() || window.confirm('Discard your unsaved changes?');
+}
+
+function updateDraftState() {
+ $('character-count').textContent = `${$('note-body').value.length.toLocaleString()} / 10,000`;
+ $('save-state').textContent = dirty() ? 'Unsaved changes' : state.selected ? 'Saved' : 'Ready when you are';
+}
+
+function showNote(note) {
+ state.selected = note ? structuredClone(note) : null;
+ $('note-title').value = note?.title || '';
+ $('note-body').value = note?.body || '';
+ $('note-pinned').checked = note?.pinned || false;
+ $('editor-heading').textContent = note ? 'Edit note' : 'New note';
+ $('save-note').textContent = note ? 'Save changes' : 'Save note';
+ $('discard').textContent = note ? 'Discard changes' : 'Clear draft';
+ $('delete-note').hidden = !note;
+ $('note-date').textContent = note ? `Last saved ${new Date(note.updated_at).toLocaleString()}` : '';
+ updateDraftState();
+}
+
+function renderNotes() {
+ $('note-list').replaceChildren();
+ for (const note of state.notes) {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'note-item';
+ button.setAttribute('aria-current', String(note.id === state.selected?.id));
+ const title = document.createElement('strong');
+ title.textContent = note.title;
+ const excerpt = document.createElement('span');
+ excerpt.className = 'excerpt';
+ excerpt.textContent = note.body.replace(/\s+/g, ' ').slice(0, 85) || 'Empty note';
+ const meta = document.createElement('span');
+ meta.className = 'note-meta';
+ meta.textContent = `${note.pinned ? 'Pinned · ' : ''}${new Date(note.updated_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
+ button.append(title, excerpt, meta);
+ button.addEventListener('click', () => action(() => selectNote(note.id)));
+ $('note-list').append(button);
+ }
+ $('empty-list').hidden = state.total !== 0;
+ $('empty-list').textContent = $('search').value || $('pinned-filter').checked
+ ? 'No notes match this filter. Try another search or turn off Pinned only.'
+ : 'Your first note starts in the editor. Give it a title and save it.';
+ $('list-summary').textContent = state.total ? `${state.offset + 1}–${Math.min(state.offset + state.limit, state.total)} of ${state.total} notes` : 'No notes yet';
+ $('pagination').hidden = state.total <= state.limit;
+ $('previous').disabled = state.offset === 0;
+ $('next').disabled = state.offset + state.limit >= state.total;
+ $('page-number').textContent = `Page ${Math.floor(state.offset / state.limit) + 1}`;
+}
+
+async function loadNotes() {
+ const sequence = ++listSequence;
+ const query = new URLSearchParams({ limit: state.limit, offset: state.offset });
+ const search = $('search').value.trim();
+ if (search) query.set('q', search);
+ if ($('pinned-filter').checked) query.set('pinned', 'true');
+ const { data } = await api(`/api/notes?${query}`);
+ if (sequence !== listSequence) return;
+ // If deletion emptied the last page, show the last remaining page instead.
+ if (data.total && state.offset >= data.total) {
+ state.offset = Math.floor((data.total - 1) / state.limit) * state.limit;
+ return loadNotes();
+ }
+ if (!data.total) state.offset = 0;
+ state.notes = data.items;
+ state.total = data.total;
+ renderNotes();
+}
+
+async function selectNote(id) {
+ if (id === state.selected?.id || !mayDiscard()) return;
+ const sequence = ++selectionSequence;
+ const { data } = await api(`/api/notes/${encodeURIComponent(id)}`);
+ if (sequence !== selectionSequence) return;
+ showNote(data);
+ renderNotes();
+ notice('');
+ if (window.matchMedia('(max-width: 760px)').matches) $('editor-heading').scrollIntoView({ block: 'start' });
+}
+
+$('login-mode').addEventListener('click', () => authMode('login'));
+$('signup-mode').addEventListener('click', () => authMode('signup'));
+$('auth-form').addEventListener('submit', event => {
+ event.preventDefault();
+ const credentials = { username: $('auth-username').value, password: $('auth-password').value };
+ const signup = state.mode === 'signup';
+ action(async () => {
+ if (signup) await api('/api/signup', { method: 'POST', data: credentials, authenticated: false });
+ const { data: login } = await api('/api/login', { method: 'POST', data: credentials, authenticated: false });
+ await enterNotebook(login, signup ? 'Your notebook is ready. Write your first note.' : 'Welcome back.');
+ $('note-title').focus();
+ }, $('auth-submit'));
+});
+
+$('note-form').addEventListener('input', updateDraftState);
+$('note-form').addEventListener('submit', event => {
+ event.preventDefault();
+ const id = state.selected?.id;
+ const values = { title: $('note-title').value, body: $('note-body').value, pinned: $('note-pinned').checked };
+ action(async () => {
+ const { data } = await api(id ? `/api/notes/${encodeURIComponent(id)}` : '/api/notes', { method: id ? 'PATCH' : 'POST', data: values });
+ selectionSequence++;
+ showNote(data);
+ state.offset = 0;
+ await loadNotes();
+ notice('Note saved.');
+ }, $('save-note'));
+});
+
+$('new-note').addEventListener('click', () => {
+ if (!mayDiscard()) return;
+ selectionSequence++;
+ showNote(null);
+ renderNotes();
+ notice('');
+ $('note-title').focus();
+});
+$('discard').addEventListener('click', () => {
+ if (!mayDiscard()) return;
+ showNote(state.selected);
+ notice('');
+});
+$('delete-note').addEventListener('click', () => {
+ const id = state.selected?.id;
+ if (!id || !window.confirm('Delete this note? This cannot be undone.')) return;
+ action(async () => {
+ await api(`/api/notes/${encodeURIComponent(id)}`, { method: 'DELETE' });
+ selectionSequence++;
+ showNote(null);
+ await loadNotes();
+ notice('Note deleted.');
+ }, $('delete-note'));
+});
+$('search').addEventListener('input', () => {
+ clearTimeout(searchTimer);
+ searchTimer = setTimeout(() => { state.offset = 0; action(loadNotes); }, 250);
+});
+$('pinned-filter').addEventListener('change', () => { state.offset = 0; action(loadNotes); });
+$('previous').addEventListener('click', () => { state.offset = Math.max(0, state.offset - state.limit); action(loadNotes); });
+$('next').addEventListener('click', () => { state.offset += state.limit; action(loadNotes); });
+$('export').addEventListener('click', () => action(async () => {
+ const { data } = await api('/api/notes/export');
+ const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'notes.json';
+ document.body.append(link);
+ link.click();
+ link.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ notice('Your notes have been exported.');
+}, $('export')));
+$('logout').addEventListener('click', () => action(async () => {
+ if (!mayDiscard()) return;
+ await api('/api/logout', { method: 'POST' });
+ leaveNotebook();
+ notice('Logged out.');
+}, $('logout')));
+
+// A harmless endpoint makes the request limit visible without exhausting login attempts.
+$('rate-demo').addEventListener('click', () => action(async () => {
+ let accepted = 0;
+ let limited = 0;
+ let wait = 0;
+ for (let index = 0; index < 5; index++) {
+ try { await api('/api/rate-limit-demo', { authenticated: false }); accepted++; }
+ catch (error) {
+ if (error.status !== 429) throw error;
+ limited++;
+ wait = Math.max(wait, error.retryAfter);
+ }
+ }
+ $('rate-result').textContent = `${accepted} accepted; ${limited} limited (HTTP 429).${wait ? ` Try again in ${wait} seconds.` : ''}`;
+}, $('rate-demo')));
+
+// The browser sends the HttpOnly cookie automatically; JS never reads its value.
+restoreNotebook();
diff --git a/example_app/static/index.html b/example_app/static/index.html
new file mode 100644
index 0000000..63aebcc
--- /dev/null
+++ b/example_app/static/index.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="theme-color" content="#2457d6">
+ <title>Wred notebook</title>
+ <link rel="icon" href="data:,">
+ <link rel="stylesheet" href="/static/app.css">
+ <script src="/static/app.js" defer></script>
+</head>
+<body>
+ <div class="shell">
+ <header class="app-header">
+ <a class="brand" href="/" aria-label="Wred notebook home"><span class="book-mark" aria-hidden="true"></span>Wred notebook</a>
+ <div id="account" class="account" hidden>
+ <span id="username"></span>
+ <button id="export" class="quiet" type="button">Export notes</button>
+ <button id="logout" class="quiet" type="button">Log out</button>
+ </div>
+ </header>
+
+ <p id="notice" class="notice" role="status" aria-live="polite" hidden></p>
+
+ <main id="welcome" class="welcome">
+ <section class="introduction">
+ <h1>A notebook<br>of your own.</h1>
+ <p>Keep your ideas, lists, and little reminders in one place. Find them when you need them.</p>
+ <div class="paper-preview" aria-hidden="true">
+ <span class="preview-title">Things to remember</span>
+ <span class="preview-line">Leave a little room for the next idea.</span>
+ <span class="preview-line">Start with a blank page.</span>
+ <span class="preview-line"></span>
+ </div>
+ </section>
+ <section class="auth-panel" aria-labelledby="auth-title">
+ <div class="auth-switch" aria-label="Choose how to sign in">
+ <button id="login-mode" type="button" aria-pressed="true">Log in</button>
+ <button id="signup-mode" type="button" aria-pressed="false">Create account</button>
+ </div>
+ <h2 id="auth-title">Open your notebook</h2>
+ <p id="auth-description">Log in to pick up where you left off.</p>
+ <form id="auth-form">
+ <label for="auth-username">Username</label>
+ <input id="auth-username" name="username" autocomplete="username" maxlength="64" required>
+ <label for="auth-password">Password</label>
+ <input id="auth-password" name="password" type="password" autocomplete="current-password" minlength="15" maxlength="128" aria-describedby="password-help" required>
+ <p id="password-help" class="field-help">Use 15–128 characters. Spaces are welcome.</p>
+ <button id="auth-submit" class="primary" type="submit">Log in</button>
+ </form>
+ </section>
+ </main>
+
+ <main id="workspace" class="workspace" hidden>
+ <aside class="notes-pane" aria-labelledby="notes-heading">
+ <div class="pane-heading">
+ <h1 id="notes-heading">Your notes</h1>
+ <button id="new-note" class="primary small" type="button">+ New note</button>
+ </div>
+ <label class="sr-only" for="search">Search notes</label>
+ <input id="search" type="search" placeholder="Search your notes…" maxlength="200">
+ <label class="check filter"><input id="pinned-filter" type="checkbox"> Pinned only</label>
+ <p id="list-summary" class="list-summary" aria-live="polite"></p>
+ <div id="note-list" class="note-list" aria-label="Notes"></div>
+ <p id="empty-list" class="empty-list" hidden>Your first note starts on the right. Give it a title and save it.</p>
+ <nav id="pagination" class="pagination" aria-label="Notes pages" hidden>
+ <button id="previous" class="quiet" type="button">← Previous</button>
+ <span id="page-number"></span>
+ <button id="next" class="quiet" type="button">Next →</button>
+ </nav>
+ </aside>
+
+ <section class="editor" aria-labelledby="editor-heading">
+ <div class="editor-heading">
+ <h2 id="editor-heading">New note</h2>
+ <span id="save-state" class="save-state">Ready when you are</span>
+ </div>
+ <form id="note-form">
+ <label for="note-title">Title</label>
+ <input id="note-title" name="title" placeholder="Give this thought a title" maxlength="100" required>
+ <label for="note-body">Note</label>
+ <textarea id="note-body" name="body" placeholder="Start writing…" maxlength="10000" rows="12"></textarea>
+ <div class="editor-details">
+ <label class="check"><input id="note-pinned" name="pinned" type="checkbox"> Keep at top</label>
+ <span id="character-count">0 / 10,000</span>
+ </div>
+ <div class="editor-actions">
+ <button id="save-note" class="primary" type="submit">Save note</button>
+ <button id="discard" class="quiet" type="button">Clear draft</button>
+ <button id="delete-note" class="danger" type="button" hidden>Delete note</button>
+ </div>
+ </form>
+ <p id="note-date" class="note-date"></p>
+ </section>
+ </main>
+
+ <details class="demo-tools">
+ <summary>Try request limits</summary>
+ <p>This demo endpoint accepts three requests every ten seconds. Extra requests return HTTP 429 and tell you when to retry.</p>
+ <button id="rate-demo" class="quiet" type="button">Send 5 requests</button>
+ <p id="rate-result" role="status" aria-live="polite"></p>
+ </details>
+ <footer class="app-footer">This is a demo. Your login survives reloads for up to seven days; data and logins reset when the server restarts.</footer>
+ </div>
+</body>
+</html>
diff --git a/example_app/storage.py b/example_app/storage.py
new file mode 100644
index 0000000..6f9c8d9
--- /dev/null
+++ b/example_app/storage.py
@@ -0,0 +1,7 @@
+"""Shared in-memory demo data; all records disappear when the process restarts."""
+
+# Real deployments need persistent shared storage and shared rate-limit counters.
+users = {} # Username -> salt and password hash; never store plaintext passwords.
+tokens = {} # Token digest -> user ID and expiry; never store raw bearer tokens.
+refresh_tokens = {} # Cookie digest -> user ID, login ID, and absolute expiry.
+notes = {} # Note ID -> note, including the owning username.
diff --git a/scripts/benchmark_embedded.py b/scripts/benchmark_embedded.py
new file mode 100644
index 0000000..dd72b19
--- /dev/null
+++ b/scripts/benchmark_embedded.py
@@ -0,0 +1,119 @@
+"""Compare the generated file with pinned standalone Uvicorn (one worker each)."""
+import argparse
+import http.client
+import json
+from pathlib import Path
+import re
+import shutil
+import signal
+import socket
+import statistics
+import subprocess
+import tempfile
+import time
+import venv
+
+ROOT = Path(__file__).resolve().parents[1]
+APPLICATION = '''from wred import App
+import sys
+app = App()
[email protected]('/')
+async def index(request):
+ return {'hello': 'world'}
+if sys.argv[1] == 'embedded':
+ app.run(port=int(sys.argv[2]))
+else:
+ import uvicorn
+ uvicorn.run(app, port=int(sys.argv[2]), host='127.0.0.1', http='h11', ws='none',
+ lifespan='off', loop='asyncio', interface='asgi3', workers=1, access_log=False)
+'''
+
+
+def parse(output):
+ def value(pattern):
+ return re.search(pattern, output, re.MULTILINE).group(1)
+ errors = re.search(r'Socket errors: connect (\d+), read (\d+), write (\d+), timeout (\d+)', output)
+ bad = re.search(r'Non-2xx or 3xx responses: (\d+)', output)
+ return {'requests_per_second': float(value(r'Requests/sec:\s+([\d.]+)')),
+ 'mean_latency': value(r'^\s+Latency\s+(\S+)'),
+ 'p50_latency': value(r'^\s+50%\s+(\S+)'),
+ 'p99_latency': value(r'^\s+99%\s+(\S+)'),
+ 'socket_errors': sum(map(int, errors.groups())) if errors else 0,
+ 'non_success_responses': int(bad.group(1)) if bad else 0}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--standalone-python', required=True)
+ parser.add_argument('--runs', type=int, default=3)
+ parser.add_argument('--output', type=Path, default=ROOT / 'experiments/embedded-uvicorn/results')
+ args = parser.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ rows = []
+ with tempfile.TemporaryDirectory(prefix='wred-benchmark-') as directory:
+ directory = Path(directory)
+ venv.EnvBuilder(with_pip=False).create(directory / 'venv')
+ embedded_python = directory / 'venv/bin/python'
+ shutil.copyfile(ROOT / 'wred.py', directory / 'wred.py')
+ (directory / 'app.py').write_text(APPLICATION)
+ for run in range(1, args.runs + 1):
+ order = ['embedded', 'standalone'] if run % 2 else ['standalone', 'embedded']
+ for mode in order:
+ with socket.socket() as sock:
+ sock.bind(('127.0.0.1', 0))
+ port = sock.getsockname()[1]
+ python = str(embedded_python) if mode == 'embedded' else args.standalone_python
+ with (args.output / f'{mode}-{run}.server.log').open('w') as log:
+ start = time.perf_counter()
+ server = subprocess.Popen([python, 'app.py', mode, str(port)], cwd=directory,
+ stdout=log, stderr=log)
+ try:
+ deadline = time.monotonic() + 15
+ while True:
+ if server.poll() is not None:
+ raise RuntimeError(f'{mode} exited during startup')
+ try:
+ conn = http.client.HTTPConnection('127.0.0.1', port, timeout=1)
+ conn.request('GET', '/')
+ response = conn.getresponse()
+ assert response.status == 200 and json.loads(response.read()) == {'hello': 'world'}
+ conn.close()
+ startup = (time.perf_counter() - start) * 1000
+ break
+ except OSError:
+ if time.monotonic() > deadline:
+ raise RuntimeError('Server startup timed out')
+ time.sleep(0.005)
+ url = f'http://127.0.0.1:{port}/'
+ print(f'{mode} {run}/{args.runs}: warmup, startup={startup:.1f}ms', flush=True)
+ warmup = subprocess.run(['wrk', '-t4', '-c256', '-d5s', url],
+ capture_output=True, text=True, check=True)
+ (args.output / f'{mode}-{run}.warmup.txt').write_text(warmup.stdout + warmup.stderr)
+ result = subprocess.run(['wrk', '-t4', '-c256', '-d20s', '--latency', url],
+ capture_output=True, text=True, check=True)
+ output = result.stdout + result.stderr
+ (args.output / f'{mode}-{run}.wrk.txt').write_text(output)
+ row = {'mode': mode, 'run': run, 'startup_ms': startup, **parse(output)}
+ rows.append(row)
+ print(json.dumps(row), flush=True)
+ (args.output / 'results.json').write_text(json.dumps(rows, indent=2) + '\n')
+ if row['socket_errors'] or row['non_success_responses']:
+ raise RuntimeError('Benchmark reported errors')
+ finally:
+ if server.poll() is None:
+ server.send_signal(signal.SIGTERM)
+ try:
+ server.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ server.kill()
+ server.wait()
+ summary = {mode: {'median_rps': statistics.median(r['requests_per_second'] for r in rows if r['mode'] == mode),
+ 'median_startup_ms': statistics.median(r['startup_ms'] for r in rows if r['mode'] == mode)}
+ for mode in ('embedded', 'standalone')}
+ summary['embedded_to_standalone_ratio'] = summary['embedded']['median_rps'] / summary['standalone']['median_rps']
+ (args.output / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n')
+ print(json.dumps(summary, indent=2), flush=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/build_embedded.py b/scripts/build_embedded.py
new file mode 100644
index 0000000..1be5279
--- /dev/null
+++ b/scripts/build_embedded.py
@@ -0,0 +1,67 @@
+"""Rebuild wred.py using hash-verified pure-Python wheels. Network is build-time only."""
+import base64
+import hashlib
+import io
+import json
+from pathlib import Path
+import sys
+import urllib.request
+import zipfile
+
+ROOT = Path(__file__).resolve().parents[1]
+LOCK = json.loads((ROOT / 'scripts/embedded-lock.json').read_text())
+# Optional backends and test/CLI entry modules are not used by App.run().
+EXCLUDE = ('h11/tests/', 'click/testing.py', 'uvicorn/__main__.py',
+ 'uvicorn/workers.py', 'uvicorn/protocols/websockets/',
+ 'uvicorn/protocols/http/httptools_impl.py', 'uvicorn/protocols/http/auto.py',
+ 'uvicorn/protocols/http/auto_zttp_impl.py', 'uvicorn/protocols/http/zttp_',
+ 'uvicorn/loops/auto.py', 'uvicorn/loops/uvloop.py', 'uvicorn/loops/zuvloop.py',
+ 'uvicorn/supervisors/watchfilesreload.py', 'uvicorn/lifespan/on.py')
+
+
+def build(wheel_directory):
+ wheel_directory.mkdir(parents=True, exist_ok=True)
+ entries = {}
+ notices = []
+ for name, pin in LOCK.items():
+ wheel = wheel_directory / pin['filename']
+ if not wheel.exists():
+ with urllib.request.urlopen(pin['url'], timeout=60) as response:
+ data = response.read()
+ if hashlib.sha256(data).hexdigest() != pin['sha256']:
+ raise ValueError(f'Wheel hash mismatch: {name}')
+ wheel.write_bytes(data)
+ data = wheel.read_bytes()
+ if hashlib.sha256(data).hexdigest() != pin['sha256']:
+ raise ValueError(f'Wheel hash mismatch: {name}')
+ with zipfile.ZipFile(io.BytesIO(data)) as source:
+ for filename in sorted(source.namelist()):
+ if filename.endswith('/') or filename.startswith(EXCLUDE):
+ continue
+ if filename.startswith(name + '/') or '.dist-info/' in filename:
+ if filename.endswith('/RECORD'):
+ continue
+ entries[filename] = source.read(filename)
+ if '/licenses/' in filename or filename.endswith(('/LICENSE', '/LICENSE.txt')):
+ notices.append(f'{name} {pin["version"]}: {filename}\n\n' + entries[filename].decode())
+ if len(notices) != len(LOCK):
+ raise ValueError('Missing or unexpected bundled license notices')
+ buffer = io.BytesIO()
+ with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
+ for filename, data in sorted(entries.items()):
+ info = zipfile.ZipInfo(filename, date_time=(1980, 1, 1, 0, 0, 0))
+ info.compress_type = zipfile.ZIP_DEFLATED
+ info.create_system = 3
+ info.external_attr = 0o100644 << 16
+ archive.writestr(info, data, compresslevel=9)
+ payload = base64.b64encode(buffer.getvalue()).decode('ascii')
+ licenses = '\n\n'.join(notices)
+ source = (ROOT / 'source/wred.py').read_text()
+ output = source.rstrip() + '\n\n\n_EMBEDDED_ZIP = ' + repr(payload) + '\n'
+ (ROOT / 'wred.py').write_text(output)
+ (ROOT / 'THIRD_PARTY_LICENSES.txt').write_text(licenses.rstrip() + '\n')
+ print(f'Generated wred.py: {len(output.encode()):,} bytes; ZIP: {len(buffer.getvalue()):,} bytes; {len(entries)} entries')
+
+
+if __name__ == '__main__':
+ build(Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / 'build/embedded-wheels')
diff --git a/scripts/embedded-lock.json b/scripts/embedded-lock.json
new file mode 100644
index 0000000..b04704d
--- /dev/null
+++ b/scripts/embedded-lock.json
@@ -0,0 +1,20 @@
+{
+ "uvicorn": {
+ "version": "0.54.0",
+ "filename": "uvicorn-0.54.0-py3-none-any.whl",
+ "url": "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl",
+ "sha256": "505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf"
+ },
+ "h11": {
+ "version": "0.16.0",
+ "filename": "h11-0.16.0-py3-none-any.whl",
+ "url": "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl",
+ "sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
+ },
+ "click": {
+ "version": "8.5.0",
+ "filename": "click-8.5.0-py3-none-any.whl",
+ "url": "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl",
+ "sha256": "255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"
+ }
+}
diff --git a/source/wred.py b/source/wred.py
new file mode 100644
index 0000000..4546a7e
--- /dev/null
+++ b/source/wred.py
@@ -0,0 +1,239 @@
+"""
+Wred: An ultra micro ASGI API framework.
+
+Wred is designed for lightweight JSON APIs, offering a minimalistic way to build
+ASGI-compatible web services. It prioritizes simplicity and performance over all.
+
+This is the amazing magic version of Wred, Wred-embedded ;)
+Original Wred website: https://harrisonerd.com/wred
+Wred-embedded website: https://harrisonerd.com/wred-embedded
+
+Copyright (c) 2026, Harrison Erd. For license information see `LICENSE` AND
+`THIRD_PARTY_LICENSES.txt`. Bundles `uvicorn`, `click`, and `h11`.
+"""
+
+__author__ = 'Harrison Erd'
+__email__ = '[email protected]'
+__version__ = '0.03'
+__license__ = 'BSD-3-Clause'
+
+from collections.abc import Awaitable, Callable
+from asyncio import to_thread
+from mimetypes import guess_type
+from pathlib import Path
+from typing import Any
+from urllib.parse import parse_qsl
+import json
+
+
+class WredResponseError(Exception):
+ """Raised when a handler returns a non-JSON response."""
+ pass
+
+class WredRequestError(Exception):
+ """Raised when a request cannot be processed (e.g., invalid body)."""
+ pass
+
+
+class Request:
+ """A request object encapsulating ASGI scope, receive, and send."""
+ def __init__(self, scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], send: Callable[[dict[str, Any]], Awaitable[None]]):
+ self.scope = scope
+ self.receive = receive
+ self.send = send
+ self.path = scope['path']
+ self.method = scope['method']
+ self.headers = {k.decode(): v.decode() for k, v in scope.get('headers', [])}
+ self._body = None
+ self._query = None
+
+ def query(self) -> dict[str, str]:
+ """Parse and cache query parameters; repeated keys keep their last value."""
+ if self._query is None:
+ try:
+ self._query = dict(parse_qsl(self.scope.get('query_string', b'').decode('utf-8'), keep_blank_values=True))
+ except UnicodeDecodeError:
+ raise WredRequestError("Invalid query string")
+ return self._query
+
+ async def body(self) -> bytes:
+ """Read the request body from the ASGI receive stream."""
+ if self._body is None:
+ self._body = b""
+ while True:
+ message = await self.receive()
+ if message['type'] != 'http.request':
+ raise WredRequestError("Client disconnected")
+ self._body += message.get('body', b'')
+ if not message.get('more_body', False):
+ break
+ return self._body
+
+ async def json(self) -> dict[str, Any]:
+ """Parse the request body as JSON."""
+ try:
+ body = await self.body()
+ return json.loads(body) if body else {}
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ raise WredRequestError("Invalid JSON body")
+
+
+class App:
+ """A minimal ASGI application framework."""
+ def __init__(self):
+ self.routes: dict[str, dict[str, Callable]] = {}
+ self.pattern_routes: dict[str, dict[str, Callable]] = {}
+
+ def run(self, host="127.0.0.1", port=8000, workers=1):
+ """Run the bundled HTTP-only Uvicorn server; load it only on demand."""
+ import base64
+ import signal
+ import sys
+ import tempfile
+ import threading
+
+ if not isinstance(workers, int) or workers < 1:
+ raise ValueError('workers must be a positive integer')
+ application = self
+ if workers > 1:
+ namespace = sys._getframe(1).f_globals
+ name = next((name for name, value in namespace.items() if value is self), None)
+ if name is None:
+ raise ValueError('Multiple workers require a module-level application')
+ application = f"{namespace['__name__']}:{name}"
+ if any(name in sys.modules for name in ('uvicorn', 'h11', 'click')):
+ raise RuntimeError('App.run() requires uvicorn, h11, and click not to be imported already')
+ pending_signals = []
+ handlers = {}
+ if threading.current_thread() is threading.main_thread():
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ handlers[sig] = signal.signal(sig, lambda sig, frame: pending_signals.append(sig))
+ try:
+ with tempfile.TemporaryDirectory(prefix='wred-') as directory:
+ archive = str(Path(directory) / 'server.zip')
+ Path(archive).write_bytes(base64.b64decode(_EMBEDDED_ZIP))
+ sys.path.insert(0, archive)
+ try:
+ import uvicorn
+ if workers > 1:
+ from uvicorn.supervisors.multiprocess import SIGNALS
+ for sig in SIGNALS:
+ handlers.setdefault(sig, signal.getsignal(sig))
+ uvicorn.run(application, host=host, port=port, http="h11", ws="none",
+ lifespan="off", loop="asyncio", interface="asgi3",
+ workers=workers, access_log=False)
+ finally:
+ sys.path.remove(archive)
+ for name, module in list(sys.modules.items()):
+ if (getattr(module, '__file__', None) or '').startswith((archive + '/', archive + '\\')):
+ del sys.modules[name]
+ sys.path_importer_cache.pop(archive, None)
+ finally:
+ for sig, handler in handlers.items():
+ signal.signal(sig, handler)
+ for sig in reversed(pending_signals):
+ signal.raise_signal(sig)
+
+ async def __call__(self, scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
+ """Handle ASGI requests."""
+ if scope['type'] != 'http':
+ raise RuntimeError(f"Unsupported ASGI scope type: {scope['type']}")
+
+ request = Request(scope, receive, send)
+ await self.handle_http(request)
+
+ async def handle_http(self, request: Request) -> None:
+ """Handle HTTP requests with routing and JSON response support."""
+ path = request.path
+ root_path = request.scope.get('root_path', '').rstrip('/')
+ if root_path and (path == root_path or path.startswith(root_path + '/')):
+ path = path[len(root_path):] or '/'
+ method = request.method
+ try:
+ # Check exact matches first
+ if path in self.routes and method in self.routes[path]:
+ handler = self.routes[path][method]
+ response = await handler(request)
+ await self.dispatch_response(request, response)
+ return
+
+ # Check dynamic routes
+ for pattern, method_handlers in self.pattern_routes.items():
+ params = self.match_path(pattern, path)
+ if params is not None and method in method_handlers:
+ handler = method_handlers[method]
+ response = await handler(request, **params)
+ await self.dispatch_response(request, response)
+ return
+
+ # No match found
+ await self.send_response(request.send, {"error": "Not Found"}, status=404)
+
+ except (WredResponseError, WredRequestError) as e:
+ status = 400 if isinstance(e, WredRequestError) else 500
+ await self.send_response(request.send, {"error": str(e)}, status=status)
+
+ async def dispatch_response(self, request: Request, response: Any) -> None:
+ """Invoke an ASGI response or serialize an ordinary handler result."""
+ if callable(response):
+ await response(request.scope, request.receive, request.send)
+ else:
+ await self.send_response(request.send, response)
+
+ async def send_response(self, send: Callable[[dict[str, Any]], Awaitable[None]], response: Any, status: int = 200) -> None:
+ """Send JSON or (body, content_type, status[, headers]) with string header pairs."""
+ body, content_type = response, 'application/octet-stream'
+ headers = []
+ if isinstance(response, tuple) and len(response) in (3, 4):
+ body, content_type, status = response[:3]
+ if len(response) == 4:
+ headers = [(name.lower().encode('ascii'), value.encode('latin-1')) for name, value in response[3]]
+ if isinstance(body, (dict, list)):
+ body, content_type = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode('utf-8'), 'application/json'
+ elif not isinstance(body, bytes):
+ raise WredResponseError("Response must be a dictionary, list, bytes, or response tuple")
+ await send({
+ 'type': 'http.response.start',
+ 'status': status,
+ 'headers': [(b'content-type', content_type.encode('ascii'))] + headers,
+ })
+ await send({'type': 'http.response.body', 'body': body})
+
+ def match_path(self, pattern: str, path: str) -> dict[str, str] | None:
+ """Match a path against a pattern and extract parameters."""
+ pattern_parts = pattern.strip('/').split('/')
+ path_parts = path.strip('/').split('/')
+ if len(pattern_parts) != len(path_parts):
+ return None
+
+ params = {}
+ for pattern_part, path_part in zip(pattern_parts, path_parts):
+ if pattern_part.startswith('{') and pattern_part.endswith('}'):
+ param_name = pattern_part[1:-1]
+ params[param_name] = path_part
+ elif pattern_part != path_part:
+ return None
+ return params
+
+ async def static(self, filename: str | Path, *, root: str | Path) -> tuple[bytes, str, int]:
+ """Serve a small file inside root; read its entire body off the event loop."""
+ def read() -> tuple[bytes, str, int]:
+ try:
+ directory = Path(root).resolve()
+ path = (directory / filename).resolve()
+ if not path.is_relative_to(directory) or not path.is_file():
+ return b'Not Found', 'text/plain; charset=utf-8', 404
+ body = path.read_bytes()
+ except (OSError, ValueError, RuntimeError):
+ return b'Not Found', 'text/plain; charset=utf-8', 404
+ return body, guess_type(path.name)[0] or 'application/octet-stream', 200
+
+ return await to_thread(read)
+
+ def route(self, path: str, methods: list[str] = ['GET']) -> Callable:
+ """Decorator to register routes with specific HTTP methods."""
+ def decorator(handler: Callable) -> Callable:
+ routes = self.pattern_routes if '{' in path and '}' in path else self.routes
+ routes.setdefault(path, {}).update({m.upper(): handler for m in methods})
+ return handler
+ return decorator
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_embedded.py b/tests/test_embedded.py
new file mode 100644
index 0000000..3c2d452
--- /dev/null
+++ b/tests/test_embedded.py
@@ -0,0 +1,267 @@
+"""Exercise the generated single file using an empty venv and real HTTP sockets."""
+import http.client
+import json
+from pathlib import Path
+import shutil
+import signal
+import socket
+import subprocess
+import tempfile
+import time
+import unittest
+import venv
+
+ROOT = Path(__file__).resolve().parents[1]
+APPLICATION = '''from wred import App
+import importlib.metadata
+import os
+import sys
+app = App()
[email protected]('/')
+async def home(request):
+ return {'hello': 'world'}
[email protected]('/users/{name}')
+async def user(request, name):
+ return {'name': name, 'q': request.query()}
[email protected]('/echo', methods=['POST'])
+async def echo(request):
+ return await request.json()
[email protected]('/body', methods=['POST'])
+async def body(request):
+ return await request.body()
[email protected]('/invalid')
+async def invalid(request):
+ return 'unsupported'
[email protected]('/binary')
+async def binary(request):
+ return b'\\x00\\xff', 'application/octet-stream', 202, [('X-Test', 'yes')]
[email protected]('/modules')
+async def modules(request):
+ import uvicorn, h11, click
+ return {'versions': {n: importlib.metadata.version(n) for n in ('uvicorn', 'h11', 'click')},
+ 'archive': uvicorn.__file__.split('/uvicorn/')[0],
+ 'websocket_modules': [n for n in sys.modules if n.startswith('uvicorn.protocols.websockets')],
+ 'compiled_backends': [n for n in ('uvloop', 'httptools') if n in sys.modules]}
[email protected]('/pid')
+async def pid(request):
+ return {'pid': os.getpid()}
+if __name__ == '__main__':
+ if os.environ.get('WRED_DEFAULTS'):
+ app.run()
+ else:
+ app.run(host='0.0.0.0', port=int(os.environ['WRED_TEST_PORT']),
+ workers=int(os.environ.get('WRED_TEST_WORKERS', '1')))
+'''
+
+
+class StandaloneTests(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.temporary = tempfile.TemporaryDirectory(prefix='wred-standalone-test-')
+ cls.directory = Path(cls.temporary.name)
+ venv.EnvBuilder(with_pip=False).create(cls.directory / 'venv')
+ cls.python = cls.directory / 'venv' / ('Scripts/python.exe' if __import__('os').name == 'nt' else 'bin/python')
+ shutil.copyfile(ROOT / 'wred.py', cls.directory / 'wred.py')
+ (cls.directory / 'app.py').write_text(APPLICATION)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.temporary.cleanup()
+
+ def start(self, default=False, workers=1):
+ import os
+ if default:
+ port = 8000
+ else:
+ with socket.socket() as s:
+ s.bind(('127.0.0.1', 0))
+ port = s.getsockname()[1]
+ logfile = tempfile.TemporaryFile(mode='w+')
+ self.addCleanup(logfile.close)
+ environment = {**os.environ, 'WRED_TEST_PORT': str(port), 'WRED_TEST_WORKERS': str(workers)}
+ environment.pop('PYTHONPATH', None)
+ environment.pop('WRED_DEFAULTS', None)
+ if default:
+ environment['WRED_DEFAULTS'] = '1'
+ process = subprocess.Popen([str(self.python), 'app.py'], cwd=self.directory,
+ env=environment, stdout=logfile, stderr=logfile)
+ def stop():
+ if process.poll() is None:
+ process.terminate()
+ try:
+ process.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ process.kill()
+ process.wait()
+ self.addCleanup(stop)
+ deadline = time.monotonic() + 15
+ while time.monotonic() < deadline:
+ if process.poll() is not None:
+ logfile.seek(0)
+ self.fail(logfile.read())
+ try:
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=1)
+ connection.request('GET', '/')
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ self.assertEqual(json.loads(response.read()), {'hello': 'world'})
+ connection.close()
+ return process, port, logfile
+ except OSError:
+ time.sleep(0.02)
+ self.fail('Server did not start within 15 seconds')
+
+ def test_import_has_no_dependencies_and_does_not_load_or_decode_server(self):
+ program = '''import base64, importlib.metadata, sys, time
+def forbidden_decode(*args, **kwargs):
+ raise AssertionError('Import decoded the server archive')
+base64.b64decode = forbidden_decode
+assert not list(importlib.metadata.distributions())
+started = time.perf_counter()
+import wred
+elapsed = time.perf_counter() - started
+assert not any(n in sys.modules for n in ('uvicorn', 'h11', 'click'))
+assert not any('server.zip' in p for p in sys.path)
+print(elapsed)
+'''
+ result = subprocess.run([str(self.python), '-c', program], cwd=self.directory,
+ text=True, capture_output=True, check=True)
+
+ def test_custom_binding_json_dynamic_routes_keep_alive_and_errors(self):
+ _, port, _ = self.start()
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
+ self.addCleanup(connection.close)
+ connection.request('GET', '/users/alice?q=hello+world')
+ response = connection.getresponse()
+ self.assertEqual((response.status, json.loads(response.read())),
+ (200, {'name': 'alice', 'q': {'q': 'hello world'}}))
+ first_socket = connection.sock
+ for path, method, body, status, expected in [
+ ('/echo', 'POST', json.dumps({'text': 'café', 'data': 'x' * 131072}), 200,
+ {'text': 'café', 'data': 'x' * 131072}),
+ ('/echo', 'POST', '{', 400, {'error': 'Invalid JSON body'}),
+ ('/echo', 'POST', b'\xff', 400, {'error': 'Invalid JSON body'}),
+ ('/echo', 'POST', b'', 200, {}),
+ ('/missing', 'GET', None, 404, {'error': 'Not Found'}),
+ ('/invalid', 'GET', None, 500,
+ {'error': 'Response must be a dictionary, list, bytes, or response tuple'}),
+ ]:
+ with self.subTest(path=path, body=str(body)[:20]):
+ connection.request(method, path, body=body, headers={'Content-Type': 'application/json'})
+ response = connection.getresponse()
+ self.assertEqual((response.status, json.loads(response.read())), (status, expected))
+ self.assertIs(connection.sock, first_socket)
+ connection.request('POST', '/body', body=iter([b'first', b'\x00second']), encode_chunked=True)
+ response = connection.getresponse()
+ self.assertEqual((response.status, response.read()), (200, b'first\x00second'))
+ connection.request('GET', '/binary')
+ response = connection.getresponse()
+ self.assertEqual((response.status, response.getheader('X-Test'), response.read()),
+ (202, 'yes', b'\x00\xff'))
+ connection.request('GET', '/modules')
+ response = connection.getresponse()
+ data = json.loads(response.read())
+ self.assertEqual(data['versions'], {'uvicorn': '0.54.0', 'h11': '0.16.0', 'click': '8.5.0'})
+ self.assertTrue(Path(data['archive']).is_file())
+ self.assertEqual(data['websocket_modules'], [])
+ self.assertEqual(data['compiled_backends'], [])
+ connection.request('GET', '/', headers={'Connection': 'Upgrade', 'Upgrade': 'websocket',
+ 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ=='})
+ response = connection.getresponse()
+ self.assertNotEqual(response.status, 101)
+ response.read()
+
+ def test_startup_failure_restores_imports_signals_and_temporary_files(self):
+ program = '''from wred import App
+import pathlib, signal, sys, tempfile
+tempfile.tempdir = str(pathlib.Path('scratch').resolve())
+pathlib.Path(tempfile.tempdir).mkdir()
+original_path = sys.path.copy()
+handlers = {sig: signal.getsignal(sig) for sig in (signal.SIGINT, signal.SIGTERM)}
+for attempt in range(2):
+ try:
+ App().run(port=65536)
+ except OverflowError:
+ pass
+ else:
+ raise AssertionError('Invalid port unexpectedly succeeded')
+ assert not list(pathlib.Path(tempfile.tempdir).iterdir())
+ assert sys.path == original_path
+ assert not any(n in sys.modules for n in ('uvicorn', 'h11', 'click'))
+ assert all(signal.getsignal(sig) == handler for sig, handler in handlers.items())
+'''
+ subprocess.run([str(self.python), '-c', program], cwd=self.directory,
+ text=True, capture_output=True, check=True)
+
+ def test_invalid_worker_counts_and_unbound_apps_fail_before_server_loading(self):
+ program = '''from wred import App
+import sys
+for workers in (0, -1, 1.5, None, '2'):
+ try:
+ App().run(workers=workers)
+ except ValueError:
+ pass
+ else:
+ raise AssertionError('Invalid worker count accepted')
+try:
+ App().run(workers=2)
+except ValueError as error:
+ assert 'module-level' in str(error)
+else:
+ raise AssertionError('Unbound app accepted for multiple workers')
+assert not any(n in sys.modules for n in ('uvicorn', 'h11', 'click'))
+'''
+ subprocess.run([str(self.python), '-c', program], cwd=self.directory,
+ text=True, capture_output=True, check=True)
+
+ def test_default_run_binding_and_sigint_cleanup(self):
+ process, port, logfile = self.start(default=True)
+ self.shutdown(process, port, logfile, signal.SIGINT)
+
+ def test_multiple_workers_serve_requests_and_clean_up_on_both_signals(self):
+ import os
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ with self.subTest(signal=sig):
+ process, port, logfile = self.start(workers=2)
+ pids = set()
+ deadline = time.monotonic() + 10
+ while len(pids) < 2 and time.monotonic() < deadline:
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('GET', '/pid')
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ pids.add(json.loads(response.read())['pid'])
+ connection.request('POST', '/echo', body=b'{"workers":2}')
+ response = connection.getresponse()
+ self.assertEqual((response.status, json.loads(response.read())), (200, {'workers': 2}))
+ connection.close()
+ time.sleep(0.01)
+ self.assertEqual(len(pids), 2)
+ self.assertNotIn(process.pid, pids)
+ self.shutdown(process, port, logfile, sig)
+ if os.name == 'posix':
+ for pid in pids:
+ with self.assertRaises(ProcessLookupError):
+ os.kill(pid, 0)
+
+ def test_sigterm_shutdown_cleans_archive(self):
+ process, port, logfile = self.start()
+ self.shutdown(process, port, logfile, signal.SIGTERM)
+
+ def shutdown(self, process, port, logfile, sig):
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('GET', '/modules')
+ response = connection.getresponse()
+ archive = Path(json.loads(response.read())['archive'])
+ connection.close()
+ process.send_signal(sig)
+ process.wait(timeout=10)
+ self.assertFalse(archive.parent.exists())
+ logfile.seek(0)
+ log = logfile.read()
+ self.assertIn('Finished server process', log)
+ self.assertNotIn('Waiting for application startup', log)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_notebook.py b/tests/test_notebook.py
new file mode 100644
index 0000000..fd53f06
--- /dev/null
+++ b/tests/test_notebook.py
@@ -0,0 +1,314 @@
+"""Exercise the example's API contracts through the actual Wred ASGI app."""
+import importlib
+import asyncio
+from http.cookies import SimpleCookie
+import unittest
+from urllib.parse import unquote, urlsplit
+from unittest.mock import patch
+
+import json
+
+from tests.test_wred import call_app
+from example_app import auth, storage, rate_limit
+
+notebook = importlib.import_module('example_app.app')
+
+
+class NotebookTests(unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ storage.users.clear()
+ storage.tokens.clear()
+ storage.refresh_tokens.clear()
+ rate_limit.rate_buckets.clear()
+ storage.notes.clear()
+
+ async def request(self, path, method='GET', data=None, token=None, authorization=None,
+ cookie=None, client='127.0.0.1', csrf_header=True, extra_headers=(), scheme='http'):
+ url = urlsplit(path)
+
+ async def receive():
+ if method == 'GET':
+ raise AssertionError('GET handlers should not consume the request body')
+ return {'type': 'http.request', 'body': json.dumps(data).encode('utf-8')}
+
+ headers = [(b'content-type', b'application/json'), (b'host', b'notebook.test')]
+ if csrf_header:
+ headers.append((b'x-notebook-request', b'1'))
+ if cookie is not None:
+ headers.append((b'cookie', cookie.encode()))
+ headers.extend(extra_headers)
+ if token is not None:
+ headers.append((b'authorization', f'Bearer {token}'.encode()))
+ if authorization is not None:
+ headers.append((b'authorization', authorization))
+ messages = await call_app(notebook.app, {
+ 'type': 'http', 'path': unquote(url.path), 'method': method,
+ 'query_string': url.query.encode(), 'headers': headers,
+ 'scheme': scheme, 'client': (client, 12345),
+ }, receive)
+ self.assertEqual(len(messages), 2)
+ self.assertEqual(messages[0]['type'], 'http.response.start')
+ self.assertEqual(messages[1]['type'], 'http.response.body')
+ response_headers = dict(messages[0]['headers'])
+ body = messages[1]['body']
+ if body and response_headers[b'content-type'] == b'application/json':
+ body = json.loads(body)
+ return messages[0]['status'], body, response_headers
+
+ async def account(self, username='alice'):
+ credentials = {'username': username, 'password': 'a long example passphrase'}
+ status, user, _ = await self.request('/api/signup', 'POST', credentials)
+ self.assertEqual((status, user), (201, {'id': username.strip().casefold()}))
+ status, login, headers = await self.request('/api/login', 'POST', credentials)
+ self.assertEqual(status, 200)
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ self.assertEqual(login['token_type'], 'bearer')
+ self.assertEqual(login['expires_in'], 3600)
+ return login['access_token']
+
+ async def create(self, token, **data):
+ status, note, headers = await self.request('/api/notes', 'POST', data, token)
+ self.assertEqual(status, 201)
+ self.assertEqual(headers[b'location'], f"/api/notes/{note['id']}".encode())
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ return note
+
+ async def test_public_page_assets_health_and_redirect(self):
+ for path, content_type, marker in [
+ ('/', b'text/html', b'Wred notebook'),
+ ('/static/app.css', b'text/css', b'.workspace'),
+ ('/static/app.js', b'text/javascript', b'/api/notes'),
+ ]:
+ with self.subTest(path=path):
+ status, body, headers = await self.request(path)
+ self.assertEqual(status, 200)
+ self.assertEqual(headers[b'content-type'], content_type)
+ self.assertEqual(headers[b'cache-control'], b'no-cache')
+ self.assertIn(marker, body)
+ status, body, headers = await self.request('/ui')
+ self.assertEqual((status, body, headers[b'location']), (303, b'', b'/'))
+ self.assertEqual((await self.request('/api/health'))[:2], (200, {'status': 'ok'}))
+ self.assertEqual((await self.request('/static/missing.js'))[0], 404)
+
+ async def test_signup_login_and_independent_login_revocation(self):
+ token = await self.account(' Alice ')
+ status, profile, headers = await self.request('/api/me', token=token)
+ self.assertEqual((status, profile), (200, {'id': 'alice'}))
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ credentials = {'username': 'alice', 'password': 'a long example passphrase'}
+ self.assertEqual((await self.request('/api/signup', 'POST', credentials))[0], 409)
+ for data in [dict(credentials, username='unknown'), dict(credentials, password='an incorrect long passphrase')]:
+ self.assertEqual((await self.request('/api/login', 'POST', data))[:2],
+ (401, {'error': 'Invalid credentials'}))
+ _, login, _ = await self.request('/api/login', 'POST', credentials)
+ second_token = login['access_token']
+ self.assertNotEqual(token, second_token)
+ self.assertEqual((await self.request('/api/logout', 'POST', token=token))[:2],
+ (200, {'logged_out': True}))
+ self.assertEqual((await self.request('/api/me', token=token))[0], 401)
+ self.assertEqual((await self.request('/api/me', token=second_token))[:2],
+ (200, {'id': 'alice'}))
+ expired_time = auth.monotonic() + 3601
+ with patch.object(auth, 'monotonic', return_value=expired_time):
+ self.assertEqual((await self.request('/api/me', token=second_token))[0], 401)
+
+ async def test_invalid_credentials_and_bearer_headers(self):
+ for data in [[], {}, {'username': 1, 'password': 'a long passphrase'},
+ {'username': 'alice', 'password': False},
+ {'username': ' ', 'password': 'a long passphrase'},
+ {'username': 'alice', 'password': 'short'},
+ {'username': 'x' * 65, 'password': 'a long passphrase'},
+ {'username': 'alice', 'password': 'x' * 129}]:
+ with self.subTest(data=data):
+ self.assertEqual((await self.request('/api/signup', 'POST', data))[0], 400)
+ for header in [b'', b'Basic example', b'Bearer ', b'Bearer unknown']:
+ with self.subTest(header=header):
+ status, error, headers = await self.request('/api/notes', authorization=header)
+ self.assertEqual((status, error), (401, {'error': 'Unauthorized'}))
+ self.assertEqual(headers[b'www-authenticate'], b'Bearer')
+
+ async def test_create_read_patch_replace_delete_and_export(self):
+ token = await self.account()
+ note = await self.create(token, title=' First idea ', body='Some details', pinned=True)
+ self.assertEqual((note['title'], note['body'], note['pinned'], note['owner']),
+ ('First idea', 'Some details', True, 'alice'))
+ path = f"/api/notes/{note['id']}"
+ self.assertEqual((await self.request(path, token=token))[:2], (200, note))
+ status, edited, _ = await self.request(path, 'PATCH', {'body': 'Edited details'}, token)
+ self.assertEqual(status, 200)
+ self.assertEqual((edited['title'], edited['body'], edited['pinned']),
+ ('First idea', 'Edited details', True))
+ self.assertEqual(edited['created_at'], note['created_at'])
+ status, replaced, _ = await self.request(path, 'PUT', {'title': 'Replacement'}, token)
+ self.assertEqual(status, 200)
+ self.assertEqual((replaced['title'], replaced['body'], replaced['pinned']),
+ ('Replacement', '', False))
+ status, exported, headers = await self.request('/api/notes/export', token=token)
+ self.assertEqual((status, exported), (200, [replaced]))
+ self.assertEqual(headers[b'content-disposition'], b'attachment; filename="notes.json"')
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ status, body, _ = await self.request(path, 'DELETE', token=token)
+ self.assertEqual((status, body), (204, b''))
+ self.assertEqual((await self.request(path, token=token))[0], 404)
+ self.assertEqual((await self.request('/api/notes/export', token=token))[:2], (200, []))
+
+ async def test_accounts_cannot_read_modify_delete_or_export_each_others_notes(self):
+ alice = await self.account('alice')
+ bob = await self.account('bob')
+ note = await self.create(alice, title='Private', body='Only Alice can read this')
+ bob_note = await self.create(bob, title='Bob only')
+ path = f"/api/notes/{note['id']}"
+ for method, data in [('GET', None), ('PATCH', {'title': 'Stolen'}),
+ ('PUT', {'title': 'Stolen'}), ('DELETE', None)]:
+ with self.subTest(method=method):
+ self.assertEqual((await self.request(path, method, data, bob))[0], 403)
+ self.assertEqual((await self.request(path, token=alice))[:2], (200, note))
+ _, page, _ = await self.request('/api/notes', token=bob)
+ self.assertEqual((page['items'], page['total']), ([bob_note], 1))
+ self.assertEqual((await self.request('/api/notes/export', token=bob))[:2], (200, [bob_note]))
+ for method in ['GET', 'PATCH', 'DELETE']:
+ self.assertEqual((await self.request('/api/notes/missing', method, {'title': 'Missing'}, bob))[0], 404)
+
+ async def test_filtering_pagination_and_invalid_queries(self):
+ token = await self.account()
+ first = await self.create(token, title='Coffee idea', body='Learn C++', pinned=True)
+ second = await self.create(token, title='Second idea', body='Coffee later')
+ await self.create(token, title='A separate thought')
+ _, page, _ = await self.request('/api/notes?q=COFFEE&limit=1&offset=0', token=token)
+ self.assertEqual((page['items'], page['total'], page['limit'], page['offset']), ([first], 2, 1, 0))
+ _, page, _ = await self.request('/api/notes?q=coffee&limit=1&offset=1', token=token)
+ self.assertEqual(page['items'], [second])
+ _, page, _ = await self.request('/api/notes?q=C%2B%2B&pinned=true', token=token)
+ self.assertEqual(page['items'], [first])
+ _, page, _ = await self.request('/api/notes?pinned=false', token=token)
+ self.assertEqual(page['total'], 2)
+ for query in ['limit=abc', 'offset=abc', 'limit=0', 'limit=51', 'offset=-1',
+ 'pinned=maybe', '&'.join(f'x={i}' for i in range(21))]:
+ with self.subTest(query=query):
+ self.assertEqual((await self.request('/api/notes?' + query, token=token))[0], 400)
+
+ async def test_note_validation_prevents_invalid_or_unowned_field_updates(self):
+ token = await self.account()
+ for data in [[], {}, {'body': 'No title'}, {'title': 7}, {'title': ' '},
+ {'title': 'x' * 101}, {'title': 'Valid', 'body': 3},
+ {'title': 'Valid', 'body': 'x' * 10_001},
+ {'title': 'Valid', 'pinned': 'true'}, {'title': 'Valid', 'owner': 'bob'}]:
+ with self.subTest(data=data):
+ self.assertEqual((await self.request('/api/notes', 'POST', data, token))[0], 400)
+ note = await self.create(token, title='Keep me')
+ path = f"/api/notes/{note['id']}"
+ for data in [{}, {'owner': 'bob'}, {'title': ''}, {'pinned': 1}]:
+ self.assertEqual((await self.request(path, 'PATCH', data, token))[0], 400)
+ self.assertEqual((await self.request(path, token=token))[:2], (200, note))
+ self.assertEqual((await self.request(path, 'PUT', {'body': 'Missing title'}, token))[0], 400)
+ status, pinned, _ = await self.request(path, 'PATCH', {'pinned': True}, token)
+ self.assertEqual((status, pinned['pinned'], pinned['title']), (200, True, 'Keep me'))
+
+ async def test_cookie_restores_login_and_logout_revokes_the_whole_login(self):
+ credentials = {'username': 'alice', 'password': 'a long example passphrase'}
+ self.assertEqual((await self.request('/api/signup', 'POST', credentials))[0], 201)
+ status, login, headers = await self.request('/api/login', 'POST', credentials, scheme='https')
+ self.assertEqual(status, 200)
+ cookies = SimpleCookie(headers[b'set-cookie'].decode())
+ morsel = cookies[auth.refresh_cookie_name]
+ self.assertTrue(morsel['httponly'])
+ self.assertTrue(morsel['secure'])
+ self.assertEqual(morsel['samesite'], 'Strict')
+ self.assertEqual(morsel['path'], '/api')
+ self.assertEqual(int(morsel['max-age']), 7 * 24 * 3600)
+ self.assertEqual(morsel['domain'], '')
+ cookie = f'{auth.refresh_cookie_name}={morsel.value}'
+ status, restored, headers = await self.request('/api/refresh', 'POST', cookie=cookie)
+ self.assertEqual(status, 200)
+ self.assertNotEqual(restored['access_token'], login['access_token'])
+ self.assertEqual(restored['user'], {'id': 'alice'})
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ self.assertEqual((await self.request('/api/me', token=restored['access_token']))[:2],
+ (200, {'id': 'alice'}))
+ # Cookies alone must never authorize notes endpoints, keeping bearer auth separate.
+ self.assertEqual((await self.request('/api/notes', cookie=cookie))[0], 401)
+ _, _, headers = await self.request('/api/logout', 'POST', token=restored['access_token'])
+ cleared = SimpleCookie(headers[b'set-cookie'].decode())[auth.refresh_cookie_name]
+ self.assertEqual(cleared['max-age'], '0')
+ self.assertEqual(cleared['path'], '/api')
+ for access in [login['access_token'], restored['access_token']]:
+ self.assertEqual((await self.request('/api/me', token=access))[0], 401)
+ self.assertEqual((await self.request('/api/refresh', 'POST', cookie=cookie))[0], 401)
+
+ async def test_refresh_rejects_cross_origin_requests_without_revoking_valid_login(self):
+ credentials = {'username': 'alice', 'password': 'a long example passphrase'}
+ await self.request('/api/signup', 'POST', credentials)
+ _, _, headers = await self.request('/api/login', 'POST', credentials)
+ cookie = SimpleCookie(headers[b'set-cookie'].decode())[auth.refresh_cookie_name]
+ cookie = f'{auth.refresh_cookie_name}={cookie.value}'
+ for options in [
+ {'csrf_header': False},
+ {'extra_headers': [(b'origin', b'https://attacker.test')]},
+ {'extra_headers': [(b'origin', b'null')]},
+ {'extra_headers': [(b'sec-fetch-site', b'cross-site')]},
+ {'extra_headers': [(b'sec-fetch-site', b'same-site')]},
+ ]:
+ with self.subTest(options=options):
+ self.assertEqual((await self.request('/api/refresh', 'POST', cookie=cookie, **options))[0], 403)
+ self.assertEqual((await self.request('/api/refresh', 'POST', cookie=cookie,
+ extra_headers=[(b'origin', b'http://notebook.test')]))[0], 200)
+ self.assertEqual((await self.request('/api/login', 'POST', credentials, csrf_header=False))[0], 403)
+
+ async def test_expired_and_invalid_refresh_cookies_clear_browser_state(self):
+ credentials = {'username': 'alice', 'password': 'a long example passphrase'}
+ await self.request('/api/signup', 'POST', credentials)
+ _, login, headers = await self.request('/api/login', 'POST', credentials)
+ morsel = SimpleCookie(headers[b'set-cookie'].decode())[auth.refresh_cookie_name]
+ self.assertFalse(morsel['secure'], 'Local HTTP demo cookies should work without TLS')
+ cookie = f'{auth.refresh_cookie_name}={morsel.value}'
+ # Access expiry is recoverable while the independent refresh credential is valid.
+ now = auth.monotonic()
+ with patch.object(auth, 'monotonic', return_value=now + 3601):
+ self.assertEqual((await self.request('/api/me', token=login['access_token']))[0], 401)
+ self.assertEqual((await self.request('/api/refresh', 'POST', cookie=cookie))[0], 200)
+ with patch.object(auth, 'monotonic', return_value=now + 7 * 24 * 3600 + 1):
+ status, _, headers = await self.request('/api/refresh', 'POST', cookie=cookie)
+ self.assertEqual(status, 401)
+ self.assertEqual(SimpleCookie(headers[b'set-cookie'].decode())[auth.refresh_cookie_name]['max-age'], '0')
+ for invalid in [None, 'notebook_refresh=unknown', 'malformed; cookie']:
+ self.assertEqual((await self.request('/api/refresh', 'POST', cookie=invalid))[0], 401)
+
+ async def test_rate_limits_slide_and_are_isolated_by_client_and_endpoint(self):
+ with patch.object(rate_limit, 'monotonic', return_value=100):
+ self.assertEqual((await self.request('/api/rate-limit-demo'))[0], 200)
+ with patch.object(rate_limit, 'monotonic', return_value=105):
+ self.assertEqual((await self.request('/api/rate-limit-demo'))[0], 200)
+ with patch.object(rate_limit, 'monotonic', return_value=106):
+ self.assertEqual((await self.request('/api/rate-limit-demo'))[0], 200)
+ with patch.object(rate_limit, 'monotonic', return_value=109):
+ status, error, headers = await self.request('/api/rate-limit-demo')
+ self.assertEqual(status, 429)
+ self.assertEqual(error['retry_after'], 1)
+ self.assertEqual(headers[b'retry-after'], b'1')
+ self.assertEqual(headers[b'cache-control'], b'no-store')
+ self.assertEqual((await self.request('/api/rate-limit-demo', client='127.0.0.2'))[0], 200)
+ self.assertEqual((await self.request('/api/health'))[0], 200)
+ self.assertEqual((await self.request('/api/rate-limit-demo',
+ extra_headers=[(b'x-forwarded-for', b'127.0.0.3')]))[0], 429)
+ with patch.object(rate_limit, 'monotonic', return_value=110):
+ self.assertEqual((await self.request('/api/rate-limit-demo'))[0], 200)
+ with patch.object(rate_limit, 'monotonic', return_value=200):
+ self.assertEqual((await self.request('/api/rate-limit-demo'))[0], 200)
+
+ async def test_concurrent_login_attempts_cannot_bypass_rate_limit(self):
+ credentials = {'username': 'unknown', 'password': 'a long example passphrase'}
+ with patch.object(rate_limit, 'monotonic', return_value=100):
+ responses = await asyncio.gather(*[
+ self.request('/api/login', 'POST', credentials) for _ in range(6)
+ ])
+ self.assertEqual(sorted(status for status, _, _ in responses), [401] * 5 + [429])
+ limited = next(response for response in responses if response[0] == 429)
+ self.assertEqual(limited[2][b'retry-after'], b'60')
+ with patch.object(rate_limit, 'monotonic', return_value=110):
+ self.assertEqual((await self.request('/api/login', 'POST', credentials))[2][b'retry-after'], b'50')
+ with patch.object(rate_limit, 'monotonic', return_value=160):
+ self.assertEqual((await self.request('/api/login', 'POST', credentials))[0], 401)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_wred.py b/tests/test_wred.py
new file mode 100644
index 0000000..b46537f
--- /dev/null
+++ b/tests/test_wred.py
@@ -0,0 +1,403 @@
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+import json
+
+from wred import App
+
+
+async def call_app(app, scope, receive):
+ """Drive an ASGI request and capture its actual outgoing messages."""
+ messages = []
+
+ async def send(message):
+ messages.append(message)
+
+ await app(scope, receive, send)
+ return messages
+
+
+class WredTests(unittest.IsolatedAsyncioTestCase):
+ async def test_callable_responses_control_asgi_delivery_on_fixed_and_dynamic_routes(self):
+ class Response:
+ def __init__(self, name):
+ self.name = name
+
+ async def __call__(self, scope, receive, send):
+ message = await receive()
+ await send({'type': 'http.response.start', 'status': 202,
+ 'headers': [(b'content-type', b'text/plain'), (b'x-response', b'asgi')]})
+ await send({'type': 'http.response.body',
+ 'body': f'{scope["path"]}:{self.name}:'.encode(), 'more_body': True})
+ await send({'type': 'http.response.body', 'body': message['body']})
+
+ app = App()
+
+ @app.route('/fixed', methods=['POST'])
+ async def fixed(request):
+ return Response('fixed')
+
+ @app.route('/dynamic/{name}', methods=['POST'])
+ async def dynamic(request, name):
+ return Response(name)
+
+ for path, root_path, prefix in [
+ ('/fixed', '', b'/fixed:fixed:'),
+ ('/dynamic/alice', '', b'/dynamic/alice:alice:'),
+ ('/api/fixed', '/api', b'/api/fixed:fixed:'),
+ ('/api/dynamic/alice', '/api', b'/api/dynamic/alice:alice:'),
+ ]:
+ with self.subTest(path=path):
+ async def receive():
+ return {'type': 'http.request', 'body': b'payload'}
+
+ messages = await call_app(app, {
+ 'type': 'http', 'path': path, 'root_path': root_path, 'method': 'POST',
+ }, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': 202,
+ 'headers': [(b'content-type', b'text/plain'), (b'x-response', b'asgi')]},
+ {'type': 'http.response.body', 'body': prefix, 'more_body': True},
+ {'type': 'http.response.body', 'body': b'payload'},
+ ])
+
+ async def test_mounted_routing_uses_complete_root_path_without_changing_scope(self):
+ app = App()
+
+ @app.route('/items/{name}')
+ async def item(request, name):
+ return {'route': name, 'path': request.path, 'scope_path': request.scope['path']}
+
+ @app.route('/items/special')
+ @app.route('/')
+ @app.route('/apiary/items')
+ async def fixed(request):
+ return {'route': 'fixed', 'path': request.path, 'scope_path': request.scope['path']}
+
+ cases = [
+ ('/items/alice', None, 'alice'),
+ ('/items/alice', '', 'alice'),
+ ('/api/items/alice', '/api', 'alice'),
+ ('/api/items/alice', '/api/', 'alice'),
+ ('/api/items/special', '/api', 'fixed'),
+ ('/proxy/api/items/alice', '/proxy/api', 'alice'),
+ ('/api', '/api', 'fixed'),
+ ('/api/', '/api', 'fixed'),
+ ('/items/alice', '/api', 'alice'),
+ ('/apiary/items', '/api', 'fixed'),
+ ]
+ for path, root_path, route in cases:
+ with self.subTest(path=path, root_path=root_path):
+ async def receive():
+ raise AssertionError('GET handlers should not read a body')
+
+ scope = {'type': 'http', 'path': path, 'method': 'GET'}
+ if root_path is not None:
+ scope['root_path'] = root_path
+ original = scope.copy()
+ messages = await call_app(app, scope, receive)
+ self.assertEqual(messages[0]['status'], 200)
+ self.assertEqual(json.loads(messages[1]['body']), {
+ 'route': route, 'path': path, 'scope_path': path,
+ })
+ self.assertEqual(scope, original)
+
+ async def test_file_delivery_and_root_confinement(self):
+ # Exercise the public helper through the actual ASGI response boundary.
+ # This guards MIME/status delivery and accidental exposure outside root.
+ with tempfile.TemporaryDirectory() as directory:
+ base = Path(directory)
+ root = base / 'static'
+ root.mkdir()
+ (root / 'index.html').write_bytes(b'<p>Hello</p>')
+ (root / 'asset.unknownextension').write_bytes(b'\x00\xff')
+ (base / 'secret.txt').write_text('private')
+ (root / 'escape.txt').symlink_to(base / 'secret.txt')
+
+ app = App()
+
+ @app.route('/file', methods=['POST'])
+ async def file(request):
+ data = await request.json()
+ return await app.static(data['filename'], root=root)
+
+ cases = [
+ ('index.html', 200, b'text/html', b'<p>Hello</p>'),
+ ('asset.unknownextension', 200, b'application/octet-stream', b'\x00\xff'),
+ ('missing.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ('.', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ('../secret.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ (str(base / 'secret.txt'), 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ('escape.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ('invalid\x00.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
+ ]
+ for filename, status, content_type, body in cases:
+ with self.subTest(filename=filename):
+ async def receive():
+ return {'type': 'http.request', 'body': json.dumps({'filename': filename}).encode('utf-8')}
+
+ messages = await call_app(app, {'type': 'http', 'path': '/file', 'method': 'POST'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': status,
+ 'headers': [(b'content-type', content_type)]},
+ {'type': 'http.response.body', 'body': body},
+ ])
+
+ async def test_dictionary_and_list_responses_are_json(self):
+ app = App()
+
+ @app.route('/')
+ async def home(request):
+ return value
+
+ async def receive():
+ raise AssertionError('GET handler should not read a body')
+
+ cases = [
+ ({'hello': 'world'}, b'{"hello":"world"}'),
+ ([{'id': 1}, {'id': 2}], b'[{"id":1},{"id":2}]'),
+ ([], b'[]'),
+ ]
+ for value, body in cases:
+ with self.subTest(value=value):
+ messages = await call_app(app, {'type': 'http', 'path': '/', 'method': 'GET'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': 200,
+ 'headers': [(b'content-type', b'application/json')]},
+ {'type': 'http.response.body', 'body': body},
+ ])
+
+ async def test_custom_status_and_json_errors(self):
+ # Covers the error paths that previously sent ASGI messages separately,
+ # plus a response tuple returned through dynamic routing.
+ app = App()
+
+ @app.route('/{name}/created')
+ async def created(request, name):
+ return {'name': name}, 'application/json', 201
+
+ @app.route('/invalid')
+ async def invalid(request):
+ return 'not a supported response'
+
+ @app.route('/input', methods=['POST'])
+ async def input_data(request):
+ return await request.json()
+
+ cases = [
+ ('GET', '/alice/created', b'', 201, b'{"name":"alice"}'),
+ ('GET', '/missing', b'', 404, b'{"error":"Not Found"}'),
+ ('GET', '/invalid', b'', 500,
+ b'{"error":"Response must be a dictionary, list, bytes, or response tuple"}'),
+ ('POST', '/input', b'{', 400, b'{"error":"Invalid JSON body"}'),
+ ('POST', '/input', b'', 200, b'{}'),
+ ('GET', '/alice/missing', b'', 404, b'{"error":"Not Found"}'),
+ ('DELETE', '/alice/created', b'', 404, b'{"error":"Not Found"}'),
+ ]
+ for method, path, request_body, status, body in cases:
+ with self.subTest(path=path):
+ async def receive():
+ return {'type': 'http.request', 'body': request_body}
+
+ messages = await call_app(app, {'type': 'http', 'path': path, 'method': method}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': status,
+ 'headers': [(b'content-type', b'application/json')]},
+ {'type': 'http.response.body', 'body': body},
+ ])
+
+ async def test_route_priority_and_method_registration(self):
+ app = App()
+
+ @app.route('/people/{name}')
+ async def person(request, name):
+ return {'name': name}
+
+ @app.route('/people/{name}', methods=['POST'])
+ async def update_person(request, name):
+ return {'updated': name}
+
+ @app.route('/people/alice')
+ async def alice(request):
+ return {'fixed': True}
+
+ cases = [
+ ('GET', '/people/alice', b'{"fixed":true}'),
+ ('GET', '/people/bob', b'{"name":"bob"}'),
+ ('POST', '/people/alice', b'{"updated":"alice"}'),
+ ]
+ for method, path, body in cases:
+ with self.subTest(method=method, path=path):
+ async def receive():
+ raise AssertionError('These handlers do not read the body')
+
+ messages = await call_app(app, {'type': 'http', 'path': path, 'method': method}, receive)
+ self.assertEqual(messages[0]['status'], 200)
+ self.assertEqual(messages[1]['body'], body)
+
+ async def test_query_parameters_decode_to_strings_without_reading_body(self):
+ app = App()
+
+ @app.route('/search')
+ async def search(request):
+ return {'params': request.query(), 'q': request.query().get('q', '')}
+
+ cases = [
+ (None, 200, {'params': {}, 'q': ''}),
+ (b'', 200, {'params': {}, 'q': ''}),
+ (b'q=coffee+ideas&limit=20', 200,
+ {'params': {'q': 'coffee ideas', 'limit': '20'}, 'q': 'coffee ideas'}),
+ (b'q=C%2B%2B&label=caf%C3%A9&x%26y=a%3Db', 200,
+ {'params': {'q': 'C++', 'label': 'café', 'x&y': 'a=b'}, 'q': 'C++'}),
+ (b'q=&bare&tag=first&tag=last', 200,
+ {'params': {'q': '', 'bare': '', 'tag': 'last'}, 'q': ''}),
+ (b'\xff', 400, {'error': 'Invalid query string'}),
+ ]
+ for query_string, status, body in cases:
+ with self.subTest(query_string=query_string):
+ async def receive():
+ raise AssertionError('Query parsing should not read the request body')
+
+ scope = {'type': 'http', 'path': '/search', 'method': 'GET'}
+ if query_string is not None:
+ scope['query_string'] = query_string
+ messages = await call_app(app, scope, receive)
+ self.assertEqual(messages[0]['status'], status)
+ self.assertEqual(json.loads(messages[1]['body']), body)
+
+ async def test_streamed_body_is_cached_and_headers_are_available(self):
+ app = App()
+ reads = 0
+ chunks = iter([
+ {'type': 'http.request', 'body': b'{"name":', 'more_body': True},
+ {'type': 'http.request', 'more_body': True},
+ {'type': 'http.request', 'body': b'"Alice"}', 'more_body': False},
+ ])
+
+ @app.route('/stream', methods=['POST'])
+ async def stream(request):
+ first = await request.body()
+ parsed = await request.json()
+ cached = await request.body()
+ return {'raw': first.decode(), 'cached': cached.decode(),
+ 'parsed': parsed, 'header': request.headers['x-example']}
+
+ async def receive():
+ nonlocal reads
+ reads += 1
+ return next(chunks)
+
+ messages = await call_app(app, {
+ 'type': 'http', 'path': '/stream', 'method': 'POST',
+ 'headers': [(b'x-example', b'hello')],
+ }, receive)
+ self.assertEqual(reads, 3)
+ self.assertEqual(messages[0]['status'], 200)
+ self.assertEqual(json.loads(messages[1]['body']), {
+ 'raw': '{"name":"Alice"}', 'cached': '{"name":"Alice"}',
+ 'parsed': {'name': 'Alice'}, 'header': 'hello',
+ })
+
+ async def test_disconnect_stops_body_reading(self):
+ app = App()
+ reads = 0
+
+ @app.route('/input', methods=['POST'])
+ async def input_data(request):
+ return await request.json()
+
+ async def receive():
+ nonlocal reads
+ reads += 1
+ if reads > 1:
+ raise AssertionError('Body reading continued after disconnect')
+ return {'type': 'http.disconnect'}
+
+ messages = await call_app(app, {'type': 'http', 'path': '/input', 'method': 'POST'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': 400,
+ 'headers': [(b'content-type', b'application/json')]},
+ {'type': 'http.response.body', 'body': b'{"error":"Client disconnected"}'},
+ ])
+
+ async def test_unsupported_protocols_raise_without_receiving_or_sending(self):
+ app = App()
+
+ async def receive():
+ raise AssertionError('An unsupported scope should not be read')
+
+ for protocol in ['lifespan', 'websocket', 'unsupported']:
+ with self.subTest(protocol=protocol):
+ messages = []
+
+ async def send(message):
+ messages.append(message)
+
+ with self.assertRaisesRegex(RuntimeError, 'Unsupported ASGI scope type'):
+ await app({'type': protocol}, receive, send)
+ self.assertEqual(messages, [])
+
+ async def test_file_read_failure_returns_404(self):
+ app = App()
+ with tempfile.TemporaryDirectory() as directory:
+ (Path(directory) / 'private.txt').write_text('private')
+
+ @app.route('/')
+ async def file(request):
+ return await app.static('private.txt', root=directory)
+
+ async def receive():
+ raise AssertionError('File serving should not read the request body')
+
+ # Simulate the OS refusing a real file read, including when tests run as root.
+ with patch.object(Path, 'read_bytes', side_effect=PermissionError('denied')):
+ messages = await call_app(app, {'type': 'http', 'path': '/', 'method': 'GET'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': 404,
+ 'headers': [(b'content-type', b'text/plain; charset=utf-8')]},
+ {'type': 'http.response.body', 'body': b'Not Found'},
+ ])
+
+ async def test_raw_and_custom_response_bodies(self):
+ app = App()
+
+ @app.route('/')
+ async def raw(request):
+ return response
+
+ cases = [
+ (b'\x00\xff', 200, b'application/octet-stream', b'\x00\xff', []),
+ ((b'<p>Hello</p>', 'text/html; charset=utf-8', 202),
+ 202, b'text/html; charset=utf-8', b'<p>Hello</p>', []),
+ (([], 'application/json', 201), 201, b'application/json', b'[]', []),
+ ((b'bad', 'text/plain'), 500, b'application/json',
+ b'{"error":"Response must be a dictionary, list, bytes, or response tuple"}', []),
+ (({'error': 'Unauthorized'}, 'application/json', 401,
+ [('WWW-Authenticate', 'Bearer'), ('Cache-Control', 'no-store')]),
+ 401, b'application/json', b'{"error":"Unauthorized"}',
+ [(b'www-authenticate', b'Bearer'), (b'cache-control', b'no-store')]),
+ ((b'', 'text/plain', 303,
+ [('Location', '/ui'), ('X-Label', 'caf\u00e9'),
+ ('Set-Cookie', 'a=1; Secure'), ('Set-Cookie', 'b=2; Secure')]),
+ 303, b'text/plain', b'',
+ [(b'location', b'/ui'), (b'x-label', b'caf\xe9'),
+ (b'set-cookie', b'a=1; Secure'), (b'set-cookie', b'b=2; Secure')]),
+ (([], 'application/json', 200, []), 200, b'application/json', b'[]', []),
+ ]
+ for response, status, content_type, body, headers in cases:
+ with self.subTest(response=response):
+ async def receive():
+ raise AssertionError('These responses do not use a request body')
+
+ messages = await call_app(app, {'type': 'http', 'path': '/', 'method': 'GET'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': status,
+ 'headers': [(b'content-type', content_type)] + headers},
+ {'type': 'http.response.body', 'body': body},
+ ])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/wred.py b/wred.py
new file mode 100644
index 0000000..b42f7e2
--- /dev/null
+++ b/wred.py
@@ -0,0 +1,242 @@
+"""
+Wred: An ultra micro ASGI API framework.
+
+Wred is designed for lightweight JSON APIs, offering a minimalistic way to build
+ASGI-compatible web services. It prioritizes simplicity and performance over all.
+
+This is the amazing magic version of Wred, Wred-embedded ;)
+Original Wred website: https://harrisonerd.com/wred
+Wred-embedded website: https://harrisonerd.com/wred-embedded
+
+Copyright (c) 2026, Harrison Erd. For license information see `LICENSE` AND
+`THIRD_PARTY_LICENSES.txt`. Bundles `uvicorn`, `click`, and `h11`.
+"""
+
+__author__ = 'Harrison Erd'
+__email__ = '[email protected]'
+__version__ = '0.03'
+__license__ = 'BSD-3-Clause'
+
+from collections.abc import Awaitable, Callable
+from asyncio import to_thread
+from mimetypes import guess_type
+from pathlib import Path
+from typing import Any
+from urllib.parse import parse_qsl
+import json
+
+
+class WredResponseError(Exception):
+ """Raised when a handler returns a non-JSON response."""
+ pass
+
+class WredRequestError(Exception):
+ """Raised when a request cannot be processed (e.g., invalid body)."""
+ pass
+
+
+class Request:
+ """A request object encapsulating ASGI scope, receive, and send."""
+ def __init__(self, scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], send: Callable[[dict[str, Any]], Awaitable[None]]):
+ self.scope = scope
+ self.receive = receive
+ self.send = send
+ self.path = scope['path']
+ self.method = scope['method']
+ self.headers = {k.decode(): v.decode() for k, v in scope.get('headers', [])}
+ self._body = None
+ self._query = None
+
+ def query(self) -> dict[str, str]:
+ """Parse and cache query parameters; repeated keys keep their last value."""
+ if self._query is None:
+ try:
+ self._query = dict(parse_qsl(self.scope.get('query_string', b'').decode('utf-8'), keep_blank_values=True))
+ except UnicodeDecodeError:
+ raise WredRequestError("Invalid query string")
+ return self._query
+
+ async def body(self) -> bytes:
+ """Read the request body from the ASGI receive stream."""
+ if self._body is None:
+ self._body = b""
+ while True:
+ message = await self.receive()
+ if message['type'] != 'http.request':
+ raise WredRequestError("Client disconnected")
+ self._body += message.get('body', b'')
+ if not message.get('more_body', False):
+ break
+ return self._body
+
+ async def json(self) -> dict[str, Any]:
+ """Parse the request body as JSON."""
+ try:
+ body = await self.body()
+ return json.loads(body) if body else {}
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ raise WredRequestError("Invalid JSON body")
+
+
+class App:
+ """A minimal ASGI application framework."""
+ def __init__(self):
+ self.routes: dict[str, dict[str, Callable]] = {}
+ self.pattern_routes: dict[str, dict[str, Callable]] = {}
+
+ def run(self, host="127.0.0.1", port=8000, workers=1):
+ """Run the bundled HTTP-only Uvicorn server; load it only on demand."""
+ import base64
+ import signal
+ import sys
+ import tempfile
+ import threading
+
+ if not isinstance(workers, int) or workers < 1:
+ raise ValueError('workers must be a positive integer')
+ application = self
+ if workers > 1:
+ namespace = sys._getframe(1).f_globals
+ name = next((name for name, value in namespace.items() if value is self), None)
+ if name is None:
+ raise ValueError('Multiple workers require a module-level application')
+ application = f"{namespace['__name__']}:{name}"
+ if any(name in sys.modules for name in ('uvicorn', 'h11', 'click')):
+ raise RuntimeError('App.run() requires uvicorn, h11, and click not to be imported already')
+ pending_signals = []
+ handlers = {}
+ if threading.current_thread() is threading.main_thread():
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ handlers[sig] = signal.signal(sig, lambda sig, frame: pending_signals.append(sig))
+ try:
+ with tempfile.TemporaryDirectory(prefix='wred-') as directory:
+ archive = str(Path(directory) / 'server.zip')
+ Path(archive).write_bytes(base64.b64decode(_EMBEDDED_ZIP))
+ sys.path.insert(0, archive)
+ try:
+ import uvicorn
+ if workers > 1:
+ from uvicorn.supervisors.multiprocess import SIGNALS
+ for sig in SIGNALS:
+ handlers.setdefault(sig, signal.getsignal(sig))
+ uvicorn.run(application, host=host, port=port, http="h11", ws="none",
+ lifespan="off", loop="asyncio", interface="asgi3",
+ workers=workers, access_log=False)
+ finally:
+ sys.path.remove(archive)
+ for name, module in list(sys.modules.items()):
+ if (getattr(module, '__file__', None) or '').startswith((archive + '/', archive + '\\')):
+ del sys.modules[name]
+ sys.path_importer_cache.pop(archive, None)
+ finally:
+ for sig, handler in handlers.items():
+ signal.signal(sig, handler)
+ for sig in reversed(pending_signals):
+ signal.raise_signal(sig)
+
+ async def __call__(self, scope: dict[str, Any], receive: Callable[[], Awaitable[dict[str, Any]]], send: Callable[[dict[str, Any]], Awaitable[None]]) -> None:
+ """Handle ASGI requests."""
+ if scope['type'] != 'http':
+ raise RuntimeError(f"Unsupported ASGI scope type: {scope['type']}")
+
+ request = Request(scope, receive, send)
+ await self.handle_http(request)
+
+ async def handle_http(self, request: Request) -> None:
+ """Handle HTTP requests with routing and JSON response support."""
+ path = request.path
+ root_path = request.scope.get('root_path', '').rstrip('/')
+ if root_path and (path == root_path or path.startswith(root_path + '/')):
+ path = path[len(root_path):] or '/'
+ method = request.method
+ try:
+ # Check exact matches first
+ if path in self.routes and method in self.routes[path]:
+ handler = self.routes[path][method]
+ response = await handler(request)
+ await self.dispatch_response(request, response)
+ return
+
+ # Check dynamic routes
+ for pattern, method_handlers in self.pattern_routes.items():
+ params = self.match_path(pattern, path)
+ if params is not None and method in method_handlers:
+ handler = method_handlers[method]
+ response = await handler(request, **params)
+ await self.dispatch_response(request, response)
+ return
+
+ # No match found
+ await self.send_response(request.send, {"error": "Not Found"}, status=404)
+
+ except (WredResponseError, WredRequestError) as e:
+ status = 400 if isinstance(e, WredRequestError) else 500
+ await self.send_response(request.send, {"error": str(e)}, status=status)
+
+ async def dispatch_response(self, request: Request, response: Any) -> None:
+ """Invoke an ASGI response or serialize an ordinary handler result."""
+ if callable(response):
+ await response(request.scope, request.receive, request.send)
+ else:
+ await self.send_response(request.send, response)
+
+ async def send_response(self, send: Callable[[dict[str, Any]], Awaitable[None]], response: Any, status: int = 200) -> None:
+ """Send JSON or (body, content_type, status[, headers]) with string header pairs."""
+ body, content_type = response, 'application/octet-stream'
+ headers = []
+ if isinstance(response, tuple) and len(response) in (3, 4):
+ body, content_type, status = response[:3]
+ if len(response) == 4:
+ headers = [(name.lower().encode('ascii'), value.encode('latin-1')) for name, value in response[3]]
+ if isinstance(body, (dict, list)):
+ body, content_type = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode('utf-8'), 'application/json'
+ elif not isinstance(body, bytes):
+ raise WredResponseError("Response must be a dictionary, list, bytes, or response tuple")
+ await send({
+ 'type': 'http.response.start',
+ 'status': status,
+ 'headers': [(b'content-type', content_type.encode('ascii'))] + headers,
+ })
+ await send({'type': 'http.response.body', 'body': body})
+
+ def match_path(self, pattern: str, path: str) -> dict[str, str] | None:
+ """Match a path against a pattern and extract parameters."""
+ pattern_parts = pattern.strip('/').split('/')
+ path_parts = path.strip('/').split('/')
+ if len(pattern_parts) != len(path_parts):
+ return None
+
+ params = {}
+ for pattern_part, path_part in zip(pattern_parts, path_parts):
+ if pattern_part.startswith('{') and pattern_part.endswith('}'):
+ param_name = pattern_part[1:-1]
+ params[param_name] = path_part
+ elif pattern_part != path_part:
+ return None
+ return params
+
+ async def static(self, filename: str | Path, *, root: str | Path) -> tuple[bytes, str, int]:
+ """Serve a small file inside root; read its entire body off the event loop."""
+ def read() -> tuple[bytes, str, int]:
+ try:
+ directory = Path(root).resolve()
+ path = (directory / filename).resolve()
+ if not path.is_relative_to(directory) or not path.is_file():
+ return b'Not Found', 'text/plain; charset=utf-8', 404
+ body = path.read_bytes()
+ except (OSError, ValueError, RuntimeError):
+ return b'Not Found', 'text/plain; charset=utf-8', 404
+ return body, guess_type(path.name)[0] or 'application/octet-stream', 200
+
+ return await to_thread(read)
+
+ def route(self, path: str, methods: list[str] = ['GET']) -> Callable:
+ """Decorator to register routes with specific HTTP methods."""
+ def decorator(handler: Callable) -> Callable:
+ routes = self.pattern_routes if '{' in path and '}' in path else self.routes
+ routes.setdefault(path, {}).update({m.upper(): handler for m in methods})
+ return handler
+ return decorator
+
+
+_EMBEDDED_ZIP = 'UEsDBBQAAAAIAAAAIQDJnm/E/AQAAAcKAAAeAAAAY2xpY2stOC41LjAuZGlzdC1pbmZvL01FVEFEQVRBjVZ/b9s2EP2fn+KqDmgKxHK7rMAg1EEzJ8OCpWlQtwOGIHBpiZa5SKRGUnHcYd9970jZqbesaIDA0vHu3buf1FsVZCWDHP2mnNfWFPR9/oO4lK0qqGx0eSt2Bz/mr/IXYta3rXSbgqa27ayXi0ZRaSEzFTXaKNImKLeUpaJgbXOrg3grIcO/ciPVSt0UdCWbRgVPr0uLkzK86ZKgc/YPVQafA/FYvFd/9topP7rahBUzOJ4c5S9fiFPlS6e7AFajKQCUCaMPmw6Mg7oPY9C7rezaiAtdKuPV6Oy+A0oK4qfZ6ehoNG1k75XAD8RLrVxBp+pONbZrAUazIEPvqSjoFY3oytmqL9nbGAeId8/unP1XqqKTvtLKIOxiB4bM7em+g0QGbWqabXxQLau+mwGiUh2DmLCnDse1k23LBhfS1L2sI3pKx54qwmetIj6pahf6z7pBWi7Op2eXs7M83AdxlVI8+vj+AjVcAVb5Q1qF0PliPI4lzx+pxriD83GZ9Mf/QQkPEJX2pXVVXtfjAWdf+9SWPWdZcka/xfO/zY0M6sHuMYsq6uzbzWzvyi/sah1W/WIILUIkCmMhXlf6jmSjazPJkEa0c3b8Wrc1eVdOsi2Ak+s8gaCVXJn68BE8p5Z+vFKy8mMf2wf0Sj+ee85AmZRGBgOX+7s6g98wyTJaKV2v8PTy1Qs4R1LvjoV4StM4kiL+kPYkh2agTpa33B5L66h0KnXZQsk+6GXf/M+EeqENIMqHSV7LDa0RFEkP3RDicFeKX42Cgcfo53QennkKK0XZNOGi34B7vpv8aWQAWr/qkA36KwTUbABnlrruXXS36AN7Vz46FR49q1leqaXsGywICwW7jL4W9j4X4jyQ1C28W2rlrYonqD1TY8W10zHyvXh5D3nCLkHOWLjsjVivMBlItrcwx7SaaCbNhpau98El+iWviYoWm+hHG7nQyArerNBt16i4LaSJGY1LYHpxTidX5/muRAaWToGjhY4vhBgR0YlbaLhwGyTVR8egPlD2SaMPtuX+QCM0HfHwUa2MSryiyqzvOuuQo0Z+3lBjZTUA+X6xxSIZyPWIrVUC7fOUTmgWidPZveRfIT59+tSlfYID4A1bX7xJAzkgHTzfCmzcuwfZaFRaIGeH22JNXh5GspPssm8XyjGVGqFzfD7PHgHgpoc9ytd2aPXfMaE0yBLQBy4uligqgXpHMAaCQ1Zo7EGkcBiNnhcCSaEsy4YIu7Q9kX8kIdp6ujx5exZHRAIwyIY5Tt99vPxAnCKwzLKIwipzLp7jfZfcDA74LwWiypU9WGa/MJND+otJ/P0E9IRe0nzOr/M5TSb0bD7HrWfm82cJIVF/zpmP6RffUapAOsm7DQ3JnRyJXVKKYfYHd/HlyVffIj4XPa1MITid25vXuloa/Tm1eZWuKx+nw2/7KnUwiyy636GFu76RTgyrxmOuDXAqHMXq2HWcEu6Y3vCUxKY2wWmMuXUJnXcl7hveGWAymLS7D4Q42KBjA6TWqVgXlg1zHnf8IV2jwBIXeFr0OK/k5ub6BmM3nNDeSfEttwVnarqli54VYoa55fRfV/hIwrqoHsLhUau+vMhuroezm9g8LW8SLFOOR+yscP1oUzZ9nFSnOM38pL3v+RJ2+N4Z9sES+7N3LJT+Ns41etb4tXLMLKlZkxLJi5BVrt57TsCWx9eD/jISXHn/AFBLAwQUAAAACAAAACEAD3wEs1MAAABSAAAAGwAAAGNsaWNrLTguNS4wLmRpc3QtaW5mby9XSEVFTAvPSE3N0Q1LLSrOzM+zUjDUM+ByT81LLUosyS+yUkjLySxRMNYzNAIKB+Xnl+h6FusGlBal5mQmWSmUFJWmcoUkplspFFQa6+bl56XqJuZVcgEAUEsDBBQAAAAIAAAAIQBMlG2NAQMAAMMFAAAqAAAAY2xpY2stOC41LjAuZGlzdC1pbmZvL2xpY2Vuc2VzL0xJQ0VOU0UudHh0tVLLbts6EN3zKwZdtYCQ29eqXdESbROQJZWk4uulYtENAUsMJDpB/r6HtNM47d1eL2yaMzyvmdw/PE/u532gzx8/faWmOx5tmBlTtndzmNzdKTg/Ujf2dJotuZFmf5r2Nt3cubGbnungp2HO6MmFe/JT+vWnwAbfu4PbdxEgo26y9GCnwYVge3qY/KPrcQj3XcCXBcjx6J/c+JP2fuxdfDTHR2yw4Rtjn26I3mqayR9exOx9b2k4zYEmGzqIjIjdnX+MpYtBRviMPri9zVB3Mx2BFkGuCcf+DzWg3B87N9jphrHP/6ECbFc5vKiAwf4EZf+HEDobTDi9358GO4buZUz/YAIe1YmGLtjJdcf5Ne00ogh7bQG+vsBXZV16FstjN9goKJ5/66Z7f+zRMPrXpjQEh4WJWiD/DOqnGezPdGfj0sCIJzv2uLVxP6Bm8MHSOaIwE0DdI9oOKCSgFMrsD+Epbs1ln2h+sPu4UHjo4ppNcZXG81LN89mIWUtNul6aLVeCcG5UfSsLUdBiR2YtKK+bnZKrtaF1XRZCaeJVgdvKKLloTa00e8c1Xr5LBV7tSPzbKKE11YrkpiklwICueGWk0BnJKi/bQlarjABAVW1YKTfSoM3UWSL9+xnVS9oIla/xly9kKc0u8S2lqSLXEmScNVwZmbclV9S0qqm1oGirkDovudyI4gbsYCRxKypDes3L8q1LdnYZtV97pIWgUvJFKc5McFlIJXIT7byeciQHfWXGdCNyiQPCEDDD1S67YGrxo0UTilTwDV/B2/u/I6FLJCxGgpnkrRKbqBk56HahjTStEbSq6yIFrYW6lbnQ36msdUqr1SIDg+GRmAECUaGM3kWrZQpNVkYo1TZG1tUHzHeLWKCR42mR0q2rZBUJ1WoHUBYzSOFntF0L3KsYaEqKxwg0EssNXbWBDwGaK4+sEqtSrkSVi1itI8pWavEBs5JQtoqQkXbLwdkmy3FGUHU+Ss1eNjZLkyS5JF7cyij70ozZa3nZkxRZvr7EfcN+AVBLAwQUAAAACAAAACEAZqfQEtIEAAAZFAAAEQAAAGNsaWNrL19faW5pdF9fLnB5zVhdb+QmFH33r0B+mUTajtq+daWtlM3HNtJuEiUT9WUlC9t3xjQYLMDJ5t/3Av7AYzw7kRqp8+LL5ZwLHOACk6Zpcs5Z8USYJpRoVjccyN2rqaQgtSxbLDGhG6agJPkrMRUQbUrOciIb01ClgRhJavoEyYtihokdKWRdU1ESzgSCC8Uao8m2FWvyKDh7AiIxiuqi6w+EmZUmOdVQJlTJFplDR87urrFJakgpQRMhjQ0O5IWZCpvFdtuiwsZ3rCC2RaYTrG+kpjmHdZLi4JKtkjXJsm1rWgVZRjCwVAbhGI0aJoXuMOtCKuirz9SurUEgTg92BHbeDRVRnbkMOpecQ2FbDOCjM0oUBn4YD3dmBPQFJWssxBkRwG3Tt+mtCOSOKlqDwUlB1FA4BHyQrSpgAveujlQC0qiRSg+CB4rSqaJzcDHqWkx0jUHFlqnaTWUmh7FG3MshWm1knT2D0ntBYhWLYXb9TOyCmZjDKuBN0EZQXKTY7ZU1VOtsqLPUiHsxxNjgT9py8Ypx5YXlwySZ/zMQ0D4IfpGqDETYcy1S51MUnRv4UYArD8Sz3K1B7Y1F2Gda9rv9UdOdW+D7vkNkv8NCauA5RJzswLC8SHJZ+7J3uxwx8SwSrxiHS6X8EhoKi/BvTGvM6pMO7vsWyTfyATN0kCUnjp/QxsQVlhdJTuNhYGOpI2ylzQbugOoIf+HOu/JeP6yJY5H2omiT9btjKHTwHZc55WNKAJMVrVK4dsI9FXF3fGy4btmQmDhQ1zNnxCE+zwUpLwqDopLZM6OYLnZ+sFNPnFQy1137jQLsOBw92+I66kc2epZIReWH1ZlRGKetwMMdUd6KghraavAJBI04RMmdAq1z32RQXILXjemQaEVB2opnMc6IQ8yrV8QZUUgrBlBn9rDXBsbkcHv71SUF/MbqzyvJ/EnsrRjmghrYsNqhejuGu+pm8YrF+3L19fZs4wDWiCK4pOaeCp//xlIMe33jYuEnWivGOL0dw7kktEHPcBmxhTjSVB5kqlj9w+b++uaLRXgrhtm0jZfIGTHE483d/e355cPD5YXLQ2Mxin689jD8dvWtYWMC6RdasM4m9T49uc0mqJ/gPVeMZXcpbZqsZP027IsxtGxADBt8KCRJUsIWb9bIxgSpsuzENvcRF7w6Jb/8SfAKgDfbjwnBX586qRKYSfHG7Zxb4vr86RNJP+P1vzsVUk+xv/kFNAuAyYDr466tcTK47S9dBYyVfeWU0CgocBeU7s3wwjgnORAFtXxGHxP+ICV/rH/FF4uGdBqPrMZguGuBluv0wwRy0TWAh9PfvmPTem1o8cThGfin38ea08FSgE8VERlqKNi3lht2lGIh8kjJQsp/o5l7nLy7YvOhhpL5+8OdfbOqmWTuKasG0ULskaKFlLeLtqlmoklekr5b+GZ6pozbd60lrvrH9+q9pJwLEEqZDW+iLFCyE89/OMvXeDmkJTX0OAVRArIKIq+IzSssbw0cJee+fr26v7klSbZA7V8AGMd0z3CpZut03vd115+T72lhA35PT997HR/ohO9DejqfEpvEcyaoes0wAwOtZ0t8ktSzGf7IZT7jvXmtv9OKXRjQvkb2sn28QgH6DfoErP+TOrPBKMpwY5z1u8w9l9wpfpr8C1BLAwQUAAAACAAAACEAdaGOVYUSAAATRgAAEAAAAGNsaWNrL19jb21wYXQucHntPGtz4zaS3/UrUJwPkVISZ5LZyqV867t1HGfPtUmcGnvPu+V10RAFSogpUiFIS9rXb9/uxoMAScme2cl8uZuqzIgk0Oh3NxqNZFW5ZkmSNXVTiSRhcr0pq5rxoihrXsuyUKOReZeWC5Gq9inPRUojYj5PGVcshX/tZ1naX6WbUgn7S+3dy3q/kcUSp9ejDJGBF0JZPG4qnoo5Tx9v4K3+vhX8sRKZHXELj38Q+28l4cKr/Wh0/uff317+yE5xmXiT8zorq3Wsal7Vaivr1ThK98utLKLJ6JlxetArdlUIdn59yYRK+UYwJX5pRJEKthEVq1eCXZz/cDb7zddsWfH1mlcnbMPhl6jh83xfAznjN7u3b2bw13eTKcCTBXxai4XktWhHfIkjvoQRCLNgnGWy4Dl9x8+/wc//cTGJ2TdVyRe0NC9wLEC8/v07EpBi57lMH5lYy1pNmSoZECXksnBIw1Igu7KYLUQuYZRYsLpqxAxfVlO2LhuFACuB7AXRTBivgOS6kpsNji3LeJTwQskEXp/CuDgt1xuZi3EV/eXN27d/ubt7M/vv+8/v2Ow1/P272T/vgYej0QJklqz5o0hqsasTACj4ejxi8Ef/PmF1/I1EEV5eTek9oFsuAIUTHMH+zn4sC2G+VFVZqf57IDYVgBhf8HkuTti8LHNA8jueq2DEtpL10IgJm/0XoHEDGF5endAEmTk8mFS0lv7gYwgQlqJO5kLViX031mRNHBTCeQCGfn/KIuB5Dvoe0adKgEkWLIHB53mpAKDG6rbiIIhq7ObrZaY9nKadFdrnXBYimTdZJioYd3oD4m8/hiw8DR+7wywfT8NHPcxKXaqEq1TKljOBYInnKAbNkiiKzlcifVTIM86W8glswZcAwYphGA2vq33LSsMz7anivCwfm41bbBIXYJPsFBhNIPR8sUvFpmbf09gLZFQPHOmGoeWQlFF5L6/u6vis2N8TRfDeEfSOACnyFQCFN3ltxOZRljHwucDXplg46qonrVm8riuz0pRFdk40JVWasLIiHwYDDXSHndO+vhCqp0mP1Kips9nXgQJWT0B6mnOlDuuiLOPghQFMJp/IQtZJ4umryLNpR3v7tn/c/o/5gJf5ged9gR31+edAWsURRRCuUW2UcGjGSFZs3BrGFPujjlOu6pZ8UtqWWJZ8J3fN5lq7QyvijtV1MJ04YO0v1aAgJrHjtwXl3IF1A5YgMM9WSBAMcA7QMEBaYGOO1gWoZboatygYU7qgfyAYh3M2oEHtilKBSu/bBVv7xz+v2KquN+rk9eu5rOdN+ijquKyWrzf7zV7/JZVqxOsvvn7ztqvCvhxis8ykVWGP2848b8AsC7GFpEVH5gycMLwQCwXhEKIc5R0aotJB1zwYD8QXkpKPPN9jWpKLtSjqmLEzBWJJVxjPwW9lcjdrNuSc0I0VAmKxAiWANXUULME7pbJKmzXkIBipY80vRA8VAL1EmTGrFZCkLZhVCZblfKkoVIOTgb/nIuUQyTVUCNq5RnbT1GzOF4DohtfpCgJ6Of8Z8jjFygJ9CBuD5AlrWCoTBaYZCDQVVc1lwZ5EpYBUggW4/Nxs9pjlgOcSIMLHSWx5OvooHuBj2fHkGTvtfA2XhVHhi8HRThKnHXx8ThhXbkxtyjAitUGQHEzPJzv37yGuZ3oWjKh9YYAq+VcACpqsLQvTyxZo5geUAGJEMGxMGbkZEDwyUliITqFf8JA0Xo4WA3c1RhwmHpAh48T19MCQDmTaIc8AyAyJaBAnzGvc+91xuhGII91bbPdiygHNKduNJ5Nn3GZA/ZuXes8gFRmi03HQqt0LOeis5t/hoAXyaTmIq4rxPIpeysQeuJeAfB7s+8lHCfF4TD5H2Gynflo246oBJnEt8tyf/++qrt4bQoY6pwBAVi2qocx6atPn0Ml32Di0J5CQteqwOg4NcKo95GR0nBIDxizvZSo3GNtXmPZCgg8RUodxrmCjvaVIjEm/CTdbDiE6x4X3HoQUUmqxgHzhstCZAs6esi1EeMhy1sLfNsR9bpHOfixuDdvBYbb0VccH0LWjQVV4Qdp4RAgDipTJYvG8KpmIa3MOs4XQi79itzoBhMwJ8rZlA2IsIXMCO0NZ6D2pkal0ImV6zXjkKUYKyaJWDpeRIQTIrWQqec4WZaqAhrRcQ864YDqnhkTPwGh1RyEq4BfMIvYtaM01KRxmlWu5XNUAkrQIiz8GyrYlZiFgUSxutZoGAtjkgE2dG9TlYWOcGg3q2YVxLv7GxpY/aOi8yYZ2sroIEeYcr9g17tOVrBuqQDK0ov/U+NYSvviGwWvikYYDsjAQeAqTMR033AK7JFJl/Zmy9mZpRdQ830lpdZ9+GDUlLXsR7TDaUGPGIOQh/TxsvP+vny/Tz4CD/wf109D/EfVTU49LrKVKy0Ir2MJTUl3s6RfuzjylC+ZSBQo2mX4d7+z6/PLSVbpescvMD5WLUmiaV/wJaW6nKlH7wZFYBu+sRpYGMNMKjrrNK9JOo+fIa7ObbWBjCrHfzhXFk6zKAnfvqLiXCBlR+KUBFoNYABDEdCPStKzALkDfBWAoS0+486bW2gAjYY6qQaHzXO8KTXkesEGjgt005QurchuPgmSlW6x7UQ3Q1DUnXo6AtXluq+2JB6IV45Tha9qFTtkTzxvh19SOCBmnSSCWyhl6IYkbYJI1vhO/gJIjrXhGofNfJVEbaBXEWGc2/kuY1hQgTlJz/OxWIQgrzKD00LbU0OYdiQbSt2R88LNmux8NZp22iI0dNs7mgrGeQfbZjVw4dsph+T5Y3xyoa44OlMh1hVzD/cwzLsTWnCo47lFtiGZ7kjKOtiMDFXLWHkQMa1P3GCLQTPvT1Eyt5zqolDib8I4sGxxz9W7VGF2fud6LMAX+wIMk518R2jm4atx33d0FyTUKxP2jD5fayH5wHszoRfX7X/P0ypEy9pjUD5RButPWrf2o4c03+wIA4e0F2s/+fKvtvdlDbh/PbHxnMtVVzrL4rHYnJ+BQuAcgDDTkAr2it/Wo4FDUTBrV9rKIAxYbsKpbQteajPYflvW754Ra4Q+F00F+TIa37u3QkUf5FZZ6txI3i5h5IRsbrNjme0TCpBZaoC3dm1IpIrcrcE95Q9y6wuI2LlpQWHLMIWhCPuMSyEou6fRag/dAIE8Q23X5s5zzR3ClsC+HQVhOFnYPGMrpAMq9k9TnONaleBiuzfK+NepmPCmFbDqf9THGc3FIAiCXKCs6kqck2EDA0F5jtqwTwW1ZPar4A46Dbc5f8Y3OJv0UG9fmRK+rMlivbryly26MdrrmBBUkHQcO53uMe5+T5o9/mDwUCMwWrQ0E+sVHCQTHffKAx7XcfCZgdXB9H6b2Nqfep37t472EcYzLZqPRclm/+Ihcfo+49r5c1rh+IJe7kwd28B+ky9RMoKGoGrAZd/b8llTjrgYLW3sV01x30H/IOVYcAgV71xS4R6VGh3F0y7Ujp3Mr2n1jX5Isui4mMw0GtFIcBSm0dZoDFJVNPUyS5lmXJLuT1wvBZEeTGf9r0QRLdYjSCw4SBXry4UTB5E9FFCz1HFHGXFDzhkwWSDm0IzGfhgwTO1bwnDPZAhfKLWZYhSohyBq7dHo0kFc5HX4aPs1oW1Ke8QFd6xhqg+i4Q6qdDDAHlfiTcwcW/TXZ07O0w/xxjuwQf9AePjl/AOan48/gYkf5A2/5JpFlUm5EMTZbw9zVVUoV/8Tr1ffyUdzBm3t4JYtax4Z1udDjPiSUGo66cOzKBf8DWXduCmp4ooJp4MODBf7wQFk5vCCY8Ag5pXEsiI8r0gFvo3mEXwnNLmOJWiR0St/DCmPnY8vR0y5rT8OtP04MgjoCcY0TA+zsMJJS6egwO62iHuAqTtdZvoHB63It0+EcpdnAbr9fJNCcMhw99XkYEASfgJZMwYZ0NbYv26pzDULi1cIV3DNZqTrG9iCIF9SkS81HfiF/z+SyKPUhpC09E/rULxSzP8KwTC0EleepRIp6wpCd42gWTdxuhfDS41rMqJdyFgW9BbzYj9eWOopH9HQXbaMpizj+tYvuO9tcrHLrqsmhg/WBxGLaOUfsjB7y3wdVrQvrAEKDyGDeNh0+3j4QZJ9Fw4gK1G9mxEUeSbGFRCcF++xlSQc+9aoqm6Xe7VVi2eS8InthWVOk3kbaNJca1e2iGborK96unTo3O/U6Gl7p451G8bnMZY2JSJNlJPcAc+c++JD7oIznf7H2qPOdgOnRGZbtaeeKlZ+CiZ1U2JlOdmM9vWo22LAuFlNP/3kdhaBgD97kC8DgqczNscIOD+4lPDyk5Wb/MAMeVPpcB1eD3QPeMwBRdyHh4jG7QmYj9zUuBeBSrXnOHrYPM7IAdKwI2DvF6kKSrqhChwv7svkMLJZnEILiqNPqiSzcvYSF0R9NJeYBTxlICA+z9lqCKWHYDBHBbiNi5AtAnwU6WRagj1g7YAo4aYrKln5cwChKOAv9FTVXYgEOGMNraoG4QZ+FhRI235MmUz2buGv9Fx4AbKpyR6dmVDpE15mVOfUq6lODAjRS113gywLhmNnWLjgpK0gYVZ+OK7CK8tO+XoHgtWgRlwIPmvwhRceLotSpuLMGNqtWE+hoB0DRWZ6xQX1hBYyoKP0XFeBcmipV0E4BirimPr42EpWYkPC6dcHwmLg4Yjoprq47fewIyEQ5vYxuFiVwV8m7b2/f6Sh6lZy/uzi7sQ8Xfzr/fjQ64A41iL+3Jy0lpETRVfLN5Y9n7/4Mbv6NEfx2hfxAj+FVitebJAx6GPLin0tZhLZvv4Dfw7Et3dNgWBbFSaJFMiP9+ptmaoz/wH9LMf6C/fa37O2Xk5M3X+/+EU0HOqh7rSzZQuNGXtFHeaqpBxrLr776iiqUyGFb9cWqOL0Jm1/mELUfu10vRlao1aIXE0VMyoJhln7EFxd/ury+ofOpXqAERN39hqLj9yhtwUOhHsCz8/OL68GxlvVSAfPHIm7zkQOjeYoNzd7IKb69Ta7+EE6Z9IM8JPng0JtOLEe/4/TPcrif1cMi6QpsoCMiEgCaqj1b1xCKNG/wmtQcj6PxCHjN1aOxCtx7hKFwcTAI6lSQpmjH9h1egcqmLMTCchFkn4dpXbA1d4clXubIMxdpbfN6u9LABQvd/Jt1OsB8dMwZL+ISvjt4nQHpyzrvOsbrP3ZGBgvRXTHvORyrex5sOq3l8Tvw8iC0eu9oJR/g+ibdDZteg3G4kJtOixg+LUQOgXCgMS5kg+2W1fgNZYNdjumh3rUIUAFTyR/3GTgdwnhygDdhI+nH6CbPeo3kSSLwCoR/GaSndh1+B5N3x1r9wekleL/zhG553n0DZuna/u77F3xgtOlHCEb2Btbzk/CmaDDikHJrOWk9OLWYdQ72W8JAhAFThpQPx1jG2t0rXZyk+5LjtrcihGCzcHOpMlbNfBxFphfD7y4wty2SR1EVIk9gA7CBzc2h3rG2X0CH4F4H7JSN/esw04P3uyaTboOouzgRB4dsQVcFhGRyW0kCv8CLNrmAn/71WrnZa1Ji17OiVpigJx7XOr0TjsZumYnurxp7Dj51uyfApGlsv/IqM69/qX+02JJuy4qtquYwd8VV2MtAy0SdePeKgQeAUNTIBLfrcfITX4rqVheDV6ZOo6/b6l4OJbF1KrAje7yI2emCzsZt/xxNxGQVm9Je3IZNeb+5kaUPfd0J93N6F4QxnEBEgzRfsW/LBsvVqW5VUYQjYAlor/ewIXIdXhyy8f1fhe6ZAvP+HlKBXTxCaRy9mE043l7+qPlLl7RirBuakqHNr4+UEz0Dx0G8Wj61LVd9MzcA8zLl9lqPR7x+jZcwN5XIIE0AattrmCPdK/F+C3Z9dlvOPnYHFMMI/KzxGlq7frDwgepqm91/UJuS87Z2btC0OtxtiKaQQNgb71rPCFuewDPiZ88j7CbtzWJPZ495wKEbAd0rgi/q//fvAusD85TjNTrb5YH7S+OyqpSewkYg6v4JmGPKlVvtao9PuXcl3qMQNdaE2An9XxniHxoqUv/AyanceU05FjK4td7/SsFXGyLsuHBb/2hIH4e3yI751kAxDu7I6HiA6EI9H3ebZ565h/JkA4KH06Gjgv5xgQPhS2p8ZPtIeN5pHJG9Hpz3v/KAt7C9R1wbtDAxLVFekRE3JIf0Mufr+YKfMM+PhBXKSR8iVhlfDpKOkjrV1wGg4DveAyidv3SOnNADuBIslcOtphsVp51Oz07sie09CuRv+nSEKI9OelXdqfsMRPS+I6VuACDUG4BYj/4xGnk9Cc/i6PvX9rfvAA7g3YpwCGtPGEM4e2xFjP8FUEsDBBQAAAAIAAAAIQB4U/EBFScAACWAAAAVAAAAY2xpY2svX3Rlcm11aV9pbXBsLnB5zT39c9s2lr/rr8DR06nUSLSddG97utXeuqnT82ybZBI32Z7jkygRkrimSC1BWtZm+7/f+wBAgKRk9653c5lpLZHAw8PDw/sGFARB73qdKLHJ4yqVYpFnZZRkSiSbbSo3Er6VSQ7fl3khyrUUpSw2VaKbh+I6F3dSbvFVD7rkRSnKZCNFvhQv02RxJ+J8lw2FyuFZki0L+bcKYKZ7USkZi2WVLRB8lCblXiSqt02jBTxPMgBYIxVlscgz6MQjQINIiUzKWMZhL4AZ9JZFvhHT6bIqq0JOp7ohdMxyPYGeQW+Rp6mkUVUYzRcIagF/69dZKR/KNJmbJ5uoXJvPuTKf1DqVD/bLXrmzt5/32yRb4QglY7iSJQI36Jmv0GDKDZLcvHtfFtD36g0/3wIOgJJ5+RZRohcwhFTm8XUB1JtHi7treKqJEk4X+Qa6mzbTWC6jKi2nOPJUlXFeld0tX/78/cer193vAPPpXKpyKrNFHgOi3c0SFZXlvvudgglup1Gmku73yGjTVGbdb2vM5MNCbplHDeLIeJfmsW5WlUlqW8jFOu/1PoiJKEOk1Yeo6AcfgkEPuGQJixxmEfDrZCKCrAzGPQH/vr189ebd5fTbi3fQK/hUBPT04tX15Tv7MAt6MlWyu8Onsxcvbv7t+e/Srp763RpB9HqLNFJKvC3yVSGV+haQK8PvZSaLZHHz4XbA8GEdgeOTLCmn0z49wX9Kpsuh/ZYAEaN5KsfE4uGV/gpAxD/E6zyTdVOg9Kpcj2HrlfodIOY3WSZpOl2so2KMi4d4nwT1W7nZlnv/tXBez6MCWA6ESlRK2+CLPjweKKcZyIh8quS2huECWSdxLLOxmOd5Ci9fRUDt+q1a57upLCP7/rqomq+3sliAANJNDkyUW+bq0EBA1s2UGqEAGwMTvYzSlCh780EDvR3SDPSXQyNBJ5mOnZZdRJc4wjVs16s3h1qBUMuL45OqtjGQfrpJMtj1cqt4qSfivG6yS2LDAhPx4oxfDMTojwRq7DFZaLkB2trPfpOaJaBN/cVv5DIGNHO/+g0Na0Aj89FvwNwBr/mD/9IwB7w2HzsaaPYwjfTXroa5so1AKfiIeuyB6HoP/MYOB0yYHQRoWtRpltuWxAQgTBvLYNgDOnZJ9f6g5zU9EddrWYA2hf+yHEYEtRoVMQ4to40SIKqjxRqUa5mLXQE4w4dQvAJs5EOE1sCwAW67L9d5tgPVLD4mGSh6FXotjiLuIG9UnYuwYTHJ3NVgBWJ3eEN//VdNNodWzUd+B1IsqQSrAoQpCMh72OvQ6cxv5W0N+uK/j6oy1220zKBvqEPOvKVkOXtgMVFT5cB0UQnT07qKO0zXMLS/nhrSxG3RNxJ/KEbnjfWvBwekRuft5bAAETMXZwO0G+tHJoX/iihRUqCmvSyKHHSthQjzrDujdZgUMgZF7A1gGqO6XkSq7AdNdRYMYYxsJfsMbDBo7ciiQwfSziwszRqdGvT1X5J2YG5ocEp0vxqLNFHlzTLNoxJHublt7nm0nFBRUZOGeCmjojz8BqWOCwOJAiZniP/rN2YAr6d3GRjgviJrbrIsUbDtj7XZRA9TZwd4GqYxJG6h48AWVVFAsylKxbH4cAzaNFFTtCBxpciU7Fu50JjqlOiRJpns0KY9x2AiBMFiwl6k2hwzC3hi3DkdbUv472ASMYDa6v4O8QsJbkhGrbyhH47ZamDHTtGaH5NNf/NtpKS1YdvmGrYGSVVBc69lq2E5H/t+gdfikHLXc2PeMHJZW5wN+tW7CoSWR0EQDOB++XzRa4uFd1WGzKslw895Ra4daiHwEYUhLxoGCh3DCCQrbMt5mi/uwmDw31iTcIW2NKLrzyxD3enM7EON7YngGSKFQVBBe+A09lPBTVHo7kbCgrV+7dABwAhAp/mexQ6N8680yXINng1MmOEKRKQ/AOcafeBdXtwpB85cLiLsM7OSbYYoRVktJ8GHVdWGhyLQFjMXIQW+ZslidwTLU+yFipbg0qMti27rjNBoEmwwc0CUu2QhGUNy0uMcnNFgh0PuYR13EbhPYXMJCGxNAWcJfK6z6+Dz5wnYC6jXQRruxRZ6IK7IQikGIfYiimPQ+MQ+Db2PYQhYpoY4P2ESAR2QWSOxATMq2aYUwgD6YHQDJidUNR+Va2CqdZ7GgOkmShBZ6uzAyuQ9PASDCiwpJUBLhOJiu8XQRYmjYwQFGBnYsCxTaIDsVCpRgnDBxgkyTehuoIMmir+TtJS+kzTT/sFeg3avR+2fI5urhak2wWEL2J1vZHhj6xMvtM29kCzPvvWOBx1NlmlVC6U/ATpgMZV7y0XbRVnzDqnRcQtPq/Q6kBLn4VmTZ4GH+kbvi1PRJ7B910yAKZ8P+P9DhHAQPZR16FiQICWJcgxbS0Y0K7qQPWsjC3u/b/oMAFtGFvCsnx7EDkyGR4lXG2yIHelZ3PwW1aPEpRZtGoivhEfOkTWzWiIcZ2zxXebFJiJDqEYb9H8b6doW8rAqyZvUSwltmhsERGmM26EUX4h/Pmt0PT2dNB8Co1Sl/DU91nlVmPbPv+5o33gIEyrFH8VZh5XN9Ak+x798jqP9lLzJX8Tn9fjs+S/jzxv+o/BPEDLh+i0gRNhJOex+YaBOpv0AzPTuVusJzan75WaiSdT9Wk00zduv/aWpA20dJFgGnwkFPXEeUE+fwRMRmpxlvG6HsYABDzCWjgCURb/NqYd3io8ywngGDm5w+tlp30YMQwwtzFwx52FWE8FyNjSGHXZ+djYYi69/+SK4OR/ftiCCYnpkEz02HwzeWLepMXjtRg+afYxPU4eUvnJAtVo/m7TCS0Z6sNM9cnrXg8n0uPQ/gEgNt3eY97AtsgP6ff3jyJGKGDS39CGx+E+Trp1Ow910biCkeucL/NfHXEa40DxNyuyrQ0Oj5ngenolnIHB/NzgIspNAh7ct/rttkbljHYIg/GsOWpem2tIB0Ka9IbRdQl7gAUZuBvmagb+eH4ieo3Hm+e+P6EAPfmc4pIGBVZo2lNgaykajfZ4x+IXRFq1g7RnXUmvgiyM3+P0rAIGQGQy6UcLgA83ZU69PMQWODomqvGPIRlD1oAiidjiAWV6/Y78VgmhtxRrCwUE6J2H7OXaxZtd+25jujHPjvy/E59ZYASndYOyEjNv6MQCQpokj0TuUdIBYmqYmls7bzc6q0e0X+20QFpS667cdN+sZHHDdfC+h0yHotH27XYgT8QZzwnlVbquSPCsdPs/AJ0UrCZ7ol9a5u77+OewUvXX8CEWuE5lvEa/Z3G3daoyJxr6zaBTLnlhXZsgR7Ekd1B4cJcq8Wvri6ET8RE6ujjYnmVhEFE/gRH2SRalYR0rMJXhlsDTJ32XcWo46du2TxiS6MXXqh5LBBZ7q+PYh8e9ovKYzuUirEoNI1lIwqV5f9HjivKExM7mzCGyih/7ZUOMZYmbaTH2K8+0PkLLVJlNgF/hDt7Z+DfYPzhytkLNh0OOSAVbJyIQ6CTw41iwQgVGkdoxBN+fVOExqdA9Tvm5S5wpTCbLnyPIZvnjCdDtg2V4e2x4jiLuNuhffa4qL5zINPhscR570c1403/zBwuuMptSENs06p+SP31rVvkujkQU1aM3fWDwAYeBs8HdSx5kkN+RCGIpNgW2UrbAGxkswuTLMCYv3jgsx/NNriS58+rjQGoosnVCkvyO+1A7ZoMbwY1UAwElKHwpKa2+pmcV7zCQzXf84cduNuzBlY8VE+90R+k6SxQQnTApmAIx0Hp4d12fHkjZuUFZHaCikK4qcQ7HoVmA8Uk/xDxPxe/zKCc4dZXX5c1RIB5gJU2MBEhZlyTTaYtlVnNwnsQkPY5q3BDVhA+3lusir1bqJFI/dDk62LFPEBCbYRTHKYKFjYXoecak0mENQeq2smxEh8PFm9M/jW/BcbhDIbe9AVkxMDjFOzaccQ+5gUtgFx/JYHTwcBIFW1kh0N6+BCxHF91G2oHIxobZykSwTWKGs2sxh8+fLnksWNeSSuC3XzoE8UJItoNnMxWo202V7ODW5c0MXOtJsH423URFt6hm+NgNrxipzjaEMm318OrzRSJEZjd0QNWDAJmZ+4lsXFzK14y4Ew1Dcy0LBQy31xmPxTegbFhcxcnUnHQytBOEsQXn8Sthka7Kdizsuo2GyBpU2UmI0f+1L0tmsmYkAhGwuIXQZxN1d7gwOa+CWP4NlEc7X3hOqHRyh+qTcgxWlrZqi3yopcTiP93jeoo4LHM0jtWTBI6nqzgR1p8rAseuM2RMSpbDw79hRdJOIu3WyWIt9ItOYeIp2lML8FmdHdS7JQokrLKOh5F9ZVJx/JDnBq6TaYueraAnofOUxLI2HugHzq1qLqW4ePREfL969vnr9/digpwtXcDmWEXhhuK9nToJ9BgDThPJeXv4tUXWNL2cTTZaT3pEuVFSR3KSPq+1yqbIvwSoqJApZaLGJsmglkWKJ3v2gOkoZiqslAzZ5WgcKSwCQsDSlzTZaoAuUVymQBDQo5txJRM1z9AxWWCvt5v9mlNjEqsYZTYmTqPQ91F/gDfhpIIfKZFGlEaK5B+ZIUxdMM+2KlNvivOJ0z6vKBdIVGAacjKTsLUB2oOQFSisWrpjitOuCFV5A5vD/OFX/K5x7YkMuiLJ57iPGAs6xuMcx63qfbv+psaWL+16r3Qkn3mFF8p3iyBnFdijwBHOd14pqLmFk2ZL4DMUIbqDLAsgChjprkbJIVitYmIh19lyukizDreuoeXd3mOGEkXNhOxh1TFpPOmPIT0nr+qsBtOoEoG2kZr2ZIyD7TxLxpvB5+hZ2bfGxqFcRpM7HItoCQ23x1ZfKBHa4dpHMDIxJiYvX76/g456sZlLT5KDUhnGcKCyRMD7TNRB3V6C/ZuogccFAH9TWMY2IL2GOK11PAUjHFR1UKIec1k/YlGVYhS4DWKS5wur8cgw2wHSbbCUBA+1v3wB3wGOaQLJCCXX55pUW7gSQR9ccNEQwGDREn6oBCkUCCIL8XoPFZnkRFXvBPhu877ExMs2qNDW9wTe9p94CRJMsWEYSHVQF2ocMN7VXIZeUQgeQAHmFZaIOBcHaQirDW9jIRTKvgFdTtITGVB89W2BBfogO5QyUYykXJZNRrxYsHJ6TIHBAfRDAMayQQLdmqKtUjGznVY50Ypa9GtCwTCs9mjkqELLYntYHDWaD0LBTr11DzzY+z76uuB66tdWHzIipptlEdz9WrWoH5koHHhXLdqlwjUZIsgNFAIyKnyPGAyQT5zRFH5+0Q9AunrrIwmlo8UGHX2mk6HOzbBIg3HaQAQUwRRJABHO/jpgUjcrhktpIo8jAMRvNIN2KIRDnHy4Tum5vXtjYbAYxSJAjabKUi/0CjzK9Qvi2oogFjFsvBWzt7msWzCjD0j120CxAKgtZN3ziLKbTFVihsG8sC+Lhk5oZyvAi27eSu7pP34XNPVGOElyQqnl6L6dEA9wTYA7F/cUmnqLtoXSRql3PsgLFcYMni4b1G1N4aMXwOwZqJDFs/ahY3aM/k6MQiebwGre/Ho4OLpFsIlFnfC/lCA/TEnZ+mud3kqq3yG7Q+1kHdsncmw21WWzk43iTx9CkmmsRCQImX2BJG4KVZFMKXLO9wEwLRheJVQHVKvtbFaXazUaKE7x1WYIbc3qKUEIucA/zYnX64jRN5ihOT+uxwnW5SU+2ObDMyFrdZoezQY/iE+kHBDJOYyHZmbMTB8oVFdVMJTZTkWda98Bq9Lg2O7pHLsOnaCtiRDAH3QE2eiSW8ADLO2vp5giOesGbPFRHOjpC/diN1QyHdp1FqHno5uxWc7IzmOl1ZLwT+FxsgPp/l03GofFw+WEDCmZh2l37DQiOO+Baqs8bWXsZ9mWSRQU6FUZjzSu1n+cPsLuK5J72aQK0jcHqw1OGfGyOo6l3umRQGwdgsmX3xII6Bkbqp0YiReMWtooZAH1bkicbsUnUXK5Bl4Ya0nspLTOtYDrVHKT35nSLa1eqU1KJp4lSlVSnz//l6xdWQZ883m0LChw7cWGQpi7u3L5L/0FoiNofDGs24NIPFhFGNNCRR/aWCs5VHjlQpKXSS+71I/e6YfFRa02n5+2tFR/fyQUshKAKUZAE69w6D7grAR3mcSI+qbbQcDMbII8cdfmNjrgcPtqCdjYj0kruWwSbx1n01jAF7GxMJdnAVEmaF9R/0No1tbmmm2iLxE75/TYFwVEpJN3bN++v/oLHZsEsQh7XtBqwkQpE/1uVo3RegyReoCB2eJ8NMam9rhL2QcZmJfWJDSFoh1I/MEulAh9DxqHd1dnIQQFmQGeJCQB5hAyVHViylWm96aEGQZKfDteGjGshR2APscHNoLAP75zW+miGNrxOggtBKaRRP1fg3N4nBch00J/94O3F95fvgiEw5qBeqwYI16J4m2h8xWwDbt/MGR6DDCCBtlWR5JUSn4pPmbFdVzomSCa5A41ON1nTBLcEGRdkVih0DxVKItgXtSUBz639D3PDQ7Btf5hPsHoTvb5892MwwAXoB3G1meOc5SZaqGDQWaT5CM91o0IJNuBuo2g5Tq9wQftBrp47tfrtZboJkKLBLWLvgca5wVsUvYGO5GvDBoNuB4ycBvx6cW3X1vaF1cVYkZE9ZtpsbdT0MA4TKn2SWWjXoL9YhL9y4zJ2aG1pPFFATSyGFmWXGm3h4HmD/S6Y/qimn3VGH+nT+1N9ID70FUWP45ylpj1i0T+oNrTS+N5EtBxNQVqFFYXVE1q56AieidX/zLHQiGQ3nXTAQUdpcidFPv8r+pW8XguwKOaS7xiIyLvV8YKtA62O+lNIlWL+X5vaYpPb4PngxIscz48v3ZhAtUWzSbEfqwMNLNRDsm07Yi5aIFNIsypz9oZt0b9RdiQEu7Uz5155eXBufWP78yOXoS/Z7vVdoL2hIdHXdNZHEygAW67Z1lnikRQvvopYPx5nqWMs4qI5rgOOmIO6gozEGhVQ2DnIV6x6zA08swHNeYkIj9os2b/yKHxiQy90BoVsUeulGZ8eT2U0nEHcvsYZdIDVbmF9OJriUShznPCUT/0J8j2vkJPeL4vO2KY+yljvAx5g4Jz8ptye35cb1V4k23EoHafA7WpaRLsp6t4+PQIlQI6ku7ebfh/i7NhnFJ2JBMAZaa4fOdYC3d+hSg7uzmY4yGxGVvMichJ2b6NC6YSDtiPypc7L/XD5/j100eoJLxgR92C+027msLZ1CXumOGLMjKnWsNtGnMnDSh5FKPVns9G72QxjY6PiL/rDX4qfZ7OBAagDX6NRc1aIfJqjt89AzfSgHVkwYBuCgEHvUFn7HNsbJOgEntIB/KLggGGKt3qgZ4wBMePJzCV5MeShaEFMQ/n73mMVINFUU883Y8zSMqvw/RfiA6JCgXlXBPyUzaMUs7axMcLQZ6mXocqAKRRfEYFepEA3kqzjNfCZ2kYLd4vR8GR+wRZa8+En0IJRkhrXlIVNjnZMUobdUzHo69nwLOxbp+kzh2uZryiVgS9rW9Kze+iRZ3eMRk0TxzbDSrllnsb9Ad300cEdwcEDBt5pUCos7xg6YAbEGiV6OxB/FOctZIIisLNB38zB6/jwrj7n3OWj2tpR+54NMhZvGxaAIyWGh51B4wvarObTvEDP0iI9NAe/XEoKvCUlhwRy4jBKD0QbLVeuyD1XDqvdJ5EYU9rAjQKFb9E+492H2t9GqXBSLiyut2BQFiFKrCWrdWk0vFZGY3IPjA+ghYoWgUZ0keUO2gyD1TE5u7GrzXjjTVBgkTKVnDtEU2ADAhSjGvtGFEeHZuzcjNv3I27kCMQRdAL4272Rsa5oBUpS4etyyUF3KVZpDiIBcNKqDVrDYsbJwnWPrG95tRQ7+SW6etb1oSDIE6e7Q2xQuvRsLgvVcKKFh3Q0h4hS0KTx3ghGRUJYb0fHYR0KfePDSZOYSYYWIJodP0YlyFkdQ1OYhaODn9b451UL5YOknj1zQHeRrzKskBVkUc7RljELPKTCWvBEFcicpEzuJZYe1+4feN9YdVwaYGiYgjPEGXs0Vtgt1qdJMduCieUNIhrarci3DtldGWJ34waAg+UKdswndZiHmA/iBCdWq+R0FI3DnwbRmuJuBj7vyCzZzJaYF6BCbIEUHW7lyxvs/D1YcbQf10dnOQyhzVxYUMCuTqlTTALxROZCRFuZ+FqJNCiEHx1Zaf0lzraYok9k1LojiHlyJMfOtUesjABo02lG5uLggKtijtlargBtCG+TA2orDiSpxcIcnnAEsV+Umd3fMGJ4iCYYvQsOj8JshbZDQy7WpxFQvlsHcHDraVz3OiSZ6vrOoRvuSrKJC/rq7eXQ1fgTpEj9AE0TNQkKSXe8OZc6IftO6quaBnoHfUTZPZvZt2zZLTh4xjveiAzPmWGeonvJpA0qodVc32HnJVVFf51nOZXOzGZNLGcmZUij1heQONlBHc1r229s5tPboXNTjbbXvi1Q4b9Ntm2j7cqp4mdU8eYITIxk8mFLMg6LQJJVpmsPYIOSUUSpZJpC6IRalHLHrW+KAF9KHhiWcxQkpkxrEOV4nB9FRe1YMXbJZgOqG5yqdO+AY9OndDMWnNn1LMMT8QbH2yUexF0CpijdK1hUVBox9DUOGty4TeC/xLNQSf8hAQSY7ApgobrAWhcAvwdkkpL6UhDHPyVBHo2mI1cJ1YIn1K1KtxSdK2J48JarBuwruTiHicXsQ1HPCHDwGRFIigtsikl2fAtjkjmSouVDEsCQM6ButfhLGg5LpTAjQm6iiUOySYNWzg6m2uA/L7LhwDObRHOg3nGhew/JUW62LOjA/AEBmow+KGVkafGfL4eUYV2gSqQaBje8Jn66Etuq2FKpQz+ag0lkKgZMiXJULOhwJduN2oVEfacw44ASduCwnZcnpmRElKm6sJPtGhwc8WLCoFathQZlYgbCcoZqporJTqKACTJzlILJMKwzHPZoEvIE8irdYrH1eLIb3atljSHuSKrNJa/OlxeonTXye6Nujb2pbQgyn74c/fnLWSiuyi916rEqyJTDwJgB7URB1hiURXncKENoMihvHOT2RvGP5pg/y/08j4r4CsuUimpbtrtb0XX8zPccWPLuCb6PHyr9f+f/JI4/wk4PrmDUKOTRzstPOrLJJZ3JJmoH+hzjtM9HBJquC+UwZo3MxWz26VPx6RNo2AELXYyf2gwzQqUNxtkWHelXYTOTb/Ko2gHbO6l/Ti/NZpR80XXgYMKl+c45keDNmdLzj3lFzlOz0D3t4PANpbBkrVtL+3VpkzE73liyDcVrGDe+NrhgKAgjy7hJ5pIyAIyx46taVE70TnlLiUQtBDDKiRsTepJQ3tE2ht2GFXtsYLHBCvopQ2kA5KxQsRgPxYm6cL4vEj6nO5UnSzojxZQI2zOpjUA0YSfBDkxdQ5aJ+TBsm20eto7B1mH6tGyl5WDYuLJvaSOZ9ZOmUnNsTLQj+r7ROhRfuXmLJV3dejs4pJYvMm0fkPqO9SppsazuEu2waBwEaJp7Lp7r8HQKuawozJmDZUbVBBHzqy60JkWhVz7HoDsufEgqGkfDpXKgGW8JlNFbVAAKBTDrZtoxvAJWexTJiuwWa6G5NpVBH3dijKteyqyuVOR66iwf5VvrJFcLwpbcziNrAT4ST7XPhH5ChshJg2lL+kiB3W8iXt+j+bNHuQgKu8r4SCFaL1RQwMW9Ca8z1pJssLS2LqGxhUnanQCpJrXQQv3DorSPVztTtKZRHznQ1pZTWWbqGimjYssnxzZs5eYTZn7lKNVJgMTA3IbOmielgddI2OhqKT8hNyObW5njAmyH+6LUuClOHsNW4l7GSWmsuafcNyy5/bH7bCklQSWFP3L6/4byE6hoD3XR10JOFRjtB+72xeLVDDeMvTc4LB/K4Pgld4wtxpzpQ/Pmv3uORjcvmKtxoWxt/bXR32CEUMxn93wIaCEa9rF7ixjJg+d/3PLOxjzQfr6TaFmI4MPV+58ufsBwxuV3V9dv3jVC20VHAAS6tg5IF/cHY9JOdXgzXOVesQOzAAssdu635SJ/qi8zGpyPdngTMXQAEBSCm6dyxM9wUvfJBv9koIqDVoSdi9a48eD40W2NZYOQBvf7xKkbxja0x0y57NKkijpKZoG1gbpoZP6Q3HER7W33EcE3VFiAYPkWfLb0wTZnnELvkJh7FX3zYSNgXG9Pc+zRYcGBuz2RAcYUET60NdunxHBXNwNVCMfnKoxSN70B/co5ZxlSauuw62sqnoA8B8udjIOtMOwHkk3h9fCRasChYziomAZjzmvbUqVmCZSpdNIN4gYgrzTKP51xPABn/kXFSk3cHJ/h1md8pZDlrY7bPHSoDcnov2z6XcCvGBTD+GmXYwZrWbfpvneIYx7+/frdtw1N+8FnnsMvY1IjqKIwU4i3+5rLx7jBRM/1yIVf2md8857tIT9y9XTEjiA1Fp/lLwcwAyt40jjZPtAGgb5Grwwx5ZnmUeyJB6+4f75HxvoH/Y2KItpz9l0/JTkgwjA0mZeP1tfA84kYmSDbX9cMkpOEl7magKAuzcD3DJJCGfbA4gmdQ4rwEBIZKNiUbv7Ud5JG2vLcRdrowA8x7ngOSoVPn2et+41Sa0/vSMcWmdrAPJo15Z7v/JmEL/3oRtd1TCB1oo61wYvoHUFLgfpE4Vk/TKbTyYmh6FO3obOigzZw8sIeyiNn1wx+JvZPVqoEq43TyJ+yoCMRTD2e6R+daIJrKd8GMqH25gj4kH6cAgYJOWbeD6pyOfpmpJJV8JQr91y4HgT3wN8y5sMJrk+6ucP81rYPsnWZPEwC3mwjQEhVS3zi21GOi4jOw7dY9b2/enNEUVDwAdTPMiahizgEu3lAZVPLjtvo9QEVnFDr6nwddavTepxNrMyNwOf6SNKQc4c/RguQVg0Y52fh+XNxiROKcR/CXn1urpjU/V5dXL94Xge7G3aI49wpQVm9JTjYQ88Kpdo3lGmheK+j76Af6ZhDAw4dnuLjjBYNY3OAqCjNKVFQTnmlD5z6ug29QbqaAZcW9gN8JweSDqeY5+AROc839XMxEs8b1/qciO+kPYDDmNRk5jgsJ06p8EZHtDke04CDydwi1tJupQmRyZ19wSD8+SBusMM3WzZbOnDuOOHo2IFMh0H73v2260DHnbvJMpnUiBy0UH0zrOZ2ZHRejaA4zOpk7iNGUdxvY/vrBZ3vBvQ6H4exbIkXRxAVWhRlwSMVcHUIwj3thBOfVkXah/90yRuaOI1fcKFqiVL6T/0jdwfMZ/J+8cgQWH9c6moGartuZBrAawyIYHmVNEB1f081QTuvaojKfE9PmzIfmuEvWTAAHPvm9+Pb9ik/eGET4W4tNqW+46jYJZnjH6HVSbXXSL3Au+SQiNdrmqj2ZqPRRz8rrul6pMM7p4P7pkVURyRg5Ai3IjJ1cBrL+1N8gmyyC47UV9prkBtWNw6L7kwsi2KCkAZNM7iT4bClFwgja8DTsQcowIvWnmGLTLQIYMqlYGqRP3sKEgPsuiF8hg63j6da2nOmWCkC70y/aFP6IKDz578/YrJ0opArZmQ7TVIy9IsGnKq+wROLI30p0e3/FCv3Ruh6WfhHy37jlVnsVzSzoFZlcVKg+KFOt0co1YZx2wpPtLZae/fsgsHBveXh/YTN0MEYx8h/Il7qvA4q2iWaAr0DS9NOBBwlf5OWHaKAfgspDI4Q+NGV7HTAb4KHGKt4ZaZ32OC48NOTbPnNPgMeoGJbyvcDPOOHYh72ujnuFwwG1gdgotmvbYRMoEzO50W+U7JoGAP2eUjzbfG2h7i7jKakvCyiTOHlpNPFelrmU5hcf7GuNV7tSmHNFf2mUPDp4eyFo17YKW8let1aLdvv68BOFvcv1y4nD3S2YyhelkX67LsG4Ms3r/gikg545xHDM7BsSo8g/ccRSL0uzQlq88XzwDMQNup+YW4hO5IBsTekRjt7LWW/4zogsD9um/mz0bkXL8YayD5eouDcReDZHXzk3dxpI2bUZ8bFNIW5X4hdXX1ahqrx1DZni9s77cE5LLq2pK6+BJ+C6hxNvOv8TMyrBFA9/+bsxZAyFVzsoPLWL58IWOUzfVYE0xBx4xoHuouIh+TMZiFXdD3Ondx3V0LgBHc4Q3vkQ62jQt8ZBPt9Hs3T/WherVZ0U8s6uk9ymMKPoIowjDE0uRQXS8yMAeNRDEwTab43cUVzGiUxQRKuierG7pKrXLkoV9XrorwFIaKAkPv0IM/Q4QLCJgv3SuATcx9chBVR+u6fvXPzDhKzjnXa1aerifD9yqv5wIYB0LgMTLnwgs45aM6ha4k4uQTz+1SdnQFaiZv0DGBf4DU7GxxWH3XQ9/ysCvBsAk6ALQlHClERKCpleFXIzLuy6U7LhiG1niLfTMlbi4ggbkrzMlyFQ7rcKjHXYf60BT0IUk6n9MwCEim1B40E/vqb5lEhmD8m/oDLzFIrOrOLkeKYDhkF3pTEMxEsom2C1yT+exD2GhcFUb1DnCiEWCWKa2PJCfDriWALUTaff7TVpak/msW9e1C3yIxnSGtkg3lUdh0p/AUg4PAvFa8p/UKRZqQRZoUU/RSvVz1HtQe4rnZnERdxYVZZl0lxqNCbW3NK+EKzj5UeQy2S8B6wuvyL/Rb6/R4jEPwiYO5MTAmDxGFD3kUp3VuDJdk1FeggQV2W6MxgR4VQWHElKvc07qLIlRpZqU+linwnEzMZ8qq+pQz3tSYC/yQbLp4DilhXXOiqEH1UCFDjkg0tlg1KkkorFxETI8WDUHuPVSPUitno4v3LqyuvDt5ckaZJqM8iAb5G8PuXgabJhkr4TCqYy8TFN6M53iQLmwBTId1sdpHtdxEQ0fyis75nLY0Sunop1mUF7zAc0y83unConi6ykgMOf0ArN4UMha5KpBXLBNYkCZXARmFZSbUutDn0rPzLbkkd+jeB8U9u2tqT+pdZ+RdZ4f+sukNGTA6OXSv2K4G5DjmFWPDGef/eM76lrI82zxkFPIBZms7+CasFJCILs0J2nIQwl7qxwvCURAOakpIrWcJm0vdZG8dOy6+4b9m8GOipKeb8aHOS+9VY5f5/yUpatn4X14k30yWtrRvnWpcwNNbbjzZA04bftYwpcEbpuLx/lHWwpR2n3cMM5wfyWu4b3kKOh19heyh96TXQNywX5gKgZdwI4HX65jCTEMAAfalD9/1uy7jx07IdwRDOPRgklEViaJ9ev3x/8d27i6vXQw/5juvMbdmMfylSwz1eHs/Yd9cpaVfM4CSPFSU/1bwmKdTgUAyvxs2fgmHXloKrSJgXzwcm9nmwABE4ESO2usiuHZJFvLj8x2Ng/8KQDsJyRsMTTAf3+GLd6SKCwfZfUEsDBBQAAAAIAAAAIQDvkZaPEgcAAH4YAAASAAAAY2xpY2svX3RleHR3cmFwLnB5vVhLb+M2EL77V7DpxUIceYPejHXQRdHDAu2i2D72kAYyLdE2G4pUSSqJu9j/3pmhnpZkZ3uoLrbIeXHmmwe1syZnSbIrfWlFkjCZF8Z6xrU2nntptJvNqrXUKCVSWov5NmXcsRR+620vXvyz5cVshyJTo3FByS1r2Gkl55rvhZ0FsjhJTV5wXxMlXDuZWDG+64XNEyX0bDbLxI4l3pY65V4kT9LJrRJzVLBiztsF0ysmtY/YzR2+r2YMnqurq48CDqqZPwimjN4L51lhxU6+MLNjmw0K2GzIVi611HsG2nMDVJuNho1KEUlLD9zyFGxy8YwW3n349T0TLuWFYE78XQqdCgdWOJkJ0lhp4lawR1F4NBAEgLMzlhkGHgfFpfYkzJtnbjNiq5SyZ5n5Q8zesbQEXse0eBKWFYqnIqvVcH1qQVwfnX7ljmn2ds3eBI/gY4NLgICWam1AEziaf6i2/i/A5jWDWJDPI1p7Pkjgk+wt7rbycyCs4xrn3KcH4lkwGTU0YFZORwIffDBatNy1CXkMQudRbx3DJHUpmsXa9us1u21l91/DKWRXdc12twbY9DRsreCPs46X0PL7Fch4ABCmijvHfoOlTwD8Qth5nQRxZzFqwBfwdbqPsOJWcg0APyDcBHeQjC7E27HtcRx2ASxnsSfyrcgyQgfwlfrRgdd1JrSHP8YGUCJ+DkZlIBGQGaL7kqoS+SgNkYpsYdsy2wsfs0/SH0yJ9koQ5PxRAe1BqIL8w+a8WiNhm83vDlJ+BecMCbBgHIuJsfIfYDMFlhSmeS4WLI7jiD2bUmVsC0rJQRkT3CoJNYPy4gAQl941QQMM7sE0yBYLLnSUTd48Cu0qQa5QErwqs5tnY7M2HegP1REQmSmRYEFIkGbeYMAJtVt0MgUSzoksCb5cMSWdv4fy8rDowMsmSgKCJzdFKE3tKvm2s0ZFq58FroAgAesOsZvzl3mIx00tccFuo1kX0mh4TOhtj+X62AbworSTQ93f3D70k4zyZVhrkX3RMayfmojfNam4xyIBQiK2OhUcPBVjkCG3keZExsAystc1VEJhPcO6WXv9rIYTgXFhinkUdYCAgKs25xR6Nog0Bad5a/UBojCfK4aQm9g/qiSGBKwBO+gb+PwsrTXWUa5xtYfc8Icce9IqFwCO0cIRd+3ddOAEyMCTHqusrQpKDmnPLKStwLYCf/aHhme1g+iuNqmS6WPddeO63W6wuXjBM7InUOIyRN90C08jbXv01PheVXOqzif1TgG8aLtb2PCJY8Cz4MqZVT/AK6rA495Bq0naL0coVhoqEmQ5tlSYSSwH52QmLdEnNOb05R68L9xquQQSQAnxx8bul98tK+Zlo/Hgc/XtmP5ZT+JHsRMWizLOMko0etHIH4KF4ybsIZrlNoaQLIMhy7T63SqzXeYwpix/ktvWoOLYxWTzH/Ogi2PIpPuHQcUIgOnPB5SKXDrB/uCqFD8iUOe7K6mfuJJZhbHPLfs39gub5yUUFyjhd+xNdBUN9EAJo8x0Z5r+gPSO3fZJiIygBachYlduQ/fzSdjo0QvlxCUJMPV5ydUYO1hUp8Q8bEfsul0i9g6yYwV+llhhwPLWPUMDBs696uaHNwbqqN0LtoPsAW8Ej1916n1VzqryNu/shJGsKmKjtXESEp1+RVPfqSsCoP7vePSoA/TWXejeDEI0G0VVZk2RgHegUGEHo6mhbTNxFTm2XkMS0ebEcTOhOnwn5k06Hx/0amNrK+LE3srmOhDXYXqfANJoV+21ukkOlA3T71D5eIza2fjUu1X/oyls5HSQChPGU1iGo1jdQupzLdqRhwQNz9SC1pX5POfFvDaklRJ9DSwqnt7LECWrSUNuuoHu8EfjaOpQDL07OudUm/PRMA3rLdbaUVKoL2gjEWJpu4WrXJ99im1cNcEcpzOK4SQJ8I85f5Ke0rGBFbn/9ixxa8P9myZqoxzjq03gRZt9A8JoPEvIbXUyVqXtGuAS/2VkC4eR1JxOvKquTELhIiROa0UXzGcZuo440/miaS9d8NZUFRtoOMv9n30+fvU/VzRfn9uTOT5GNB388923+8Bl+ykUrnUgpzDb18T5InLoJl6fu1F0dhyKLgp82+3kZ6kvoKfBQHVlbD1xzU7NuijoPA7GoTY9Cr6mk1afmkLBpdXvTz6e1hdWWLK8Go2qC2t4oY+gdFXFT7Txe4gK98beY+nvXFrBuKQ/YF0Yg5FhMNNdHvZGROK4MUY1lN4SNpTeHvsIOEqhsmZlJzVX6oRkzIb1iAeGTGMHHnVE+yUhvCdGq2MVl+bTdP+TNMX7KUzerf3QEWX2siAI4A1RaLipQgTDF+6YvmgRPABRJ7emVw/PkOOgAy9oXz/E9++GTzX+d1efA8GXz2jdl+4dpf7K/KeuyrB9imb/AlBLAwQUAAAACAAAACEAP7LDH8MBAADkAwAADwAAAGNsaWNrL191dGlscy5weYVSwW7bMAy96yuInJJDlPbSg7EWCDB3l6AtkLSXYZDURI41OJIg0S2Cov8+SraD1ss6HwyKfCQfH1kFdwAhqhbboIUAc/AuIChrHSo0zkbGep+27WGw8eiN3YOKgIyxbaNihLW2aKxupgnIS/rNCgb0TSaT9II26h2gg52uCAexx8OLalodOctgzimgVRNdUXSe9MmH8gGuri5hfmoDTzkNvtWIPhaLhdc+cn/E2lnuwj695xeUs7iRgg9EupKPd+tyA9fgnn/rLU5n2Xm7Wv4Qd2X5fS2elqvH8mM8A4g3SRW0D0JMo26qGcxvIGIoTjyDJh0tVJO3FOdCZGmEIMuqAwn8zrtIer0TG5apFIB8ZVAH1fwc5uM58otYfPYwmuKkwSCpsTuzVagBa0Xb6yQFE4HWSHoiz6OPJzzbdgz6xGAc/JqMsuB8OiJ4pUvxpAQByFJQNWoPr4Z21RJd1tF9Jrun3CO6bLoMKe87c0sX2ZKQOUPK1JNusKY0auzpmD2CC8lNXg1SpjoDmrMsw0acRN8cvV42hjhd/3sFKSsBoTYWoaLyqXSxU6gKmSFydMy873NO8P+0/Ev+L7uP0WeJ/AFQSwMEFAAAAAgAAAAhAOAFe5pVCgAAXyEAABQAAABjbGljay9fd2luY29uc29sZS5wedVZ/XLbuBH/n0+BytMZsqPwbKfN9TR1prYjJ5qzLY8s1824HgYiIYl3FMESoG01zcP0Yfpe3QVAEvyQkusfnWtm7kxjP7C72P3tAj4g83UsyIZHRcIIfC2oYBHhKZFrRthLyJKEpZI88/xnstiS04huyBnNJf/3v8jzmjsHJMv5UxyBECUJl4QvYZ2CBIrFqeRKU7zJEraBJSpjUL5mOaylII3EKBZhIQQSgD0WomBHbw6PgUGRb7ZyDaRFsSIyp+HPLPedAxCdKy0U/hN8w8iKpSynCWhbLoGQhkyghpytaB4J1Lzmz6AQnERvBGgI+SYD+YgYK3ker+IUdGRUhmuQp8AL9nGSgmcp05yKaEwHj2LlimR5ljP4P9gpyU+FMDEDC3iRkySWEgIMSwlEdwnSLFxzQtMI47fJpO8s4ScJgmUhi5wFAYaM5xJYYG8VNeE4Zi3kcCqhWvPpIkQzQ/hZkmNefomtKD9lvGHV9zaL0xWKSb1tCCsYLk0+zXO67SMstjlb9hHCIFzTfDclyPppELd+AmTDP1iwg1gkPF31k554HPVvdjOdXM/Hsz5Stg344icIaB/xVuZFiEdiE/1nsNzmenc/nb3by/Hh9Prd5Xgvy+XN+f3tfPYVnpplxaRkL7Iklb/CsQaO5vADleIVS3DN0/OECzj9ObBOpvc5zTKWOw4VgumE8bOEyiXPN+TkhAxg/9fHgzJxNuIphB/kAAri73RExr8/PO4LGkhFSfJ1vvvJ9cXd9fn84824xeyUOZCRk/Ls3CovPMcBGEhZ8voYyHo3v1xx3jN5K6MPUFxQciekXPftdWfGaHQOBcQTdm8z2evOfR5L1sfVIOCG5vuKR+0tLQoyXlIhx3nO8xZbta61bTZg5mWcqm2tMLk6ATzXHbT4BsN2IDzPsRjm/DRfPbXVlaE1aodlEg6toEMKep7rEPjnDroa643FGtoF7Ot4ziUPaXKRM9baryzRYVWs6ErF3euEczt/N7kOdAWBPvsc3VdHhx4yTO/mOzmOFMd4NtvJATY7N9uzu4vgdnJ1ozgOzcL9bDI/PVNLR44DSqaz4Pbu/Hx8e6u49Mr1dB6Mr6d37z8EV+Or6ewj0P5oaNOb8ex0PpleB6dn09l8/A5oP/zwh9Kvi8klSCpdxo9q5ai0u1qByhhPL+BjMfjbyxEdOFenfw3OPs7Ht8rQ+fgaaK+Pv3/zPXSLJZE+Rj04/zA+/3Fy/X6kDlHmW/2B/w7IHcIB+aSbQgCwwFKhWstZgZ30E/QwIaEqsLV/arWeT5Ye6NH3cHb8WRCAD92Uc0YFrEccah576Jo+MfLpFoo4+lQPBSzyKz0aJVoNzsCFNgj3QhQZkXiVcgBmFMNRJZNkohhVHY2aKjvuNZU6ThWV3g6BIwjNYmfXPgdkmsKkcrPFgSFUTRsBGaYB1C4gGmoOoIsYRoEtjhEccJdKZmYhYbQI9gS/J1uYGDZxFRjQFGhNcLqA4cxhiWAjRxHDBMAb9jYcbtWxvDoEwTJmSSQCEH+okXZ2d3F4dFwx6QIHLQOrOoctMnRKIFcts0OHcVGJl0jdpoNTGyTtZYK0iXiabBUTwk+bIY3izU4iNi8qFVkPHx0OsaaZbUEPh8xhqBX7eYoFXy4Fk/vZ1HAIU2V/VB/1Id5spyqeAQDTWXnSVdr5XbKR0r8EM5ZApbGWTJOoN4rY0konF05xZEoAoBe6Gl0kUFkLzhNQdkEhzTzy6q2eCR90QB/rvAIt2J+r3PPqokvoSoxwMlYMDSQFYCq3IpjIxIbeSkPXZ7dKO7QbmpUaSF3YG77Vhp5TiTdQztiK0cDCBpO0K+R3uOxD0jZYeSFHTZcRcWt5HzEioFGUMyFwfx9taKiAy0CR4/Av65Dg3SJpWdU+Jbf2CZxxdHEHBljNLDGjz5PpGTLH3K9+MeWOBxxAXcQyCFzBkuUQUBfbnD6Mfyr8UGeKH7UxyOqvy6FJf9QpEwsq5VbpU7LSv4RKhvvWwzwvmJURosgwD3wjUAfFBATZd/oFZQ/Cu7y1HESAwPT5BoMa+9ryeDk1AVqUNaBUAcHK8K1kIpA8WLAApSA2kC3uonYMshnRvs046kuHw2qRJSDXUf5bctySozHUx19oUjDVbdwGFf8NTLdR8jyKSFpsFgAf3KjX/exufvHq6M0rKD64GXO8roc6uoOGQqt+qn5jocWixogTDGgdBNQZFJB0rVh1PPzuO3LcJ2X4ze3OtQzJn2DdHsubMdAjnWulbwuBte3NtV5zW2KqDlsWtlRjBdUrjZSwR3rXw7vUjmGwed4H5JnGUg1QP7LtgtM8mmD/yItMNhjxPu+LhLHMPfSPOtmYP/Xl0fRW2xO4g3JUY2qGIZ/Vzy8Dz9fd01W/nzS98KxTwexVgX04fFTeTS++kvFm4RgwtxVV/wnzexcqqLvWN6FCmZv/DSr8WeArS7hh0D6jGkgx+VUogg1APV0xDEzKFZaqHWBQGNlBUeT6uM1NoTc0gwbLoAkNLT2d+8U+jR3mQdvvb04AZYbnNELMfhlqoggM3V3g1MODDS978KTWsolBS1f/kHSuQt5OtKm19QBO43L/y8HmK0hTmrsfbAzXN+KNDkftVKvGDKEsM7uEm4JwB1Vvkn3H95Yc7oMUFZKecukgSCsTGxZUEGCiD1cZRjc75xp87gqE5sGrrnrSGiqV1vIZjF35djLdNfMElhqInfVbk69qiJZ+gx1Zjrc5ua0MTenGQqEGRhi/LZU+cveX2As6cJpuIRDdIotxLMPLOU1D5r4McR+vFxc6fvp6kxdv97isZJZJIdbWEGeuv2P1A27RTZEMjm6fn9WmTV+TOGXCOKy+R+o5258gcgOcP5QheOw5QuyUKIQv7Vq464XeF6nW1gEmK8yoeZVOeAwjFUTdOmDTzrEZGbcTUS3u7Z6Y8Uq1NwuqidkyMWdZad/OTFoO/tQoF2XIyWelGj9/k3/RQ1+crsxy+SuQ3g6g6KpWZxyKEGL1tWRHHZXVZl6UGhW065W5hlK4tujOwaIdM79etR/+PAsLB4Vc4jgL46u1iHf2UForeOKmt6Dv2Ow1USe0iZ/0QwAot8aPRjTdxhk3gqIuaJ3QwY3vfxW7/snIrDbeRP9fgge949cSvPq5+FcaPPMdLGkoeR5XsHkFPsOmD9AroEX65zRJFIo+1JF8HFZxfMSnjc/KqsNRGwS0zUejToZrwvGoc3pD50t5rrEIzB3TXdbtuYWF5rqypkLBKqDwYBnDnMgHXgfo1GuU033C1gLgBvQr9WkalmlWrhlRhnj6d6koMny7ZdFUPcBCE9u7U/Uiov/25aO/XCz1sqv3axwq+uY2/+7jau7yvUr9sRBnoUYJPOs8LGNmTlen7tIeb0rwVq3KvOcM9Rgv7DWnUS9msYp6XRPW43IstGT1DpZXu6ljgh772a4Axf2lwa7MqJnLyuhyIksjR3QUu4ehLNJhKNIQy7yd+Hgobn34Xumj4jc+7VG70LeQKgF1MIzNph3jrPwNqsqWDBvjbeY/UEsDBBQAAAAIAAAAIQAHrShUO44AAIVBAgANAAAAY2xpY2svY29yZS5weeS9a3cbR3Yu/B2/ogf6QMABIcueJBNkOGdomfJwxbosSbZzlsIFNIEG2SGIRrobpBgd5beffauqXZcGQNmTnHlfrRmTBLrrumvXvj57WVe32XS63LbbuphOs/J2U9Vtlq/XVZu3ZbVuej35bF6tVsWcPhvnl/Msb7I5/DRfF+vtrf29rteV+aNcNxt4z/xZNea35sH+2j5syvUVNtn2ljgk7EC+O/3uefRZftm0dT5vb4v2ulrw12p87rF553fPq+26LWrzPfz+sV2Vl7ZX6eE5f/MyX+dX3U/LJ7e7nzr7WLbv2nx+ww8st+t5W1UrO6TtZpG3xfS+zjcb08pV0WIr5hHzJ6zUNPnAWv7mL0uYoddFXWyKXL6ERS/sF+9hssUljO09fNrjB8aZ256ikc+m27Z07b348fSH6auzs+/fTX8+/fGns+RDP716d/Zevik+zouNtxGnl/Dfzm+/yxdv8jqHjTYrknjo+aqc35yZjzsfw/Xv/PJl2TRAg/s7e1Wd1lfNefOXYrU5q+tq15PvtvPr59Ut0MWi86mfGqAZ3dCyqm/ztsUDIY9gVy/406L7qX+vyvW04sbloatVdZm7jdhUm6mQZccD2+Y6eGKT101Rmwemr6n9N/Rh+olmsypbHId8DUO+3ZbqpCzL+jb53QY+63itaR9WhXzj0dZ0AZs1b6fw7hXs3HQNu5d8rvi4gV2Y5rB3ye9v85sCGlvm21U7ba7hs+k1LHvy2Tf5vFw+vFjBav2iD6v/WDG/rlKfU0/AXYCzLrN2/P5/vzmbPv/L2fN/OX/1w6SXwT9zPGFzYdxtsW40wbwrVsuee27cwDhXsGu3m1WBm+M4nPnkvC1ue70X2Ql2B2f557we9F/0R9klMMHFSb8dP89Xq/xyVXwYj8cjeOp0/XDRH/Z+9l/5GT7qPcneFrDfd8UCdqWqgXgzXPSsgslcF1m+bSsgzHKe4fJlTI9jOAvZFqlkY85XVjbQFOwK7B401VbZfJWXyLLGvelfzn58M3395v3561fTd+9fvz394Wz66vTlGQynP53jabdbhb30e70e/J2ZRSimd2VTwnTwAzx8zYDWa95+nGTC0UdwMZnHJxluxzA7/hNdaWNYrzqHuX1ot/D9B/hylMkxvrjgLer3+z+WDTDi1Yqm3WwvTV+4Enl2VVfbDXyVI/HmsB33ZXuNj9Lrru/sLl9tYdnWiyyvi/VRm12Xi0WxHvMWT2jBeOTn67tqTreyuVwy4ALUPfU21m/o2f1MXVwWyCzMp4tx9jJ/gA+zAg7dw9jMin7ewsqWtPfzvGkHP2DrIxzEWCY5FAKE7mnzyzW/M17BorhVhzeGvF406SU9PKb1aHA9Bm6U6jnaKm4CxsDtwrVmmsVWR9SSjEK1b14rmwwkGKC6NS8t/iHfjXl9/e7w30NZrBbU7sg8a+nqugCSWxcNrBv8kZdrJqjLvLE0NsnMMt0uiA0RVdGfE0M9I7iAr2CFinoCZ69awfReAPstmPZwtDwsXCkYsW5+TN1muODwDZD3GpZxPS8G0P6Iu1ZLWBcg0K17pjHbq33gtmjw3oEBDLyFWPbPW7N6m6qhU4SHM18sHKFln8wcf1d/pm/h8eui7gdNmae9edjX8GhATzApntpttSjGrokh/VbA6hwy6hfIybpGCNKSO59qvPJsNOzHDDh7f42f8Yo12w2yXjhceh70a52XTZG9BaGzvOXrfiCzGRoq4ysdONumLuCcA6kBTy5WA/eBUM3/QcoikoGflh+9pT2nNdggK4FfmvI/gbea95FxUIsZ3G/3a5wI8WjkJGNz9PmBk2w66Lt++8PxFq+5wdBQlCJA99iIxhVSIazo4BM1+3mSfXJPfx5yj9FTw373ijTb5bL8+AVLAtJ8uUIGWBd5AwuBrCvPZrPv3eL8ktdreGI2M3Q2onbge+gS1pE4eJ41GxCVR3gS8zUzT+wTm76/LtZAB6aLe6C7q/IOeblmro9ePW/RvDXrm5W6zNs5cFO8t+DATtQ1hpf6zxcjfmKKBDGBjW9pqZBZyx338yiDq9/cbtI8fj/4z3Iz+Ir1BurA9jLUjQ5p9OW8PSGGNkSi/nOgFuFA8+3VLZDmdIvrOy3wHCQvZ7rEJpkVx2GDiZmf0A91V/9QrOWyxi/c7SwtZdI5n14QmHPg5Q3IHUAPsA5MYHgwgCs42dyehrZ+cBtCFwRzJXrSU06QyRTeXVeMYUrIGRxfN//4qxOcs35Dbu7g7irG9vNUS/zlCb/siAfZjR6rUzN+9Ui5bSY8JIYpdd3gaUU5fF6QJjUQQcfILNOqXuC9R7v2rviPbQHk/8Gu3wUftkUBYmB94AuOiN2nwdlvSELjVuEIW/GT2DjyBeoEf3tArrgFCQBEIpkF8HFm3u/hwVQb8DeclePmGrjSijltsV4gK6h069DV/XU5v2aSwq7ui7qgtbnBcw8bcg8yPNk/4KUCCRaEDZIkCyBY1aUakFtaMwlQLBpkV/NigWuVVXc8Nb2q/Ow4Ox/D3WXEUX+Y9ApL63V1Vy4Kulhv8YjgN89/PEdaqYuGZXc7JPjwsrjO70pijSCEtUBoyyU8hMsxh41AA0NWoMTLY4E3QH8Dxgxd5C1v3XXbbprJ06ck5gN1r0A6bGAg/w7tNCh/Pi3WT4F5Agd6mi/ukIkunj4xrR+71o9pVpb1CnktswZ1u5viARmZZjFETcwN8UoZZctVlbcX7jx4zICIewEsi56CExJS+rhcL4qP1MnQviWHkQRyOoxRg9AStTjoA3fqKwlXeDJJf9DmuGymRCgjfKun2XZDIsggOkqjDGZ9YqaPHBoeACqzK/Cu2tZwJaEpb3y+bs/g59CeJyPnwKU3ofcms//ST85EQAL6x5uKSbmhFkkj4rvUdHXUsOIjxPMTyEYTtOZNZrzvwr5J7rcvTbm5GVIifGHJV3rhK91pmJcPrG9wFy+L20tzZmk5gOZIgb6tQI8DRRT6LVtsegXXt/qEWx87IgdSq+4b0qTyumz48iAVAZlpLhod3v6mjdWDPUoTHs14DO8viuPLVTW/mUyyzUN7Xa3dbsuUiPEmF2HQR0mzP9R8XF76Y8ZrGOzr+PuzF6c//fh++vL0jU92cPNn2ZP0uJuitSMGdoKWCJB+11cwk0n2h/G3429tU7SJadJAdlAXzGZuZSPc4reVbcNf+3H2jiVqb7WrDd/4hhmmR/b1jnG5QS3qakOUOpvB9Es0wJL8h3blA5s/XQiHhDbevH398s17aEGI22M+/CXsKY0BDSUiT8MTMMYFEi+ZwIie2FAGn4mlAwVOISJDY1ZMef765cvTV9//eP7qLN083hduey1bhxOC4zaaEcjH0HZ95eSfs1c/n799/erl2av3j2qYRWUQj9d3ZV2tUeSDx+oSubZtXNHjzkUxC6CHPQFRrp7MDJsw1qDbfDML29/RNt+N3DreZuWydKtiTxw1aJil9DjRC2CsMcgbuJ18hUI23OLwS3WJVxdzx+tqtWjQINSi4LCCy2rdWksKMcx5XW7wABTzLV2R8FIB5AcHEcQK0MXhnWIFx/W8PUI5EcTXFZxSuHzY4sUiRpVZY9R2vUJJgqSOatMel2vhnmQ6zudz+rYiu5sVLdSEQA5Ybkk0AVY4hy3d4CoEk0OuSgo4UhE8xAI9NFKDamYWxM1pCbrEFuQH0JdB2shRlshJaoLjlxNfYMk1QTzmzJ/aMeKYLkleWZBPKpD7rVAzB50e53BfgqSF4gJby8ylUzVwryArL/J6AdpxYH8zNh59XJggeOdK27NnhWNFfKKU8uRjqIiIzaglyW5ZsV3NNu6kC9h71nlw3/G7yJAl9wHfwMRgFwXTFSijul1LblXd0YixgWTZC3mjrTZEgubVUqhki+PpaAXf0zZifnVEo7CECpp/dS8yBND2o5ryFhNIcoK8J68vS9Dy4OjICYRXSKpFKvPeQM4wBWoDIpuyti87Rr/j6YD3WLyw1m1FnB1Dtf8c7cKxXcp+NtkMtS2Umoq1OQD7WqIrM3ksSJQuG/wdd+t9N13Yf4uqYE0zZxkdtoE2Ea/8L5qeHDQUsPLVff7A59pbacWnJ8jZS3LK4h4NVuVNITs15PvDcGa6X7qng/uiFSTVHTqRSmBT0/tyAcecNpV+NeRjHhgbHY973L145fq6qIHuaTeCU007vK7M37uagc7gvm28ccjoiCaQ1na/37JPm1ka8MB8A2JT4a/4bf6RPXrrVq8CfFzebm+lPz6I9BDs2ZqF48uHPTtPbtdswMxvW+M6APVUa/gPk1RD9sU9rWyATTdDoVmzA3Ql/eFrNLSiF5709eWehpCUUeOt2X8COwGiIpl570HwbCZ4j65RTDZqQ/c/ux0wItChxH60Ju40knnTokOfe5piCxMe+vVVg8tt2vJnt3exZWF4WvliURqrFY9padzDTXZbXl3vGxWa85sK2GiTL4v2Achhs1Fmixqb8MgIbusSaGtNigiKIhNeSVig5Sq/woUq1sR9mHjdGu3d/rop6LxXWww8eWDZApalvCthZEiZotvvacm7Ir/3eAdvVr5qKjgm+5q5AqnKMlElAuEhKdFZhUxxP7OWHTcijvLPipfAv4JQp5ySWZJ81G554X/IkGGnZ+/rrbkw2IAJT25xNHt4PdFfARu0sFQr/ggU0dkmaL7btz43aL8QMnFMz2eeJE8yk+ztv8+USJVYEqIGECGBShbdK0NGZ1kaEwKRHfCP3a6yiChKktsN2CMa/HleB7WTnPvu6TGdTbfrmzWImyZwA43zTVtvkXmSFk/t0aNmYihy4d19yMBwOtg+TfSmKDYNm/KQlld5W9SHNOJsut4EkLfLsEl2hbHzXyRE5GSYxXuWnSMNi3Z86YEedL/3+DiKIi+VjhwgEfCw/VE7M5t9ODo+xsaOLmYzX1CobgqYBalT5X8WU4zNIiHSzIijtYjlihuQFI7dN7TZA2mVe2mygWzkCC6AqoSlHe1vpGjnY3tJEicSB5Kn/xwyHMvCRCNaNxhfQqqBsd6OfdVnVaEVnrW5Ri5Qd0U2xkJz+urdOT/diJu6c4PUpqCUYyUZxXRJjFgXBer70CU23kvpJ6DakMRJ80flFhiS0BrfQRugvtbIpmXHTWsGhOeFDiwsTXK178lFwOJNoh2Oedm2m63P3tHhakJWJtm7a9J0Cl/C9dShsdEUek7MxmeMo7nAAJbWE5daa4qxfIeF055bLeHXs5kEJIz1wGYzIz+RVOqkMaMuOusk20rmjrHtsJR9Y197TzYyYCctOQ6InUOnaMopL7dtwf4A4+rUdxLIH7fVHe1wzxc//2n8Nc5HmmLlHqaJ/nl4B36gWLNdk5ixkBO4c7jPguEGK+SxISduarLqWt0RkRGoREb7EOk0fxDzyYEGxz3j2k0TPSV4eeaIzq7/0S2JNnV29b+jpd93WE2JaeD6zGbpWxG/w3Act8KRWqOHsGuDv+0YQyiAzdg+bL6IxJDZTK/k/n6/6eg3Eqx5HaLLNV6C1J0VrIJnfn4yIcqxmoLYr9BYCHp6y0K4MYhRkKJ9VC7LJxPTkJthjnNRBGrfmbLFnYKHP3gRrBfZSRDS2nMEaWMAxOPfU2YoG0clrmVrNAMRQz/O/ugJmRc4eo8jGulLliHJawxf8WdTnydFX8NGiGEIpRxgvu47sjZR8/woLF/e0bNn/CCv9sst+TFfsvalX9DTET/x1MUwRTMODR1ArvrrhBEgeCLWP9AHqr5KyOH2iS5R1j6QEBX9FU7LXyoulR6lGBNvaRJqqRt3wqoXLJvINhJBZD/2b+vw2ykZi6dGKzVz0YO9MNso9AOXmqy5kFvgzNMEE3jtLtQbNujV+Mf0ezhMebj4WLbTBjMNJirpwDrAp7CZ8MDUxdA1xWo56gVGXxe1GNwZ4RGVoBz3WPJkRk+5k9P1xO4djB7/gvMVtdF9kKJHdx6q6OkOSjURoGrSyZPYuUS7Dmfn/uw4r13v7DrCnYv5+FMdtREf0eiRzuPqPRmE2MpFFjtmUHcR0zyF4UIzcJ6aVq5Tc1zG8tZJKGRLo8bhLGdo1uEnss25gOe5uuNUg9qHowPXKOPBa8oeP4pKkd91c3AeUAR1IiNtqMiLZZ2JsMy2s7HjSI1uhKSF2QxYEXJDepZj/1ByAoWM1JSWTGnRwsHtDGP79Dmc46pYthS0ZG0ywbtI2/Dmhwv9pr281WskJaOBqy6UgYeDAGvy7GCkVuGvcpWxHsGBnPOiJjVCzmvWzIt1XpcV65vFes4ZZGgp33rbf7ttyIi0Rsctjm6TX+WS5GAij/2RWjtjOB6nrHPwuRlMsCyB/BKtENugKAcO2xBJxqghQnBHjbEwha1r8QfaBvlnwETvfzWUGEYcLsZt04Mqggm+BZ5v4gxJvJanVcijH5mCz5sTNoY/euGsyKEn/r0ktXEL+Ko/J5TT4Isr+AHqp8xnlPXx8/4IaHPo9XXq+aqOO51VTdIdJbP3g9bVdWXWpBfaJt1RVksUPRWvYscj46DP6PEgC0M/fpJoAyOTBnaMasVojf231V/eyr53PgQbOkZhVCpkH564qvAMthVlrFAkARoAT3VLHM1v4wzRMIZ+W/Gfe6G+y/JqixbVbUvh2NrfDA1xpozpBXa+hn1ekS/ispjn3GihAxrQdY2BEYXXTrW+z+sFMVY9nXH23YMzgWEcB91SyEZMkMVlERK65/T2khlknGP1hn75fMnpCpQpqL00ZWOtK8aMPjv66gjYN2Y45I03BHTMWFc98T+7BxlnjMAlTPwuyBzRjahVZ3McTFuc4g/VFkSk/9jCUvPQUDSBcV4V+JVuRKJ0cRPybEIiBSrRSFpm62fj8EIMFSgTR65Ppy/6PYpNBa/ao+J/HpB9hys6G1C3oWl0GMwp6rKrL5hZJKk+anLx23Z+0VfRFH0/M0zV2AcW1uEs871F3zUFUKE92oQySVPmtFgr7h++JkbL4YPkaQsXKDXsHeNFlhNI3umI+OgpK7GNw6+81TjX3E15z0w4qfXokfWcuJLQuneE8nKF7rdIDvAPfWyf0XYvFqXieXSP365OpGnsWqT44XCtoicetWS35UdyWi+ViBdIzsZnJg6JX7tcqRntnQnuZVLjSq9dx7Nu6dIPBCu3z4nIorCNZTPBQLqRLQaCoN9DmeJJxkLS9F3ACYciC+bGceik3E1d3oIYDdep8qs3ZctB++iowdyIgOnf52uOisVsDlhqvPUkAlEyv5soLg7OCBoX1zqa5wkZEKr1FTyzKBsQ4TGBeWGv9ZKjKu0N9kDx4PA7BlGq+DY8iVWNtzvllyTUlX0E9vuQwDq3fd92A3lFynkHZS33Mvukqu8YfvSVnyzkJWvuau5Dnx2x/YvoxuBHTICiSiIP1ZJUu/H4vBs+YY943D2fasDd9olvfSVC+ZPNYyySWu8yzZudVPbIJPiZcx07qQxdxIGMkB7u3nF6fNcarqwi7KRIjkYiVkLOZHLJ+UYCzL6qKBXosqBQf87FptOGQiVeaBQLTFwoHfFnGgJJ1Ubn/OSYhwutCeYf2dxg8tFnaubsc6XULKSAjE2PJgz0AX2uKKMrm9F1jp7qwr91vCBh4pMYRkZBs/h8Ljnm9+y7Z2AKCdDB/lRD0rONOkXe5mmUsYW08+QPeonQq11qY6A6prva+WZgjep6fBjzjERnJ4kZcL72p84Rfp5+8sdg0pg/96O2VJJWxMaSw4k/tEnS+7cozV8O7acuNqscc4CO+6OsP+2HmveB7XjjJEPro7ghv2HZH/3p8ZHnKmYEEVxIWKPgCGwU73S4ekPJTKyh3HTQJrajjL2PGq33oh20/tQbO8VoMLcN2BJbBxX+jkui9ycR9Jjuiq1RgUvJN+DxI+RDgi++Dk1/gS8pMKwSstMNDZoMjxR3sHGRTCpw9x7EDT/dNGtWmG64Vs1J4g0bcWfA0DjPczZzoQAUP+4yyCSdBi11xLY9TlllbVkcX9ZFfmPSQo4RGgRGQeYcIw8SCghweSu/Ntdk2tXM0jNnE01cVhjcjBIaWYSsgdvbUCWzZW+LZTOxKahXZXu9vaScU8lD5azUp2XTACE8/fb3X3+b2o3IT+fvCT/pnHTwrfXSGebxZzQbF3X7YL12vol3gI24/Gf0oKiUbgYAumc8AyUDmU/G+IvPTvtHfgdHBwXjqCAcn6P2syNu5cCInL4fAxejMvjf07rRlXryzagXs3CTCZswjjs/KLBDuhjQsuvWM3Tdu2Xt9/s/5HSIPGsinSKTOa50mjxD6LXsigEKdMAwHoXjZT6nlKdqTqqDBC6TyAf6N2oLkhuA0cY1pV07CiLlDs7K2O3tvoxS2n+0VIvzdgCkPKTI5PZjfAFTChJnn3rLNPQ67IgB0WgXajc++RQiElJ/4jnAxvGu+Fvft5e5edN+EDwYmjPM8+Hnydcifd5/O/o6HGRSabMjTn4bjiO6r+0Qom/cq5+1n7/AUU6njrgRO2ySvFj+7iRzkW8aB45fTp0sr6ePuyIKio/zKcYBSTTQd6ArWLg+4/n1nybOPMm8J6MH28uJD1zoPUETVt7g9LyP9bxpFmxMTjmSPTlEX8pwKwewGa4hcpi5Ox6P4NQijRCpD8zyjNzUgftcukVXwH2DGBJAdSaXRwC8YpEP5nCn0H6C2rgq8vV2Y6MQMLcghaoi3MJng8SlUOyhdG7MlfbCkA3UmXOpSwpsRQnOwI3dSrYmTFEEEfgds8Yy4GJoC2+Kwhj5CQKMHzKLMVNGX52CYGEoKpcqQHH56Afg3nj6VvFuFEIHybY8HZNSh0JMIzmzM3l3pjIPJAVCvrGtuNZRWEFEP1LheJUkIMBLEgESwS6WJUg618ANXcIr/jtfemMvOQ93bYIJg9XGKUjKi3Vh6SuI0mXzMGFWdXf2MUfFOiOsnskkcZXg9cBEldLhGrjmCLI03LbBEMeOkDJq93iX0AUEBwCjjDo6/JUdxbfW38OtZb82Qe/mdIRh7zHdONSYervuzqTNXEB8kC2Cr7UUrGAbJbtKZ0OcuOUspt7eY9g8alYN+0p73SluKCSyIRbZPYtzKlWaY1X8YzDubO6VhCdgS41vsMDpiuIhqzdOSgkCemeW3pf3um6qCJOFyAQfp7zyuy6Yv/rOHdKSMlYig0nnYJJXSIfUjuEEB8qWFmBFhxpYhxfpOwZYQUAnZdOdoczGt3C0CPtmOXoJqEZEVZLYEZG0bNHhhlvMVn2kKyViYkgwG8OWDB+A2RhAVZHHlzIGi2ZTod8OmwVdcumPCSVTJEhmMpJcoGZZrFbjnsfKb4oH5pqUX8KaAl0w6/I/tkCRFfkQJbdI8tNNloYL0asW2xUa69prbizPriq8rMh4ypkdeCh/kZwejiARaCgyAzrqrA1eg7VNMw4JYSotBXIly/4ii3Jv0AJJF8vXXlKxWhP8jYw5DaMfUL/5An/YCH5WIVZAdLB+cKoaf30LhNJGSKADWfePp69+mP7L2f9GqKGjT1MSnafTz+NVvr466oUBIHjvTfGrLTQ1IOEkwewZJSzJglPPjvFcfDADwYBwajju/Ep3nuhXRKBkx9QJhaiYjkbZUbGe/vTuaLjvQtihxrj4IScA++Hy7sB7Ye7eaX9OMfeNFyv4X0xE3ksz3xWiDc20uTbbyJ2eKoNLFNTjB5CpgKJvKfUsEflvZTUMyJBUFq2m0ill8W7EISI4Vrb/BGH+KolnnFradCKLnwtxQMv7diV4dxDcDIv2+iQRPDGiuAD1dRwmEIB54paTIA+HlNGYRKhm0puKLDXpgJH/8PMFUcfPHkW8FZBWYFKmWYJdoUgAAowzOW6IW4ndqxwUYtdoRxgT97QNGJMNXWTwNoguZL5sPZQXrWhsLKKbYCc1FiTFItqPrb1qzCqmkcfHeDXgrd94uUZmOEdoKHRq6dBCLQlAFCP2yX1C2e6/wGA9VqegaXiwlAdHupVAjuGwRlaS2TcU1luHIeiTHKTa7Uko+bHfusK3KDKN+T0COOdhwp2RrUcUtWRWE4Y2BQ2QoWfG2euavrUt9rwk6QlcZXoHSPkgwkcAljVFmIXQN8uybtpHWoj+zCyIbK8B65av2Fgx6B8fkw0m+QyGwk1TEBh4cOCRABhaXwwcr4m/+QcMWlsj0PvickCxhsNYZg8Pn0YysjCg6NWjLDX7NpPdhIQAurpns6CpsU+yQqfjX2EKC03A/jEaBP0rruNRjXCdpRdTrxDcGdEw+VUX53FpziRtYXd4SYQsA1GSQGBxOElWh/PTkflg2kOHEcYFsqHFtuZDYxB8lAlGgirHiDMvbzGMOicaZ2+IbI8iqKeeyldvq3m1Eh5wr2yzAft0bNOpcWqreFWM1C3n1iPLmUnpHEfkuJR0O7WgEjPpw0w9hkqMg2iw1CRhSSGR4gCtnrPVA5UG3g3nZgqm6KaQYlEjRqOF0XBb/uo3fK6Ud6d70dKT3mcl4yBZ91+1BDve+tuwSCIg3fq+NHEAuMbsGrp8oO2isIvW3sTm3poJJp45S3buXiqWinsm4zXdXPZRtjNhC+KoQzynatuoQ+vIRMme+AYN55+D3e3tZK2Kmr3b1zLVFGmkzKmphvYaUp9gvIqYNzxZoi6QO4xMKmwOyu692ofxo5x3O22zkYlAHB9T1FLdMbYw5h7O4O1my6GzHFyCrwQJJFZdmM1IP1AyoqYFCd8TZI2rwiAK+pYbXg0PCgq6vmQXnodU55JJuEzB0honjBJbV9rL73G7O6zr0Y+s68nIEV9qoFf9h6Nm9gUG8PdTvQ3of1fvjvV3F1HRCYXmnnjJlg5JCDw2VwSvpcS7DvlVHL6TXgeGUjiDMdWNWQzoZWqGEdc3ZQEnmhsLymfQWi77n46yozHWEhokmh1+zj7Vd5+jm6q+G6/QFrMZKK68BH42xW13VO2hZwodvCgxeo0MKltUzDCk2+TIaTJZo72Ed9vdMddoKcdv9m6zvK6ejaGVF0Uwejali5zFf+hrw+hxKX7upsXKSatgCHOuC0CNRHNMpNc6b5s/6e7Z+mRJk4buvTl0W1PM4719CxiuoMupwBUs1g1WlTtwDb3F+xHzq+SyUxsxo/i7pmgb4T5roRfnYHLKF3Jwi9sCiz5Tfc9ErbYYiGQk3cGe2LygSEIvpGY58HQyAk8lo1GD6v3BMD5OSrZRiVPT67yRhQzTq61s4VuZDCq1GFc0Tq6JymjI4JCvFArNbIbNI0aSbczA6c9mJsnTKgU3xQPZrS8bSqFjeMENIy5RIu1spGAruGXv28ygkbL51SLd8B5znLcTKtBMOplRUTmMNUIstmK1U4727UB70+mi5LVdD5vyR+FLe1v9gG9emLa5Rl5gZvpzO0az3KrKF5YcVlV1s90YogjE20zXHYIbHBXDH6lex+oDOngvxNHrpFGdf49h6uO/Qs9EMNg1Nh/1nMRNiEeUGEFHx6FD25uiF8wjJk2NcqTx49nwEh+cWNfjMbxSKXoKDic0U9AgxZWrgbDoG1yHkaQ5tOPse3a8hcBetatrY15KKKKPM7/OSN3zkXx2uOmMA8znTWSamaTG6qfbPclO2WlLPiHxySMvZBx9WPzrfLNRsc7i4z3oaDL/OOk4cX7QK2VvEGg1L2PSwSFToK8SMr5yXZDgkJcroU2pIzSxxYna8auKOalHiVQDswmSSg0lcX6Lqz4k0FuE2xhWxopJ0w6BGiesR1NFC/NgMZ0MW+7gnwQS6UrHmJJVo4yDguycc5yAE/P2T1Nwm+PO6BktRaIGZc3rC65ehGGwuzpCjYz7IUhcqZIiK8iiF+l22ODeA/ONRwvPrfGEyCY0gnQYjXxLSnQ3pE1L6LCkF9AstaxqNgx4WX+R/YSNQcNgUXE9BjhZta5WJ+jWOf/iRfhI0Q2XwMn+J6l9ZdRO8cJFJnBeLQWDucvmpdUfNUZ/8Ki7fvHYreL71xu4G58S5rj45/bSi64bRZA7ncrSc22fsEEfkhaO9w8x0bzxoEZGHkIFX3P8vkbvM4Y/rEJR3kEvk13TJcGVZjewiq7VwU907byR6Kcnai1S8gVHZ4VyhTFWxjZurGr2dJR9xXg3dMvDX1/d3KsPnLjx8x7pRnoPO7WFG/f15ESNQGjZNy3pwZeEvmCCRsz5OWH8bVSmp8VHKJEJ5gRsQLjo+QNqRFyRVQBUanepUgDbfZURrruOpbM+YhVl8GzMzjryQNnsSOMbyN0YKLcUruAAFMVzczPq8AOCeOsn6sKkYGqIBjE2Gau7O7HfpIfEgheFxNiMLdLMwtS2PECy7hqJqeWFiiqb+qT5TYyig/8MGCzzGXrSNj20UDKJ+cPjfrod29yh62VJSbIWkuSxIiG8PZsxlQmWD/qwb9h5EuR/UFQRFjRLYEcTIAahtxpNnleJlD/xLMHy2tQOiuffN9pvg/v41OOE2DDbLNlbcctVvdW6oY/Vz+fpFHN1kVUh2C5jHtHvdH5L+E0Grjw0yNiHxu4MNh05uHxvoweBBa+k3Y+MwizOhHncID/feSdNKJkOoOBhjHclwXXn89ombX3gfsy5+qPEdAODI8cMSThNeC/at4OXPJOp60IgH+M8BoG5opuIi6etM6ZtkzUGX2oQq7SJ1agRRrdwdlWjjaJLO/nqk+yXQudDXZeLQge8iCWDWxb+hRkiUhewo835dYW+PKM/oVNu7eCicslbw9p64+x07TLcZzPe/dmso2FMUb0sCny7Ka/WzNI2WMEaxAyBVN5ews/s9M05ZbGKRGDMOR3tGv2Uc6y6Oj808+rrf/iHX9/E339pE5vtaoUN/CHZANCcTy5wymmTJ709KOWWvJIptXQREul+cDR9YYkRJbIpnkduhctkew0HR0lyA+kCNkeiEfDUkY0XFaY9GDKPR6YetEKI8hXVE4YT3FClTokCGocHXg7qeLvBUmsD7nZX6q1jEj3fs5ysJMv5QnGYuJCo9Z+TTOWEKe20kOmKmOYV736EDKjvlHflbbnKKfZELkI5giQk4G3dqOva3PW9WNJw9QQkV8JeexpP2lwFIsmpVIuxi9FgTkginAsHMFLUiMPIveb+H5AiZlaA2CdBpC70J6C6r3liBroDNsSf44huCF4Gty7j0PYVVl9PywThFd5/rm59qrtkFTCdwJ30CUZ3m60W7F1pMYPR/OLC3LbcoHzYSyqyorXQ9LoPS5MqTBmZZQ3GbYBsm45WeSdGWVU5VFkkxdXt1RhdSdUXP2DOVbJlnhraZaleYXWJDpBi0WXRfR8Ar3Vbdc0cT6W8Jb4RhPXOq7oIa3LuDIUJV/aD4ff8p28N2bcNtNpB77uM4v9T6x8EkZlkWsRBw5hdW2Y9Z+hHV7VRamI6eOF1XNvS1O6w1hKpuOzng2fnbc9TqC6trLZjK0151RnzmKDOlKuRCRo3l1nDcs1rx9NNgadfQYo1u1WDgT2Obzs/H0tz5m7xN44gkkw5TppCvvbrauiquTvD20LCpYB9gZO0NTfZOuZquysgQhzcZd6APHq5LVcUCkUBrhyDojafPIlLyrQQRIPnkrNxKg2YxyXzzn/dQMiwWnl1jfdEs8lbSRq8RfuseUYX4BTgVi5vaKtsensb4jsqcEcU2aWEZy4Al65GR9kGlVo4alQSFRu/gI1Ot/EAS03S0O7SMXINm2RNL9XQH4Uz2F0XSv80JiNd40XGFpbMJPB+j+g4Uc6EqUp+admo+pSGb6RmgZChJV30hiW/pp5n6Hg2H52KmDUzNU2jYkcTFzIlRm9VGdIbjn6z2JSr6grRssXNpRtgyM+jhqvUsDRjSnVVnZXqnPE6X+J6UIFYDkNHATqsPFOzLZqHT38LZEnH6GPgqTD57bq6X8cMtmkFjc8D1U4U9lospgqoa4KBh4YDE/6IadJseEMhu4wY1qFfWq6/0XWEElm8ggznMU9zGCvK5J+WjV0vsaFTBihldKFbEms4kSToGwF3jsswSzMygWBRVTvVKox3NnW+TIC4YkE/t0SU2Stj29lWYhpy0j3ipxI3E2O5cKTCrF8VPWqCap8GPoQc3jMuYDdjyJ/1cXG7aR/kJIwyVsthp8XH2DlukT5s0oUmwhiz5BpY9QrZdXe9NPQVLhzgCIrTvJCcS2PcpUJMNqdq0dng5YPEx+bmnIucoUve7y+WJHnrfL8Vqtwwh19rlWU2w7hkgzWvpErE5THrASsvcxC8Xz08MlrjvaVTxlRpBbc8vqt5XxElLmXDRW2YFUolGy4hJLxJNEQJANquBTmyWHiVDjToEu40BseRMMjE54Mx8cKgvLcuW9WMUAnh+ktBUHyQN4wDIf48NwsJEwdZD4EQHlUMPqfCPpsaMwyAV5qWDZUSEtlBBZe8+IyAPx1YdOmbzqJLvrQQR36ElYNsxe19dYNsNtjBVYPMYHTNIIM8AQqP/GpHE0btKGuqqQgf1XMijPFxrwPCl2KUHt9+XBYq6iYFGUsxWI/orKMqluqrEz1UTSxZKykWGvdUTulpH0BX5lGqGhMBVVnd5MIWBoqGZCSm4GOZE2Xe3uV19L2WdIKvYonDFAnyP36tszp4zUKZwNU0kkvRfqBvO0nygGEcXnYn3J5RGIpw+B51FlM5bNui9zq2cFetmJ2VeuIdThV1SexmXESomygwqeDD6zfvz1+/eneh8MWSxCCHUu1GauPjaj2aCuJv0yThP9ddl4ZUQk8fBBH/huq/Xufoof9pU62tkMzyRqJ4TIXGEqPl/IAq5IxRBUibZIRWYT/hJRXVewlrKcAg/ne1NclzpryhFEjQb7tyOHw1/BvoPMamYrmcCR9J5nyTtUCq2tDYfPBK/4SkHavRUwTL55c5OEBhTqrCEYBl1FX4UVTLROvKJlVQZV6KxF3WhcUGZjuEXyzC1C7ywFyw6gxJR6y+hIN1Ht2o1rapzCPFfJVGHuqNApsB0j3XOC8eEmUbUBzCJPQyyD+S3EKuZJFlZ5Ssu+koPIT6jpendGnQkmykHOgXoEtiI1RjoqMCkfwCEwnhNmlgJ379evqCWRd8xb8ExWZ8boRh/v4nEUCo8DiGB5U/AixXn10hkKv/SWA19p/1HIl8hHzGRhkd3ifBQnB50xPhdGGFF6trnShmtwNikX1zRqDbA4qTxg3U0H8J1D/eU3hAnKQ+fGHCvaJyrNCHfxG0RzDlE0cVwddMBuYB/it4xO2secx9EnZGi2y7o7+CR9wym8fcJx0wgKiKaBRAL1ZSFnnZ/yNjJU9ZCJ9OxxYZJvtkF/vzn/pdIaSJrQ2DMhnVpJEQBzz2xjKrlVEFBVFqe/hzAnNwsUTQFHc/s1kiGh5bW5wd9AljCwR4LUOfrHXb7Ea3j0apOvYbpCOJEB/XnAvX/7d1f5hw0zRd6xXIV5NAAPP9d6nSfOYJE38qH3NwSkfFgJ2Zkdzrh69MVIB6z4+kn04XxeX2ajoN/JRdULQivFHr8NM/l9IxfobfmU+QvzYXURNTqcRGiiL9hkF1jR+Ns9hyIHgxlV4Hplf4iQG42zV1pBscl20BmzVkCQMf+FP2LBGP5LVNrnyvs9gruwOH18YFdlqXlhT25fwxn7zePttEYPJItNfk9J7DpdrvbLH/lgB92RBmGrOxIHzbO5g7RuAap5sbpmvZK7zeb0eJqLMgouS1qK+3OdpEV+UlAm0VcNEL6jMZipB9sIhM3pYNjBN3Iw5OYVcfe+AGAjKdMch00QwZ0x+xEk1MKHZCjChoijprypXAZd8V9X1dGi8oBxLYBQPxNgCXNipeilxViFriHHQRsil6EZOKbYnjry1xe/0nqbu3Y198UjeJb8F4ElB5uGc1HXRBxkgcdT9O7+QkaDZIuk4xsbXpKB1ldcCR233s3NGjIX7yB/i7+jPDCDRFfaey8Ps72+tbMVYXOhlnP2CRT9/rmu9ralEulwX5XMi82P10xxHdf0zjAFEO2cIiB+uHgYqKoWGPMuNSC6Uu3qfh/+RGEYe8fPDO/b69ojpIwTknF4EELlmOwF6sPc1RQ4ZNGEAcU9aLKSAA4983PoON+Nfd/1AAEmFEB9A54SmWc5QsNfHh8dLxQL84FhtKjB6m3TjGks2zVXV/zIVXLErxSlxtIkZaEbYjEofxEoxYJTVUfSwFL/DXjYcowcmQHobEKOtnfQFZoEaGGs8n1ckugdEH/yc8J5aeDU6oTIK8ZVeVWzy1oEro1tjMhBBsdH8DttmdJf8hpQVfWCSOUD2mIrEfLnbFvgXKWYj+sQfmAl+JKVZn1weCMt9hX7ba7a7iVYo8v6+rjZKzGptGjhuG2NcP7hohDhWxnNdkYVEYoIG7IJrQjAuvSasS22tA9hj4FE3Be0M8/z4IFnpFo74pio0trAHKowFFxVGmiQWzIY30gnr5GP3IiPhKJyUa/vCRwZFG8sGHBl851WGUyR9NMa/Wi7x+IPk8hV1tBjjeVJsBS1GMORXQEpLEwD497CSsLpLyPA6dZKUIyYbc2Gff3ZSIg6hQHgIDkYAtCG7Dozf5fTAActI2pCzw3UV1Ag1BMd16xmN2A08pdM5mjBuvcFN5Off44sI6jUEv9VpZVWs06VG5mMbVwNwzoWfjP+ycEOm/93A3zK+lSPddVS7Yu8nAWy7CuC1vuzKGUvXoUsq48JhIJaesnagNLo+UNAUelGf/HGfVQUQkf3Oup+cIc+Z3r4Dv/nWB3hFThf3dtr4wFffx/AHO1E1MQsDHEC5CmD3aZqaufORgaAA964KqxFa6JJHBuwcdEBS3Rmf92UINgb0jtnU8yU55dmU932K0PlsufP2Nwl3GNibAoDvrbYtSLIKV56LwnHCkDhEdhBLDEArGY+GDFDSm1d4R8V3ZBwQnpZhjCopVavuAws6CsLgnTubl88ZGziFLCIyXSJfOfUlxJ9UlRjTbsfpropncVxH9jrLpX85+fDNlP9z03fvXb09/OJu+On15Ngwqi3SZsBWbJ248JLWeIl3Z6/0hbzBTCefTEc0+jTbIgjZTGaK6iz1PmT+/oYc68ZuJAa4eKPqNR82txqF1nmjJz5z4nUTi5GMEIr7dkE20lZkZ/6C5DROyO3zXkVb/SHsuEfmvsOQSx//rGHJpSr+tHddZ8KfwjKzXqrwtWwNQ8fu/Ty7ZDwhepYIwLfyAyT6uiS7Jy4tFR/DJwuDt9fRFqANJO3NYA3dDWLb8I44U1HbMFBoTwvGimg+Cl3R6lmmyszHO5DS3vGtk4BwovE67kr6kLRN/pOeiXOqpV5b9T9MB0evn7JMQxtS9M13llwWjs6iWsBxkhG+AgnQMaacI6jfVbd3p8ZHaGaaMSzj8ChWXDlecZSHg2NAfhiohkXEjjcrlP3Z4sJ7PZccDdCzIzb/jIRt0uushE+m04xEJKuzOoznImxOyiynjMB/wtJ3HQU/LhA56lmcWPZoixqnGEvkNKZLBnncSZYrhMDV3uZXg9q63a/IxpDsiiAbsLlsWgQy0k2URs0JRoS0Zo/zflv3hh68vfltOQywEOd2h3GUvn6LXPvcp1Qa/JrsIfeiXr/bAYFLWJrz/r+p8cz0YJoonuadLEFgkSXyQhiX1miXiosFGxGfo+TcjPWOzMhGMaZ5I2aKM2ajpzzgUL77couTl9tPRqlHcX8T5/Rbs0VA5G3L8/NDQEh4vNo55TGE5i0F956uEsfcw2MimoBilwXTQF59Zf3jAhi5W4iENt9Mxs9+Ul7gEg5aTKAibgg4+r27KoOsZjDhu9n9yYx+1qTjerk2NE3W7N/VN1ZQSjmbX8NAdjhPb1aXym26vxETtE19SN4VEgvqwIybEKs3l+evh/xgjNP37eqTBLekI7rWxjekIXwYRsPZlL/IWa+HFkMD++199RdHsBpjABZamMMRI+LN1CSjCkEFkEEHEAHozNJM9t2TJudFwQ9XSJnrZvEgPPT0M+eS6YQagRq2MLcrIqbExUhVWKLq6PqTEkZ+i8GsKHHkZCenyRhPDf+zWepDoTvvHKc5NvCyX1qQE4OvuFDtcGD5chG+8KBgwEb2Cqm2Do7ir7GCm8vpeyFtttWGdQV6nFMRtQ3nJOxoK81T5bWIcLt/VJLoeNY9oKQLYM6mCDK7hXyEV2y3QOdXpmnLJpHR8vIxEi5mR3+Xlih0fwXtymuhHAnzLwX35QcC9rg1Yc8Xmqv611bG6ybLDWoKjHxlI5XU6ONlEgkQQE4jsLAATvCKx9x8//gDPJWq4KXwnb9hy+Ti0QFfA2UAF8o+R4WvDAPvF1R2Vqkwn5NwYJiC3iVS4ZDopUj7ejMGFMRVCqdC6eyN1SwasurMGOxv0LdBUMvbWCIw4n7poylWJiDbCUVNoIq8qEDqa8wbvZ0YV8d0I1qYodcycgdOTctgZxrAeXLGeg7ZPpAG9avifEyVLRMLXDrO9anuUFNKCHZuOjKTHohoHdfMUpIoG76KbwFA7O34pyJPD8iWBFRlD+JGAvbGQMnNIRDOO/6IkLKy/iCFnAoze86PIuiHHnyB13CE+sXkG++A4fCo4hTj4LlmVbPR4D2KxMCxwLG+pBvWccsJ8zbbQ60oSvvmegc/zlfBCioHjMVDaGbJzYk54I5sOVKsUHMi+AQuSJxgVKA9Yv46EZ0lGB0fScfqia8sH9HoMAtc/PnvMa4K69Y//1AsdvYrBKYypLq62D5TLNWERV3yvmgjx3vGNcgofcbbxEUKNjgP54Lz4YqUX8vMDJq6uGeipWMh1ZX07g084kM/D/ugL3m72vb4q1qxiJOI5xYLFvMOGutzmmwHF+dNrw2FXrCEtJ7MB/OF9jgYHqXlfWBQxYVredxGPp6Y64F5DP0eM4fUDC8gOUdcKdjdGx21bdiBbsXWgtR9VJIJTX2rK3bnPH3bb7rusUCYr/SSbxsTR/97la//C8XQmhZdF608qFM71MP7E9Cttf/ZjyOyuJsNNTzryMayQYFo9SdjNmu0SNi0ynI16O7Ap59cVUNPDqnCg5Murk35dLPrDESKdn1AFgNiuNw9guBJVhaxUMFZ0MnYgoF995XiEhsOCS32F9XKAc7bp4Ldybb52qEwkPTznj2HDzoFtpSKMVCTW3D7sgo1cywbG6MequnGnRy4fW6zJw529JhgmZvvHCcQ3hBa2apkJBDegkHn6fXJpN+XlSqtccw3dA+MsCfIkh+Y3FfrNJA7S9WUmGutdtLDnVrWygn0ajcVT2Nwe/Ex3wWUhndKnoCm9zCmpjBAqvqjOI0UKMFiURxUUf8BBA/6OawcUyjlGxgzogo1gHkqt23drf7Qfffj6Ylw2+Wq9vQ0vwkca0KS39EWUNnqyRTINimo6HwdpZcEz3VG+hEBFLRhZpfNRIiQ4r0mYNBd1Puzt0MRDsK/nr1++PH31/Y/nr84OjJPuCHdGqi3XYelrRQcmxDH5tk8dEqiGqsWJrK7nvu2KkfuwP0buogvplIRB2Pe6xYyK64GjveGuPA+uYSVcdGfdLtYi3ZO7yq+pONtRRqnOQ7v7hhsQr5r8xqutcf+D0IDh7vUf6fRZe3dM70rknIUpvibK667FVfthdHYSmLAaKgl4U0+Y6g7olpXohsq/hQXssnOydkzwBO8ICHZekGLciSxQV1eRXTTGLbDLEqX4B6gBQAeLfAWfTW+pWEi6/tEBhlNVWWR34YC/pdUQlAJEfDhkBeJiBn+Dk/X3G2usgnYMitoGg/F5zMkn96xLZFAXcy2WUXDlEXJjYWXIYvFtXhYYeKFhGoAdNi1CDua6VANH5rsKNibtnzPgV1yKgUvZt4VXJV2XvWGTAUPBjzOuPMXjRTZxn68JLX82e/fQAFfDUjEKY21dFIvGFoPegsoSRpFI2AkGo6wwmNfYVAnPzJRlMDVsWVC1Nfkc7ShvAUMvxfJe0hZMhaGvvQrPIthwHg1OV3sbkqBsOPvmoUGl8+7Ds8kF4wtjY7FB2RGtLs4kUB3RYGAA31lYtz32cK8tBp5jq7EA1+U3ZgWX5WoX2hw7CFAAVdP6+kLDglnESH24GHjwrqwriqWkQsBcfYxqBxgcvA5QQgV/ljfXSnI3hbu5pIgqeba7ldmsP/2jXe4/TUHievPj2fuzPpYexNNkv8vK9e6moP+iRhyMGLY3ui/UCC+L6/yuBGoq/YonUc3ybqQ61UGGHYwFdJSPMDS13tsI20BdvWZbeIqMfYS9jjbNve14tb+4EV0phUdE0cl7mxJBgWpxXm9bqgPvijWC+k5ZKnD09rZkOJEA0kmOQjcYeMcp3uRXBEJhvDFoNqztFHm4e9tROMXaPyp83av1tq+pahkgvUeEl7yHzuiP7GpVXWJpZ4yEBXmczcVlPYqq4sBhxUNK6PH+nYHWMxj7L9zLY91bfkWbR7u4suxdUUTuX+LLZHbW+EF73WHjZ531ChNrqEHsbL1zP32a4Da1jcGsWEbtNFId2y7do/x1vxBcD3PqALff8WJEwLQdjGi7gyju5Na77c6pFBxOuljsLZfjDVAvX8D7Dqr8SISVzJ8Qg62+RuMCOI1N7+6v2z5NKnUQkuE0aOlUDw32FY6QdzhBy3cUYbK5uzxSs3Ffn2ABXlSgpnI/08cDz+30hj1ebP/TN1+NcnAj1Zqo4mCR1zq03RS/KdfT0Ew0oNM5ckMZede1GkBbPwRBjeEH1nHq3IImXkU1b2D+2eWKomhUR8MLp/IKBXRVvzHlQoOLtrfnbqnvusrIYKgCtZgvye1FvjwqElnfDWcZFi37Xce78JoIM7P6bkauPovGLj4/dgKSY6z/rM9JRzsK5VSXICBsm6xYLr1aG/6TxPWa7WWLJg8QQybY/8kH0l+I+C4YmRX2l4qga8nYb8kYWq0ZlbOiULHghcPkOXY8Hh9zStFR0ykAP2EEVNEkzFzcAjmc/2Y7Rwp/il6qbd1Vk4hyuBqM7kW0MX5+1dBvaduX2bnAtE+iTjY4e/2CfNyj7F+Kh8sKbqNzlC/q7aYl6kxQEXkFUDw+QUbUtAt4fNhRXEsqjTJ3LlIjIFHtzAheHV0+hsCp43jQmGtxP4jHiXOg9Snox5QreCYG+vodLVT3CKGBul5XyHnpl/HZm/M3Z+lBysohHdoKX+34PXCF89ejbPomn5fLhxerbXP9S40hjvXAvdFh7HKb8cgmcfu6m6S1eZaCpGgO2gBZPVR5E0uH/qJ9m3rADiXH8oTQWKr1caAfjHRpZ1uitreLlRkRmqpO1AX7ILFajmLPMyzQe1fw9JtEc8B8aBVUmTLgH+01sIPLB8Q7tBDGVFAecwOLDub4RBIIZSLIENiimHjSJaGTSypYEFcKGTk19UnYfpepBWmAdxJCe357WV5tgZFNJFAcAYOwki4sJGIvGJ65QqTKhN70RN0oz+BCkUIMM5nPs9lI6xj3FQZxYFHLRJlumWQhZh+D0DLHKbT3hSA7tvdVr+MWVFQVEi1xr4hgD+JEMRcitjkdcIHmYvE79KDu5KL69Onitik5psNcCIs8VSbDbjDbLovhI22E6Sjm50TPUuOE5Te0ZDU3tqZxfqkkOhLH5vo+wzNIFLHWAh5lOJrSKBYau1zPDD6lNqIYw48NKEjUI1JT12Xm+YhI/Xc3hz2GHq+FToPPdbVa+LxCIjWtH1NUPrJMfm/LiVRh4v+nN29f/0Apv5+tFecAHIJvAt3q+wqDUuDF2WyIs53N7KIIfHxdbFb53JTepqzcZk5gBfDaFF7r1Gv0+nRhx8oTohXYvsfS66B/3B9l/Wl/6D4ZyyfplhghdNmffvIad4vUH5PlSisbas0RXRROIe8fVc3p0A9M1TL3aircoef7rjvd1n6QQ88HgOmKgJhKrGFalxnpsalcOMNkbAYFw1lSMfWpNP4lBflOV2XOgRP6aI67ShSR0yMugibFiaYvQJ16t72kv4idDDA5Xdy9POJGvqbbE4Y+X2GBRfmQ4e5pqOiIiJA5MVruUgJn5RdYvlUznk6xLkQznX74+mLo44yzhd3rznyol6ajP+tPNb8k+zPzV5UnBrZUIkL80Pcn8foMbSEnU7RABTn5RTB+kfJbNQEUko+U7af/NP56nP1E9Uxt1QtbYtRVL5Axkit44JfpQzoQDOxSO12I961Ja/eLA2YDY8Cit5phqpRTWKaLO0hVwTHNTjK48yQsBytEBn4QW42IyhdS9Ww0aYwC6YuMpRilaaGAXAH2mSk/ggg73hj4uplKisTUVqE/d6ZumsBR46LaTNEMh46IVOnqryCroXSS3bV1zhOVZ+i1EWNE675NrLSSgbCYpbpyJGVwg+lStPamzG04N+MEMfZ/O3SHIP8X7B2rTRFg0lrK4bk5utrhaxrZuKeBZYzzgDbEtXF/XRAxzWakwrOHCa3ejO/iUwZFKGAFTRiJEb41mmdyOOPslBx94vpSrjV58sjVBEJ2QKB9agPgiKH9w8BNmeZDK/wtVjwz5bWbKqv8fWZpfuoKDZw6SzrGKKOlwsUwmw3We+umpqiu4VI8bKK3lo7Lh3BvSoPO7jaFCg+pRZfaSSNVm9AU+XMBdhIsrnry6tColBUY2fkuckt3GdUXbDtnFx7AEZIMkAFXT6IBl0tVbieeqrdB5n58Tbvstt+B2nd4YveZv99LVRleFZLI5JzMNePSZflsobWDCiK9LOorI9sxsghJWY2Ys19iGJi7C1zJpKCYTaL4C5Vjcd+lKrb49WHOqfTpyOreBk1S75eIFoYQXCUh00hQezJi/Dx4C/UgxTBNwW8YqmnI0T8H6ueSFOeiKN0yHFaMhzmiX4yHPguCOb5kQa64AsWhyyEFKx6zGHw9+0sh93BqIXjs5hKhgqL5Sg6jGQWuwQzBlFrO3TPvzng6vFRo28LnZkNZCHt06Vtz1GltcudHh2bMpPUsGQUmNQEbM2jmb5rRjz5iv9UUZLdp0WGr8Y9wy9moYaKq+ImSUlkonJeifrdS+Ji/7mmq2qXvGyLrubzQWDax9XZSdzcW3KHO+Ap1ZX6ie2l33aIvLtqTil961LTx3/8JoqgSDyT72rVkcXGadIWbruiq9GJ3ztqtf9zx4/Yikevs6Xid1p1mS7rz2G4hK55BGWtnAGAG2qX887dUMMZDQ9LRp7Ykn969YXdT8zGTz5wzaJUbnwCx+WsdGesXq3lP1lOu/gMsRV8DzHpLBppDkx4rGFGRGjsW82tgNfDTFZNrk6on0naQYu9XlSJJt0k4Abs7Q0t+RL9dMIC/mIo7wnFBeKm3a5tGnqumWKBDQ7KKJwihbiqs8qyK3RKDvK9B5ytJhbis4QwUrVS4XSL0L+mg49C22xG4XC67zn3abxIvBNbJknj2ZxfZh9O3P7y7gHOYmQ+/cZ/h//uP8LekezOdPbYvPnAHTnb3RNU8+wf4a3bOQzcVFPhJvRZ/GJxK3Gc8ktd+Dup7h9irkBFywe/Fwpljr+ota5caJ5MRELCcjxTzAgEHtbPJLGDJXt1LP1YheBLGGXySSLiKCfcwHN84wD8CljcpL5yzgPEWZV0sdjj/sreYSHtbcPZ0d0kMaxta80aQew6XlmBaMcHYFgqzSlRXTYzfqh6SfZWoiG83r0UvCsO/tHqpxA9aedTKvDyD9I2l0WzlI05KkFrtIbdyGnnMZbsTX+BwTF3GB4MJ0mcSriLNplBd5NUDgAH06tgUX2Oj87eI2/QM8PKdST41DZ24658I5sTRfpSLahtxlIGgnWZZhTBuF5PMGlQ7BM20V+utK5wt5pxIu3Tl/IjOTWB44UM3i+zhAqW7StVH8dLamK6q9qHxAiY29ntZZt2hWHxqUZvgI9uXKVD8zbqSagduiads9UYLLqjvtP6yoOJ4uF2MrNgkOaJJqchRxu2iO/3D37PplPhph0QriDwile/MKjHN+mrHbm+HTlGw3X9ID+XCqR1Bisd/Y89Ays/D65vM8pRCNcfIYnMNcfEZQpc3YEOccO2VvdTORyvCya3Y5iZTm0AMlOW5MbegtdVoMa68vS0WJZcfcgXpifYJ7WihTKXekdJGQ5P/IDYQddhnPqxRCGekDDh0wCxOUYj97xlt0nhF+0p125QOt+rGdIfpJApLiTPxroumaH49oE3XuDvTahMA3JHIv+sYpmxYIazDXN6j0FH0dwXBpU2D0fcWAwEDq55ZYYTPxiiRvdpHl9WROWJWFR2Y7oZHBG/E7LZTsPD9yQOc62gYoiWoGNhBKgterzuNnMdCjuT+fNX0h2l+zI99oEcuHL6Paq2n6t8uHTENlgfxRV/2UHegu6MjZ+wgBSOub1RoZ5hM8b/11URcyr24AGpG8HicQOlMm53snRjE4XcG2eP23Bjc5F+Ra9Mgwtviv6lXOLK8Br/ynmDG/OtuCTFgP+6OMMWWf+sbQpuFvfvB8Fdl3J0pLIi/kdtBj/5L7wZq42/kZuDj9D95L2h/RgiBeuAN0dEUlWyI5h1eJ/hUojBE2iyTvItUt192EwWcxt5D7BU5EZ73N3UH4fQDO43cPuJF9W31EXt+AZz4hQ9KA2dVKpX4FqnKA3TU+a9+1Ej4YqnLX1mjtisjw1YYyncEHuBjeAoj4tyuYuHMWzOZns5typQlzWO/ZhgShOla0bl/FvvSWvFsGwNB4jFubv2aJIFh8IC20xuzrg7YcI57ynK4x5hUcY6qwi78yYMEWxMaDzrw3brwPlBMh5mc4sZnH3OMv5tMfEPKnxmyhil8mPpKKr0cHZdHo+zo+Lhcb7Yt/Cp7fPLNt0G5UlQpV+WAnhtOuiKdf/9NPJBxSLFxyxKNa7AB+QfGuu3sTEjv7/i5KNbWHonSeoZnaCeYUbVIoVFC9cILuyO4PSKsez+U7ZE349uCK/VIKiF3as21s50YRd8GGEXdTPEFHfwX/sJVq4U29ybNwHGqn35Jbgm0/Zi1jS3sHdblZdcmLnsRNZhIowEdOSuAPv2CKFFn7lsTLqaez0AqNkUhoV0jHVAriRd6h6wABdaysXF6L+kxkkPzYmTnjBH7yesFXg/rJfE3xzSNi0OuDm3zTaGawRXm4vK1NpcqbBdA6bGMbk0dbKQjrm5KB8XgEA7IVOqYKaA9pjJ+c8a5cB21PXWMr43mojhqmY4y3PtG8i+rxunQil3AGsd9Suw+g7ASWxcYCdztvE5GJpNbd+AN/rcrIsrB3OJi2F/1tL4zYPcdbq/hrhqcv20pCTPqLyi44oMt7Si5soMQHgujf0Y5+PyOqb7DqfIYz2Y/IUdfTiJXEH/olDobVilzTlO9cgsF5RR0HOWhvqHbbr+Qa24YudjZa4mnfCSA5CB8khKZX7KX3QmQ58S04IBHLqbbQ9xL8UuCOXeoS8qyBiHxgQ4AQKHeywWnEEJazm+5KKIX1dZs8rkn6y1JU7QHOKg0g3Wy8A50ZQjKBcikx9k/wP9v848Devl2gWoox5DcLnQUSXDD1IgL7G17vPWjsI14mSRWA1cygXcWFPfSfSdXkCDqYv8hPt+RxJ6slCFXQ7o6hkwzrJCBfQz/vwH8DY1or3Aa9fwQz3wCM459/G30WFyUhQI08Nm9bY50C/jGh8mzixH/5kFHKCzWX4HgS6F8gbKghcQuEdDWDw3Fsk793CHJnEQAsmErI1P708eTTQhxAbK+0FhiXdPDPzRm5glMSF8D97nVhsOi0WolktnNgQrNdkpPkV6SoHaFxVYTLXAQayLYndicwCDoSNeITewBr5CTsg+/QoYcEs+HC/8Qcc0uTFQLCZ+AvYFBvRSUdVsK1L83XhQtAjnggeDCuMrGYQ7aV8mD9JU5Gxe93aczfbQ1nOwTDAK4L45qByFpY19G8AUnmfMVx3hPFn230radJxlOdeUcmAgVdV8wQhzD/7AMs629mhx8+fulglPRBhaNaxweiS629iR7iYHiAsTvCsWQsUXiJysc4qLio4XhI6B1BY3A7hP+ayNw/OpACIEAdWJgwn1V3+hwiN1UaRQOiU4wWDo4F+xxdVf4olVa3RTTs4hFhj0l4e3lfE7VWT+xo0hEv+0/KypwB+QCD2LGzc5WnMAaH8lmdCTP5DEH0kT/mONl+K4E8Hhn7dyL6bo3XhpNGBhSV2yoAiv85BoRVsBWTXGaiYpIucS0fpPGnWXfFfOcEhE4qFNQri2dqZZu1nKotH3wgZMqCstQdeKXLdKtWPATa7GafTWb8UFxx02nOOpuGFAKU8mVBfEJTRrjh5BtIX8b93aTcidl9b/qWx8KMcqIMFMkFpy917Yi9X3BaR8IVvfgWI0zJzioPgowzHsJFB3ULNYCbZhLeQvvfmqr5PEO2hJ4OKPao3YW79VRY/GFe6HCQcQWCeWMj5xGw/pv5RY7znUa08cMLvktMYDkN44pdLwZpG2d+Git8aNRFtdJkIaQ9prpbbFGDRMFmJyODTw0G6H+HHnXqxU+0hoYrXNpgey71J/d3PEuGHQ3QzyAqXqJ9yFtBU7/ncqScvpvNyvCBTY18AJT4ci9pJQqQ1WwdhwSCoqm8b9qzv6DOL9D18wuO0Vg7VNyEPqVjkDiLAVIUkszDCxDmeNr1CxX5X+SIKTasImvFhOXZKi2fjByMfAemLInxCijhoUupxyCqemmmO72EKq16npZWTgPtebsWKVFlVyl+4LAhZGPUkY3ivEag1SXV6pcYSQOaW9J99Cxp8Cv/1LdI8sf2cXv+fg/7qbCzsWEuqgMEJAUFrZ5qSusj0FYbz0PCYnNZ3r8WPiQCh7aYodXKD7gudV16Xq+oAiHJaMhcSfZ8THp/gw/hLe9jPCq0t461Qi5SnZSx4E2BHhz2mxWJZlD3ebD6elwxOyumqbNEu+28+vnzsNtyrhVoOxcwpDasmhOPCM1cQpf+HOOcIFk5IlaSUFddM448LdacEUbRpuRLr/icuy7yrD8/6PyCV6QHq39FnUXfrN6CxfhcE3JCCPCBkQZNjTsrLdgAFV0JvuAgk7+W/FUOON6B5qKDO05u4XQ/sqlNhSuSpC9TTyd2e1242keaDmgBAUx5eCVknvXt9xzy2qLiAFrFT43yhDtzkuBJOWcwNNxYXiC5FXjWbxx8RN4LZvnc7F35AuLd+z6YPckP6lgGPTYrLXr2uh+eKgQ+U6BRJiJItZyIx7O/nKFJul10/dSpHHUt4jOIZnsUliZQP5MuOKvwp4RIwaicxgOF+yYQDPgYuC+Za4hjmT5QkwJbv1AwAeT7J9bqCRGdhECHRtCZP0VhwmdwUD5mhdSsa3JdjJSG9JNtaQtkrUIUCLsCtFdwUGov2FWeKr5v3q+c5A67MKU7PkJ7anszRWL14kZNdpkP1z4qUtSTYlv4CsOV5YCPCmv5+liYaNxqeSFnEQHDuKXC9cDMQoOvT38awQpeAbiQyThqGL8JB2J4WfhGW619qaXqFvP3xw0kgNGs8cBtD+Pyol5Xmn7HbOVj3xohS8NqajvJmjk4toyqK609uqN3dXDL1lyk9on676zVT8cgxakRyyClw9hMoyniUAdzhE3A2Mqf2YPovuwrWr40F6iHjKlLVlayuvGNodHh2pfj7O3BO4KH2CsD7VSMHqyDYihAu/SFWUDu7bHngjhJ92KkwpPjdoFPy2PF8RivppJO5g4e/UOTr97Lu3UJCv4gP269J9ExtxX5p7g+i8PbK8s8bbha0huFy44N/s34lDmQ5MrDB+DCslXlLlPCr46xA0hsC7kRuCSJQxiswDl6orgn5vK3a5kX1rnq0YXnMHmBL2kxTLBmB+84hraoB+TAm4i/95hW6YCNw3D6/QSpAWvRqML+vaufi6xjBkPzcSPEZU0iFxrLRaHYFfFeIcopq5gc1uAxHJF62ue6q7Q5xAnZKwchEbxHvBbR72cs5KlCLuBRDlIaLO4KxxH9uahvcbbHh6R4F6SqmopooN1UkgSFCUbzeGpIif5tq2wKsWctowwI2Q/AoQzyTF3FXFKV1LJJoUncN0kCMQvMiMnG9TD27IlDcwl8ZO7jJCzTB5A2uAJh4SNCmTIJsh3TyRtvbI7VNOpWCRbsigXGhrOB6frQHZbbmvaN7Eg4vwJD8j4VXzYM3GlIBXwDjUEIntO45szdm6O8XtLvukk7595iipPhQfjdguUqWCzRRn32iJLDcZeM8PXkDnz1ZbgO2DgoPP6k127mlPr7e0lDLlaehYfhsOzZaZgwM9ms91FZywrz9kWavQsCgxkV4bMwMMTXCYP7NoU5OCOBwIOLbcGra/rSYRfpI9EU1xYq2wfDIOjd4YCb0cdnRxDJyOpNnAbhK17pnwbpU7A26QnBsfIoggZsEe7LhY0kdU3/I7sZpv8qvCaQGybBgFk6XZVh1CitVtD+7ZpUUiqNZa22VF/yk9icLEOFM9DUBusXliv06i7rcq6rehgNjcliK/+WpTNtIDJwWLQD5MxgIsnxwnvBAaOXldreQqEqUa4Rbp3Ya1IlwIH4hsssbKVAFWWdAaL7tpeHJpa2Yh7lDi8vVjf0W6m4KQHzVDiF2tBP9O5TAEn1HRO+6kSNzS6iZF7+ApoBGyJDhF9bkwJ9hutCjLOKAUzOLswos9znEkSE9ug2hnzX1Asx2jWnvHHZ5K4BCYyWaDa6PEQx5xiN9EYs9C8wfZ51BjG6exqS39usFSUxzibae7pjFCzWcQ5+YZ3gdXOIvBfaWPe2LfKzXgjRExQVRl5M/x70FqnmLnQcZ3xlWn2gF8cEfoxSbWh1yC+BKlYCRcqENurg2rVZc5p7tfl1fUKS7B3swG0DixcIXVkRmzCZzBSU4FdCNLmZS46GwSxbstSoiFeyesRhn+qB2npa0dqyVrOtpFJgDezXZfLgdiUE5TVF44T7ba/KLvuOQo4C4EbJ9uLG+DsUDMO6Po41cWWS1JqCdWiHuchtSFs+1iBlahTIg559OvDgOCY1FLiHlE/yZyN7qb13/DovlZwrsYjSyQCbRihqrGb7t236J1ES6miQrm86ea2sRFBPpm74uSaYa6g73dFp8SHycve2FoYJvnPCn2ePMXjWG6Bg4QTohtfp0TtXxOU1B0jMm1wghSFvvhcWLgd4cUU98qmBNsH4j4ecizoeMnR53cFH+9qtbCoNAHv8Dbct6b/Yfxs5GLD1vaChac4d2dhpUbvsoWBmQ3VSt6+tXhRIaS0qYDOMREsFP6JRDUt+RO31zi8bmtR0ju0y3essqqLmyxN2apao1RiqXJJQ1NjkaXQMbn0Ou2Y5OmEk+FbQYufO9oYDHfypH9Uyexnnfc8i12cKLUAzklKDeFuuwKsLt5ZXVXe4WDw1rIVR+iKo125bCixFIwvdJzddc6S+4NEHmzXks1/u2NemmE/508zNFjk7ZbrOKaOOdVGZO3SWOl1RRItc/FRsG9KqgpRddPifzHWkSPkwi1mI1Ge3TMLxF5RPskeqi0qtddUBhjPQ3lVuwA8kAqLvCmNZGVt8mzqQNnHxDf07UDlEWvfpb+wcH0YhOKXtQ+/fZLNrKFqzKC/yOrwns1FH5kZ+zpF1+V3VUnVWJd8s98+bB7EFGbMBBaENgl4+sHap23HtvBNAEprFb4JGjN61lOMebyuF18rsh874wO2idi+agzJrqbyytQUhVKtOY3FfpSAheXxiGbAH3eWB+cuWYiahlK087PsWzlscZR08EM/LrJIdzqU9XFCqQDhIhkd7PLx7HCHF0EXkxj8txlbQ9cHuxlms5Lv+iQW4+w+yf71X//VMyJglfGqFgtXcYs3HlpXgXzRbKDs4E+yY7k7rAmRTB9uqU3q5Tyv6wdTHcz0M8CixQ9+RAzaFS/LtkaROTaAkeMHr6GFdtI21wzjYvBYvOY4sYb8EBReg5ZJYzEdjrypmErAXO3RT+SXcYx1WFMrpkM51AJwR6Owfipy0Ip2sigboFxW2PQiXrftppk8fXpVttfbSwyBebpBg1TbPCVB8ukGJKKn33797dd+dDIXZkb+TRFjt6inUCjRDSm4GMnTsotZDOQj0vK36yCYiQ3iYzittDg4D/kI/zY8AgPdyFJAsJYukCpozDFGaM/sHjbpvjAcQ7kUHs17gI5/evXu7P0owmb4Qr4ZnRvHQ7ue8NlqfLIOgcCO+bBUGlDGR4+Lxt0cyDpj73LAPtWyecrkr+CgiXCYjsGkGas/2ZQ3251u+hVva/nVv8ItJAHHrBEHHvQieyR/gdLkYDjyNqcXBwJTgztZ84l8If4Fkkqo7pNFwvACJaVUjdhrQW64NyYK9oTAfqF5CcU/OI10DtGI0dMZVbZotosn9dNfuoszG3cvuUiA6pA0qFRO7Ppd63Byep6MwgeA8Zg3nwUgyFYjOLE3VgC46bAOvOgaRQbc8toDMuLaXHJSMTBYfvUf0DsNDyU3nh40HO3EbGDPwwQXPUCCVEVmRnsIPQt6iSqd6Sqm25roqrHn+QY5u7Xokq8FNNrjRVGXqEbaW5SutdmMDsqMC6jjCSpyL8qYHHJwDQRe0KFfIGWChY0nM2KtswzN6+W6YDDysvUCV9Gw36huIxcSZwUuizrUkObXsLZrPde3xRI47d5rkK/Rp9/+/utvA7jpUAZ122MCHGhGAVC88FWEhpdfA6qxYNxJBG7mu0gt9EswpKSMGtW+C4nLGhBOFEf0IkimMNnL7dV0euDZ5QBgOhq/i85rR9VPZR1Mp28s+0fU5JE1F3wKWv5MOEeMPQwU2e9ohhmbe/l39WenIwIBfaJuPicgsxPRLIaD4IzVffLFc0SZ+JODHXc65efsiD+/3oJ8N0WoH+Sz/N1Rlp5sP7YQsWJsxh1YdruWrGNEsZm3e9H+3LTowWbUCKe7XJeLYkrGhL1Zz6Btv1F1z0yUQwf/YCctG28w4Dln6yPczf79xWwXzfhWYz9qRA4flGhlxro7tsIqGg7hlr4pio0V/22PawW7CEcNrTd8LS7gLJernVgvJKGUS+cRpPlwpLnKsU5Cte8BZ0dkG+aF+qrm/DIV4oBli8gNx5gXV8Ua43G0oRUvmOMl4URki2q+tRNU4dgYqWsKTJFFWiQ5D72cDBSgcFnXHqa6lqp6+/MfzzO+oeA+2gtI9W0ESeWD7FjPmGA3mqtR0HlMPgcQ4RfFlcsOfvLG0Mcj0p8oadH/OjhM5sng4+AllCrNkyR2+l/74qd50P80eAV7Mg8SJ/WIaxg8bY65ecN6d8KpAwN1c48y6/pGIDLPmL+Dx2SfzFOaWWjBKBwl343mLf7LPfJZW07Qoy8FW+kQAc1FVUeX/T8y/5sydON0Cr/h5kynn4VXEzv8k1j6/pxfQjOg0Ie8LqkGcHRhp3Vm1GU181PWVG6al6amgHmBeW1AH3iw40lcJB0LoaCiKOYBX8zMiyqw2/eJ2xAtbb9l7NrGofXj0bT5J5K+ZmQfPypgP1QWpYjb+VE6nrTUFcTpTdPIM6oMz86oWf1wL9TAPUUF42PNSGiFTux4ToIiDstsZxWgVOtUZyCux7zUKgrIYc/SLf3dSdb3C8TI9LyZJXGdYVZyBHenXKLuhBe7Kc2GtoYLXd3QXfcm4nkPovTjehYbBjYZ9bbL4BMiSn9Rr+lJ7uzWu7klb1RFWwVBHtBDWTjlUy5fc+1i8sF2M7VXHkWUjFwE0qqC0TpTYzKh67kPhhHXcKf5ni/DcD4VHcj1Vak2LkOhhtXYVZicecnl+ewDpFRec0rPoCCUnFEwPR8gnBEKspTww6vQY3hAV8+6u8J4CwRVQZ3X5j3aAFyEdMBsG/JDhwiRXC6YXFecWorFjCkG9R7fpTAzXvtHgXNS3h+KvxRvY62gtzmmUREpfDEONrQI+zSbheFGIYPWkEM+MQ6UJQ3bkoo4Hguz4nCUWDpVk5le541rLYDZMgPQIoNfYY9K/2oAbY7mTDdDPwdxnL0S0rskAExHAfmK5ABzJfEfk2zKxpE39GfipkrwI7TiwNXIosFujsQeRa7zqAs8Mt8JBQoBI7Wm13fkWrjoFAicF5cXaUOhOa7OKs9RUdr5Un0OmsvC5FPja3AaJeAO8zXJqEVouCM/YlJF2bu8JYGhZY0idqEHYQf5hlIQvCmoCFTPDzS2mBwq4LCAHlFU86e2rtwoKTlwROkNRl+1+qKKK+XCypr94u0LPZR3eetfCNpex/Grbs60oGqZxBgo28BJ7GwBBAVrHB1RpBKCGVUHk9TQoZ9Z7+0DVdctESEyXzdGnYp2wwSsrmDmMh6dTi5UryNSs1WxNHEGlw+xM0z8XScBYn1AtGOphPfj+auz0HhjpxAb64wtOWru+7MXpz/9+L6XMAlFyrvPP1gPmXrciP6Y4iJN+esYwQha9d48CN5NvxGn0ZuFC+d29urn87evX708e/X+4FlpJnwIp48mFzegsf0RUe0xvF4vQtTy4Sshuzx9efqml0CAOzWBNUTeqh9rmiRHqANLPU73/0QFyI049MqG62iqgBav87tSG+vVCqbzoRTCY4cCkLwfSaFgQAZNRDZn6pFk4bWt5ecUnfsv6XO5f5O/bIPTl/hI3lMWN7THoJQnq5G6Yg8wXz5nlxxtjY1GNLcdAXc0bedtJLYrCv0Zmb+M4UQiKOVTjqpMi2OhTJU26VvnFcaGKUfXSXb8rBO8fnBIYYw0bCSJM1FaoltLPxcxNHBSuEr90NlZlPEYDJRTU2zGYAp9kmwZkrZA0fnXlOvLagRes6RqrBnyCm66LQayjBINKblHmrPOOJsEibqPNEyqQ0cz+k4dd3gcvssXLr8xnU076DPgho3CdKMY94fWSCHB+mS5iI38zAr92jTkXMb7XlyOoly4vAOEqKN8lbXFpCX+02XEoII1hiITHuO4vAMfuIMAVUPDDvnNhR/sttkQtmx8Sh47GtpWLX9jpeGuIdKDAzfQjyMn9zPgMgN2hYTvDRvOJ+NY4qj/pN3jv+mQDa/mMe8aUgA+zd9bH+I6DTr3GGKnaADU+jtR4izP5DwVcQaaxCc0UD4jCZezWfqjL2zkE8zvc3aPYFEHteSWo/s5gyw/4JhkZX3H109cEzvaMIe98wF1FHaVFP5ykk1IGt6VlDTFCmkJtR56Agy6kyZyBdvAchiwmFsGyJVrPzoLaOgLyr/ZKxaZe7kgCAtpBSakeMMxRco15uZmJ+ZIfY2pcEt5yMIN29QxBmP0dXC8qDA/uKEAZozYUSuGaUw6lzMblGPQbT3BwYhenLmJCeuBsOEjK4OCNnyc5rpHeJRtIcOpfmcQiLP2KjADGxrJCtkNFhE7oF35kExPCnBdp4J8qbj3RhKOnboceUkUJTwbO2OieoXjPMWoG8ihSsT7ZpylYRkM1cEkYLjSTkjZsyHLj8RG9ZpNQDaazAQg2vLWmSNJ6/i3b30r6Y2miqot3G5sBWIJNsSphithQYvQQOCSlwzasikb5VsjlCN35Kf/oKWm9A6Cyuwg2Q49+mwQL67y+YNVudBZJUYNiYqS4AOdftYLspW902wNPo8172DYDh7oaNtZ2ELAQWi74DjgzPTGS3GJEup6sQrzQp5QJn7jEvPNa3Apwv5sinmZr0CYLvKWLGcqdozCAmGlVvmDsrs9CWOqHhNkbKKrvv6HfzqUOXyZhmJEEKWixOpJpAhrlY8k4K67yQsDUpYddXekTMostYSna5AQuFMVEvYC/j/JfmRaxi2fSECJPM1e5UW2gGHPdWSgLQCAUorFqAjahYdfVaHSsWfj9Ar7egK3+a7I6/m1nxXUMBwpj1ykp9JLb/eHQNbC6U3xgDGZn24mbETCJrGiAYkDtprBGGSC22YwpHHbMX/uRVUHdO/CvTzekmN8LOh70Wpw4LsZUfdqeNspFWqcpBGhTXub6pLVYTabqs7rEjYT1MJys11JDXCGYAw2ON5RjK7wlzpvEtv8JHjTP/SPPPjPvv2Hx77KiQnPvv3HA4wNTzKC81ozvh0xLhUCS8vStJJ0RnlvqNiX69bmaSdaVPj83pJXi3JZhsD6u8H1ucGfNosA4l0fABiZXIuyN9RcvC0a6VwIXLCiHAkOO0bwomCfHjCjJaKXp4fhXbz7LYk7CDo9ircFAVhQPztOvBrVoSvQqcx82qlz3UwSXoEoOBKZCw7RLfPO559kr9dUi5knS7zKQlAIvIgK1GSQWaqU2RR7Gva4kqFHwXbct2iKbd34fNJiPfOfH24ujOmws6HP+zTD2AioYbw9w3N34E4qWkJ7JQ1qx3Zt0Vq6MEC4ELdD2Eykm6oCpcncXIWMMstX9/mDc+sRPhejTdhG/mj43KKaN+MN4USNq/rq6bdPV+Ul8JSHp1Uzvm5vV0/gp4z7T7OprrhN6bzkYY7KCoZKphJKeXVnmYkHMxIB+tkEOsbDtyKBfxS01YVD4vBQ4xUP29Dvde4Sx2U3yu1LCjBrrT6D7hqTOVTYFGbaOy2a9mToO2+DVWKlxRRIt0JnMj97pBEJ1t0gLrv8vuMxHOEtqhVhAVwqDZzfy9tUbgChi9IOb9kKi62+kEzBhD5M9HlcNuMMI5M8xHjG58MczHJVCtJIo2JoUMCodal4q/9EBHTUeA6MR9sLbAUdyTxLKfZRZXTlGdOhoAFeoAWzdAfNOcH5lRSM5WQHvGS3VILXhTj1DNRjakZdY0oOx1yfNoAqTXCiOxu4vCQJJx2Mqbmm5+tGc4YZdFl+2RjsI4cTiXEQcCZbZox4GlMH1wvSV8lmSsNvKrbTGL5dZGuUD0g02g3wGfvdd14znfmNneYePKjJaRnHscE8TF0w+hKzaDzHJaUIkmu24fRawn9J4EuJDSi+xIh/fqR0BoxHM8xMri7mqdi6bw3UFwWsu67jbXyXlcu0IqPhI+2BDCWrq8R4IkAUIetjyO73dIdunZSD2wOMTZSCJWtKITHcUoTkV4Rc7a1DIg6VVMmRpGkxHXMVxlCpfTllIWUTtdRe19X2iovjvVE5Mb4D+h4LXaAFSuXoMTAqg3AnYr0cEbqUI4EbFzEG0ThcqBLXXMOY0e+2LV+nCk7MDdgWt1LGKokasm0NCKgULePz65GhHhivieJwuI02S/bR0VicPcnHdlUhpJ4txY7JT4UxIGLe/BY/UC/PZlO7QgJZDVKcFDaksqCYI1CS//SywCp3GGyKvk+UOOF5LyFXJmZA5FVkgUMJcOV4Rf+Q8vH3WIav52sVOB3gD0vMCB2OVe4UT8+L/BHVa3cYmX3TRmjgqxggEq5CKnTIvp3IweRSgpJX7X8bjikMNyURL99eIceWktKEU9wMQmtcwm44csEmUlREB2dSA8gRImvOW6xpvtB7I1Ti4AyRDDhr1Gh3zlfei4wTxss+p5hDSl6jMyTq9UIQTgXyQkD6fK2H2xLiWEl63LIg2J7jBpZpfm2w/kPFu9m5hWaRolX4vgSWnD/Q6XOwdgacB+U/LO3dgFoUWdkG6ToxKpU1gtO1TqLO2D/zmGzJH3dGiHlKbiwqGTS+k2yatkT0v3dzFkg2xsH/5LLBPqMT+RZzVX3+Oe5I/PzE9cik78+JtEzjLO51enmp45P9aWkaVvmkK0c1/Yo3ypOpFDF305s22+Wy/DgItnR4SK20Yn5dDZr2AVR86WCULa9O+pi/hkALdU1AXhEtvtl5I+osUQ5W2RuF5JnFfM+i9iokQpLO6Acqgym5/MASS563ISFSn3clK4xU1VFTb4rSG3TwUSBrumaVwE/XVEtgY7bsrsJWJ2wKrnGKqnqbQGX2DNUMwiL+K4NhF6oV1vEZIL6Em8JnX913p8IY3UXOsVgNSjv5yjOKggwnw6W8NiPlhzViByne2QzH2Tv0yX4ZBgGMa8ra3Ul4DxtfhktJA76v78jw9o2EacP1gge9LD53dR/Y3MlJZ3sIMjG9x2i46aK6X1/V+cIbsM+kVO+9DoZ9crI3ctu80SlMRE9aG0T3o0ONoHNfrtcE9RAM325dz8cx9vcp/HKgF90MZ+fCDTui1O3Q0p5VL+G0121d/2Cv9QuTFpN8ukMa81/fs64UYreDziLHZ0Xl9KyI88/W4E7yNMyeAJqJtx6ZdIXHSzPBiHSaBEfXpVcLgwk2lXHQgZpCyIIkhV8+cM6G9qLHmidwFNTrjGPLzkNDIcLg8lvGSf+XotgEEj35DRsuCCDLWZMk6tBfvJsK+TNSWIOhAbeqMQ70b6TUoijONraiK8Ca4HK8xEqqjsZD6DLGqGznpA0maerBtlmW35QoQz6mnAu3pwsKYWukE0zRTZhoSwaVzKHGVE6Dr8y+GSOraxBt2jsszFwGVSm9Oo6CKV3ILWjyMLeCl8lv7s3t+32Q3UdZgqB3CnC028aUxosrMCVr4olDJKAKTF0y4EW4tP3sadYf/3tVrgfL/tGnj5+P+i5yz7Y//H+2rOK5Li2fAPe1kc42YjRbbGsOFS85bUrA290S22LSI+21ab0qIxLGJZjPaSCwoNqfGozt8m+qeqO5njpQjnYk65vSjTte105qXVoxxvvhxtibYr0L8vGHry9SzgWy+3957cl4IgEpD+Yc7zqXvGN8KqztLM6BMUY0Dfqp89AfmXcTNaSc5fSwZZPSSpzTOrCimCvE+NrUD6LCC5Qt4SrUiEHaFrJQqYOm9qIFhGQV0lgnrC/dVJLgawmfFqI7BXUhdImz792rwGE8CgqgWGOm+YUVqvupBZJ8Z6qGRMUjvCJHJSZZOIuLy+/m89PYyo+Eya4SIBHDewUXQpgXZPL99WhmXH+F4aolVrJUvtQ8KIZADAkbkGUU50O5lhroGCHSBJUfnAyvEEzHFvsDXmzLOUg6GFqjQIYWD3AhlHMD/TH2cMMlQFmWSoDlGE7Oz+A1kHDXJTAPr9iE5zO2iHHjaN8MkOVzVTGpyyVlC0IpOEvcnCooBcNqhXd1u/m9Igfr6mG0A+Pb7rs3XkafIpwFUpIrrz5FzvVXNOy3wXjw8JuswszNCRw6OwdSTYuwCI8qOym3oCzIpObO803ZSiUxH1dMnrJ4Yc41JYPwCzlV62UpMFVTM+s39JMcXghnlLXlLcW3y8PePG3lKgnc1VMdZ89ZuJGJ5o7KDVmrG90sQuWKaAhADe1rYn+mDvtYr6YQ4Mh5JGQVo6Vw+8EuDwtyaIp/5xKnSxIkmTrwZHhWbCqUpS0fMkQCUKK2J5YbKBbhUEFM/7u2Xw6djT/AWbmDr846clasLWtcUFpwYPzfCT7HeJCOReLmYFKcmWFQdqqjthBqxIi3xphtXmcesjCJzaLaeIXeHKsmZGKioqPOmK5ijcd2odCOLF0ZvhVWb+tqSu2xkCTKX8TfgEjhPr3mAg31DUFje5zTlK6y6MCq5pShwaD0lK5Ak8/RzthVH8Y6ivHMsexjdEU97/K2BDbP5C51PdLtoWwPlyQezqq+4XwoqWjXGKW9fnB1zdKtdBSBq7ZrqmNn9o/rHcA22It3zoUsDCbDVVAniaqHcGwA7hxwaW81ZcPDEl5VwoXSZVcnccKy4S2XgWN9Xl8K+9pyMU4cHYgQhnWxLD+yhwAzL00xsH1NecHUhlfAdTZxF5vU8gy4CTCACQcO65NLk6G3gBHBkfe3COUVW5LZCHdKYrMlTxRyrAsk2lkLROuzoAkcX+YNyBizmTv5yORQXcoGnOEE00KuPJsBKfAvy1WV06+q3Ndshod5NhtqSdF4BbZmCFoykoI6Ttd3EsCburgrq22DMsCMFLsTVgx+evXm7evnZ+/enX2P4La5KqRDZclA+qo5X+b2wBJCMxPHNmPWRtb3YDmIECVicSvoIf4jqpSAHTqfXmSbWw9xRCwZKdlm55gdqtJfjCiMNg6LoInKJ3JTmAQGHuil5sAAn85nsz8zLcIEF8ircie27xvAe1oAT45WIEdUnbYGim9UZ0nhe/aI/sylRpUMusVfG/5Ecmfgegnqdnpib+8wRCsmRy5bxHeoYU0B0UDXUqqOoJ/pmHGxU69+BByznUfMnqv99VR4Ncz3IhQGBT2SgqMqmeFenSaKoGjhSH9IPI4+4EogtBBYqbSJQBmtOGM/0TIH2WV6Zm26ANy/oCaKTJ5uPttz6h5z0yLmHiyfr8gqRPzwmfl1VVKNeNuep0u5tforlifZM9jo+Zgc4qoGe+gnfiFJTFExhZCw4nY8uumaQURKcT2KfdUhFI3EX3YRTDSfX1MZJiS8+AF14OIxxuQXjS4ixbiVQwo/4L+vvhJZhYPxugpCADOkaXlSFok/JyYCZ0yX16KaD/BzZWAz9eHjUxKcjREt+Ak7y20lMgucq+Z0oqPrZAopT+CT7M01io3PJkLHx3XBHil8pQSpHKu4zmZTQtRntncDdzKwf7Tq4/1iIhrgZSXsPyEVwfvWHhET1IYxf1Tjc7s2OIRqOfklvIVwbyepxDrrTuvCWrdwLgMEjr8tjnS6MktCR9wPhYEcjfuBuVdGTqLIietvLOlYg/60P8r6WX84doYPL7/UmwiR1mRHB15GTbHnYVHJex56Uhp9Hi6NAs9AIriGvhqosJoUBS/7n/C3z9knevxz39A7A6TRh0FdDZn1iR50UFgj5rQYuxd/mmp4qup2BJ/4jzvGC0+6P6KH0IxxIpwnPh/fTIj1HiPtc3UJhvzzWhEOLnVgIhnBeh+Cs+Qfd9uIY/W7Du63E6mjbceUvRbh1BVwkUBY8Ri5gki2VpxqVkxNykptbC9sYaTrYxzXonGFbkCN0QVn3DxG/PLIW63EnH4/yZyfHqPoA8GTOQu1hayE8VubsRdJS10k3tIAoKQXrEDNxEgf0B9s+r9XTUAn4xvURmuP2wp2JToYUCVkxSw71QVTVFaYhLZrz/4q/8+SdRfZnqmJf4dvHpwVX0F70bTyeYuRyHhKQhyIJ+TsN6UJbL5QI/CMIIRLcBBx8RUqZg/QfnOPbpA+InF6lWS8qIGy+V/9SfYE45Ayou2sNIW7baAF40hg4RiuEmYcM/RCDEZAYVMLCqKYGxMxjGvLCJDGlFkXBm8BW8BLcbWCnehTuRddIsbEjfyvPlYXmN9Yi4UlV0cWyNriYybcCciL+NFWmWHKJX/g8g0U1HEi4T2o3fN1wCRjcQseij8MmBXzZPzhf6HFYar44v5MPCgSlHlQ/kw8aOvNqL/iQ/v3EwtnN87e4u1LzrtyhVgaq+zmPq/JF3xZrvPA3cN8w7w9YHaupZlhF3w98BCU3YiRONz6CIvH2rZ3MDcO/LaadoBB9r2cZdKD5RgKA/OOJqnO/0w+723Lpje0qzYNVzIS74cTQuty2ZK7CYTAS/aX2rpIukkJ2Lb+R2vWjtJolLNWF9YL0+CYN7P8/h3M2crwnWud5Dpu0WPom58lbrZwUQcCwdKojTAmDWnZmisDZDuVGEXcUUd3arYo4Cjae0XciR2TugijdSNrIBhVhZxL3PuZNJdwr14V2UBYtYflPBuaQo5uM7DU8q173ssHgOeBJhJjR/a35yqyFxHOLebYmP4G7M+rXDPeGVChWOJePCb7kqEvkgE90VVToWKwzh7QKQJJHEFCFx512FFUWBg+PaJ3wsgeohm6+fjMzmzRTdlZQnwAPScU2rz4NVdTZuCLaeqF4cwkRqWbk2p6QW4eoTIn+RKX6LFFlsiEiwd5ka8sGjrdw3kY5fzezW7bcL1ncovNUTa8NzXjKgb6camTWHanrW7gfEqJDhyGcn6JjbdmpxxWBucxk+eYKsKTJqj9JMhb/aLxWl4/sX5RGCEwHcl6XFSVFP8Q3GziEIWJHrymhEe9jCjLInARuoZLHtHbYvmI2Oqv//D75CFJyPBOp/OinK1SGqokQeitYXqpsNXzW4e2xCRwWVBQn6J/dgAXbZQI45/jHdi56jyP9AQj6JtWRsFgQMS3iQatS9bFi+SiicUgOMpQ2jUSNrvsGEr3utjiRpa1o47eaGTFZJBzUGz7y9aoe/gde05PWmrpXJon2ZmJfbds68QWPWRhhVRSKsNIDTw1sh/DhrDXM/T82Zwuerm5zjcuddpg9HsJPR7xd9CXBeAKpeEoigyZs2Gew8R6cZEkq0J22IvJKwACpWeVS9pHk3bPDqOrMu6lDZt++m2JmEGUaU8V6LSX8j3JbEG8V5BLmtbEbQqmJOqJxiouGYfzEAbHW6+NdelXtfHaOGeUhxfCxRMsbAgHzWGOuef2tK4uo+hZkZIVIXPkBhxNkdeFq76Z8Bo5esh1vrgd4rBTTjF7flhJJsqLjNS2NG4o7fX5un2LDrHBbbk++dpPlsDuDKUc2CMCxFu9gkNx/LOTe049z/KZEMYIAtSJ8Nqmgs0Md0zMk+51zZIruPQaNH8/pohwKIkNe51pmGq80pVRON6RS1ypHKksm47pYmht9HzMljxxdBiy1vfX3m1K9isqg4h2HIS/ZvuNOD8pTIBSDkq0XpHn0jsj3ChtxDh7V6JgEm423IxXMN3boj4WuLaFOEulVD3XSAfhK2hVyp6QWI8S3K6AAxNq8MDV18fdRKHCDEIOLXul2HHKINWFgxurgaebDVzZ+K6qI8wmAI6RkhaPKU3TCLG2hXfXOVcjbu8xvV84pyojMMsGZOYSXWsDfIU1MYOXqmtO2mYHFsHTKEjypqTbY3TVXVncN0b2ZlU9x1hWDB9kPyxZ9ZyrCJ77Chq9A+mZQxQmzFUR2zce9xHq+s0GwYEvy1XZPoC8uoU2FebPV8AanaVOFF8dSyjFOhHmxRrENV9hUxy9MRh6sVjxiyP6y4YgklspYE5fOa1aDyehxLrmvdgFWva+0fV7OvDN2AJcmHMf5coab4gFnKvvdAgwK8h2MCSMrriy3PgwuGFnwUty9ghi1BPvPazPQOg/tDSC0qKtDSvdy26ZOTlF0tPDGfqt635D/ogFXGcddmZy3uWCdRea5hVVBE3avSISwJ6PeUy2fcmB9iQD2PGb0GLM7aUr/o47r+fkZPaA0TmbpNTpisIBRh1+5ISHGO0Q5JQ0kqJ1Ts6yp/ZDVx169n+bu7bmto0s/c5fwYEeBEwoWnJmtybKclIa25lKVSynEs9ubbFYFCSBMtY0qSVIKVqN/vv0uXT36e4DEHKc2nlJLAndaPTl9Ll+3z73qSfKLTF9cG3unRE7CqFO+YFrHFwk14OkAFfFzQqOPMJKGL0NIdJB1okpKa+FfovoJeMuaK4WhnCaWj1KiTtSOzYJVXZ24VtdOKWTZTMYgdbxACHcVuaIGBY4iRITav7RiUuyopRUzgi26bbhK9QT5Q9fl5WZvP8XZ8rKVC/chjiCWKBqX6sPQ8RB3e9SL8ae4XGw/JBD577Pw3So+4apKOTdMP/6iLCb6PU0OtvLoXx7NFGp8HvuW7gTnzrwmezce6o0/y2p0vRBTIpsRnJ4gaxwmOStBCNzp0EkyHTMtkarRzLaclOOQqe68qQXsCk6jmleba/GBTmRVmtnxArkdkqrFTxY9wzC62IIQb+UfcpRhSE6SgGZ2eiBX5wvXCo3CmW4KhftFgCvIef4hNTa6aMqXC3P/0RnwG7Xzx05doSVAqHDiYsljpQck4mQDyMtP2ESZDyEm8jN1KT95g2b4PGaeLEQDReTMiYiU2OkgC3wFe5m8v+Zs1dAMXZWAUpTh9FFggPreGFl/oDNGsAwg9SXECIK0wliX5PzMAmuAxdjdKYUJAuU2yDOiMcJFAVZ5Kz5+vplE3iD7zus8/auuwAf5QjxUQKEKjT0MR5g7GaQDRh3QgOLIiAQ3SAshl3d+IBDhgiUxpqg+gqj6qzNz75Ikad1RxFKsMCMKXFXaXOopmbsFTWp7Lbz5RZXXpjwnd74stWA937xZfJ7iBCycDXXFVJviqwSv/w/m2YibOrW3tIAKNpb49bwCDSiZW1vkmFuBu6/B7WhlRTlVp2P4GHLVXo1hZidbVL7a/0anPC9FDA9N7tLsx22GInXyh7ErCcSzNxWi/pXmKeV0cAx8hQUoQ7B7x6NRUg+jgeEo2sJ2Zsz5y+IiCAxZEVW0aR0+W96lVAk76hAAlwTolKjlPzcPqsJnTXc68aWunjnk+jWuaEcC4trJiffqQ1Gkqb36ShKCGJ8k7qxlYfWc+b2XrSO+doYPYDcQBkUCK+7+yQuvQOu9cN00/N37wksCryL2xrtEDhSCMX0OcwlJy9PxqoVjtKUOaWfTy7dbZt+PgoJT2+45URXAS2ni5WJTCBnP1gQ427YheFXkEA6BNziISZkHz6K1k+HRZK9Qu1kQAhxbzAHOr7BW4sGRsMAxMjXKcQ4Mg71RUdaRUSTCSDA+BdLiy78G5eqBO5nM4qmtvgn/PxAqtzwDSCr6VtSStSrpdG0aiiEgjKyTV7oUHDw6k5qYM22mefZOTQ8RGzBp0NXNbi9r6+qTPDJfaomCMGZouzxR8NAexByEPCukRuQU5Z9m9lvnJ6ezMBIz15kdLWbf2jfKdrznOlfiinlI1srTsMj1N/cdzEanhTtrSHiiYnpG3Ct3+b6oxCygadO2wnzgh0wRtrQ63xOwzCbKMf2RdHaAbJyczN6Vn3UfSv9Y7zcM2x6rH3c4U63A4g6754UWNF9r0m8PZ21qvN8XynrMLPOYo51P8LaA26mcBiZuyXb3xFlp6Jti4q7ucZeLOAyHnc3ducG3jyB/7QvrgJbqR6cXjsJX6W/S64hjWigiA+XzBK+KqpECIfRrDfb/GP1MFmWny6vy6G5hY6AavLX6fGsKOAyJXUe9DLnvMPtMVDkSNi56WJ6MvMVF0dQcTE3gmm5vgdZqBWGYI3GPpEpRXXbRKhlJ93geBbWKiOw31F0iDodbIOeez17hY4PjEtB/sAngMXBcXs3JWm8sM7lhsNteAwacw46oGDxEdywEVVQGhLH28/HFPq7OXt/5rmDOHDX0iXG0vFbvZUxzOn2KtqgcIevjR36sN5hCjynwS3BsCpXAgSBTKrv4OG2fmQfaM4eHh3xzfldx6z627NtTnnbEKagunGcOhSkndrQBP5wOpwTqNJP+KMCcZdUrXU4XEuy/82NTEIjU9iOFS+ub4Z/zDoKmvyjmO6SDQa9fLSu2SJ7pH8/zdFlkQ36hdh6pX9xNhTMNm1CfZOuL/+HWaGhm4lDwQPVtNlOBFIkDXVC/xuRk2Xi3EzdN8JzB9N6AUyUb9efjoavI60EnzRowZUw/URVlfqH6tfeF/703/2DDzpCpy4+SjFozrvb1DcftvydR3gSWjrGuQQU5jxwxKPHEr0SLn8BTM/U0m7rVmac66HUcDX7+nXDJVYKGvcvbfuy7lvSfcu5ZykTAm1Ncn9B5FIrrLj4eT+5Ubl6mBPYC3CMEjjQRJIYO/MZuSPwpOeCkSSwllM3Ac7BerVcX4FLOn1XBC54N8KH8E9o5AKiJ68qvTZFUhQNThXofe3zQuJmJQKbJjooVxSNmB0T5I2AbNk5UHwAExCzrxZPmQqhaDlhuBtj0MspdluymKX3WaeadOf0+bi7sJnkL+dqMF8ZBhUhYtgEjcgPuK2Kvw09PQTDj4SooUvDDD0jX02TISAtPHgaQdW7tvYTFB0vcx6gR3D8eN+C+WkCWj2hwGJnU/fKWRHrTHJgLlWo98DMOBxkxCP/S4zFemx5FLZ/OQp47wa8ns94K3dHzWZRX5zn86yPsB3atrMiVNBFI603wjrIvg0mnR7vrgAfTh9dD0+zqBB8kYV/TdTbHN5oj358usnpA3DABQ1qc1eMqM4xFba0g9tkLSY5kioMWEJvwkmlOdafAtLgp0GX/zOaTlcd6X2aiYDyjlLdMadSmHRUieqqCXN0Ih4881p5MLAkKTZu6m7K9AbXXRHuy83e0N/7NH/0kAm7W/Q2P2Uds9PuugyZ/NpJ/IIlFkIEEPi51XPcLUo3eI/n5s35NUHuYpgtIeU7GL4XzMBUCaVwbEAG7N26vuYc5BLDl6IT4NrwZHEuljwevkWdEcDj2FsCXZnhVljXVgcZeAd0REnCbRnO9aZahemSfnATnVLEl1Jof00u54QHxSL7B9GrXvEuTEFbRt21jcL9bqACBTkdSfsjCiMPA9MiB0TkW9+xCu+kbNi2cfWRRINPDMYWL3tUDB6PIbTlkeig16dF/cYNwxlTvgdYLNo7BEGUh/sEawHMzUGVB8QhVrSl9usvPVVsA/o7g2yCDMsfkzl6KrJopsOFDQY6GuYQPhqRgCiKHi/1Ok8sToKei+cMAiKf4zcQ/uwxgKBtKvzpVYxeVDc2BSt8ZZ/3GJ0rZ7TnIlM+5zd4aQ4QLToMzWNOCKKDW1gJPDUvqPROtxYPHIih+bur2rH3ZwB33UYnlH62d8urt4s3FSgGJrc9KkOa7j89nqU39PRk1ndWnV0UhTHt34Jh9NnEzzojmBsNr4ouA+inx8sOs+ywh/BL2uH5Cs9UGmaVbdI+Y4NAHB9qMuhVCpWgL8zPEeMWi8/+WjbRaT8YNh/rW661Y48+455Dg5T/zy5jLlBM3GFCVItPRr6Y+T6Of13+ahXUVvGK77TXkms5t3Ctc/x7rlAm2Hat08qGEVzf9tn08tULIhJLCDal+ykxQfBxb1BwpbaRvN1k82lhkwU9hhDCclttVpQltFgi+D9V7WMxmUO2YG7zxW65jFJ5BNEjooHQb6/XVcMRJyPHxsPhD1uCauXE7QTXHWqgltS8vnYs4aZtY/EOVkFGF79WABK5vCDKwdAzliDmstm25ZAPAj7U5RVSHAWJkJ5cyDRFamOb0reF3K2rj3am7phHZCFdogdQ0VD5lpbaliECJNRFpP9EumYRlmsGhUqwdjvEU25qKDxHoPXpw4vVLEAoSyreNbGbVPgvgOYO76fV2r3W6Yb3FSGh+7zJ7VrS2nL9YFzVvarGLSIN1ScmzMPj3Sow48wVBwKAiwPIv2AeQAwsDKFJDlmCATedlasKYYLjzz8povrKMq6GHUUtXhYEimRBkAkfSOK30HNfF1z67oYWFExY2ykpBq0bmvFra4BZYm3bEna3JR9Fk21Z36xw74n+o27tlNraeh41JaKZDm/WvOFBRNDW4hJIucHiGlOGPBA48TDf+NFRRbH5rhKS1B9GUn3B98Tza3YccBhO/xu2N5A3Pbw4n0GZjjEmkSONTuN2s9t+iJjkW+b052rR9EDeAODzF18ff318YBS1q10D6Z7zzcuXf/7m5M/HL//0Tf+r3uxchBawGe+a25bnNZe57Y7pWcqCH0IrAleLkI757FkL21UdwR6+r5ZLmXLooIRgfmWJ0mEjJKERA7XIgubrCOu9Go/x+vj0mZZo0N80k42ymWoXxvcl9ZCnsGZadYAtHeQaCjNDkI9P1By2p8LjQZm/byos2zQ3RHB5Rb26lEpXPC2JvetAoKpbsrWEwq5/+B2e9RZVtbgcgaueJlFl1Sg1bBlWbZLgrsUFEAnq5aQNJHOUZo3CX65E8k0by+yv0Wf+8Y/B/hgNlPCWzS5mz16nkuQxfQNNyRY3kqtVREGBoUel2XF0qvdACC2IjmDrSOAMbXQXhFVTA595s3a0Em1sSanfkniS7hkTAE/WokZIw2C0AWe8KBVivP2hhVCjK93SMpptv38AvltzP+xqMwg2n6/K5UMruj6lxeyauGL29BaRBk4vnuOnBaQqo/lg1WdQCAFgBqjQ6vVrGBCzqcXq5glVLzAP7vaCesiQm5WACnpiPx946O9u8ec/16sdxRYnw3Uz5kVC0vnEQ+w+vjWHLIgzasSWdPhxBqj7/tYLeE2iczliTGaCh7G2QePyCbQtF7GHxQc3PbDJkC/QzBF77K+7rdX/kdJkvUjKXpkCzONrrMyx+D9SD31CXMSNoCANQkEtaZLOLPJa1HqB8slnOADmR3iu1NJAqqDYQhl2zShdApPNTzAKpy2CNF2BsroGWXtbCbKsZpjX42rsfgpw1PjlNnXrImbtKj7n2JKVvffMGnMJa6p0KUSYbQENGVKAB+TcePuOUynQKgFi0/I83hNYKAX0yUPC9MWUels/+sD8RmPcXg5ADE1IGW5BzAy+lkAOMDsUKtiuygw09o4dZ3eatFijPTduS3Trj9FwBq4MIMfCWtlheQMsUQkgQ+5NIEZuIpYXgAJILT1hZZrVKdyhMFeprQ4be3LSljNYSlJBD41j1unUTDwbERsuQrMT46EGHTJjgho6ULOs+Eu/hUU1R8p3fYtowZXgQPmWcCsgT+dmBRR1UY+2JdzVjYPaKX1R8Hj4/Xp57eScrXCCQWJRHgIsx0AVGvanKFKDlUA9GI4+ssFCJRPxMR8hLEMyzstNubr6ME5vEkq2D0A3Wy+XGJwzycu6dghQAYTU2BxpSD9Fc2tzl+Rfc9vWWLc8zvuHJXpEj3gUljsY/gIudqsR+RNLJdqwLb3j4LJyAnYcOrwAEH8L6UW5wIv4wwRcFV+RXRmkyRZJtRd38JfhcfjB2GLuZSvxp2JUIBCvyjym8DPR2NKpFW+7BKGQ21+MRFtMxyf6jvpmZcTllNBAQIbrjLG2l0QZsb8CVSTY391l1iIH7W15C0DllBbnlIRZXKdFUH7+gV/wdMz6KDRWQrEz9mq9XApKowhbNOIbYHGuY7IGqKtGzrLU4mvZ+gWsDSCvAxCt4nI+uya4UmIElSoYsb43ErbYcttKt7KDmAs39YjuP3A3oR7VBmbQ8HKGbND7NQYb0SHmeK/zhzsB7c4o/e9AYtNWn1DLisD1S6+uBQfYUZgl9JbW8URQvzUTMLJ7er2xfrgYbyh1c9FEH8rrG63wmx3VUei1nt//ePa3+fmbN69/mf/n2Y9/f6M4fcVewo9qGCXLuqKabbnZBlvSVnE4ak1VSHSA0ao5EqngULBtRUQkSBN0m9gue3Qoxz/9/O7tT+9jLNMlb0A6P6j0hsq1PUdhhnQx7uECDEbfjfO0d/Sv3r19e3b++scfzt8Mgm115PRXQB63YlkeSoEmjbqx0B+YUx7VyHBbAben1SfiLeSOxIhB4RH0ANCa1ldXuw3Sfbj9kSqbblLHYa6H4sqz35NEElOo1+QREdu0LB8g2X+ALXy5VMAdee7/I5n712++P/v7j+/nb89+ShqVq4f8TjtneIbuQNRFuRIt1dXTgZpjp24bPODqS1EXSdPwwrEEfw7j88/df+/dPXZfNgBSae4QNEd3xELJJOXes2zByqggbFVZ8QdSRPTr6InYSLNUtl07Jk+hSflz/jLpWlNlJ7QEEOPn8hB0z4INRyDbOoxodyZbyybpKQf3yUC1hN/emEEIWjhqNY1pH8zmQdiHtSnMfcnwvcBuZMTBO3YkmB0K1L14KV6Vt4C7ItlzXE4Go+uB+tDYqC0CDc8hUMYDLi4getRg4p7cOgeDKB70OdgKx//+TT9TOYBk1DNfuvgN7MpH9kUAWGY9uJ2JqsEWsk1ICxKQqYWqaEe44QfWDbZCvnhcBrgKjHVu1tK71IK1TzgYWCtTogF2JAOCzTjjOsjc9cynA0BcHTUvkt+uG9ZYPScqg7kyawfxdD/YJFMGlbHRs0V1b/YeA/40BAdjfVzguQddHaM5tVG7VvW2itikiSy48kxuHjOUVdizZTA0wdi7Xt2XG3LjEMsIIx2BmFy0cw13EyIP+oH/GE2fpfXFBbSXnLL72U9tnWr2xXg1R2KzBOyVn0vc2JMs8cASZG0tRh2/H/2IgDS5a2yA2VfnuuJr4aIKHYh2O7QwK7yX3aHICmnZfXt0HnxAfDePUhcnzsrIJPheSRmCf+5uiaUd/XJbADE1yh/Ml5I4COk+MGcYI3DZZTbRtUVaySUDxijfAR6TDFAtwBmh2yBuDj6UAo7LGY3uLrZv6KODi9GkmdOZ1S+xwoW2SDfQpNvYxfBmvSW3nVkIstw/Vg/35vry3xFCPSoUn+Ec+d+bieJM8y9C5PmvxcAoecLauUYDelFHyeIxoflI/wZwy98CgxgiFe4D+0tJqz7szG00B79uaQEl/JCDfETncaMSwH4sA/ywynskw4N+8oIyw/Yo+hcel6hQQg8kJMLZQeAGmLihTGL4JwI83cTq0ibgQ7XfKYQtUn8jOcnFxSvMeXBQpa/N0r6vP+EvLN2FsWfXO/Kh1Zth/AEHZOc2/7uDq/wSeP1AfkNi3MrIgI3NMLTOIgIcBaw7ypZaN1LBXe+2SObFvTDPEWkg5fJIAEKYvQWs8TKD6n5DPsuAFrEauvVB7z3h8NyW9cZmfBFUKcckzBxMp+U/Lv9xNZtJWDiejTkPDQWX6XSMniEARvyQZ9OsYG12M65W1/zbmYKw2yaLYJhfGYXiD5kKY5yAmyej0vYClP5BVecsa4FfTl3Wdhzj8ThLTaXNvwj+F0+Njs31LDgYAAydzrovvjO39xojKzYfKZ5Gr7imvBaXFYwqMmGhRADLgL1DEC5gxUXTzvokAIoTKFYTlxTwEyHTcSvuTqoQfDbeizczwEVI8U20diqvKEtImxFoB21QLY9mFm62H55OGffmadwB0ELPTvzE8c7SkXBU0JapmVFa4aBaddeUN9X8tob80TaJ7/bdadz1VC1OL2a/H/wAaA394/DBrROPcuS7/BLAhVSntvcGXGSHekl/8XSYyBn3sKIpwOO/A+iOAL2wN00eQ8Uk4BMOBwPsdTL9zESCamZ0OMxicrQDxCdD76uhBBWygDfgZWYMIYtxBUU3UHHyimx2UX9jnx/DP8Kzmh2KJoiIHsH5Y4EChlo/re+o6OEVuHSG34yPkVQzPIXZ8NB3RrfsOAuzFF/zC8wU/xcNLEq03Jp7aVndVcvJyw5k6LkYeDoRb8GEeOZMyDZfZir+BjBpv/tEyHHTTJDEPrNMESS2Ca/qn1BLAwQUAAAACAAAACEA25MkldIUAACwVAAAEwAAAGNsaWNrL2RlY29yYXRvcnMucHntPGtz5DZy3+dXINxyaWaLy/Xd5Sqpycnxlux1tu5iq9ZKUqmNiuSQ4AwtvkKQkuY2+9/T3QAIgOSMRruSXZeKymtpSKDR6Hc3GpO1dcnCMOu7vuVhyPKyqduOxVVVd3GX15VYLNSzvBINTzr9sds3ebVlsWDdIkMoWV8lXV0XQgPpmzTueHjXxk3DWzloy7uO33d6iP4IUMKFHBEkdcv1+zftti951c28uqjLMq7S2TcVAp1580Nb983M858a3OrMi8u4jUveaeyDbVFvYrNDQD9M+rYFDMPEWbXvcjOMJ7sayJixLrj6z8vvw4t/+f7iz+9+/GG9YPDj0DMEELwSSHgiLV/QmEt2Dn8HhM7PwIWld+mtFov3+Di42jf83+N26b2HZ1fuoyt4FL6p9hdxUcSbgtNb/eFDEAQ+fIb314u3F+7MtxeezzZ1X6XnngPhfzTpEYNFyjPWxELo/S+ztb0CIA3sSEAOKvj3QbHGZ5fXPnt/vWKvvrFHX+JDSRXP8/41bm+ACiyB15s4uUGC3MVVh2LX1azlCc9vOet2nCkmMM0EhFBvfgFxxUlZ3gr4Q8lSoOFL0uIGKn4XovguX8IgsWaXAf722cuXN3f6ifyLMH4vUcSfloPiVCxbzojCcgUQXEAruaaa5SrIUmPhs8yhLGzkOFWvTqPnz3mZF3GLtFvjOuvIZlwE3O47VlfFnpYluioa1hV+IjhqNDyCDaWCLddx17XrSHE2gBnRKmDsapeD/AvWC571BUPh38WdAqiI0LRcAL3kUgLsDSyYAcMrLjqeMrGHX+WvyC5E/otZVsY3PCTCphxMSdzV7dKSxxD0nK9R24FvwDRQdrC8a9C0ugAFfAvWhS/GfPxwAu+v/QnnDet/AEWpwKprhuLyQHXgT9JyAINqNqArOXWXFwW7q9sbgiHmZQe5TWIDzqHjcYr8wxekopb8ZEZ+XE31Wa6WyvIqpTl5VfG2rEVnZC0jhGm+Wt0iZhRIDpHEbTlMPrChGs0JeIDtDkS8yG8kBdbrxSAeJ/mxYTRy2/A4Wxkxw59vbeVy3jginHT3U5FzISnhAemAwQHSKZTbX1pUWE2mKInFOXl1W9/wZeazeQmfmXZM0EdDBxpI0qwb9FIjcQe+ksxJSRjEsCZpCex5WiHAYghOQ6KrtudgntAw3OmpJDMbruQ3ZRgKsOlPy0tebngLI6QRG4QK4OfdmWAQ6OBz8Pl7PucbLA5/qQ3+cvtFGtTdgyjMGrDFYiQza3YF/vrHuuLOG9i7IvMRQZMjHhQ1Dgbrs+R1jBFOAQ1GZKfw2jgXnL3vwfuX/Pu21UZ1/ANBQxVvgdsgN1LqTfhwl3e7GmxVrGXAmwWRebbZQqH9aOEdhGEFIWEY/l37aX6+x/h9LjBKCaYDRtt+hI4uHqGgi3ntNCEFxLRxeMP3kojwxxr8bwvL+SytE/BcImlzCorphZIh4CiJ0pO7pwtS4qnJRlzJlAOGZJyloZARR1LkyU2g4w7cUUQhM+i4G/ChLOBTBZxLQ0FmHjYYOFaLSPFnWC2vWBQ5wKNZczUh13fmw8jabTi6RdpT6g/sBM/JW7RgeeXiWbdgnQA80B9lCQBncV9gtFQzD8edAbJnAxxNImYj7andBQG75S2mFXGa8nS9Zv8YfP03buvmzQ3u+gPQ4vpX0zMwXSMhmJqx8YBzsDLIwo+AKRgSw7zDsj2wSbEIDBFCDQGWMYWZ992sBn0cIfBpXlOMvfImSmMURo5aHTIyF2WKSaSbTaqHQ0qpUkgMml+w27jNIa9bgy8maw1GKIfsroMwDdA0NiGDf7GluN9CugFiXdRxSqYtkUCXaKDXzE5aSbbUmmvQhsBdFz0Da2qRI9y4YAgAI4rJC0AAWAWRZMoSCBA13dYA7Ft7dXjh6/UuCpAvWHKFs6ejAM75eOTBnRHN5faMWZa2BOCotELR+lo+f/kSRQrfYZ7vT423TSfQaj17hkxElrrMOxAHQpyFZS+6EOOweCCMJopvNvu5e7ScjtrMr7ZX4nzdKL5LG0xY+SSlFvNZ09a3OVjV4HFsg41R/YU2Y+90tI2HdyFXGXZxdOVRKWdC4Slp50bNElrhAeMfRharSJr40pAMYYBQgf46KcB4YWWBZkSkkD1as3m7BLZC7kDFe7oIQZkCpNU1i/uuLuMuxxFgWrouTnbwphhFBjrFJOZH/yVoYfVQM50eC9bo+qDQ9lIRfshJudKazH6LnnHw5d1ojN6Pj2HqrYwNuprAFfUdb5NYgIyQfIIlhaWxYCkgWgkhRGl5U8QJzNjsWRqLHb14FWH2pNJr0WdZfs8lrWCSQgmH4KdS/7XFWqmeSJ/hEwg9x5wKpDwN2FswaPw+LptCiUUU5VXeheAwYwMXrAP8TYjg21f4NtJJ+xug/thwCFoFjD1WmGQoj4jTZos96qEmI0mIDMfeksfjsAjxRBCu2hDF1d4SGDmAFkGfXqWGwAgBHmhWFBDHK0R/qhJb6GxGMXSBQsZv8VRssTQSw2y1ADjlJK7QZMpIJEU5ioctFTmoGlaP826PDgNNKwmqJESsinBysF6L6tqRG8lKpT8ggG40WdZpnu11zUZv60wM0YD0h+iKMYLYIM4bEAEnECbDcWXJOOGG4GWK7i45ptIkTk12cbVVkervB0SuLAH+LOG1ojsSYna345WuGmkK4HaPIvQ7C6HcjlAUZ0GsipynQ8YJFAI5BGXk4mS4s6KsFwAzSDycivQAwRLtebF2on+ybsfstjwlGCWCOv5N1DiKaqzqFUKFsfjUlaVztyaBQWrbkVdVobPPvH+DhP/MjiDg32qpl1qd4cbwRApEF/WHTIE3BonVHXJVY4BDwD+CqJy5MUaBZ7Zp8DN7xIcY6oJZ7pae4zm9wd2tDuRY0xhVTjXgsUwltAnBrEVDdBMlWR/BqbI44r2BIK1spO/QjsQ+S1HWSitrd5cnPPCs9AepFiq5Opc0DJq6WXryGewN6WDSMGeongjYOx9luY1ECOtG7MO1WbBr9+tRnVRRyqCRQdZDuZF6BOkPe8FkjTHfVrUl//w+4U3HgA5gsfpO0sVdAJOjxeE6VsqLxyxoiBDQIV66bDnqOE+X452sLDorEokAstylt+NF463m62A07IMccq2IQSmgA2z2jHFe0TQv5obgO1etnVVIiy0Qo7S9TEOl547uT2lsjcyG0lpAcc5yFahoZumFIG7eK281mVvwDPy74I2v/AKWARTMoAVqd5S6LWnyuOII08hBqJl5xT56SiFwPYCDv8iXyD8a79O0NGltQWNkVoIn+LwQRMNzPdYfFPEcVFoKxbn8NYT/KxuIle0bto+rG2U6mCqyvLP8GWfsSxx6uGT4Q9tMs3n1cMjmKf542lyeqH4ok6f1niuPNytTfk5ruTn8aAT6JnfUgd2clL0r2n5mRqtnP0X2LpEGqXr07h7O259tl0+Stx9h15Nl7URTJ2c/sOrpGbsmy8kZO+FwPF/XQwaSuzX7cbIuMxHJhdhOzcdpOYGRR7I1Hr/dYfxC+anKNVTCrSN89gvIqkyfKM2DXUeLweViCo7mTp0YzuZFzxDK6xD6kfGhpqWnyLt6MJDWFt6Nho2rsD30aKxyN/ZoJW8yGAlLyIRG9X6rL0h5p7VphSLJMXs8IUDNAhUXyZxkSZ+ke3MjAgwswGHtYoGIIjBvHH55o8B3JkA7h7ByEqUtDk9w0ZrOJGJpqyH186UkHUgLKh8dnRk11E1rYz180EK8vQByw/8GHXsjM3+sQE1Os4ZMnmoolm9zyigLHefKOoNRFkQ9bmWLn60yGvnon6gyptwCgTlUn+m1QrGlCrojUs9oFZjGDPiP/3ef38aF2gNVBaiDsJouPhRNGOyxx3KdxABcuayG4MR8oMXQgyRJotJmu3yjy0Oq8GYl1LiyD0qT8XaWDkNngX4zKEJEQYx1qiJk+4suCbr1GCkfOz5aWh5U42a7nKok2g4NtZJJwDlB0anDOJJ5aTg/JyCDKAECbZ+g2auzYT3FRQe6EmADd1q8mwXqAjvNZg7Nn/O589sLUqC3F2aeY9FIJZcWOYwBnLSwZEePzGU4cYLmyz7Sp9V7ufgza71E/DfRebX0s2u8ouMhfddoPLW228t+ga4r9P6Parrqv/6t9Rw2kuVtSeoRKqWf0XfTukD6fII6p0A1PId5tedYzVUycbfLkx0Tu/oOo2jIRsoGm0Qy7D/HNoe86knoJZ8wMJIqHbB3so6rpsiwFQ8OIK9Tz7fIQjr54vd558qms6OfwE4B30pcVOGFEaNwy/UUmAMyhanlgFb3WKP2aFfeSHo0hb6/79p4ZEWUYTKtpLUt3E5Dik4asE1yzYbu8XFI6ktsZBftKD614ko5aNxgEsSbuh2a59RQm0ZuiVM+xK4LtXNfzZRbDiAFUQq99HIRZkW8hTgfWxhXh4bpXXqmLHRwLL8H/eYhbQXGU7/wwcFSQGBYuPS+q9m+7qmBXpWEUcD4P3urg9Op0kiTL6RmyPhFpnRDQkRrULfbyukJmVEgp89m6EZDq/I86qahj3VO4ixUEUqP8tkuT2EjUg6qBlsF0f2IG3XjACSYS6cihT/exnlFQGzL8czKprF9Wo17vOgPeByR/0H+jou/bXZPmwKM4iGxyBn5GMlTtYDQDu7Us5meR/9A8KcireQm3vJwplZk5qJRPj4CBEEAmMOru4rgLx6jCmprY03Iy5KnGIzgrY8215cx1GjpeHpsoCZVQF8ibBejZP0den2zgqo76/qazy4w35buqGv3KIcpmO2kw4CuF1rllGzKrvsi31DfG3YLBBo0ztzyzkaR1E9fLsATU8OKocmA0HPfPA7HzV7ZBLoBp4+JIZCDGVkrNVj5ZDkJ42LAa4vXLlay8prWqucClwVhx9aTSoaDRQHj01zIkypq/ZDYtFzUxS3kATndY8LxRByCs+zq5lXBb3mBB/g9lsZpods8foCcihQitNcUkc8E2LhgGwC1Lt/9JYqUTikclGWCd0Cj+g5oaE83RTbYH1+vnZNsq00GxUgJuizaguDC0wRzCNm2QtdSlJDaB9pZW/+VV3SwgBVHUdSdGK4BUFurjzjecN4MqRJG3rFAc4pNKuZsE5BoWhD8pMP1iXnoHCsOKJCtJZIjy4hd0uniERNyTaguDEIqL3B73U5GXpd7cIqVlk2fkQwAuxXlfTy4H+bqCmcvuroMXXsUyRtu2PIi04i+Q38ESLaCF1lg6lnvYxnsg92IgCEAjdTixR/++Pt/GAb9add1jVi/fg3Y7PpNALR53aDcdeI1FcNem6niNU79JgpdRzbYxitL+ZRtwJN4CGOJjjNaNaAxq13Bs7pLhenYW7omW8ooPZE6ZPBzTA34Jtyj6WU41YK4axtPMG7PufjLO01MPI7ER0pZNG2NRx7Wzc1tmMH6UDOx049mITA4misD3/DwSsc32GHz1RKxXUHW4lvZIjyV1KIXqsnmq6Wi00qoRrH4Ns4LqWDfzeS4wB8N3h9obEF5ughnPb0+snY8VlL3RUrstIh49Nzg62Eb6GN155njYhqTnujmuxHpRgKLrkyxQ5y4OPa0rMH8z9n5yL6VB9jdbEM05n2bYBZKjDYIKPJTLqlkaBMLc23KVg9fX5rCiLjdG5GsIYCQ3VMkRcoaOpqFR47K/cnLfsr/WZGmvZDsgFPcpVMll8zn0fHjnb+3msf+A1u9JlwajPxDPnmAI/W8AhU95J9nfPMk1NZ6NymJ6BfnmHgdVxCTs1r8o7iRPJy10ekyFLfAIiqoCdQFC3ps3a7IQmoXOpfjA/UROwzaUQuI7Ohx+kmyUF+YP1eAAvMIYShoR4G8YJuWxzeyZMexQpjsk8J+r50bXpYJGhK6oG63r//wGtShjdv9a73JXVcWL0DuXuWYykEIAP9/RXHcwun6wb05/S4W2oeaXhxyn5sZsrFH97bMtKG4E8/xzKuExBLGTptNHlpEv39onQch2x8Dq48GsvwPX18/a4UGlR7rMqBdeZHjpR+Ahh5+PXOnZ2E546qoAR3jX6ev7LzGnWNt12H8AGy+J8u8tm47tnWNt8nzKqunAE/R1HkB09/DMTH0LqcnfXS2fT9nh9OrpY3G6JKnrPHPzL2Uc36su7fYCjTTZSd1FP0BpUMqG8rT6uxBmxvMAYLQStpddSLwgOGVjmQGkJ3pINF1QCVzkCOJzhiUk0XNU3g+4wIZQbWdvUVq88LHPsnpTdLxExCugldLZ4kVGpTfrR9cAtB25qGOz835QjmSR/7ziH5zCM9T7/7KC3Af7dXxbl0ZN7LLHeLPHLJAI2veETAjri4/nvnsLPgFIpwR3p9WwWE4Hp2fsDMbJ2ozBhhxucm3PZ4Dzc9fySB+0qB5+NL1ExBLGZ+BRAHpm/pKh2P7HG1RRZ4nbO2IbZxrcT6+s8y7GCJ5jGPbEq9TjMtFk107F4NGOFt+FL/Hx11Tx2pfsY9Y7Nx6a+MRfOapdfCpo86ezkrXGq9ProVK6qJuz9Gh0F/mpdUUilf07/PPPLjQCHz54cWjDiQAJgcytA/Wd4eTh58xFbYZqOuQgXtw8cGcomBvtP7w6LLwbDVm6Vwsc+9JqJDn2sey7fXROvFTF3CB7rowJP1X2qurX1GkkY2cr0fJhfq6JNxk/ldqKUzqsokr1WOgsudRMSrA3Kl1voBlXK7K6QIUBGtUjKIvWClzdf0gZnhhx9T99Bl7zESJDQfYtQcpqiw6+E4BELchnO0gwE2fF+lQ9QVCFFy3etIVDsAkoNQYSyOY9Y2Le9hHeGw3hyt/o4qf/Hau41W/UWfAIEUXMuChrNb+EitzN0p+MZPcQmdl6/VwzVzdIQvYu05oOZdBNEkEskQeCld22d66JJF34hkLcJbYPkEp5+BXGfxx7ssMsJql9fi3z07Id9jJ0sofW/n/t+2n2XabsY+277j6Ux42X7b8FR1gbnv8kiEUeVzixHM2HAoc3PKh2enQEdtz1ceJG8/TvEF8QvhfqH3YbK6vrckvtUACuvTDMmUU/Ul0KTjEb6LI0DLQNTcdY8qj+0oGiSdostRcGIdfQ0LbOaa6X66+Uj/+RnQXvIzl1eeU91CnyyAdbtv6A0r8v1BLAwQUAAAACAAAACEAHhwvo1sLAABWLgAAEwAAAGNsaWNrL2V4Y2VwdGlvbnMucHntWm2P47YR/u5fweo+rIT61DQoisKAG1y2l2ZR9HJItgGK60GmJXrNnCw5pLS77nb/e2eGlEhKstd7CXJB0cPBK1HD4XBenhm+bFS9Y1m2aZtWiSxjcrevVcN4VdUNb2Rd6dnMtuV1WYqc2lK+zhnXLIe/3efmsJfVDbY2sw1yvRFNI+6bjmX3CgTZJEFl32fma5rl9W7P/f4Zfs50UwilLNFNWa95qTsiJXRd3groWtYqK8SGt6UVJ20b6QhFvq2n2je12vEm28hSVHwnYPIb1qTX/3z7Orv8+vXl367e/HUxY/DP9M1rJbqul/Vux6vi6NeKJjf99S1XMFoj1Gw2A6lZ9kMtq2yPrdlWVo2O3fOC1J5+J35sRZWLd7pR79l/GPyB3zd1JRL28s/eq5EX5uFYMKkZGJg+g60LepFaVrrhwNIbbI6MEsMC/ykBjlKxiP2ORSkKGSuxV/F9gppj90xW3jDJbOb1ce3dJK2y97XWci1L2UgBE/XfFqyUuqEpdrMysjiqQ4ZTXbJoHki0NxLtUSINKhZFyDlJfNk634v7iUZ/kQU71C3bCV6xB2+8xy+iuSOLAzrUZ71Z+PQw1uMXidcFPGsgivmWpEYf3sfDcjDPOQt6Dj+Dwmd5ybVml6XMP7y+z8UeAzbun6wpoyh6VTHRtbJmC4FGfcC5KrYFnyiNZ+htfceaGigEa7VQKXQ1Zn2xYNfQKO4lYkMhSN/NFlyr55sSIVJkSLGAULpE8b7n6h34wXsw2+8NMxzGRC0SfSUrXr5b13Vpffg9Ee2E1vxGOAr0C9Of/CmTFYyUxVqUm7mjRsWg87hgoBHbvVBxkva9LH3SU7yg+eUmcNmdLEu2FuAxu/pWFGx9IJ00cifYnWB7hXGFLZYPRE4Nysy3wmPYEEOYJdPgcICXmm0FwAAoeE36LVjJAQbAkVgMnrtCvaycSDiz1OkK9DeJePGghxUJyO2TU5qNQdtOqgsjzYsSn5evdqC1Wn9mV5yJtRYiLhr26pt3TfqqOry3lgeZe0wLDQiIhp0Qy8IPBLH4ZTlMGrFFJPJKyABx0CeLo9dKoQc+WEkfoz4obcuSJjJQWpLMR4Mv8SdsJhMtByZ0JC54/4FcSZQ4jOMgesHfhIIoGIaxljcVpkQO/oRGF8goZeDMoCnM0jkvywMx4mtARqCsDmzTKnBOZUIfHDM1mloQanuRt7UMuyZ03ULqfckPqd8jb+4XrCaxcDjr+SaUSMyck7sbwLAiGgTCSAsUZ3VaIpb7fHjb1GAHMyED9BCMWjatKV3STluzp3Hoc0NDYttcbT3QtO8KBzs20/fYdA4CzadYH3fup9GJ/Ah4Ag/4HTTvCmjuKKDSMPJCxPS9RKmFmd6njMYgNCzzvpEqFUjskT9IGLL9dLyCJiCgFDbQQ4pybEW5z4x/xh1BMskmCedCHbE21BMqHrI2hG6AgNML9hbdHT26rKEC0VADAzmLS/lBsNXq5UvktFoxyDcK37erFVU1Ay5K8IKbIgCDIAhPGwMDlQLSXasDu3iwYj+yByPv4wVlcRw3ddg3CkbbbTmcPlSszXY+Ije8lzt+HzvtzdkHcVhCMTQAz2RK4E30gE+P/6oCZ5gy/2KMuaGhyoECKRHAAD0J2rA1wA4DmoGjuYfqFsnpN/nkCeVoGvmSF/26InY55VQJaOTQrG4BXRmuBgquCvlvAOoQ9dFLOPFZ88KU9jhMl2bgP6D7pi3Z3VYgb1XfVWbdwxnC9ZqD52O9eNhDnamJk0N/xpsGKqcO6FuO0G9kI1FrBjVnjDJ0S5Y5DCTzrVkbdNIgldSJTWVpyiCONCakohDFYsE+Tz8Lshz9mhzneNTrH2DVezxn0XyhaB55PcOSrhSbBrU5JyQaz5BY4dxAVvSECS64cqMyvxTpSFq7IkRbKVx+mwoAgBwMsMe1tpsIRp2Vd2IU41BQ+3d1KPTlJVUYjbylNL8iXivEmJxrcZTLHZW0eashO7NbXsrCmA3EassCKoz9XlQgydXGGOgYn35SsrnQvVC6zbdzVAq1clogosmqY2wE6lk2Yode+WNbN8iH0gLOB94GZYL1gj50/DLgGavwiaogSNIuYoMywaXEo+WCozki6jTdeWIHfZ9RmFB9M6hOjJ8uzfATnzKL7v7OwLPWJV0KmN7ZCDNBMOKgl8PwMmD5bG6UOwgZ6NtE4se6K2RlV0iQLK4qihaMmVY8nTT6knD2BCtC6wcn8uMk7yPzW57ai0r6IjcQaSIZ/V0ChFU3LiH52cmlpG+5xDgHK+SElQocVSqKWFtGYNbg6qbdCdD9GvIUYMgdJzuZEFX1rQR8N5lHVrf1BwOMsDLIldw3R9PBH6bSQYYZagiwsipg0dFACUhbAJjD6k2YN9JjcIR7CnaZju6FuawCwJTNIO8QV8yYxxgh+Q1Ac9UBM0Dq987ogI0Kq8iLnuXFajU/xgzojHovsN7EavOiUzK0TCGkVYy3FXNiPXQG8h3Drl8DDg4n/dPhEnUcRQSac8N6HmycToAlOcTSk+MXActzFPVrQtOAlcvDo08nUW26yyYKQDQa7KnTiroLkdBjQh2R7fzqztHZ1bNbtp6luNEQ3kCZOcboI07fdJRDrD7TVsAhg1BUPBwRxyJ77QzQ9844QhyiX7quFAT9UvLEWhAE7AdfzCbKZPi6mAQ4nPVv0YApJL6OxWM0oh27U6i3vu9Qoega8GR8AptGzvCCXSteadzd9a2GmflDhYsjfPVW66FfLJcs6vA4GtjD6Nus6m2W7RNklITBOGBpIP8chpbyNLs+05zD0RGfCufJrg9uXK9+cY1L9zgujmClb5k++tGcGvudu60Nk8fw8ANpKjJpPyeIE2oJQ81txU2XhSONLToNILORBrBx6R7PKD3DzXlbsL2pv4OV1jdmh2xq98CVamZZCytasdvj6gpqmv4EqavaqG4qamFgRdzDqu1kIRYUHN5m2uDwJzwLpBPD7nt/cvj8uiQYL1iVnVOxDOSh/HkFZuOwgvcFenapM73t2lUUkzuv7uQHHOlNTavnziQPOL/fqEdvp498x5u9Fz7PWfh5HGBk7202G+1qdHoKN7xwp6iQm00p1/4FgLystchA0nwr9CwMOJ9bZnebA/rYE2RwkHqyohgyDkLVxLFPgjVa0OX5dVoPLSdUNBW7Y6jzPz+yh+kj9/FQCSJhAAT2zOPjkaDbNH4WEvwpHWFBt9X8i4FBOOD/Hhp0djkGB/78PxIPfBZ4XuW9flpE8CX5PyScDQlf8sIUBoQGT2CCqwHAJ29EBa6Px7XgP/tS4kWKtulul3S7KIaeWOHWjqzyWimRN97xgr/5z1Q/GPau2t0aN+43fRlstpjpzMIXBnXacT53UygsRXA09OlAbrYWWK7RhnUvenlIz61pnkKmcXly9knzz7GreyK5O/d4ZTV/poP0G3pnu0jX46c7idkt8z3El+YcH/mjn6P6rAkq0Ff6a1HuzVWOkRJ8Mx29Q+ATnWO47gATD1rjxKytyYY/zzE/HXJOHlkGB6RCqeW1avuj0vD41e0KfwXUpy+6eG5irhHkdDcVj6nqPXhK4S6ktbK/tzlRGBz5dEzvjpxCymyFjdL8dA7u9tbGCdidv7eVWfbT9lZ0Kq9ObAh6M8XNh/DOatw9DDr5PfrrrR9zDQykv6STPLQDGsEY5qHjCjXE0wcMHfFyOKGzTxRe4eWl+Nu2wit445Pt/nqUuQ+Fd5qO3ZQyVSvUqnQfKvXD+PW9PDHGgJ9/LMANYnFEsDw4/6QLk3ey2dJ1JXP7seFNq+kWZXjvyt5WwrMLR4KS9kwGWSXLdFk3OsOiJI76G0/RPJm4A2XCAC9AHYcfIjVe+9kU/qCNeqZU2xVi9l9QSwMEFAAAAAgAAAAhACkjUTaiDAAAzCgAABMAAABjbGljay9mb3JtYXR0aW5nLnB5tRpZj9u4+V2/gnUeVmo86iTdYgOjDppukt28bIFtijwkA5mWaFu7ulaU7HHT+e/9DkqkDnumRWsMZsY8vvskuavLXETRrm3aWkWRSPOqrBshi6JsZJOWhfY8MxaXWaZiGgvlNhZSixj+ejsEEZdFo+6bLN2KfjmN5LKQe1Xzqr1qcKxb0n0FSJHHK8IoLvNK9ksaVedRpgozW8laq7qbjHSVpU1UVo3nPRPfy0LsyjpWQopTmjSHUIiPh1QL+Gm1SsT2LJqDApC6EfqsG5V77//28/fv3kafPrz9+ONKpEUj/iV+Kgsl1vTH87xE7USupEbxNHKbKb8uT3pFrIcfgDwc+9y0FfzWTb0U8OvuLhA3rwUPAtClCMPwbuUJ+BBlsD9J44bn4Ncd4Pv64NECYEEAChgWhIkGu4k0uV+iInBWFW0O6BuiKLDrLJbPsBxB5/Le55EQRO4TkNtg2QvXB4hBwOhrBZZQMO3+mZDC6jMi1CBzlXSQUpCf9gPcR0KC73WEFPsM5wlSIlZA4W3RkPA9Epvd05S1uweFaKR4UUjnVGUJTT0X/mKxDMTvhd9jETcC2UV5dWSfallFaIVMNv63QuqWVltsGGvx3SseTIu0SWUWpUWiCl4Ns4CMZnW71eq3FmYuLKhqBTZ8VBEYs9wDejSHbQlKXYv3MtNqyXKAXczXYrF4Iw4qq8Dyd21BLgiWjE4CLpZl6R6wZGdiRRMHYPl/PQtgT7YZWlhDcKTWYDHabG1EWZH5aAHgpNBpsc+U6IkS5Y5giW0Lq3foOgRlM0P/hrblqkHf1MBheUwT8DhAckqzbEgnQTnIInGxaeEDuWlh3PRUCpVXzVlkMKSDkG3zw87dIGtlwCRLoN8SHkMg2CqU8y69B4CntDlARGOIBAihUoCSoMtiT2Fhs/nyZbvZiPgAgGLkxMeh+9tXm00gmhKYAJ8FeZH8CExRgrug1CsEog9lC7Z3gK+qQMskOW+zMv7V0L8iKRkb41AE8qVlZjOQbSCqJHT3GEPETeDNad6aIfKEDtRgx9hKcZEZEzw2xlxlMgZxkXGpQTgZfXZpDSGUhAihW6KlggAG2Ge8gAkgxLzhIv5ruOGjZHxA7WkVt016VETJAPusi5ENg3XuMrlHK9UKJaCIW9FrEe31Efxi5HcTWzbqDkNxVLUGbwWbKvYqWa3Eq/Db8LaH/4lUCLSYDJOg2RxTnUK0tIaoQ/Hmp79/EErHslKC5RqD36ZWUpsNGsBms4T/hprHMbCRzWaiEjB2sOCsLDA/d4A4TjblSdYJS4Zo3LYJpA6I2yUo75wRpRUEBo45W6nZcE6da1Ashd0tpmutiFjIuDLBsFLLE3g5eBIE4z3k6S7KcWjnMgDZQeBdrv8I3z+RY9ReH6khYpLdq/sKdADpGRISx21eCfPOPt8bpMelZ9XpCmw9/GqXTQS4nozYxbUic45OB0iMuoJ/1ya44yxTCRYJZdastTpwKB8bhsIdmKePPJt0Xa3A+nXz2ak1MJWYBAv8f76jddt2Z1bihB03DtmVOziEWTHaZa0+IEE+5SKctTQZuhHmwFWYVncZLPl8exeiv1cAaQ1Z8Mt2MdxVhRQxE983xIC13i7Fx7pVS1heLMJfyrTwEdSL1R0WHH0gAHk+CRZLXiyEheXCSVRGlK7ubAlG0Q2DONoX1ZmUivxgIgYcH1LhSs8bxg1X2pfZAMhmKcSGoey7T1mne6zdyAVMGYeUBJOVxMmag2RmNOFNI5qhrId7cw0uSKuTtJ0csM1F4NFaGpWv7CQYAJbswCDhauX4JYSaztRhupbGr3xQHVRy/CWYCAvgTSVUHzsSO4i8PSqL7Mw+NGRqqod5MNYHhzUz2GpvrfURC8w4g5pL/Ail2/uyziV0O7ako9aEF2Btp5n5Ha+j7AgYbjiy4gLIL3sFqUB8aL7RBKTVrcwg//zSQjYulMKKC6UMSecsdKViTvWAtIB/YoCkl1zNAQQBLlESGAieJSKB0E4JOVFHlWFtqKmSOtUp1TwqrUV5ghZLFvFZlG0D4b/LdG9MEKN2Bvq57CTPmndi4IfiKVd5WZ+HdZBRR1rEtcr7EkEmSYoVLtFuZogtSvsZEnepMpqpiEwLaEphjXUcz9V5CihmMj0DAcXkUIPhetlXXJC6vntls5UTQF0msJEZ9g70NW7r2i2HunFwpR2YhQ3OThSOMBVFkU1cWmU7N2/N4AaPe7kcZrqZztauAOai66tmMgDSEY7Rw6bxkBsre0TzQc1Or8WrW3fjlU2mONAHKAQzb9oCY0vltPjjuHEFsgsC+2cwF5/RYA8ddQYU6fSfyg9CaDHbvAD/6tkAqYEixJ9ug6HUOqD0dzg1NBFYczucZ1PhkNobCXmZT4Zh6mpqOGeUBjb7iV1SirZI4zJRXSVOPsoFugkXjCvsqrIRDV1I5O2BpcaEa1w6T8EHNA0MRU4/QAdNU1QjcTxfz5udRQ4B8Dryt+q/RX7zKHLSQ9RqiNJGG9AGsy6gPa332jkHML0pjww87jG1IfiuNjEqMxqx1t83QYgeV+B/OFJAix6OlzFptkwFPnGigeCHIQmDHixpkVkdTnEwHx8PXb/dh1/bJIbirQ2/AyfbbBb/QI5WYrHZhC7Hrv8bwLNuauZAql9JNg9Q4nEO9en7OvINikWXrTl1wlDUb94tvhpOXn+d0f7Dw1cUIcC2vQ0klz5cOX59M2c93rhkJJkP+HgGvWADnTDPLY3akCRI1VAHpRkdMZCCoNXpe1bqk0aQYqgKOJWfqLUuUB5YKFBHQghMk296+BqMqHSCEYNpi0JBl6kl5G3xLoe8ToWGpczpu4cRjiKSK+Kw5roTqrhRzeVswFI/mOsnXOk5cn+9Fn5forrYAvFcvLwNxgJGG+0tmc+ldmnTVQRQ9h7I2C1/4XztzqXoPOZLvE1Ty/DYcfwhI5idsQKYnx/1si518xumXe24le0+Q+6utWHP3ICQotvzOQMEvpYtyCrCWGKB3cCTDeo/MKKR3nxM5jM+6pyGGxRoRt/+bzXqam966sBkXNLHo+rwLopjlKMOSiYQTUyWMt+eUjSYpZPUM0mejByi6jffXIyoBtjDao7E/iTkSjLvybInv9cIg+DhFDCr6wY0IofUyuLqLwiuElWrG2YUWwjuc4eUmb6ED3s1hnTPSWl0HKQHJ4qzuXFo2nOZ5zHLvWK1ODoNAjbXLb1HYs+lOPL0iDNzMramc6ELkSh4sg8k2aWm6kkXV85xaRZBROk6rz/eDqcwYZNzDRqzR+yZLkKoB+YK7ILtwM+hPImy4mvaPtBAdRCXeQ5/+Y6kOyYwRwt4sTAu45hn2Rd8eP+C93t8DaidlrrOyVjFUWatmtaDvTimFxUmtXJZyO3S7PZeZJQV2nwLHQ/spdJHi61qTsqc2TMo13Vm85uKSxLJEKPrR8g+nZDphm53g2H7jHPTK+DAjSyYNnhtIH4Heh6di8pUK/HxXKl3dV3W/uLdfaViig0gaNM6kpBHql84UZ24jWK6JsRm1Fzx3ppbVBA2JixHgt7g5pi2LztppIVzX4u/li4LwcXoeD2kE46Hcf7lkpcRry7Ekwu5m92oaNKiVWOgfbomrIH489oR0c3AlK4i5arA3TqC/KRTwkfZuIby+VzodtuVScfRFzFd1+HS/3IpXtwGkzBfqSQylyY26rNahoXJpag7BEnH4QZWB3pwUO6NNUbjV0UH5kWLwKofbNQeXD06R/O89MXqbjVf2E7N9hGRgwkjzIeJAp+oc1rxl9HDly7vaH47Y8oIbMVtGUHZ5gdVmDcPmBruBrkBj5B3bdY9qhEGuOnyJoXQ0hZry0FisMceepoFmCZcYmilEfNcgI9zGwi9Unew56OpFYtTxs1l5r4QRTTB3Nmis62pz0MN0EsPJzoWmORmApc5FHpEO85ZkC05H1XLm3mFpE8/4/p/8Yk87VVDadoy1D8pMfT/TA22dooLZgjNY5AgzQWHudxw6ujuGQ2OR6YU8c3fcRWFpZPzMIrKKbysvOvvRH5Ij2hgfR3CgMwhpebre8REFOf8xgLqjBLLgKqqy6pOZcOZ4iTPZPG1ZbHfgJWQ2Gz8viKKGAP7iizOpsuMUh3pTOpDsNmI00HVqvMOyqGNhUgcoZNIfl7QPxHpHo1oDIAAuSuDDGfmjQo4Fz+kQGTD63D3Cm2Gsu7NkL24BMhIVacDb3JSZl/NoaICiLXezGkbXtX+YXRTO48fU4OFYK/LfKeXXiI9/XXZMcSnZP6v6rzOZL5NpICK8R4ICQbXacvuuvb+84s7folGD76OAG6GFO/fUEsDBBQAAAAIAAAAIQBWAULFRgMAAIMHAAAQAAAAY2xpY2svZ2xvYmFscy5weZVUTY/jNgy9+1cQuTQBZoX20IuBLVoMpu2gxWIxyKUYDGxFpmN1FMmQ6HwA/fGlPpz17KbFbi62FeqRfHyPvXcHaJp+oslj04A+jM4TSGsdSdLOhqoqZ3QZtd2DDEBVH6/R4FF28axEGKek4fgeSGz/+vjQ3P/+cP/H44ff6gr4ly4J5TzOF+6dJTxTVTXpKrzPEOtNVVU/k3BH9MbJruqwhz1Soybv0fIz31sHbfiz5nR/akIvzfOv0gR8YaD0soF3P81JahBCfCPuzrlYFF9cIsE/8MFZnAG/EuSLgmaYxM1qtXpCnoFldgeEggXKaPUKBVEAbAcdQEkLO4QpYBenIeEkL0Au4UilMHyGUdK53d+oKE9B2stpQI8zpI4whzIZzqkpoxlm1bIOjsgJEmrdT1bV7ShDmDttoUOeqiTnZ7wYFNXDwAlo9PogvTaXWHU/GeidhwHNiD5AmNQQGynQqAbXwmnQfHrQ+4G42QSiOZvHQNy2tqAGafdJfBQ4YpBHzZg7GVnhxDcY4GFFmK2DcQrDrYg7qA9IQ92WEYmg3IjtkvECIgSwiAK3KLsOu7qGH8X3+a96lF4eYB4/uyEgRfrarZ8YLOb1adZwlGbCSH4bldDGWOvmYhLYZ784p6PURu5Mnh0y972cDH2igGM4mZc6IMhbIDWemeanyZI+4IP3zrdiFmF6kr/U14ulVBJKBlqvCjOrO8ieFYGken1+98PLJl1hbBwJ1r8Qeb2bKCe4g0fb4Tm9b+Ks8VOC1DTNfL0pODexrHS92kbdxiaZKamSNN+6ZLXJGsc8jlJ/JLiYNU7/6lJF53p2ZHLnG09+5FCM3rB4uhqpOGHWTiJAzNwVVpqm04qaRvDsy4TWqxTJzD2/bIQcR7RdTL+Zy3LjtaovK3nCA6+t7GxyIxg8ooGyh/F2EfmQcddzDraPM0fkPMb5Zi4sfZVFlZcS76v4SGUsjq/VPNq0GEzxcKRkH0W+0GPWtuszVzED9EbuWbWPfdHlVf5xm0RX03ehzIu/eBlEi2N3B44x/ClqIYUY515jwJj6z5K9ZfelpllluYikHFq0sxBJisiy4cEwCf+9199HM2+qKzaH/y8yncUCfSnKfwFQSwMEFAAAAAgAAAAhAJvMjmGlFQAAbEoAAA8AAABjbGljay9wYXJzZXIucHndPGtv28h23/krZpUPJndlIk6Bi1a43tY3m90bdG8aINksCkOlKHIkMaZIXQ5pWU3d397zmBkOX7KTRYGixmYtcWbOzDlz3ufQs9nM+7jLlNiXaZNLoeq4qmUqyqYWsRJ5XG1lfhKxSMrDSRxiVUuxqcq9qHc4Oc2z9YXyykN9iCslDZRjVu9oxkbGdVNJJSq5L+8Bbr2La3GUIi1FUdaikPAM4bUg1jKJG/gNk7L9IZd7WdQIay+yQrzOs+ROlAUcaJdtd7ISubyXufA3ZeVlBZy+SKSoTwcpdnEBpyu2c7GT+UHAhH1c1/BAwACsz2H7fVnJIPSAAlIc8rgQQIi61IelUZpMHwhr2FEKGKtEne2lXlnJWMGRaiQj/BeLNNtsYGJRG3ogEETXItkjoYf7ImVq2iCGfwZGAsQDxF+FDwTlH/D3Gm8HV8uqKiuxl0rFW6m8rSxkFeP14VS7WVY4WwmgrRJbWdfyoUaqwMHEtixTjYZHCANta8H30CBSqpEKsGXyEwQAXStRbtptjlUGQAuxPolfKrktq5N4H4rf44qPs4+zooZ/MvVgBh7o/aneAd0+lJv6iCj/XDZFGtdZWYTioyZmnu0zRAhuJSlTwoV2q8LDCQ8EXFnRYV+9fHl1Cf/7U3/zUNzkuaBJyIawFPgw7K58xSvPHWgUyAykx3vBh6XjfRNpAEJLHPE0cWD+M/Duz3oSR4+YMoo2DYpsFKH4lRWogQJYl2Yoz9PPkjLPZULPwnidoKpI4LcZBgEkQQOuZqjOfAM2lX9vJI8abtQj5issj0YnFPq7x6Nh1NRZbgH//OvNL9G7N29++hB9uvn1tzejk3579+HNRz0iHxJ56JztL3F6U20bVD2/oWidm/hv9OT8tHflhybZ8czJSQTiDUo0kHkj6vDjv79/E73+65vX//r23S8LT8DPGCofowHG01MZ73Y8QdWmRw3KSPjX8Nx8n5j+uizoEsZHGVkDSqM+PvV9XMV7WYNS1bPtA8/7JK6REqDQP8WVP/s0CzzPS+VGRE1xiJO7CAyU8gkwfloQG4YfkLdAdd6qulrORYFDkTrIpD8OUrf0AnH5o6gbsDW3g9Xiv8TYM03IpfNxDrpK1bTjki8LtMMv2T2IPRoWwCZegykAtRBruiq2RN1BOqvAs2abLGGxmxO4rAbNA6JZoImh47KdjUEzocZggqDyb+GzmdhklQLhKVL5wJTCbVGfSdQ7JKntEsXGQiZlARqO5qORGzkXWUscavZruLwOZmTN1K5sctBoEuS/UDCSEjiwOavLqxXqdDgTApPG+gHEQ6kygq5XgwchmoPFks+cykrpw/0NrBOiAETcKzKdmwyUTcrEWa3odlar0FyJ5RXgLNJBPn4J6HHLKHawfcRTqvuFvWi4fWabDlPMRRiGLmcArNslLVaA3AKQrmH0XVlIGMFfjAhx9UbWyc4HPqXdiZuIPTs7MHvhT12d2i/EJBvaBK8GIXcH6fzEQiIJDyWcfFP7QWeKzNUTi5wFrMPEW2QsUlzdlXoVqxwaOe6yXLri6D3r3IW+rnbhyOmHJx9fBkvcXfWka3HVO/x9GB8Oskh9fSfEJe52dvGP/bUPHRZx2cCFRc5XhB5NFYNFY04Lll4H1AvxdgOu8EWFnA9+p5LpXH8/NHlOjM/uXSt7CJHnznuwVIluNTndIHx0O+RZxxV6AeEUJ6HrOs5ND6HeyaVql3wkIf7DOOn+LF4uvn7XKs7AqUKjQFznz16TjwIuP3jt9bFsgc96xyLY1xA0FH51H0ycmK5LL3xhj4M0troJdB1+P2Z5mqAX52PcFIQCbyurL/jwKzz9aq7hHFkxoRq34ZFVZkz6s8hX97c4iGzEJG3VlqPRtKJxFogfxJVY4DLDQH53RGOqpZVhA3HYnuldtM1VhzyrI3Brffi3QKXk2E74NsdH2vqx0blGJ/h2cbU0CNLjMFNxDmbDDxz8eP/ZbI5LzHRcfbV4tUQJpaWDBQT/1ZJW3b5aLF1kaMVcA1kaLAoMBfPsP2UHk7lIahBc7dVoBW1078KcB+YYHYV2DL6GdXknCwfopimSoR5rT0uPDpXcZA90NKBRl7BBB4XZF577+GVqM1rzCJGIl+SxAqeZva1Fa1UisPJ1FPn2NErmm1Y5lOvPC8dNcwYO9bhLZWekUjH5NMHakZjcfXdMW7t2SsEuG9pD0L/tc/QWAGod3hSnsbUO1yAiYQSuQkXk64kAj+ZlsZ0aZNpKOIWS7LzBJPjo6jPU03hPWcEE6agNc5H3cd7Iiat0zQ3INS+ZUmufEBDrtcEM/In82dsCdstSTtaIZAfucoLusz4oKij/C394DGZByLmPcXD6mmHqteHN/k/gnX/SIWQYp6nPX25fLgf4o+bl0YCMLvmi+JDoF4wY4rFbNpp6QOFx96XHBpOrzyHT9RqYuce9FT12LWaqBpmaeV2OQ4kh71LV3QG7jj90Bzu+THeIpAWG6Hd3CAQbNfD6M5/hXw5VeZBVfbKaoY7vpIqI9j6uIH23Lst8oLjcI4Ig+Bq5uZgxNY2lRaiwTSKVIohaMrQ0o4EAd38hIojx0G3/gF9p1y4h0RS6RLHU7NKaoIV4qbeWuCjBLI1gdTEVCBpmCxpTdh2QUfAREfGZmzjU/6qdNMGmNglB/QAV4yavfbvfHDRXYBhXy8o3bPoEfk9v3eL8lfsn4GE+m7Dtgbayc5KXATkt0zv3xX+gUzezprgrymNh5PTii3PUx4tZqxL0MSr0z1z0QZ6C1tiaLMmIuWX+t9bVzJwPrWbPFPYN3KTOOKMWerLvyuWEG6AF9WuyHvMusXpyra31tHRPxE9AV9BSoF1NRp85ft49VFd378pcIiUUeJXkotH5yCQ+oMJimembI73q2jFBQ/NhTHubO+twOsP47rofyrTs108nThp2m4H7UsR7+V31yAoavwKhHvkkKnyGScf1147gEIDrlujPNvReGz3+DhFMnOUmG08ZXbDfJxt6akpxlIlKIkuxUuIEp5heagrQMqHLC5rA18LvUb9Lee+Paf3nibPLv5MiXbG02oTfCIszaPKf0yypOToiG4gn/fLYnZj3AI54qpUW9WpE1BEnvbqTP9VweoHBeyqg2BwlZve018ilFSpioZUHCEWcC15MGToYAduQAethVbBRxAsc0FCho9QpbcowmoQEhsUYE+/LVFJmLt6gt2qrIzh5XQHRGa1YqGyfAUXEukGWUjVspbAWmG0yWH3z/i1BNAlCrnrlMAmd6zUVYTjRmGYVsCiWLzl8x5KhLhhyJYswA/k6VvEBA3PUFqey0ZnFtyaQL2RcMRSIDCWwNeZq4ZvFAJYBthobhZDSUtJajhttHdPWQ4maceXUOLHoOqxrqibZAUgCg1ytcC9tolWgD7o44IVz/Mo3QORAlBeE4mL13wkiHOrodiWOVGjkhCtd+tAR1tQtKV2hdwpD2BwcYszZpouF+MfwlV34O2Y31tKWem259p/Cl232dUqkRoJvHfONCNeLBeWkp5DDa3RQCwVX6LiquZYuGM7R0ApV7uH20ns0OCkyEZgbhWXPrrddP6CvXT90D0MFwKKuylyBQThytoilKZUxPKScD4mUOlAWxpEPF9TbjS0kg5pETbv6GdbL1dyFeURag8d6EGXRpvhdOEVZXDIvAPpO3ZaAY+bd8mQB2hOOo5p1Uu73IIzKhaPiDYh6jwjAXuUxcrGJWIFh6ADk+Vg1ckAfUBm5cpEgOpVEHyaP8cy0FoGD/+XkgtF8zyk0IACXv7EY7h93GQgKEY3lM5iT+uCCOiq0DlJU3SCFpCXBgGSbgV9RK+GVZgXYFO05gY5ywbAeU7tsw30FthDTQQNvvdTpPoXak+stfcbinSO9ONKLLUmJCToxqE5FjadJz10T8+/U6BDM+Mk0lPFBbypB41pDbY7G7KEN1p87H6ZGJmrH8dklhqaXl7P+RMfQWtfdmEmjmiDq14j8v8mYDXUoKOObNMVaYiGPxgFAtzEVKzwj1edaaQ25DIgjWRF3EhSaCbNiAwIJy/2myLM7XZ00NjLgJhgpU1I/YCfkA1j0JAP77LXJtPIefMYUNrvh6CwBR2QtycssN2K1oih9BcpQf+SAdrVqSbFasXPHk9ywd7Wi0uOKQlGsCNo1iNkK7nRl9tOejYDDgAbYsC3VNNL1FnK6iJHaapx2kThlojubXBq6xO+wDNWGBnnpuTU5QT8JufS6yTvMPDIv+oAH5ZUVx5lzzVfX/MuEAvT/OTPONQf0Z0QqbA4pmHxfWxTzOJjIkeIcJ103opra0Vv4t+Q6gctUIwBtAm8MnhnsgevItLG4fyAwPydHpjoE5szGO2cE6v8I+7Vq0RY8DT2YlfDs124UWehSaJtXQPBOJ4bVl6IX1rS6iEtG/cjI6aGYj4Uzyw7ZKY5RY2TnAMR0S6xWHNorPs+caResVh1ms3RKx6MYdOyP4MKYYALLz9gJ6FbgTAF24zTygerSupOvjDoEETfqEFt/hgjFdF20ihBvAgIg7dxpvwxWFZJrjM6peOYeHQtsCcG+xB6wo3bM9+CYV6Be0v5sjdkEf2AkjOrFjYx7xcdBGwIzlfaaiDMioLHxDnyCGTxnBTFVb7puO2j7pYYlZOOpO6U6NFHmeQieWJZnQMDowFhNpG4GWXCbedCZbI7c525ywQlxJtAh0XhmIvzA8LXfNux66mY36DTiB/2t4rW3DzrRZpNhrcy3dqRX8MrSB9oVp8sCmA0bS/12YS9TA49Ck16kI98CgKXGMuinbnKNTjeV0Z6554+dYaSvISV3nzjbDFCwuWcapm6Rl0F/Ui4L3UIAX4Jeh8VPZYPJgTRWOxSs/BifFLcig9i1Hg/w0zau4KGiRtEjaO4eIOvMog6hct+gPYMOfE1e7lTLzjBbCYuwHN9yQcdz1hoPUfxxuhBnrgPlAIlgr3mw29locQi9ZQ5jiwYknqjwObeWFZg99l8S+wZThHHymh/ik4252bRm26xjxVFdO9kHcQsjDP+Kur3oo59dXgX6k/n9Az6xE97BhKUD5szPfzjT/KO2JNz3XZpoVG8VOPkDZxVYnIKThYjXrTlW9zBcfjWTYvG9atZK1t8DUw7R7SC6FD455Y6pBNpllWOayO5gT44Dai3xVDpDRP4J7T6Ng5bavCwPDLBRDWW3dEuhuELlzq36dmfgMnxXwc2scGeOWaVgmS+3IWs4Sndq94obESnrXpQtyGDugKqRtBz8u6rJDxBUivaGWiizujF9kotR9IQwipDJjqpgQOsex1j9+DR/dZGfSgbAkTeYW5jrwxCRteJyc2Xie7k/1KfvxeUliBtNOndsPAqlYBwIaIJjAT7TiTNhFEtuNZTvHH2/j+tkZ136vlPZduwYhRp1SlfsuI8ZhTP1KIw1OIrtZyBG6oluPzcHauCEqmwNPkWdSV1nsRAouXltQzlvGLqNhDC9s2EA5BTrFz1rcWObZzFQoQ40TNyhstiXKUYQzDdrpDy6nEh3YNIeGENPoett5l0ckATbr04RBlIh5VAkUz0g6youkp1Nusq4aF9uEaycYS3XaoxrumlyJw3AcEzP8MDudW/9fKvguGHoQuj1CJqiE1/KVupZEWqriODxlfOtDF0cMnzPPaMtEDqvEQyrerRf5M90U72tDpoqAyksFC3czCkPUhVw0ObSOWvfmk6U3Gzgzy6e5g8XdUdwbWzv2yhQC+wzPTXKbJucp715DMC9bt8V+5C3L1txGWYpb5ddzxbzxAUtu1osu8jrbrxuIsbtwNs9zpykjDdsorJ8Y2lALQzDVjDxA2Iz1h5WOt17Y3HNRKJ4tKLbm2S8qmQXjE43+e6JOvZQ7XUVW++0T6ktrbrAi7AtbIrCan0//yzEe/DWJLsWpwsqWMkREAW9LYR+KLohxDysPFDLObD5DYVwjLKZ+LN16cdJOeli3maLZTCxhNiYqiGTXQXfqmamneEJEX6eGLvcBscfQl9XMr7zvF6DutiW3AvQL34cseyRlPt1ZtyiGku9uoPaAdO+jOL6lJW8jOs6Trhf2jRbkgNWSX7XzI2dXpgcG2ctYkXJbXqL1RikI3n7uiq0jlWWCD5eXIPjn6iOnxfXNgOd7DKJTquJIm21CI9se4qGNb3zYktInhXlkaDI0UgXF+FnsPl+D0Tw6PbjjXPX0KuClRGaDK2stSIyieWnXKqJviGnW4g+9V9WW3j99zU0k3KfyjO7k57apVM5Q0F3xDkA2S+GEWmrwcr15zDa5PE2ovrFtC4zL72hqw3qZp/VxtHSpnlSVkdf4XvipZxnuQ6t1NPVzkcnmBcqp/uJBq5HBcTPKkoZWK4PZ/NvAGCam1rZOQOGyxbj3Ut9r8fi3Mlbz890MztvpgwbkdHKRNUgS4TOR59vRj24SU4aTEad4DS/2Y3p9YpgdLqdM53cGV2HomDXBpj06bqKY1xOrwNqk4uB+R2+pn1ntDu2pKOhLazlgoj4oh6Bg5FFVpt8unaUNyNiQxE6ylQ/GPiDEmRN8HTOb9pB5hdlXEZYEOf0fIFU5mJkktfPLDMnDFOelGP7Rje6F4Tolw3bC/i1tNbW/rkE+IBGht4iw1ep6GV2XcDgV3rwichcO6maA5piXO2kONlum1CPusAKchGwrQZfYuPor65OXfONYbCgQMIte/F7Q64Wn13PtKPYvaA27B7gj94fvZ/hw+K5uDp3zwYML2uNFOiYyBnrBQvt5iPxfj9ONyRokfVjRTFvwFSwOamqbLa7bgaqM4MgkLdbpujpvCs779Qag6SbM1o4JLezy01ZznRVniBhd5JyN3D8mYlaTy9l0yFT/yYG+WJd0XHji/MZjkn8uUGVAwH9qmMPEDqI1YmbEyhIc9qN/goq5p5bVnXXEL/xHOeiavJ+toR7jlR8UnNdeyVPmN5CjEl42ldzjLvHyrgHyAeRyzNJjRGzS74NqsFoCxDMnT+ZkmbqwPxSjmRwXIz4T1IQJchZBWgyTrXfQC9ul/2qu04DYVFtrNyAL9p1s2SugZkqF/STAlMVg7HsvBMXf1Xwy9W7pxxpKhDotwLRSQaVVUWRb31g0qlco12YTk6k7zGuCupX9exbxHv62yRfvJ6/w722jkNje7zdZzzXfWLfInMfdlSNO+DaAf380al76dOG+KHrmmxmF7o3gPyyxwvKYNsOz5Z5pvo6Zx1wM65z56nbSXwPIkl/3QAWXph2oIu+k/eT3hTo8Dufd96/veSOmmKvX829oeumzeg2L9dxrvzgdjOLGKfZsntN9LIPkReuP+Jw1Cmg8d+nUDuZ5xEEhmDkOevJN99f6D2HyrO3e2MfDbn7cHqEn9uGxLIAU7qWfTp3qdo/7UXvkv5XSD1OCtYuNyBI2bqp9Us3SPfA+x9QSwMEFAAAAAgAAAAhAAAAAAACAAAAAAAAAA4AAABjbGljay9weS50eXBlZAMAUEsDBBQAAAAIAAAAIQA7ok37Fh0AADxmAAAZAAAAY2xpY2svc2hlbGxfY29tcGxldGlvbi5wee08aXPbyLHf+SsmkBKBNgkf2VRS9HJrtbK8UcWHSrJ3K5b4SIgciogggAFAy1xH//31MScAUra8m+RDXGWRBGZ6Zvrunp6ZF/m1GI/nq2pVyPFYJNfLvKhEnGV5FVdJnpWdjno2zdNUTulZFF9MRVyKKXzq13mpvxVSf6vWyyS7xJZVZ44jXcqqkh8rPYz+CQ3GHW4RTfNC6vf7xeXqWmZVy6uD/Po6zmatbzIE2vLmxyJfLVuev1niqlpeHMdFfC0rWWx7d5qviqlULVZVkpa6iZwu8k6nM5NzUS5kmo6n+fUyhT5hR8C/aZoM9Dp6/KT6OI6Ly3JAmI1erar4IpWv4iWi8aysip6oov1sPeLmyyK/HGcwi4HAdwxCDTH+EBfO4ySDryuinnraFf3vANrLBJYQp2ePe+LJaEBtgyA4lsU8L6552hom9BXwVFQLKS6TDzITBy+PaBKAiahDfQdLRIu3NHEhkQmmMbDPLPJameUefqyKWMSK3MAuuVjGJX5Se/w3mQDQ6Dq+koBGovBk4kFzsPEa/op8TjOVH+WU8QhIoCe0KH8iHtK83tmHpMgznJWAlwnBqRZxJRZ5OivN7LCtgyYH3d5AHhl+itMVDQRLcyYwmYibpFrUQJqRHBACsUur6em1EdUmE6fRmBoYXBUSJB0GPwXxXoEE5zOJ2JYfk4qGjTQL0KcCPu55ww699cHKqgS/hcE46BomHE9BEIYo4mO7CngIZA0JbJc5Jpnb5kkpXueZHJjF8mzFk44BCyB18xAYomeYqGcZoOcRVI2zI34ugNfFxbqS0DgHbBU3SSnFz0k2y2+A11ATldUsX4HiKuKsTGNoKV6+QPwcnMAnMXMh46sy0lP30DIUQUnKILArQB0Q4nQifhV2I5kh1sNuT2Tp8EWclrJbX/DjzqYB9MpahzD6xQ6yCbTFbAdGqaK3fz8+HB/89fDgb0evf2TYpNFYhY8BNzIrUfdr5fZ2vZQ/xYVG7gTU0gQJuCrlDBU6aL14lVakMS7i6dVNXMxKIgzYlYskTaq1CHF1Mp6hEMjoMhITWOyEpzwm8XgLzAMkV4MBh5mnAVKZRDKrhm+LFVBdDTkkHdntSMDsoAkL1voZ0IBrOsSsqMYU9x5V8jqsoh9lJotkemZ7j7pGc57IZSFLUmKxqxE+kLAjB4HRiGdxBeruglgNpJZeRuLtQhIYjTrTEtA6mQAlJOgGYEZg2GQKzCnKpZwmcarU9AKgp0As1vg41GQCL5bQCUgcq1blaon0K+FnfkPWWWAjNAr4K0MpgDEqNReeGlN5v7hIQDKKtVhq44c+QAYantQ1EP5mAWZhCjJSITBcW37xD/Aaejgfntd0KqnpqsQmoKsAq1FcAeEnkTiCiYJeg1/JxQpWeBOX2V6loPccANg3qRQbI3pQcaCWc9UtTX6AeHVJUa4uL2XZ0M2IX2gLOCqtmRDltEiWhBFQLx+SmUV6i25WuDVWEkFG4iBNplcoFzjNYJYUAVCEyRPMk1QGNUOG5EAFTfRAKmUuVRQnAUXVaDWjioKGJtUSS7MRMZi4WCVp1QdzkeD00bApP08vYpYjxmG6MFoCmhLoIKarsmJl4GKyBLokBVId7RW3sbhwuQ06rdIZAU0qa2JYOMdlmlclOJ9DEQa0OpDGAMfCT8QGfo6TbJ4HSpmjSzWGJ0k1Hod2SJnOe+aXIr6VUvuKSQ0cD0MGyzROssC+ZPTjy3+RNYI2+GEbPHigcUyKhl+QO+UbL5xO1JgFgKNnfjN3Rvjdf9syJXzkNyIEwRuenIsnsMEoUYAqwpAwLqPyAWERDZNrQUbQPcQuqBJ3xJssXYsf4nIhvhuKb6JvQO2wvGR5SXHAkiVrfPrm3cnB4fiH/dO/IpqD4Lzz+9AY5vkqm3bLsCs+0chpDv6hOHpxOtzdOz/P9pyHoE6XwGtS2y3+NdwNwTcTB29eHY9/fnPy/HQY7H6yv84ejG4DfnuAD4a79rvwJoIeQjm8gAUZ91zsPlF8hoLsuXViV0/gGQiKQRpOfK+3B7MDY0ZSwlL67bffimDXQlAsr2z72ZnYpcZg2PdAL+yJ0egZ4tJqFPyHMz85PH7592HY9V4gXEC36OcCeiOJrBTLtD4AqpqvGYGN0pYBSJDuGuHhMNwl3NiB5klH6R3p+SaPO7edJsuMS3i7NIxjaAZTVBzYfyEaveCJcQ+75RbAzzqkmHbEqZTifblQ1l9G6FnHri8LVi/Nb8iwgYEugdo7T//8+I+gMCW0lQBiChb5ku0gWHC2JFFnB96Eb18+e34CamEm8THYT2iwBDOxdrwCHpEVMEGA6Wh7VGbJcikrAAXP4xT5bg1qeCZBOGdymeZrMAtCnObgxUn5i3SnwFNGXTxN5QdZ9JNyAYC0+4DOGMT65OplDgqaGIi6RsrfWyHfwQaoeHyU3yn8fddfKje+GKOxUXhbtjQ16gKfhqH4ndh9OOVItDzzpjQS3a74wx9qQYZRMCEolPD7ebdN0dzk4Mw2dUwYHrw7OTl8/bb/BEC3qJlfXC3jTaZ7Gzg6h2TqSiItYZ2kdz7pid16moeFMCApDChCYHvWLoa6NYHl5uMNTR3hQiQ/RHzAjAJfQaCPva1bk1oG0EBPw4eotIGrYtzVofvUPuExBBaLMWq5UvQfbQVCXtdnQJlv1lEIVPQzT7u3rBZGqQ1iRb7/k1hlJXlw21Em+u86ahKbx26ORLHWbAa9vaFa5AwA31IciPoceVSCitZplrP+kxFiLc3Bi1xVOQqvj7sdgc/xPceMc8RhjzI+AluTtgTCQRyQrqlHUz8Hu98HHcNNOwJn8IhD5keRUALcA+m8TCBkLCxcFBeM1AtjDlj3bDcAsGLS81p/vTiyCsyAbgB5xkkRCTbG+iWC9END1OeJK+uO+gjVYkDXAg9Ml6goHCXiv666NTXx7G7HxHELaaImjAxVjFcuIfoWQS8QDgNpyIrBKoiQxK7ueoYswML3zBeXsbfOMVM5LxIQH9v76ehZx9UYGwcgwdw+AvLWfUCzWvRhY+ZkE6xsxj/oC/3pWF+jn+V9pSP6il4+neC5zWaed4KwwUvdwLgaByopkkqOpHRG6ji/kcUpxVF/ip48JLPtPAuXN+WiK/78EHwK8Q7DyxMlHX2dONfeSyH6r2GED7IHEXoyXQDzTiX8LHWWkVawf/oWIMUY1hUYoV+sneEiHUZTU87il8iHmNiBxuBIXMAQECQuinx1iR6FZXpRcux6vK4WGIFjHI2Jg0JynAlRNSZdy0UqP/bBFwd4xKSYRQBAKzUbbIie+iPQUcbzK0obbBy/+fnw5PSvhy9famG+GyWirxLVlPetUfGUlPAP4F1cKWeFouxwFz2At7kG1iNJQiD7ZYU/VgVo2+O8pMSoMuy7y0J+sCphKHZBcQzqisNpSl6GbmeR6QLDJl4LUiJqvDbwAh11JKq/I7FnezhUG7rLit7mnJNQMQIIWh0N2qHzYXHQ5cNSKD/kDEQJv1cZKCtwwrDvLYvyVwFjQIyJqli7wIzmHoo/1Aj+9LvdbJWmahbzJAMbtq5PpAWnPm1b5m0Qaim7bXWWuu46EOWgeiq7BMC49l9vLZfNk4/I/zXqPOCc/i5aFpqzwUNfWYRJFhjrEu4m0ObxMwGfD8VTMCSV6sr45RdD8UeP6BwMqoZnu8nIvuKI2HkHYJ8479nV9d8/Hbk2Sa0dx8B1o3+SZADz1rNcIU+iL/+pQ1J3hjzLBPcRqC2P2kedpr+D3d0bYyf9RLOjXsOtB+3sdF1iCvNVnMWXxIHRPrhD15RWi2z2+ESWED+PBoNM3oQNn5lB98znntlfpMzRXo9mbV1lnlNtuZhDqC/2R1n1DxZJOsPsteg/VwZ6LfrHZEkVu/QPiyIv9tnxOQXTloGvdqAx/K/GfF/kxWE8XfTfUHq3NuavixqDonH0AmTzNe3wwA/+snfMedkCJwvUlsVez23abcC73Y5EypPcgcXfHnW7V+AEoLLWy8OB91qb0vzH0fHpUWlwQNxbB2ExVGPhfye9aFafTyGl/m7ZV1JbMmPec/8J3JTnybQKeUtnht/VToznbFGus313vGUf3YxCbo9WnmaD54eYEifYANUk7wpwpr62V65y35HYh68XpgtvYqgddO50k8CfHDyoAh0js/tR6v2iRT6jPXGTsjfbM+17zqUIJxMOnsxWg176ZNK9z179f+3uOjWKIvQ/cX8SAl45GwzEX6LH/jYDzx2cBaQDcA6WU4x0E5oUINiEl3YDCNxhcsoHxEkTgN/Yz1bbN28xxaZ3QAEbW2jzCWdzO3ZJ1OEiB/XGo5a3Du4zBjFdYti7cUkHDiOqPSzVRSUWK/apB8hfA80tEa/ielWyA4U7e6w9qLHmY1nWZuUy0ZeVsrRJYLuofvbOT7Ow5h7FNZsKbLYU2WzaA4L5YNlCmtQeqwnhO/XVb2CGhxbmew2GMxNVGqF/Mr6+h45LWVRrgz3kY4JEu0E0Z1iBnTLQFCO9zJFTlgSbygGzl9mIrG0jVOW6HZj2XTyXelWFjIClwiI4//kBbvLBf3/lUSGBZ6cyDPq0CRh0e2KexpflELrunx4cHTUKHObB+JMZ43bc2HyhciwWIsBSabFQsyoeRvAh6XslO6glB6ioteQYgdRb0Pjvh7XZy6f8uxKl0q34QaxOJr1GEZDlNtbfBiV6t7iOV7V8350IvHGCAaPXsEBvQ2OYgW7rPqs1N1PSbW0FjmPAa0jfzHXgpsxWU9my+Y4WgZmu3MRxmjmjBtInk99PJv2yWqda81kx20BCW4A1A0ZQmCWGtz1dtUmMtJ0shJ/aOOL33mPiRmeDu1Y6hfrBIq9awfOzFKwVaf0eYnPkofOdyqxgdhJB18W2Xt+gJhpbq4tDcH0aF1glmTVLjqFAI2E8Ex9LdXvRQE6MFViv8+rI9idvehMWyprOF6zRHTzYSVpbQhijNn4xD3XxsfYcQ65rzL0yo2VcoAoiCOuo7HajToShC2iqRA0zUuWUZVRJ04zEAWbEiZu5KmYPudNPxEwmBgo7fkgawEGN70tTY2LX69XqOP4Xo+glLJ+39pwpqb3KDYBqcFzMcu2i8hbV01kkXsVr5Abg72rdTnGwdKD3x4Us8/SDKeUMtZ3s+aaxbg6Y3lblAxpdiruQ7dMQoHFHt23DbgCsqFamSx3dLtaI1vdCVWkFFjMNRAuXtWq8FwTFLxRDCDCmKvahek6I2PPLLPnFyG69yIat7TaJ9CSxQdjapHkOMANe5b1F1+Dxs1S+gwParYDxS4khSTy90rVPKrjYaGAxcCm1bm7Rn2Bn4Wnp4dARqh7taflQGpSekOhJiOBbQLTjqs57wKfE120avts2sfYOZbiZqbGIYCjO2Nw3mBUJ3KV1KHZzBxvVJSM4z4LoH3mShQDVFkZiEZAOj0MvWLblkKdtBeTYMdoemGH6EjP9wWfFPCj3TrmR8nxLrGybsha1scN0IadXYxUthi0uu6pvLRdYy994uroAZYQFiJYJqXYIAmEkEnWKaI8l5AV0veSkadsodSaNH1fThao383a1/GZAhSWR184mKlZZMxNzpocD1xn3rIopOtF9/LtHe1/B7idE2Pinw5PTozevb4O9Ua8BhWuih85gx0fHh367bus60L2XcQG4KIL/C89nD7vnkfqAvzALXknEI0QzqWuWXZQxMMAXJn/bcPaPHEI3MNk5RkDUOrrEkx7GmfKAQWvxrQi+CdB0808sCYDflGwhMPR+0EAEFVq3prvGm7NgTRFQSzHlm0Ymgs1AdHqjFHk6o/xEnGH1XdTep9trfSyLgkulG2+7W9itfdnjMDjAok4sFp2BzIOfTFWBaqK9Zj6svu4oaJlm+xS7nxlFcNViTcQbrvgKAuKwawryfx2Pe6p362gvBfuOebs9zMtIJbrOArsXFIy6fl86VlE1W9OekNta5wy4COnJgL6MLKtXxdonoGd4VDfVyaD941RCGHCUzeRHsuJbIAROFaMOGmp26J6OEudfmmQ1kf0nKhbHXP1tj7+Tq3prU8Pv72mT3n+WSfrlHhbpvTZI/+Ow/zyHtbviWEyN5efEUXQaAjgiGFvdukPFD07VgZNW5cJ+U9Y1EcqBVnljr1B7hwonShFyFQYRklMa5IfR0CZ4pEfq3Ejluaw7Yp4U4OavMhg0XlL9WJpnXU4Vqbqv2kTQIWyZi54KraI+atlPeA9ilktPeTtAaGRB8wBW7HLRKU/LKSOtQcbzKCpRzFByrCpP5jVUKItBnr/E/TSZUsGi2iONOdfugGlkJBuZyB2ndbiZqJisyysckwoAapW1vdrsq8WqFAusgykobEo+soGmmIQd0KjbOgWsMlbFw0/+8uRpvZiYU46rAg+r6WRE5J9w0GzLMbtJlw7AuyqC80HQRbQyg/+Oyz25TMr02aplz7NPrGHhCwFxVO2L5J66Fjt+hrLFuq8v1rZYyPefV7eeNtukb10n18mtbFaLjYk4gd/Z49FG9e1o7h1CPqw0L5S80/HNOBVkIEBSL3JQWk7Jis/pdv5Yi3mdf0BOYpXl57raF0f8TdPTX3R16SYEELBlvnR9+X+HSXDDd7u7SBX9an/xm+ipN9ND1nmZvOECG1yhEkdRxRfKHJRL+G1UImm5ig931aqgP7pSI9FgKgjXq7RK+lQe6hzc25Bg3RH70ylQlk4h5GJRVcty8OgRihbnUmCQR7N8Wj6arooC9N2j6fUMfuhTD4vqOnXJr7KeEAuWVVKtdCpylRHv7J/8+O7V4eu3p3hsDQ98aUpprPQdWKXE7BPGP6lKTZqKST6hgCkW8wxCtbU6W0QaeRED68WIWNcWgQ6Op1QenKdpfmOT3QaMU1ft86g2/T77NTwDq2HPMwymz+Gj6zys8KGo1bTf6b+eV1a73sPptVWa99LHbk3pttpTrDcVf37Y3bLp/qfP2nanelxThPjFOt5Wef7Psf7vdqx3XD6iqmI2OqbyEDPTutAX/VVXe3pOZlaurvkoVQEuE7dZSq8IPkQR6WnnEOWp6/h94lVNb5LavSE1RYf6eWNmXsQ4Tg8rlvHGk3hZun63p92ncVEk8aU0p5AxZU36nYulU75boxZJGE+WUlUA6gbHdeJW9sVRMIMst3MNvlZfFUo11VWYq62a2R8N0V3EBk/RVWiuuziOP8RJitUWfBkFnZx9gbW1Z7i/qmovAMiZp7VGIxR63s/mXOrAyzlziojdxIHnjao3jooZtOhI1eoX6v7e7X2LB0Pcpm9r9wb4LwGJF2gCh4H3HFO/dPlLW82QvgIGcYErr4EcOad0/XPI6uKWtj7OJQSqjCkWAxptMPGaTmxBGkxbule6UNGFKmdSRSBUnVYrBNpckeUWdakbBFRh12xVONuFtnjL1KGVjaP6PEcSTJoFnVGULRfS+DtiXvmZU7RWr/ByUBCJ57yRVLq3HzCNoBHt1HL1ha3P8+9KAaEkdDUy+6rWBdYXUQEPn3qvy8QZvhtxO+8YLP3ufF9FWCCIh6w6LfbOuVdl4Nzqw2Izsizjys9oACY8ui9kEjsHsit/Xwf5Fx/w+6+Ca065N9WLkiuG+SvA0dzwMs+vxGopYsNsIDN3yaGSKVPh9eXCZgSNbtHQHKxYErP0dja9O+7NUGtmmTFyAoztLIilxpMAxbAN1qYbBPSdP3wdVt3R4g+LZuOrWc+VjijEmXXl1Wm2WoEmnSGKaIAJn52mCy34XAPMS2eZVG8s2TS+D2DuMqPIOBbXCV8yMk1z+vznKgfTrMp3HadKpbpK+c+VzKZSn632g+sqvwLdSvk06zzjZlf/Ao8Wgeu8pJNR1jtrYCiQH2McUuxdrwWdk7NW+0y/RHOuX48+B9j1+vx8MyANQ7OFIpK6moS8nRTv8+hsjpJtjPwKZJaqWWolL7ilPuEUwmRCPmKhS9yMatVbskBaHgy+0HYr0Rr/Kg7qiSWQ66O6wke1xP3YSpJnNuaTLkOBDcx7LObBAoaiZB9ZbbI6dUW4lz4yh4kG7jZ7RHcjzUIAxCMqh5xKZGoO+Y44wbIJdU8O9Orn877lxAJDXi4ejlHOwZdKINiuYmTNd54juom7yIJqXjV5WBMXE7s7YAh/CL9HmVb8SdAib4XxcqlWyG+7noGCFkqsx0np1N2MtaRiHc1A38vX43qpgb1AjwT+Is9TI+u2CEsJK/smptCKLj1wNAE5CHiesKwSdXgLbwlaVhwMlDVHA2dzlH3IpzE7GarIS1dU6dKuQl/l1Ky7UQckW1Jeulye16gPHap6L10whfuTur7edR9yTDyjeo9Bk4QEo2dgdBv7YXR3WKfjpoJhbRH1Y6VLX6PMHLJQHUN3IdSA4tKh6D+xdC8IGNpEg3dVsO6C1Tn62nWIEcS/r/ZfP3959PrQBenvI7ujfyee+Ik/yoQbZKioLqRQvkeC2e02OqQy45Zd8a0LvePvc2szBOoQRGicz8ecWaoxqrq7R9skj0UPkIaaPRn7Kdj9EmZ2pUpYETaltTJ9KU6N1gx/C1Wn+rIgnd3VLiFV7AB1cN5jPoYky3YhbF1Zo2ryV5NJHo4lMpNyhqbUrUv8NWoSfRnj2zu/RsIYwlb5gr7MToBcrD83dZ9gO1ZZta0r1o0qKugQzhyyTDBpQ+RAisoMBB1TomGB97KAhqECr64zM8qmQx88Owki47C4H7VTRsMro2njdRq56/f0pwvvN8FVK3XbO0U6pmTWvFO8EsFvw6r1MtDf7l7UOsdzGK0Ewt576tUjsllYJECTYrpYs0Qje7n3c5Kp0NmqqojJB1J337l8zZltVaRJ252co/aO4JfqttaKdlzRaQff4PISpSvDki9Y2TWgj5RznKZYGln+m09zfZXkalHUBD0LgAOSNAHUoJVBxAUj1zvjm+VqF7+GtXpgkMHlGu/TfPBAA+7S9cSg8xqpV9Sa0B8LluSMz988pKf21jQaeAE+tGgKlqY4Q1K/
[Diff truncated. Use the raw view or local clone for the full content.]