diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dbe252..54a8f7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -230,5 +230,45 @@
- Changed: remove `from __future__ import annotations` statements
## ver. 1.21 (19.04.20)
- - Fixed: GUI. All paths are now reliably treated both for QML and Python
- - Changed: README installation process actualized
\ No newline at end of file
+ - Fixed: GUI. All resorce paths are now reliably treated both for QML and Python
+ - Changed: README installation process actualized
+
+## ver. 1.30 (05.05.20)
+ - New: `examples` folder (currently, only an embedding one (updated and moved from the wiki page))
+ - New: `docs` folder with some useful internal descriptions (currently, only a logging schematic (with sources))
+ - New: issues guide for GitHub (OS, content of the config, project tree, enable verbose, etc.)
+ - New: GUI. Show the app version in "About" dialog
+ - New: GUI. Handle a theoretical app loading error
+ - New: GUI. Notify a user that the "board" parameter is empty
+ - New: GUI. The app can be started from CLI
+ - New: GUI. `ProjectListItem.fromStartup` property
+ - New: GUI. Expose projects' `config` to QML
+ - New: Tests. Preserving user files and folders on regeneration
+ - New: Tests. 'verbose' and 'non-verbose' tests as `subTest` (also `should_log_error_...`)
+ - New: Inform a user that given parameters have overridden the config ones
+ - Fixed: GUI. `TypeError: Cannot read property 'actionRunning' of null` (deconstruction order) (on project deletion only)
+ - Fixed: GUI. The app now can handle as many projects as needed (use QML `DelegateModel` to store state in the `ListView` delegate)
+ - Fixed: #13 (new parsing algo to analyze the CubeMX output)
+ - Changed: improved `typing` annotations
+ - Changed: wrap imports into `try...except`
+ - Changed: new README logo, add sources (draw.io)
+ - Changed: GUI. Icons instead of a text for "Clean", "Open editor"
+ - Changed: GUI. Gray out "stage" line in all projects except current
+ - Changed: GUI. 2 types of logging formatters for 2 verbosity levels
+ - Changed: GUI. More general `goToProject` signal instead of `duplicateFound`
+ - Changed: GUI. Projects list is now saves to `Settings` in a separate thread using `QThreadPool` and `saveInSettings()` method
+ - Changed: GUI. `ProjectsList.each_project_is_duplicate_of` generator
+ - Changed: GUI. Optimized project' `state` handling
+ - Changed: GUI. Insert board ID from config, if there is one, focus on that input field by default
+ - Changed: logging mechanics is remade from scratch:
+ - add `stm32pio.app.setup_logging()`, `should_setup_logging` argument fo `stm32pio.app.setup_main()`. This also fixes annoying logging errors on testing because the loggers interfere with each other
+ - `stm32pio.util.ProjectLoggerAdapter()` subclass as an individual logger for every project
+ - add `stm32pio.util.log_current_exception()`
+ - get rid of `log_record_factory` substitution
+ - add `stm32pio.util.Verbosity` entity (enum). Acts like an additional degree of freedom for the logging setup
+ - rewritten `stm32pio.util.DispatchingFormatter`
+ - GUI. New `BuffersDispatchingHandler()` class
+ - and some others (see block schema)
+ - Changed: better parameters and configs merging
+ - Changed: make `platformio_ini_is_patched` a property instead of function
+ - Changed: improved in-code docs
diff --git a/README.md b/README.md
index 5cccc82..669d78c 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ The [GUI version](/stm32pio_gui) is available, too.
> - [Requirements](#requirements)
> - [Installation](#installation)
> - [Usage](#usage)
+> - [GUI from CLI](#gui-from-cli)
> - [Project patching](#project-patching)
> - [Embedding](#embedding)
> - [Example](#example)
@@ -49,7 +50,7 @@ stm32pio-repo/ $ python3 stm32pio/app.py # or
stm32pio-repo/ $ python3 -m stm32pio # or
any-path/ $ python3 path/to/stm32pio-repo/stm32pio/app.py
```
-(we assume python3 and pip3 hereinafter). It is possible to run the app like this from anywhere.
+(we assume `python3` and `pip3` hereinafter). It is possible to run the app like this from anywhere.
However, it's handier to install the utility to be able to run stm32pio from anywhere. Use
```shell script
@@ -94,6 +95,12 @@ $ python app.py --help
```
to see help on available commands. Find the copy of its output on the [project wiki](https://github.com/ussserrr/stm32pio/wiki/stm32pio-help) page, also.
+### GUI from CLI
+You can start the [GUI version](/stm32pio_gui) using `gui` subcommand and pass some of the arguments to it:
+```shell script
+$ stm32pio gui -d ./sample-project -b discovery_f4
+```
+
### Project patching
Note, that the patch operation (which takes the CubeMX code and PlatformIO project to the compliance) erases all the comments (lines starting with `;`) inside the `platformio.ini` file. They are not required anyway, in general, but if you need them for some reason please consider to save the information somewhere else.
@@ -102,7 +109,7 @@ For those who want to modify the patch (default one is at [`settings.py`](/stm32
### Embedding
-You can also use stm32pio as an ordinary Python package and embed it in your own application. Find the minimal example at the [project wiki](https://github.com/ussserrr/stm32pio/wiki/Embedding-example) page to see some possible ways of implementing this. Basically, you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), (optionally) set up a logger and you are good to go. If you prefer higher-level API similar to the CLI version, use `main()` function in `app.py` passing the same CLI arguments to it (except the actual script name). Also, take a look at the CLI ([`app.py`](/stm32pio/app.py)) or GUI versions.
+You can also use stm32pio as an ordinary Python package and embed it in your own application. Find the minimal example at the [examples](/examples) to see some possible ways of implementing this. Basically, you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), (optionally) set up a logger and you are good to go. If you prefer higher-level API similar to the CLI version, use `main()` function in `app.py` passing the same CLI arguments to it (except the actual script name). Also, take a look at the CLI ([`app.py`](/stm32pio/app.py)) or GUI versions.
## Example
@@ -138,16 +145,12 @@ There are some tests in file [`test.py`](/stm32pio/tests/test.py) (based on the
```shell script
stm32pio-repo/ $ python -m unittest -b -v
```
-or
-```shell script
-stm32pio-repo/ $ python -m stm32pio.tests.test -b -v
-```
to test the app. It uses STM32F0 framework to generate and build a code from the test [`stm32pio-test-project.ioc`](/stm32pio-test-project/stm32pio-test-project.ioc) project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test otherwise it can lead to some cases failing. Tests automatically create temporary directory (using `tempfile` Python standard module) where all actions are performed.
For the specific test suite or case you can use
```shell script
-stm32pio-repo/ $ python -m unittest stm32pio.tests.test.TestIntegration -b -v
-stm32pio-repo/ $ python -m unittest stm32pio.tests.test.TestCLI.test_verbose -b -v
+stm32pio-repo/ $ python -m unittest tests.test_integration.TestIntegration -b -v
+stm32pio-repo/ $ python -m unittest tests.test_cli.TestCLI.test_verbosity -b -v
```
diff --git a/TODO.md b/TODO.md
index d37f2c0..07ca4b3 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,64 +1,59 @@
# TODOs
-## Business logic, business features
- - [ ] Issues guide for the GitHub (OS, content of the config, project tree, enable verbose)
+## Business logic, general features
- [ ] GitHub CHANGELOG - separate New, Fixed, Changed into paragraphs
- [ ] Middleware support (FreeRTOS, etc.)
- [ ] Arduino framework support (needs research to check if it is possible)
- [ ] Create VSCode plugin
+ - [ ] UML diagrams (core, GUI back- and front-ends, thread flows, events, etc.)
+ - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker one in Azure Pipelines, basic at Travis CI
## GUI version
- - [ ] Handle the initialization error (when boards are receiving)
- - [ ] Maybe `data()` `QAbstractListModel` method can be used instead of custom `get()`
+ - [ ] Obtain boards on demand (not at the startup)
- [ ] Can probably detect Ctrl and Shift clicks without moving the mouse first
- - [ ] Notify the user that the 'board' parameter is empty
- [ ] Mac: sometimes auto turned off shift highlighting after action (hide-restore helps)
- [ ] Some visual flaws when the window have got resized (e.g. 'Add' button position doesn't change until the list gets focus, 'Log' area crawls onto the status bar)
- - [ ] Gray out "stage" line in all projects except current
- [ ] Tests (research approaches and patterns)
- - [ ] Test performance with a large number of projects in the model. First test was made:
- 1. Some projects occasionally change `initLoading` by itself (probably Loader unloads the content) (hence cannot click on them, busy indicator appearing)
+ - [ ] Remade the list item to use States, too. Probably, such properties need to be implemented:
+ ```
+ state: {
+ loaded,
- Note: Delegates are instantiated as needed and may be destroyed at any time. They are parented to ListView's contentItem, not to the view itself. State should never be stored in a delegate.
+ visitedAfterInstantiating,
- Use `id()` in `setInitInfo()`. Or do not use ListView at all (replace by Repeater, for example) as it can reset our "notifications"
- 2. Some projects show OK even after its deletion (only the app restart helps)
+ actionRunning,
+ lastActionStatus,
+ visitedAfterAction,
+ ...
+ }
+ ```
- [ ] Test with different timings
- [ ] Divide on multiple modules (both Python and QML)
- [ ] Implement other methods for Qt abstract models
- [ ] Warning on 'Clean' action (maybe the window with a checkbox "Do not ask in the future" (QSettings parameter))
- - [ ] 2 types of logging formatters for 2 verbosity levels
- - [ ] `TypeError: Cannot read property 'actionRunning' of null` (deconstruction order) (on project deletion only)
- [ ] QML logging - pass to Python' `logging` and establish a similar format. Distinguish between `console.log()`, `console.error()` and so on
- [ ] Lost log box autoscroll when manually scrolling between the actions
- [ ] Crash on shutdown in Win and Linux (errors such as `[QML] CRITICAL QThread: Destroyed while thread is still running Process finished with exit code 1073741845`)
- - [ ] Start with a folder opened if it was provided on CLI (for example, `stm32pio_gui .`)
- - [ ] Linux:
- - Not a monospace font in the log area
+ - [ ] Linux: Not a monospaced font in the log area
+ - [ ] Temporarily pin projects with currently running actions to the top (and stay there on scrolling). See QML Package type
## Core library
- - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on...
- - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments while old ones aren't)
+ - [ ] when updating the project (`generate` command), check for boards match
+ - [ ] Remove casts to string where we can use path-like objects (related to a Python version as new ones receives path-like objects arguments while old ones aren't)
- [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably (e.g. 'DEBUG')
- - [ ] Store a folder initial content in .ini config and ignore it on clean-up process. Allow the user to modify such list (i.e. list of exclusion) in the config file. Mb some integration with `.gitignore`
+ - [ ] Store an initial content of the folder in .ini config and ignore it on clean-up process. Allow the user to modify such list (i.e. list of exclusion) in the config file. Mb some integration with `.gitignore`
- [ ] at some point check for all tools (CubeMX, ...) to be present in the system (both CLI and GUI) (global `--check` command (as `--version`), also before execution of the full cycle (no sense to start if some tool doesn't exist))
- [ ] generate code docs (help user to understand an internal mechanics, e.g. for embedding). Can be uploaded to the GitHub Wiki
- - [ ] colored logs, maybe (brakes zero-dependency principle)
- - [ ] check logging work when embed stm32pio lib in a third-party stuff (no logging setup at all)
- - [ ] merge subprocess pipes to one where suitable (i.e. `stdout` and `stderr`)
- - [ ] redirect subprocess pipes to `DEVNULL` where suitable to suppress output (tests)
- - [ ] Two words about a synchronous nature of the lib and user's responsibility of async wrapping (if needed). Also, maybe migrate to async/await approach in the future
+ - [ ] colored logs, maybe (breaks zero-dependency principle)
+ - [ ] maybe migrate to async/await approach in the future (return some kind of a "remote controller" to control the running action)
- [ ] `__init__`' `parameters` dict argument schema (Python 3.8 feature).
- - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe remade current logging schema (current is, perhaps, a cause of the strange error while testing (in the logging thread), also modifies global settings (log message factory))
- - [ ] UML diagrams (core, GUI back- and front-ends, thread flows, events, etc.)
- - [ ] CI is possible (Arch's AUR has the STM32CubeMX package, also there is a direct link). Deploy Docker one in Azure Pipelines, basic at Travis CI
- - [ ] Test preserving user files and folders on regeneration and mb other operations
- - [ ] Move special formatters inside the library. It is an implementation detail actually that we use subprocesses and so on
- - [ ] Mb store the last occurred exception traceback in .ini file and show on some CLI command (so we don't necessarily need to turn on the verbose mode). And, in general, we should show the error reason right off
- - [ ] 'verbose' and 'non-verbose' tests as `subTest` (also `should_log_error_...`)
+ - [ ] Mb store the last occurred exception traceback in .ini file and show on some CLI command (so we don't necessarily need to turn on the verbose mode and repeat this action). And, in general, we should show the error reason right off
- [ ] the lib sometimes raising, sometimes returning the code and it is not consistent. While the reasons behind such behavior are clear, would be great to always return a result code and raise the exceptions in the outer scope, if there is need to
- [ ] check board (no sense to go further on 'new' if the board in config.ini is not correct)
- - [ ] check if `platformio.ini` config will be successfully parsed when there are interpolation and/or empty parameters
- - [x] check if `.ioc` file is a text file on project initialization. Let `_find_ioc_file()` method to use explicitly provided file (useful for GUI). Maybe let user specify it via CLI
- - [ ] mb add CLI command for starting the GUI version (for example, `stm32pio --gui`)
- [ ] test using virtualenv
+ - [ ] test for different `.ioc` files (i.e. F0, F1, F4 and so on) as it is not the same actually
+ - [ ] mb allow to use an arbitrary strings (arrays of str) to specify tools commands in stm32pio.ini (shell=True or a list of args (split a string))
+ - [ ] cache boards for a small interval of time
+ - [ ] count another '-v' as '-v' for PlatformIO calls (slider in GUI settings window)
+ - [ ] Project' name (path) can be reused so cannot be used as a unique identifier but so is id(self)? Probably it is better to use a path (human-readable)
+ - [ ] Analyze `.ioc` file for the wrong framework/parameters
diff --git a/docs/logging/logging.drawio b/docs/logging/logging.drawio
new file mode 100644
index 0000000..14707c8
--- /dev/null
+++ b/docs/logging/logging.drawio
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/logging/logging.png b/docs/logging/logging.png
new file mode 100644
index 0000000..14e8a63
Binary files /dev/null and b/docs/logging/logging.png differ
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..587b4a7
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,8 @@
+# Examples
+This directory contains some useful examples related to the project.
+
+## Table of contents
+> - [embedding](#embedding)
+
+## Embedding
+Refer to this script to see a minimal basic setup enough to perform the most of tasks with stm32pio projects in your code (i.e. how to establish a core library (`stm32pio` folder) in a 3rd party tools).
diff --git a/examples/embedding.py b/examples/embedding.py
new file mode 100644
index 0000000..a8e0fd9
--- /dev/null
+++ b/examples/embedding.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Import the core library containing the main class - Stm32pio - representing the single project
+import stm32pio.lib
+
+# Instantiate the project. We can pass parameters at the creation stage ...
+project = stm32pio.lib.Stm32pio('./stm32pio-test-project',
+ parameters={ 'project': { 'board': 'nucleo_f429zi' } })
+# ... or later when there will be a need to do so
+project.config.set('project', 'board', 'nucleo_f031k6')
+
+# Now we can apply any actions by invoking the methods and properties
+project.save_config() # this will save the configuration file stm32pio.ini to the project folder
+
+# The state can be tracked at any point
+print(project.state) # or ...
+# Will output:
+# [*] .ioc file is present
+# [*] stm32pio initialized
+# [ ] CubeMX code generated
+# [ ] PlatformIO project initialized
+# [ ] PlatformIO project patched
+# [ ] PlatformIO project built
+print(project.state[stm32pio.lib.ProjectStage.INITIALIZED] is True) # or ...
+# True
+print(project.state.current_stage)
+# stm32pio initialized
+
+# If we do not setup logging in our code the inner logging.Logger instance is not allowed
+# to propagate its messages though
+project.generate_code() # we do not see any output here
+
+# But we can help it by configuring some logging schema
+import logging
+logger = logging.getLogger('stm32pio') # you can also provide a logger to the project instance itself
+logger.setLevel(logging.INFO) # use logging.DEBUG for the verbose output
+handler = logging.StreamHandler() # default STDERR stream
+handler.setFormatter(logging.Formatter('%(levelname)s %(message)s'))
+logger.addHandler(handler)
+
+# Or you can just use built-in logging schema which is basically doing the same stuff for you. Note though, that only a
+# single option should be either picked at a time, otherwise records duplication will occur
+import stm32pio.app
+# logger = stm32pio.app.setup_logging()
+
+# Let's try again
+project.pio_init() # now there should be handful logging records!
+# INFO starting PlatformIO project initialization...
+# INFO successful PlatformIO project initialization
+
+# Finally, you can use the high-level API - same as in the CLI version of the application - to perform complete tasks
+project.clean() # clean up the previous results first
+# Again, disabling the default logging to prevent interference
+return_code = stm32pio.app.main(sys_argv=['new', '-d', './stm32pio-test-project', '-b', 'nucleo_f031k6'],
+ should_setup_logging=False)
+print(return_code)
+# 0
+project.clean() # clean up after yourself
diff --git a/screenshots/logo.drawio b/screenshots/logo.drawio
new file mode 100644
index 0000000..21e08da
--- /dev/null
+++ b/screenshots/logo.drawio
@@ -0,0 +1 @@
+tLzHluy6kiX4NW+YtajFkJp0pybdKSa5qLXW/PoiIs69L29nVldPOs6JCCccBAET27YZ4PEvlOtOaY7GUhvSrP0XAqXnv1D+XwgCwzD6/AIt158WGoF/W4q5Sv+0/bvBqe7sTyP0p3Wr0mz5R8d1GNq1Gv/ZmAx9nyXrP9qieR6Of3bLh/afTx2jIvtvDU4Stf+91avStfxtpRDy3+1yVhXlX0+GCfr3nS76q/OflSxllA7HP5qycxWHfv0zxWcJy9BGy79woVxXsD7mX4j4/M9Bn/9VDEPRZtFYLf8rGbqnOVmeLmIedVULpPv37c/QqPAvlJuHYf191Z1c1gLF/CXy3+eL/4d3/17znPXr/5cb8CHjsuw/dyLN9CgW+TXp2v/Af0fZo3b7I8s/clivv4RbzMM2/umWzWt2/k8qjeK/ukP/fV7w36t9TDAbumydr6fLn4FI8s8tf6zvP9A/18e/Vfn3sOV/USNK/2mM/uim+Hvsf4vhefFHEv+zVHaGMzB90v6zkqRy+s9XJwrof9D/d6k8QunTDAwC/Qtlj7JaM2eMEvDu8XjZ01au3fNQHn5e/m1VoO+yzkOTcUM7zE9LP/TPPWxete1fTf9CUPbnH2h/rEr8y3i4oauSZxpO1D8GBGnOnw5/HBLBnuuorYoeGNoj/Wz+0+G/jPz79bT/dzX9vxrI/1Pz/0d1Iv/UJvE/KBP6n5SJ/v+lTOr/rsxHPyN4WXU/QMOC1VYPvKhRnLXmsFRrNQChxsO6Pl6Nsi14g42Spvixg/8i3/zn67+MwfxRyDoAo4iW8RcA8+oE1sP+PJL5qxX6q+V5nUZr9KDL7yUijn3xL4SrvqxhH9BbKgbm+dKdTyl8iueVBj0/xA/HBM9vTh7m0nteBN+u5S2YfVmQVnzk1x527RJaDOMp3tezEqrN0Hz/F8KSJKkZJ42HOl2FJHnDrvFgVnAtTjMVjYJ9PvIoVZHzbXTOma12eA9fK9L7dh+euZHm8wNtnnF6gIT0rue4b+a0GT+Xcwre9HT/+dUzGhc6DKZymDoyTGFUER9IW38Z8YZRmywx1nQoLL0xL4E+KB2a36jOfF/PyPsPzLLkzVBcZJj0V37x8Pw0gmfQKfdgPftJyaJhQgUueII+boQR0ENh+kwJ7aFhmPfGQYxXKZSCLOzGdU6lFFZhINRjWKx3ly2DrguWn7rcr7Fp2TljsabrhG7OSMX0yYdclLJ9KJKuk5rakYXrpoQghNnlHfUy3aiMRqSc9szHwAze4rNLC0TJ3Kn+fNqc5yGh3NNNszyBgc2fJs5T8UV8FsFSuVyGQr0/viWWGxCnuohsE9MwxxlfZKCrWN1L7XyjWMJaywFrdcaFAkOr78O6Fbm2UWRzZUo4IAc8C5EkVX7GjS9eT/lgiDQNY9tdq4mwqDtrZkvnxT2uw7pxelB5DjEuezEXGzw3H7VrXA8lYIkQL889iJ8YNipu5O2oZJYW3WeP+7Cw577HFdOfGwZSsZgsjfOez1esA/YElF2bxp3S0yV9PI3qM8jmGb1oe+4JmCyqrX1pFT1QaTZGT+/Mqey13BHuegW3uwuOnKpmN+085VvpuOVCeT99H27y6Z5o9MCXWGUfxCzzMgTTodQse7/Q3s3rQIDRTSb4mmo3LSW7n/vOZI9RhFdT2s5TrORXMMuhiXmlt9f1Ge35Zvlju9AtlAUS4evH242T10Qbb1ca8me7Y12tAN28Wl2YEqjtgkjIxthQNGfV3y0tYRKCEemDSaomG1irERjKAHqfwpXdOwSWGOUaGDbHGKhZL0AQsvvhEYNVuPdisTdWX+wm/Jp61290P/GRYj0X65ATa+gKe7msTbjSRSHIJ5Pw2skIpkIns+5fLL3WHZMMeQ1MgFz6oFusT50D8Taoazl1h+CugqIqHsn0HRTI8YzclvXMiR6vPTptnHmgEHu8QwdPmSnS/RzeCfmxJFFjIYzGa7QzpMSjECPLbbOTTfRmmOGopOJ6oC/GNooego8zKe3gz4Gs2IhTeymTWOx7LaTasbyl3MqPPa0Egts7ADbYRIAv9MdKlDt1ig3Hcw/Asrpgwr17jqe0fJ73P3VYARhIlDjtJ8rJtqynwcqmNieBBdiPA2YhgInGfntlPCnZYBClPEnRR/YYnva4qUnhB69YfMMerGbvSD5vcUe9cF5H92Iq4Kb0Mxl2qWOjTirJwd2Xq60LOX/FTUgFUjoP0+YORBkBsj3BkRX8g7g0cr+9xMGqDm4g2rAbTCoU+A6lLQaKlMjMxRlVRYGzhJKdQC+RL7iK7OlhSOrs80JUVUEeQxUJ4aUgm+/bHiORUdaEzqDaI4FhAmsLnDpzJXIwmlBwdVWfp8VsIyOdBfNBGJFUBBVRVgEB9hLep2XxFsEdF/MNFdZV2CxhKK8QRYeZng6MNDG6jrEExp8Eb+5Msgt2dx8uxkoxa4oVqzCF3DisVLFoVQZWwX0K4zMIkGKZiqy+RZ9j0YtN3oXzLguYuVpGSssFZ0VCfBP8/GF0hJcbmYaYfZBdS8+DzAtsW+Ndk0VV5lAZZD746ZC8wyQxkcZy+JBfB9dj7o3ZKsa1h4piWn5QABQ/eeGaSsK9ueHDYUP+ClrHyrpyFrntzVIfNvQYp8nb4TNacFRK4evKhya0NaJodTbWKyPknUu5EbZeCzMUSCjpB+URRH0Twgyp/sDvtkvzO6Sag18Obnk0z7J4ppOkTFOvIGqw+XXgdh13GtGkH6gIezmYhcVjpUWblciPGRZhzACjiypmEERMN1NOTAPzGSwdD8Et9L50zFrpuXFTDexNFh7KsBszZ8pIqTe2+UfZl37OPJGe5cLnh7xj534+l2LxxEBRmFl+4gRB0oPH7JTFcrSibUsh5FRE4hbh3bywwdKDGjkZvgo8sLwlnAPHP0GKIoiiHXu8M/rvcpny9yhMPUDBCP4YUwrbKszcH2y1CVgiP01q9ZND2yv8BlZr4qwySoKTDs0XVgTCccZmtY2s0aNRkpomUs4hlyrjY7uWqKsaxN9lGDJv6GEL5ObrA4YAVAq6GMSnlWef0DOWRo3hWguxvqWnjA5ZqE3Rigqtu23TWo4D5NZ26LmRrRpJVrSjOCRehLS3dbW8QWmBgo128cRYKNTOLpa0JdTPoujVl/ZqjtJlWk2E2qiTozAKi2/HIqGAl3MHQpBD3m9me9thLZacp7xCs2WXpbhuZdaJxZjufNbScIIL2WPd0NA7CInJEFrLCcGRWyXl+yERF0uK/ZJm1248KIeVByLrQoLfDWF4Ada59ddwIRy62we38OP43pWvhglLlu8tnnB8LvFNQbCDrM1kTm8AANxBSfXNmnxD8e7tmtpFdYAFs7vZUWp85Jtd46tZAs6iqgCB9gLwCb/H6rwDogICvkBc5nfyxYdKzteTlerumnEnI34Zm2JlRUCKJ6D5jahalsm1wasuCrsUHRB7FNfgeMjySsYT/MAxqzF53Ud7VGGllEr/qfzLGa1mqjzCJ22BfjXBtzlFVeQW532aMhv6opm46snvbcA8iY1LvYLhhx6mVTrNG6+34dLrStOOjn2/3kszX73tqK3afprWSWE2HS2v9QnnHlka2F/I2m3Rue9QJN/aUkVXFb/0BRCB9jR8CrfmRl46F1fQlqca//rNyd3PI87vXu7a+1Amoyqzl326sag73/YVjM6avqFl4q4NfZfUyLwbbIKvXvqWxBB8v+jXIkZ9atbJ6QY7qvPPPo3qVGSTD0c5UnnXha4+EmFnbUNRqCCqNBlR/pq3rkwgm/SxNSmj13feV8uD9thV12dpp2xx6Zsccl36QpE/oGuYQqLpVnQ6w598lH4iPwFKB4HGFpj0Ou5MMrCD5v1FGw+4qW1CYe3pliVEO7ALrZVS3wWIeJiqHn4Q181WzdCQ5TbI1x1mqH4m2IgPx3cJvMmOc/n9ecz8VFWDa6vQk77xHgfekuJ7niRShOjrB+Hv4xt79MSvzL1KGwojgCyiHen7qwbYX24MBWHzE2SkCrHVrWdUGEa4W5qWI9z2kZGi5HJsckXC6l71OpxyMNqn67rwK0pjTjy02wTCNpM1LnRmJ7niaRGvZr6Z60jjMoSirry5G+Gj3/z13SyHGi+5+mTlSVx9vyXhDdEZ0mK4v5qZtBL97qS0Lp+Ab0E3Hu0lcAuIJKO8B5NPUAxItctBOKG8gD0tWWWsiP1ib5+hL64bXmWRkyyZCJpiCWXtCMngSqUZyVdgt+WAbC29AnoAcg0vGMLRPRJupVy/fIZkS/EOtksXFYwW9mF3am5SvOFEahULrZNyhVVL3ydOqnJw3TW/zYc5MDgmIwV5yfdJUSJCufstslzNKoS8NhpXtoLmVp/oJQbZqzw004Q6x3GnlObN8B03Z+kureb5S6VL9tQ60VeYJ0iU4YH6jgTvAqh94TW15Ia8b19y9LPWkdwmzM5sve9MT16IbeIvOnxSDna8l+vrvk2ZemfX3MAS533bWIzy2U2iXF4q5IpQE0vW151v8UOVkc+Is36uhNhrV7VIxfF8h8MjNvp7IjLq3tFd4NDhHVkTEFCAfd72l5aZLENvOPGT0+7F68wp9ThzXR0D73olTZu91QACN3z5zjPjQsW95+K9LPhVm2/4IkSfndbljQ6T6Gyzemup293cRLznF2A2WB+1JDH/WOk4Xdmk6s4BS2nOPP44ywRQoH+CnCHwo5qM4vedjbE1J+/4NjMXDCFGnT6DALxTRBPPfjaTEnTESDl/JVCOyJt4vKcA8PcpYx7Si2ph97qdXScc+FZ9aQ0I6qBRdK3jfI8eKkyuANznxegHwnXnXK5zSiZJBodk1aJguQ7oT79/oLfvfPM0IHxxXREVJZ3s7SIk+jb9D13NKLxnI137oLLJjru8MtFmpp/ffKGnwys2ZRAC0FzHAJnkGcw0LETn50V3b1jZanHFpYcWeX4VUcdMv3KIRI2MMuFxc1ZE36JP5Nvbb4L1uCCQ/IruP4XM8yZ1MCxr1pOtDTI0ZJSTkq+r6vApCkeQTACXq8kFYG9/vIGkrasBUgoBxTdL8XGNlcQn4EVIloZECih0jK87SQMweHohMg4a3/u5dKSRZbeWmyrItRqVVPa2OkdVhQK9HTNfabPSQ7uZ6tY1xkCQvH1gOH5B559DGzwqs7+MYvsIZcmAVF0fxuEwRjODMsXm1EP7LG2Esj2oIHNX7rMy/cYY31qN+UjSxYQ9Mb1Q9k9gvhsQRJWpK/xI8keV7UjpEwWn2POeNwce367Ga8JukgWGte3l43vs61ChYg65F2bl1aGcmS/klauR22X5o/D4lmS5eDQ3/bVzzqy+M9h1vt7+RTeoiqJy2LolhUhio6DBnAn6ySpa7JrqJ2tiFWgMuXCIO8sOXu2YJk9CJcV2vXwzdJHfWlqPpWzAF2L6Kbn2qB0js3HwZGNdG5Y1I0b4c54AionQ+U8gBWUaAmP2hhkS3hleeck58nvw8RrOy8/bv4tfq5B2IZDs+yt8obQ5uVjkkuuQ3OKcz6to3yVsualFxC/38WQiFWS+c7regFQDfVYTQ+RwsxhCm+6LpHqzp2HxBkZmdA8wdgnu3pCUxVTvZfrdN57MzZpKgJwNhVUK0MH3XV1aOxC+GwJ4Uz9hFrwpa05BfYX8fr75FbfEQAI7wNcVflrLHdAhFt7KFAzx/a4hDAPIngmJXnLf6KM4TpEtesPr28P2DY+jdgXJ4MIZFbSBsAEVBspCi2nbJJzSAD3QlKZJaLUffrfnIk3sOsREmTlB2Ks3E+rL3gZvfbTvATP7KmS3wiA2sOuYeDgCCts7qiP64oK1TXVPY9+dTFILQ79PaNBUCG1yvcYHz3fS/kURSwU9MsNObDQV17w01MgjEsfyCVRasn73sj3mV8zMPVAPAwk1LuMa4hgmiIf8/TZPPMHgKffhOxf84k2yjFPgMOsSRhXYBHa7h7A4tlJUX34bwk856XwE9b31orWPdge3e8vW8p7OZq1dxDvPfXtXT/BtEeqKLz+zdyQXnUAgFGcsfVuUP7Z2pqYYVMBcU/jyiPFtd5CqAjBR+9YvrVrk358Lt6Lvi5h32Ce3oxuPaBknzytkwnOnJ7bPEMjKl4rPK30Mbs1lYc188gtSUpbPdThz8IwfPEStQ54UDBQgEyWFztPpPeQxcFFE2y9AMTyKsYeU7alMJiuyN+lCbfVB7h5OAq44JJTs/2CV1NMj9SKYh6q8B00t6hcvQG+gpxMdkEqmsGi3+cvc2ekrbZAdnMwsXClWmEuR3efNrFBJvmX8NDN5w98bqF5mnQ18J2yQkfjyzyvAz9kCB2DFSz6V7z1a3lSzd+i7E2e1d03y1Sc32d+bTRuxjrUbkUSNqAR+a98/hXBsa1ThzPN2PgbSoUFIENM7S412HmazBX1muj3G3ZyqcusiPP9AoZlGWwV/Cfjrf8A8rErtakh1d3KUO2qMOngLyYfmGGuSoracdOhCUx/TOR5qhc+jYSrkvvZNjQzyKmUgAEJhbsm8XlC6X3aUfnaQpA1JXbdnKgGDfwGUAYVaO0vIK8tS86C3anjYwYsGVUJUpuLoxJaOBroDSWeW8+HX6NEB1Japfj7BY25KzI/HYe8gpAf0AnyllEkNnY2cxofCj0t8Ulls01qY8M2Hm8ywtNTdTwbJuEOhIUVAN2H5CduB8639VWBzHaISSEp9VIEKtx3K9Kv5H8ixY6luI2kd8a3Modm6vrXealRy/eRc0SJ1Z0Vq9zKdp9szenKn99d8exTrn0CjfENqxddhI0O4NEVS+qOqCDUNFr/CbFUyQImqDZJ2nBx5LO/3k8jkLbV9XDE9vt/Qe2wafW/ObEzbNam084ZVGLm8ttBTfIC7sY32fSSM5U3Q8ghCJpSzoyEVUIjWpq7twhN9hVynFmRVZcYjw/j1rWIv1oM8Dsd1AeBFkfondRh8l98UIfsWknoikmVrtLjLxwDh2afXO+3I0rxPdEcbaqOZGkKNr0i6fpUnbb8DJvUmiTGvULoq5KDgpEM3ymsRMcUgrIMWpsBd5wrUarc31+vdEzQC27zMe0BDcQ9aozYkY7P3WV4SR0ffBdGx1y4bXKSp1JDHAqkB+HLy+YVV7WITpWNlszkg+uhi5HcEPmUqLO0bytPVVy+vi4kpYZaB1jQQ1OiepIjsCnoVTh6fCvWHhTzR2eOoykQoX5UnNvXUqvKdMXoBlwFF87Ol9VxpA+vddXYX2eeZoxOUTbP+AZRwTjFrz1oChAUXlI7rqcvGTDqWqHwtZ7p4US0C0/UsAMXGenuAQbFwsFO/eakFFlWShFn7aZLzaLTTQQ2TGwa2QkGHbkqvW6B+Ay6vNw/vuo+dOmk1Rzy8RaFMCz+klKskgBJVL/cacLQviUIx+y4kq86csK4yyyhg5lsiTkhjbPH6KAX/eTHy+mUATVnZmbX1gOFAZebAzYtpqMSqDSdRMeZbWYz80rVHxbgCnxj7pEMhzroOz+QMOwuK1rBV8zxHxlLm9QITXUotLDgjklNPAAvmKge3UOa6CrZlPhPfeqzUqDXkzUMgW0A9hScxniGFkrt6TDXXH1RYN+EwRDN5p8fRl1ReSlSBMaVRMjOnrRKXvSTMkr6v70fSxz0q4ip5NQFh7ysfQLdVdh/MkyM+du/UICE1vxBeSV6pXcaS5wTI2KedlwVH25S+2i1Jd4uxGS+gOHGGsvROFvxsfe3JAesjNpUMp/aWpucSU5jvxrWYgJSuIRSCq9ucKznI+8YSrCENilZfTYU70f7WtF67asLeLkAhXE00oa/9csY2zL5oC7xviFdLhucWSaUSiXjSJlYhhcbe7elAYMW17yktxARMGVe7cw1QQxaLyWu/QQnXDeKSgUVfH5mFks+I7h3nYPk8svtXHju63hdjGzM7uw9vt56w4F8/DCDGVSsf2BDTvWI+eRYNYqgcq5ZTZqXzarXJwNbGyvjoPSgRKRyRk4imVlNQetcbrJH3veWhglMI6jU/THvLdjw30g9HoSZJLpaHvsvYcJzQ8MT3rX4HOtnRdYxt+muhsdPb0VtfafMIPg2aRvTwa+rXPXOyDV4h+z1TNIWSBSh47hDUZdIMA/ZQ4NRFEYNKQPsISjuf+YbjfX75wMja1UfXA0RwDc1ltmBeJyMlDaN6DIMKzOFYbDAwB1KI0MXoQSE2Q6gpNjVIzsSSjbwHrrUw486LZpGgDABw25fKW2Tkj+sNiWvJSBmlRUYKFlvoNyNrcnJM32KLaxkRBsrAMZPk8iSfMdU85aOeSunmeF2wpFz/vrts8iBQuwI5mt0qrR9x5TB05YMs8JAhpUbY85hmTYzIFuAyZbVKOlV7TndRV6gaylLDPbqQ0gHHaryAzZiiMj5NEoZn5Bt6guRnZ6oOpZLdRgEQSwawsVK8sZ/KINMP8c6NkE3Zsi+WmhW+TIbUwhf2tseRLVvCi++lWS+QR77Yn21BhR79/clFh/0LrA9wMPZyoJi3aT17Q+8Y0CGjIYizH6dzGKT38CQF/a5BzbF7NXLIt9es5vvgB8tI9YvUyAC/58YQ8SPqAcji04QZtLxeXyLLbXnbbYzrazzbUgLN/ZECvgN7TFtwePAQK0Ln7sFDyh5pFiWI7cYW/WGIqkhS16BWTZ5JVPywcqReFom6/M/oR40V0Wtt86Z7ri8j6TI4fMi0sfljD4Mi1iQShT/Zkep/HB/9Kai+t4xK1dwZsBm/tn26u40GuUO4XXSQxnKd2Pq2oNA4vn/AmQ3yiScJIUJjgS7BpunG9r+4zfIfzKgxWNvzGRf7UZYlIxJeB9QvRPzJiZ+9K51SvGoMr++PFeMgorZ3LslN+jV5tGJeOiPoCyOfykM/GBgqMMpiwjejcsy7BVVs2CrpUmyLvB54FbOFj5kMQhu8nmUhJZyKJyHE0LsJ5AbDwmMYSx7Rc+y2z5dfUCbIUsX6zb7fnMqBPYPkGump3j/T6qKOHjX8bb9W84Jm395S04TGs2ikEDvKmrM7bgrSsM2f0H1RM48sL7gBSTxv4bbbB9srwvyHZVA02YGduwBFL7lmuFgRw8PJqpgSM1smXjNWW07xQMwrRVph8tTQGNuemumPwVq6KL4/He6P3/D+AIbbFR9zmxaIMFDnpr+NNg1w4mYlUNKDQ14QRgZdv5EAwjZ+RIxzI00dXTEywnS6SZUhxU0zdeiKhF55kfIVI7VFGJfK+zCacWAFuslQu5n1jFWPtq1iSWZx3QOBnwGVFdEB2aj4GrEky0K3SPqbCNNmmAjoi3gTTqBzTl29TnJYK7bY9FXpY4WcEfCEzwZw7b1z2/565IZ8927muyGehzo3UE3N64lOZ6AS7y1fPYngTgWrcyquZUl/aVrRyJpScgd1TBq/9+TEi6yeD1M2lVQuaVwb5mcAN750Gaaxvo60rqnicsockxBAqh/OOKXlR2TAO0QmLnqBqE2YJ6h0+SNx9/KCkzdImvaTgzUVHda1Ikc1/ayfM+LbJfNEMbm+P6l1N13e/CaWDhhyun/8hwje87XPkDqD3dXN0ylAgNAmzGOTx4qohRgN8G0PRimFwRiST9gI3mfCWfofyx/G5sE8ZmLgyAA1pFFD7rIgmGNO4knYqEcIIG3aIZNk+vwLbXnAgsMvihB97SAVS93lxzpGoC3w7D3oPlsmwQfz+0Vl/IjFPruGvl0qcrnGEn4bfUErnfhg76s1uue6g9tUEnalYjGjL9dEtmfTeQXulykshL5Ch4HV2to0TvkhOEyh8OcVSgHxaWzp36O+mrCGqki2oYQfdhVN0fTCUe3C96RLds1tcMOhDq2iLq2Cwf1rgrZbKomY6uG3cv09OvfvJ9jS9w7Q1/jMaYwRrEgecmp0YhWjX8h0lOrv/n99/zWb7otFXvCss4RSmSHUi96SCy9Dia5CBwfy2kO/HMOKOtWa2ZVi+Oc4HIvF3rkl9whFXtg98hhS2T6MitrjXvtb1gFCrypqj2EX7JqDHeo/V/G7kjaT2s5o2Ycvfxfv86zE+24J0u5xjd06z1Aq8jNb4iO1WOidoS21a/jIJPLozfznzKifWdejFfllG4ulEHlwG/c6kA7I+2oGNjn6n08U/vHE939dpyn/zJ1wHxk/T4QC3/5rJPfY02eU51lm0rXPM+zyuf7HCM+T/ict/W0lx/6jr/Yl2M9dT+9f+5J+7A30mgF2YsBcBYZxPjb7lQuWDKFXz7qAOuTp7H1Im1XATq5HCTWVBj78LjGGGTJMED6VXzI7773fxltXRuyUQPCnKMG2FWx6v06S5ajPrIW4XQtpNbSASPilgLcbitIXgpzr1IVE2Kd9iIRzlt1I1m3ENk/klM9TNhNT32c+BvvgGAOdehbHWpjAd257vLM870bPkyTxNluki5FDuAvLeHFvBhKHzyyMITOAQsBL4C2vBPFf8APuPJUzkCLtnb7OKLPePlMcmGxBiB2At1n6dQ7mn5xol8yUb/l54diDkdUiHBVRLmyDZbDmhfGBX3l9m5QAyzgG//AkCBe5xkWHIjNF8lYYfoiS8eF/ZQTQnJGlMufrgEKmzu1ctRZO8wu/VkLAe0PPIO1DtOoK92EqXsfAXqhgDIrMFSGLgL0FgEQoekdPqmu4+E1dhOlPgL7F89qVY0203xqoDQWHubaXmtC+5OCTyJ2ZRyoiZwUCfycIuxS1XrigU3RPyHTLt6ug43toQ09bazgsI9oN1KhS3SEhgkl8LkHY5z434Hh91I3t1xlB+JsamkPJ2buRzs32LW+sq6ASRwPPuM19H8vi2x1YN3Z+D5NcQJ2AqOxfCKnUZV9k0k+ZSnXWOK7i7TPmk6FvnxDFcQLMTjN3yPO7EzCb+bsQWZib+4qP2a92jG+FQy8OE9g7qbgeMu/RRalroWlSRXY6Ih5wWFuSJJG775M+Wnc3Iy2UaE7txPzJS915UrST9N3EcfcbzvaPbXodmryQSyHHaaU47qvQHwInws8EStH9rNeL4nyOV2+1rzu5uId2xB8E+JGYPyYNshTvnWrEw38HkQaFUdYqUJnodo0+RwT+tApRPibuZnnwbWTuA69z2Mf7EkKQQq/x166RrUM/2sT7ppv/LpT0STWe5y3uOoRCDATkI3va7iD/RrUPRNEPPDRznGdL18bAp/HVHqgs/uK/A+S+tsWMa4iMxqLK+05fqgpOOEtv9ZTBKQ86WnMpAbPuJ9Gx/LGJL5jseKOg18mvXukohk4N66+rt5TpOzqVVmpeCG7Nqu+gH1UAnWkfhMefDKvPsl4UKRjHcXqH2xX1fbME3k8chKTS/SMSwotwPAxuEaavKu/DG8U7t0bb9s1il8z0JOvyRWZvJ7eOBIGgOijJzSDyw78PARwAFC5Su23bXopVU9fHvt75JYpm38dFol6ZaNIyNCbgUuYuq3hUntuJcn/xdwvOSYnjVAkJhJ/INbNFkYTn26sbLckCUOO+KxGJ1jUNrpSGYZDl9I1AdJju3ySmaOYtgO0qh1DnriFSb27RvqqcXONBsZQKXvEniXim8BSskXoJcYgNjpnRLK77uKN2JHzEUaJhIQf9zpAVng73mNSICKQAvljGT31lQ8CiH4Cq+FRUOjw1wuWIIuJkxThGCbo6B7yZD79u8TDV6xOjZ4095hHqPulJCmyFCVEZ8Jv1lYox+8D0/tohy8MwdXC7afwaK15V0QUVaqCOww4Hqr7BMCq0sVSbMd9MO3STY+9/9TpVu5VaUEsDOfvrXJdFkcFREMMw74doJLcRGi+wA9ifXm0pAn/6QtGmgE+DRLlu5xDUfBIiiqYV1LxxbLAcZu7015zqUMueitkcPu28IwLfjaV8MfrruGrevQYRUaV+GnXTF6iLT25J2/bcdgCWeI+iJ3JkoETzL6zs7yf2ycaEcklfGGMZ/NgoysCb6vy4E+URE1LYpnWnyysHdXLZXjUPgo7S3ApmlsUAaVVCKLvUy7Ri6aYMer0VPwEA3FAXuY4v3dAGiqbH+jvzsm9k/ucDof5cVTBzfAxRy9ilLYT6xYS3Fc60yj5ZVRReMVHfpfIQ4YV05OIYR9FDutlLpXpaAdwhJCisamqM+CndqehO7j8zVhO8qzY72Wi+s3B330Lk57AGABtiRsgtyxoYgnG3skdYRX1n5JDFR8rpxclwt+nQp/RvorIuaaBL2WXlbMs2ThTXB0MOUPf4wuvW3g3P4z8uQfLcZ8EaNszK+QPnxntlaV4g42VZNok+P9iHF57Ytltf7XbLcnk9EYplYWVmmtH6XkWVvNlvwjEC3BXf+RYom2/wDxuKlgxSYEHZvuADAb4Xr6z7gb7FnW79534TCNS0EBYnJGHR7Vof+le9LeU2pnmmyeH2BLbQuT/1TJKE50NSrOERODPxrUv3SvzNAxec2L+Q82TVjkR/+24PRXvVlGAdZ6cR30mdhMQQedhadD41amtIBVOPPzVr5DzwXRZqF2ibUxuIQwaFaToCCC9+rUUtENPcxyFsy/CJ12WCrY99fhTXX/BTA2lK767o2mspPJSHlPD9ODdcJp7HYX09hhnx5oFsEBewYHP1P6tB0fJ7eFQbMCwBipTuaAogkTkHQkaP0ZPyLYEf9Ic71su/32+qb5P9iiyKohQOLpDVUTK5mjbx55xlOWLrKRVk/LjaFtZuHF3SRVHJE7ix12Hp9G+xAIGoiDDrB6HBDLbr+cK9Q8BI0nX5nxLMZ8qi+76xmzkCTTPzNAUBJQyCJJ0ZM9PTNGnIFsG1yu5CTDoT9zxPetdXmkpIM03XLeRsrfM0WQ8Vcv2YJJKsRucvg/k1myR6fTkByUgIx86aKsUXTlNLib59R/UjJdRTmqZXGr7kaF4lhMCO8MSwoUE6rfh+4bNE+8eKvksSF4m2XmwlxjeOj3t386apc7pUq7y/m+r75zRXAv3Uzeub3gBuEBAE4QBczfoMHgJ+vq23g2zfg9QXU8O+uxWwgYnfaN/3nYOn21pn+EL/qqs36mGseNrYfQD1w0exatM0DU10AVWGcN00u8YARdDjAFmlaRj02lzVd30ei9Kx3tJV5/pdr6rxMzH6vCq0h+FvolN2wn0dTEYa22ywmLbfbblbGVukPDP4/X1Z6O1IZmAfJlqlxIvgE407EVC6It5c3ll1y1rW7ghGQlk7J7qhzJQc4LNA5fraPhFswRJ9WGZVTSlto48+z7IkX5HenN/7W3fvChVCk9kxnEbfGL/amMG+xTF7pfM8d53nkziePB524jMAV6+2B+TCIpl63PpZQxq76QVFqanNYnffahzH25av+6KheO0vav2mFAznkMZqfG4DHAYntvNHsOw0Jg9XrygQ13MZgCJpu1tpszwDPa2UcrgbOe9PDsiV9rrBWBDEyLTbcN1UHicK+2SY5jpYzLDj3O4uC1B1xJ40BRkPYtAFOGsYSl5rkEaJPNFI4FyMmMDnB8T7gf1+N+E0u51PdX4ydd/rioOX7MSnwsYn0xbntb6so/mciFKgsbKQrUK2vn2ruedc8bE1W1sGzjQs/Dei6OAUbjXz39XxYa+Ckx6jKgCio7N/wLdYhiLn3sPFP5TEa18IcrMG9DKbxVmJZpqznopggPEB2Hi044KStXITHtL7EFIeeuSTRBoXw4Ime7BlhP2vbVI42Wx8lkcck01+V0eXvLxruGVlpnoXzEhImehNg8YzKYI/8d/dtOWNNfz3fSjgOI2TWAOzwBfkxHDugenuTh8un+/3uLe79D8PCVPnr0NDHHMQlFWwvCgqblGAmD5v3XKQBYV61KpyCWktf+NZQw4XId55n6ZpfKPH0ONzQa7X9czwsRryQWNNvbJ4QwVxwvdPY/zkO7QohjwnnV/EFNED+kJ34KnxFP1FjVGku0v0WG9F9BTXfvplRh8vd93l80z+At3z/2P9bmxpaEtud6bEgnLX5Dwrioixc7hqMEXWre/qpWc8csubBxFEKgxd+0qREFuXX+myKI1cavx+T0vjf9+OzoZmSM2kRtxEizmp+F2VvxD4aqEkTvt0VQcsmJM6vN664L4gDMrzHVqC2jo0sSbja6WEzwn17gaV9LAPFqwbrSzLDyj+bKhPxO8StHkHxY3gLLtq2cGZzRG4NQ/y/ScYHX9FErDN/1DD+iMQRX0vfCW/twz7WqPeq/Bwn1zjvwOkF3S6tnqd/bzlcCSg+ybJTvZ9L8kpK8mDhwyG8lgDsX3eNllFX7r/OIvqt7Yc26WdR6k2Y7Sum3Pa6e+LionZoQ9zi1PgTHQ4PQzpdvrahme577HgY1BFw32vQPQM91bPbpLivFPkwVH0+Fe4P5+tWFHDOmjnwbBa/9O857eBk6QkPTgSRh0UmcuQJgwXv9y0lXcy6EGJj3A+79T3+7Y1MXDigq/rOVz4LlZAaTDBb//ROSsNVF1b42B1fL6GgvK1XtxRX4Itz+ZKzZngl0q8sHXnCo769lz9YSZKM3wIUtvRFSlJDBwmyY+/vO28qZSuNoVB4CoTw3deauCAe3+IKMIPevPnXBuagCCkxp/2Hcqq7KVDfEMAmRsc9/UqoTnAL9sW7qV3GXS+3LaF81mugqx0Y7mLaDJBVUNa8dCsvTNBys6Qo8GqHf0LX7dsf5nltUmJeM956fbklcBc4BjT6xhEgkUClSZbgyaevDiILcH48jClvMwHnPNAKf25B8spy09ctt9X2nIfF6mlBKJfRLJEKpZO2KK5d2mYb1wASPG6NpeXvS+uoHsFU56x8OPIWR4XHTPi7XE4xiTekjTq1W04d0NA5GZ7CwFEyfux/xRzD7VnCul4fK2vPDIRuWxjmQSVCsq6oM0bXrgVGCunGKXGRHNWtmWTjDzo32Au9EJVCGY51aWvwZjE18SQvwopruNJOgG7S/ChAaf6MoZsr+xcdS8iWmLIZDH0dRRvDxx6IePrpEs2RHXeHjH+yE8szHj3QJlPgl/HC3y+Ugz4QklgthB9PPBrF1YkYic8axpAMMmyqizPiLy9dxaQIrdUDsRN8gvP8ITr7d4vJo+lPhbvk8EiCZ5poRAjM5ScSRBbKpFa8PH1FvTyTvNFwTCsKPimAGZvlpyeZU/+aEcnYzpQdRHExX6/S4UcFVNOSmo7j/bdsTnOn0rAGp86SM6UEa9n10S5SXjtxLHPy81nxGNbxKWBFFxkf46s+gdenfboYzMjhUw6RPLD78IUfuPFGWARHcYHAcbtOwlba4oq6vqJ336/64UM8Yp77Jdx91bdYWT8EIM6XmveNBbwWUSWos+HN6Idva67fuv9Pbs9sin5USDQFXV9387eCY5if63BZAP6TqUI+zmY+V1kJkylLLk1yZeuQXMOanDdumb1BpyF8TVD5snMhwo9eWdrd0j++AKkn93FyIhimKTJoFhre6okWbWsJA0ZDMdZJs4whCEK2w7R4nHE/Ulmokfc2O1cCNwK6A5Hmy8H408px/fXLqvymWi+8JP5+9/ruOX14gxzlT4nydinVMl1cPDSeJgC6zNvKzKSAcOIaw89WEnjA2dKi7ohItXwWxjytz+baP8wF1+E7/ohlco96yl/7T+1HPK6nGMQZLmrRKNbysU2hREzfPrFkmTs+VtmGMat4Ja0F3vD1QydgQ1NLEmmOmQ8k/NMA5wwct+FFLzkxc8mCAGa8L5PWHsMBqR58DfCp2v44AJKHXzoV07Y1k/MlLOus/e9sj9MVT1EXwZHqAAcKbevOMAUb4cjzeZLcDcoY/ry7q/InTsOaY5VH7iTL8qPKmypaxor6T7XgbcEsQSOzdRWNMcYFITpm5HkqX2o3PC2Oz6YODF02Qs3P1kOYjmBm1GQ9bRdKA/XWIR+aiL1ySB2HdsSTXqStM908B2Bl6HjDoK5akmTKS/mQnKVSnKQ6QkVK9mFrK5P8oYpu+c0Np/EbEjr79djQU3bNCs2suQHXpLXY96IX/sJrazTCjZWTXd1fqp4gFXkxoQriOTZ6xGPnNTj4crMl/L6To7ZNjC6CYwso+cxihj6aWggrCR98d/PTUJfu5DOX9QHhTof7G3VeLV2ezZ+byWkXppTzmkVextHgWwzCuXArGtOThhLsulOIYFAjAAdAfvLzT2cXZ4xPtpvOOwvNfswaZ7iVJDusvitjOPu1vNCqg1zFk0uCFq8kXbBzdofwziuOxXDWg/i8oUs68xKXRyRDLOzNMPQ2Mhf9Ydz86v2TrhrRMp3oxHXQouDeq+TX4jCi5N5xWAsw5n5RzH3Sj9uAz0jFFA5c5+ELJPzRCf9K7FG708hSHtekJiiaN/1dPBq38xG6L6uZN1roI3aDktUjAamp0X+aLI9zQ6xMJTuMj+pgzaq423+MbOMtKk3Rqb68SAJ6vM7JfHp1ktS1Q+w53VNm2GDz19QPd6OUpaH7+7ft9hvbrmYVh1JiftkC4FR83q78X4LeKhxnCDTUJUwHhUjsSmFHwa5bAF+iO6HcxPblmbMtedfWYP9eZzvuSjFUScythB5c0pg77JdsCWHV4acLTBc7zXzYejb/N5cny6Y9eUUsHubSZth5xMlfGWWvA9fRjFqfq8Lish6f40QZFqbfjotQpyJ14CigEhRy6Zbkk+f25g2vebfKbRy81xKZcRtOfokCVkEShx8O4oZ+TDY2VXTDY3fHvBaXRx0nKIoWOvrJ6vTQI0++UJc+BXkNB/Bpo9/wUI6cnDSb1vYmLUNShDoEIVDmQKUGDNPYAoBsEoERSk8YZrCky/Pmp+ADjY6pLnSJB17OQ4qG59hOQqdgROb3yX7Zb5eAKQdLl4eHsiJ2LEax9UJgvJ6NVcURgSBnwU+cprDtstJM3aPaNLJ8wnkt0yo76rVMMWHXV3rzT1a0MVLOKTeE9hqsz8aCir/4QI86olV9Fp9WLXwBpjwdm18WGPEczIoQRqMaNpnKzavLdMygltKGOUT6pUwFfhkJ6uqJBmwpRFqksTlMa7rNAI1hrquXuETARJ3mGrpMh/wdgJ1cRxpWIC67SbZQ7Yb4wC/eULA1iLn68/nIcIP0Q/U/MkOhvpb1b1MPYgUq68n0xnbVWFvFxT2BNJ9k5/mvfMPC+QHLFsBNLCZmaEHJ39mDKVGbjonaf3MdThfHF8r7XmUkOUmD6uTaratv1liBOFwYacNbGCbext8NoRdGIq1EVNZdO7nvDl1+iNGhhMp2khhlglX2VA6SGrIoXzKmxgRAm4tJbOKtm86/7pnD313+wdunCLW6NK94xPA2rT2ZgtQ3zNb3zyVBs/295e+rLoWK98Fx6Eo18TdVbqzW8LN/sLr3bRfpQTSloDFDXCaKv8UNojuFzI3dzKbTQTddX1yVtL6zHZtT55qQ8U1/Jhd9gFRke2dMVmrOhznjzRbwxmquK+UMzqvniPphgBn0giQkjUPvzNCwvzN9sSdUn4qPpXspqNZNUm6IylKqtFmzSRjMgkrZMYngsiW2sILSCtM9v5FW37abh3sskpwYTOivsn8gxDzHn0mnWzPiR3yyyupJ4jhg/ekCtyH/hD2oVQD+84hExzuryaDOeR0y16ZE1els6UM7CycxsCshNkdByn6lVnowK9+thozum9zNolswLwJYNMf5JAmiiKmHKJ5HlJNzKSdCOTp0rjWaXreoGQZS3F65BQOgtbrGu69zzZvueelDPkzV/u0Z6Tj7HLp9QWih+wBSDP1eqTHv6MLz6QuXGbV+4uDansbTv3XKnVwH/m2QBbZLe+tBjUHEq9tUdCgTLuh9ISgy3iNRR0XHJecrrgiQjFHzKsjAslfOToYts/rSW37Fi6dLEKqIqOmD05nPIvva6ldkvQhRs7eHgz+qTiygYpvNf2kezdyDVMaLVbX1/wRvb99kfCObYk5K/sgCDKf6iKR5sWblNKMZc19UPFEQogCA8VOmBrbCneMa0HooJdgf+hNG3aJ+EIWURHYplicRUniorEK81XrGDEx/uNa99r2Hek34BjaG5qXZMcrjHUW5AE3a4yCQTOf3ABNl2uez55YbUdcqenqpoe3apKakVsPXDvt4uwFHFE5lPAsef/yboM7kXvUz8HFiF3/ml9wOJOhD9W7fCbcEyiHhYJSPTOSJAQ7DKouhMHucDTSejvQE8UmpC1M0HhFVwpf+tvDjIuqgZ0gtY0RkFkX2OYJkAOOdwq8HUh1apn4lgqbBR07Y1BtvxXzE/kC7P0E5D9/4oEttLYSGvlIFB+3Sq0gWlm/RhVMH/xxCudcChf4brmUqtCbFrRBwMs/h0q1X8xJcbuW2at6IZUCdPbp9lodQ6F0eLeKBac2H3E8/Yv8Df5gAQ1yjcytf31TAtIC/MBsCs1vl5acyodI5DAEkL3xqTaiaBp/sqkC1uSJhRDvy2EzYEm/NQJx9z8jo1BusFaK41467TfiwdfkrGrL+ERWoRkUTlATgeOkBnw+Z1o8agOT+KllGVYv/ak2LLRH8npk8Zp4HJ2yQ2CCClTc4WHUy4ROjJ5YO26p6XkxL9kb0aB0k6kk9CGNdKG7l4Ws+lixTOiJ/fhYres6v0luYQ9QR5MypnzRHfGB7NCeojXDw3vFnazF0ZdArUj6hTNTE3tefpAZBC0epxTIpnF153Ns9Rg/f2l7dpzfxULJHZ71a1XJzSwFrBhijZU3hWlDCH4yV7CLRahygX4NL6Y4s4oiAh8XmiGxDysN8E/d69ujZcyyG2tRk00ICoul+Gvl6yM5e3olCZGd44GGaQG54immcsWSiV3932x9x6KjSpDs18web5Z4763Y4Z0wwsPXP+r0nVm9s221BEVVZERmZKLrCEXct4p3n4T0st+gp6q3YUOp7t/vRdtnx19RV8Hc7l6Hhkg1jAB+BuXc4j4FmuwXNO7Pys02weLQx0i9Bfl95NPA45vAVVVflIWSDbhau3D6MHMCsogKeNDwnmDdoScWwU/FU5l2wdXyrcfo80gh2bCUdS6zE7hEVCLQxfLChj8QtGlna16QDNJjGD3bf7bPgzOrqgpDBz5vF8W7FwvxrX5wFUBLmVn5gAvB+W+H+vMHKxpym2ZqcQUHjcSm53DfMtyg9rXi+kYh2y3ofseui/gZeZhq0W/8l8So9XIOPeTC3BNtOW9K9yg+alD85c4lcbouJo2VLI4QirZeWT06ZSsiEhf/WtKA1PMl75LlYG7M4wHMBu21tIeBrGcptuUk1NfCoe5ky5zP6YOVA0eG00psUkMhAMjnrxz+w53MkihZ3hZX+9BXKGAPae4ePzgt64xEWTdxI0GTNcxRDIGJXOI8bhCNBa3M7jRJx802fF7uo1803hoQvkAw+PrhaP2Fl+6MgwRuW1NWdJaXt30KR7YPXkk2tSI8893IOtK2EnBpgA42tvhcd+fIlcNyff5uuVeFkQkv+h3+FF5bV8obw5FZhTGo/0ZNgWfg6ftN3vw7oV2JySby40KLe55nspmemKJXuEki1qrCQ5UZ7oIqRFhWgpY3IAk4XGOAR2Y4UbBqJYCEc0Tzid2Lt3T70VGCxvetG2b75RGYI/+Y4KeQwFicfpWkStHkG1bsD1rhZPJWk51yNr+Okuv0Fd4RCy2quBGmUzx/EAi/rDQ+qR94zDw5QDzO+EB+83awUHj9ymFJ09MlProXqC39i2Nc/VoZMVub4lxmqLWqEeBPC6px9/vV2XdGC0ZMiN7ZiegCnsyKc67v9cr5EMEz5JvQPmVpNird+Q9lKUGdNcbXKDqWsvIrNki+yQ7I+Tfahb1HY2cZq8aEV9YT9hTmtWMk/OQzUvOutYoydezkP3kWHYFF/aNsHYL08pT/NV5aW3i5Gx/Pg0apJvYcWrShDv/ydkwB4Wi+2vtn6Rwz0HkwvgnIjGn8S4fmTuCb3bGpObnK4uqjuNZ8XqZcK6HZ5ykbcj11FLvrT88vJJr0sZg7Ztvx9Op0UEvUqu89YX4Xl1rKfr0CdMxL0ip5jXu3wdREgG+i2qdcT06PHwpU8Cbp9z2tLRjqU5puEvQ6WdpomBG0ry2u2jyw7jgU797LRyrzQfEpnyfzzpF4ZHwRzkMtbk19CBSz+gNA8UIptasQ96UaJh9rRkO1BRuMnIXWjSHhT0YpJk7TMOo5eOC41/QNFP/PSf0c1PxNJ5M7QeacckCfqWgU+XmxK5rNF0yuGsuDaSsiqqLg4Wrv082OL5jcyN6cUweONBUxrzQ1yhAky7BOYJwNyihc208dxvRc319FzEqMyMBBxBJziYenYmEb3B9qMcWH0IOAUIWzk7ScEKtHIwzdl1tWynEg3aBdCO/FTju5fm1jdrP7fmdsLwJixFsqUs/ZWg3FukpdQBn8UE3GNwJJvkoyIuVoH7mRMshKJ0QzHKw3MfnyKENb2R9SlTSBrDX8V2/kBihn3DsPYGjpvFof1ZX6K8cBz4UhV/oDnEtJzh0J04gUc9Z3sJlXlVpzRtiBSdh/lUuK0Z0XtMb60oyBoBVeIe8pox6bUXUXcdODj3F6zl0R47KLlR9n5Bu3KAdleAXbkeYaflssJ8y7WBic2+qdemG9k+ksc4buhBPF4Ixn/gpBftQDL7MqnmFu1FsP+oedeCYz9As/8aCljzxxX16DcoeHij21UusksxtkmsMVnwV1LRDa7t1wk8CRSGmxQOGZK3Fns7tLsrw92r1tJSH2FjRyNVVZW9ZNW+JxE7JZC6AygWEn1JJnwbbqhzl1KYJ6yEO2RyhRpMzmpXtPr15o/I91evoYfSSGPA9WdzPGEPnOCRk8+EON9eecCnzmlmi8soWpp69kNBV+jOCsZ4dpKjxXv7AgjL/E6aaYyhKMT0JBsM3f3OP9qy8NHy68rHw18tPFG2np5Vzsa8YvWsCrDia8kj9l1jynrLLCnpUgDNEqdQOeFtOOXCNEoD8Toq5TcCiBnEDSpnPlf06dpmfECplo8r8nPtLmhX7gZCZmI4S+hrNtsJmnrwIEnO8M4LypfUf8Bj+aGIufNA+A8Vuw6Wq/zcmKUCe+BxyZxzp9F9b+qSXbOezR5CHLTPdj9TRUQlddHtlKRDkQNZjAq2XIxD+BPX8gZEiDs5OqSxWPj7Vg/Yy6CWOQbLN/CLRhndof4DAzO9LPwwMYbEpijJOGR2aIbQ66vj3R2rHIi37/ckZmY5CFgRUdoCFa7zfjLfHex+0bmdlIsnv8wwIS1drVVqY3SpBVGlpAEspL3qMMkjYssA2w1dmI6QuIB0CHRsZ2oQnVrUR/Nt2nRVsOWMAjetnAsMH8hiw4RzK/Yh52fDjZEA4Jo2nVSGIpmPrrHqFY0sYe1GJ7jdzKhMONCOqXrx3ZfBS51DOtpf3ZhEcNZqQYr9BpEIP3a8q/Stz0STraj4fyREiidXraeWJaPexVu/lGSJrBSpHlEQpaeiXqOT7uXw9Z0JC9spxjQX/x5NxTlyckgAC5usoBKYUSV9o3x3Iz2FOzlbBaKt7ibqPlLxIc58nqbbO9mA/7+xPNQV65UZq00i9ItC/Rxu+aB3+kwXN+kCcNKdQ3Sid+PRudvppeKbqtf18iOYxgnsMpScn+N+mKqcaHkKJ81dqOYk8iaBnz95sC3yHB2IgXaz+sJFJepXnNd9nMbIw17cWuzjcgUrLEvmGIEciLFnPHpXX19er8MDCwuRu3ryS/uw/tuJZ7cZHyaL5l3Acm6Uyh+S+TP1uE1ZrKwSvHugzeD6PcIt6tpnfPyDhh/BxXSjJXNnqvgCPo3KBigetvE5Qo5BjXImjc7oHCIoh+Dka0vCsoCFx+fLWbcqdwmo7VBQZYU3YwPEbsdbh2FPmVwCwyhD9mUSw//DVA1kG4ML2UKPl1qd2KX9l3mbbyvD0AVVPRyvz+58srb0CY96eZfG/Mh9oqeKo4eik1lhF6H0oVuXTcgbyk+26K9QNsQkJ6Z7ZDiGia3IoaO5uY86rMCAeiQQkae/JN92Fo8msbwLSitM3geewrLkcQ/djfNGlB11sRs0mXNnGlJo3TvPRfvuGhVgGM4/zk5vpi815QhmlC15Pmtf/n/0YHsfmJn4qQSWZRU92M/ubmtdN4uWOxqnaDWcFuB8neINIn+VkDF/RWHCo948jDXJwNieuruaERpoNzWNo0fcOe0ZK6es67zTeEXoLeXZajzD/ZuGdz2g6NcE/0t+5cAtF4X/HtT1EkybmaZOUEGm9IXyGZir3gZxgZX7ZdV8N3I8SafOH8GhrO0TuLRZC5VJlzLKlQAC7UyMDt/Ufv5DjOZmGW15DDZPT76oVNzTHR/BJJgOXS2T/3/fx2/5ZauKfxsTt8K3alFhl/D+Zky0tnohSL3OD8BMU3VmS/fSOf+SWi7wc9Jf1oas6crKnHBI69fyKDBO7QzzgiM1XtWF9M4QZMef/tVGRuFxlOu1xOLzh0Zdjc4yFTExKWwYJbke4UwOcOQShI+uW+3JI9CRRLF6PelWAMIpOKwOv5P59406GXEyYofq0oj/+u74AJU0VylvhGCfevT0cpxP3ZDjSmYIpec7C1fRgGyjbbjXVNmI8mv/qQpabWZKzhKPK/2m8178RNSwBZy8pW1fYK+IuJQHb/u63rJ8Bx/HrOebE+86lxaTw4BoY/pa/2qPFikmY37+mx2x+8P3btUXlR4E+SgVoz/c/96zRXX5rSEO2HrWogO7PMoNJQkmMMmuVyjVVlo55N/tg21WBpv553votO/LIdsiZhqofmn3FXN70ksIuU0mf/S9W4DHT120sSv3VvUg6IJNBJgQlBorJ5MoteP8zgTquFSub5T7y27U0h//Ikz2i/2nX16wdkkAB+u1LE1/xo+xKYJ8R+DIqioIe0w++2JQ72L3JJnL4P58EB3QwY0lnLafJ12FcWC18hwjgkZyNAkb86WRLGVUXDgOA/d8LyNJdcdC5C4721mqPZU9X4Y7rqnsk5z/qOf3wgGXy2qY8Rf1B6v6YzASUJbYtyQjggBe91a5z+qrl4/2I0qD5/v/hwr068QERh4113dOPPCZYVmGVZGsUwCB227ThoyALltgUmkNICv4VjzHnWDXcgPSfyJI/RAg8N5Uc+EOR+nvyvNruVnerIH7Crl8YxiLKZ9HP/UPb51IPtQXBDro8j/P4JUho9dLlez/dRqwDiPh922cdvkmV+hKYwTlFBsPhMbTSWxiRnzfXvsiPcwPwh0qKeGZJ5PCls14bg4M5fZaNmqphyOj2s8IYMBwaRRl54ChInTjmjiJdXhZSXtypXI395Mu9EjIKlWgNyqGzJGUwklxYZfLIscs+aShfokmWN/euUCYA11emtplJHdvo8qDX04FZMuYhKKVnYciw7XvWvCn8sPkXF8WJ8/wBxwd8fMZnDRqp3aAMeKCs/4jDEV1Dq55r55V233qFadAb1Y45MOVYwpVhFWQt/x+18+M0E5wTvPtAXs3L9H/dKX9SJhvZDszQECdL8130Anrf7RZM78FDismxIEW+WNhlHSPQW0vDA6zsvCLo8E7kfZ6hyMcrG+7kqDICiaLuOXuDhI/AsvTTKIlsT57OSvyELAyegAzKOInhvJL/YRwZ9Eo5Sqvda2Jir9Re6HcSVoMtFcfIgaaojPMbRVAOHXvLEYD0n78EVFdPH1/y2ztDjCcgcLht/I5fncpe+u9P73WmTaAeMMyj6R/SVMdeY1XbQH/FLMYvITW/uzuqVIbnG9zLX6pDqU0tvsa8q325FDtqJiQ6jcIqSZdxYqEGNALZWxYQNE3xe+TG49JHUx/5vK4K1NYb6+CW/M4S0dtstTSt2CbLK1U6y/GUa/+fR4/4O3wnykmJ01bfSlRhdNm5PbTYy9CyG9Qoe2OHzqnwuTc2Riqm1GrR6L3GCwHs7iRX/kQItcz9NdWNWqKMOJfNMegAlO416WtgjtXAVy0QEj/7e/2Z+x3ERpOnl/1XbomvXOdrkSA3ZYTyVQ+HxD7KAXkkyEGTJ95c2G/j1WSTXZFG7O1yiGJb/dogjIfyrdkGKBfoSfiIZf0OW8FVJBmC3khN1VJ0P1SJ3+1tUEfVm3ZkLsLGKFCQEolnQ+cuN9Tr8zKbUOZRWXHYXQ4ZeV5Bt99N3eJJ5rEb0HxBuH78EPudMTCkHHIjvxELyDXcwZBiWARdVkUSJesuC10x8WL+0D38fiAHBGpdZ4Yez39BCPqMbNepN09vjy/KjKJ8v12QxvCTGMDgh9wo3nrNfzLeSLgoXtyLgtlRT+7xrVdC8PgIYyL7wqHDu5PzOdd8NlrJfhCuS73Cj0bS1VZFPK54d7gQKAeVRIcjeRweXSeOoiIxb/TVtE/qLUk/41zOEIFYVQRDy1B28QU9IeQYge/PfGE93lUgUjXM0wd/LHsdtKyqDLooqo1alwmOzDrMEQtzOCiw4Uxym0mIJJKsYU9cSthnJd8vnaeLT3vjTMIDhpZ8+0cuUHmMIs79ewssmkumVKYYp8J/Z1YeeLh9QzgGM9nhj17U5IEi1tCJfNDpT9A5LZS5yiSzVvh1AKdwYcl1Kef6IDHPm9wbgNMv0F5hnZjL/+gUJmHqAE+N01E9RJtPLEte2X//tuvu+6bHbUOvPX0lBda2L0rG9CA9zoY//r2uas3IF7DDKMOwsN0Yu3VYQTa8bkAj5+OdDZUnkiR7YOwQRDhMQPBcgOWn8AWnCGX0WwJ6hQn1c4s9hgqLBeWsv75cZ71cxL9+Jvpj7oL02wTot9NzWh3RlKap6n++3A0aB9f5T++7JPyjUy+M8J7lXYhlwfB3OtdEwMlBCipXDOJIZbyigo+xMDOoxTZr+c64fEBltw1FxXFl1TYMg/QiblpWH+VyxXg0uBIF//mwfRfL8BzOAYd3VELYdyhxcuHsZh9TTsh5VVYYTQiMIQSJVUN0/4CxNl2thtwyL7M8OT18YRoy/8djEN0cu6DHrAgfkIQ6eym6AM54l3lM/0WxNIRjhUh/tbzCWlaDC5jJfkJJ6lQDQ7aL5t9RDinyFCeN95NCfg185pw1WtIgGmi2Ck2IVHEVGUc5pAXwaqU37LH8UUKL9HnzpKkPpNW7TFu8qg6PbI6/HF2/NsTWg/KxHJe250Mnl30KMrQ929mN/3bwpRatJYSFZQN+NalE65l0DR89KRBb/87/NaGM5RFLZ9tCorLNhxsUUSxwOf+sraetRFfOW/e30HPF083faDv6TSZ7XjmFYvLy+WBwLJenuOCIG+vToT0HrKM5yPWXrKf/Jv4ZLqfONH7nMdOmKvqHAdGPaPZXaB4kHsYkk9q5RkBEUKh50YmQOdOeQxwzI1qZFeURPekNQv7BoNr7fO5mwWMHOZH+p/V+PTZgKuzt0zOIBvmlASgBodcjeIEXefc1+WZ/0Ij6Hxmwnlg9H3yTanor5Ng3gd1O52RZgkMVslNrYXGbn34mjCVxAlFiSFUmSFC1cozx/ip2wTObXGJ9NsOcM6DPCWF7uZf2e0VRLckVWj5YmBncy3qNKF16GrdyhpPiCA3v7xLsdjbCx/D+ft2jt/MpLQZMea2zOCzns6tcIqFWH4bxk4Vn0L3PUrgmqJOs06A14mB5+YAQkb6QRRZEhDnLmvn4cPdx1UeFL/6Dd89yecric45PvKaI+Ny2fDgspHxV7tZAc1Q7ib9zyeWmkYSBhA8ZX0QSdxUPte7mrb8p4i7U/UcgLpouOyu9fMCbO9yeCLqmXIOYbhJPf9Qia0ZoWTLSOv3TGH17kRFG85OIjiEZ5HMf8U3fdiKKySLLw2Xm1A8iUMTPXdf9owcu2nojMy4hWONu1qeXPo0ol3xuEwoqsdUiyW/MzibQ9aCCecsSiCXzo1bKp3lM661leLuSDh7GgeC7LMZ74y7f/uoEKCXnPe6bFR77LspumZfHtvoNLLK8iSQ66W1ihz35xCZJ3I/99lG9Z2vy4mZlagNOeQ650BWxd8iyQLFYiSYWXlIy7AXqYfEjeH8JrspY4DRZaj1RWFDS3VnZGwbkGrMzy09BdaepLKVpwVc884QOuNouTxmkODDRVjO3iG21AvIvS7yWwKsx/8x9r8gat3TEW+Wtpy0AnISgKAI548BN0pJnpF2sUTIwkRbcY1YDPYfLOI7tftSuoE4eeAc11NME3jv7Ipaakun1oei01vFbjq0BVhBWz6udrvtIUhuIXIFyfRZthzxqH5VJ2IaUxt1tSzkbCHZ1RAimED2XgeoZC0JaVLyrtHUlVOlZ075WU3B5whcGwLNyJ5wU88KLUsU3TpOnxqWvh15/WZszw8Eok/7v4d/JB42dJjeTxq/dEVGaF4eQQ/dS/QZYZ1TSiQ6E55dPfvWTglyYY3fdsrjG7X8FEV1JHgkCaMtOX+yS+1nlfVGY3CrXqNPqf/+2mKKkjO1HBHsl1fcXK/oqFuVGmXONmWesILPz6ksvt/HVOX8YyWToS4wa92ZOAHAcTCisHwd3UeeUpzf94HSXeVsqQJgG5jbbqGMSHHAQzxU2uzafK/bJg4Jye+1eMBAXcilfmjMlROoKitu/VEVMERv+JajEWX5TDsGJTE48f/fEqBqJkfkjC4a1/nGuLLxTWu2uND9y7xYUF5A8xYnXMv1fKPH7Ry4sx2Se+DzqbEE64KPR4zfevHWwN5lL5RBOyOlAV1uuSpAjNsiPgb8g+X2zlK+Jvdi2yIXHsKKZpz53c4b7d/b2lxTMiW0IEfmEwD289KwVkgJXk2DL26rlvDM156/yaUlpzL4JuirxtGxrFcpYkKTH1A8K6getwtyCUynUCx1bgBXnzDZMdBT6e0T7uY+FonmwjjVmw8oeN6sPkl/JrmgunIEltxdHeXLcoEj4FY6TZwj9/v2S2+B/Au1YehyEq1sD563+Es3JiqIUMyWRaoJ2Jtw/C586rgdsW5PXTnKJomFLhXk6tlR9ZVoZT+DTvki7/IrZzZV0d/+Tzr9+vpsQeNRH8iLQKD0y02jmWpe6zqtJLM+Dxrs7qpBbf79Bw7+yH9fFAMyDHzjmY+lWJc3OPdf4s5pXVfz2B5Q1UTZVFSc4m+mOup2s+jTylh1CSGsEQTuH8z39mTtH1IwoDfY5L12A4tHOL8wBuZdqusYthRwQP3ZzjVZbqn++7LsuqosmSQYKXXLY0UZt1C4bxsB/2S1KjxDDqmSpLMd/rXxX0/SdNkusBbQ4UQaj3L3d8Exxz5K+yCE+5DNcJ5kIiFjP19u5Ly7aLZN0pCMHrrhAhrfjumNsxA02Kwl1ssGQo42pdZ1UqbbgRVk9TYGQYy5PfZVlX/x74khqHc8VlNq6D9Z/RR6QUkI3CZRRddgxC4Ff32II84/85dQzvjXbN40gpa+fAfYN/XnFFXa570dvqy2jIGfkPqh+ILTjhlfroeJgpA6uMBBFVeX0oXEkFjXUmbSwghfdeIu6uKJUCimRwBT9n2aZ80oGsGB0Y0brTD7DYvqkvHXzOnCHAi8XAc/l84T797L1KySnUSh++aRQX4hJlahBsWpahI34z+TzPYZsHoxd/g65U+d9TFcuvrUcPuZSLWwsaFXw083EP9Kvsw533lmZXPi3eNqFYzCpo2Ccu/KHIVeFDEDqukg68EP4oOJjZRxGCz08DWsveW84108OVy+M/H2fdT9mr6ypBaxs+5SI3MtDLDnDLucp30WSr3TDaz2/zGy6oZhn3Exu5hbyrjI0K86nEMA4x6oxC6uciXff9bvsepG5mJx6mwBttBF/JSF6mjk88+OqWYw+hfuY/F+Xx93oeYG80X1i1Tsjtvy/QehuFNTMBJtiA/bXtNcYde/nq93qnIVL/9VEH5SU3qnFYkf1GDiLXjvB7lKaGOE/yFrRorJXGXO3KmaJ0ZRUOF0IaAu5vUObyvygM0eLj8prp2+kH41lLec8F/jebjfEP73Qox/G9aFXAuC2osl1IQOEkpFh7ZHoRaPwmqc/PLxp2iM5ZFHVKyqw+VJ1+Wx2zxwA1uiRLtZFyhzRzKRRH7BDh6CCkyLOGCbmNJwTbV6sol+COyOuHCZ90jcSAFKSHsVLxQabJ9aanfr6VvvR9v6/tceYPsK3cZtns2kzBGkCyNbsiD7TUcaRgV2xg4cdCwoPA8VV7zOdc2fCcy6KQHoAsoTyZovsEd88Yyy9JZiqY5+rWUfGHT/ImEed0+ogjblF9geqygZG4YoOqHVtgPPQJ8ixtJiBiRT0sTlOGYeLdHiNLiNdopiAFzbJdMMPl8nuvYFCsUP2s1WDD+jrgN8x21O6rxYBzbjML44PTJM8bcV6D4nKM0zooomFUP5GjrM+Uh5giJt9oqql6xt93u43rV4leTRQFGm5fvM9AqCfVsqKHXkniSKrcFWcrZbDyQXx8Wa3dvS4uT/6lwDJpxwt0c+e12/7grpjlbDZDFBXUlAlYwOq/CSxkuFjQY0RlCc4kp9xOWjWioBtaRMQQz8ufvGFL8ckl845lMwD7mIV8U0/G7NOD4ms1juNdRKd/A9Es9ZNqcqsQ/bSpsK3ha74E9InR9seW4ctZjhWqMO7DQYjjqAQgv07DK/HifKa6bIAS84QOC7TZT2TVX+yQqbzpfn9i0fbp48sItlCO34IK4F9ufkXeB4II16LniN0rnfsN8INIf7xFIVkBVBoPO+l9GxstrU4lNKJR/eX028ZTV8Qd1kO3X15mbsdLHzfZsj5Baq7f+utZHOj1v6h1i6HJghItD3jcms+IVi3b6vsfpB9HEUbJ0uQzuHfAIF6poHwEbOC455ksRn5/GKKCOEZu+XvWqoCUkplafZaq7nRklnvI4/Q3hOOxVgvOGHspSgvq6yuW/fEvPWGELTlTmU4fGraSnZ2VNzjZDnU+16t/QHKkEXeIoBgIyphuJKFPNlKz9BQ0PqukyVC0X8lxFDxhqR3ET+YJR9aYiFgRY4hhIzWmvcs9hSZTB63avzFFvDslL9GzQXVGMZzpFSV/WZC7Qrs4M3t/QyK8tOiaXGp4Hf1YVYfypbDzdez7MAyl1Y/htaPrjQcMKWgBglPR7qG18fIIFMYciR1ozGIhr5KEhWDWm/43lESDeSrFluT/4xGX72Fexj9dAhIhf0NsKerAG0D+Mrr8nIv2bntya7b4z/1K/zouhL9fMSkDJFWtpy/yAUOmH93cGBJ64fE3DMm/LpCF0l+GBcr2q0P3mxqCneivllp5L/8MOgX9imHK/3hTtTkFgttoO3xoDiMa0gBnfWPqk2sM9Nehbeqnkfhe09UO84LmlOSFvASQbIBd+7u3v5YOuPR6IOI/UXPOraw1xOSM07MIqFQT2eiYoU0UDZl7s0pDyMSvaEluM3SFwVHwJ/0FWVsq/hd+mUHBoQR+Ns9ba1c+VWP5ktPDSyM22iP0q/BnyMdKco0Qf3ESbg6osop/yv1zfP+U+79eR1lHTptrpdTm+aZGHvWgs3vOhY0RZUl2W2pSZ07CpAlxS+w8IiFWUEb+OZtXKx7GscxZpPKFG4BBCJruhw2HFaR5+gPBv+dLtH/aDyQ6bLUvQz74Oo6uhYbPMcZiaV+t60q3SVX6jQZWO36jgZ5lKlB+RLF3zpTI69lqSSF4Ws8aauv/vAPL/3q+kqYPnhUkJkchsho2Ctjxw7gA6Rph1oT6z52crb4OyXLsOqoh3Un6ZV0SJujP0hO4gsfY9J69XOGEOCmWkV8ltHa0Y0pLrdLXc8Z20sJlwQKGMJkim86QDHbe8M+iB3iG8HTDKv2FseEtgtf7sqrtMCG1aMWdJwyt6piKk4t7Q6uglxGEDzu78QhPzFN4yIaVZIMud0g+ntyq4bxc9L6gKt+L/0xuvaBP2wU+zVop4X28nFZXvJ4E2BdX04rlYbyzNSDAFNPkzuj0SlbiJk0ryNXAFMt2VTQEdt5VtEeEsP8m90j3gaRjndled8JrmknW/HMiWW5lK7xQqNIh1u0gPmHhisQ9a57XDvKPAEOYvo3inolO9vOyA/6jhdbgSj+nd04RrP+QJBC7suFa/jw5kScTEI2IFtLtdqVh791mcZW/x4Ikv8xldSFV8qKYaqybjbTkvKZ9nKCzu/SmD5/ZDg32W+/OKHpuiIqw0X6KBr/RzojM3r9ZzAVNZeszAUzb2qw+DftPli1tU3kJYTMDrBfU8Vdqiw/o7pVkBFNE3pMClQRJIiRbWiOO9Q74xMQKzsJZrNZ/vILd9KuzYfpn/WDyGxtt7HvUkaJZhSZ9CYSbqH8gxKhOhP7epgF1UW6slw8L/PbFM0YyfRamofF4JdUteBXnJ1asE209XvlfsVby/P3lZtDXdkbhKNnfU51t9kP0792xxs3/LjGXQz7WZqzkVJ5F/AnwVXs0OqYnBd7dtmurq6OgNI246iyDckQjqibTp8prgVOKYsXaJQxDwNVrBtX7rQApAPT+yQkVUSJ3y2nWCr6msB27onpbfHSW8HreKAuhc16aYfEgxp+/XGLbn5A5z1C1avY3vuw4EKKeEXeCsjrSd39dBCaeBbtkMY3FGJN0aBhuCMHhLwAHEpeaVfXIpPv+2ketxiat7frvVTHMYFWwj4NEv27kEi+2jTwiSFRsE0rieFKZtAO7id8mP7zSro4clKxSjld/c59XMoyJeuKjocZ1Bf22YqYY79lAT1xO65kT1O6Pr768VSrWJzzank5gQSrsL2Ay1F0KYg3nSuJvu196/16B8Qqu4Fb3ECPBFPqqFeEXdUvofcQH922bTpqvcDBWYTYeFah/XocUAMUshyaQW/ffm15mySjUs7jTFxAI/4B5TMFqWFKS7acJSbBJdZ4VlvK9xyDYu15gthfhcsUDyCFN/huM4CIf9+ha0Xzxm04QJu7gZ9dHAEFN4s0pb/3FIZBXt7ONPMeY+Ove5qHSdj8U8wvSB5ak1LmSKnlxv/39DAxYjlQxNBuv5qg9oPHE9Rd2jvPLIF9NRbR7YAowCguqAIV28jmbUE3xuEGeZVlYAy+y9F/8XT4GCJH6XxWdfSWDM2DAHzVdUndhxR6rrsa1a5BalaC+gSt42ZCXN0UfikNjJyXOOi4O6i+RFQgqHU1Zgt49WeCBGy6yV3j+lc+zaEJQ349I7ATcr53nEnkuCIB69LXutuYtwjObr7PkDcGnCu54eV9ZP0OawbyIQMTviAqnwQulZPGIjdCQgmCkgQ5G5IJ218wXE24qIywyxPagnXRz1Pg++84KDXkklN046jNEejEmfeZV9iAiI7rB7KQ2iYlBzkGUyb4LB0nd8a0ufXlR2+TnRbg80DKz0niXxwrjapnPyxtisI4P4OCXt9VF1vcODmL1yo5sHw2oVPEODsO4CIYkyz741zU+xvoRm8VDDHscLRcOK+CF51OMk/juyz+Hdg5mM0mmqLyU8CPoKctg4QlmKcwzJ/Nxz7PQwtYpQ2tnEDvXd2tr46UcX/MoyXPV4toxg4Xp+NFhjx6uiw8j+fdNThIFJlL66o/4fsQTvE+Dhf9mNLCTwE6rMpJG/WpPpoxGUPgNYE8my5WlsICr5d+rp0APGssH9l/LOpCV5+Ys0zMFSIMD7sr/87hjnvkBm0ilM0c7+8LKIf22+5+DWdtLTqgCydEMfwoqu3j99nzS08j6HMHCLWFg0y/zZK26o49fvGq0m1l1+GlMW5a/P8Qn4/40e9eteXEIaK4AgBvEL2l5wd5RBUx3RONguGXYQ2cypcXDdhTMNiukXOAVt3cf91wFKL9FBbkkMa8xiWGYPkFZF0+KqXV4QwL+pPR0TgqOzoYS90g4OH6zKhXof/ccDc5n0e16OYXJeDUfiFp3/1VMvMxKZkfgWI3rFQjD0Qe9A/O4fHl2FYiVuQFUvJXTttsMZQSx0LbmEox2eO/oYHIIzBMSNxhXYPxb8LrEYUSMQg6wmAa/w2w3lPsExp848PFoYQSVR/4sT4+kxI3iRFjAhz/81DH3xwXMgy8be+FSHs+KWtYtkzmMUuqK++VU6mXBGLjRT2c+u+2NkbEE6PZQhvCKMSaRLdOBf9/ORT986DIyMvvvym7owX3euGsmGFVkF0F1eKgYfJvu91LfFWGW1/UTl8V8mcUvFDDON9qis+5aZjRBXIvW7RB/hVYNwf3RUadkTIvOGJkijuMp5Dj6le2tfKlSlX8gCcfF0wdQrdEXrVCDpm5exKUSd5GfDhcCm3HUWKw9BuJLir9P0cguD7mziaqpbgWvIeYYeD/OpU5u21+Z5I2XMRK1NvdPJBNFQZOqhYqj7nz1N3h5uya0y9pMNMH/X/UWJ34YRNVPzPc6imjd757JA105iGZBL7LNBK4jPn1yfCG/UaaQIjkitdweJ8zVberBY1z+YuoKsQKO5kV2r7AxOWSLmh2/62jTNkEO49YnM+p6ymtlDob1MWCC3LVermoPCPRzxF97/psNY/6Gv86YG+mM6ijbnXiJEZn4zYMPLjbKs23EiQUVKUhHcKIEylUP/0gk88UgLQ7FRNEe0CuLuaea43oaq8R9av06Q/SAXjkWKLxZETQu9JJOHhV577n53H9KJpIsQ4D5UbwWQ2eSj9eEk35HFYI2/Pfn/asX2NKm4lNEIPdHrl/ZlGUkSpmuiQmMspiCgE+hDv7N+XMHmxtNltY0AFdOO/FkoZiofUhXGy0/P0NU3yrTF6ivDLT1iOSQuI/dD7YgYabM1Q97ToC0sCvCGtUgRzDW4RVPlLDzNY6d6nfkr9XxjZXDtSKxXhkdyKtPHIsvUp0pylPwsv04BUJHn/Lly4UaC7ruuCEbwO43kzlk6Q0uBZXTtIkOcIPb+93ogSPzLwmDn1/9ZtsDu4WgB3EVfLOE2ZOOWIaIW0ckGiZX2FXF1zwmfdRomRSkXBPW2/dkz8INr06JB7AQd4i5fTsgowie5uvW3ZbArKkIB6kxdsIQwp2tVO8oFIqLpFzYyfli35kXcG11PEJi6wOTTolrLuXGJKqtGUPI5JYNQRISPdV26+cFIfNH9gLXg+rpO3N9IUZbwX2UgK+2VcsqI51Q+ePSs4FLRyIJ3qQGyxkD78+x9BH5z30gHuvTTGRCvGsKVuQNbnBPfdEHP4+RYZL3NHqo9xAinWmBgCHwKw9A3VpP/J+4eX/Dd1dY1vYD0A228CDPeGE4JqCv4ITu8jKB8RNO9PnENN4ZKwFznRIO21AApnONvtMh8BZKdkpkhXNPBqZ6wv6SdoWkYi6rmR036VEY5kc6/LbU0caSGYb/OZ60/rUF8Nt5R7ESTm/Uhq9m6OvdV2BiWMlf2LnaegUH5GG3h3wtCBnYZpi1CS/dTO2/dGKsUTjnifLAzerfJZhTWabrShGg6uK4ryz09kQDA30YNRCfZuVO4J4BldlhBR6lp8rf2AHLJImi47qR9qAInmGgs8vQd8j6dM2dgA7VjnLy6e8nCpCBKBzOCFrK4m1gvmx4kbTEjWo7vNnEuJ45VRjpb+xYvwszgfftXwO9vu6d8sbYV7pE+kskyPojRFg0Qam8C48gRfydLSSevKrVlvXe4zf7rJZPAcfSnW9ylhNi3NyL0bybdHCwQPWdmh3UfGUaaSi0m9R9ee2dIkzRMweKPexgQz1Hx+ZCtBeewHuDWJ8SKUk1adyowyOeJlYsS2mSrdGbbue0o157/UhzulwK8okzZgJsijLSXQgp1l3J48/Dy3Iicm5cESfv/u1vrlGSWmBqHRNaEWJP1VmK9AkFsASSlsN9oI/MizP+rOZqVxWinjptbAWIfeh6CCWPOvEduOK4UJgslLfn2N2PwmMEWLfcKszUoKqji+TPP1WUgZHJbvDEHcj3T/znbzrOc5nzuigobloMlt/Ghx2GpDxBfr3zcfBV4vyvNBsoUkPxy9xd5f1rezcOFQ5K5NKdigJWtmkZKMJDGIcPfUeQxn4qShXM1nWj4h83t3P3i4+6IZ6d8EpA6PMVt70sS/pXV+8S4pC60ZukV/dJk0xgJVbBxD+x0eRMEILiM+XCOLvtb8MUL82ZrxIyblHNMzLoaVpqbZ9MId+umfelOecJFltyoiffPPUj/4mhIFJ8M4ssMJMDDWL5ua4gna5v6jSmuyp5gyIIhAIkG2UiOjDKkt7nR80tuPkTXepgRT6+JjM171S3PhCD5rnV2P6vMx9DpGvu1WO3rH7F8H1gdgfpTAxVSJM1G+Jveg3I2gOStPI5CpDH3UosrbCVQO1SUOXP45ABlsRmqIJVt9+9C9RMc8r86uC9FZAyMJlFm2W+p6Qh43ExN7W6og9CfJ95hpos0h8H7mee4DstDir5bz4vUUcnP9fdEZtxo7UIOtwU9PmASQ9iLjbTpTK9SZjf4vu0scjcDHhfCSPUlQZt1nmsdnywNWcOFpirveCbevoly6woV+uC7utyXnI1lKecYOFiIGwmSzfRxLP3L/qbWhGplZ+1l7eHTOQ6LLlLCbBliDA+H3uuCxveibq+xMFn7xsl5uq+a5wQzK8wP2E8rd3U7y23AvrfyQgHYgw79ZB4ykrFbQ1wmH5wPkNABpMQEoVyFiWs3UGAECQHL4D9M4MWBR4rP5S1a8eKGCSIpW/Wo8AJto1+DSU7Mf0+Gj9dF2Z4FyH25nsYOAnCH4jkbXv47xVmbOrV2pcHLxkWu7pGh0pDgOmgbid7cLdrsi4OIjqkqc1x7G2oknErV/Ub77fuxxfvJmk+dpm3zRResxWSM4TBDXUc7NCaeOsbFfs+673j+YeCEuqlzGypxKqDxAR2n8uYy1yzROaC0mwoK5YmzD6o4ZlUzsbWSNM0habUsS8hT/i9RWZZlVzr7ZRPghn0oOiPAl4oZ40I+ZLlGk3W88Ph8PVjvNH6fMcrjppAmMm4mKmm+FTU3wvvHOWATZUCz8yOqN+dpgkMBDYcguQI9O6nTliHQR4fkR7cKxVWrKX0XIuLYkPDz7IzamGCbDnPgYd4Vy57ajZb5Z7jqDy7UVn5MzCoA2MoTYjWIYZ9JSPEnIOSNlLmfMc34gRVZtBCPYXyfTsiVox5Us+6jBa20Lqm+QZvWme6INOHV+tP8L2lZnJ/3w3A7lxKjHcYyU/xnCzc7Xr34iEHLbEIYWgJ1R17MjPWNYw94lKLmcOlPpMQCclK5nuZ6f+RBu2wLZunHgEcZQpZyAfejrH9e3neSlL7IQ5iTM33XudpNcQv355fEriMoswXxGZ+fSjyduGj3J3vWjsPowFGP6M7PXZRiCJMfFrdMz4SgmGfT1RYfa74X36DBuY0BCFLBqn8HL+caL4xVUOEIs7QCUL2+8G8TB3TWhVaSJ+O2V95vD8f4LN99PmOTi7uEXkccZqC5lQH8uFj87ktx3L8eRhQTrF5cwKhx5fI41tBdVXbPFqVm6rEv5pQeLo6ibAJEMX6ivGXtEj6YMZ4wP5zBABc87fQHAE87UzVZTi6lpWpFVWlhYlLpYV60ddq7Gl+bh9dAYUbsfQuLwKH1u/xiYpZA4YvcvEQHivq94QUDGTXqXoLG4Zb/Fhsvx8BD6AQJC+88xA1I/AkL3JeuNhjh25GoFw7J1M9vi2K/LKD/6pxnmxgb5CuGIGD9+eSq3yOG5xCj472DhfZJWRVuxLZATcj7rsOq5+r2zKG6ycmRET0JbKXrC5RhsrVnshZtWUxxW87vo1IJ2dZAu4l8v9FOaUpz1UGmQYnereBMjItaJJ5BXzNGo3MDzBenxByS39VgP/5mzNPYfly603LGvrGsq/YiXuinBRZhbFXFQ3vrdPQmJpmkZ533AVKez1meRqwfeYpKzVRpjxPzd7vsjcutY14Mzp/r6OnsqwA+mE8e9wcngTf4M9AfvInMOy/PCxAtPNMk1QyRnA8iYCe2yXECTn6m5lI75pZuSU9VzHEwfP7LGBR+pUnCq7daFmxkdIvTefqSxZGFLV8kHdkZxsCWev9Mko4nzhT9+jkNxqmMCJ7otetfxxVHHUe8QUuIwmleV2XdPcnXD4EseLTdZ23dse4/ZhNIDVNkmb7b4w91FWolOtc8NV2S/LNAWFqck7P42uGEL/4ymilzhTvqRUDJ7UhGXBDjujq5bf/gFIootnGQ56NXybCt4EGelx1Lvk2IC9HGm+Urm/LejFy/IGNLY7jMMbZ5/NrOe4Xj9i68r8ej6mfzEZ+GVdW/GfUcD4JVZ5U8qlNhOQ0CM+R7zxlrbbb2OodfBZDLzyAvNrvxLgm+7OO7xsSWaPWgZXqNJoG2f/43PXo0u9310VEmVCUmf9QfZ+r9/4wQGKIGHLmPYoi37EnXdq3KkvbJ7TkmVLpnoExNYcOiMsuwEDjofm9az546t8bIaFRQeBWU2LhOGDxDpa/ZD8nYM7wO/ZAwhtZat1xSrJYU/UEtaM8KL7k33sxREYXA1WpL2O7xLq6CoX0OUahw9H036jGnbyD4ud6HMRJ85Wl+Uj0GZL0jhzfAZlZlhu9H5HQiwYmZdWJk2va4cz6PfYcfE6agkOcnq7YMnh6i5+KQMlMy5qVcmrqaclK80zarTEqIw1mjsVXPuxVF9S6hkD9ezD9EDi1mIzPX1Yyx1K9CBp/ave/eL3/zNLgfnag8drJDorTuJPBvQI2YaIyh14OIJTRfceRpqrOMxjStvjlp4o2VP7eCege/uYEtzq10UVsdQHWjBEcpvE4IT0Oss7XIBEkljpUvNVCoaHWq8q6i+5iBxSZUj27seOb3Y84uGSjmWwJqwA0db1zVtMAfXeL9XkKzbo++2SttPlR2pIWrMcxDOFoFpaT+GQd5PCHoLqu5/BM/cyr4Vf2I/KfcYC65knzm1YUZkFHIi2reGQNRswPw9komk/jeK2juFb5YGpPEQZiONgwHc9yYRm+vmaWZMbN1YCqFSIhYVf6z/ptiujXGvaxTBPU/FvJREgzz7e9eZO9yRapX6IKejIU5vtHxOr+rctP3EhfGCdSz3sGqD7URF8cz2PiebOrA3FjZm3LkP7zRUeFN/qg220Ck9fj323MjFeuYZtaY/hT7R36gCwtCiNjKTHfd1sqfoTbmlb67dzdz++uqv2+RDkldmf5bHrnXwMRv9JcK9uE/hXcfSJRd2KG6ALvNKjvBcr2k/ULpCC9GIL6Tl2mmjGs1e7/MOY5CWValqnrl+ZRBe9OzQEGg/hgGHKzLAtDVefGTuV+MNYOr0IqNY+V5wbDRyhCUZQg7fcAzU24FqgOEabkYky3YQmMos1QlGWaQGBKbTNv2/YdoJRla0KNYxQNx9n+1k1wSi3Rxx3jcWPQTyasuL/M5cBWpj6mBdphDu/bfbxGoAb7wj9N8hlNtSvGErqK86Xa5gvShPu7XUElWjIry2HL6NmZQZr+eX5yCTk/ozQa9ZPBami2aJzf7Jm3u60vYy8cu9E9KDLYCHMy1c7Q+CvRorCPDsW9aLrQvPKa0yUIXAh4zt3sX6XaYDv14+EP/2+mONjfy/JtroecfpZlrX8zMvkhoTnsEZFhFHR8+FHGseA83TZGChLuNmRtvdJsgm7buqLgUsCJCZZyzI0rT/99n8dRdEj8vWwQ1dFsA7zrjfptrMZOX5vwBP+uhG6DojBZtkt/DSm9F59/tIvBSFJiK/9nGFbD3ZsbRlIX9TvDITE7OX9z3UT0pUjvMuJl+zcMpsc+7Pn/aHurZcehZVvwa/bjuSGGR7HFjC8dYgaLpa9vyVV7H7oU0dGrotayJVswlTBGzpyZSBrr62Odu0Ek3G6h4DRqb2QEcxz7fmdxce9at3OO66RwTsWPKi86pVqvUS/j0WO+SQIw3BwWFAWcSUyK76wFlw9/i76/TN0ZUi8Y8vFvR6V8Ldby5h2kaRNrmjZ6yt7Kd21Ota/81pTKiG66Csfp9+E7CcrroiFK2O3Yhlr0yAlTJlIaDceyuJqypZu+WCSg+t1dcTN9bGpwa96YFUH9wSPPRflb1kFOa+8Qq0uxLfs5RMJGF5oDByHLf4c6gYSHlaLMyO0k7QKibFkVjm7KtGfcjEkCxX8lLUJvL3NbkZ8ZVaInnj2s87k9VyWoLZrzT2Xz+Oc5vWai/Tjqu8WvV7X6RJ3wrAmhPTWa3k1Q6dzqIU02l2bGeDWioI/LMyVW/ALGxYhE8lX1/mIlolmgsnIdzPqrac0Xbge3dxGmg2gIjbUO3ZGEBSieLMLE4JEzJrI81GQaMxY9iyPNc5kGOSJ7Sw+J3OfTsNimNcNDpGsWYcHEk40ocxL/zh9EIMMBiBLGmRWEP2uPbttWGFFzV1e1YwjGG1mJXMXszx6xrMd9A3rnzBLt9WS78Z8Sy+hAXkLqUZxIHgebimZ7/8pRNU7SybkRAGtbpZ1UY3ELoFRKHdpsv1r3V4w6551XNdnGpNwIRMZ22L6nXxsOeFcX/kknBJ+HgaXQL8dJ6oGwlOD5ezi8NZrpDzPytxtP6/SGPf/GcYBumK89TJUaY+qXxFHuyJWP2q0rL/SxEYI3saQL8HIc7fR1nzMC9rfGNroRtzCakv5N4R/L2+SIP9nopN+kg8cjy9tV8Z8CXT05MAmKanjK7zux0WIdR6gNw3EchUoroGEer1rHbq+CnZtm3xZpXPbxItj3iGa6cuHykcsxZ2+EeDhITWiPLTQgQMc+5bW7LK1txAb2X3LnOmETEVMtnQaGP1rm9oli+A+VdXyrUD6GrqvvSMD+8CqXOnkgOA2jfnGfE1N2xZGZD5S7cXGpOaO1EpvSQpF/EuQwZZhAirrncYFIX+uU7WRc3EQQoNnDansiK6puytp9I0b0duHuIgxTrLJaxaYmaB+QIeqJJvR2GylUUfJ0kT0+Op4pOUyFO6E0zTH3TsJeyivoust1My7gTh1mwnFFcsMRZXgaa1XWySmjApueptxgKfHCqepNQbXygIeBcrFUAL1Fpi5gifG5Pbev68LuspWN7/5cfdECGU6zloL3bjOFR5Eof0om0kNuojDrCVStwALSWKyqY8rKPjabsLsDueTRsqxCLyfOj6my440X+Xx4Ho7Ds2PjbyGR9GuA5AfwSdKpNC/lCOoYRzBkz2Osq+2AjWS36YbvfRbBbAkOe4Aa0oc3q55VRgvfsBAd1bFamTsfjI6Qaj0t5Md0w7Bp0nwFy1b7aGMq7fH2+HXZkRPyg2pJTbUAB5IuOJ2fdzoiBSxoXrQFnDbDqec81guhzl5UYXQZiVTxKpxaRHwNwGS37gEp3ldvf4mum0dp09zH98vxnd1W1eGtH+RPy6Eoc1m6Czl4+Tc6tUqft8K0CCWnwEmwgNt3w+xOgHvjLdaS6wXBr4XZpAQ2FVx7WyHxKTwTs0lITWd/U+C+NXko0q2ocVQZrJ6TeYL1gj/o5WEQ6fQSVtOhd5QD8ky4NjUZA+LV//4b1bUbv7LShCuQ1bphSGJ/QloH6jg6P3cckhdaPPIe+eA7ZWagA5foU9Kv4xBjsBsl88ozEaa+JB9j3joqNI1h434L569G6XAVLgsOyAESF/D2nYSyfsvIx6P026avNxgnUTdkBtJ/LQL2mXhXMUGJ+0XT0iFw01j4rsyVtN1EBqIugj40fFNuMbrXr/WXEWuOptREMi3rfu/GurIDIk1IYV5Zn1nzHjaOEIgvvmkvZkV15U2sRslfl+Fb6SGgR7Z9y6fR4+q8xBUMuJk+Kq75UytDEgu8O+RZHdiRCqv8QNlV+eGR+QGxVJJH5BnJpHX9It4o+oUjXsF2E/2As+gapfkGmx5AF4EBBcKMMkflr4cN/15CSgiNdiDCJR8isw3GvauVGe8t4vJxxeSNjgbbOcZJjwDs4wIt3Uu/fhcEqnT8qb2rkwhPX5jTmdmXPy8VXWDnqyy3URMpkx6p3Ur+x1Jt75yq8+x6LThrYg0jO6QnFw06NKb1+iUSudK9ONyodY+GRrfHPD/NGhUIjStF7GC4J5MflF0PC621b0hDZZB417/TW2nGcYHLHiXADMdaGbU2y1sRj3J09GSJsvd1JF7nkpk4viO/ZGVcAJNpE0EUCpsWXCrrWyptLQSb4x5t7tUmzEmFZeAyBynp5o2wbA3O0XTN+ziOy0PkDNWOzwpxqIjfY4+yxZNKfzlbRpwkxKP/m8K9jNylyuxlp/On5Lajt1U6P212dbpV2fTA6MiU1NqwI/t+t9HdUNh91ZCHLJKOpZlB0bfOp5nAj5KSQqoFGMEniRN9MF16laBcwzRPx6ZjmUYFiekSklPjpFEiR6Zgvo14z/7jpMqJpX9zjOJpOoR+cIWGKtKrFdxdLqt4qrdI5HqmYMXE6Ny5kBxsG8JeoWZ3NdEhSvlYpsdA9+XZfHXP6pudW6nC9PUHbR5rQndXLBWUNQ6PH5i6+zB+K8ORcVal4H3pCEoYe6G+io2p0UHWhxEyt3dNRcILJR+kqVDibavr57INR3ygnmeaIUNb/Wmpx3h4z/g3DuYbH6FjrL9znCSOga8il4Dm6qPKOfx8YsFD0bhJje0aJdfcLB9UvaGYcr4p771VeM8oUACcM077kAk62Z3ozu/qgO8O+6js5EaVogMfYP31tbqyveJRGoxiciUIhyioaBoosfRLZqyRkbrYfARQ/RHcU9B7JGUVRyTi64uphNr81uUS2/DlrkCUU8Q5RRaZcw7BoHYRKuo6Iynaknk+rUFHBgU9pCjiv+ew5r5v/zLMPsxlvQXU1EV1f5EY13vx9YDXwqcEiTcil+n3p31QeaYA9BsUIXNzBOfsV751E9FqSZBW1loy0OLdSVEoxoYbkZnUTW+qbwP0zc+971sBg2IH5R6yEADfigE9YPxOY5rPp28/NvstOeTfarXof6pWi8zyzX+S7XCH7R7KwPi+1Wr1eMZv7KpXSNbzoIbS1ngOen0Me4D1RTroZCJZKv3onudFESvAGG8LMJLD4yyKEb0qRwByr2T4lXZjgjbUY6nnReMbepacQiWbPvYuCtSZN9RkTrkyvILENzUCfMZaw0pXUEQNHi6Uu0yztO8GHpWqF/Spr8XMIEkSbWf6UyYrVAp0Nns1MMZr0KZJNOYM9z5+OswRsYeIMqObSKss3nlUpbE06xHfdoRNhFJUHsWk/CEd9UVjnJ2MN0PIYuiU2ePX0/MSim8tP8BKmTX4+DU7nkI6uYrGHGSj5DFg5iiV4uCKf++Dv+RMu3LBlrclj2N8djJk+Co3PteL1qH34MrQ+Imh0l9dmWWsjq+FDkjZ+itQF3BNUPt91/1x1nh/1VUFuyGAvIyhKdiFpMeMSV8wvx2IRDJz0POwylykdSIhvshxWmlXtbLq+4nVYyGZd9A04606i6Ap1qUTAb9rtenRBfEwCxcBB4Toyof6w4LbBTIVHu3tFoJaqWi2XUShBVUhyYltG0Va1MXFYKop8Org+9QaFDb6I/EQvaNf1k6wIC9jAm0CIJ3B4TLGH6vlgZZDC70bXuzNbVhx4ZlRMxqmaKQtZinIUmDUCX7l/tbNZQteV4yTZH5TcjTzBnspqmUS+9ep9IINKosnuagWd6Px7VZTSQl9+aZcOnRwV5EYt9068D3SrTRUyiBi8+Lq6lqBPf51C7MHYgIkYG4iCGESNW8vU+yCeEcCLL5H4WJa2Enu3OUeLkNUZ1hwSUrsqYvX9C8CyFHS7BnOzDhZUFuHaLhAt0/e4ZyB9uZI8QWIHQvoiWXOALxT+8cAexqXMpzkADExwPzRpTX1GMeXvXcBbfApRIus+TK8YGZuz63fjCdPlxKmyFzAut8gL2n0Y0+90fDd2aj3h+t4p7U3s2eYf8B03cdlTo9zls//eJs2wQTw/vvXnjhty3nchuzZO4xD/uxY1nlsc7/O1urZCD5binFY7fp+drIQ8vc9M3bj30P++fm7nY/7urueHcy4zfVzVgjQ8uOfB1mvLv8d9J1zBKZ4zof1dxAIfRQzz/+fHctyLX449Zr23b+hfz635/Oan38+9/sqzP0DZvpTyMc+X+fr+cg/9wJ/vnH9eYv8fXv8vZnnHQT8PWiV12X19+TwPz8YL382lP868q9s2O98LxA8mbzr/nn632sIqLP/zR1Af+8g7rb8z8f+AWHd+g7zFD8aAyx/h+TZ/N3G9e9A/Vvxdxip5yPPrrif/vFWpYbhd/zTfw3t8Aztf937fuP3+3eg4+9tvgdKxi779/M8r8r3r/EW1gSmeWzydP3n1b1Tgr8L/POZP3fx79cKPc/j3V6tffdXSP6IzX8QC+z3895Q3XX/YXuBvv+e7XFXl8OzLX2E4BVP+n3OdRp31N8dfZ1l7wnpo6rX3J7i9D37Mcfv7f6kNs/+efYqzsbj75suTvLu30X7n+f+K9//t9Jczm8l0X/f9/fr/wuR/L+V5v8uuv9JOIH/Lpz/v8km9j+Rzf/ymN8BnP6Xd5yOw/DITJz88+PAfx+J/72+Iv9ZXyHsf0Aw8O8/IPHf9PfBV/8T/cWB/wHh/9+HScCIWJBcpqoqJEW7kG9V7d/A//MwPdI3vS9/NvU/CLLyCqIxLvVaj69Ar+P0fxTP/6oEybiuY/+qyzK9CgqzRX2+cv/HglP/3Povm/68zuI1flT+z9u3lH35D4ipPVq3DkAWyvH1E5rtVpxbPq8c5PnFmgwVPn+ZnDJB6HlRPp7kzQZEqPwiGdLqPNDtt3oTbFqqpYiSbcdt7bG5OFrlEpOnt4rncKgpkN2V9ncyGqlHctaAJoG+OuhlszWbSoyAGDlnb1j2rDzX38jxc3xcutTy4KTdhOsZSaA6SRXambcpTi7xfQ6CQd8edwuB5DoPw8/bGRePN/JIfl5gSKDdjePsO8e4WuQwAH86ZPLvUnh4aEbAyfRf7qjufQ1arXDZCBU9YynrXFLPmSTpc8jcTtwZOflHFzGX9yZ9Ttr2EKMKk92mNH0G6K3kQ49v+qRt27TW8Bx/sKnr2yF8MOycpAv73dikGYZJV5PVeiC+ImO0UzNuU3t+3b3ZKBEFZWtfw8JQeU2PxcfdqTrKm0mKzf1zda3ICtobvRyii0UaF+AXGvjYgQmpo0c72UVsdcUR6YTT+j0rRNtBPYgIaXRZctaEXXD4ZkW+rQLoi77nZtFK6x0O5LhY50qm99Byvzvcb63jFVqCUonsAnZlqOqOPMyCFQSNSwOC13uTenf3FfWt+44HFQ36rGlv2t7pHY9gI59PP4S3NjhfYXROaZYWc2Z79fIFnkNS5Dtpi1JloZOgD+WcINsDwOQlJbTloi9yGzDK5qg6DoREsIs27Cnc+wo6hIQm9vEggTsg7ghP20rpiag6t6D9ycEp2kls8YuYqBURJvfeKljpH9F6cf38izOmHodd1Ju5oIpuas2eCdgeez/n8NbRdoWSvg6Vajtl9Btq9fZjjGodeuVlDSZe+WqHEQCoAF6WwvX8tVi6tghh3jXUg4+56YLfRKDACEXFQ1zB1FIoXvyAh5gykJABynMNrrd8ydiHy/D11QagMI2yujvMc2cMzxV1PRwcKkut7mRqJE0qLsiVfnmBXwLQTquXLYmJkCVCWghmqUbCw74DDz1tAYTVzsg1ZLKOb/qBeV82bSWEWl8IJYgqjZkcWVdgH9ze5bo97Vpv1l+fsN7cBkUi3SnbA/9zzBX20Y/btBcXTWtt7++j55xjCXKoqsqr7RrroNMVs6rb+e76/Jtf7G2OVQQcyqN9eNcZXduXeRUrAQrhOx08GHRFhR7yt/+tx+WRhYtEpCPUh8q8kWL0op1+51GRoN0PQoDOw9SB6Iu26KF1EcT4eOcEz6Dmb8tlHkHLmXg4bpoir+YspCAePQq7eNR3YVd/gftN7J3cnuXNIW25+s1S+NimldzbiDRE3bcf0JUUspgvoDpNAjBT3LlqLkSutDmFsqo/3LBT23dxNCq0ul9OwUTKqjc+ymDUjSyMcqeDk6bS1UMHPoMqcm0UBwjnoYIRpVhCiZDk0zYL6d5cSLPfjKkjoO0lBBtbEe9MLW/d7aM3crmN0Rv5HnREM9E3Pxn7iPeinO3Ey0T77bFAPj4tP+uyFq5y8qhQGqhnN5ifkBT3kJvc9iiM2DSx9to9Jpj11meikaa2FgkxOLOxnkeXqmqSikJS8I6+Z5QGYf6brfcjuTAZs6/0KXIluKHOtj+/cxpzdjS+j/J78hLumriG+aNl0ub9FnfUbsrizStZedmfxhBjDq77GMN4GtbNKbihEnaQUhMaZ1118g1jTqGM3aJa5RajTvwofp9PTJ53GyyGgbeLyq3iSRuj8Ru/z5/YPTUWC1IndBuLr6gsOzoYlCjV/DyPQAzXe4I+Gu1NABgSm7xkPg7N98yFDeeJxtf3KNIjKgxktFakwsHqMiMjwwC11ZyLFyL5sGghHpAxeKYq9jd0dmFiglxuADSLb5oHmXbDkSmUvmvIb1QDaJj+Xu6mi21kKzUFC3X3AcdydH05S/wpCIsYDrff6hRPBLTksmcbkIZrqz4Ze6gcaSSjPW9WMRfWF3B7Jm8zhguO0s6QxShmoy15NOVtA28HLcyyWFXsXZC60/UM0alVm3MbQPk+xqzWbsJg8Ikyi8ZvoIZ3Nps/UgNFqjpklzfSjHhUbb1InW69sOyi+ijzs1rSRwJBexJ12S+Mqzr45s3Kpn/3NPXo6qrgQvueHRYh/hbELx4bpYxJaA2cO26SidO0KKcpX4zUm6xmnBTKMArM31/3e5YokiUIZMuMI3+MqjLuYq27XwIS6+217vx8DXvFaISBJqm/K5wv10OmAKudNjXdER597+XeBY0Q9Inu7qVUfbrkNDqkXw50znuhvijqpef0Bsm2KdGCTWWvGlziZXaTaAyR3lVv+Xq8H7qwH1jFDlpXguE19NGb+83z2qQ1uYb1q/fraGV3fpXezSIfWPz50G2hwqrpZ1h1027YCCjv8FIYjYHH4F0speGyRZYUgH0ptvS252NTWtTAfqZsdY9bRvhMB95pac6Pn0FJ67bVL6iH3wCIMwBUK9Wzt2A0hvyST59fjLClxSk6iqnnoZLWMnQkv5ZfUOG5HfwNRHfit1Sc9oHE9a7g87fuqBisiNshKmlhLl6ChtKrlXXy7Tf/9jZ2qnPKCBDlNxX1rvfFlq8xdeDNUfktitN1gVV5XvyY0YBtbkOznq/DDzBGBF4HXnMYQuO6vRZa50ABZ7WvSD14B6zAZEC/F6J82RxvpZORZFcC+fTrM+39fRsk0qZDZVGVEsLaLjWzLUVzylRhu87kadlSbUwltlfa0gbbK5HEeRwv3nfxTobchuQVjFqeNeGZE3VCt4OVHK/FMX7IC7lNwqpfDDgEA9gJqiQepxgJtV/v2nHO3D0aAzCiHgZeqtMequadET3wFJlitBXrXyORO6SRPwBRtCToIuGpbWa3pIner/X18LwWAgfusSSv/qpWrpvO0GlhMBjAaW9IX8lHi7/wMugsXmR55118cB656ZON28lD40nV8tUPlEXc3kaqBOHosrkKNL3xDWO19fLqluVxi6SZ+TF8QDTIakdObZNcGkwpNLRZbYWqvAX5XihoQOkNXS5FDaljyyDu3WkeatXeifzdWZ6Byokpi60NIKKqC259o74maArvikRYQqvTHxwfjFweRDc+ZTfWVVebLyLdsN68cAsYPUGuTVOPzc4+G1zyyO64MTqBsLfxMe2OrQFn+i2j3KgH2Ez3FWh4nRl4uocjQ6ZPkgmg0WNv69puvWCQ3FxAkqaUTFmeoEWQE4ytbP9SfKJiAqF2a0XqIV7OufkNW7Vfcl/cUL/e+Vqrl+JvsIhdcRiuKucuIbWdcH1xOHYBSedSSP7e0xwuXwmL5PUduhkVE8ZAc7OhkLY7G88oH8wS55f7+GSbF33pm9tN3nT+jiuCNcVe7LKmzqbIBxsReZlKdaRPQjE2+jcPWV7BR6Ejtg0UyFqBB36vp/Mtvgs/Qaw8TzLkTg6wUC+g5Kzy/sSxGAHhQ8FKoSWqHp2o5SinO2I7mJXgg6U/DOAZlNoUYqAN8F/W8U4NRB1DKN8tr9878XkXZbBfNU4TT1HGtm4fiuglPr++NaTCyNsB8nnGjGxSImtp2Txnv9aVNLvldkqvcRMArRCmOVvocTCG0LnsB+Wj/DXS634yy6TxIU+AraLIgBRuN4JKLwFr+PuioPCw5s9k0RvFfCxkt4E3KTS+q5YuLKBSEOMSr4Thc/l+HkksGE2nT8kRZpBi3xxzt3RqEUZNPJ6lWfjq0gpPERIflEIeDIPHwcqpEEX+O39KJLKEhmIZWzwFMefFqBuDKhO7n/tQiKAx9r5m81MI5+HLJ/ixe1vW0vWVCwROr5PA9GnZx6cgfl3u8LTvg90fyGGVqq1+a1bYG1MgFBep0qpsGFlcqgTm+lz6MkFPfGQt6IuzkK89xJXRshAaZZnBb5THGuFEH3vkh7M2xj0kN+vvMRFhsnyvwtFa5Et0OhqftVK5TslZCU6I5wG7Z0WMW7ABoXS4iX5NZQmqk6PmNsIJqrHgtW6Bfm1L9OIjB+dFi7AY/HJz7O534ms56+wzQxKbo8NzQ1OyyMvBW11D30eDffCeKUTpAAURihH1rEd+2mMYU7Tpay7rXJOvwW78xcwjY4G9ZsiEr5csVoF7NsHenfBPDcJFVTB00kkU6cTeCev7tfA8Z/aSfDxeMqC0NmltVUBFdGNk6LzumwkcKerRbWJoOGQl+ntqysDY3hoC3vcC49nqlnA1UQZGE5WOxUnSn+HRXJK7l/M7ppANzI2nVWRTgoIvKDjLCOR0PUDe4Ah67LiTNbgp12TtyPyKvPzBn1wymuuJC2d553XXX1NtMIkrK6iDuYNrGunZP7zkYP3G24oJYzNrL6BPetcKt6ggMbKIGIGTzL88PxXfquK/RtSrQo+l2RtZPMwqiDoRUtTOm9S+4KphBNRp5hAZDe/y8Zw9tI1IdtnkLnnSv4rY7SK32fZ4WgH2eNPzLW71ED4ZgOuAcxdi4iCifA1C1FjTCSl9Q7KnGCq6W6Vl92vlQy8mLYfum49xRmoSp2JioiMYzGhfkKSDr/3RNJ4XV6wEarfwWaih+FQLb6raPrNeELDt4Yp3hKeY5lSaGc0dlB5u3T+cGJRMQza+C/KBtNsV+2yK5UCcbw3DQiGd5PDKIy/WyZXH8ESlPEIMevJjinEIzabAXaNGiQk60TUQlgppgyZwcp+UB5oxlPdFf5ile8cEYQTbzluvG6uyySGMM3pkIGh5qP/stkyWkNEv2e0pc/NivzPAt0+PQLqumAJb6N0EohaPvxOUPUTpIkYHj2QdnKp8STj6lNrj9Rc/MkOOcJ4RFoLmQvB6wPyah6WdgGcuhq7I7iLLEEcdD1pCedOKJQqkqER6ILii+glHNymXVCN76BEyy2plJzIv6JG4pOXRthnl5Kcks6dEvcLfV1Lq8kfIzVVVp/M5xY7FvgkRiOWoitzndOYlA2WWi/XrMAfWW6nhJvWmi0+GxWtRuuMLPggewqdlfhRtNbSbJL/7zbMRrDrG3VBdMLgm7APVhErgPGmVR4eUUM629JW9Iy4rpSgr9Ckw/c55EN9GSTIlEO5XzWd9dXdCpYoOLjcLA1pw8bkSsb5IyKzxg50WJXxtOEUzIa7nARUuLznNjmyR63v6/EXRbDiB3cZ3gW5u3/w8UJs5U3wqz150+wf6o44smucC/8hcZfP5lR7bBVPawvZeFy+vibnPQE+nvRCWupP6suhGDw2c2ghZcPpawuEN5/YJjQs7+pxCHEkO/YbnFyt1Lj0NE3UxtWKMN5Ze316tJ/lRzcvVj2lFNlYcYML/Yrfma91eLiwVGfnJPpQkC2T3ensU8I3ZZLVxnZeAaB4ikJYf6tWctFzXt8yyCEKQ0xLR0cDC4NqdCdPUqXFv2TZzFBK93mMatbzxNcXvoW+U6cUmGgKPS22oqb3gVmv6Q5waFpI+uzH9Jg5pXivBihJFtHRFlcVjRb1LOJbZFuDpo3JLM9zCQ978ukRIPaf+Fbcrl+kNN9L1XUcKUYniUCupKpPdzwxBWdHFxNEHI9n0DIipzLIOX6tlaT/xTdfNkgdhbLUo0p3o16vgr633VUpcSMMLx/TpPUtJJLxEHhbtmJMlZoiPMshyuEhzV0wx5F92VOeF5U7mmy+WoyuaqfLNHm+8nuReBiIOT5oVD5afcdrOo6aQAj6jhOPr/KTcXr8302nJb0hqetVJDu8lezZqiYdEMYd/Q1RFVUWa8PgyXuQLhqt1fNJ+dL0KHYgM9SsAakXiilqZW7cUXxXXC5oXWb9JefW4EOcuC/yUihw7ErSkKLSgTUn+A660KtC2PamvCtWBSvXHgfYZalizSz4If0v9wlpj1GOq2wmpNgW/tv08idkBcMbAZbbULPDlywW09gJ5XF2DDQ8mgjVVohGeqbdVx1HfORVdlmx4LuoT3mGN9Xn4g9qxZ9esZsfNRpGJWV19ac6a0VkPrxGPjdSsj2X24GzlK2WUpihMyGBW57tS88GYH68UfAVUPzMa16QJU9mb2K/tHfOg1rnFLHFaU5VLqz5rsLdY1Qt+S4Y7oOSIOJ0nMO349Wk+r/XDjO6vP9kVUN60HvsHd+c+YMQ7I4je+N6gUBFJd49zCBbS5opbJ3jHkmIXefrkKI4Adw5ZGm0Y49a1WT8IeYSXhQsgtyfUWPVEzn3DoN3HVqo6A9/U19gqW7zp+zXz3wW5n6uJZtb3pXe1sDW8OSe2tKkqgtMxLd7sa3vthTVUfP6GuMmI5E0bG2SitXlVb9FgeoOpX/wTkRSDpm0cnOraTY8+02ZkNDk6XHBiahGDi+r7ns4jRaX7e43KV0iyd6UZPwA7OnAPJYdZdRZC/86zcQrG3AKmSfmc52R4FUOEqaGMeXLXnqxCUbU2zi08Cn2u1dvMlL7MLTGp8Yjl8KAyB2OypsRj9I51VtPFY/0eoY9h2gX80oQnIQT4GD7xS/WKkvC2Q0DPaVQePtPix6VtygYlzmnnctfUXySuwC70HqG74gakF9J97G2ZojaQ51yxPTjzyxD3o4NWP4bnIOa00lETgfNvMgGZvQkKiSIj87dGlD1t0knXZuWB/vC6P2Bj9KmiAOrksbhapMpZYdrJWcdNLOxwBQDft0ELj4Ql0qjAaHcBn3uVcx/4rhAnVK0z71psk0u34OyVGOiT9cC5c6A2CDQqZuRspEnxMPHt2oFkhSHwNgQoz3zAVTOckSeW8se8cJgnqjTxRlDGGEs18WUoaRaQ+gyOINWVXRCR1rSZCNfprmYP/Rktwn1zmp1FP/m1A8sbtLtEbrgjQgLgDx+QvR5THoibn2ah2/TAtwFHJF5DI7d70FjpztzZicrHP5eMUW0fIRw10ByiCXfIvGHS1y7RLhvBhlJayVvh3M9U0XURD3rmOO4l87Z1NbtXPm+uESTd3w5mLpti7FynCriQyPJZUd2D2lrXVWlfHpQo5qPl5H3mMLAS6rlKSGArM0fyOKvmeHypu0kiVFXOm4+00h5h0txZOmsLDv0uUTGETNObGYxcE6DEN7JYD1PheHBV8uNulADR17eHxdSoSQ6lMWmo/Qe5HARvWKz5XG8wwT1a7IKXCG8DEce4G+DSC1emR7TCinLmulchQUIYgKj74TxabdPN4qgDwbLn4zHi3y1mAUOXFmSp1o5PPd69160vT20TlQD/qtQV3YnfeYhc1okoedfFPfaXZMPwq9DLN0fQHdq6svPM/J0BhTYWJoF82TUJy2H4c8JvlXb+S/CDs8L4t8pUR3kuhBnX18yprjS+Cx0tKP+QbvsQKPFhjY1pm294bu+70rA3195MnIZ6BCle24b5n/4NUUTtduiFrg6Va6Le7ovsAIfz9qDO2DMeDzS0r6IEcg0YM233o2A2NNL6Izmpmugoajh5YRAinaNzqz1TSbCBNCDuFeIlk+ZeamPF3arX76KOzKtv0JvA5iuI2/kxVTkHOvthpMOyqbM49LAuygu8ZU6eMWKBVtRuDDM+1G7eVyhxD+QpDY9npr82xnx6RW6w0IxbA0IJj4Tb9jXp4X2rdduyVYAEYmJcrp+D03BmzQXR9PXVCiTTSJh+GVisI8AcOoqJfAKWnDbv7Mx0AT4k8jHAyNK6sY1C4oE6iH8f1nozo8F9bxobmOZaexxfYuMrRabTyEfXmLXa6X1d45rnehD0VUzLQBqpbtoaoR+A2+ztvdGuA0XqqpPsAg/3DWd6ktwIOhT3r6gieJg7pCgNQRQpNAQs8y6QxagbpgkJwq0F71S+Px8pPTvpa+aeLmDMbsY+oyd27xWdtR2T/Z3CK+vVKwT3OIk/6WaH7ce/WleuwymNga1aZpg13JWDY3gkhqFzXoYtSJ/VzU+zy2NQehhj9cC8ISMpX4Mpq5+sbj8oz5MombN20ain3SZUBOuZr/DclB8pvhjRvRXBRBaqqwclerVsBbMbkPBGWDN1xvfzFO3o46KzBg4mqbxd5w3Hg2GZtkMCjZ8jR0cHekBDhQNmMSFbm8GgU5fA08oACWj6QD4X7Npz7U6g33m+cV/qkprfAS4ieL2jBTPUgL0d9g2Kj0BwyqulNd0RY8FQ1R9WwEta93zECbHnYl6uaSbzxbxkJKwjP+sAmebDcDctHzFaGvahMwN5qPK+cL/tD7eXgZC/SPHokEeQ6CF8a9DQRX+Cf8ruvHNbICZBw1cDSOyNck+mMvVZlk/oBaTmBLmTpukbe95+mVATypQpaNNZESZytZQ5SieGNL8u33wDsfgpntLObhfwRgJzMvhEkB+rQlLegjLtlPYwUBFH9V2X0qRv8YHZ4s56lVtjlqv9Nl5Ob4bjfpjTjCHdkoe4cxSleo+NpYT63jcSaV/1mwiBubRSJTl3C8melZGW5VZZ6cUres0obNWZSpHlody/lulKPSf3eAYB3NQRAOn3m9uM/VZ9VEc4agDz40ED9i267penNPfm0XbzB+4rgm/Z5pNtWp/RNruEUwb2nwY44fydtabB7JyCo7fLI9N9EoG6Y+Irfn9ZiLk6DeVO0/KlsdEuwyVpzxbsuMB+RG02lK/j5YrXjSh1lvDmAHqge/stIyovBSbTvVWNgCAq39XpByNlXOM6dPSyrofH9lR8hFnMunRT/Oo75lj0TRwseuOtLxz23lsiSeV2S5Uon6flfUwrAjoQROjfSiaH8Q/ogS8ktN7AnAOIC8Va9E44yGMw89P7fTUzliFytT7oK0xOj/R68/ctom50FPS/Mah9+47Y5Q8BEfJyR7g++qnoKn0/Gsy7hJBrfTHWQ38XsK3sE1h2rRY4qJpVae1uaSfqw+wrDpRVn9ZJKlCiOWQ64XsPqcOk/WY7jAnSoyrXCGwpUuIDVBEJ6BpAcQHx5se+yH9hcT7RfkkUnbG907wTV/2Wjw+T5Be/1i9fNs2bQZ+veyLbDKBi42rbu8zgd65iZ3w0dDgB/1oBkRh0+DhjYVrvL4fSEhO7ICoQS9tpp7yn1lTSbnBQPfGJgHC0XI0sds778gzE5T583ENWeEM/JAjOluEUS+8lJuRyi+iv/MKjJG9jbTpqJCybkxnBNQcqkIsdN0pgAJLYC82pPBF3lU6jH/tyU15F5uSN6FKh+Y5tAc04J9ymNe+EESMJGRul6Wm5oW2T2sGG5rnP/fHmervLzbzI/f1/+AGlL8K0VN9HkklXss7jkVreqQdNn/pv15HkylBANZ51ouKsG47n81xM1TR74sGbeEQk0RD4YXi1filWiRRBHR59HcyLmrXXhselgfAnq3qUSN9Va7H0APW/K86RNnDcRztEfRojOPcjcvPWDDgtLMN7cblysu5HyMjAsmkBpYZp5hoTZVDSRatv/pBwSB4OQICgbTm+b2BfdG60Sj1seVM+nLrLzMfPbQshrZ6YCmrZION6V+2RH7Agc9/mGKUkjQ1m3tv2tbY394vR4J9NbFxaUFBhHXPiu6Doq3bKreOO9YIRIuiMCkGStwsy/5oNMdMG2EFm6c61P8vGeMVriovUMp7R0UgClyHTsqYRNAfO+gyihWz2ThEaeNE3PhN4Rj1fEMbR9FW0DAo9zkWfvknOVp94X2RLixYxgVXnas+NRWiza/1FNPxKLG+QEhLG06pkf+HJg98Ydq37tHLypv84C8fSZaCblXkyCspLnwFa3zkK3YC3xi7sW39XVsCP+fPlt0IOhn6G6dc4RmnmAcGdSpKuxC6Gddn2/i2TcWm7JiwKMqLk51eNP4qJith0pV/Udtr0xc4GqLx6zmpV2X7QlmwNr8X9aO7Kr0U7/jJywt7/cNOp2YHOVnRV+j235anTAmrrAqlj/RT1pYDH9x7sk146s9sT4Quyexj1KNhktZO1wXgbpSjObUtIbwrE44kbgwx6FHqnzu/Dd7YCe7XIwD4xTL6XK1dGr6Ko+8GawV+pr6TDkIwkr22W8Z1NhpnuErayE63BMyCDVEsH2Oaloecla1hzapRCt8SbyvDCWaR704zCZbZ3bvrL/yge/TivC7v1UvGmUul6ILh3RJ10xRkuCTF46w3CuO79nZgLwRCmEqapmgigeh0n19OB1VzewInfzytMxSMhf1ZJDdG1zQS8QU2C7GOx7/D51sng0VYbstgVqUxaC/h7dhgRaz8BcoJNHypBMjDsLCNrLYxeQQ6aRz1cJRPqAUnKIOvnojZV8Bmdo9WVRmKsUv20D8mz3wV/UnSwOvjot5/O3WGDm2wxyJ3LqvrmfS3MkETTIKvoHM8LImwypsbmgtTDVJHIGwhY7QPcfiUMMPn8FQMJ3iQf3PuobESjK+SHD9Anj7inI9AFBSO9dYldTyDvN2KLUi83mpJgxUmikF+rhsf7dG9tggzqmfCShjPo62p8d4Geb9rd/Rhr3ac4XnrsGMeZiDAm5p2vQnl6Zg0stjCTVsV9HF/VGfDLxzV1pxCqnK6lmPPNr+b6cEQXp2EjqJq3EiuPpOSWYywKhDlz7lnmnVAElIYJ2XjCQF/7cpmaCLsMrHufp8UP0kcAT2lJPFzYQ3ZYTZHnubDnTLyErGfDw+Hq6E2Vom9PeXnPeYc61SnUozEN8ZltBABo86GGU73yXipUD9I1Qi8Ae15NFyQn46Vs/SBLLde0/f3k2a/in5rbKMLj4csQSPyNfZMyaweAHQjCH7b8PUjyMxTwgXUfIa6PCCnwTXpkfGSI9+kQ8aMbBh44KF5QlDompLHdDMSyEXpZiSAn6eGPYFfUS0+jTgrDFK7Jy+re1wkNVqK5UqjEcxgPICe7tbOT81xmCmw2lD0YzFecQOzTii4Rulc9NPDXg7gjOUAHLFFPvCa4OBzZQFKXwKPZAveLCTgeo3e/Kj9FQKEa5s05id8nZFgzeJLG6zr1u0GRd8YzuwejlzFsaR6Wyhha+rcz6G+ialBwkP1V5HxxAAnDXWvsyUb/YoR2FPZ6gD9UmGmfGwuj4V7NPqqGxJeDNSLIrwVH+XRHDrYSme5GgYz5KbgvLNegMtNLjMdO8JeOUkIm3KmU1xqlW3BVwaRWQjRhet2OjfxBhI7BrZk9TsGl1VySV3NHL/O7+ozHJvz7TVjqT43v2mEeYAjDxW9ijpE4AN7P9tft67PLZR5upVDg6poT+2Vh5iuHMDx4bT6cLLoXlAhq3AKnbfebwcI1DhoR+0JJrZ01GlfknCCgeMm9lHjRgvxixWhMmsObWcmcYqxSOMpLAlpYstvcP6s0HyLDLppSMY5fxdexfrr+tX6Tt+jmoYGH33+EBqLNtBXXiXdJ7/Jj4bHT5yzT2avxhM1/c/Qt5ufiGoD+Ench9huKv7aiToW9JQ7DZgLyQjFxZGbmpTB/p+AIVJ7wM2QvUhFuwPEcekiF7zPwz6AMGgHZKuXb+hcsSvtc3K5gEHMQow/+UUpvFu4jtQ9x4LTPpOIheCHCw41y8b50MXNcuGpfB677hjor9OQN0uY5pslLsVSuslbpfEhplN+VyFiS7TucA2zn/ZtVBzs+ls73y5AQzFry7MGh5Ie8kqn+deAUZ5G1vyDQZERPUBRDUbZr0R5VN66anG8RN14CDPsPXHj7i5rt72Ad4HNAoR5inhACe/ex/xidXb3zMC+WXrd2Ttk2Tgo9bk87CwqkfMt+7ivMW6L46MlmL4K0qNNapLLIPkZwABLpfIf+DdKaEPZo6YFMH8FqP2jreI9dV6OLPRjzG0DOrCXQIdX90nq5s7xoWiut2+dzVj0fw/oBP+sxJMoVbL/MzGYVDtj79ZhVayx57TrhXEt7Nzc174DauUIsjjnJ8rXWfjPtXVz8SPHUg3+SGOCKkYXzb4E0yf44ZzHAeEA5BvWBB0U5CVtj79upoOdLZRZtU7es+I9YwlufEshb7pwP1yFHa+yONq+2AqkKNlYmL+EdlW1V41+nIzj74Y6gLUT6HXxFWG7AZJAW/2p2WgXDnzaydEZ+Sc0t3dLqXQP88PPUtGvJS6j6/a5lj8GcV9yRKXCqEyjL8/7rEoKgEYcvaVu7lKEg1uZJ6ViB5d6+wSB2Bz6S+kzwgR/PnELR59LQE8Cha2HHt9QJT9HGQ/rxraLZeRJNAhMJmBiGPYLl6nw5S/FHSrRS2Rd1Vh8MbShyBih3TkgYPq1vHPdGhmn+tSU/HgEd4EnK19Dor7NSO/YMX9BHARo69uO0atwVEf6wDcxOkrTyPj7mwm88nvumbmjhE8GIvhJBv4CI+k5xi/ZgUjFDXww/k70ZVUnnLgK3bkvrM7rL8W0rii2IN1ZIaP7+EPwJJ3bDiCa9uPQfRDWJvGapUfyW2O6KCWU2TY7FIZwZCoxfrdlrnKJRxScNpvcm4I7IclXZWZBMDgL1QrQFiATjExxHX7/5fXhFRP4ZlxuEe4ukyy714Sl8IIkNgsthhQENd+hSqGJ5Gle1UdtHY5ChNwUbDxW+14lNlYsHxrB147OYlp41mGPcCR1rCfeOIKbIkdjZp1bJFnt417Lcc7X2lNW89EXk1FNa9lwmi18pee5grFl4TRY8Jc9NDXALaJFqWlwY+fdYNq5YSocMgYzOX6Pnyw8n+uEgjMV8fO/SLC8YTrCpPE8/C0umEo4sSvFV2dtE97h9VxUCWwiBb+pAkQ7KuyFadedDEhcJVwSfFCjzp0Mo358HvwwB8wGuc/gWIKlPygunR3HIDM/RxIWhwDfIeE/VIhtRJM8j6RRJ+u3efpJMKIE9M3VWNX3wIZVn8w6HX59OpggYGdMCGNbasSaTcpzviFHCk5L4WW3mNy+TFtzXqBM/ps7G5JqpYSoDv4ZUt6+ochaOh71cJ218s/tbLee+0ScP3EYB3egDJF5btQAZgO156qacEVHQoP5aVcTvZHRYfJfXbp6a1JjvmeSji4Z8L15TLjGOTsK78m5f3KF3iCV29qFJ4STQAyG21yvVVWeXL8sSAiwkwtvYheRPO4BPlJ9WH3fr7dvJESecbITVOJq/HksDI032z4Zqs0dSqOz3PN5+zrPxEr0UTVmmE9lY6k5D/N+yz6/Fw4X0yiipXRUGA7ZInlP0IGdRP2KObY9ZKHz1oyUqtOKos282/1a4e5+hMATSuRXKXS5Wpjr43sL68PljJm+f5ONAVotp7XmBRC3lbt/FR0e0bt5mM5GyE2gOFI4Kb2z4+JLX/nk3uUJzdtc8u9+1D06FHTN5ueN+jvP1axBPRfyVzX3n6ivhywi6OznQVYeW6vHa4FSP9ViCEl89xlQc/ssy4hJSEzUi1rZmRODYDXqxfxK6QfQG/AcW4VdK44FSRgu0MF+17312zqBZSap3ssOG9uKV4heZqcxdvMpzNrI2PSBmxryl/F4DLFHMsPxEp6dWG7h3+DZaJUjk6LUODfp+6XqNZL7kf/viSaGA3gjnNSkKh9BGoG00QKdlLIDXY6h1xpHSrJB7LBn6IDsQpd6HT+sa2uIYhqXA0r/FFvijwsK60ZT6LJmWb86GrqHm1+R2MWMWHWt8h1Yjj3j2nEQ9qqvLo1JeUWq2VhDpsV7xB0Tdun+tN0ohx1wKOf4LioZD/Pm81QT4ldpRvjBuchgCIth6GL4DInMAwDggP5oDEo6P1t7ka2U2n16KsA2Ds/pGnetfgn8yDS9BXmt0qpp87JFSnI/K6W7lrkvd371AOwMHS/yhXgjNw4+BhYhwB8Rs00Td8i1Sz3/zxek725opDRpkJGQqt1L7Kee84oaLHY8AJffZflFT1kgatga1bjCmvNjRCCB1fWh28toKQ7vxYztvfPXpHxDYbQJUWITUIsi29zfeNHCk8U3waX6+u0MapB1YfSkEpxXygyljAiTKbYiqPc1b+YwElbwaTP8QOAbmkxOkgB0CSoStc57GZn+iUjrdtvhYdXqBmRgy7/1RqRTMH2XvdRMESQAtHu74ueesMBQ2PdVGQsdkrrJfhOPKVwWfh5H0Hzk60sRoKy/L3kiSUDMQjW/5ZojxB/U7sUN/GYTgnr/O7J18/dwNhQePduYPM1SEE2huD5e9Y/P/X5quY7lRbYt+zZuTwxBEElmARJiRc858/ePgvlVd1bZsS8DZYa0dpQuadK6GTaGos2yuqI2G90yaEsfOtP683xoIK4w8aqbHgx9kOLQgA5XJvZpN+pddzUW5GqQR31/h9PU8Nx2J7w1++CLz2DkvR0CWcTAL6x2379FxFkOrg582aw73to9NnOMYoSbz4RI7HLJhB6gSTfqm828pxu6KwwyewefXEBk8EhRpTuhAQpShfzBIuPa5AuSIH6J0uiHwm0VZ2TgJyVPS8kylLw39THOQ10y6gh6wiE5MGXNbXbcDdAL7KDp5tV7XJHogluuKRug6TJtNpCfBpzuzpZJI+kHxXdWVrMPfMPtSX0NutRriGu+whSrUJZTEbIf5W0UDYv8FAyton8D8tilknX0Z2phXcvUlrHGiR2eLKq5tCo5dkOQ17t0HvPZDGUmMjLqJSKy5X2harTRTAdm1MC9GHUmaSaDdo+02cNXPstf05oD4gfTrtpnoPB/Ysbm9VJFoHuTu7SH6W+Hr3eEAab+MI00p8zAjOsi/SZnq+vqmZj/VYCpiY5yadXfp8zy/XGaemFm/wROnzemB9iaOwmmAE33uRMftty3CViJjgD4+YkwvBBlLmg/md65q7W7zNDcfb8tB5b/jI65VDNDTFph4D7o2UtVCLHV9UvVpmwXhO4cHuUTQzk4XkNdNWw2I3jKDC8zrQvKly0CA7IYTpvqaiaQDDjf8EE7HEukVRX8fkiemq8uw6syERvbyRJryEQoRWuMY22IEAJSENt30aDmrAmlekljEtrDtJHTADmnSN70FQRqfKnlUnOtGW3D/diNV76GqroY3we2jwWGLtdbKF5lIfbLLN5qFKdICz3ZM0RC5MBC5T1hHrYltoVQQXn2mJrogIvfM7DcjFAc+9oihL1+WPuqhA+7Hnadl1ewyEQC3XVvT+jQ9OR7UA/t6hWbIPyo49GERdfIcwnS8bg9qKsDVNBoGL+haT98RI+rHzvIKladlYaMDowUL1TPaujIQcWzRhs3+UUTR/fbYTU9ew8KGW26vwuAcp68N8Dp4kEvph7sCRs/2SviFoREWOwODzQqZEJiNDPLm+6AbQ9uzaatpXNlvaBbTRFxBC78ZrUdS4GluHmqYYNwFP2eZVxA00DE3jigqEi5/yB7vRH4XnaOuJXkBYvBrrAHjARh780sSalD24oSrc9GTaQOtyO29mtuXB89XMztIdmBEfDtMqrbRUprlX57OOupcARJ/eSXJSgabPgtTb/5CYvSZOY7ffp5GW337Wy/G9iHR4FMqDXYNe+3INAdKrUR8k9Pp3bpp3T4r5TLjFqzPvyiNBN/cu2rqyOhUEqV4/J18MkViTfWn44g/TeQxrbfTdE6lz3/LO315iMr1y4IwIb75vxa7MGwZP4usCxjerHHsYbVcVc/ikkwShhac+f1GGNWt1+z3SjFRX0nQQuh1YVQ0qdnrJlLYJ1AiSvK5Wr12DDEBw22TlpYTD762+xW/QytCTtsQ9AOz8y7k+0oZ8Aq+A094qz5eigI8Y2b8cCwwg/oeQdtKrNUb40rVh2QDpTcPuvg6xrmy49NBsdn+uhoGjutxzb4+RwyUC/klGfKuw6Yqw+ZIWpC8mRz9ZFBOmQg9dcrPUAZnQAbKwseirkJwhJZyCzRbwC+8r4hRDT0RdKH0+pYCkMODnC4Qwl0Ys66gBR95HzixbWO7EhGdmTk+dZYKKJ9viclHGesbP+3trT0kvUZYF8ZrWGFL2YVueiG6ZMz9uJUDG7nVMf4QwajCs9JYSdMUmi3nI6tQXvvYZ0Z0t+8DMIPwcVhXYc809T4dPVtfnJub/RaDC+UjNvXVJFEAuAWYYm3j8lEH3ZbXTm59JEIH3n08H2dT/ZS3eUF80OmoNmydDnZ16vq3qF17UVE+mJzJt/ZozJUFYtsV/i2eKy65H0H2Jar7u4RbldPPPwbJ0lBXtgL72fRbPzae++yU8IYElCw/1gdDoN8ycS+6+Ll5QVPZGsEslgGcO7oy9YyMdEnkjasSZDbTfiJJN76ga8zfXjjJlgalwcWNCfqVpvUGNjVr/xx01F/KL5oVHzv0VPIBpvhItDjupoXslzccei4mE4GnYDs8G91XBRLoIqzd/Gj7rCqZ8xQNIk14uGMASk6TRb+BjWgOCNM3J22V31fyJbjaiCPYEXCrw89uLZReicqmwNCjPh+vsCjkZfxOxeYlMQlzX1Zkybc1Dc9WeniNInZdg3KPnTQLXznAcSuAydIgYnj2Zd8eyJrppmnKY0rHN52ugNOpP9+rXyLuSmYtCAzvjV3PcF3RRDKLQxx2jbr9VgL1lmBe8XQT3juAWNq2Rpty6s5Fj6tfUpxkf15nlSgvYlT6U9A8i+fkLcjWYxAgus9NIkQPbTO6jSA8GtHNbl23tIKwhjSHMezmDJ4n4qCo0IFcflDz1etUcIX6VRDYtfVm136LdPuSaYNMQZj4pxFgJ3C1Jx6uVAjJwXq+Ub6nAiPmBYPTbXIU+YHlE59SQkQHNRMyvi0yvsQaSjk3/EJS2b+MaXdo0og3HufwYVfKXe22jhwAu6Df3K9AdJpob4tC0KHmEO/9uEl01k/RgUClnhAevNM4pKUNNaOISHe5fnt5OZlDzZ8kzifOBPX8ZWsp676GVSbuWwiJpYe9fgwY8VvMex3Ht8X9RoUjjbHhQ1phpWdGEqhov5IMDTRBi027PeUp6DMmIzhwRC3u6sCY2WaAJoJ6CaAoaR4+Sx1dYkGXS9Nre7jzwwjbg8hya9UkVmSOuprQVlMd377RPfSR6PjDxpYj+e5taQ27h0AMG21b/YskKvyhbI1rGoAe2Skgfnn95wy6d2az55qjOax/m27AzdgVJQQNPtjPz7+FzD1hAMxxTzOBzUzeXF4LhQMiJkRtSSiq2BbI7l7mDS6EWJiiCxm/0Y25fTEoQ0kG3HKK4vpkbP8VYv8ZqIpr5xDl7srJ9oKfpl++C/R//01p/pEAkvjOQpLQp92umX+l29FrEVlXYsqaK/qaaTMcYNR+COJWELxi67f7R805L8fxnX706idBe3TbGkB3M6OB0ZslO1LTDDcfHuZ1JmclITWwiv22zlYW0h/pPAi6Z5gfVbAirvwsLY7t33Z0beX3cM/D9dzZ9fzNpkyi9yqlcMqB2iuOI5IcwTZw4ZIwdLdPcJ6dQKyiJCqk025ZkL5TEpRH5CdNNpJa3XxryzSQZRxVrN6M46qGUQq7DRuA54joSMBAmEAMWSf+UnxQmexBvcFnCAxCFburVSmDOqfRlTaHUV6vRIKUAzj1zMgASpKk8UxSXrVTsbaj+i2TrYYWvJXtvYdBn99FGsAEzBotm19DhkThmNQ+WeKNg8LlmG4+XSQ/OkzMddQE8vYwooqzgFJ3rmutnkaUWITGIE4PPreNuBAVqmArAcxYFYTWZwb8cLstB2IaHTCJFKSpGcwtEbktOY2KxU7fLtfLfsPLh4cBfybZJwkQ3afJFyNbmB4t2GTzMrqagH7qE229Aqu4hEpHy4Dc6nXr6i3569RjU+CSQ534pOKsGJVIkyKCOmRxUL/bD7oX5t6EP0gMH0Y/hFpF6ntP/eo1vzOK+6LMu3JvqrRpvRX4w1zS2W3dxzw/GZD5vTUvwrc9oo1spHR3Z+bfszJJurEntC8GeSqV+/MkGoxAYxXm2JOgTsFhtKP9fZsbG9JkhAzim76YXhJpzJjQpJ1Z80/yOUaSUDre/tVsoPqwxihT40ntZtA0IiaNxDe/8JUF+7Lm3uDGkNKirqfVJ90eLgjP3URVH/aqgHFRIpETNk9OEI5zdpJOVC7PwRXbrU8bD2kVbGKVPC97mfHgWNU30CDUk0QMNb/pMS5lDPvDQgM9Iyypbn9REdx4jyQwC8Lhz8fQSNwDqN/AsopyF++SpONQucNT/VN9KfBJWZtJ0Ch9jnj/oUiagrYTBtlyEmGzN6Ncusaax7taTcmVrd7+spZu6hfRKNCaEdCLw5NcJ0QThmDym6WLRbcDEQnij+qS4atq1xmjK37uH4m4bS0d67hHA3YzpCp+gd1U7ADDhozWQT+J1Dx1Jvemzdehczhltoszp5iDZ+VRgucSubfYSqfkEdfxwaJu6BOFJq7SdJbrhRE2bwxEv2XJJBABBf30J0KlFm5HGBONTRNbkblpwJGv/bpld0B8MVn16ch+1OUi6BMZGFHI+YDdiyxe8Z4DeAh98YkGnuKtQBBQ8mx9ZWZ1JZcF3MGihZsV8iy6pC6+1isXWiiMy7n3AzBFaY4Jg/W3rnDgLahTFdpM3W+I155X/jHMVbVpErSTN1UhhuIxh7ruDwLoVd/RDm2tOOWlUgqPk4t8Nok5ydAjTN+vg+7V7MLmCslGVGB3WVO2gRzKWC2TMaQzQ/PorozT1iFRaDNv5DF2t7tfC6CHujuN6Q3gRNyi5QcxSVeRlS5iEPTmm0OTYMseSig80lCDqmPU9Wo8L31/6//NgyNyQDR2M1NGNX4wjQ7k33R6AYlTzRvM/QNJnrQ6bIxu7dGjBsxdHs1W8pQf8+JAhZCsv7kYVDrCesq/PjG/TpvIRl6lEJCsj9XX/MobU0G84CDSxyI5QqvVDdjkVxTvkDsnE2YKgmkdcuBOKjZHJEx2m2OS4VExCH8zxiVloz486Gu/URY4xyeI16UnlvC+N5GrljjYyDPWn2naMkxLYwSjuN7oiscPYvWvRCLbiG55bvHjBkkZpxTahcrb9DLbAGRnBvuyThgLY+dKkGmhBwHrGu9FDy434YSdykcJIvQGx2WyCEzxyFMSgeEgwAz3DGUuMalPUDkZBEvSLeX5CGlaFkoi/bMHSAC/6LOr0+PE7TNaHG0dSH6K5+uMyNURvqHJW+fa+syaM7qqYh/LVU0/Fl/JEGzON+LTxSKhSWuruksgUxcgY9VRr4Gk3UuHMRA8YNR9S+Jx9mzyMzpuqorrLpghk4JTDZgFFRdalKQbxeEps5m5W6xFd9ycJIX6bTl8FFuXI8GpPc1ieAi/kbdOEsuFhWfgWQNgcRmiWPy7fUFPnwEBwx8cJgJdzGFH6G5iR38KOs9cFp1VT3IMq0ydD2aGwROj07zF86zx5VwxNBWvZ7yGBBiIRc/bqnVLSxibT6WnZ0Se3qicXOnMtbT43MuA77LTVH1/gt4iNLC874g7DXly+6lwbXfv6QxpuhKlWu2m2sKgkG9cVAA3RA0W/UiggKlHKyz0jg2LC2mb06ec8rp0hir1Z0AakCBI5i69BikGCqtbKfDE9itaCnKrWiMeOwQcupC3abtzPlzXZMphCjnrMR3R8G1/tiLkUsRdQZGNhqcERfnoDCkh89Fr4YCT27IXIC6XLOsGgZtyLHauQf2AoGTVZ6QiD6QSeBT9KjRi9ffTjl3QmMyGq4uEEJUtR1RQ1eCv8zVtFRKyu2q1PGF0VTcfv5bkp29oCFhVsh/l/Oh0vSerG2QCEDDaVbiF/gbs/cQh541lcpw1THeazyL3VsxusTB4l5Dg1EyeWgrR7Mzbc4Mh5M/egC8y1196RtFMn9N6o/AlOPQgfqSvOm/mW4izykmECwGZY3pxSzA1Tz2yN5u1CP3P6xpyBMnBfA92jxFzNPOIl98fDeIp/Ck1cCud27/JhBuqiCLjeXtJED5oOE+9LrIOt7Va6S4Lb3IHXI/3bWF/1mnPQjpkyHgaA8sLtkP/oud14zZPkoGTwEp6tUkK+AC2ZSww4Y1FyaKHl5TYY14ylwy3zOQGKQsifzMQNPxlmRtiQz+0sLhQa7je4hozHhqfcticTkov2Ol7nrcK0lknHVao6By9ABVbODLOzA6SZfpSSGg1hUNwcAfobHoMABC7RwZLv9wBsjAshqPTO/1Fh9WBcR1EC7DbaxIGfa4QHEXt1OWJnjmLGFDqhzRwtfMXrAU5REHd61wO+oIS+fsa4W2P3wyWbrNBPmn2VYf2la3AdFRSbiQZB9dgTswcYc8s4aIMy9+G36bXG/QuphI340FQckdalU8iCrhsWjyIz89DL6cIkS7aNKRp3AdfhGggtjhgPG93G7Bm6+FGIGDNg2YsTJDqQH8Aml0lkOnvFHnzBaynvSCh4e7HohF9SCd9NyTUUXvO65mQPb8iTFRdyKsVIukM7Ql58saei6qOxqJmb1PS5dRvN9tn3zWARggBhEEo3aMG1OCmgiz5V5LBrirFL4oEnfTe2g7ZtyeyhLLjBc4UoQzkoFqUoDSquY42d1yofuZQveL8UocddYkKL9f6hazabYZmsSBv01K/zYNdUQMVvCzxMubKqPj2xNIL1Gixo8DR0e3rOf7HU98ijp5gbjmt5DB9O0311U6gfq65utxruYrdmBZk/3qb97CkhACO7NbCC75IJN9e4v6bAPmLnQqfLbtH1hTPI1NZiTEBm2BZQm3Y0MGnDQ2RpcSpZ0g3sCsIdiVQZdFciBuOEqbg2Rrr3zpLgbwdRznrnQMQLW5eBEJuhbKj80tsyC/BA31uJfQ1OeRPcpvdjA8UIGjpTb+QLASFs7J7bS5M4yAXBKSLhutsoCs1jsFh6ySWTpdFPHOId48ngu5c2kX8xOpEds9aSPTmhMlmt4JMqUZE0hxGBAbZYdGKpzq8ZMeJgyN838TISmK9hSEBJbwi3bmbRqU2TiOJNA3QSQSDcf8QwQlc/QRIZIhZOAfSmSRvQI4s3M8c+pkyQo03Twn1DJSomZQhggFMcLAazpM+s79pi5O5JylXwMg4CmyR5+Y+28pEakH+bj4K5y4hnsDvFZ3umz67iKMdJCbmKfqmHMd9bkMH3t2sVmIW+j1ygKWj5pURq46imxvCi9vPHWAmPcxpZBg8BXU85nWzi06SgUUjeXrrslzg08XnytthnZKgpPA5X6vvX/lSgbBjxCwZRQnxizsBgMGn1YhQh3jh5N/xGynqZAVxyT+87KAd3L0eN84H9UBzncDOmRG1FyazUHJ9w1BqWdPklEYnlPydCpYwtZOCwaNAQaZjOR6t3h4njG6vBDSUpKE8BHUXKpWl6SRAfwVPLBLeNJB8LiF1k6tOEKQ5+lQqNrnArZ1URw7In/uLUoQ6Q2iEaFiHOrukRNTYmHUDg3aF3U6ADaVukT9BgCXlHNBR1wPo5zX4IW21WY4o/pZAmyDnk27o4tMwVR6Gj9vNQfOgffVfayqGrbMccIxb26zj0KcB2pBGxKVLQq9Yc114nzdoFiVBVDXDthMYl8Tt7FUZbq8ZCGV3EDRTqRF6iayBTzuGE7mNcCZ3AbbJ5haf1K1KKFOBQ0wk6QbUHnCcKHpd56fe1Ap5lzisdz8EvFxXXAnhlxaQtyJzpcF1w3vKzZ3ftYEkz4qFQiDyxioCH8Eqql41ZUz5dcat5J/mxn/MFW9wxGurwci81lXoJQ89RAU+Lav5DvduKEFdJrcjdODTnSEuxHxY3Nllse8JPli/KYiwmeoVNK5JvG7lavag54wiRdJyrg+BVIhcxdjASATj79RqHFUyoqQFbZR+ni61CnlGkiTreNQc68/MMYQm4hbofnpWOERizu3193F3OaABzzrmv53QbCEsaaU861RN4sOB1BcSsdjLfEv/ImYcdKz+kQQgA0PG+4K+uvvdwy2UBssBfwh3HngYFiZFE7szMH7tFq5fwNbirJWGaM0HKIovoGv6AnNI6C+Xadkz6pTccclMG9hMNvT4+0DPf3WYKG+S1b0EfckkdCEDyGsbMqLiJnfoOBz2+ITb6nYBw56fBjFHUjuENw+Ur6/BBZ65Ic/S6KWSr9j0OkNpCywtNd/kdcgLKiKKCBiRtVBvE2qaJpTe9Qv6aCYX0V4eGRsoK01/t/2GrMgMI/EAjxIy3W26aheZPQwFetV/0y7MnY80ZyYxBmkMITBVcehb6wJC5/6CHG9TB/UJwpzdOoeGkA9BZZx9AAQOKJtKOH/NhnZsC9oMoTi5zdEuXREud9Sz5UmCzQ787tO2NikCta3hdvZMkgmU+2VWP0QBtyZoTG9qg8xGRI4D79xw8YZmHIjdU5p01W8Wa5Fdy9GqKsUDaG/gHw611g1CZ29TNsFGXUFFqGK2vJvEByYLnzCEbslX2teekWY0PXX6wH1vAJYa1CoJ6NMHXx8MSKSutgWgYUoxUH8yyQ5A7KUfZeTCPrwNSqv5Sil2foUxZjIhXkA9nmkl952CsRsZrlK2zAuQCl4MbtbbHk6MZMVwOgHRkD4SSeiFJkuiRxCysAeiGiSOwUaXbToIemOTOa0dClacsFIW/cxYDdYt21Fy62x1cOK9Ik+GltB1SYOHW6Rby0oEYqIu9UON2v3eiAitMYviJRK6kZYnaVRibiNDM1QCmx5ZMFpwQDy+wcdeQ5HvLz7aPq2pXIdOIpR8GItspNgtMO/v8P0XETwNL4c6EE8lpYRuQGeICoNPzsbgjdbM42AvaZ7jZfMlXI3SmH6JIBAyjRE5DnMkFOPUX4mzEnry3oM/cBXUR16vC4kgPlUhEQdfXkr2B2WjLRa6igg1094927K788TqTGLmaRfmZ1kwCnwnecowfcBxhHba5jjxam0hjtuT96ZUWqnsFacxCCx+yWQaphwQb2O3122/De6zE/pt9cgE/bCgwJZlUDx1b9gdH10CBV+z7r5zqGgnhlcB4UNyP2vBDNqX9BCYzh8HJGb7JTQx9E4loP4yldsCo8bhSUuTp29nknH6OH3HQSHICz3Q0yJw9k3m8a6Bz2UOSUX73ChUxCnTV9IJlmhoWT0qkMF5GJH4b2X0IoU+2xdd6XkXBsPL/uT/oUxcqhyBCgwp6Q5kEyhXMjm/W8yqHgA6Axd7oEiFp95YxWDAhJplByIUhz/FoRSi87NyWbvBhdlt82QVS9Mw9oanKDujR/xny+IKyud0SdF6C1Z+SxfFen0gcyD9SruOrKATdPU7Zkkyaltwd9sivGwz/zPH6ivjiHYhIlfnqGwD/gAFZIWdviCOAkx8OIsiccHtjDSpqVXUC0dzJxjScscudT4pXPQ5uC3Fb2YoRz5TG15BCuDC4PSFcKQZ34td2U0PtksJhPuk4DYVY+twKgGUYjYy0Nck7OB1sYAcuCvvf9ccU7MsREDT5uTNMMybYWWLF761HNDN5YkaiSfBXDzB8gsrTpDswHSmDR3J2SnDw6J35l6s5rELrYSQ1Nq/k6D1+B1PNw+4qA8FadjL81BF0SFe1Mt1gTNFe1XhhU4VMDMZ8zVN4Dmzf6XVeFRd6e3NjBSo3ur61N7haF3ft6aCe0ahtbu4RnXqXfYmduM8xPF1hQgpX+hVxMdWs2UE/EGhwkZmJORaUM9sL7VLcRoqXscSh4+njkkfuDYGkqXx44YqVYEABZCXTsvFJUDNTHoRoEyVHbh9Y4v2WtMkuREUPQLqHt9PknzKCaFlucHQoe4O83cTg0vHlPRjdB6lEDTAwSFAvtGHV7rUfSi5rtKyXmhT//tXqjMEz73xPqDI1heYVK2DbxL959w/CSY1CSEC3N+4nPobs9dS/qD7XOPqgqbtytZxWQgfnZW97RmGijIcruiW/MKHIvgToB7Pqpncz9NADur3BSSbNTZvSobbMWxNAOgs7YFnw2GBd0Hu1pl2HMr4kE4nCFOgmidJh0XfgnGx0+YB4dVRKNG025KzIfnS9+zTpxMmom4Nk8hW7VB1vM/PU03gV1C/PN2AS3JbK/ijtnJuUQkyu9wWLFvqXRpChnTo9vSpnhN7E4JM+Ky3Q21rsmLft24JbPxuDa9oC9yIkJ8oahxyeFbOEVgiRH4lbtCsr2qk8IUj1Xsh6kZB0xdcu067X5WM2CziunpdQHkRu5iA/Lh5JhzincxnNKEk2jkOc9GvgkvnjxwLFchICbGk8dxZZFf+jwYM+LfZnTaJx6F04TQP3iTjX4bJKjkBGQ0d1fkAK3zZRG8mhLAbZedgvx+7W0NtreaqrCQHooIZavrsK9h2lKXbZ3kXRWiSMxwUDg4D6b57haCRcHoK66zHobqmO/lGHeJbZdwG5nabjQQHoQa6WJEK1N94+VTOrM3v3PeC1PFmyEvcGFeppErlDamRLL+KW8Te8XyxdQDXRX4mVQKKRIXSpz6ctqcwMyW0CcHFV5Cx4qBJAXo/Ttq0b9sfC7xdYtOpt+T42p/sDTv7HhwUJ/66SWdwZDgraDw5ty5fveZAvyTISetf2vUIYwtII87vwdqTN3IZXi6guXiNMzFOwQxvZWgKqnTAXCWSxHQeqx4kQ4vn+uJ/znf+xERQ5SBBgfWm2l6apHBCgeihGgSsvjbeOmE8MqqOSNoLWsf3c2nqN1GDDoojLRMQHuWYteO9kYoCCoE8b9IMfdqJRI3mtTTNuZCXt218IJGal20Ibt/5+a6shwoz/0ZtJbYqZo0Gz2SdBd7eufjdmm/IcOgu7eF3xKLjp7Bg4gAOI5ZMqOEX2cpx1xoGNoFHY63Z1kfHNySlGVleE2o1/64fhbA4r0ZRcO5epgs59MS84tzB/WBRUQNdhQ359bCazHrXKN57y/RLtI/LmsLrkouPl//WzP0ugY5LPqJ9OM+4ZlaiXowvW52k+4MfG7Hw7gpB02jY3R1tUqSA+eIupsG41wnGtKkzMmlDt/NRkPw6NZNRSEoycFGQYqZk1+ulWgrBA324Iu7EKxqifcpAKL6ifXhMupRpqiI0fhsiRwRNCSn8e0c6CDcLsE1idVvF92cmnpi/0UoYVfa9HkLeSBivY2rxHjZs6I1qwcGUCUEjO1j4yTsVomXJv1psaI+gLFtxjw7v29OLIMCBgv14xkE651u91lyJqtcP84mDyIWfeWzK/aVxHpUOw9iY5SDNqiDHsh7W2FsJf7wsFrjjPalfgbYiygBLSNoZVa95IozlkTIyAYJ+NCjEpK+D2QC1mfOXeoY4RIVUfsEwP5ZMzBaV4Eiy6vgsxFtfw2JpXxan5/1elR8Fl5QTwd4tRPYvt5WBoa/NAxBgIXJTzzI+E8+PFaboJ6ugi+ivwBWhODRgC5t1zCRlFUOd8HKTnHULkZvOPRHu+wuqNjPKofCXnkpvJkOlDsKi/9rGgN83iJYObAtVUfKJhqhSTnXUnHPtnkavneoI1d/d1ydOPmxXQXRyQ5djFT00GPfFgbPXtHxu3aivKJZB/7tgMOhGFL/6ZWzSoc0g9ydOKYoFWZrWiHWIlH+QPZed468VqGSPCrY6FZP8ODbg2nHfP+1otrVohHLz+zYQ5HfHScHTSQQcC1m3V9Md9tKKH+y81X3dE8NfodenaeddYLBW2cxJ5Ol/k26DmYTym5DnX/ZBxdRczobzL1xJmJo6nDo6tiBp9OxlsFP39Ta3SyV3jrrxGstQJaBB9GVwaPTSSgktGRroILYjZkIeVRU5TrYaMp1sZJ6awxStpUGvbhZfFo3/WCaULvPPX0ORlXzhrPbtx81eMva64e7+CaUpo6AIQGdfqswLrep1gLbaGuxhU3GxyDVeRy3lPDlJdd2PT1KHk2FcjPCzEPtY22RvNH4FMzdOheiY9LeBO0VfVhlWnlJuJzCc8VMPTcctJFpsifEvRd8sI96nxMyBO6pM7xk0QjRVSi1Pg6de4vsMMel5FpzIaqF11KbUkvXxWezlSaPLJR3tz4h3gAVC78PkGwwOfq+qxVcjWQ4HZ/h5tANwHZdk6hJGXDCqXMvSyJYOtWYNUOb3Ct5i9MqMFxjCi6CdDEP5ScRKh24iRuv6Aaoogospi1XsntoRsoqzF77EUUwbnvRFHhXWvgDFO2JVj4xWq7RN2JlhQ1+3a1P1hp/3LxVPz8KCSiUNagDX87N7hnmepggO9Zr7gWzpKyEvyivD+Omyu38tOMLvN3iiDt9Iuqkvo69knOPjUfmJ7s4e0nmG90LfT+a7ohE0oRUr1IFTAph1aRc6oE3GupgkKpLPqt36TOzHIN0WUFxaG1WaXDBDOkJDOpPI09biKWnTvZuYYR8H63wnYbvY+uwu1Qq0jeEusVbFai5vrvNdq0+rJyAxfmgKNUl7+y8KZMgJa3COIr7FY8JKYkYZSP1G5ln5Z+a8rp0ehMSE8DhxIlrV+uHy04CosoFZyrao15OdhRe4A+xKxWXPMWUIQZjpowNipZ0FnlYuCCZu23aTIP8o+SIWNLAoSkDQLYQ840YKTy5vLQUzk1t3z75x4vJc9ArB0FkX9+G35SLa8Jkp7NyYs/p89B5Sm/8q+3r5ffC4VFr1DYvsdaf4aetut/gmwizV/AZCaBoD3bUYGxjPMFya8p0cXSIV3RDcGwdSJD+KRjjvF6FFTk/tF9n7Po8AHgnwvsaddPtb1auKaUoq0pb+rSsdzn8FISwTOzfrMINuy7LV4xzhMLXgEJWflG6KNQ1MitBFrZeM24qBUDUdP+H4q/8SiQEDP3D+cn7KMXvzYpo3CLOiGaPmNL5S0Ar1kI29NrAxgY3TxPjg2+YBTrUP6As285e/Rgc5kKaPZ1AUleHsMdDNwH9R2mQ1GL0kTDf58Mey1Q+glNkHxCIICcweqQ75SNyElkemh9z9CaxuyOF/Rki12C6aGqvAVGB6PGAD/cjXTydfqRvrOkqfYvC2wwOwvvX1wSXeGI4w9cVs8S3Jp55wuGtS0lOaIlxZ0lQXAWiKuD0dz47AifZS55kNBmGxnfc2bsSxHTFnJ11bq172huLK03iUwC8QeV7wNTn6bQn8YVoWS1ZqYUYDFEmAYXK1oFvUDLESpO4TZZo5YipHT9z7+PsDIQRkCiCnhPt+o9Aqooiu5s9RT1VJd5fJ1FrdO986n6ssgqX8bTruD3wRZYlj3FzStOkobyQxn1e/yyTEf50+97dWvSEuE8DEeuDQpuMLF8fH0fSYDAfTyYoftMSrXbVyk/1QOMajBtYLRSDk00v6ohSjS3BC9ySOliQw/N988RgEHnYkz8vMXHLKSQxgmTYCoH0PLaU8KzPFoIhZAUMV4RO4s2of96cTMsD2pGNeB4lCpuXGPrlemY9tZRq5/WH304HbwMtbS2pV7nFfrD19vZfhqrExCfqEfdRz5+CM1ZQLx+uEh7QtouBfzCAvlmbxbH+iU0O6YPbTT7t/0hR+twlaUG9s1kKOHJTMfYoN37e/I6iwuIUVHPuXnMjhc2OzSTI+fy6GTtAfk5K18UIT8G7TmW/khiFqvyAvCtw1sa6mEz2d2RJsOgAJ4CBtlC544YaCh7aOBCnKzIvKSw7mVhELJO5e+lW6qXnM3HE91YWv1kdjtQsJbzVfzssr1wvIYLvqEYqVSXaz9U9WIRhxrtcWOJLsGSOW9E7wjeXWD5ppuIWL09q6XOcU5YIDqSqM0AxZ8eVfHpgaMZnWOzIUCXqWX7hEhZHUqpKee/7rQgcec1rwDfataB/Hhgb3Yrv1Ezk1wpy5lB4xP54YaZ1pGX0Jw2CYFE5D3QB6c3FgM2WnhnlIzMtw9ymadJVEDzr6nVcv7ihpUkSbH72/Sk4orD4n+Kfu0vMbfaHaF4ECZMGCqePCfVKZlx1V0YCPF6kN3sLqoN3RQW4eRWGQme2cBZuMJORzms+Zw6jXe3GQW4vcpmjJmBeLluAFcmuBfSD1CzIWqE09oWKgrfKU8LvuDV7aZMcQGvW2MagtD1n0pYRORRN+C/WeoDFvYcr4deZLG4tP/lU5eyvecWrzkZEPod9JN4TA4kyCovLoORKXjyLo0ZbV8el4jSFsLSYL09xnUKOABWYmP48P7jr2k6qadv3yd4yG2/CHTn/Y1pvOTqenfXyyHQKytCnSwIJCJwT1K2kt/1q1ZR82zwlrXFmiHt+aHLDd9G4aUhjx3xFc6e8ycZKvLOjadDUrDQgRf66Z9nxELYYdsj40h4Pdg1QCWfzovrzknmplZJli9MKuN5/lhi+AEgAHLIMX3uLLPClMva8sfky2Tu6uOUWonsN9ApSH0eQ2SOhn2PcBDeTMwk57E+4e1/WPYAXcZ36FFJNHSLynzMzZ4/e85lVijHhtV++K3QqZO+w4X6uMx76jOh+cytIKBQ7ssAT2t4ECKZeJWTqDuOsjSqyuNqAHITFESIzRHuu0tOFlsvpUHjvQ6JGBAfKCi51Ol2xmU4flk4+2sp80Q8B3pvz+XRCLe6PkGqRpwPZpC/zvQmj90t6Eba5QvnzCreHb9qHXuCQ0wdASqe3RiGqmLGmZ1mRi2vdA+iu7+zvEeGfZKW2aZtTasCluRnnf6EARUB+JQeya0mNwEo4i9Jdj7SumoCXtl2suG3hE5vNmtAV+wEfnJF0G2IdvnEv4IjDZ84Qvai7FgZMhjBk3SG25d56HykKlUDyhxFrN6NaK3C2ZAxgSwTbvgz4XFw/930YGA9An+DagBW4QDR3lYxA4X3J0CCRBNjRKKHeibNKga5ly1Fxu53e8EMFZ1mWLlbw7UjqxM/NY2IhoeQS55W52iAXNONr7FGjlqQPnaU+jPfyDMpE0kfuS+i0wclU9wddAVC+85/ybCYVUW83KG7v6tKNvNLZ1EydeTKgTFmiTGWauL+ianN8qemyRXhUHDYdRN+5+EmFeAJFQePPE8RG5LmI2pLy5ENrvLzOq1j+Ltt+PcTHfZ0UJNdXZEk3eIOm1gL9ply7F/F25lPRUUuRtsYvEQDOCLVof5sOPKYosNFQ9HEBYvrIt/QJBA8epsefdtZmDFWVYNjFJVbUiTWBH3RtDI1DTUUs3LvytgtTrHeAAVZvK69C2g+ALM9Svz9NGua4VBJpG2DLOybcH8ZeZBXS2MQqoSBRRqgeGNQxu+7CboO/GdeaQl7Hz89nC+9m/noxpev+4nVJ1m5sZqOwHLifuiW9g3JBLKYl+BW4lvJ3TpJO6hBtK+EG91xZdulMwHfHHmM7bw76Wz47reVt4GpbYvl59QYDv+9s/RA39fu73Nh/8tAxhj37bqnVe0QdMWEt1gvt0WPsn+QbQlqn2QfxBUeTDx2j7TL7Qs/uPmM81KdBCCoDd36YMzHnh/J7y9+gMV1/iys2Z4S3qxMucf0aNYj9t5VFHHWT39m6x8Gm14jZ+HMj320aQagfetOGInS139I2rO8fha3umVmxrge4QA3DD6AeUelixLsBkGz2jr8pegYg+edgVAkXo2Vp8waJe9pUberOr+MsNGR9g0KcOidkjT4BX2TR/K9YstxskyIt5AppQ2A7phJ9X9qX30xMCxUQX9xnRcXkRgDIe3o9O66vQKxzfsJN7ldTosapssfbSzqMfs3+mn72deMrKfr4h0VwaV3VjR+MmDqRF9smPL7ScrdBkQqJGQtytS/aMm3M3VHeYiOoQITjBmvxvq0GYcuOU8RsymatzVxjXw+ZPZjGVhTzNPH/0DhWVBP2eHMqmZkUrbiLV6OLwsdwr/Did9r0tIN3lAcZi1yycFbxQIIjOTrPBbPi12qqVTORa2tM5l+n50G08ySfrnLC1OkAttMCZJgk35UPq8oi6H9tv2K1lphHZR0WGCPEHFgMWO8wvFr1GYUtngFj3FwbjteREqsDgwF7tmMvN/R605cV1WM1jipun0tWinKnr3+5bN5jpYBbWzBgq30j+vZeHcPtm89QWpestxuMwK6eNA9QJUDPgEbgtRVxYWKxuRhc7BXxgd8zP9k9BFd5CGF+Ucuo9XCDv4Bf6AcqOdpNGolTZOE1QQq7mOQESX0QfCjlS+aGGyablTyflwh+WN2Vx4QPymcYvvRa3jQ9xOX26TLCahreYCHCV/y09qiQ0cqFOjNVMDHCylPVpRH4LVWRDvw4pHLT/2Q3qBoZvGcX8BC7nvgTqdl/7dtolQhVt3r//DJ7qE+oP5TaBSphf1hOHsH9eFupPTVH1EQHBFz9lU8uWWYn0h/bq69je/mbBwCz9fnW+f4tFqWi4qEeuMnXGMkH1iSnVyWnOLO8OfEZPTEmdEZg2E5iO95nStOrIssrkVtNr350nqvFGftyBTkLS7zapqCBdRU/GnQgoPCF2D5uwfvdkbkOBfTu+X8twtX9dMvsTrlPBGV57eG0enMNcXxgZ/zTU0LNntoovW6j5e0NzN2cSpixbpsLqMmy5Wv8p0u6tYgS+PiqC8CgQBTkz5SpnE1nXEVios2jaVAaXn9GPBR6xJ4pY0vPnjA9NYe/T4pxo359lt+bOqtc1zft+hYsjdjJLo/eT5o9wfW8d5a1JIELpQYoGtfX2UzVzTMku5kn3IuzL+Y12bRYdn4bSbt9qJNjuKmFUjL2EH4+ZZvkZ9F8EgPlFbwmakWYFcsy6Cbv5+hJiCibkWx5YPDxZryBPfQFqw9b0Zb5q6ut5wNw4otUKcc0VRd6EIrHG5TdIqe9ARB11EWaL1i4A7Mkw8DQh7SoIJI24iDQL2aJGwfWyMZJfNAIcUieerhUWHMBu3FIeg+iR5YBkA/Dwmr5/AvNFYXXJhSXIwfojmsoy0RJ+VEn0NB4wVEaLja5mweLvl4XwQa/fTijaFDzBOrmMVkQlwQ4IgYsrRXVIzlLF2T4qewpnTIIJ1CywYr6tC+94t2ndGJVvj/O036m5/54lEEYPZf03u/5OfZK8HwdAYkdSF0Ik2driDaq4Mru1E1tEgsNHVsWEYmXPyoBpln3a8Q3M0Bau0QXn0cOtCHTtfY8ogIlAae09KfHspcnvvnb/FLlUFD+mZigl5RtqBv9WIca+Heol3551D/UF4XBqtTdy0pMKPGu7+FFPlA5Pd8mZufkH2+7ccSv504gyDIbbNvYBo6uqB4lrjnN9Nq0mftz/pUQBBmtTM5SL7+21dR+g4dmEMcR7M83OPAfy9IsfTmNYNxjeh5Aq3anLUzTEBCLGIcVaA2c2d9gFwgamaucjv7mBq5pbyEU7eO3b3CcKCTgpJ3NqfRd32DfxeXmGz2QU32U4lg0qvaqc2MVO5pJIie6c6WKFfttbCbqhuF6bt6P2kvfH/Dpl/sWXgNKCziv8TXJYDPi1X44JUmXFGve0GoFhq6ZL1y50DvtuO7BOvMH0P6G1lctMaiB1EuRtKJAyxnl1ZaWH8pPrWmteuuaBZ4u3zlkOMv92dotF1k29Ka8w7WU9C6M+BqdXmBFw7Vt0tKV2nyIYOrpCS+B6s3MYyRnwHriCCaMxO7GhTNS56tf5g9EGEh8dpL5NeDNbJ+/hb3MMeDGmSOI0fdQlgveKIi6HFL/L2pCKtMfmO411893AlEVWoOUPSTQEOleGG2jV24Z1enUinwhkJKA3DUxoDt9Z8nHZRiuB6Opw3oW9U/yyamerN4ikyXKEHd5IcEEa5CtVLhC81yZrspubXV/xutIGUprKofWvHqiNeMFZqWkrxWf81KJizdHbzvSmVPjv/Iaw9itEz1RAQ0R1YIRXn4Ef+Qm9XV4yRtsTv9fLmi3jsqq8Gm7QMPhQakLfDgsVKV2fcV16f8Ox1PoBhmMUdJt2BHKD6zNzqRrHTrmOuHKovxACrZ7b7H7QwVS9G7CvR2ikxx/oIlnx7NSqxIMtRqeiDPbfGCk+W3xF/zNkI7IfQI3xN/7k4nhf8d/vov0KVr5e2+4BKzNY++tyC+bRT7ARq+IlLFZZcUZvchRDNpHiFU1idOZd3hOQeO/NFtuUWAs96n1jafNu8nM7lkTgpttYyT4CgfyZgagvAxju1IeOVKwHHAIORDQ+WX7NJ6arH/YdBvKCFed7iVFiObW2YKzqQnq3RGjCNc3mQN6r+gLQ94YOWwz85Evn1+gF0H4ht0xaJd6Yncok5CVss4RU3jIIHDrjX13rKr/gBjKe9PKPNrV/VoMAKWXjwmsRcaAtIkd7VP+KdIcxXadCnfw+a+Gpo82k1/cQtJqui2evHQSHxk+CioHp9Jftz2RdFZ+E7N7jj+QJoCcoWKAhtN4X9Ps+0R8djIEX7HWSGVCVMfk84dYrkZRF7kYZwznbazYrkcWXjVKJ4sJBQbcwJBSRogpZS8NfIAlVCggyjUSLo76J/VGeni4JTEdTXztMNm5ca7rNukbLVcWS80CF5kXX0ciwMOvsvLan6zEByH6gviOehYro+KCulS3jwtuodyp+B4cneuYRRbMx+JovXwSTk9uXhDlMQDJ64pCKoakfTUN+N5GgixlML2IhWqHTpzSnWeDXqMG4EB8EqHc51RXSBBHwLb97raKGFgNB0yYhIDJJllGUeIFYnlkZ8W/OrmgFEhv5GTtirTTWT0ct8aONsvAC4QfzzVHZCPTezOF8tR1RuMJSHkGhiAqkIzLjBTj7rw6X/H6Zt7JjDsPDqPR9Yl++HgI1g0D/CxDOQtMSyPnqWKARFtCN9YcJ3VDI9vAqQMUse9OFlkXmYPc7TRz4r7XAt6N6GdEUKCRPu6M0G7rSkZ6L+ayxJCX1QYr5ywnI3CZZupDQOmwz8cH/KkNZN6pqOIu3pkCt7pmqn0oDsWzruJEQp4J4tV2r5WxUOg/CWrbdbjLA8yBj0hbX1WNENFU3g5W+3GMxJE/hFFALlbaczv/wKnhmYkYR1GTIHJvgvDXymXG5Vx21a/suBBSQJ9Y+AzDN96nyR15252KEUP36uKWPL3bFsxo065amadI39/0Wxljrfo5QN3HeUf3xOqfIxshxwwnW0bSM4j6zVLEUOljPGHMMu88ue8Nke3mGOjE3fbzVLavj8WdMWhHR0viSQz5POINLyp/Z+vvojtgbjJ1hZVB+OxajeFAntEhcWC9GmeRBIProuU/60dW/96JiY2QpNHqf2/i0Jrra2ur4SVHNKNNuOLQNQX5QCQxdZhHaBVv0ZEeYHQyE3/pSAsX8Qui/rIzq2oMseSDSemRMv4bbOpGh5xwYErcJ5XG5BUyF4QPFUUc2PrRigvJlmTscUhQvbgjShw/nFiiSZce9En4QZYgXs8p+JU90lXQpnoUr6ecYlxfHy7UV8OZxlBFYvmloVIFxAlkCV6Sobd/oRNMfeg6S5xm2Rg9GIfA1gcwvcDNRkmmnp/IraoflUdKEyvJCh9tkrtf3pUSzXTGY9PnOtIdViswIib3k0/D12aT9P1fvte2otrQJPs25rB5YAZd47z03PRAgPAhvnr6YWnn+ruqVmXtLiIVgRsyIL/yP8ZBKB0scpCzv7VXk3SNIRUEAUDBcn8i3ryH8kpgjmfzliDun05VQR/ha0l7rC22x7rOJJygGFEayxHFDDKJVB5xeb5sJIgGnXKiOHinbJQS3jHEnlMVz/OXH2ZhNLDz2gsf25Lv8nBikRnJNjgddKyjU0sa6CooEdudbnmagsLRxZu/3oJXZKWXZC5ndk9Ni9tQL5pSdPXksU7U5So6HMoHm4vutsMstQUnCoXPuw8GZHNu+Ud491w/C+Gro7PIetFKD3x2d5VHWWX5hXK2+ZI9anj+TDzheMKCifftMFRs4TONLe7gz3TfhFJCD8knza0h1zeQrWIQWuJL4ZP1mjFsupEQeiU86roKppuqeeg3vyVVjFUvc1hiXaiqJJ5o7dnldao9UaDfY5lE57g/faqKQfKQ9m1FeIlFk3O/eQ8ma8JibQd5xLV4oSMzFsHVYq+nj29PVNiNGoDMig80HNMMGD1Z0n0cRT86dlPje7Na2V3i9ipnVkosoDp9V9M5zWfv1MW+BTnwb0UgUAoXhcVRI5ncsfg0RYXn3qEWMx0FQMH4rPoJ0n5jPQ/DcUEyCL3z5oMVP7Ww3CyEnWWjHl7uuXWiTNCGjRr2PmSaVD1U3mbESP/9xu0Jas5RNBhfXSaJbArcXRFjZe/bMLwW64+pT476qTBgF2e1VrAtIBihhJYbfJ9m/tQdHxBJNej7sne+JhaQl6Xp8aR6qOzSiNJRNvAXN7EsSr/to4BaKSpQbIhFzAqxPpuijwwpU0lQvvICv5dO5uAhIHo1bBns6Tj6XpNBX1P5KmQG8A316hJtoGPPt8USntEXf7/PIoxbk8k63X8wY9DDJtPjn0wYVpb/rjjqfbzFOJLQlwuJyNo/oKkcesO2fpiiC4a0MDjrpBfzoqr+JGz4Cszvni7kkcgNFodfn83GzVxlLFpbHJB+wbC4hOL4YqVSN7nB8aZxkgMaqr0YMyu4DKoUE+nOZSVF0dymw+QEbOKUb98xXQ6jOUBm3ntCSO93tsfP6muG2u6GrZOl5K3U3pHzSQW1tUYu5F8vHwhUojdTrbMU2a/qeryF0KCePS3xKoFy18nYhsMS7/HSvtfChqt3w0+1x1yqyQrm7ufxVIEhzRD56wGxKmmSOd1/hCMF1iuzxsDCb0lQUIUcdxF7sJgehEX4eGGuJK4dgkJY9WN/AkH6iGx1g1G178WIRYPYXJS77VwQQL7VgvlqMc5yNEaX3GEmrWmbR0Swocm1JyftLuxTPBgeTdQR39KuF9QfFp8BEYtCRPCzkHHn24+f2sOksr1appwdP5D1kMK/R0NmxC9Di8MwUueXmzRGorFUtdcNJfN1LeM7hlzzUkJMrMslGTjb35+nefIYpSM8GdCw/ZjGt80GuSMssU6ysJsICppfbbOY9t2g8Nvsk7p3/yGLzHYnT5WtNcugpyoe3fBymqvtfJRNjj1gOs7/8iwydlnbRUJhD9/75XNhvhIUs699K+JsZrQzTYzbdWeT7uRKt0QOCh0IcVwXNBvPCJmOVrdBtIcfzkGY3dIR/3y4VCFohM8HOqz8fOVNn6ZCRmFeYNE37lt/xdmCLwdBzIroUOTvVclI4/MVqtxCSMZxa4aoxxC3Ccfqqj0Fcsvv77AjxkpGP171MsJN0mR5E1Rj7wC8XGppbOYv0Z3uWYVbBlvg6z22jo0HSb28UajXIYv1t8k2BOJ41vYyGw/tI+shVhCAwsAYggS+GvM6ORvQl95otqgaJCojsSCJOYgsTDwj8qIbDe2RFdwOvshjB1c9YwFS54r0iFPj45lUhFZDC8Mt3gQK7jonD+ehe2A3ytrERrr+xy5ZqkNvl6/bTL1dslWPL16ZWQj00L7Rp2V5TK83frzAxJgtYsexSMdVIHEWCusBdRl9tjnKTP0k8k75rRe5OL2lzQVl5HDUnhyQ/nm1AZ6cOOAfm1qg1YlNpXhrAUX8QUsy6NzHBeKMTC6zJ1cULNTksOetub7jjRydQF3a2pMmWrS3RbxxBBTYT3Q+YY8A4G8CBZ7zVTC8s8509lpixvkw+Db7ew1tCJ3wJlc9tuK3zSlxYMSyWQ/V0rO6HdNDNb9XhCC2TCXwjLlX379TBPoffLpfBxCgrnzPrz92Uxo0A/NWsZiP6jZluI79VsZIjue7CenuZVq6/xF76TelsZyWaDYA4BEKtAiuwZ8eqI12+JlKJGMWufPYjzbQ2OdfHVPfxq5cz2xOeO2P9cWu1MYIwVJwNE1w1YJVvTn7kf/MeMnUygYN3v3qBHmSsD+97zz5Vja32jRhhgKSiHQ7ARfqVGugjHQSf9l/yi0QhDs3duLrBg1CG/GHYL8OTp7nQAZGI9Ot7mImVBkmZe2v4ikF1AI6JzauTedOetSBeOv8rH+4B1VffVEkvyu8NxIGEt5m2oov04Tt4cBw7ft2gMgd7bVa2aBJ+X6bot9KGa7OObCzYehhNDviVhYlafosx4jRBG0J6SnaOKVqC9CBFKPiEck+rdS0Lu2ab8dpDpgnKcYSmN0ds90jSHakyazwhyMZDCDb+KvMq6CtY76JgRJ8dikIQuRzs3TBCBOIHYV7X3y3mHjUEC9P7NOTPcVSq7rAPfIaMCIlMixHyhDdJlMxmbyYtQ1KAa3l+b5YungddhyM182dTNsullKG3fCZKbJvnt9l92ohFDRNP/vG56XCJNkZyP0DLgVPQm7x1tLkWHmd4Nc0LA6KN7OxR+m4CXBxeGF0hJmPo7teUdM2M64+OaN0o1q0UBh+gVMWXbYZ23t11zMQG/GGMToKAzPCZPuIGsZylK1w/v26FzBGuR6TU0vmZEtZ10rBsOlMm1+1+LI0HdYHwybwHXDqIOLFatBvtXW49gnzUBvELYY4iDY96oTs9yy4m28jDZ7qPLueZ4MWxyhYu6gev4W7b7TZ2Inbg1etoHg8+ldHMwqAAxL0L7lkeMlEmIGGwxI/+YXR6SFRPYfI6Vg0gmJSde0fCqETjRM/VrY9e6Uuh0Zgsp2uiX/mu6HI61efQq1cOaiPnsYtTWQuHQrsGtm85Gel4Gdvh+sJDIUaHAmGKD2FwWXe+VgeqNSrF0i/tD0MJZw1keEqjEFej17Mk8sBWwx4V+uFMhM/5A2yejFbMTzlZnS9TI6R5L3i6udvOIjYmx/4d1I6Hn9gKk3M01EfXHA3C9aZ8Mp8AFpI3XQ8oNmTfniLCcYgc1lmUF9e4Wzt/7gdudCdo4M28Mm9Cb8bR6VK6svf9FqLLrnI8VX8YqgkHKZs3e3I9FglGnsQHTgSOMHXyuYRHnA2MImr6WOla8u4eynk+O0NJhWe222l8J+ugt+Zh0KEY7PDSIDIL3TmqvxNEawPs8NrXFtykeqqdeg7cKmBSVlgh3mt40jOaq1qZvaQvXBBIDTHgwnIL/hXS8PeM3uttNkGne2nTsSpZBA16iD/FsulrCaDgXoiCPa/oQxEkJTwRecttfUc5XN+4UJlfMD/KexMyE+6SnKCna3PpBTpo5qrUKAp2Y22PsGYzBdELYVCRcX0LND4RhirS7RG5Dnlf9jkeZxVyr8hc65czvX5Os8o94hpyU4+nuHSMq69wCOT6SJnXA8v3Y17K7yvxz28cxMd6KcANwbj8CaSu8EFtj/glmdbJ9+q9EnJvlLByHGN+83JgJnCivjfTRxUZ7sD3SJNDVqX1IeAfh3G+aPUVuRskkzIP+xi9bHdErJ0Rm3ORlUQljbWCmWhL929a8XRNSIKz8W8egWVLqL7KyFKpi3DPYx+ampscardETqa6GpQg1jihGXcn96Et9TfPMJ3k6h3GxHSdCNmLVJqz2ov5rH4wSjenj1u3Ap/Im7TtGTleiNGckPMYxacvw8nLaukreoH4yvP1DwcBufzJRYmy1W5ozgV6ZQo7XisfiPjqjaHNZDZsyx8WM6exBS4zWolPPjxPEFFX04m2qm4Qt3Yw+Hp/TCG/Pj2fHgF60HnV7FI0N79X/IrJTV38QCG9lvAFYXAjP2C/PKonHzRLpEW/5a/y8HcF6s8if0wdU8Zvdmou/pXjNKpbLPewJRJcrVjVX49JHvk8a9OnrNKTTl7DqOMgMiLJ/arLEhcftPJ+zilT1GW0h58yzRV3bFNKnYdJP8Un5SDY91mFPQrVxMSPL4j3Ns6Hz4EVBczY3/VxN3ikoN6Utunv/osw3UM4JlBOxcfaBJI3VGTRmGDePMLBvb0Uo/qvnXxIODSik87190AmF1075qbvzRjyVGW+E0kvnEfbNDE6Zm0QTjo7iJz8LvjvCKXICkmgDNZ50RrXHlm/OUJD08hGE2N7igFV7jHVz0n+cC50CFDdkSlgYSYQuP74dby6lN+ODTudSBdRWLZr3VyvjV3owEcqnCJK20bRm6Vputgyndnzcrjw2R9suL78xRPWRj/V5qjlmJwOtr3w4JfQ4yM4GdWaggzC94INx6NNSROKmQ9T/SK+1deiFWt2gx9bIdh7abimrE5YE/uYZtvny6dDd27hrcZWqzslluZGStBfI5tIGfGKccqgkXAUMWZFsWHd1GEhwjYOXmEb9o18tSwQDneLs/eUBo8JWJPfec5zXrw87KeFpRKw/Vpp5Oh22xaoug0l6EGv3ndxlzipEikkREJhQGpCfbwFtvGVul3XqlD0pKO2GOOAgPDUERGc5IGGR//lOKTSWn7kHgNC6anSH9hbGmXlxZKab8OOlVdOlnJ5PplxV8bQUS/1Y6O58zgGceLPi52YtOmhF+welfBA/ar0w1zJH7A75ZhvOAKj3jEGl+69CakrrsPk2MM3a2MIfVhkHyV6+zo4gxhst63WdNNtRKfvBy4lYBebmfdFHeHX44odipBVWxC+yBHcDzmSWh0MY6RAvFRqCL6QylCirKVbbB1Ve10QqCEKYabCl5py8/Mx0D4r0vi6ELxNl0aEb7r3tjXI4hftQXUpA5f+C9O15Jv4N6ZNR8ySDRoGgVvyb1MT0+IQ3u2353jBjx75Vooe6x5zfBLqsd7jGuPLZfqln24QT4h6YoPWAFFsKZ2vS6a/CnegcV7ANKbrY+4ae3I1KlxxdQxGIGXmML6a1uJms5chP/xAiw3RHUTvzhuEDOVWs8Pb9cQyU0JbtX6jVBTnVVer9kvLImzV+2RDiGgfNhRzKP11OlI8UirYK7rfr/CW6gDNK12TAn2dPnFQHYybOxAeeFERdKvhUcosHlq5SUvMDNIjhapzyV95BUpsNManlNehG+lHsdHSXaM5VhuQlM5ZBZPyLK+8PpUOjYyRHULLRkzsIWQm0EV564aPQCpl9a5UtRI/cDHNnVQ31l+hUV1EtoS9pjHd6Ch2BC0EQIWIORkB+skPGn5pK5VVKjCTdH2jnQD+erI83KbRleV0s8tjeR0OHD3IQb1k7I5jfhGIM1n7bm7egtE/FkMhxw/D6/g+zUr2cQ+refsScLPxupeoLiuX2+ggBH1CGNQwFCspeZ99Et0YOlnlRX/TZGJ/7Bq2KWS0O8RT/37F1p0VlrXUteZBjVSd6oVdLu73CzkZTXdG/jkSATG4vhHe+bwNuO1uKFs+ltN3Ou4Y2o7j6ith1JnTEB0zscWzUwTpU8Nn96Gb0p9ecCKRWbKrbxB5IgN5YW6ZdIBKtvHbmz6WKG/6IrSmk2zJWKCj3opfrwyvzZVl/yaVz9HQvUOaw2fS/aIt8+7wX9Hmm769iu57nilfS7vvrqWiSO7O0tyjtLJINbtvjOaV0q3ewnDm1TWu4DlSOFz7M/xG3l592kCLl0kniFmxOI+5R5rVKQeSa3GOuz7GhQyD9jahPbPMtXZgKVp0UFXMm5e+KEheLI3P3iF9Cn3Zo+/oVcxw5j1aA6vDAVd/VjkXcZJhg4FDjEDVCQPT3OQor28E97VKPOvd6qn9If3iIxRqfeb1HfcHrmo2PsLB4IVw4muo4B60O1FacqIy244Cv8BdStWTA2qSfNx7qHoSv2YqDB3zdleRlT7HUN561EvFojPwErSP3wxvjbvgqzbOH7Dio6teU3tsRHPRVLy0tsJ4E9LBfRYiliR4U6tMRrVF2UXxLX/UdQjOj1CHtKUMC+chQ0M5vxDHkJJ+UsDpI3hM3ugL6DVdX83eJgNt8O1BBCCzIRnY2j0JU9/Qnh1pLX1AOcpMDt9KQa27GGJxjiYYtY6y7RRznIc9X3uOHXvdvTxbi3/2S885k7AJrouJ8uRn6Mod4j5lUj24i3bR6dopS4KkehhbzCnpvQxHlQ3zHNcwokzF+Mw723JWg+B3/GP9XoegvoXYODMGeidTXGaHGjwEMWRgMC1TER+mSn5brwRBkv5tXMvkQtrUF4W7Z+Jd51cp8h5MBghrk9JIYnUmXu6CSWu5pVk6WuUhcN5tQwz7yNTw0O0rvvaEfk2DZN3tmb+qlVk4CUm4Q/e5b0kGe6i6Nww3pqkXeihSrmAroVAIRZKAVaTejyyzt11kQj7bczHJY3fiSncZZkvFw8V+efmzm6nvmxIYg9s8f9HpzrNk1KaRX6746ULlpZOqZIgp9EqR+ZGWX7xTwFy6axroKEgj0jBOLHnErSgN4aHY2GP5i83Ow1ae907CJSDMz56Qdn97TS5uQm30XydETFPL4i2A0OPbqTVJ/UaROX5FtcV7uQ1WFc4Id9ZNfwhoo6xN7IiB0Vx7fTedV0dc6ki2AW0yUkrRKelQ56U0L/pCkbbi9Gh4U2jpNTiLe3l8KLj3mKh0iPbM3eyggsCC8jFiEl848M986Biccsl5K3TE7EQgvYnd+HpvQ1S7RwIZc9Mcc+fO2dBLqlpA7teVyzBmvq5O/hoc4d+CQ2aBh5mRZpACM9CHrgK4XzfR89OhFMUfzrt5j2j/vsw0977xZMv3cDfro+fhg9ovmzk3jo/li9X6TdR8RoPE3v4oaB5Wj8o+I8/f2mZTfgX/QXEb2f1dbuB+npnyDfmggaQwAPXoIFUmdO5A+MyX4IiGuqQwyo2aW6rs11EzWVTJWaiK6nkj7qa6GwRJ/8jQTmrsV/H3OtNhRszoXlvfkoif91KS2w1buOJhxC+B3tXn5qF7LMupb7E8BiZLPQJPe2XDPcpYrMvQsrALcVTZuyueC7XROnTvi4yWIyxpw2uh9PBihqxKuDzwe8Q4QzM6B7lCCQw2YYgApSgwx3ImVRCnI68ib1i7r8yZyVIA++AH2vrakknHcZOlyhweqB6YlOgDuyDn7QsaGTFnmUMYJtvJgF4YZOuPbFBE0BLm+Lbad/vOIk7zneWsnHRAGU0N8IMMljOtkOgAeRFuwtK04xIe3jcEvnDzm0tukJXG9SAjDGjV41vFiDhmihGPlu6/BkfUxYRGpMfcncHUzXTJLPjrYyZprwHiDzTWpdEDECThwc3voLM/kS5t31bhBGFSX2y3DpTGgHwVRbA8YN8yS6aQQRV2FdcTUhtI3W/yOy/UIy5qlgmZ2PeN2dFdCp9yc9qBsZAmiuqejNhAdUBKkJb1OTRljxUbRGWao1CcbAQRYuyCAV8Z43zw3UeOpfu1HhHdtVkgfvxI852X41UZ7BeByvf4HetRMwQJyR4q+iccIWw8aFcwnoiLjiuPITjBTbParuzrOxgTGZHVAKJn6Xd4nrMUTVoUTC3b++MQxHvZwteCsHjPTjulAQJai00eCZqYeu14DOjSzJS5EUV65E1GJ+FDpx8++y4kTY8b3ldDciKFt5jPhqM+cOkBjwkDgSEyIAvt5RbQl7mZ52lAJgt5BHgeG8fNAZb6ZoUiL7qTAW4pfF/SgMkQqooa8ldurJM6pd64y/LnAI1EmPgmi5Q6gJvkVc4pCLZ7bZTVkd8Rr8nS7WwxlNR4sL+kGJBhBny60YXX4dLOQvPNA5Af6x+wvnRo80p82ysx7R+Z2HYnOq/rXTta++urn8cm7eEPt7R0X1S+GY5RTX+BeynI+rmUOgO1ZMz9pApizrAGEuoY99c/R00jZyHmTDHjPPhkkjBZ1kL5bMuaJLbQ8CTZBkIzHIfBEfs+aukD9X5EhS0lQWl7i/sBfFwx+d0tRSH44FQLV0WKxyiIps9ag+pjgZsQPaQjHdTcEL/gh25gGGhJh8KjTwySd/m/lul1oGrrHXbFN9z0TweaFwu2eguPfVcrqyU8AqTixm9MmWKjhbLx4VOxrk0/WS3FKF5FuZ9smsk1dVEfBx7XQgAxy7zsz0ANlE8QA6pGgQgeP8PntOc2/L0vQ8LdJ5lbL3T7rCDhYoKA08YcOYJz3foHfRpL+yKqzQ6S7Q2zWkGqBDNHIQcZS/b7A8KEz6NJxP2zvRr8FZ1fgzn0sMFKn4ovRt3+2ofNnBFTx69lDK0NwHJIVEFaAkCK2WrmGTdpKvIb9N0DRB+av2FlICbBxCCb0hveKJHPn+GhzivQXAXLnEc/QmidPPZPJ0QRvTgxVp3FXoai8ChiQvvlya2rY/161YKsB/BCee4WH63DJvrcfz0yiP2sDxCvPfgTLvyYJmuWfrL+VmopKplCeyzuXhxzMSPfvZ1cHkHzuHBBuAymV+jss83P354B89MY3ZdPqD5Du05ze3LaaDH5Gi63ZjfeGmZYU+fcsOH8FkRyKZCKUQH+vuMqE7u4exPpeYu23XAOt2T76XOAE6CYehvnaWHxDvrhCvireWMnyHc4QBCZgse2Oh6ZeQ+V577qOUhEB76gXjPugEoPt5qQNyEFE84vn/e7W7TfbCCcI/I6tejg7Z6UPhD97G+KVgwlxfSvJOBSAG3oYXUZ5cFF2k7S4IfvBK91N7tn2f+gzGccViHt6+76D8qx4zbXxUMtyCiOfx+69V08HyHYf9+vVwcOwM/7Ln0XHTPOeTGzYzfOz+FhHJ5PmbpPy+Lvk/+AKVmPhmNZQfjv71jjUq/1ODyfZcWwgpOYtKvL/+sA+Lb/Xva5wt/Pc/w/yKP+oW86Pyf+PkLwsWCL4v/dX3lhpG+BW7O++1+vv/P2Yl6L8+88GBxC+f+gbH+KxdgX63w9p1z/PkX/H+JF/X8/oBUZuMJR52v1dwoKk3/HqqIuq3/fjkD/DqbL34Hyfy7967j9+8L/gBGvbNF1//3+32sEqvO/3xFfZCoqPltVFZbhXSy0uvG/sH+PkHZb8Xfa34HlHw2ed0uVfsHL34I/iwMet87STvv/LfN7XNex/x+apVlbzuM25P/HAn9+P//HNeh/FFnHL6DP8i0y8Mif+nzY7x+N6f8ehf575Hmdp2v6H5T+e/toH6AV2TpgTOeAVLEcARsarl/xfvm8ekPPf4SdpePn/1wBEq+fF+XDqLwdOBhdnDmDHo+VBdJGlJo3WLpUgtSeHZ/hHxkbt6xSMtdVxgvPqqw85TtSsXwJ8g0FQqCneswZ2vYfcyMq7a9cPwIXSMbqa4E82wXdnj/Frkgd3hB3exfRnc3bvRDmjRLIe8vRN4Kh6rXMKv3L9aNZi2Y5mm14mpcZOqejqFUbmW1oujno8vkrM/nwJWWP/nes4jlaWyhW7ad3X//PedsNNCDzLRmmZJyRdmqm5BHz2aCCcTtO+zsW0178+vDw7zfGg5ZP+mQ1zqtBz6lxvH7HSFpeDKtJMXC+HdN+XMYVU37P7PQyKEhscCyl+ZAdTir7na/Tqm7rdnnw2B5/p1Qlx98x8HQeUDP77zfc2mZVt//usw9oafW3X2kgs8NM7rJ36oMm9qxaixeGyzD7KM9mWZbtSA2+bu5JFxtf5ZwFOeXWU+7b7fX/605VhVYt6xPH4eyMpS9yzMgj9d1dON1s+75uubmH3AclKDLDcfz0ZHMX3DELS7cpwrqGaM2AwCqxDMReeLY7ZGFKgUgVQwT6uT0MrQrhJOipLsb6Ix8mfu7YMHrwE6bgRRkCtBN8MacWD16b51EevBGixTtZUJba0DeZxe83wXGAYU5NXvd0oSxNqJUerHMZMTwGBgaDKP8iBAEMdcXwvvEF5SrQEJRRpS+1Rzt6fts7IRXnMU2/HMOVWAYdKe2EpSOUjKRatuyrQs6SRWSA+7Yk6bWjed8Es6OTu+7xDntmmyOd5MfEhAzD2cbmUIr+wiSqdaIssfXBwYRGUFS3d6Ra91SxRyehdBfFlrZtZtaAvEghLdIz/nEePaamfj9cy/+GYw/cPD9KFNwzkYfPE1a+DsB/5zLVneWtxPG2zOZDyYvM+XDmY1/LFvc8MdhJ8RKqqaG2bFG/rc93iT83RoZyfb2+wXTSkQDnh7qY6dD4D3/3uZHWuMw/3GWXPQ06jfz9fRGYBLDsCKWG7EGU9PBBDqmdp6DejlZoSzGyy49KIunLpNClr7oejInPBhhPumLF450vCCpZluVW4cxU6YpgRjbKUI0br090HPSM2ZjyKjTEje/y2U+9aNAqsvaJ6yuSvUTzgRHzfXtDgsU5XGMpnwHqeux4wjEL/e6YLhN98LDZ4peSgbZM+UKv3O8UrBAe5GQ/W/2Dm/fBPkR+duYDtXiVhY2wp36VP9Lom5Kk/GZDmkpT9HZ1cAh1ic8Oq6HGF8HgAMY0LQtnnbYQb6sOtG7x3K+/OlukdB27RnedfRTIDgEd5Wmjtneq26rwbJ4pUNchrDzQLvv5oZpvEq8+znGHo7zlNNw21f2TXnxM0iaByPvxyAZAi/CRZmX2SK7Jgagk86F/xyrsOTY6n6N9HudP1jg0xhKWXX9fCP79J38c7vgdA4hIKWnmkWXjAED0Hs2nPS5JNvn2O3/2FDPD18uq5xvDxrW2w8li2ITXaI3Mopk3GjgaGH5E6Bp6G5076wrLQpAlcZVNqBGKhMApeKrLJ64cDChW6FjwU/i7i4pWGThdo5h8ZWZavdcXEScxn0nAJNFtKZZJi+nzsjrN+lkH2e7o2duUGoI8v1jDOaDD4z4Pu3oA2bO/NIWrDT0B8vTNiOXn4y3So7RdRCJUQeXvZDOYhITp/TQ9xGLkn8SsHhH63PIRjo8tKwcR5rOTJcaC/5PL+FEuEUFkBKZ5ij/5lwaiTEfMWHUeLTZkBIX/XCNjDXf0fcN/6a7CE01Zljv/pvor8SvdY7zqJZbIGwvnMzY8kcCmctQbF+F5URbvRmd51rH4W4eTBUm/7fYxh3hP13BtbqU86yVmnIXhSxAe/fZhdfTMI4foUaebT5bRxrNzXgY6w4SJuj0R249gruk13CNzK7PYtmU6Sl4fBvrtvNK3SsocPJf/qsdO/JvQCJLxcyi7TbEPuq8/By9lgtNRmPvXkomlg5mSAZmaBlWPNKlpntU96XwZbGXP8Sr2Hiho8HxYcNtvGp3nzBcXxzK8+NNgDKC1wD/ack7WGseoAtGkpoLBOBThS/8cVQQBRd63RYWKSYODFh4Jr9KDg39YwLEwXfJO+crZRu1qRHuWZvr1Depl1kWVSx+xzIzVMyaz8OKmE5oE6W+1KgdvtJF/tJfDljr33O/uzEJ7v40XKT6CUPOVPqnj1AjHVmJ/5X0PQzwiV3Gl5FWldrU507Nb7SozHpWGdblQ4STJS9nfaFWBwir48xUlmsogim4uso923aSzBzPUPG3LbwPB+f1Rib8x8YXFSXyRLeuKcBOs/jw1SKvQRJBU0SmW0vobxnLQEXc8V2haKRZzE30kvQkGTzGWpmmkT1jFDGa4TxNWG02GEJmS2NMDS77wA2a/+nwcFa+VKnSR6q/x9ksNYBx3ksOS7pfePjLKqbEyksydtbjj2+Vxr5cPnpAf84+HU+oN8u6L4uPg/hTkLJ4heZaRENAe1qOkqaESElu09PWVAbNs12jLPD0eaKeHbIwlzASGleuCaPSjnzaUgeBx+0iiSPK3gZncGxGDo33tixYvMLY4qmnLkYTC8LuB4W4ViAvrlDS0YTydhet4tFArsFAfHsd/URWHPMCnX3rp46hHCfiLkUeaeAcS8EJeqgASAs1HQkxkeD9X5xfHGuKXE9bn5k4AOZWMyaB/JUmC9Cs1pr5LKdSPCrWiGSbzR1s+0moKNPNscaHhf7Lo5Gw4wG2n5g7eeeQAnU9iavztoU/KCRNN5uboNaAltNCs67odFIyiGDJOD+ikr4hNyeuj8R7GZFyQHLaqHvJjP/blN5t/HecoknzM7qGBicrSicKiS3QYBsnjmWqMf7fLxOXCGM3XCRIiQ6AEgEeZjXtnj47g2bWVMz5bty7Q/EUVRwB2+O59X0b97A+vPJwwAFRXOlIB2PbRYU5hpkkCsYSec9treTCZbVBISxPTi8q38/1ee8gzb4t87CzgtRCeK02SULJ778j0sDTP4zIlQMn+A+TDGX4ZD5yZ6k+OtVduOY+a63wb02rc5KQhnLtHVVDnQd8baa/a8+WQyAHjqxfYoY8fDVfaKkNbUWv0aMooE+56ENEvFQtom4y4E6fk5EHkFmkjhLyNrXl4rGAmRp9F7+A3NYss9kx4oDNMu6Fno78zNTMjgz5w/9EbHq0keVaWvib0XDR3MKui0xAQzuh7zoNdDo4pb9kBMzysky8VuPhMmpV4+sPhbMGnXC4E+B/Rw0gfbvJD5BNsv55Lv5n2ctTzKK4ryVZavz0PWhV+flEPZeaFtFS90Pnw4F97rGWflCR/ujh+kPAJLLpHgEwUbT86DvsNQ/l4yYH50fV7o7Wua7eK3AShWkoXn7cyyZgaob0HZLdYxsEYs+EyVJkAyjnmL/wimO46E5v4tcuGHsJIJfwdmu6uhtwHjjJ6lZJDJXk3fDam7FePbVOzn0f4Vl7wLRrmfreu8TAEQArQI2ClPbpWkA/y0QYC8j9TrIfW5frLZnQP6Z6112qUKRpgcTE0azDEthGgARtjsuyF2fodfYv7fkGR0kxp/z12vlPcUKG8VCHAaaVUy5/qUXjSii2oCb3bdRLiGWb1Jl4ERImbgZc5+RMjDx+2hwaTYp17wSQ2v8pASn7tD7+5xzY4j4VOHRX9kf5ohRm3fOtkOHCaPeZuc+SnJ7bRKQAK74vyyejdcjsoNMywqZskXPpY8K4zTTUx3kJWUWtzL5MkMjq/DtUXL3c2FhutwW77Qvct7e1nLkhBf/acMPM+toL60TKTJmiElDqeNAxyTzMby87FY970qivf7Kz2iQ+piByuoNECma9YLW7o2mPiYQ/9eCSVHNBtfI6RZr0iyRsf6L0bccOMoT/7oZd+SI1pC7ORVkIQYHKpGvgLlTvM6cP5oxiwg53S/tl+Y/LCOhnmRj6sj6m0t5nCZJUotjP+9iAJJPeispu1eDftct3OoiJvJySYC/dnZ3lZhWRFx4u/KMuZ3a9mURiFF9gbzgpW0bAQfs9V2/AcL4MSbMY4g/CtYnq/Rldo0tQ3y1O+M5TT1iD3Es/A65lft1oNlF5HArJjJaOV8nw1CMWKJzQ3cuMbpZ1q0yio+FaUkTgnm3A72Qdo67SnyY/3YPyEtgBTembLT+5nuAvpof6H2uQ8Qp4bAH696QAeBIuv2MeuZQ0bQKjtygowOFkI+Nb0MIzcbCNVA5bu2bbWaX8W6VlyDkFM8VoZquRrNviR7QXVJHBBi81lTbPmXZdJDwoErP9LwIRGyVUnC4Nvo5QoN6zztzS7ul0HByIsa34rw5dYSfMoJBSIvfMgLKZur/tnH5g5i/qrh5OisUMCrLaSYxnvV6vl8IpN+S2fNEK2Xqmlfjgq7+ukv77DKEgu8XA1egT0yJ1Jvs+aTBBmiv2QHgK8ej+iYlqLYsgY0L9aybBn87jmiHV3MLAP1HYDLKVSMCH3K7hr2HAUe9/dq4onbwgeILQQ8szfXdrSh4VMlsfYzUi9W8Kq5fdjnutsfSEEZcPKSzSoYmMa+WahAN4Y4KPQbbv04lfs2xGTUHnFHtWdW9/vKyfn0gceAfUh+mLo8yaiUt8tMqL/mv1x2L7UL6WAvh/g9FXPrx92IEH0dJaAIA0LFCUzxtAHu5O28Wdu32GQc3e/a6DzmTCEnDCIrHDgHHt8EPIRT+40SD7gze97r9TvmU1hcHkgkae63g8/SciSRGOojriqYxLIFqQNOiFkrISC7+iv9Gz2NJiQLDDRe2vZKVTtT4bS0lj8xl1My6VWTs3U2B46/OTwz4YAqUGWfOkzT2qQt2igMAl+cQVKxIAxRduP91IbpAopynC7XCXCnN77DaX70A6xMsGdCVxFsV8QsWLwhrQirjcMDm+zRlA6Zf70Qh1kB7mVp6S3L9k7H1MQqXjZhWabomvBKbYVIR8kNeDnmHhHeTGqR9I3AZmRJK6P4T9xBTeJ2HNXMUNIx2P0mw8uHJ/vdVIGxG+ZlS7h89PduOu4Sr3wnJpCB9CDpaqpByVJPwCTCeYMpnIZ2HQC/wBEjvEHRDtS1pa+L6YBJNtRMTbC4nQtuIYZJ0PNc0MNtQJqhsiX1dCa7VTiNQy4jBM6dL6CqTVP3We311E9ut98oL3ISL9o2kGDkl3isH6VshgXfCjYBpE0Cd6UGG8H8qOZGsrt3ZxpMR7iXivQQObzPNM+Yhpww3wfWkeAyrJHRcz8wwUItvEgiXYDoeyC2mqPH7+ohEfSld9cb51kpMKdE2oWZ7bDAyeHUlO7Pdevo+tIYd1jQ7bQ11Atyd1edmcV3ZieQM5R4iOuJf0N7JUjyWSr0OBdQm8Yset2aGqHP4WiiMA6pXZsSg+1oP29ITECwPxGepSJBvQvQ1a6dpVMxq9taocdUs39wnO6eIeIl4CKbnqZu4sMXIx7INAvLobu38I/gHPwhk1m44kW0ZMpqEDoGTTNIAt0RLnJ/pzAwFmHiyiIYnDNzJbTZ58XgtEcKV/KHje9kD42Byzum8dQxX9l1nQTGSC6rX+J+cMVI6N8Q4oddkRqXXwZ1KkIX+puJiKmLHtpzHCpuJe+fcQ8Y6PhMXuMI1XP5/N+e+eLiK2/kaaU6D+WWstaCY4DVW6PQR7KhQ3CssD/dv2bRSXq3TwPlVOe5AsOM1BD1yJpFJPhpYDep0zBSMWDpC/ZHD7NPrJC9lorQe3ABpWmjIAds2kF4c63egDHblIDEweZpCTlrKG/MvHW2bfJBxZHpmdh8ixh9BQLdYqScyBu5mTOgD92mBkvpikS75u7sIO0hbOwwiGaH6Xa76iUNyl05lQC0IFoCdl8ZxrBFM+tCmmtpzuAMe/u5WyxWUTLlaoqiyNFJQWoKu6E3nsPG5YYI/lzFwQC926+WIhtyMO28Uup/WR5M2JmSs9z8n5BlAP0OYYxK9fjXUfJ9tIt6Fw/THMTRUvXQZqARwT0B0MJbkiLPCKsyFcYjL+53vmmNo4SP5ZmoD/qESWNjYw4sN6n6cn3QjDIDUJsWL4rkMLa7biYVJRzXbOnNQOHmz1GWd1FJCwtr2KzD6sqcXc7/EzfcimS9jmEmf3XABiPtA/hw7Imz1//egyZYnv7Ggo4KlWzLtuiUZxhsa1M1xf0RhPmMpcBYQ8OHsf8zzoaA6yXGgwdiRG1fAeVPO2dsgUpQfl7Ec8RKpxA1yR7BSefEhV+vXyUly3U0Y2kVnkPV9KA6b0tT/4APvjkpTGYdzStUsYN8g3Un6eYfXiV9EN4EbWAGQ40bAjF+zMnfxPdeVrlEFsm+JHUYxsDmxU+QN3Ku7QcaORxahS/sKzPKJiZzpj1gy8oq/LuF0cRDnTE5Nhy5oz2DScK7pYDDwvYYsSxtP4m4hY/+ZrcwZ8XIeJB/pHPWxxtXXKAP2WFdJGrZexb9opSRhS9f6LPHTbZyvSqUC8mqD7AzaCP+3gcffvN0/tvZvMqZruhvSAxx3KKi6fghYDedj9/r3M3jAg/ds7bT43ISAoSKZ41ATXQNZRqI9nR6AAWdV7bRueZ7eWve2LkDJ5V6gsDVWjDTwaF/s/BF7OpSFR99CAf/Tjqn7eH4QUp1mv6S4lnBvwNmZmYojRSsMATxxzzDQSi1HRyNhj+NfAIhLsBL3JfGrGff6D2lOkXt7Nf8y0XxXxIW0O8GOk3tpOmQNsp4XhnQKjvv4DCQBrprCFa+rUE2at7zrm/qL1B2gux7u+IjrAF/DQCCG8XgsKlnEt9OWauJe41PsaiZOyWNL0brK0TbOnW6NHwa9CcRwJNFGr9G4N0ahw6YGDkquDG2gk6Jdj8mPbhr0mObRaf/ax2TjhYEV8gOG3fwKvwYjIS2O+2nRphm+o0AfW+Fv0m6wEoQLQWtkIUhvPftS1RdovLH2IRgMcvapnKXzF7U6PcyAEc+jx8KHkv9do+SvxORYGKWEJsP9hH6bmP3FqM/fkCEz/E3JevuXlEzWwwYrAwJbm0Q7RVAHmMUJnzS4lBoyKvocy23Htyq5wfVWm4FzZ3t/Ci7CVVgLTeK483SVA7QfUQ73bdNSov//Nz1bNVitY20tlrbPSPkP/IGSTROxcBkURa/njqsWAtjkZ2E7ofESzC2JJrdqAir8fQc9BBavFuCjZHBkBgQE25lnvV2RbX86T5JanZ/Ujtb7PEJqM5rOmcPlvXS2GBTQyxtDJduugt+QdBM7VpxvaxtQEWhGgay0nxQw/s5cpWC/DLG2OyTxHObsYOMSaFBrlZEJQJxpBzj2E9vaMNrA5qGi9FzE0Jz0RrGbbhLV6/eYTHiK9/7TOmbYo83TRqlHqXpiU1L8HDj9z+RqMB6begBxTZ9QUoggedC5hGoYbbZ+FXE57Z2rlA//98VtaAbblJvAk46z7y1mnZ1A1u09i2b3oDcaAGAKqv9zqQ3PmmDJB/KSvh5wU1hlWv/bvIpChy8hRIF0tgh7wBWlAUPk2QOQJz1/flIjGG+WqZ1susXovd+c3c3jIf12rdKuGrUT/4x3nlJRDEUrNwEYoi5jJFLvRu1PvbQMJqxRvyun86OGvgUGPu5mLWYNUEDIdA8a5QIBoF2RFmYtJvZNvQvljgZmAi6fUn2AXcOrDdDxpVa2/hOs9O7MR9byDAWgeWupppFSQ5cF+cLz8IkVk5BeI2MuxuUfzPz71HzQ5dlJ7PLrnUg8ZkmMp1FVIeBvHGhbk953rxtDXTF7xD/gZkMB2vIxa0OsCOzuQb/2TWI+lqNL19qXH1v5hQLe4XJBSx1fUxZcHlqt2V3ojV+cFbLLWqz/s3ugx4HAXl2HDYVX3p9fE0HTg9QIcjyLKBeHhZN9/tSPSmJjZp+T/xRnLa34t+4wogu8IQRUAnHCZQYV4VgKlQXAZiUZVrnZ9PGC3uZ+UF4mPYo+a9bm5Dl+EB+KTNTDZ5OirwIArZt7/flFp81nb8hEZw1AK5a4pdA+HigYRK4Dc6ODW4z+glNCfKz/XurMLJu97DDbT1YvrtI784xgcazgWh6NJyF07yHpsI7DFz2SZYotryc334D8vRPNWStm6CCaV+UQaAKkvFTg1l3q0KyiXsVK/grSY/O1UeWXIpDlsdm3hxHuoYqR+cnTfXsJEkBNfjQJ+QejRvb6qXL1IwwTqpqWXZAbf6A/uSSNMCOoKbs1ptxlQshu8ogu9xhvuYS3qaf2Oew/RFoq36A0JfD6GY4Twa4DAoyBsVvMtvrhjqpkyggHbnXrp5Lwkg6Ss9+OOmiIAo3Z4dQGIgm2rWEXzrSKR65NM8WKx5ZFeLBfYn+TeXTNadD1BF78faTjWWs5/v57XAxhAZZm388rid3D6Nyy3QerGovWe3IUpJY5gXGsuWkMgZY3qQxJQhXUGmVOAn/5GwTCII0KIj36pj7nyK7b/RtXd4di2NyjCRziBNRCHVxkRIxtEwKbTLQqPRyOCwr3jSV/cYkTR/fkHCuE4azqD9THMJPeNZzJKec4EmebhOA1yHI/xVVQnlDH4DJKpMxlmEaWEeIUKEtLyOcjDQ15KjZXLpQHRzOExqMHHBVjonwo5lE2DIZPi30djOnSEYX1TPAbp/ONtE9PoptnerfBcEYHJ9KEcgTe3mQD473RRoa686F5V4RukfjnlZrZlGNoYPw2IOv+gHJ9QyyQnIu0201m0eg1Y3L/IkePChXRbIHWfC9QkZHl5U/5ZqRx8WsXHk/cZxnJJBBxrTImQrWNfbY89H0vBZZStGYq4wPv4c/ajmWyMwQ5Jmy4sCxE+sx2QECP/e0DNddouVw6GLk6yqYpLej+O70uPrTbfws5pNxq1ozIbrwaNSTLUKzzCa3JNb8thtzG94VzFGWpiK5hs8/bY6Zu7J1rlr3afm2TET6zwArJslw1IQfjCLCqFZnblXe0v/ttW+g9VgTBRFMS+ssq9fevCL+khBkOdVrnYZQXGuLc5F2wR4gheuKXSY8xgZvPiJvuiJvbnhQX4v5X3gMeu4Ub5HOON2QFZsDLtrGt25I8O6w6WcNZstS/iN5y+uZh/xYw5MPDcnC/ubP43sZ4mZYwlecqxWx09qGKclWBtiSdFUPPA4sQgnEzkoe1XtLv3oSNam1yb98+fRFUBfkaklAXUqSSgWx2SqYyBAspAH/TIJiCz1HIxIcM0j2aDeAr6Jq3hV7nRBL/CmvB5r8cXvF/FpFLUrq8UOndQkm+3KJRCq5qXX0jK+XOu/tljepU890St+dzRv43Oh1Jcar1DER5KFXME1XkXkmvbXVn3iKCXUywfpUxRAjAL1ZRnO0jsKASj6vN2BhmLK43Pezb6PkjXcrUCY7C6bdFibB7Uzo5wC3CFqh8Ghe5muK9FV2JlCgI7d1FhN6eQn9vHME8f3T8N1MiGpdy7FyStjs1dGDwfi9jv76h3Ymu7UMEhy9XPj0k1b2hmduZT4ArL1UwCI+80nhQ4nPVE7fIJTCBtYKeKFiKcrtdztXblcv1VELpi6cPRTXQwmPKvf8C+IrMmta8bWAL3M4b3Uaud9z9QWx9Dqtxs6pmxlR6jnf1195T5Z8/lFcIHq3keX7nFCsr336u3Ve7hoB4vPMWDKK8NQw/vObe912ZrWxbXIjrkGN6SJdBIDozwk3w0Ha0Rb4cDAKPZg0oQL49lA7WjayNO0/apMuj6suKHxmm9oWv1vtt5ix3I2WhJ9mp6bYWhmZs9M28zsp2876z99z+BKqVKlMrXT/mCtiFgUDO7Uv0h1gsaA/Uu6dzp2+SAncvfFwGPI5iHsnwUKg+kW7WNZsoc9xrU+LH0/K4Ovd5r1PcO8nqLPpKAGCFtW2RksjpeZfxFAub2Lxpt9RarO8D13gikc4TICabrUtOgj1FOEuOa0VBTf/Je5IVC8IIkUB2ymu75PKG+s5hZLPLgoGSxPCuH0V5Pw+sl3Hb7opvlFx0bxd8AjF/IahxbBXjECTsB8koNz+PHXkaWaNg97NBK0MyGj9ZHb57/RZiR5kckm1LQxhHOkCQxdKLn4C6FnOtHPgKl8jenoV/QQvT+mqVXaTUVsiYPu7CeG4bvESYw1wpa5E9woMZl7qXYUrZaqz0IfQFlfomWXPxhyAz+Vv2qpXLYbjYhqugzsp6ETxcl+b6tpOy+DhtvBKDh5fnktK+Zc86WtZc6kMFuMJYGSg5wWW++BrZJZ8iJFp2uOZ4z9YnefP1qvP+ux18W/OGLHsIBuenGCN7HHLMdfufhcOSzfO/HsjfSomk5qNviJKC+8pJ/j13Wfhij8YeoHSNAXVdsv1Z3Urr7on3gSXQ2mch+PwPibQGNfgsWbjefBhXVg+xgsxJVS4y1hoGirKywjOLwVokDL/S2DdnJmAS9s9CPdd81rXCQirJTWdcD3Rlcald9X6o3aNpCOP7izUFrG5sxaZjadrAFj/0YDXB901Gl1B1Lz9vufnAhUz1BT7UfjDhkK6L0vmCL54D92hjc55oMFkPDcFUSBoClGMwIC9vsNXQdJyLer7xmFJCGGrnUxSho6lscIqb9Ye9PB1sdbl7Rxn7GyvLIVIwQ8M9wUAr0ryCvgYbZQj+m3S9wq++tOr3KBJirSi8SSx1UF/ar4W/WsoLaZHeJ2IykNSwatE6vW1WvjS9+IcgjL8/qOo5emstX3UCo7ovHL5hX+4789Zv5VIJYnrcflqDcV11V/q/usRFb8DVGD1vePsgvidj6DbHjewk2EVdmGT2dquqQYQqL9kAkfVVjxJTT9z2Di+vWYvfrXD4D4shxVA4lseeTyl8MQ++6A9N1JfwUZvk/gcPEUpFV9ukW6AD8+mDyu0cDmAnD6GHMTRrL/v2gmfwqcCl3XQyfFA0ivB6SwHtMHGkr7gXgPwF2NaOjIk7AnZ7aVNJ3RZVOjgOPzdJcbYUB7WyFBSCiJCoJx862sjPNZOgnbHmOJJxoQnYzPuVuUddAM9VAfibS2E8EGYPRSZPextwyoHuXT6CzcpNoA3HMIiELZOFyKPuCMLAlLbdcQN/VjgoKD6urJd5qSLDbyNZPMawvntEViCip6tP1xRYz9BtF+vylLOvEgwAPZ4MpNBLvdjmwtGy9bnf8Ugg8f2znQJ1Xsc3UwXb4Nrdvaj4TTmbwb08P5GPgOJqNmCj2f8fY5S/TqTUAJEWJ+Demy66R9zxmrZGbpedxzOY0Dx8jsHmcwIHrPnHeTgTNIy8/oVSjYNzkXCgpRaPlfLkg3CqnKXZx7jWCf2QhR8BZmvL6DXIHcaMWZIzXMTMSrbU7ajLW2+E5E+loTBpD/JngoxVZD1+AiZSn7JbI9AaCptiAEqEKUO64s1ta/Z8vuaoEfkaDl0h/NAzhlub+snNnFpiDwdy0yuVfbrm+REjCeDNUMYKGy2kFn7oExGqygSaOlyEv+fhvs4vHREG2trdymMJQUxVCU49m0bxgJWBS1RfygQgFplsLBIZKfp+lElY314Ozfy38gyHjwK5OhFrroTr7qwIeMAqywGvSsT4BYUX+9J06mBVtf29iHT5LWTOVGDI2SB+EUEvwg249fFp8AxVMafGXrEShNXVOkAmg4fFK8DlY+NeVhijh+QOa1nWovY9lIYd1D9cp+y19Hp62FAfJvAPP7Fab2iOmVuy8xEHdYDtqDsX2XrP5uS/Fyy+5GU70FjFabq8HJjKUqunK68n7x+weu4REs9N3UeT1FavRDE0Q18Hujf4MHebIhAVyXw4Q7Q7JpuxZmGzYDzpijDkB9STeabFfi/42Oyx7TrXyygM9UXu9/nGytm425QJILp/WvQR+Hz3JSrev8gHHxGpz9tTMW6AJd2S5l8hRSE4PhvnFO2ZED0LH4hAy3rmrrk8PCVuKonjjTPFNMws+rXqXh38DXgP1i/AsLXDyJvDTCWv6mbrs5k8pPSMHUKozkEUR2ff3QpZ48tttVOQr2jA1NPUwJ/9CQ+yDsCZps+KXUO/BjD/3nYIUw7ove5S+32mdkh/IT5QIZ9l6sjWTqAgNe94LFOKhtoPhmdNKT/E9YWPWXpH6n5LeA3KWXM/1DTkozI70+o+CFMjSPkcXMLx2E1/tXEsfrfhgiHfnnW0XHMwZUv/592NENoci+fLM5Tyu7vnBTvGqRm4wcxx5mmiRQE6+1/48LF14Ii2EYZsrUH/4JQG3N1pBvD/I/S723AyA4WcYOyPC+xiY53XUaOYTs+dVC4FEhOIUAKegFvlL51iaUzDc6h04bwMB/q3CsD8Hj3lPM2aoidW+m5XdWsCTVDskBf6mm1bpZOBuHvXA+gQp+MqgLjiCxH/u6XAqOnElv3Zv095SJlEj5YEFWPY30jbS7is1+P7Tp1RUw9zmkOXma2vpV1s830OplVJi8/JizMhdCqONtnITSP8zdktTkJEXIUYodKYlOl5NJ/wjjr2nRvkbmKtnTcOQvDIqrDH+XkkfQIAi/fHXm9Dmz2f+m833QACp2S0scfoYGoUr/07qiRY+FUs73wZquEclr7P/8z1jbagn/rgD5IuA8l0c0u6MXq5ZUcGAau09+Wz7J8IMFASI8M/t4YqHar03Yi6wAJBJr4DTGhznO5Vmy7kMFVGq5LWKogBxbSf3ZwI+Y/c9pgOlY7T4DwJvoBivc121UZAGfJT/QVSOVy2IvOPWciwt2cm/CEy5qWPvCHXDn12DRGwYwhRZFmQnb78fU30oaVEC7pKcv4zjmk11Oj43iRa+rifx1kkFYwZRRKHna72R/P29EkNy76Sal1M2g5gGefl2NKfpMKsWwAK1CW4AVF9rvr8i6YMv9FNSCdF+UHLaObGFQnlWX79w7fE90x5uxjDgHCk70wzYYL3s1BwR1arl/29Eta9IEJMk3v1rZVTtK41Y+JW1afuLrhjgE1edjgWLCfuEI4qPE9plaDDm8UFHXQXydAWhLxnvebDqrksxqkA8CIrSjYfXHLe5NjJr168nySVRUfxTE4s93lPFNixl1KWfniGeSqhIuV9v6BXhe+F75OGFA0zhwBPlYD/0fEGoa5FP96aiEUhIhELuc8OL/nRUifZmdybMl3jLqBuM3Fpi42TH3f9Lkxp8K2xLAAb88nPAwJMQiDYC1Zid/klkUhDj49a12zbuAjfi7p60nv7lFfESaPV5UiH3uh5DNoUPEaJdNFfxbdCrx8Se2Ns9vs9Q3NE4IQlFb6lUz3ZGtxbwM8zimyObT/3yJjvMRG/Pqco+G39GDukBh9+Qoijned2hqD0+cndUVmxnPJ0SKsA0MA1W9DMmh7uDPzszvk9PHVyKI1Ds7+3sVL+6BqaCTwV8vhbvaw3p1X7bxmRao7x62uxnJn052Axus/4ae8ADkcgzK0U9WnUsbYZoCbM31fXyzqvlsdMxj6LRFVugEBENiyjHkLlkuxvAmVgTKD/JOtiKxmRqjetN/eaUISaxlg2zg70Sir5EkhCqGngQ/rWcEQel7pF3lv9SmZVkwRdzg5hPrOs4B99RGAJjrli+SO+IJHU13YuVSX5JePIHCiIl2/eLOaLMGG/uLUiTaNp3Fu3UYnPMOaaDE6QutI11Ib4PjPpMEvNdxX7TIh6Ml5G9+wY9rihpVhfXTBo1WpZAXaMqvwzJLaS/3kjiN57TVMJIUOfhrJDJzNkHhHBtessGBXJGEzFHIcOjLr8MeEQH5RFueYRU8XhxnucPiZlDZQ4dp5ps52Kx9bI4cNA5/rzU3yWFk2VgCdQvI8g25T8vGVlxNOfLfTBtMQhPURI0xZDo5PL/EFnCGL+pgZb3b4sYpq7rFkkbsJVQhiO4GIfLr20pui70Yxwf4Pz/1viMZAImvXZfQ4ma4mPZ3kGHgJw8gtbeRB89QEbTfXS228LCKBX7J+tCsa/azJm8NiVggdRwnpoPnuuPaqY/ldhu0PbqN/1XeRoHaIVsxXO5Vws9Z+zLJocdHa9N85g1c2FQHaGDkER3zoihpAojMIid5dzSQTf3+yqx6DSuZZ1onAcrnkS5859ZujwA7jgu5bwM+lhY1UtoXD8zOospp1oWZP30i7AnqkiJc3sixIME76YxsDD3YG2bK94uhfRdKiqv30mIbz9k0+NoP7Vmo9J/Il9qNLGH3s1goyj7zAsuqK3LZOCuQNVnu1ymWNqoY/67sSF+J5lqf0AxgHwy50s/iYEGoedOF/xt1SspD9S7agqJfkpF53/8MzjfM9lu24j8b1fT951DOrNCFw1zmGZIYBgBrwa3OUaQxnolE5/tL0czmMPwCjdpH6jTQgGi1pNXLf50z+ZfkhOYAY4yUdweyN3Dt6YGLE4T2klXdz5cKGmqX/e7ZRPULSrNykcEvYB2Np1ufP40rmjZGuVYvtNAYSwEVDi3ZWnrk7DuiTpumGanmukm2cYDHOPN83U3zqDPW3sJvcXRhSwVk9v+Z5RYOZcsq7nYyeULbNi0Lmy8S5vY+mvnPfSjzXCwkida0GiH5J+DBOvzjqDSKIkn/FJxbX/rBkLyrwsWgTrrK3HcuoL8NMCbpgNzbrL6x1uyQpRDywjfvcNW/uUy7EsAOfPC4hQJugMmYVwT11Gf5tjyou1cBMemCOg0v8sYipYfxnucC9OuHRU/uUerS5RTNe3HWRe22hOyzpGkvw61itj0OGL9+B7TgON4c7ydu224e+/6AQn5PwcKUGQeAQpLv0PKr+waJtiR+b3pf3p8uGPbqMrTcwJkcAtfN82+xDBrFMEakLWhoV6GpmaZjsV74P/+rWK0BSsOrE4KTSLcbB5DFJSnXaQ6aRqJcyEQGrX7olOYvFeX/fcFBZtaPZqRRzmlDOwF4Lr3EK95OTjNvql0vjjdzrJ8mmP7OcbBb3t5hm/CEoXeNvctCosOVO+ya3iCLY7SjyFcopH7pqrRXrkFl4+R+2vptocf2D7rxlu9eA7Dw6+h7GywAqgR5OW87LBTXL7Xie/P+S/+6nrg8NEEkAvP570W/c36Ee+hr5bkwSsqlGUi2cRh3YKxXL9/WENKUgoa59zrB35veU2aBFAPI+2E5373vCH7KB8VLrozWllw+go3bAb4YxE6vNWgURzOeDnCNu5NOUvnLu4XDX9eCdMDvIBC0nwwZQds2y1sQyAIX4DAex9PneSqDNOB5iEly2UwW+C+vhCgJLP/+v70eX2a/iy8JrD1z6QZhTJDzBna5X25veMAgeb53PHDFB/uzHTkuGZlYggMwtUbWs79/VTk0mQk3mGwe8/s0hkfQhMZXDG6+tcKhMFf7dImX/9iz6j4PPn7q/oeIydyVlGooWBYrmBwgjFYvakaxqpTB4Y8yQLC38IiOEIluYb6CQiJbHX5tXK/znHUf+UJFOM7KGYArOYBWETjG+6wS0LKawvGk5T3vxxmFoDDTHhZYmQFHXkIW67Vcv7jWUMPywjC3JVg86x5PqXkOCCCA2RH8C1i8Z3fFTXZFRO/9VvepMoMPlqZIock/sI3X68zO19RTBWH+rExqsRegKcruGWYctQQZjPHL+61pVESxc7aw2K7lU+LyIPAAH8LzcTjw+ssGGKlsy4D0wLIa5ZOELeCZn93QtLrPG8K/06RLIpY6zFFBjGCEnwN0zP4T22vjuOOZc71HosY+ycV1rsV6Xjc1lOJHcx+xxaHfhb4e9l4zseF6OfapldgP++uWOwyXeQh+sodaehCcdVxSYlePvcnGLb6bb1ln/0IRQ4NGDW/BUEOpRyWMr9QntKQjWNEVy79gJHL1ky+fSAMdXvpDhxbH6J042JJo29aco1BHog4rLENLopvST3u8D8n/dGDaeKRTIyx+fYHUUA85phvGkk3ePPM3ymwznCI/bX/pwY6/fzGQxmqYENRkiMedG5b7vNPXHkdjBXYZ1eL5mlXYxMuwmtHjDAkgAbdNEm0AX26eNlEl+ADQWUIN/sQ7auaeQuX9088Qha/nxXQcLxGsNjeqkS2wqem91gXHIahUEpBgnWYJYzKF0yV1n8se6PIfcpc39qcuYozNZcf/5rSxHq6EjsTY8rwuMB9b/I8PFrtK9C6KrPL3wFZyOhgIVQyDQ8MX2cw7ksbMhjz/chxgg/Pb/ASSIHVfIleHHq844/SpBnJSGGz+henKIu79rt665IpW315D2SdiyKK0/tQrO9vgRBd0FUB/9BoxPWln6x8dPJpi2tsrbbWVlR+7GzhckJsKKrCMzgN3eS3V6mZs4uJmApftyODZPA9068Ex1yddIkWNmS/K2tuK6N3YrLBYO1vCNxw5P+tRUCCw59FmHSllR41cvxZSQI8PUUejJLHu4OzPlCDjh+7vQntfuTlxDc6iK7lFnucFJNfymmGS1BOLbV59pXWwLdkUEIU+UJARpIpR7DGdXYPWqs/NdF5A2hRP3hHzwl/EMe8fMQ1z6J64ZCu2ouZa9+gKAuijaYyUXWb2m6VrCMwW87OD9CqxHsQ0bkN5olv/leu43WQvASLkOui9SPZpx1n3QiyHf7+EdPNCxbH8u0YE2Led/N6PMXg+4021BTX4eAlRHjghhc0kCwBTbRMGFV/9hgfF9HyH4xFWy6urr5jqZvWvgjVQxnbB+dFHf65wZsFiXaKMvMCx7Sy3u5Clwiosj36UO37NHPmkd7XafzlkmgBb3wdYRwUuZWefLMHTgE/US0bqiOJQ3dFM9NZRFVYIctBNIIP+8Cd/7fK+7ftOgMIht9WdHXf6EDFWhB8ddZheLzK82rjVEuRxQnLTDwKF/7jCYXlt0cL47/cbDzcmVU7lQMzxkAPUNn+eH9VTUxf7sRJZRVtQ3q/5xqW2MNMNzo5oN+tJ6yAhoDrIqVVeZK41M1RPKga1TSfEv+w855Bpy5ZpOD+ZWF3C43hu5xEyxzVqPNz1YzKb/x1TSOn2onoAa1RX6sRrdDfkWPNglscbjbZoqdV4fGygrShelx6tYn4SX+QcRY8FpERf4J7Hry+p5UazqcG/mMpyJY+OU3RKELlCFD0DocZ1oqbE2Kd2SBAc5Qmuy9AonikBAPVt//O0L2ZKT1D1Q8tAwOKo2yn769Dkfky+HDXKhSyZHTHeWefkWnNrD1AwS1+SA+d/MV7EN2EZIQ0bRbTvA3F9RjmiMMXM6A8ymRxrEyxDqnDlBLYtWMDL/SH+55B/RGGfyO98iRU68/3TjYYuxog+hxokPytRhODUAQiCxaWU/E1t3pWUkWI1P9K/IcdwajUPqkyg/0EFrcS/anpiRKsxNqyw5gwT/xXNOYLK2Mf94mqCCY8W6CdODGQ8Bqv2x/d+EaZuvl17ASojjrA2RUl4k4wWVzT7hB4O0l8H/BR7V/KTNm5UycYAi1ZM1L5NBRmncyCX8sj0mvSAF6J9keqd8BW5XMCTfG0azTzhdO8bR0hjJ294U6D0D8nFD65MTbNTwFefD//FteYJxSLrE2BxeBRqO2u/LM3st22Ge6KrNT+ANF2Z+5eNBeivDw8a9GWxoV0f7NRPmC/7eVI7PXNaJob8Mp6A2bP8U2Hj379NP35VR7anJj6jRIT8lYj1Nayhwn68HWpXlkaS1dOWGPQJaHpxmoFIoCC4pxYHXAFUTjitMUfFqVnGT/E1CbM/pf5nT3smrLOrbJXXsSONbokrXwEjUp/lsjyevk4JpQPV4oZ9soUhHoZoDVbbrT9ziTHInjUx8MsBkt0wXK6+QjKrl9gxERUKyyQZKH9rLaGCyzsB9C1XIq96BiulrOx4USeByu+qOMfme8uhZOMVzGelJMzPCI0AE5j82IdjhV4senvCXpxQ5hw9H3D9QumEs7ga2RFpxC2rFTJVcpB1vAliDbjVY6OjYYnjOn18LufnpUXvek/J4uiy2aX9crqKnrU1I6lCudYicCZIvkLwjfBnVqCf2Th9vr48V78A1ByWWZUnz5seF9g0i7u8pmuLukeZQLRNlhHsa4n8sUph+W4VNKtC7sGwxk1/TS4kokyKpThhGFdf1KTnztFuhWkkXQRPqP37cZ4T8HbGpocXq6UsfAHL1/YHqxOHjWhRGwdk6Jh908dgCfkJdna6xr4ZTeA1wMbJuAq3+NoecDfmI6hCg2MXwJ4ocImic/6PjRRNKnc3PJMOJW0guH02yIMaM72yqZbKzp5XZRTV+fkx9wwua6LGXOLk5gjvX8Lz69v/KYlJyRh/CFAG7CpJdHXNdNmJO1vgH7nUiJ8T93EgY8o1xoLfS+MaAEuUf9IoeisiHCF+a4Rdd9EWcKjfWaTrGV5sQOyyoVaWmvNGn8JMfeb0BlhOjJ8ks/ht8K5vhxBrUGZsPZzKbeynn1BP5ksgq3q7YK9NSBFS8Izqi87czf6QCYpQnaKzBy6gNlPWVqv8qRX+fEVSxMDD84yucDBAhJFkK7pwMtiY0vNHC1Xgrxqe6ZpkZC1eaglcRakA0shv5HXXYBlXYrpNX88vmhiJqyQKlY4M2Q+UOZMFjiCIyI6McUPWzVQ7wRATRxJDvwZgMmQqa+N7PRXXz0DZANIr5Yfc+Ej42hN93LXor2BsMFSOsaGYv8gfHYXQOmK17dk7xBGXm6Nqf12Mz4CK3kG9/CGLXIx+JIccvFuJH92qM+Bchmp+9zUO6QH4Op2AQ20q732MdcdkWrlLtuDyglDnRLLa4p2Re6+rh0K1WtEm9NZWTFBQjful60L9QrkqBSILfD2gtpYFqejXVcOr/wvC0tN2NtCZKtxGYAzDYBZPP5bp4Nxcq5dXDKuPww6gWFvnfTjCRb8SGfcs/XoazM1i2fT2UM6C4hoJCUJUCK6TK8Aj4SC1vPEj5i6UsBjcX4yTQ3AA6bcoG5PleAzCKArCUvOJkuFi/bZEOIabTG34WF/6gnr7YG/BFgLe+llMIBMh50rCqFv1rf2Q68S7cnjks68NcbZpEPBDs/6TfP2Er7iyVgDMwDzJlMhnmKJJ9GKUJP3+Gh2FdbxPPQbcPOMXv7xiY/IABfNyU9b/2ngSijL0T7JBxDEM9Eja7BeQLolULx/TNF8OJCcssfEA/CdPFgCyttzDUu0eqmv4mluT/6tLort900VyYes5+irX+aDcY2bqk9z+CR5OJB/qnRrNmL77ztOoj9c8FDkhJvtuseSGkv5Ci8dqP7j8YzBfa/X+3tD66lfZR7MNrMLjotb7oMfwS8VInZ9CGgTu9Uw3YE0/pqyXS4hRP1glTXhgYtRw7knUmvXu1hoGnnicoebrvsCLNlA8J29WxieKPRPEOI2MArlYC81P/gEZk8zmjFwyiTLVIZ5o8RJoxIfdAOMr5xeUNjI+hRlkGE6d28J6aSLBQyKKjLsNG9qelz4eCwfDzQzPhFb1x6CvCok3ftxx3ynXdUFm2L+G7lABn4lSu/ys9F/69ydB/n4btoUBUCY3yTy779qAAY09nXfKUMKzL0tasM7xQRF90E5Q7waVrDbf4VE2gvh3NtwfejJR3hayjnkvcinqvfgd06TlQc63B/m/JCj+ChKOZO2/gei/HX7v22CfNmm75PRbmYRINCGhVATNf7+KN5XMFp8j3D4piRdahmcznSRJZBCkZ0TjD5v8WIZ5+AtpXVqEajQdwJHKy6/fVvnlR1i/PqjaJ6uzyx2rEnueGvur5g4OD/CLW21kVgEZmwXQ1UsZo4CVw/tx8V+HULEAlLzzYJWaoxNh49dDfTyyn4JdqkWYBKhUDiZKuojN4HbJ4pCcNqY5VmwgZ3uwu187uNurEvnf+a9kwJ/6f4kr6YbYafYtHvVC3YwznhfrVNLSTZK6EcLfILIcXORAUZARdb7YsvHljPOJtndXMDVE1OrSvrNKAl+YHn/bqapJhkfjwg6oin3puxg0rI67c5VSIlbyq1z4z3+9HgRnSlQa5ihv1o83g1gzVE2ewY6lJiHbY1Aj5s1GJ8t/gqRmSnEOOX/5c790Ge0MS/W/FnYLh92Yjmx+++wdpx680MP5oFgzmGzcEBs8JZXfa6YhWr6P84XggjsKepAFevm1P0dqNDJSGqI7S/BXrCI2F3HwNQa9iHojV04UL/dr8PynMELfG+TgENQQx7c2dOPwfdeHvUK5uYajZDj6nPLz2EUfjU+c9of1HJTqlYXzYad+DmXr8JUIYQFzWBzHWdPiKAkF5W0nqlMh4jAUcFDyqSvpoHNua1SbGaUQ+yGMjwPZjknNK1b1nrw+DeyaX2RKfdi0w/0fEAat6LEcFGT+wz1VIFVA7KOpUIvDFzHrjY6Z3wW5/bEuv/2BR0glYZXAsmla1+vLEKDXKRd1bGGRtFXi8feX9cOO7cAg+mo0c0KXbcPz3Mn5wEn64lZ8L3phxV3gR7hvHtYt1c0nP1i6/2ZPJp94RNJQ0QMwFh5WN/yFM+xmJGGFL0gB/jG5Zph3en8C81A8B/7FOvONp/+rRB29W0yJvCXNZQlTKRd/JrqKDmC4GNl8nepk9PTx1waa1ziNiI7lppPc+kZ/8ccAAM4X6YPrgKz730EI8ukpPkPreSp0UL3gTlg/M7ve9KiCYchLFHyHDprpCvH3/qd2qqskmogJa2fOCI+vvRb+HJ7JdnObZ4Uhhk3X+b/3Ej1JhSzCUmovsgOKK+gX0VkGwMt+dABjvVywQ4n+5pD5ko55KARppmFXXED+QiGKzD/NDHiHOybgYuP54D5AC1bi5dRwzwzAVigkfAB/+Rj1ZUzQ+lU9PHdmaQfP11yfgNC3up5iCZ57Uk5jd3Xg0bdTvnjuUnmwlYAsCiy1w9bdqEmH3Ns85VQCiMPrM6rXjdA27+y4yClgbPds6MeDRbWW4869BVmdH7btTUNwYsotSmucDeA+9Qi3x7N2HmADZcdOrqha9Kw0TQKjS9LguKaCCUDzCPftr0a0lAKU212JHUGhrahWuMr6IftoOb4Eu5KAFIsQpqz+OCw7gZy5QNYbQydUVEKqTcMtRRa6UBp2D+8rLLZhVNwOKym/hl9RBWbZkL5eOklWI9/6G/1tI5LFutG7mffxQz60boN77YMtHfxYo0CATyQ19h8EZtF+iMjN7e9kILffwKL2CUxAUR5mU9eE+nW9oPGv2Jh/Tup+zQLDzk7BOvtzPbYlYBzVIci67rCdRzrdC6iC5juZ0laLwVakCVH/YH8Rti/IVuzHcWMYer4o9eVq82fuUHQ6GMzyk2XWPlmEVxHkg1OqfHPJvsJq5qrPRlfOGAi+Pm3FJaz4QaiXq4IEiP3wqRe539GnRejEijTtz6BcRRt/VCavHEiUMf1euK7967QkLXV32a7OkPN7AwMqidJRtBVmUtWOE6YMmOUqcafi2a5E2dd7MuOBiqM+9PmuFE3ceM6UoUC9HIHitK1iEFIg3v/6P6bt17OYtgz2As13JS0zMvmoOL5AI8xcyjT6qtWTs/c7g5DH7k8SMKpI1BD8k8x350XwBJ5xPCP7V7YxETSQSFeMqQdAW0p0EhxgeAPK++s7RtREZ+1PZohIHdd/v/rJM/vr9c2vALXkCBHIWU6C4DjlkgQSABGRWHa7KQSIBymUwpwPqoRj2UtkDuJQUr5xhhhMuNNo3OkkXjho2SJbnZScZ9nYnamZcry9/Ok2xSFC1dp7rKlukJdGSaoLbS0yud7PX4185/S7rO6nGCFkBtSwa42cUJNA6/pQRQzqcpOa9D6IrIvZeRqWlKCzNcypTj7sjFE1qOpUmhd62CgYvd1BHGXmZbI68LAluQrftCea/5ZPrtfIvxySiwaHkNDr+TmwGAuBNKK/kfjxmKMbqiTzbAAd9Gjd3rpBQfiaJjoVWQ8ox5R3qNfv8c1SFzrz3MKg5xkmLylixE/aLVm7aSYueQ105vOAH3gMSWndwrPVdWnFgxw2ldYIhx5zYtbPkjAXxlljr0ANSB35Fpi985vlEOPsuD/iu0vVnTwAdRnFVg857AN/lW97+zUWprOfe98S7qAg2e7QUFhotoeedbQ/JX7tzroQ7U/dhdJwQdgivxuVmtYmrlA78Xj5r7+WescCqEw+81h/zbuK349IRi8xPaz8LA4wfEm14ILVg3ODF8bMwpYZ1UOL8q1rUMgOc2jDQhJ6nflhRpuzEDg5/ut1lprh7ScfuP16dIE/A0CvMN+Isdqp+cYSFA9gZnNMICV19MhzwA74dLC+hCFaoz/pR/wyXZq2pFrb/vFl9yJsNtgHxlzPohZHbYQbaS6FkMP3p8DCL7hGAgzQFX08VFh+ggqbyt+WVQqahU2ve4k195s9etOzzaptpC1vVySQNrZKXsXLhG43l0V6CUILZXkMTZkJnvLoa9uTu+95g6iGyD7/FtICMUsmuImoGy9g+0QmPVXrSd7F4c63jScyuxl/FdfOd2EFILzTqx2NP9UPXn+ia9S/vz53mdaJss+LO7WKhx1TwGuFfIYU874riI3V6eWY6h4iedwGAfHXyVQzHdAEc0WJpktH02PIniANGVhbiFatTdxkDqvkv7v5xYIW89oO1pxY+eq9Rq2pz44O94EPFY6ZO3NhbqpNge35Rc7STa3yMVRQ48ODMQfC9WRr4p65m3hOTE9lgoifOB890G+dhedZ0QU3TBguAyb7sAzKdLwttEFFkvewkpKn5gV9LSZ5jIfmTn9EB81zuGWdDPoHsLm0bi66x6H4yzELvvAj/VORmNPyZyLvgxUEiET+hoI9dtBNYjONsL7/zlsr+iFdsLMG/uT6mxi312Wsam/hCbIuRl8J8H3Z/dX6Q3X/bKK4Y0VwVs6ttssNvh4kfdsauZQPA7wZGWn89dbXGADS4MrsvxshmuDoMxv6RDDIeALSaJ+rwIFsPtr9tXsKEktHETQ8PlfGoSwwz/oyVN2b7ffVXQgXdLVFnPRNZJGpACHZusccUrDzMq/3HHwNqNA9S7KCOuSLYOCfRVESRcs2x3vX4k35T+wJ1V4nhxClcqZXMcdIGt/Hcc8+ZaEaRiS8Z/J+vDQvO4bf2pNgkb+4wEuJp7zqztXr2VmsJOeC7IOdjD+0gqq7DGGSHy5xqVaDZo/fXGCSTX1XCIZAtLuWSiSCJmHL0l+UUuOJCYCe/ZgpnXZ5DXjwXWkem0jzF8FJ/ESJGjbr2umW+dkxg05wZ8+kbWdglDqa6AO7uTvgNaD/5HGzfMmxsda7q8asyUU+BbE7WQuzfq1I+AFQPskhDntZPjcbiz4x5TNfcney2uvPjQcCt2YE2CZ/j54hIy/axITcAeB5ENpHwZdlZL8Zb3wEnsWv/CpYVbiK0Lb/xkXye2RyreoGSZIyulaRRlJrCgHk7zEJ01PXLUXI1i8gAue70OO6jUrkUg12Fpr3rUXXvejpDckurI4W9MDRve5PRh+GS2GT0nbyZkJanxuOErbe9FULyeTimrmWJh6hqS/yNCbEaK4D0tuv8N5/xq/7B3/HOm7iLwp9PKJz2cQDuX2xY6gvbc/uODuO6E/kfLITf7dke2GFWr/WjXsdUqbX+LPrwrvWp2ysx29ZajZLcSR7nUpsloFEvetLSZxqp0nMoAln9Y8VqwnhzYiYlKMnr/4U500VWDQq9AuoaBRparIUNs+cHTKkej00mO45ibp7tjYFTHc4vVDsEg89cAuDjiYKYFGDBS366fKT+9HdxDl6R8F3PNRSCW9Ivm7LiF2UBo9LX8cWQJy3cZITtOHuyV1FeEkHE2t01w5lRln114OcqlZS4L1g2qqu/MwAb2/dJIR/EbMHnibbePEFL1RFEsshgZRQLP1EtiYiHTbeE/euK8Tp4vjMzGYsS+pZs30Xykjlw+gyFhcx08U7ZnvykMHHGNV4OnMz2NEbzqMgahuoPsGtYqJ19/2CRkM+K4al3JpkhGaKQNsWTsjvOr4L+dYTQuHJ/vxXMexzDPzCEfdnh5S/M3/CevECq924YdUp4WDJ6Jupy687spJoZteT0zeIk9Y7YFy8RwsFdiTyYYf7i/IcZfwNf+WKEeIOXxfM6uvsyPGIuIk1hIK0RTCHYrq3PBe5NXCqgK/I+LXvKo0L9iQJ7md7EWvhrwkMN11Ye3FAJb/2+RxsSPzu3FYY1wEPWPkRLmIFv8hXVoJxwprEFiIbpmufUEb+IOjl9f8IKC+uAqzy/fPb29DGQwtTPxP+LnwP5sTLQEVzcf8qufNkrytHKR91vHVmKZURHZJ8phLO2NafLEgwYo5Ea6jPC1oAzfwybIoEDpLFxwDr9SjmR8ICAWKT/Nnwfcu5xiQ8dghQsXu2/RobGL4C/EAr1JC/dC3GpOHiGH50iei33vDy7ZY71j70D+aIQkc+aZGmDpZsqb+2gO9XfJfu75rUWalH0PlHRdQK4X74BuIDEmnlS7BQwFECXtpZFQlaNp1+X1pyNUb3xgyU+ZyquGK/9Sq69CRMIwGuGkusCYaaYZU3ilQXfwtWMjBrDDyjP62/afunA7V9ea8jOb0P17i/Le2c6yDJBdtCssZW9xJMfEKRWq2J7IHTozqPUc+48EnAzz2y+weAqO1xv9NGQIROla8/lhyu9D5K7Xja65OwwEMKg6GQ2SQpAwSMNudvKMm00HOKI0bFNXDbyj5WylQSpknxzDwJMUeYYdo62h9xk1pzSV0I6iKq8Mzl77iPSwf7WsXgo37A7bEVzgNe1GEOn0JxDv9Oiss/BwlFmoGZoP/XdC9PQv788NTyAMI6BwpCPs4LR69QW5DPfIMYaBXsaInfWzEICWVCbbexQE/RlzzI49+cKx7CBZAamox7HcW4DIuf/3xDfBIypyt4x+ovAsrTJeqTMiV0DGRTAezhvoJWWGFlAs/nEhQ6lbuaihN3F1YMcI6Y1VmUn24zrTh6594MIi4YLJ5tmkuANPgqvqxjVrZDBxc9+5LhLMW1D9z/8NXKKrF3WGjC7/Wa0ATHfL0RkU3Sv4kBvKPl0vOJ4ybBLWHlMP7zm0tw/3ZpLXB9f3ySSvQeYf74ILXuPpEil/qI+reit1N49ZDh0rz817PKk4XzK+jwhl/aEdzduHMwsnJygbCIkgK4IHOK1/ZWmzFeFOFa2qcu8QywijH1Hk0k82OpKUnfYlvU73bJo+zAXT7OTx46HK1b0UGWm39b3aUggjw78bRWaEpa/eliIo342L4VI+7EHlaUuSjdv2l8KYdwZXH5IEAOYCSnOo6vzOKEMNgV7VV1HMfJ7McX++Mb75iNQK+CAa0p6l32k4PBn01K6A4xWfNzgsZNtxSUmd69owUp/lpo5w0In4jtAADZ2iD+Qm5SsHElqcpU8gZxikUnC2NE6Ecz+ncIVU9d870UamtLT3qS+8kphQitTNltnSVOn4lu0eknB0E0uTRva2EJCeaQ6FV+eKkqEeyDywjj4iPf3yGj8mhR3UAaKdj+LOFLs7sWSHWM5ns8jr4MJIQjZs+W/lkXt/Q3uMA+aOHD9JV+PXPs4ThMTyiOnBT8PC9wAO8LvSq/VXDpseKZOeNfnnFCSR+/SCI4wtGI+M3mUgE7mJe53lr7QPtQPqbsqF/+Gq46FdinkQBA5VTpbT7ECLaLMhNBMAZglBwyj7cDPWHA7wXWx/Bv7kmfa/0I9M9nPUTvr05tBFWhOYjXPRJps40oPMdc0FlViSAp+JdolDhwUzXpz1p5vgEYB4DUEgHUDyWbqyR+/Sy5Bt0k16kib8MxXdmCySIjX+Nh+sTwceeLYftrmRCd5P4ktGusGRT9smpskL4QY8zo11wMQHvTRyVyY/HlJwwPF0JqPmRB2l/6sbBCufZ6uS14z6uy4kA4Af2mXMWeyfR/pUtg9h00gwohmCQi9UNSCuRvhQwDv5f4bC+L/4KTDGd3xWvJUZV02qSk4Yjio3R3f6Mh7PEiYj+6wA7tM7gl5HDQ3Lr5T4CvWveIWGdELS3gIuuZ85o2wSle0n6iXCSHnyxZwNL8cSAvb+8rIO+YK2LVZfuFVxRL8Snh0Bk+1O4tXqYpytncx94F04L/aoQ0u6mdSewEglVqrX58wbn3vZzbMbk9RKvY21d2V5EWt9A30SW1irCHwEs/n534keOfVyJ/Q0hpokGy3w9QeT9jkOnAj/Ek1Ou1JfV3TWbW+t3v+fvma9ERbWr4nIuNot2tGP01A6sI8BtRw8McE1gLFHwRkMrioKye2VRwbDewY1eDF4+Jyor8a+8YZ816dDi5o42O/Hy1RL13T0vqdpOXf8rrnNPdeRZPC+YOOCdLxPgngFog0vYBI7JI2LeaI9Zfn38e5nVxPhUbS9katIcYJMB7KORSmuTPCLpAwKgqFNqIWJL8JIkpEvQ+7iyXkUkW8jIoLk/LM+n3FMkG1N6DKxembDPJi+utrriqR78fgQKbBKg+K65nd+1iF14E/+raaJ5+EGauPXGBceF9T4v3q9SPNpOGCnA7vC4z7PKbE6Ep4xK0+fP7QkY/diU+Ee4nJU1FNAK/HuxFFs8IBYhjN0CDoiZVH1ExLO1hi+qkx7wrpil6viyHR0DsAQr70Wj+q1HVdFMrv5uq5faFcZVTLm4aX0YTbzhj/6XoBmc7Q9LnHpDB7fo15kgRU6TorkrHT0gWuJ5CJzPSfDmE36yKRUOjMj8vuk51lLBTQLqhdrmcZjLXEor8JAnncTtNF06U/kZuY9DCyjz0pcR+JbJSBg6XSNWf+l8almJZq2a8FMNbW+PLKX7pjICy0/6hAcfNXfJRFIAQhpttG8JoxqOliubSLXTHXM+VsJTjXHMozktPkgbq7kbePfGDlnxlG2kGGYbrbKeHHLpaCrIYBuvI3EPqa6q9pHYwPFIBFU1liM3eWVboI7B2PMCe1G0EQZvf3K8JQX1bvpz3n27WdkYmRChrHiP96xCsqhNiSxwUs9A+ekSmf7chP3/fR7nvFsGwax6/O0EWGbmH3CzsPsOXn+fNp61MDLhXthPP/lg1o5XpsgcLziRLmeCcuihf887LVz0yiuMLgjmPSbVhCrpyA+o2stlXQesns+qr3pUElyuajfIo2HuHVLCRUJ7n3ic01NmIjPBfPVhKrRzSbilnJIH3KbsgbEJqjE84ZXnpbs76WkEazrIc8BrvXArbdes+BrVcn61Gz/av55f1N/yb5uP+rynyFxKkLgFe4ZCvt9/dX6SBQzcXdRt/bjFCctgPfn5ByycCqWi0XXfcFKMvtxpiR2+k4vDvpY2A3qfgw/LNkAJeV7Wy6RYYV8GfeD50ieRurdNZANv+f5NrCPD6HuDEzQYV73hxvNI17zaKK8RksDwFjgB7IiQK4vPRdunygEOZ6zjf74dnoo9Nf62o9dsGNeAWP9ySoFiI15Nc+LgpeGIZGQ3pLMELOr83dkaZ0IyHFBFq3gi8EGQM6ArfWJQI+Q3ENRsoZUPFGADkXwMbEy9/t3ZqnuXl+kdLYsEXotfSv3uXY/t70+fRGag8Q8wvJUnBnqVu8jwdEt/6y4AIfiVUW2vDevw61aNQ8OgJ8HbeObpYJFxs1MCgeInMW5rfKl/ElO42uaiQkyRSuU1gcXaybAJekNVm3NDMWFIzL0EYBbbZdythblxoORTAEGUYAhESYFp4gLFKFctkxI+/mgT1sWkCX3xKBJLVpEPVXckwlwyc+uwoXLOsGq8MOZONArF9rH3C7gk1ZdfvVEGODJ4QDFxtlP5viVXgVokW/l2EDoTdw+sy5FTt3WZkFQaO89m+0mmJsHHC3Ga1Y+SEw9YzqGr4glWlDSSaQZsqune0i2k+HN0QDb2aJQTOMMJVfNAqW988PSJY0O5PopQSzTXZLR5fApLZdpK7QAyGQnJPaiOJVRXxrjGP11KU0Ltodc6uHJGFrmcH/o023gtfi4VcQCg8yOSgE1iLvt1KwmHtin/NlGhGjTcoUJ7IDGbeUcmLaF1eAwxch77Rw/wvKyoDSklwbgk35skX1sk8YSH08Oc9U9JEVPGIek8hqpZ3mfYmIPZorhzoTNfye5dyhJeI7TsdIb01EUb1TfSgYaiMAr3ErqQkXyK3WO/PHYN+qKtE4KWsbJxicaNrGAXdrRlY/ERwueoS7g4e5HL9ZCUL+vcG4vmy01oq4PqbEkgTUfsjhwbSjjTDanjGE+yifS6NT5BaVtkynHFSvKUnI2x3fA+Z7ntRHT3RIyTTfsHJV7Tj33baSbsDn7jxQqitunDYl+9VJaz2IwQCPrsa/2Lgz2HqcFnCFXFTlCH+bnvUsNywJxAjXX5DhhpIvlquRf3vcXl3DMXzjCvNENm6TejwsUXvzG4/43alCL9jQeMqfgFax6DCV0dL/1XoTPLij14xrJiM9BXwpfYaa3nZrcf6ycouJyEk3UkBVGowUUeJAvTiiZDlGcJJgAUQyah3x5/0vGdwbjZDqsHXLH4ob79rIXNl5evlzk/9HP2r/tV3kGDfozR0TM/8X76+o9lSpUny1/QeLZZwOMBBa7VDa6359UPe+rrbxsZsVq+eWd1bkGRGuHtEevDW3xWuVewL8gUeb4ihuZOVmPHsvPMNfYp8byI7DQmd6lOyTL9ZXTZY+EkeKdOuG2zQ1/0Cidam8Mm840AcI67CsJ8hd0NTg18eAI/Ia9LoP76thQ9wSGVhY9J9Y5YMZ6WeW5ZuF1z6p6A38IlqF7Ci8jTgixx1G7mYxzMknDPVs34OCuKVBbX9YI3n4wmFSVIN87sG5wklp+DKdoGTyxnkxmiNwoVjkqSU+ZbYRDF0khz0Am+javQD7GwI52scY7INffGOTW+MJds3PYpZTX05GdJQVdYttSESrJKwf3iiKDBKIaHef9P4l0H6noBea3Pw5i37btYIQ8VD8LW8ZRtM/HYAHwCI9YTblgCBBqYccIn16r/ed/xMTi948HrlOCNkUfSvZR7AzuUNoH55cR1DDDyArjXNtNTO8DTpGgCUwaxAkALcEDrJtX9+i9GFsbly/jYE2fPzqwsu+r7BR5PuJBvZNKho0AmT0TTT68mxihrJFKAvEuAaESnPuvyVdvH+RN7NXHT+IsKrcoH7G+GCTTpB5+WW/9xA2n7TqZskOaeR1E0tFesT8eN7ITRyOBKOeRrXtesfPqTqK37Dx7Ruq94Tz+TnbUg9f0pAXl+RJmPG5CcBRwbFn6WMek8uzBC6BRoAnGj/IFqJPdJw/cjlXU6//XZkrmclCBLWg4CpkeCPah8P6UC99Avj0EAVMvWbc/vSKddNUGzeUEB9FGNO1BMqsuMzvA/Qw2aJE642VlNAcs+1nf8FCtGtqIEc+X7gGHq71/03Ev1EGvs9lnkb7rmcc359nskqFRcIxH28lD1OoOXoiIWvMVOte01I33Y4iUap3+OyOzaWDTHQQ6QwFycsq9C/TsrfRUo6yJvZnOVonKvCJjQOvx/+UonQxP46KtMNF+VfKLR64pnKYqmsLzLHnhTUe5BVT4/NXpHzg63netJfpCxqtdzDO4V5eZM2bpV3rrV9TvvsH2pesJb9sqiAbujwtWJvy06NlEuVJe3C2DUPVfVqSQn8efko1/EkrOG6VdT0qHFeF++i+Gt1DlQhGod9kN5841+ID8OCsKmLZIcNoqEo2rn2+FSMp2HcftPPi+70j6QIosUphDeLzE6JrBGRM6mg4vFip+u7MsOJMUITsuJQ2sJhBmnGBCbmJtf4o8QdWSP8e3Lk9MGaBCKd1XXNXz5AxTdzWz1B93ElZohqEKr6uRU+A+z3dJ1Z/A25DIyMGfIG2ZCN9b0XJUhdrSWiY3upRkKaA5tSqzXiU9v8pRvNXozVzMvSYjtm+EXzjzP5205MHkng3RD10bsez7TLeBdj2TffcpQiqcxX4LsKjZ2AydsKshZfq8jCWDLv0yw9KN5d6Y1kzuGhoIwxFst0EDx+QdWnCcFwVipczoyhlxfdAk01dUbchEa0gSe4tIfHcRf9RPAG/5VJ2DxjmD3cj/SybRPG2sRkEGEGx1XXGKgRxudGm+HPzmByhHTDv0o6Vx9QmDeym7JxY5YPuhwvovyo0SCYhfbd/S5eLOrXNWJ152PEjA9WOadNfF350KuoxGKC9G9z4i0oX56rUS1pyfQCDSw7lDAiMdLU2WpwGIkhJcEJ/+pMji54haI/+yYybB/OsotHjdA7Tjq9lVkXhsGPP/XvFum5RUcySX/jvO8L+jDrgTjun9XL8Dc2oePiKfO7hUc3V2L2yg7FiXwx9Ukcwof5Vh97+HTpH8u+sc2NOIjSjdOQlyotq76jpqp4TnWzK+D10aMRz0rDYHNmWi7RX/m2kJdS7nXsUL1RIvORK4r0E5MEVHBpNzAqir6bQ03oVRDx9lMSixJr6bX98xxmpQnHfdrefy9iGZOnYup5Xc4y4uGI4dqwfDkm/EsMn4K13jhhQ7ii5MjKWlADJRzQC80ZfEt05DHIY4melrrb0s7/wvVdqM5fX22Hjws3Jm1iQVm9rBYBeaus4os5z8G9wiF4jl7k7/mr0EOyufKcKM3LvaqVvy+b1FnU+SXa3zCAcXMT7ZJ//1SFMHPCjcX7/c/BoWnhmfx2qJv8Ln8b+zjWpHZ03SFuSio2E9wEPGJw24CMIAQTaeJCNc8UYzZh7/yWCE8KJuoFf4gxK6rTEyXhhZ1nwpmgW99FQxUZK2mD7zIvWKWDhsRsknhhRtDbzdrfytCHw4L5AMBqkK2ZaxIMihoV+cW8/C79juTn10f4HrHLTdkkMvRjVR3cOgg7/ZuR+76xMyOPZ+p8FQVBawwQBQQHgr+qMl7pYZhcvHBqisZiqkUOqcrQCilcBQjbhwrXaNCkaZSO5vvVoPS0LEIaDiLj6kK0y/Hv5tpfnclpRrYR0GvLwP8qIhaBJw67uB9RJfozk1OdGEHncRMHz5PpUQIaJStyWO75nCEmXCsOXTAmsCdsBCQ8c9906a85809pV15P59LmRKLTaeV8Xd9t12jkZ9Nff7t1Z0XJqgCtCNG+DSbF9RiYWRcN3PVIOrRoVpenY0Gs10sv0MtDPMeQb9k9A07gWkxJ5j5OM5ZpblGZvdAhp8oZ3dMkfvI8Mm/eDhymWuLthVdG8teqRp5r2M61hcPdvRnGETIVQxN3uiebpeKSwh391X3dyXEkzxYMgx6U68+7Q9Dc6cvghpf++JLeqwe+T0sRhBD7iAFkBPKfZzCCVJ5+R+6pZrex+OlH0H6GpRsWQK8t6FgtlmmI0fJ3I16IqKWjJ3gpytLwLjf3QawRSJubr7JIvtfxkwY1MVS3bIF5065X/3mfXs86Y4j4vqOqPSBaoglsDuPwQ7kf3n+BTvaHh8qwRbu1j7Lt/YY+BB4h9mVvj/9eBnqjC3fvdrznZJMgV5RuinqZ8d98yWzsD2X1VIeLlTIbZkHQVq3VbriZN+VJoNrxPixwra5ZXQ1REECK766PkykwZw0phOGtAdivx4Pbv4RRaRAHCqtpEXc0d9u1UMOraiZ6s3+UeO1RKgvFPx69v6CXO/EzzPBPlf5VZz10z9FsxsQIrxQLQSTzzILlw44rZStSWlnWyvQMYXzrPu8uovjW0EeuE+WhFhDLIVXEiRfbkW8+T1Z0wulAkQYidH1/UdeB9azMMdE0+2pH5+xJDBU1Pguq0lJSW/rP+ahJqao90EYczsQGUf0iOo+HXAAWtALwmpM/XgnxRfEb3P3TyqnSdt8ouw6K/ASAiLqXLj6WlDUTdu8RkijhqMFouyHmIrb7mC6wX26rJQx8NH3VyMcnUYGnchk3s2Dvn/Pv6/d/KvYGD1g6tNopGMVZNj5ExOVRfviDuPr4Xr8TglwhWl8MiZFlt/9IdNZQYITAlwv4z8d1S/jdPCGhS0n1MTrKzou2+amw/0RV2yR/AzxuRcmGL/yuTT7gmMGpWP1yu4JJ+cB/8GfOh4ncMd/id5c80xoJVMzUwTSKMVWRn9q+BBP5cA+JPxP/N34T5+rPEXGARtEqc0CIydHjuMJm3sswwTvdlZ8pDcqRZ0K3iAdAPZEkZl5owUfBrB+GbEL+fEkV0EJRIrnhJWwnoc74gqEdC536BDpgHY/VT0biLzeo2i1jIXqgK8ykmsRW36P0HtyY15uZJPoVT5LkDNrfcftTm3Ir4gOpzbcu3All1bZnmqQ/GkTxRvLuEmu5I57YQD3DurofcHKFZwpckATMhp7y/Pb+ijLYb+uRykqebg0W7zOBRQexx5S0ts/+ZnlHTbDth/kmOAT3NqF1LsJ4RKdonmmLMX09jHfZvfDl3VwKZnl1QOtw8g9KlCm6g7pVHxOgqiUuq2OYO89nYBYi4/qKtHPYcbTYpq8Ah3EL6f6pELchWmLPtreBdAgkiUXjSh2MxgxL/3F1rMeLZlXZXWgLXoAfe6AKHrQ6Tgmf0z9tuz9fwZI8lwnW3VHkVNs9amBPP+TpViCVm+pw4gf0gGzMJAEZ5FjuFvhzDlh55iOJf4YRSxyu1YUWGTtDL+UI5ktvRhBSjGSfIIDtC8vLAqKZlaXVLzD5OgoW6Z2/96Hz8vYEeVm/ZKhKNrmDUX/ZFkmML0kdB9gSzCS9bKMgTCacJnxzpJnXD2Otoxe+Hu9J5F0ayi+d56rLwkEGA0YfqiE7QK9lQb0b4rGcbsjeaGhfBh0MYOUIYplKrNX/cV36jk4ZDK7gcTIKXzgIWNPWU+btUXowzuICFBK+EoYx3urLkqy1tS5Y3hIGfHitqI6ngHfJi1cUrxua0L7ASU4QP6L4UAHQkimw6JSu8CQx3ePgnSInKMPaXJ8mzsWBYkBhWIiuqKeHSgtABY5HzgydDMOQ8kyQghzstm7w+KaMSZu114rHFCFP+8p5w8uGV/aEULgMZ0rWrLCeZno31pJ311wxtwqiYp+uHLedYgUu7zgz4eVdezCxwe5/IziI788dwQSoPwORraOLFlq3k1IZ9ucU8MUMKgDP6zPsj+MhZ74jQnZ0We9IyHcYkBygCqGV5JZk9NHT+iXluUA/TgZWiFq7gWxFCC3oAePnxNYtJv9jyRiNuPyc+uR39Tf8FNKIktGNnP8pV5SAr0gCFaIOj6PYaObRnfczby73stHkSSR5//PSAVlQ7P8moYQuBM0iiDn6BrbQ7yEJmin4/q9UGfpmqq12HeMj0tcOMORnu6TbKwlU3BKNqNro+2JC1jxfHNLXxhd4USvJTz1q0AsiF3lF6OiYd333feD8UBZT+nTKtm98tzLCtt2L3KUtYe0vPfRkpFeuJ8vkfrw/WVdpZKWbe9PLAu22UiDZasNAf2fa/rPJQs9H+Lv8ZZhLEAB14NiS7inaN5w2nrW4kab9B9izcaRDHxVoqteK8Sqy799RYRaWxCGs7e5+iWXfcVHuPEXMwyEW+6msrjxPfMbin3BTFI0m+GCubDCnpD8u+RAaUs2a03fXQHh3y6fI7aCFhapgxn/6DzLe2K41ahGLw/u7IZ7n/CZiz+dHJL+K7S4ZgmIbmxD92aOL4uc8m3+338pf2fsJZybWLYiywY9wshXTdgTNXJr3vpHv+l78zR893OMc4TbzAyWfHG/r2HMmAXNk2BvSZkV0ytM6bJa9T9vQDGlQLlt1wj2G/b48exsRFvCVYgE1k1Bi6W6Upefnr2k6xknLWNTZ00y9aSMDQpJya0JdvRP1PMTmAFnSA3Nmpwf9eS+Y8q+az6rquzP+J9IFVfP+ummjWeS6W2TeXHCF9DHmJNqP/RtOoNoPEnpDRpfc4xLBxcewYeLJmuslmLKk2Z/y/RlKnCCkHYN4bI+qhC8/MSivjdR1qnz30iAociTNcLYWd6OgJMxVW3f79rR1ilsuj71v8dTqT/1zCbs+8WNLi8mOSCjwJTEHpzfGvPbB5jmh3I/Vw30CosS9/bC2nIklvtwlytnxTmLfvGlL+3jI3Xwx3gmazr9/Nz7xS9p8yS/+yYhdslEH1uJIebBdXIE7iWB3//ZChkXVimciFNVhxwN1I0ifl8OmpfQmrl7MkMb72c/O/Y9mBMFVM3BTx5qpXteiVQ8V9PtyMv83FGvUwcxOvjeYD4UNBb1hMs8mwmllyjBIg3No1rvsddzctgf/jc3JyG+ui21G5GWMJNvORzCVNnOsEQQP7IuAshkByEprpMFBwSL8tWLUQKeEvCb7m02h8sbRraK4wLt24J08yTRzYs8v6grphIa0cAS1hRxyqZifeSqeCMZ78RTU7idSbbJAgBkhpOoA7Nq8fDsL6jZFQ92GlNo3qeQClRzhJetdFZtrE8OM8xfLJ3+OqtSp9HxfwM5L9Sc6EcEFYw+JYdiCZQoxQDf/7kMOnJlvqE/dyEZq/mJ7zG2QG+DMnPO9VK8BoexUoMJFDnhX978xJnh5MuRRPKyRbfaORlOD5kKTKucZQK5yDQKsWY4uF5QWyn47nPqyEPf3jcFKT63gCLXT39VMnWGYkvl2X9MzIyFqQ7rw/yYgfkIQaX9fPcuD4KDMFKWzpt+TCFymU5bUWPOXnFvIFHL9mySKrwdF2xkjC2qxQbb+iH+e/sfPcKZZBrl5H6VvKsw3BAPWy6LsvCC0tZ60IuLJLxteTk9mL8hUXqpThxKDhi98lsJmGfMSTZr3dZpIJeImR+jlgGQFCyM/KcAcmf/6uwLlyoHmjN/J3eR2FOJCkbkY3s9Zy8g2oszmiTeF4mlryxejxL5v2ASV6wZNTugnCzINJ5zjbYX8FZ/0DoNiyZNEU7NK2H35q0Ijxa8qImtYNgisV7Bb7ywVwmSsFddcAkJf37mt9aCU9hSCJ1fdUHl1TYb28HtYtahyxxY4YD+4QLTbXBctcJcd1CuOG0itODxGRczPFk7Vm343Cu648l/T70HhsaNXzlJa/PK/yI5RdnUPum1kS/g99cgsLhSXPcnyJliH7eUZ/VAX9l30goHQq7HgMngal4SJ/qOfBZE7qBloj2P1H9Dxj4tXRopm6nokJHaAjPMb/Fud4wmlAoPWD6FsNRfHk3IPaiR51Gwu1B2rIJt7hiqsTRTZdVlPJeUsDQRSCh5/f9f4vPT68qddDNgxyDCr6z6DBcL4Xx4PEYLXIlIgSNsUPmXPNtZ6cdHuPGdkuyH84X/QDXhUJwQfK5m1dRAuBTUxKGHIjfIfihRpIQPuJW9EZzntS5oO+eFPIb9Z0B+cWHred61vSaueOLZ3Nov1UwacE8Gjs6LbJ7ED3Hgw5Cp96ZeihcGUlUvrDE4KxpziEligxogw6D15SOtWyyGijazdv64W7l1UFnZwCKUpslrSmMx1w7isiT8ERmzUlKFRhtZzzBBfxfzT0pE4lQLVh49N+7ZJFFjpPOxxXGpOLaqtXH/knqFmf49lle44EUriWaXG6PB9qIX+11VrniaDPsjnKJdPDTQPkl4ZLrb48dvZ0a/91Uw5/srh92uYOSzLT70IL9ej6IBEQY1Ip1QcyfM1j8jOdL9Y+Stb06rD36/urfN2u5ubQI/lQMw5kfd5nqzrDnJtZ3ik9teQmWd8FngeCGQ5Ec37/J7heoLeVZiq2fpcc6V8C9AnphEpUj618DEWfGamVPzUZ89vUaIdm0Vd3aaXKzapCNJHo6G7zUvSXfb8SrnRpp+GN7p90D7c+CUkJG4THG/WUxEF65F/adr3BOYAPXWgjrtzw00nSovjRa5OfqSGPVw8YggDyn9F9XshQk6Ct3dhNiE9u6KumHQTWvenPzc4QDJW+c9TKJZQOVCV0gD6DO0ink6B2/bKoFzWsGWoPfuVzlNpEQuJFTx2UOYxpLdITJSq0XoP0RRqKI6jMR7irNHiBvm0OWU6Se+u4oRWKzPBr4DyVP9Z4fSu2JKv6KDHHkvKvzDvhVMAAdrjTCH6IhmqZt23aw6PYMOX9rrWAatxNmwEyUOyNzpj16vSTSvLTxZTmO1rcULkDqu26z0nY0A1lHvUGXmoln4qWq0AoJkp6z3PsFsMH9ADtZRkZD3U53uY9fTXu16yL/DbvaWqUqz++K554pp2/kAl64IOW6yuCWFisEB+2IWexR+fsSqCuRoIqxRmk4ZopyfE0/B6UXRW9nkAH6dVsTqFVjA+Eg/6HDpkPdBVdHpbLkv2E4z86BmfKNJ6Phet0pKhNs6/nELZdy04uisQLPeAu5zO++0D/rHhHXCPwjie6x9jIIqbmISXbBZzhHptvBicgIOqaU2NvvoSee/+Cuy8YVFor9AMuF5/3S/KsmdBlxJeJZzQeA6sGDi0bHkrGPqtpeww1RizI8JQvbfK5IdItIp1t+yUaSKmts/hpUSt+8ZSkCJBQqAe0tTBKx4hr3qM6A1fbhj3ixRUh/F6WogOOpgvn8URy+3zrwYB3c0seFwGpIYZ2TP6hHYtJ4t4Ygv9uz6D1kllik7OGi27lyunXvvxafVT+nKSNfa8FV4/Ten6E0KY+G5/n6ZveNCcf/TQtzMWgMN+Sqlag5FSoFWGHWn1Z+HvPg6X58pE5uSFiqA1IR9mqtPeoJjtwzeRv5/PnJzi6dt+gKFQgdbJ6OtNFT4cvmry2ob/vt4Fa0MVC4sp2P1mJhOSYH9HSLoNnDDlaeL749FcKv04EILYpZqZuVbeeU+saS+PZbZGQvGh6BdqHcpJdIdj/cb2PTCfT1BeaDEJcXKslld/2edszupXOrljuW+EwNuPWvKdaqH57Jsai554PjhthD2dODJddp6KzqSQ14qQ+rO1jzKmgHvzLqSxzUnvEmOJ5csIYvrLE8gY0qeeaOQIE3FRHxqbfcFyMYtsSoVCErK5TRrP2vYjcWVIncSADQm0Q2WHmrolsBuqn3rZo0SLoWejCJ8XFjbvGmCE5XFIXOay3dJfcsY5XAMnirmM8qsJaMz/UkOk5KOPuj/fv29luHNiNTj0ZqSmxQX7BZCt3FR7UVL/rJsEVOQ2+wCz5tgopvPPsUUaYcPJRkipXy02ROt4MyKJ1osoucHp1ouNNZFx4gEqamzxHoQHZ51YM8Kj0PRZ94u71bdOKmNqkavg70okQ7j3dxTXfhsKHHtS9QhPnvQJ27Uh5r983jfxTNMSdTTl+1emFwt4cEvrlceTG9BsijLOgVdh4HY5YPcHSCi3LkUhso/qRudediXEPAQTNLtTuPZ4BBVhl0WY9zizdZfUu8MW/BfoMfHZA4UK6D9z3jom0kNrshmqTMMwwjbe8gkcPIc3bvTP6+Sl92aa5+ePzoe16Nk/ldMqsMwoz5fzUR+tgASMYbUT9fNVzj66325AbKoqleA5gQDjA3kFu7/gRVTqKdxoq+qPlxkwwoxoMqvz4vqDbXlpfcYWMfQ1kemEpvbZCzsPRObttdbvVEy8GT6XZENIHkUfAoo1LMWf7AAzCgtWDMAL7S8HB1FEcAEL8qiAPYkg+ElgjjufN/qzJoYzohW8qAPNm8P5Yd/AXTEWdOhl7cl4KkQZRKoVRfMIgsJo/PFoY/iVgN0VUwNN5jRLhHWmwhqFu+8r24sXZyT5tUQEBAsNmj0FWHL0Mp/Up2Md/oYUR9DZm1v6ZLbTwmSO3uofRjITAahCBf/kC1OtNJiuRkk89fXgukU0xd+0U59+HETZaPwDplFhDKE/mxCgHonOTlwsIKApJIYuDp9AhS8EUeAyguepMwAVdf7xNpCmUKfAavEUInS8T6OWRVOiWsExYLChME142jjOAwUDy3gcMrMNjrUr0gjpJ+Xi2rUFeXzkRr6NQUPsUQG2iXz89QuljUEZtwT+dezyxaxCJXsDKt+TluIbgUVNS4vmhy2F4Eb1+VFJmMCTGgs14f6RMraJ/90JB5R7bayUjMLdUTdXiztBaK+5CjkBoKTzZH++dqRjRbQiSaqEaa6fqfattVRRmbs63Zoh05edPQ1JJuoLtc/Uu6dCxOXO6f2iuVr0p08EL17pHvEouB7No3cyV+5sAeTbO6HGWSuEsWLzTO9WCP49WSEArZR00tUnsbT31weKJf5DX8QN5oIhUAGJRb18IIHBcZzxDDd9989dBRitGigVpUGwxsoCNifb/hntl19X+R1r5BNHEb4h2bSpD4extvzz7ygysL+/dMaDVgEt4Mm3cN2M4Bm/QqQPWCiKA/Txn+g4x6mz8UofSdpfsh0w/KbVTNCD/5Aa6Nq4C/051sVeT0xrL/J3JpjdnhTMILLZFn9tQzoaHMgoHMPTPr4TpG+Y93A3TPWbv9Fm3RZYrlkSSXRcHky58+dRo+GiEKXo1rBKsWrcfY8UByUWhqVnJjQW/jVFinZGto1gh6P3fwqJOYl8k2gIwqW5IdIZJ+KLjSp8wz4zFGNpc2F4fa3AnICX4wCXTHcGleLhOaA8yw/3NCmF3ypWoUJuKDfbTQn4X72hPFcy+J1grk/jv//oJixJ01B/BkezW/hm8/t+D+bYRfIYKMr4a9/7m4ZwB5zpqXM3JPs9Afduvg9vgrbmMjkggdBsXAZFl8tVBUTxdrN4QRsaTTIR+VdOZ/pg0ok5bCjUV6n2GJn8vAcfrfac1O91414qsOHCYHRzF+QdOL0s40KkVIc9dUCYOU/I4s0YkYbJ6oscO5G+84WlK/gzTDg+rkRCdJTtSGI9Y9H195/n/YFa+wITV9VHq4iGDiw8L1tqTBRqqeaq8UzvDY1o/FLOCpo9ybNwSK07sxS8duD13LIJqI0vnGG7neZBVptSopOgHn5x7dDYhhcyakJFIL7A1ZsDQyOQcefyHL2Y5zeJgNyruG59R/XXY8i940Mta3HEaf7yBlhXPO6myeNAYMRoSC8I4QHZIM6uYJbsvFmH5jiVjPwrL2BbjlEkJrYvjy3f17+a95t4EmLsz6a9vBHL76lvVBzjRbKrr6zuA6VrhFpPCReLy8PaNwvRyyukGxB7lgtTC3CVnm89TaX+IVzMzEz0bzRFz6zgLg5L12Ucdh5xpWes3uchknlhRGqkmqxNGT6u6CcQOMERtPfFL5znoRrtyUXo3fliyqIuACGUluP05BKRjjuolxRmSj3gIYauE6QI77w1nYNRNzphp7uOPZFpwRWW8LCAKtCY/MiwBSngGWGCuB2I27KRY0CEHbd76WAMSv2ypgW2CMZGaTB9E+rJYQfcXkY2hsxByis+NHJX9iBhIUk4WT83/cao1X+RyuBskGUoXT2JfRfvBmgw4g1g+d5lAj4tnt8nxtIho1/DHE7pCeg8TqcJESOioaw7sG4jj5VejMjqgdN3WXQ75ijtWckbmeM7yNAv7qbs031UFIb307RVSYzMtbPeh/g5v9wlmfTsBI4cvPWalaW8YMuyCxju8q9s2gaN/lJSVJKsDUldc4C/J+uqz/UwZYhzwhR0or6BHnqKzcENXZ5EuG0hdmuQ7s8aa8I4PHw9RfntmjtTKdBvyQ5mijShofwkm+BT4sRhs7rhhIoKiC9i8XsDKEr4E/RFk+xk/OP9L4WthiFCKz4eBTNwiTXpfwZHZLGxjybzY70VTZgd1O/igpDTsfx37fbA450DdMdn4zdPFja49fwxVg+bWT3qV0UpKn3wy8nWbxoRQnO9v5FmHJIPJeMZX6Y38DLa92yZQsff1Ei5dOh5RHjhmAwY+kGUWvQX3MUilqiBGk9N/dJUNri+1C8O7BvWcQ+DOfjZk+aqaF4GmxWTiTLFYN8FdxzgBS4uuMyLEAi1uRE1F+lkGLG0QQnN3+/0/QmGVn39rnjYjn/onM/XWPc2XBucku1QnzxXuf0bYpoXADmz/FJBRIB1ZDVCjFHkL3zKdjQQcxuO/eWpbqEeTrPP1Os2POhgBKfjIef5jHcyesd6YV8pm0e+BEru+kZ/9tzEOcwdl49/2L45f7ffXiIRY0Lx/EmVcazaJqFGrvsTMhYWFFl01FR+uIxFYvP88Bxp1IO8Q/77iuXLIkI0oHVFBAlxgY5T0PrpWeDaIT/fjLGG0YJbHqKGE4+ChZmXGtN/Z6jdHTPRfvZFIgpkfOfxX5xqJkVWJpWdFk9oYgweMA6i4IRDFVd3OvZs7H/3WEwy2hz+9FJCDeRpoewJMbHYhsuHdcPmzwMQ1GRdzuKgTZh6xZudvUrvH8uUs52I9WcAsbJwvU+A5cn0CI3zHgFVXAOSzvdoLjYc/1sMBSdnXwEhRufIwKHQHTIn883/DEey7tx9i37B+9Hd3XWS4w7LCjUgEaUJrotWgu/Kb4lIsZaWdY7TzBiFn58t07HYdXX0TcphbW1UXKtQ38+K1r5M/j2AVhhsTJTFmiQHzYhG4Zqzso2Oc358em9yO/lmuVOUE9qBh3bO8OuUpfFw9MZqjiT5mxGyrcvk0pcxTcMmHMEBQ6VgiMCigk8KtowEq8WM3x4EbIU80e9NTexyb+TYP9F63A1L03/e3Q8I5hkAjft+YG4IiDoM39wyZbA1Hm+MTjWNvpmmyorK0GPN02A75UAIiQc2u3S2vnN0NCoyb64v05FXal8TZymQXS3XDEQIAzAzMmcTWKTwaYAQ6aq+Obu5CUwcV8+nMq/K7EDuzcXyk6b7Rs9pn9/y8pK+u/wjmOBsfj9wFzQaWbbSHQCVJRu2A0HF6te+BPuNnTOQV0b/L7Qh/RQX96+spi/iVQ+IgiX3S9TRWNEti1mSiz/EA1EotZ16B69Lunor09Gm/7H/P5kEY5fWn5ebqqkfI3GT47RqPdoMTpiUdemytU6NlrMWghze8uu0/skW7lObogMjnAzH285YqBppPd4NkAt+NZZmPPhiLUs7YE/E9B4GOdUo0OPk5nvKPkyIC3zYd1+Up/+jLYUaCoIbPMMZfkO1GmwWUxHGs0ANdHKiDpXaWFvNx/y+yUx0ERrak2pj4d066j1FDaWoIf/GfQbHKsam69AT8hM1dDN78BXjjeDvBpbOMsNfMcqQSKLQe89DuQ32suaar+g9srRO08IM94/05rw+NDyaqWRx+/SZv0MJTeX0V5m9Go7H/k0rM1fuQfNnKHze8bldn6Dk2eZB6x/ipxQ6+1T/ZcET9wK7FpXMrAJoAP0s3DLy/j4M7BO4BSno6d6c8deUNqrZWphSmMLKepyXNyg+wj6RbruxudTOdB7u+WbByidiXb7p4NjFdUK6Z5RkRQ3TZIMLe9Aw7EgSQlwr+BycgPwBmdD+WiLZfCoRgC+ECTuynf9jlFUyaOBA2RcuqO/nhdvYiBKaAF5MhGDKiguuiel6e4Lf4ua37mRXpM/od/vZ9xOjhlQWAEYrlhEXS4IniZxh50szkAAcqWgwmJ9Nf+S2htRtCDETguQQC8CltQfLCI39/XQWbHITjQGamm4LI3vcEw5IHvh0gfVaCO9zCvfVbLOiFaKfPO7vmr3IBrV0GPxoOOJ79RuJxCtcQMgkM/9t2nLl9WNpF5BF2HDsO737UV9j+ewdE27eEOCUn328bD1vUhRdnZRyZ+zw3FfMGoVfVPYxHPRgVHoZNr5GHyQjbjX/KFL7uxlHH836JNzzNEMpZ42gnvgetHXEEaLZQftnhmFcaAmVjXaux5it44Y3TEiBhpme54I6NkD9NeW+YgxEEdR4yZv1oLH69LSFDMeX7OJZnV7Uu4WZ4YGOEipM6fFNpHkMZupWJcT6CRRpddXYuDgIkE4IoJSpFizyRphUVx7t9nuwkt90896VNK9hwf53Jedjzv7vlcSuerPab94T43HKL+0AVWZW++YBSLqTD4CnaDV4PT9aavOHJQmXSxKTlwOk9sX0/bl+crCZ50B5DRAQ2AaBPawKlx1P/nqOgdPHnCoZt1GdxkL7S/GAgkpdFcZ8j6hZqP2HIoaabfNufT3s/IkfCHtTQp0cnyfVrfu4WJRiEPRcxd/DvLTTopPBE6QwETC1MTBen0UBV2QNIK+Y2bBjjzFZBXuuqj7myGbc8PKBaQAFKygIi+/x+TDuV/wx69wV/DnxQG1P8/7CEG5giegUpTLbRNdf7j77qbe/+Yreh+b38wnaC0rJMomT1XhjgE2JNjNBhqY3l0xvXn3ms6dGuBTUSxphwVifL77RK956aL6VfAQ6y69hRLhWUgkJ6A7BjS9y1D6Oe7cDiX85Gh+g4l0zS9ESpsQRfs53G9Vuc39R/armb8j/QGhrU/xTFtZn/lECB+triWYv1sxEmuvQI+HwmuZ5F4Tchrypjjw04ofssIGkaQ5w8bP+uVBTpM7NRGkwwcYxJvWQR7iZftrpGbX9mmplHU3OvirOT1u/4k6SLL/vwK0rV3YYx+dsi83Gp18TUSl14dcq1n+YTF7ZC6bpYfp55qdYsSnCL/x63J/1gspSPGfJ66RLZj/OOLgTVBMsiRdyN35TDgVRykBhRK3+67/topnreBGNEfOgoAa16p9fX7sVBBmjKBkd5wkHAdaDeG/8uRned7qnnPQ0JJ82+6y/j9t81ZLhKkDSPOhhfbqoxeikNT6CPtl7XOQRRpHyuxJwmfJixiOFZJnpGeETE1MYTi9jC+hS3R04nX7BE8QPV//QOSqFxyrHdzebzSKbYx+ls39F9QQiLn8gamPZn60Q/+yvcdPbiNh6uanwxsw2/KcpuUHfxhLivJuMqrMNUY6hgXwjO6NP0LCl/vl+EV1uOhYtp5/nKKdVwcachW+AY9NbuT4kwZj7MN2fQi9z/EUrwUiNSCznMbyjiRqRyUn+taHQGKhlgFzMvuGeWl8CpbAvnd0n8qOmoDrzzSp28hg5Stee+mdkwnZ00Z9HEh7+GvjQUlDSiBfWmOfGn43r0Kx17ykZs/ZNtQNWcNH0AEg1PI1d5bQN02LvIaxDXeFxyPU1IaCBvshrTgnVI6RUTLpvZFQwQXaSKjZYwmqdL2dSLnudofFdfesHFRjndNKLBH9OS1YKImA4USTB1r27T7mtKfyR7EG8zN9AKbq66fh4Uht7YRgTQPWQFCPawgIalIa7ZBQzOBOKJ4rgZB4KFySBNbSyNQHetA9USV0DRnO8p0Bdr2kr3cceLE634iRa9BaOsogBg6TY8s3HqtYcnxc7biVxfgPacG5UDG5G+MK+HRqlyiZdIGvPgqKijbIOSqwmf+UGXsPJn7/j4sOy1fz+9s865HeaiA+EggQQrZsNvckvPcyCCESKw+8ceMWeswdt6pUXV2B/z7V5wUcxQiSJrmacB3LnVcp312TKCW4qutNWDOKldT/D3jMMhaF/kRQ8vHPXz9ieeKpzTYbFaZLHgUzY/ICjcV1cIMFN28OtMPdsSft+hTn27P2lRnjKGUMzQEzBBmSzeSkQnPlI3f7mUd6hZ9OV8B63Fwrof55L24LMf9hEeLeZNcDQOQT/tIiD/OD+0N29cQNAYJjTvyPB/F0Ln34f271HVc4X2wC4jm8unE/kkujsNZi+kRcsp0l9ggrpDLFeICPQ8f0geD0mS/+jl7PndYiUISUxx3IakxtCth+xmVBzz1ZvEQIcqv1QL+DkE7yZ4qM7QBtUj7WbeN+5Rprv0TBTdfuS0ACPKF3E8vUCSv0nLrY8uKWjAWVto3FjPBWOZEfD//jcv7TkwTH6+NCFaiHejHjjjin6e0otnG7uFIqIT2eQsksLU2NmOaNieAuuvbO/hmsINPH9wv2o9Nb9cPAbW+a2WyJV9Cod8d7L6JeX9ZrYPDKwVqbKHaNznA7ngP2yjHy3H7fe1+9TNtjF6+Njvjhrj0oiNX97SmB7YPYETGYddYdrsDimVIAaBK/CTDX0SoPRYk+HwfgVOLaukq0GpIUWHbGpoJQR6Zz+aL0DSKpgYqkuB0VDP77AwBFfjHHp0pHeb3OFRMjXdNTWC/f/YI83yNkDRNW4euYt5SdOzQBJmHN/nvmBfgc4VtzLbC5P39eW/mIBoZCP6yoAYAawevjjb54tbtWrfJqbwd/bO8o+a24eqQrsZ3gWOBvz/BIsMNOn3V8xQiqOn0mw7nBQNwC70towvUIwtlr9+TEbhh8cBDth1J/9QTGA88cEhxdvfpghRJbVaAKqJ7rIDR9QCqj2gscUsjG2oHbKPH05iGo4Z9oT80f4UsNA/b7M8dM/4MTqpkMrf9MsDnW2feN4SQ9A7Fs3hM4QrIusU/eiq/17/rjl08XckKkuXJ2b5mO0mtATZuvY9yNfxXZhYF+KlTl6lKr7il05U/3o+rzKf95j4z0YgGAZwuglKdt2mtPZALYS9fdg9fjNyaUcq8c+1xMH2bRYK8Ms85Ei8eL5ondZPh6ISK4gmOGvMNjP/PnXIiw0LmCBAU4MIahujWiyIXMVzVe8H/s4J9owGex5MEBYXuSzw4AWVL9kmfHRr/7ZHrmUc5gU7LoJv7iE55sRhPF78oY+iOk376HiMAxxle6OrIvOLgAv3zBH4y3+3O8vXLtPn1TfZQI5Xqyqi/b5dmEodJKZCdPNOUUwrqseDCCHM2QHXhxpSW5Ds2WFPy7p2IYIdcnkvhFUviKVtlff0l6mm/B4lBVN1z9RLy4vN12jAyXoSIj7cf5CvQe2f+ltx2fb3miTa9DCyHTP4Pe3CNvF+ZC9lc1YFLXHzX/U0swpl4HfNB/xEIRPf+0zOqBr2B2pSF57BJ3HGkJoeWIMf11BN541Z/jjCxD3dGtIIWI9pBcB8zyMvkDsbl5sGetgfuICCmyKY6WJl51f6NswGG2rZr+G0uDplFWyiGNu4BCk6+wk6VOw7C3RrSkcpdCHyOBWIJYehdGC+4P8ZxUaXscsSPKxgL+a01fxvZNi55J6418UHxr3R03Dh3RiwQ4G6VbTPZDD1Jf5sFZGrDDsv0IkmIzE822siXnk9kYim71U/zbJhJSLiZrWxsfgTr+Op/JPL33pFxiwfC92caoeP9kokTnLG0wh6o8Jpu5JcoT0MQPkLUYLi4+EInoM2yyGBKmJVRXCgSQVbn7zW7s/Sc1+mtRJHWMjn/xxNIm3oBe1b8KLkYBQUdZkBcPtt/Ez2jNDfHyK+uW/ja9EqwCaT23zk5ZLSrR/PZZ3OmjsX2WowXHfdic0GK4WSDxeIZ0WEuvKoJpSzCqnpHOXaalYYNhsBFa1YycaJtkGOlRVSQCPxD05qeCKnyAcbxtTRw1C7cGdCjYOYCawCh+g04WvyQYE1jfZNM/vOR+r+4R0jRYYJfD5+f01R26IAJ/oqTXCdGbmWZuQPtz6PQsrdT/m977VNbEJuuuft/y8O0TZX0gVbrhmBhivvpilLfrZUOTAX9LgfgDfq6wStXrKbRPeW0GRXjQbB/bG6nNXmGRtX0ZvinZF6jbDpD3M1S25opfOvX82W81P1KzoJAQ/szaaHGWrpUeZQU63vb5MdZHhaNmorFM2mA6PfbZbRneOIZq47QfYccz1cel+nkvn9ZUtx0j3TOl0ccu4O4OcN3JYzQVqqqgs1QFzjKF6/QR3klXNNFtC9eLmyu8jLAwlXPESo5rZ/xIvEnxKHCceqzJXX+K5YYHiRiKSr4gLVLFAH8Q7uLsH6510czXBbS5En5mVzg8AM6Qv8rKtGaM+KizIDNpaJMUHAfiphrKu+5JP+Wvqb0QchbMs4BTUK+Jf0nWZn+9h1fOyGcvCR1l61JDMzrEmT6w7Dl3Ok4VoF80VXxDznvYkQ9EHy7k8aSNRL0d0o3nGflZSYpiQylZdmzZJ7dVzLoUcB95OjfFZGfkooZKPB3vlnAoOyzMs8ZXvS9uO+6/0KXnYyIoXgDppLZ2Er4A2vc9fiitK8gfmQwqgJcb9c1LNLXSAWnGizFrWowviipdRUrLIHVrZ6c+q7KAY/LRSztgctTQxeY2NBsENIN+8KIeaEQeibDgxn9rlf9YIrNp/+vfaAKWWCP/jqArJ6I8R2Y3/2Zw6uxskPorJhhwyDmGIYdgJt12L9STYbu6hDc9u53OyihB+vwWAEuzUTJk+oyBZs1hw646Vphhxy8aQrvjP0dPUxXyGz1mBbVI4B2ux6Bzc+2WEIpgxCkwGbUAkDDddyXAAVJ0OWkzAygKJDTDjhwd2Pu3r5Ozjl0xWlSNsx+sb9yXtDXCWnvrreDIoHVTcgQgUUeMPhOwKDrEnN/CS0TgXJi6gDrpHNF3kfe2NsiEljZAmFF7kjR2IWiui688SUHMEmCDhWIiA9RGgjfXUDjO3xVnu8eJfFdipvmZLZzT4jkWwqX1jg0py3rExOukgTaFWxqMvToOjp5PG6ENgGKNoJLWAWAnm6FAO+ac9GONFE0qUk0ZBYhjSeHMFZKFx3TveTwptdi+qJsJxoEUfRbQ+BGtc21EXI8fl9IO2F7d+d9ks1H5+V/UPThEhzGAc6EaC0Q9pPTTk6q/mRgdMrfeJzy7wQaG0hN7Cey5+8Qtpp1mKLOZEEu0bcNcIWX/2wGVZqWAoJl+AqM5jrmVpSKotSSzSze5qWcJ/OwzcN+cRi4/I/YtfqHKOa4SS3EuDFw5Dn0Z4NL0yE31RDXv7vihIR5PdzIebdLiauvvyOpORxwubnfw5AB69bImT326GyF78R0TK2eW7bpMlS7M+rJPJONlH23XfNxQaw41FaSCKgOqrCdhKFYwYlkxqY2u6vMkub8bgOK54kq2HkgZaRszf4sYFx+VRL3WS8wHDcZJcRKiPhmhF0gd8Ggfc7uS7vKsP8izXA9PjBDxdQrDvCT9BLf5orPZh+/8RSexIaNjmxc6fJG8Jd2rEGMuuxuZRtwbPBxdV/wuygVQTBym8CUrFCX04BOLEPIM9w0FmF8KCZ/BAnKufQiLTlx/9GbGQVAWDzoe07ZEBL75RDh1m3XQtzEYbwp3gM4i+CrxGj+AA8CyTx2ZXY4BIf+Owu1V3h3AemeKlBvwI21Q+1xlroWJItsN5DZPEvaG0Ix1d5p944cbP+9pXJ7zAUPunXA9bmuw2fEXBzqjI9FJ6T+9ceGgQ4gSS6h1eLyXjBWlKeQP8jNwCnqb1H4t3tVHEFUmRKaBKGMm4Ef+vRskzoK++jU6lASR093RcUtFyBwl6agAH47PVguRChYefriFLshDpQcOsPVXb0ftlAoHjJ6lSVZNvAqL6cfCXqWlIkjlbCS/404Ke1S+WDS8v2oL0nHWwmIu5FATkEeV/cx9Wn58wc2LpxyJKYkzyX//dDzWvQqbVZ5g027bt5wsMPwtP0ML+PzvgBpjlE7ICAJpFnt/UaGr8h9Om8UVzdrv+c4Zlu+nveiaZ0c7abe6vMWONxeaCF8BM2DjZyKQorj15cTHf3ixrmiI5DEGBhcQu1O1vuIPvG69R+bjhaH9Y8NbgyP/s7wUuRQTKdSc1gtX+P2PlZQb/aq+/nM8F3OAnphDZTId/hKV2gb3JJXRHrnkSRoslxZ4z6i0qyNuhjJphWcN1FyT/p6b9fLIEv8xIy7WOPeV3t7jttC884lpHcyDpaJt8WJUw8s1eZQKxON8AkROes+1Qb2I/HG73XU+D/9Pely05imTbfk09Zhrz8MiMBIh5kF7aEPOMmMXXX5yIrMqqzOrqvqfrPLSdiDALyUHCcV/svdZ29+0XDSNQqRuuKYpI7/blnVrWc5JYfs8XUwovZ/hhvrnrTLv6BJkc2CGEBUyeFdtE6CBMqfS7M0Oaiy3EsBdzUGdApO4rBqtUM7RX0nU1acf0iYYpMUm95EEI1OzZULj16X0Ffqbcmr0mhHYLF6W2ca94XQvhkoXdLSlBbioFft7QHYLcSCN09aCGYrNbA0TQ0Snynjdsi2qIms09QX0f8KiacF4FZA88Z2kNpVPp58CPHLRDf02dBaXpyiAqSE1AOUmiaKK94Af4uoS4zSgJtdeGWq99ct2sFOyRJMpzFdYU5S8ukaoD6SPPHmhgETgEPbj4BTB9VdcadLUf/oV8bamd8iBaq7xTMAaCG5b4SpLChruhvJV7GzYIpFc8/lSsCBXqRYUrUSaGaoUgm/FCT+Jxe277l8jll+oVqC8wn7pdyVmu8VTeE/o+WVFq+ReRA06PVZVCo8yp0JdS5SIMD41ahaLodhCE+NZUs3eYJM+56W+9E0I4nDMfT0FESs+dW9w/F8mE4caaLZUe5n0gSBGBX3NzKGjtVReoMTFzhF5axVy76xiZVnF7ZRaUW4Kst7rUuBYkqVajARvZXuXYqHK7oYIkCR8gaJyhz7iDeHbFIGMcnRbnrgRrtM7LszAV6ubnZ8QoSTPQaCI2WjaXnnu5OOmbJBVIXPKVPqzmYsC7mAOFPCOPGeXUpI0jbkWBlYZeQdcrnF+Hfhy/xcNaZ0i87RSowT2W89sDiEetq0rbqbF+Wu+O3sACktv84fjFqlBo2KkY6xDHldNrrwC+rw2UcHdcebTPYQMpy0REEW8zvKtKj9BVuBHPxgUzuTSRVbqWYLZrHT+LRlHfp8IojFetv5qKATGyAvYfLUVwRtOU9yqhwRO21PXB5q/rI24hSJ+AHbxk1tXUB6gZhYda47F1bXt8qOm2vTsJYaoxn1P17Yaya6LDYHJjopC9RTicTPjcjXeCC9KapWkY2U2u0hfcA/wBtSTqgCkGy9HDb6G+RrUnTVl71Zz9Y6vwlZOiFw8MmR5Kgcxcz22GOsA/MpUBdIBcWeod63gspyJN05ZbWut0kVsUcgNC5VQIz/nHO+ftoyeXaSJu/uANYytUc3BbqvfD2JBmXu5RqEnOLiZLGoO5rDOTqldY1io6X+4DCWjzpX1MLksgNojJ8XTwqA8S9nLfaNrKGn8jYnR9L08DCuPCIjeULrT7YuB1quKtVYGRHZVx5CfNu8AeVYoYW1wbrxSkmu1sK8X7fb+0MibAFzF0NaaU0AWeGXC7s0Hj+xVBGyGmAgwmbkCFO6Elq3mHPiHF2gLfqh9miu3GlekspmA7qQOqihzjowlBjEx8vl5eOBfOwWZ47j1NisNY2BQQIvZUD46ZGc6+A1dbhnQCBlJ0n4mMO6PesVwHXKisE1u4OcNi4m2O0UnH+BNmDRK+J7xlAHt0Z0h4oZ0X+qZ8EKmL/LRWdn5fuZ0AIH87xMMh1NAE7WsuLSayu/qmVAvV9fCGr1ca7kjNliz4OeDsRtIdSdpxU+kZrNPWw7Xkl1wk/l21EcrtCH4vEeZh+8uKlTkBv+pKFupUfBDmaxrUkSh1PWtoaPV1v6bTufZ9goeoh3UmLQdPR9QAwrc0TToLm+ZoeiEpqLyApMpsEfYeNKk1E7irfS5GoIHTp9mHlLZ3A5GGoBwdMOpePpOpCW9+oSVPejxaNRo03XYjn6kFenFrk88xqmGTGWIufgxNF1vjL5V9wy9uCJwJwzykRgNciaIEEJacLqx0gdy4utt2f+eDsfHUM3kKsDw9EoP5YwSeGCSOQ1K53KHnjewb0e8NHKk6D1IMed00EhvymYjDpCTvt7sbTPyWUvvTnPQsoIHxkdD8kVKAp8B+Gz8XXVZCY4x6Qt09QWAngVdlKQbe4h3GLtFTjCt6YCMsVtVslAFkzLnj8tY5vae/zMQB0xoGW01lx4+Z9uk540s8HM7wsnD0cYidF0x5GNe2khu6zG03ICigMRFaI7RaI4PoDulDoMzzmXSsvQm9YVCULpUvkZ/c2p4Rq42TCVU5uD+Z1N2oy9qavTklOc3IvCugWYPWU8061pUP3S9uohK5adqooFzDt/suNdPJOaNA3Od+ezqUb24bHmx33NG8t8Xya6Br+h5OtPoICKJK4oILCu3C0s6tZWZt6e7RZmh9W3trd/PEd0Hzix50JM7c5C08rCy6x2t7WvPl9YpQbtXuZdopgIbznWbGIcpeX6D3ijxHM0gkuhFvCl3FJcba0UUXB/RiMNQwJ10TwfuqDWjSNvUWtc+H2ED3ESdDaXMz4IxokGP7VN4xcb3WMqZdWkNkScQhapbgE922DlKswICpvE0a7wodxbnjOsiy3rjZY5REzw+xwZtzftWD0ZN2gr+n3KFtAkzZ075bSJx4vvPt4/sJsLaI2fUe85fr54K0pZKsh5IL/KFAgkfT+YUPva9i2sRd8iZr7LIzWPy8zS1wwMpewt2Wj1SwdWVBtLCCSAiiwOsbsRK0jOIVH9WifwGgS6pXOIebhGrd5Y5OMONDRk+W/bIe134oNteNDWZFlcNG9atNMTmjv68kthW3xlUfMs3xpkddKZWqpbeRrvRtilt+xVN/e7dW/JbMwEdfbd+8t+J+jvCdWdLiqmTZ3ewHtkDQSBiTwD/c6lGdLVFfh2N1w3fLVlUMoRBrxjINwUhlN9OmiiLe4KuxWyJsT4j/ZnIjhq7ZU8NpHKQTiwIKBAPykqg9HtLUqJsmqj60xI4fQg/dcBjKYUPPuWu/yGVfIwdNTqW1iKhwRO+E9rSJ0PMmZmjsVy1WclZzTGYCILkr23l241fiFsQiW0tzR3AZkEaVnxr8o7bRgl7enEBCXIZD7rWVzT1L1ZDpnTS689PCKtir4A9vke4ZTeRMkK0ua7qMKTCWLoGRBwF70qKyXTjm+FM4U6jKiyAtPSsBIZeZ/3f2//xsmxk5yTpnl57ZqTCSO9MtNeOTRIG/WhU1Hm+rGATG/Q0TZCj0Jm5F8z2/iLR+Zqskbtw1nKSnZ+crNgJncfNJhxv09vBsxvOJnnJ1XUz63g3Vw3Zl/v4MgM7rk9QmKfvh7lHWjSPeV++keCQj8hKz/iAsE4qF1wBDWerlqZEkyQUt1ckpLvqX69lBjHj5U0pLwW6fC2rKW2SoRKxnlYpTNI+azxa7gVn9tzeDH0LyAhKof+rn/dZBKrMdrKsHbHkIU75ALXNp7jQ5+nEBRwehBKNOVO4OXRs847l0Z3/EXmujDlWSpgyUpvvdAkxwX8AoYonXsA9Gbx54S8WHP6J4+/FoOyBykdLnS7QjefcCDbrKmBVxhYE3tCprsCr64DrUR8UeZs3hMdsh4a0KmmbyMdK4DJqMKg/jfXjqFJwXt22BOO3r3G6JdxNDVr0Kp5A4T9DnVFymwVqT1NiMw1Qhg+gljfZuZZIwSraD9SSj6SiRXbuhtfqFPLtl6SuUBUnFQfetETD0yXNcw+xCAfaBkjd7xxNLmMngggfLTrr7Ob7C8+Gkz0x+tBtGxfgpAOGJpm5v1pZGSX244zsyMnTHKZJL7xRVNt0DNscyrpRkx+5nIuQg7h/QuanXbW/ewmzkJEnPO0uBfbjYYq/2hdIcZ6dJM0dySbyPPqZQqLqZlrsNsGFFm+YDQ6o4Tg7Un7GR2RJUPjnNab6fKaWymIbDDsrpFWNKk3derSPQE410yMuzl9ZXVv/DJsflSMNGSJQHrCh+IMnd2JcSAQ/FlGM6WEXmeTDGSkYZx/Hm6iSw1MYkq7UzvaSxOtSywXsSi01wM638OJ7L/oGkICZ7ABEwR6ktIT2jhLZeQazUo4fMjgITxJTo6oquXoCSWBQOYNBO7QaQY714DF2s7KBnRh+sFxNpub3uFMlDNWAgGxFnV0UcES5OY50p+GFGbqQ3o2LbgItP1q4i8rzr1DrLVDqpxAo2JhKJkDuorlgQrhqK99fVIlLqnEr9PqglnYtp2qerJgU0JR/QWqzRU64bLQRtO/vhljZUPan3IHsbzu1uUaWStpW10HKZ7/wwoSSk5pb8fMDYbOPSjVt0Ij1zJ+tgJJAxsUT3dcQsOZbhhNIEs/S0jGQK13F7LqiPw+SVPbrpGm0djJf9O2p1q14Tt8hGYhkv9yhOaZ5Q4QP3K1ItRoeGcTLnoH1MKGKlShkbkj6clVWBBSr+Uedq0u3Ym3WbOieXCVfOza92BAfDi+OiOVDvdGNvSb6+QzeqjHRERre0sy6JUuFqC9XQ1MSrwtQWdbXETM7ZyyD6AZHCYhgjFAi0N8Cj8slVTqcYhqYCh2N+0WQ+s8NYL03Tt9rHW1vUPTa9vOQM0PuYugFqj694hL60dpgrtOOZc6cr62oXL35u/XMfH8NCCR5EoO7ISimRJ/FitNus1vBvWd3tLuAf7JmKnGHPPcwv/WW5ZaBOJKrF8jCQcbnvWLGMhFDm2xIMlBbJZCUNtwaxmvoFJYhd5dAd/rgNMtXUHSw8B15ifcvQjsRCAAaJGF8kTckJPo3HdN3fWntdQ3vZCLlmfXRdrwgQMg5+sJbByiY9pleURQFqCYECKY9ZU+1rYCY52TRIMD4i4fhb3Hd7gGQ038eAXRH4vXsNGsVgtyp5D4y4QB6O4Q/w9MRvcpJdD1JhBkaSQWFXDd6rKwwwhMtZ5Ll6Jp0tsuKdycftGyUvXXEG52oj9dJ0Lj320EKVAZadUJ3KE3K6cvE54TXzyWXijZXgUvDEboGzkyt4DFdsohLOWOi9ZhCOQ4lzfA2FaXER/ICWSaXfg+7lwo+HH3oZzsvtaCdRznvTc7uUh3ZrD7OYewyEjhCctvi2UcXFFtgzkqdhHRvWekVLpRwbpgVWnbBocAsnMCocrq+ktxQIrcXpkc4cSlJ4UGpNKsTwawuzcC/9MHdn5yKhaIWCZZZi6iE2DcMbaphRTR28LEEO/T2lzJahQ7GvBUFzm7D14OnnwEBCu0OODu6ZLO83p3s/bnrI+0NtBINOVNB66D9K3oczatjrriIMxQsmgvucCk/y1pAy6Q2vpG3FOULXydmeBMsD2XcXC7d/8GBeBBgglcC4Du3eBrghn2C/HrZEyBiRCw8xNjouLUSFSXHHUoPw7mZEe6hOFAUpojSCnb6Qp++xqNJ40KX583GO06oqdS3U1cv9pb4YWyRLC+Sxa0K8eAb1EHqjwqBHdU9NG9mDzwxKF/DgYbKTEkdtgRN/P+dFji47D/Y/oq1mcfS2FOIPmD8n3HXf01UJ6NtFNGUZgjrFK3fimRk9HKKq2UGRH+QLwUXE3N5PU7Bk65mGTn9cZ3fbsHNEeKBoddB8yO31SCJQSoEBwWZAhCAj5B0X4/tl5EqpEAnRH/JNMkAMAUSxeLjh6EPI36eSHWlumBCSYW+JjYPZWOBLzsVx7C0iyS2pZ5R70VpXclDwKvssqF5MQGExCA8xwl1HOmFryU1PnxN6v6kz6vDlM43KyR+8jp/xzWoPMq8sni4yBISkiQry2YhqKVXGPOdBnCx3afD5KapcI7jNpQLs2aJqnYJ4eu1t+EPRyIeozm8wSZReHX0rrja0XBn1I7tYM8APiurBknzgIbCcJ87JLyakKl61AkTm4Pm8ri9ZPuieFqBs2PhE6b0oIKWCNnc9LCnzFQ9kRsa9G6EstpQ0Tl4cXvieLVV19yi9Ui/lhdNo3WI4fuQOF4+rRXiTsTO/4HJtr9f7nIH9Chad30d/sg3zxQqUxw/O5c2iV+gxJVj1EnF8wDy98KwtPMjMUxzad2/Ir8wuHyzGmeblEmFh/KQEaDApcIt4hDxybCg+Zo6Rul+aL14iYnXkQKeeW8W1HW+RiQrCWtc9DVWU1q+rk6ZqZDgXwSPg5qHH+7mrJhFC43STVNZ9AKusPYhdUQB0vXu0BI0MmK5FtjKKw4EzVPcgCFDMxQtNpWkcbm3vqQwVLCOR7zqXLceONpGU+Q3N8+pYr/BUoCDNL93lieRyHl3uSmERt/fEYgNBstNDRXtPxu7PHE/kZURRMr2QZq3YALzFDIl6VcPYNjVG6el7nEBM4kcFWNucmAIrJZl4yWWmYAUMukzz+szhJEmwx/PWQJwVA0uQQxLaduRCUW+DPIyxI12CzstU/fDuQrHAZUW7ykML440AWv94rik2eSu5T2nGrk2qSq5vTQ167DrDJbffetKszMQw4BoG/sqsgoimsnT0nyDTF7jXfmwrLo/GJPd0ptK5LvYKKC2RMJ23NtpF4jpX9CFaQnETkHtxJeKDn40DKgpdcJP0TNgWqx0N68XTjb/4mBYAVK03oljTGN4WnLrH+eipkJJnIou5vOo/z7nhMeDyDfqYM4M38AZMeD287oLyQQo8Y453Ak3FtgOILqf2B8WmLo2trfejIZtY86DH8aTg6bLeCSVwppv4NBgs1nPTxyB7yWpx5Vhchx1DX1+awY8gvhu0UEQni7fd6CRhEzkFY7U5V3XXq3W3qquYqMaD4J72w2pL6E2hVyaAwyxWZ7wgVFATrR23w9wUKLse9DetG4LNaVrvAG4UYXH99QlvA4u21ROPKo90n0Xre9f2kh36VCOFoEOY2wRTaOz7ogMRUxwDY0xf0cDjLrmee5ejF84RXVDsyF36GA/nDvPke1zW+MwpgjmqTia73EX9xgm1yksSmKcm6ctaNeUMGTTNGD0StPjTfZxxMJgQXzqXReyhhOopmZx6NRnadcCiEPPcpbiJBflQaRzXZr780BTWFMJbM4umQDN5krtV1hncPQrdeFXl4jChmaXlF2Z6OJ1t1oeszjomuzBFpJnl5dy9tHXExA8Eo8NS65whbSLGTVRDFBPY/YU4hatjDo5YHg1dbJZVyYGwidVjmQ9VrzB3TjtMJG+wwlkIym3ug3XAKH2N7wrTKUDqX0RGA5ldREagBdYU2fNk5aKCGf1Q7KX3F3TU7fjrroxmcWYnRholWK5w3AfLiOxF7UyQUStIoRrOus97yVwGZBEbXcaBTJFxRXCy0GUjuJYFe3Qx2EzGHX9ZxWygXt66r5ZomgIISrhCNn+swDlzNQY+lynceTpr8qBmm8kaFvMtCmQXHLjaJHsBoky/xjB44agZG/xfK/z3tkKWUgZ0f6ucd68yw2TAj+16uqXg3P1y+QXlf0HZrg+jYnofb3DoeFs0YZaw3RAnAziOHPoC/P4Cgk+HL4X6Q0y303kIwbuES5J/LESc3MKneFCWpv5CfJy3JMOUbB/nwaAIFX5BuWaTkq5JpuGw0dC3o2AKDPjI++M9BX1FCPq3H+rj6FrEU/5xBoojH2V5UmT5Z2UQ+vNrwvGjIPv1SueuZOf1gWfYuKSuv1XnfI1ARfxP7gj+vKOwnpOP035BiPq4LJt2R1MgUBpGHweI19yBcq6bh+JoQAS6gSjbt+LjVfb5//z481vB1wLEtqC0qJNvxwA1/uP5QHucV/xWDKo1Tu/6s1pHg4PyfGqO++Ph4+U4DV2VcF3dffYmEVHJMwVVL+r6u/I4TKjDPR5EsC6y9iiLjl4GEGBBRxZRWDOfB5oijsEF2TUvpsTuP+59HcL+KBu6uY2T+PPqdfgE8+eiKjvLv12u7Vrw+TEP4279PBXcl13s4KsQ7HifDWFcHFX4w2e+FfPFkERT0YEKJeH4W2v8ALt/FbF/Dk/oB/zBBPQj/rD/APwWhtOx20v7RyFJ+esf10YU0J/A72iRP3b90Zg9eNlsRxP1+demi6q5/xodzRoWbTKMX5twyIrWOpoN9NPxTzsLnK4HXQD9DCwx8SRw4tcj/mcLgP6KwzE/+xn6odfDIfrsSfzjSvMwFktiJeNHKfTZ3WLYFDV45rmuKQD87bAdj3+a/Vfg+AN20zRFIoDdPhmKo82TAaCyaLPvLvbd6R8/PzVo8J+A6E/BgeHwVxr9nfn6gqA/IuZX4/Q9YlDi74IM8gNkxqlBkb7o/hQ4wwcwfsAAhTxRgviJwcATKsb+/7GBQD/p2PHXfkN+e/eB0C/Iz4xTOE/dJ6zg3z6hJiloYuyEHzj8rXbQryXyt36AvsfNb4bmDwD93px/VvuzCeE/gvWP9nIC1f83MPhPbcC/7lSJ3zvVL/iPVgz737Ri+E+c6J9A8aQgv/djP2/Ub6Uq8DPGAYRPj/Dspqlr/twBfdiM4+cb32HG/oT/iYbw25u02ABw2c/68Pk09UezMB8SKopb7GtxGNi0OAA+HKYWBFzi8EwoCcrBumB3KmoAT0SEETAJ3UmGpmjD+msPUP6jlfvnkPvuSfm7cPOtW96/h8P3sEGoH2Hza+F/HDfEfyFuoL/EzdiEdf2l/ZLWIQhQImCkgiApGCK+rMfHuvVLHb67efpyNPxPsfR7Yy2KIsJx/y7Ghm4KP1sGxv9OW4X93lb9lHL976IO/tGD/gC772n2923/O/f3r9PpPzK4cBi6dUS+lnPTX1oGvANf/v7WGzFoPZQ6nepxTE5CcE0cHIpOQvVXCuBHx/1nmuAv2do/QQ70J6x9TU7W/kee//0j9h/gZyj+Myj9xO/B6N9G37H/Wij9yg3/Gkrf2OL/BEpf6D8D06cETEEKxm8VOF57n6//ZpCRKPWV/r3f/AJTxFea/hF7KPIVQn8CPxr7itD/NgLBfJsOhBZ+PSaBDte6OAFn/D8=
\ No newline at end of file
diff --git a/screenshots/logo.png b/screenshots/logo.png
index 5bd4dde..facb389 100644
Binary files a/screenshots/logo.png and b/screenshots/logo.png differ
diff --git a/stm32pio/app.py b/stm32pio/app.py
index d804308..325a546 100755
--- a/stm32pio/app.py
+++ b/stm32pio/app.py
@@ -1,19 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-__version__ = '1.21'
+__version__ = '1.30'
import argparse
+import inspect
import logging
import pathlib
import sys
-import traceback
-from typing import Optional
+from typing import Optional, List
+try:
+ import stm32pio.settings
+ import stm32pio.lib
+ import stm32pio.util
+except ModuleNotFoundError:
+ sys.path.append(str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python app.py'
+ import stm32pio.settings
+ import stm32pio.lib
+ import stm32pio.util
-def parse_args(args: list) -> Optional[argparse.Namespace]:
+
+def parse_args(args: List[str]) -> Optional[argparse.Namespace]:
"""
- Dedicated function to parse the arguments given via the CLI
+ Dedicated function to parse the arguments given via CLI.
Args:
args: list of strings CLI arguments
@@ -22,56 +32,95 @@ def parse_args(args: list) -> Optional[argparse.Namespace]:
argparse.Namespace or None if no arguments were given
"""
- parser = argparse.ArgumentParser(description="Automation of creating and updating STM32CubeMX-PlatformIO projects. "
- "Requirements: Python 3.6+, STM32CubeMX, Java, PlatformIO CLI. Visit "
- "https://github.com/ussserrr/stm32pio for more information. Use "
- "'help' command to take a glimpse on the available functionality")
- # Global arguments (there is also an automatically added '-h, --help' option)
- parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}")
- parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count')
-
- subparsers = parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands",
- help="modes of operation")
+ root_parser = argparse.ArgumentParser(description=inspect.cleandoc('''
+ Automation of creating and updating STM32CubeMX-PlatformIO projects. Requirements: Python 3.6+, STM32CubeMX,
+ Java, PlatformIO CLI. Visit https://github.com/ussserrr/stm32pio for more information. Use 'help' command to
+ take a glimpse on the available functionality'''))
- parser_init = subparsers.add_parser('init', help="create config .ini file so you can tweak parameters before "
- "proceeding")
- parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project, glue them")
+ # Global arguments (there is also an automatically added '-h, --help' option)
+ root_parser.add_argument('--version', action='version', version=f"stm32pio v{__version__}")
+ root_parser.add_argument('-v', '--verbose', help="enable verbose output (default: INFO)", action='count', default=0)
+
+ subparsers = root_parser.add_subparsers(dest='subcommand', title='subcommands', description="valid subcommands",
+ help="available actions")
+
+ parser_init = subparsers.add_parser('init',
+ help="create config .ini file to check and tweak parameters before proceeding")
+ parser_new = subparsers.add_parser('new',
+ help="generate CubeMX code, create PlatformIO project, glue them together")
+ parser_gui = subparsers.add_parser('gui', help="start the graphical version of the application. All arguments will "
+ "be passed forward, see its --help for more information")
parser_generate = subparsers.add_parser('generate', help="generate CubeMX code only")
parser_status = subparsers.add_parser('status', help="get the description of the current project state")
- parser_clean = subparsers.add_parser('clean', help="clean-up the project (delete ALL content of 'path' "
- "except the .ioc file)")
+ parser_clean = subparsers.add_parser('clean',
+ help="clean-up the project (delete ALL content of 'path' except an .ioc file)")
# Common subparsers options
- for p in [parser_init, parser_new, parser_generate, parser_status, parser_clean]:
- p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(),
- help="path to the project (current directory, if not given)")
- for p in [parser_init, parser_new]:
- p.add_argument('-b', '--board', dest='board', default='', help="PlatformIO name of the board")
- for p in [parser_init, parser_new, parser_generate]:
- p.add_argument('--start-editor', dest='editor',
- help="use specified editor to open the PlatformIO project (e.g. subl, code, atom, etc.)")
- for p in [parser_new, parser_generate]:
- p.add_argument('--with-build', action='store_true', help="build the project after generation")
+ for parser in [parser_init, parser_new, parser_gui, parser_generate, parser_status, parser_clean]:
+ parser.add_argument('-d', '--directory', dest='path', default=pathlib.Path.cwd(),
+ help="path to the project (current directory, if not given)")
+ for parser in [parser_init, parser_new, parser_gui]:
+ parser.add_argument('-b', '--board', dest='board', default='', help="PlatformIO name of the board")
+ for parser in [parser_init, parser_new, parser_generate]:
+ parser.add_argument('--start-editor', dest='editor',
+ help="use specified editor to open the PlatformIO project (e.g. subl, code, atom, etc.)")
+ for parser in [parser_new, parser_generate]:
+ parser.add_argument('--with-build', action='store_true', help="build the project after generation")
parser_clean.add_argument('-q', '--quiet', action='store_true',
help="suppress the caution about the content removal (be sure of what you are doing!)")
if len(args) == 0:
- parser.print_help()
+ root_parser.print_help()
return None
- return parser.parse_args(args)
+ return root_parser.parse_args(args)
-def main(sys_argv: Optional[list] = None) -> int:
+def setup_logging(args_verbose_counter: int = 0, dummy: bool = False) -> logging.Logger:
"""
- Can be used as a high-level wrapper to do complete tasks
+ Configure some root logger. The corresponding adapters for every project will be dependent on this.
+
+ Args:
+ args_verbose_counter: verbosity level (currently only 2 levels are supported: NORMAL, VERBOSE)
+ dummy: create a NullHandler logger if true
+
+ Returns:
+ logging.Logger instance
+ """
+ if dummy:
+ logger = logging.getLogger(__name__)
+ logger.addHandler(logging.NullHandler())
+ else:
+ logger = logging.getLogger('stm32pio')
+ logger.setLevel(logging.DEBUG if args_verbose_counter else logging.INFO)
+ handler = logging.StreamHandler()
+ formatter = stm32pio.util.DispatchingFormatter(
+ verbosity=stm32pio.util.Verbosity.VERBOSE if args_verbose_counter else stm32pio.util.Verbosity.NORMAL,
+ general={
+ stm32pio.util.Verbosity.NORMAL: logging.Formatter("%(levelname)-8s %(message)s"),
+ stm32pio.util.Verbosity.VERBOSE: logging.Formatter(
+ f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s")
+ })
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.debug("debug logging enabled")
+ return logger
+
+
+def main(sys_argv: List[str] = None, should_setup_logging: bool = True) -> int:
+ """
+ Can be used as a high-level wrapper to do complete tasks.
Example:
ret_code = stm32pio.app.main(sys_argv=['new', '-d', '~/path/to/project', '-b', 'nucleo_f031k6', '--with-build'])
Args:
sys_argv: list of strings CLI arguments
+ should_setup_logging: if this is true, the preferable default logging schema would be applied, otherwise it is a
+ caller responsibility to provide (or do not) some logging configuration. The latter can be useful when the
+ outer code makes sequential calls to this API so it is unwanted to append the logging handlers every time
+ (e.g. when unit-testing)
Returns:
0 on success, -1 otherwise
@@ -80,27 +129,14 @@ def main(sys_argv: Optional[list] = None) -> int:
if sys_argv is None:
sys_argv = sys.argv[1:]
- # Import modules after sys.path modification
- import stm32pio.settings
- import stm32pio.lib
- import stm32pio.util
-
args = parse_args(sys_argv)
- logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance
- handler = logging.StreamHandler()
- logger.addHandler(handler)
- # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v))
- if args is not None and args.subcommand is not None and args.verbose:
- logger.setLevel(logging.DEBUG)
- handler.setFormatter(stm32pio.util.DispatchingFormatter(
- f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s",
- special=stm32pio.util.special_formatters))
- logger.debug("debug logging enabled")
+ if args is not None and args.subcommand == 'gui':
+ import stm32pio_gui.app
+ gui_args = [arg for arg in sys_argv if arg != 'gui']
+ return stm32pio_gui.app.main(sys_argv=gui_args)
elif args is not None and args.subcommand is not None:
- logger.setLevel(logging.INFO)
- handler.setFormatter(stm32pio.util.DispatchingFormatter("%(levelname)-8s %(message)s",
- special=stm32pio.util.special_formatters))
+ logger = setup_logging(args_verbose_counter=args.verbose, dummy=not should_setup_logging)
else:
print("\nNo arguments were given, exiting...")
return 0
@@ -108,20 +144,23 @@ def main(sys_argv: Optional[list] = None) -> int:
# Main routine
try:
if args.subcommand == 'init':
- project = stm32pio.lib.Stm32pio(args.project_path, parameters={'project': {'board': args.board}},
+ project = stm32pio.lib.Stm32pio(args.path, parameters={'project': {'board': args.board}},
instance_options={'save_on_destruction': True})
if not args.board:
- logger.warning("STM32 PlatformIO board is not specified, it will be needed on PlatformIO project "
- "creation")
+ logger.warning("PlatformIO board identifier is not specified, it will be needed on PlatformIO project "
+ "creation. Type 'pio boards' or go to https://platformio.org to find an appropriate "
+ "identifier")
logger.info("project has been initialized. You can now edit stm32pio.ini config file")
if args.editor:
project.start_editor(args.editor)
elif args.subcommand == 'new':
- project = stm32pio.lib.Stm32pio(args.project_path, parameters={'project': {'board': args.board}},
+ project = stm32pio.lib.Stm32pio(args.path, parameters={'project': {'board': args.board}},
instance_options={'save_on_destruction': True})
if project.config.get('project', 'board') == '':
- raise Exception("STM32 PlatformIO board is not specified, it is needed for PlatformIO project creation")
+ raise Exception("PlatformIO board identifier is not specified, it is needed for PlatformIO project "
+ "creation. Type 'pio boards' or go to https://platformio.org to find an appropriate "
+ "identifier")
project.generate_code()
project.pio_init()
project.patch()
@@ -131,7 +170,7 @@ def main(sys_argv: Optional[list] = None) -> int:
project.start_editor(args.editor)
elif args.subcommand == 'generate':
- project = stm32pio.lib.Stm32pio(args.project_path)
+ project = stm32pio.lib.Stm32pio(args.path)
project.generate_code()
if args.with_build:
project.build()
@@ -139,11 +178,11 @@ def main(sys_argv: Optional[list] = None) -> int:
project.start_editor(args.editor)
elif args.subcommand == 'status':
- project = stm32pio.lib.Stm32pio(args.project_path)
+ project = stm32pio.lib.Stm32pio(args.path)
print(project.state)
elif args.subcommand == 'clean':
- project = stm32pio.lib.Stm32pio(args.project_path)
+ project = stm32pio.lib.Stm32pio(args.path)
if args.quiet:
project.clean()
else:
@@ -159,14 +198,11 @@ def main(sys_argv: Optional[list] = None) -> int:
# Library is designed to throw the exception in bad cases so we catch here globally
except Exception:
- # Print format is: "ExceptionName: message"
- logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1],
- exc_info=logger.isEnabledFor(logging.DEBUG))
+ stm32pio.util.log_current_exception(logger)
return -1
return 0
if __name__ == '__main__':
- sys.path.append(str(pathlib.Path(sys.path[0]).parent)) # hack to be able to run the app as 'python app.py'
sys.exit(main())
diff --git a/stm32pio/lib.py b/stm32pio/lib.py
index 8ca62ae..45ca8b5 100644
--- a/stm32pio/lib.py
+++ b/stm32pio/lib.py
@@ -15,11 +15,22 @@
import subprocess
import tempfile
import weakref
+from typing import Mapping, Any, Union
import stm32pio.settings
import stm32pio.util
+_stages_string_representations = {
+ 'UNDEFINED': 'The project is messed up',
+ 'EMPTY': '.ioc file is present',
+ 'INITIALIZED': 'stm32pio initialized',
+ 'GENERATED': 'CubeMX code generated',
+ 'PIO_INITIALIZED': 'PlatformIO project initialized',
+ 'PATCHED': 'PlatformIO project patched',
+ 'BUILT': 'PlatformIO project built'
+}
+
@enum.unique
class ProjectStage(enum.IntEnum):
"""
@@ -47,16 +58,7 @@ class ProjectStage(enum.IntEnum):
BUILT = enum.auto()
def __str__(self):
- string_representations = {
- 'UNDEFINED': 'The project is messed up',
- 'EMPTY': '.ioc file is present',
- 'INITIALIZED': 'stm32pio initialized',
- 'GENERATED': 'CubeMX code generated',
- 'PIO_INITIALIZED': 'PlatformIO project initialized',
- 'PATCHED': 'PlatformIO project patched',
- 'BUILT': 'PlatformIO project built'
- }
- return string_representations[self.name]
+ return _stages_string_representations[self.name]
class ProjectState(collections.OrderedDict):
@@ -91,32 +93,30 @@ def __str__(self):
@property
def current_stage(self) -> ProjectStage:
last_consistent_stage = ProjectStage.UNDEFINED
- zero_found = False
+ not_fulfilled_stage_found = False
# Search for a consecutive sequence of True's and find the last of them. For example, if the array is
# [1,1,1,0,0,0,0]
# ^
# we should consider 2 as the last index
- for name, value in self.items():
- if value:
- if zero_found:
+ for stage_name, stage_fulfilled in self.items():
+ if stage_fulfilled:
+ if not_fulfilled_stage_found:
# Fall back to the UNDEFINED stage if we have breaks in conditions results array. E.g., for
# [1,1,1,0,1,0,0]
# we should return UNDEFINED as it doesn't look like a correct set of files actually
last_consistent_stage = ProjectStage.UNDEFINED
break
else:
- last_consistent_stage = name
+ last_consistent_stage = stage_name
else:
- zero_found = True
+ not_fulfilled_stage_found = True
return last_consistent_stage
@property
def is_consistent(self) -> bool:
- """
- Whether the state has been went through the stages consequentially or not (the method is currently unused)
- """
+ """Whether the state has been went through the stages consequentially or not"""
return self.current_stage != ProjectStage.UNDEFINED
@@ -127,43 +127,56 @@ class Stm32pio:
Represents a single project, encapsulating file system path to the project (main mandatory identifier) and some
parameters in a configparser .ini file. As stm32pio can be installed via pip and has no global config we also
storing global parameters (such as Java or STM32CubeMX invoking commands) in this config .ini file so the user can
- specify settings on a per-project base. The config can be saved in a non-disturbing way automatically on the
+ specify settings on a per-project basis. The config can be saved in a non-disturbing way automatically on the
instance destruction (e.g. by garbage collecting it) (use save_on_destruction=True flag), otherwise a user should
explicitly save the config if he wants to (using config.save() method).
The typical life cycle consists of project creation, passing mandatory 'dirty_path' argument. If also 'parameters'
- dictionary is specified also these settings are processed (white-list approach is used so we set only those
- parameters that are listed in the constructor code) (currently only 'board' parameter is included). Then it is
- possible to perform API operations. WARNING. Please be careful with the 'clean' method as it deletes all the content
- of the project directory except the main .ioc file.
-
- Args:
- dirty_path (str): path to the project (required)
- parameters (dict): additional parameters to set on initialization stage (format is same as for project' config
- configparser.ConfigParser (see settings.py), values are merging)
- instance_options (dict): some parameters, related more to the instance itself than to the project:
- save_on_destruction (bool=True): register or not the finalizer that saves the config to file
- logger (logging.Logger=None): if an external logger is given, it will be used, otherwise the new one will be created
- (unique for every instance)
+ dictionary is specified these settings are processed (see _load_config method). Then it is possible to perform API
+ operations.
+
+ WARNING. Please be careful with the 'clean' method as it deletes all the content of the project directory except
+ the main .ioc file.
"""
- def __init__(self, dirty_path: str, parameters: dict = None, instance_options: dict = None):
+ INSTANCE_OPTIONS_DEFAULTS = { # TODO: use Python 3.8 TypedDict
+ 'save_on_destruction': False,
+ 'logger': None
+ }
+
+ def __init__(self, dirty_path: Union[str, pathlib.Path], parameters: Mapping[str, Any] = None,
+ instance_options: Mapping[str, Any] = None):
+ """
+ Args:
+ dirty_path: path to the project (required)
+ parameters: additional parameters to set on initialization stage (format is same as for project' config
+ configparser.ConfigParser (see settings.py), values are merging via _load_config method)
+ instance_options: some parameters, related more to the instance itself than to the project:
+ save_on_destruction (bool=True): register or not the finalizer that saves the config to file
+ logger (logging.Logger=None): if an external logger is given, it will be used, otherwise the new one
+ will be created (unique for every instance)
+ """
if parameters is None:
parameters = {}
- if instance_options is None: # TODO: use Python 3.8 TypedDict
- instance_options = {
- 'save_on_destruction': False,
- 'logger': None
- }
-
- # The individual loggers for every single project allow to fine-tune the output when multiple projects are
- # created by the third-party code.
- if 'logger' in instance_options and instance_options['logger'] is not None:
+ if instance_options is None:
+ instance_options = copy.copy(Stm32pio.INSTANCE_OPTIONS_DEFAULTS)
+ else:
+ # Create a shallow copy of the argument, a mutable mapping, as we probably going to add some pairs to it
+ instance_options = dict(instance_options)
+ # Insert missing pairs but do not touch any extra ones if there is any
+ for key, value in copy.copy(Stm32pio.INSTANCE_OPTIONS_DEFAULTS).items():
+ if key not in instance_options:
+ instance_options[key] = value
+
+ # The individual loggers for every single project allows to fine-tune the output when the multiple projects are
+ # created by the third-party code
+ if instance_options['logger'] is not None:
self.logger = instance_options['logger']
else:
- self.logger = logging.getLogger(f"{__name__}.{id(self)}") # use id() as uniqueness guarantee
+ underlying_logger = logging.getLogger('stm32pio.projects')
+ self.logger = stm32pio.util.ProjectLoggerAdapter(underlying_logger, { 'project_id': id(self) })
# The path is a primary entity of the project so we process it first and foremost. Handle 'path/to/proj',
# 'path/to/proj/', '.', '../proj', etc., make the path absolute and check for existence. Also, the .ioc file can
@@ -179,8 +192,9 @@ def __init__(self, dirty_path: str, parameters: dict = None, instance_options: d
self.config = self._load_config(parameters)
self.ioc_file = self._find_ioc_file(explicit_file=ioc_file)
- self.config.set('project', 'ioc_file', self.ioc_file.name)
+ self.config.set('project', 'ioc_file', self.ioc_file.name) # save only the name of file to the config
+ # Notify the caller about the board presence
if 'board' in parameters and parameters['board'] is not None:
try:
boards = stm32pio.util.get_platformio_boards(self.config.get('app', 'platformio_cmd'))
@@ -192,32 +206,29 @@ def __init__(self, dirty_path: str, parameters: dict = None, instance_options: d
self.logger.warning(f"'{parameters['board']}' was not found in PlatformIO. "
"Run 'platformio boards' for possible names")
- if 'save_on_destruction' in instance_options and instance_options['save_on_destruction']:
- # Save the config on an instance destruction
+ # Save the config on an instance destruction
+ if instance_options['save_on_destruction']:
self._finalizer = weakref.finalize(self, self._save_config, self.config, self.path, self.logger)
def __repr__(self):
+ """String representation of the project (use an absolute path for this)"""
return f"Stm32pio project: {str(self.path)}"
@property
def state(self) -> ProjectState:
- """
- Constructing and returning the current state of the project (tweaked dict, see ProjectState docs)
- """
-
- # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}")
+ """Constructing and returning the current state of the project (tweaked dict, see ProjectState docs)"""
pio_is_initialized = False
- with contextlib.suppress(Exception): # we just want to know the information and don't care about details
+ with contextlib.suppress(Exception): # we just want to know the status and don't care about the details
# Is present, is correct and is not empty
pio_is_initialized = len(self.platformio_ini_config.sections()) != 0
platformio_ini_is_patched = False
if pio_is_initialized: # make no sense to proceed if there is something happened in the first place
- with contextlib.suppress(Exception): # we just want to know the information and don't care about details
- platformio_ini_is_patched = self.platformio_ini_is_patched()
+ with contextlib.suppress(Exception): # we just want to know the status and don't care about the details
+ platformio_ini_is_patched = self.platformio_ini_is_patched
# Create the temporary ordered dictionary and fill it with the conditions results arrays
stages_conditions = collections.OrderedDict()
@@ -231,7 +242,7 @@ def state(self) -> ProjectState:
stages_conditions[ProjectStage.PIO_INITIALIZED] = [pio_is_initialized]
stages_conditions[ProjectStage.PATCHED] = [platformio_ini_is_patched,
not self.path.joinpath('include').is_dir()]
- # Hidden folder! Can be not visible in your familiar file manager and cause a confusion
+ # Hidden folder! Can be not visible in your file manager and cause a confusion
stages_conditions[ProjectStage.BUILT] = [
self.path.joinpath('.pio').is_dir() and
any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])]
@@ -246,8 +257,8 @@ def state(self) -> ProjectState:
def _find_ioc_file(self, explicit_file: pathlib.Path = None) -> pathlib.Path:
"""
- Find, check (that this is a non-empty text file) and return an .ioc file. If there are more than one return
- first. If no .ioc file is present raise FileNotFoundError exception. Use explicit_file if it was provided
+ Find, check (that this is a non-empty text file) and return an .ioc file. If there are more than one - return
+ first. If no .ioc file is present - raise the FileNotFoundError exception. Use explicit_file if it was provided.
Returns:
absolute path to the .ioc file
@@ -284,13 +295,14 @@ def _find_ioc_file(self, explicit_file: pathlib.Path = None) -> pathlib.Path:
# Check for the file correctness
try:
content = result_file.read_text() # should be a text file
- assert len(content) > 0
+ if len(content) == 0:
+ raise ValueError("the file is empty")
return result_file
except Exception as e:
raise Exception(f"{result_file.name} is incorrect") from e
- def _load_config(self, runtime_parameters: dict = None) -> configparser.ConfigParser:
+ def _load_config(self, runtime_parameters: Mapping[str, Any] = None) -> configparser.ConfigParser:
"""
Prepare ConfigParser config for the project. Order of getting values (masking) (higher levels overwrites lower):
@@ -311,16 +323,30 @@ def _load_config(self, runtime_parameters: dict = None) -> configparser.ConfigPa
# ... then merge with user's config file values (if exist) ...
self.logger.debug(f"searching for {stm32pio.settings.config_file_name}...")
- if len(config.read(str(self.path.joinpath(stm32pio.settings.config_file_name)))) == 0:
+ config.read(self.path.joinpath(stm32pio.settings.config_file_name))
+
+ ini_config = configparser.ConfigParser(interpolation=None)
+ ini_config.read(self.path.joinpath(stm32pio.settings.config_file_name))
+ runtime_config = configparser.ConfigParser(interpolation=None)
+ runtime_config.read_dict(runtime_parameters)
+
+ if len(ini_config.sections()):
+ if len(runtime_config.sections()):
+ for ini_sect in ini_config.sections():
+ if runtime_config.has_section(ini_sect):
+ for ini_key, ini_value in ini_config.items(ini_sect):
+ if runtime_config.get(ini_sect, ini_key, fallback=None) not in [None, ini_value]:
+ self.logger.info(f"given '{ini_key}' has taken a precedence over the .ini one")
+ else:
self.logger.debug(f"no or empty {stm32pio.settings.config_file_name} config file, will use the default one")
# ... finally merge with the given in this session CLI parameters
config.read_dict(runtime_parameters)
- # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow
+ # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow a
# propagation of this message
if self.logger.isEnabledFor(logging.DEBUG):
- debug_str = 'resolved config (merged):'
+ debug_str = 'resolved config:'
for section in config.sections():
debug_str += f"\n========== {section} ==========\n"
for value in config.items(section):
@@ -332,7 +358,7 @@ def _load_config(self, runtime_parameters: dict = None) -> configparser.ConfigPa
@staticmethod
def _save_config(config: configparser.ConfigParser, path: pathlib.Path, logger: logging.Logger) -> int:
"""
- Writes ConfigParser config to the file path and logs using Logger logger.
+ Writes the ConfigParser 'config' to the file 'path' and logs using the Logger 'logger'.
We declare this helper function which can be safely invoked by both internal methods and outer code. The latter
case is suitable for using in weakref' finalizer objects as one of its main requirement is to not keep
@@ -351,7 +377,7 @@ def _save_config(config: configparser.ConfigParser, path: pathlib.Path, logger:
logger.warning(f"cannot save the config: {e}", exc_info=logger.isEnabledFor(logging.DEBUG))
return -1
- def save_config(self, parameters: dict = None) -> int:
+ def save_config(self, parameters: Mapping[str, Mapping[str, Any]] = None) -> int:
"""
Invokes base _save_config function. Preliminarily, updates the config with the given 'parameters' dictionary. It
should has the following format:
@@ -376,13 +402,15 @@ def save_config(self, parameters: dict = None) -> int:
def generate_code(self) -> int:
"""
- Call STM32CubeMX app as 'java -jar' file to generate the code from the .ioc file. Pass commands to the
- STM32CubeMX in a temp file
+ Call STM32CubeMX app as 'java -jar' file to generate the code from the .ioc file. Pass the commands to the
+ STM32CubeMX in a temp file.
Returns:
return code on success, raises an exception otherwise
"""
+ self.logger.info("starting to generate a code from the CubeMX .ioc file...")
+
# Use mkstemp() instead of the higher-level API for the compatibility with the Windows (see tempfile docs for
# more details)
cubemx_script_file, cubemx_script_name = tempfile.mkstemp()
@@ -394,11 +422,8 @@ def generate_code(self) -> int:
cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content'))
cubemx_script_content = cubemx_script_template.substitute(ioc_file_absolute_path=self.ioc_file,
project_dir_absolute_path=self.path)
+ cubemx_script.write(cubemx_script_content.encode()) # should encode, since mode='w+b'
- # should encode, since mode='w+b'
- cubemx_script.write(cubemx_script_content.encode())
-
- self.logger.info("starting to generate a code from the CubeMX .ioc file...")
command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q',
cubemx_script_name, '-s'] # -q: read the commands from the file, -s: silent performance
# Redirect the output of the subprocess into the logging module (with DEBUG level)
@@ -415,22 +440,30 @@ def generate_code(self) -> int:
if result.returncode == 0:
# CubeMX 0 return code doesn't necessarily means the correct generation (e.g. migration dialog has appeared
# and 'Cancel' was chosen, or CubeMX_version < ioc_file_version), should analyze the output
- error_lines = [line for line in result_output.splitlines() if '[ERROR]' in line]
- if len(error_lines):
- self.logger.error('\n'.join(error_lines))
- raise Exception(error_msg)
- self.logger.info("successful code generation")
- return result.returncode
+ if 'Code succesfully generated' in result_output:
+ self.logger.info("successful code generation")
+ return result.returncode
+ else:
+ # GUESSING
+ error_lines = [line for line in result_output.splitlines(keepends=True) if '[ERROR]' in line]
+ if len(error_lines):
+ self.logger.error(error_lines, extra={ 'from_subprocess': True })
+ raise Exception(error_msg)
+ else:
+ self.logger.warning("Undefined result from the CubeMX (neither error or success symptoms were "
+ "found in the logs). Keep going but there might be an error")
+ return result.returncode
else:
- # Probably 'java' error (e.g. no CubeMX is present)
- self.logger.error(f"return code is {result.returncode}\n\n{result_output}")
+ # Most likely the 'java' error (e.g. no CubeMX is present)
+ self.logger.error(f"Return code is {result.returncode}. Output:\n\n{result_output}",
+ extra={ 'from_subprocess': True })
raise Exception(error_msg)
def pio_init(self) -> int:
"""
Call PlatformIO CLI to initialize a new project. It uses parameters (path, board) collected before so the
- confirmation about the data presence is lying on the invoking code
+ confirmation about the data presence is lying on the invoking code.
Returns:
return code of the PlatformIO on success, raises an exception otherwise
@@ -438,10 +471,14 @@ def pio_init(self) -> int:
self.logger.info("starting PlatformIO project initialization...")
- platformio_ini_file = self.path.joinpath('platformio.ini')
- # If size is 0, PlatformIO will overwrite it
- if platformio_ini_file.is_file() and platformio_ini_file.stat().st_size > 0:
- self.logger.warning("'platformio.ini' file is already exist")
+ try:
+ if len(self.platformio_ini_config.sections()):
+ self.logger.warning("'platformio.ini' file is already exist")
+ # else: file is empty (PlatformIO should overwrite it)
+ except FileNotFoundError:
+ pass # no file
+ except Exception:
+ self.logger.warning("'platformio.ini' file is already exist and incorrect")
command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.path), '-b',
self.config.get('project', 'board'), '-O', 'framework=stm32cube']
@@ -453,50 +490,58 @@ def pio_init(self) -> int:
error_msg = "PlatformIO project initialization error"
if result.returncode == 0:
# PlatformIO returns 0 even on some errors (e.g. no '--board' argument)
- if 'error' in result.stdout.lower():
- self.logger.error(result.stdout)
+ if 'error' in result.stdout.lower(): # GUESSING
+ self.logger.error(result.stdout, extra={ 'from_subprocess': True })
raise Exception(error_msg)
- self.logger.debug(result.stdout, 'from_subprocess')
+ self.logger.debug(result.stdout, extra={ 'from_subprocess': True })
self.logger.info("successful PlatformIO project initialization")
return result.returncode
else:
- self.logger.error(result.stdout)
+ self.logger.error(f"Return code is {result.returncode}. Output:\n\n{result.stdout}",
+ extra={ 'from_subprocess': True })
raise Exception(error_msg)
@property
def platformio_ini_config(self) -> configparser.ConfigParser:
"""
- Reads and parses 'platformio.ini' PlatformIO config file into newly created configparser.ConfigParser instance.
- Note, that the file may change over time and subsequent calls may produce different results because of this.
+ Reads and parses the 'platformio.ini' PlatformIO config file into a newly created configparser.ConfigParser
+ instance. Note, that the file may change over time and subsequent calls may produce different results because
+ of this.
Raises FileNotFoundError if no 'platformio.ini' file is present. Passes out all other exceptions, most likely
- caused by parsing errors (i.e. corrupted .INI format).
+ caused by parsing errors (i.e. corrupted .INI format), e.g.
+
+ configparser.MissingSectionHeaderError: File contains no section headers.
+
+ It doesn't use any interpolation as we do not interested in the particular values, just presence and correctness
+ When using this property for comparing, make sure your other config doesn't use the interpolation either so we
+ just can match raw unprocessed strings.
"""
platformio_ini = configparser.ConfigParser(interpolation=None)
- if len(platformio_ini.read(self.path.joinpath('platformio.ini'))) == 0:
- raise FileNotFoundError('platformio.ini')
+ platformio_ini.read(self.path.joinpath('platformio.ini').resolve(strict=True))
return platformio_ini
+ @property
def platformio_ini_is_patched(self) -> bool:
"""
Check whether 'platformio.ini' config file is patched or not. It doesn't check for complete project patching
- (e.g. unnecessary folders deletion). Throws errors on non-existing file and on incorrect patch or file
+ (e.g. unnecessary folders deletion). Throws errors on non-existing file and on incorrect patch or file.
Returns:
boolean indicating a result
"""
try:
- platformio_ini = self.platformio_ini_config
+ platformio_ini = self.platformio_ini_config # existing .ini file
except FileNotFoundError as e:
raise Exception("Cannot determine is project patched: 'platformio.ini' file not found") from e
except Exception as e:
raise Exception("Cannot determine is project patched: 'platformio.ini' file is incorrect") from e
- patch_config = configparser.ConfigParser(interpolation=None)
+ patch_config = configparser.ConfigParser(interpolation=None) # our patch has the INI config format, too
try:
patch_config.read_string(self.config.get('project', 'platformio_ini_patch_content'))
except Exception as e:
@@ -519,22 +564,20 @@ def platformio_ini_is_patched(self) -> bool:
def patch(self) -> None:
"""
- Patch platformio.ini file by a user's patch. By default, it sets the created earlier (by CubeMX 'Src' and 'Inc')
- folders as sources. configparser doesn't preserve any comments unfortunately so keep in mind that all of them
- will be lost at this stage. Also, the order may be violated. In the end, remove old empty folders
+ Patch the 'platformio.ini' config file by a user's patch. By default, it sets the created earlier (by CubeMX
+ 'Src' and 'Inc') folders as sources specifying it in the [platformio] INI section. configparser doesn't preserve
+ any comments unfortunately so keep in mind that all of them will be lost at this stage. Also, the order may be
+ violated. In the end, removes an old empty folders.
"""
self.logger.debug("patching 'platformio.ini' file...")
- if self.platformio_ini_is_patched():
+ if self.platformio_ini_is_patched:
self.logger.info("'platformio.ini' has been already patched")
else:
- # Existing .ini file
- platformio_ini_config = configparser.ConfigParser(interpolation=None)
- platformio_ini_config.read(self.path.joinpath('platformio.ini'))
+ platformio_ini_config = self.platformio_ini_config # existing .ini file
- # Our patch has the config format too
- patch_config = configparser.ConfigParser(interpolation=None)
+ patch_config = configparser.ConfigParser(interpolation=None) # our patch has the INI config format, too
patch_config.read_string(self.config.get('project', 'platformio_ini_patch_content'))
# Merge 2 configs
@@ -546,7 +589,7 @@ def patch(self) -> None:
self.logger.debug(f"set [{patch_section}]{patch_key} = {patch_value}")
platformio_ini_config.set(patch_section, patch_key, patch_value)
- # Save, overwriting the original file (deletes all comments!)
+ # Save, overwriting (node='w') the original file (deletes all comments!)
with self.path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file:
platformio_ini_config.write(platformio_ini_file)
self.logger.debug("'platformio.ini' has been patched")
@@ -556,6 +599,7 @@ def patch(self) -> None:
self.logger.debug("'include' folder has been removed")
except Exception:
self.logger.info("cannot delete 'include' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG))
+
# Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check
if not self.path.joinpath('SRC').is_dir():
try:
@@ -567,9 +611,37 @@ def patch(self) -> None:
self.logger.info("project has been patched")
+ def build(self) -> int:
+ """
+ Initiate a build of the PlatformIO project by the PlatformIO ('run' command). PlatformIO prints warning and
+ error messages by itself to the STDERR so there is no need to catch it and output by us
+
+ Returns:
+ passes a return code of the PlatformIO
+ """
+
+ self.logger.info("starting PlatformIO project build...")
+
+ command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)]
+ if not self.logger.isEnabledFor(logging.DEBUG):
+ command_arr.append('--silent')
+
+ # In the non-verbose mode (logging.INFO) there would be a '--silent' option so if the PlatformIO will decide to
+ # output something then it's really important and we use logging.WARNING as a level
+ log_level = logging.DEBUG if self.logger.isEnabledFor(logging.DEBUG) else logging.WARNING
+ with stm32pio.util.LogPipe(self.logger, log_level) as log:
+ result = subprocess.run(command_arr, stdout=log.pipe, stderr=log.pipe)
+
+ if result.returncode == 0:
+ self.logger.info("successful PlatformIO build")
+ else:
+ self.logger.error("PlatformIO build error")
+ return result.returncode
+
+
def start_editor(self, editor_command: str) -> int:
"""
- Start the editor specified by 'editor_command' with the project opened (assuming that
+ Start the editor specified by the 'editor_command' with a project opened (assuming that
$ [editor] [folder]
format works)
@@ -588,7 +660,7 @@ def start_editor(self, editor_command: str) -> int:
# result = subprocess.run([editor_command, str(self.path)], check=True)
result = subprocess.run(f"{sanitized_input} {str(self.path)}", shell=True, check=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- self.logger.debug(result.stdout, 'from_subprocess')
+ self.logger.debug(result.stdout, extra={ 'from_subprocess': True })
return result.returncode
except subprocess.CalledProcessError as e:
@@ -596,32 +668,6 @@ def start_editor(self, editor_command: str) -> int:
return e.returncode
- def build(self) -> int:
- """
- Initiate a build of the PlatformIO project by the PlatformIO ('run' command). PlatformIO prints warning and
- error messages by itself to the STDERR so there is no need to catch it and output by us
-
- Returns:
- passes a return code of the PlatformIO
- """
-
- self.logger.info("starting PlatformIO project build...")
-
- command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)]
- if not self.logger.isEnabledFor(logging.DEBUG):
- command_arr.append('--silent')
-
- log_level = logging.DEBUG if self.logger.isEnabledFor(logging.DEBUG) else logging.WARNING
- with stm32pio.util.LogPipe(self.logger, log_level) as log:
- result = subprocess.run(command_arr, stdout=log.pipe, stderr=log.pipe)
-
- if result.returncode == 0:
- self.logger.info("successful PlatformIO build")
- else:
- self.logger.error("PlatformIO build error")
- return result.returncode
-
-
def clean(self) -> None:
"""
Clean-up the project folder preserving only an '.ioc' file
diff --git a/stm32pio/settings.py b/stm32pio/settings.py
index 5ad21d3..4e66c3c 100644
--- a/stm32pio/settings.py
+++ b/stm32pio/settings.py
@@ -3,6 +3,8 @@
import pathlib
import platform
+import stm32pio.lib
+
my_os = platform.system()
@@ -21,7 +23,7 @@
# macOS default: 'Applications' folder
"/Applications/STMicroelectronics/STM32CubeMX.app/Contents/Resources/STM32CubeMX" if my_os == 'Darwin' else
# Linux (Ubuntu) default: home directory
- pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX") if my_os == 'Linux' else
+ str(pathlib.Path.home().joinpath("STM32CubeMX/STM32CubeMX")) if my_os == 'Linux' else
# Windows default: Program Files
"C:/Program Files/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX.exe" if my_os == 'Windows' else None
},
@@ -48,12 +50,12 @@
# Runtime-determined values
'board': '',
- 'ioc_file': '' # required
+ 'ioc_file': '' # required, the file name (not a full path)
}
)
config_file_name = 'stm32pio.ini'
-# Longest name (not necessarily method so a little bit tricky...)
+# Longest name (not necessarily a method so a little bit tricky...)
# log_fieldwidth_function = max([len(member) for member in dir(stm32pio.lib.Stm32pio)]) + 1
log_fieldwidth_function = 25 + 1
diff --git a/stm32pio/util.py b/stm32pio/util.py
index d253168..7533cd8 100644
--- a/stm32pio/util.py
+++ b/stm32pio/util.py
@@ -2,95 +2,169 @@
Some auxiliary entities not falling into other categories
"""
+import contextlib
+import enum
import json
import logging
import os
import subprocess
import threading
-from typing import List
+import traceback
+import warnings
+from typing import Any, List, Mapping, MutableMapping, Tuple, Optional
+module_logger = logging.getLogger(__name__) # this file logger
-module_logger = logging.getLogger(__name__)
+logging_levels = { # for exposing the levels to the GUI
+ logging.getLevelName(logging.CRITICAL): logging.CRITICAL,
+ logging.getLevelName(logging.ERROR): logging.ERROR,
+ logging.getLevelName(logging.WARNING): logging.WARNING,
+ logging.getLevelName(logging.INFO): logging.INFO,
+ logging.getLevelName(logging.DEBUG): logging.DEBUG,
+ logging.getLevelName(logging.NOTSET): logging.NOTSET
+}
-# Do not add or remove any information from the message and simply pass it "as-is"
-special_formatters = {
- 'subprocess': logging.Formatter('%(message)s')
-}
+def log_current_exception(logger: logging.Logger, show_traceback_threshold_level: int = logging.DEBUG):
+ """
+ Print format is:
-default_log_record_factory = logging.getLogRecordFactory()
+ ExceptionName: message
+ [optional] traceback
-def log_record_factory(*log_record_args, **log_record_kwargs):
+ We do not explicitly retrieve an exception info via sys.exc_info() as it immediately stores a reference to the
+ current Python frame and/or variables causing some possible weird errors (objects are not GC'ed) and memory leaks.
+ See https://cosmicpercolator.com/2016/01/13/exception-leaks-in-python-2-and-3/ for more information
"""
- Replace the default factory of logging.LogRecord's instances so we can handle our special logging flags
+ exc_full_str = traceback.format_exc()
+ exc_str = exc_full_str.splitlines()[-1]
+ exc_tb = ''.join(exc_full_str.splitlines(keepends=True)[:-1])
+ logger.error(f'{exc_str}\n{exc_tb}' if logger.isEnabledFor(show_traceback_threshold_level) else exc_str)
+
+
+class ProjectLoggerAdapter(logging.LoggerAdapter):
"""
- args_idx = 5 # index of 'args' argument in the positional arguments list
+ Use this as a logger for every project:
- if 'from_subprocess' in log_record_args[args_idx]:
- # Remove our custom flag from the tuple (it is inside a tuple that is inside a list)
- new_log_record_args = log_record_args[:args_idx] + \
- (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) + \
- log_record_args[args_idx + 1:]
- # Construct an ordinary LogRecord and append our flag as an attribute
- record = default_log_record_factory(*new_log_record_args, **log_record_kwargs)
- record.from_subprocess = True
- else:
- record = default_log_record_factory(*log_record_args, **log_record_kwargs)
+ self.logger = stm32pio.util.ProjectLoggerAdapter(logging.getLogger('some_singleton_projects_logger'),
+ { 'project_id': id(self) })
- return record
+ It will automatically mix in 'project_id' (and any other property) to every LogRecord (whether you supply 'extra' in
+ your log call or not)
+ """
+ def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> Tuple[Any, MutableMapping[str, Any]]:
+ """Inject context data (both from the adapter and the log call)"""
+ if 'extra' in kwargs:
+ kwargs['extra'].update(self.extra)
+ else:
+ kwargs['extra'] = self.extra
+ return msg, kwargs
-logging.setLogRecordFactory(log_record_factory)
+
+# Currently available verbosity levels. Verbosity determines how every LogRecord will be formatted (regardless its
+# logging level)
+@enum.unique
+class Verbosity(enum.IntEnum):
+ NORMAL = enum.auto()
+ VERBOSE = enum.auto()
+
+
+# Do not add or remove any information from the message and simply pass it "as-is"
+as_is_formatter = logging.Formatter('%(message)s')
class DispatchingFormatter(logging.Formatter):
"""
The wrapper around the ordinary logging.Formatter allowing to have multiple formatters for different purposes.
- 'extra' argument of the log() function has a similar intention but different mechanics
+ General arguments schema:
+
+ {
+ verbosity=Verbosity.NORMAL,
+ general={
+ Verbosity.NORMAL: logging.Formatter(...)
+ Verbosity.VERBOSE: logging.Formatter(...)
+ ...
+ },
+ special={
+ 'case_1': {
+ Verbosity.NORMAL: logging.Formatter(...)
+ ...
+ },
+ ...
+ }
+ }
"""
- def __init__(self, *args, special: dict = None, **kwargs):
- super().__init__(*args, **kwargs)
+ # Mapping of logging formatters for "special". Currently, only "from_subprocess" is defined. It's good to hide such
+ # implementation details as much as possible though they are still tweakable from the outer code
+ special_formatters = {
+ 'from_subprocess': { # TODO: maybe remade as enum, too? To have an IDE hints and more safety in general
+ level: as_is_formatter for level in Verbosity
+ }
+ }
+
+ def __init__(self, *args, general: Mapping[Verbosity, logging.Formatter] = None,
+ special: Mapping[str, Mapping[Verbosity, logging.Formatter]] = None,
+ verbosity: Verbosity = Verbosity.NORMAL, **kwargs):
+
+ super().__init__(*args, **kwargs) # will be '%(message)s' if no arguments were given
- # Store all provided formatters in an internal variable
- if isinstance(special, dict) and all(isinstance(value, logging.Formatter) for value in special.values()):
- self._formatters = special
+ self.verbosity = verbosity
+ self._warn_was_shown = False
+
+ if general is not None:
+ self.general = general
+ else:
+ warnings.warn("'general' argument for DispatchingFormatter was not provided. It contains formatters for "
+ "all the logging events except special ones and should be a dict with verbosity levels keys "
+ "and logging.Formatter values")
+ self.general = {}
+
+ if special is not None:
+ self.special = special
else:
- module_logger.warning(f"'special' argument is for providing custom formatters for special logging events "
- "and should be a dictionary with logging.Formatter values")
- self._formatters = {}
+ self.special = DispatchingFormatter.special_formatters # use defaults
+
+
+ def find_formatter_for(self, record: logging.LogRecord, verbosity: Verbosity) -> Optional[logging.Formatter]:
+ """Determine and return the appropriate formatter"""
+ special_formatter = next((self.special[case] for case in self.special.keys() if hasattr(record, case)), None)
+ if special_formatter is not None:
+ return special_formatter.get(verbosity)
+ else:
+ return self.general.get(verbosity)
- self.warn_was_shown = False
def format(self, record: logging.LogRecord) -> str:
- """
- Use suitable formatter based on the LogRecord attributes
- """
- if hasattr(record, 'from_subprocess') and record.from_subprocess:
- if 'subprocess' in self._formatters:
- return self._formatters['subprocess'].format(record)
- elif not self.warn_was_shown:
- module_logger.warning("No formatter found for the 'subprocess' case, use default hereinafter")
- return super().format(record)
+ """Overridden method"""
+ # Allows to specify a verbosity level on the per-record basis, not only globally
+ formatter = self.find_formatter_for(record,
+ record.verbosity if hasattr(record, 'verbosity') else self.verbosity)
+ if formatter is not None:
+ return formatter.format(record)
+ else:
+ if not self._warn_was_shown:
+ self._warn_was_shown = True
+ module_logger.warning("No formatter found, use default one hereinafter")
+ return super().format(record)
-class LogPipeRC:
- """
- Small class suitable for passing to the caller when the LogPipe context manager is invoked
- """
+class LogPipeRC:
+ """Small class suitable for passing to the caller when the LogPipe context manager is invoked"""
value = '' # string accumulating all incoming messages
def __init__(self, fd: int):
self.pipe = fd # writable half of os.pipe
-class LogPipe(threading.Thread):
+class LogPipe(threading.Thread, contextlib.AbstractContextManager):
"""
The thread combined with a context manager to provide a nice way to temporarily redirect something's stream output
- into logging module. The most straightforward application is to suppress subprocess STDOUT and/or STDERR streams and
- wrap them in the logging mechanism as it is now for any other message in your app. Also, store the incoming messages
- in the string
+ into the logging module. One straightforward application is to suppress subprocess STDOUT and/or STDERR streams and
+ wrap them into the logging mechanism as it is now for any other message in your app. Also, store the incoming
+ messages in the string for using it after an execution
"""
def __init__(self, logger: logging.Logger, level: int, *args, **kwargs):
@@ -118,7 +192,7 @@ def run(self):
"""
for line in iter(self.pipe_reader.readline, ''): # stops the iterator when empty string will occur
self.rc.value += line # accumulate the string
- self.logger.log(self.level, line.strip('\n'), 'from_subprocess') # mark the message origin
+ self.logger.log(self.level, line.strip('\n'), extra={ 'from_subprocess': True }) # mark the message origin
self.pipe_reader.close()
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -129,13 +203,12 @@ def __exit__(self, exc_type, exc_val, exc_tb):
os.close(self.fd_write)
-
def get_platformio_boards(platformio_cmd) -> List[str]:
"""
Obtain the PlatformIO boards list. As we interested only in STM32 ones, cut off all the others.
- IMPORTANT NOTE: The inner implementation can go to the Internet from time to time when it decides that its cache is
- out of date. So it can take a long time to execute.
+ IMPORTANT NOTE: PlatformIO can go to the Internet from time to time when it decides that its cache is out of date.
+ So it can take a long time to execute.
"""
# Windows 7, as usual, correctly works only with shell=True...
diff --git a/stm32pio_gui/README.md b/stm32pio_gui/README.md
index dc2a15e..8fbbd14 100644
--- a/stm32pio_gui/README.md
+++ b/stm32pio_gui/README.md
@@ -2,10 +2,16 @@
![Main](screenshots/main.png)
-The cross-platform GUI version of the stm32pio. It wraps the core library functionality into the Qt-QML skin using the PySide2 (aka "Qt for Python" project) and adding the projects management feature allowing you to store and manipulate multiple stm32pio projects at one place.
+The cross-platform GUI version of the stm32pio. It wraps the core library functionality into the Qt5-QML skin using the PySide2 (aka "Qt for Python" project) and adding the projects management feature allowing you to store and manipulate on multiple stm32pio projects at one place.
-## Installation
+## Table of contents
+> - [Install and run](#install-and-run)
+> - [Usage](#usage)
+> - [Architecture notes](#architecture-notes)
+
+
+## Install and run
The app requires PySide2 5.12+ package (Qt 5.12 respectively). It is available in all major package managers including pip, apt, brew and so on.
@@ -18,38 +24,48 @@ Then it can be started as
```shell script
$ stm32pio_gui
```
-from anywhere. If you have already installed the latest basic CLI version this script and sources are already on your machine so you can reinstall using the command above or to supplement the setup installing the PySide2 manually.
+or
+```shell script
+$ stm32pio gui
+```
+from anywhere. If you have already installed the latest basic CLI version, this script and sources are already on your machine so you can reinstall it using the command above or just supplement the setup installing the PySide2 manually.
-If you rather want to launch completely from the sources, currently it's possible only from the repository root point:
+If you rather want to launch completely from sources, it is possible like this:
```shell script
-stm32pio-repo/ $ python stm32pio_gui/app.py
+$ python path/to/stm32pio_gui/app.py
```
or
```shell script
stm32pio-repo/ $ python -m stm32pio_gui
```
+Either way, you can additionally specify the project (and board ID) to open with:
+```shell script
+$ stm32pio_gui -d ./sample-project -b discovery_f4
+```
## Usage
-Add a folder with the `.ioc` file to begin with. You can also drag-and-drop it into the main window, in this case you can add multiple projects simultaneously. If the project is empty the initialization screen will be shown to help in setup:
+Add a folder with the `.ioc` file to begin with. You can either use an "Add" button or drag-and-drop it into the main window, in the latter case you can also have an ability to add multiple projects simultaneously. If the project is empty the initialization screen will be shown to help in setup:
![Init](screenshots/init_screen.png)
-You can skip it or enter one of the available PlatformIO STM32 boards. Select "Run" to apply all actions to the project (analog of the `new` CLI command).
+Skip it or enter one of the available PlatformIO STM32 boards identifier. Select "Run" to apply all actions to the project (analog of the `new` CLI command).
-In the main screen the buttons row allows you to run specific actions while represents the state of the project at the same time. Green color means that this stage is fulfilled. The active project is monitored automatically while all the others refresh only when you click on them so the "stage" line at the projects list item can be outdated.
+In the main screen the buttons row allows you to run specific actions while, at the same time, represents the state of the project. Green color means that this stage is fulfilled. The active project is monitored automatically while all the others refreshes only when you click on them so the "stage" line at the projects list item can be outdated.
-Let's assume you've worked on the project for some time and need to re-generate and rebuild the configuration. To schedule all the necessary actions to run one after another navigate to the last desired action pressing the Shift key. All the projects prior this one should be colored light-green now:
+Let's assume you've worked on the project for some time and need to re-generate and rebuild the configuration. To schedule all the necessary actions to run one after another navigate to the last desired action pressing the Shift key. All the actions prior this one should be colored light-green now:
![Highlighting](screenshots/highlighting.png)
-Shift-click on it to execute the series. The picked actions will be framed with border around each of them:
+Shift-click on it to execute the series. The picked actions will be framed with the border around each of them:
![Group](screenshots/group.png)
-Add Ctrl to the mouse click to start the editor specified in the settings after the action. It can be combined with Shift. **Hint:** specify a `start` as an "Editor" command to open the folder in the new Explorer window under the Windows, `open` for the Finder on the macOS.
+Add Ctrl to the mouse click to start the editor specified in the settings after the action. It can be combined with Shift as well. **Hint:** specify a `start` as an "Editor" command to open the folder in the new Explorer window under the Windows, `open` for the Finder on the macOS.
+
+Currently, the project config (stm32pio.ini) is not live-reloaded so any changes you do to it will not be reflected until the next start.
## Architecture notes
@@ -57,10 +73,3 @@ Add Ctrl to the mouse click to start the editor specified in the settings after
Projects list (not the projects themself) and settings are stored by `QSettings` so refer to its docs if you bother about the actual location.
See `docs` directory to see state machine diagram of the project action button.
-
-
-## Known issues
-
-The number of added projects that can be correctly represented is currently limited to about 5 due to some architectural mistakes. It's planned to be fixed in the near future.
-
-Right after the removing of the project from the list there are several errors on the terminal appears. It is most likely caused by the non proper destruction order of components and isn't something to be worried about. By a similar reasons the app itself sometimes crushes during the shutdown process (doesn't observed on the macOS, though).
diff --git a/stm32pio_gui/app.py b/stm32pio_gui/app.py
index 629f187..1487985 100644
--- a/stm32pio_gui/app.py
+++ b/stm32pio_gui/app.py
@@ -1,16 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import argparse
import collections
+import inspect
import logging
import pathlib
import platform
import sys
import threading
import time
-import traceback
import weakref
-from typing import List, Callable, Optional, Dict, Any
+from typing import List, Callable, Optional, Any, Mapping, MutableMapping, Iterator
try:
from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\
@@ -30,39 +31,54 @@
"or manually install its dependencies by yourself")
sys.exit(-1)
-ROOT_PATH = pathlib.Path(sys.path[0]).parent
-MODULE_PATH = pathlib.Path(__file__).parent
+ROOT_PATH = pathlib.Path(sys.path[0]).parent # repo's or the site-package's root
+MODULE_PATH = pathlib.Path(__file__).parent # module path, e.g. stm32pio-repo/stm32pio_gui/
try:
import stm32pio.settings
import stm32pio.lib
import stm32pio.util
+ import stm32pio.app
except ModuleNotFoundError:
sys.path.insert(0, str(ROOT_PATH))
import stm32pio.settings
import stm32pio.lib
import stm32pio.util
+ import stm32pio.app
+ProjectID = int
-class BufferedLoggingHandler(logging.Handler):
+
+class BuffersDispatchingHandler(logging.Handler):
"""
- Simple logging.Handler subclass putting all incoming records into the given buffer
+ Every user's project using its own buffer (collections.deque) to store logs. This simple logging.Handler subclass
+ finds and puts an incoming record into the corresponding buffer
"""
- def __init__(self, buffer: collections.deque):
- super().__init__()
- self.buffer = buffer
+ buffers: MutableMapping[ProjectID, collections.deque] = {} # the dictionary of projects' ids and theirs buffers
def emit(self, record: logging.LogRecord) -> None:
- self.buffer.append(record)
+ if hasattr(record, 'project_id'):
+ # As we exist in the asynchronous environment there is always a risk of some "desynchronization" when the
+ # project (and its buffer) has already been gone but some late message has arrived. Hence, we need to check
+ buffer = self.buffers.get(record.project_id)
+ if buffer is not None:
+ buffer.append(record)
+ else:
+ module_logger.warning(f"Logging buffer for the project id {record.project_id} not found. The message "
+ f"was:\n{record.msg}")
+ else:
+ module_logger.warning("LogRecord doesn't have a project_id attribute. Perhaps this is a result of the "
+ f"logging setup misconfiguration. Anyway, the message was:\n{record.msg}")
class LoggingWorker(QObject):
"""
- QObject living in a separate QThread, logging everything it receiving. Intended to be an attached ProjectListItem
- property. Stringifies log records using DispatchingFormatter and passes them via Signal interface so they can be
- conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret them
- differently.
+ QObject living in a separate QThread, logging everything it receiving. Intended to be an attached
+ ProjectListItem property. Stringifies log records using global BuffersDispatchingHandler instance (its
+ stm32pio.util.DispatchingFormatter, to be precise) and passes them via Qt Signal interface so they can be
+ conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can
+ interpret them differently.
Can be controlled by two threading.Event's:
stopped - on activation, leads to thread termination
@@ -72,56 +88,55 @@ class LoggingWorker(QObject):
sendLog = Signal(str, int)
- def __init__(self, logger: logging.Logger, parent: QObject = None):
+ def __init__(self, project_id: ProjectID, parent: QObject = None):
super().__init__(parent=parent)
+ self.project_id = project_id
self.buffer = collections.deque()
+ projects_logger_handler.buffers[project_id] = self.buffer # register our buffer
+
self.stopped = threading.Event()
self.can_flush_log = threading.Event()
- self.logging_handler = BufferedLoggingHandler(self.buffer)
-
- logger.addHandler(self.logging_handler)
- self.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter(
- f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s",
- special=stm32pio.util.special_formatters))
self.thread = QThread()
self.moveToThread(self.thread)
-
self.thread.started.connect(self.routine)
self.thread.start()
def routine(self) -> None:
"""
- The worker constantly querying the buffer on the new log messages availability.
+ The worker constantly querying the buffer on the new log messages availability
"""
while not self.stopped.wait(timeout=0.050):
if self.can_flush_log.is_set() and len(self.buffer):
record = self.buffer.popleft()
- self.sendLog.emit(self.logging_handler.format(record), record.levelno)
- module_logger.debug('exit logging worker')
+ self.sendLog.emit(projects_logger_handler.format(record), record.levelno)
+ # TODO: maybe we should flush all remaining logs before termination
+ projects_logger_handler.buffers.pop(self.project_id) # unregister our buffer
+ module_logger.debug(f"exit LoggingWorker of project id {self.project_id}")
self.thread.quit()
class ProjectListItem(QObject):
"""
- The core functionality class - wrapper around Stm32pio class suitable for the project GUI representation
+ The core functionality class - the wrapper around the Stm32pio class suitable for the project GUI representation
"""
- nameChanged = Signal() # properties notifiers
- stateChanged = Signal()
- stageChanged = Signal()
-
logAdded = Signal(str, int, arguments=['message', 'level']) # send the log message to the front-end
actionStarted = Signal(str, arguments=['action'])
actionFinished = Signal(str, bool, arguments=['action', 'success'])
- def __init__(self, project_args: list = None, project_kwargs: dict = None, from_startup: bool = False,
- parent: QObject = None):
+ def __init__(self, project_args: List[any] = None, project_kwargs: Mapping[str, Any] = None,
+ from_startup: bool = False, parent: QObject = None):
"""
+ Instance construction is split into 2 phases: the wrapper setup and inner Stm32pio class initialization. The
+ latter one is taken out to the separated thread as it is, potentially, a time-consuming operation. This thread
+ starts right after the main constructor so the wrapper is already built at that moment and therefore can be used
+ from GUI, be referenced and so on.
+
Args:
project_args: list of positional arguments that will be passed to the Stm32pio constructor
project_kwargs: dictionary of keyword arguments that will be passed to the Stm32pio constructor
@@ -139,26 +154,26 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, from_
self._from_startup = from_startup
- self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}")
- self.logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
- self.logging_worker = LoggingWorker(self.logger)
+ underlying_logger = logging.getLogger('stm32pio_gui.projects')
+ self.logger = stm32pio.util.ProjectLoggerAdapter(underlying_logger, { 'project_id': id(self) })
+ self.logging_worker = LoggingWorker(project_id=id(self))
self.logging_worker.sendLog.connect(self.logAdded)
# QThreadPool can automatically queue new incoming tasks if a number of them are larger than maxThreadCount
self.workers_pool = QThreadPool(parent=self)
self.workers_pool.setMaxThreadCount(1)
- self.workers_pool.setExpiryTimeout(-1) # tasks forever wait for the available spot
+ self.workers_pool.setExpiryTimeout(-1) # tasks wait forever for the available spot
self._current_action = ''
- # These values are valid till the Stm32pio project does not initialize itself (or failed to)
+ # These values are valid only until the Stm32pio project initialize itself (or failed to) (see init_project)
self.project = None
self._name = 'Loading...'
- self._state = { 'LOADING': True } # pseudo-stage (isn't present in ProjectStage enum)
+ self._state = { 'LOADING': True } # pseudo-stage (not present in the ProjectStage enum but is used from QML)
self._current_stage = 'Loading...'
self.qml_ready = threading.Event() # the front and the back both should know when each other is initialized
- # Register some kind of the deconstruction handler (later, after the project initialization)
+ # Register some kind of the deconstruction handler (later, after the project initialization, see init_project)
self._finalizer = None
if 'instance_options' not in project_kwargs:
@@ -183,14 +198,12 @@ def init_project(self, *args, **kwargs) -> None:
try:
self.project = stm32pio.lib.Stm32pio(*args, **kwargs)
except Exception:
- # Error during the initialization. Print format is: "ExceptionName: message"
- self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1],
- exc_info=self.logger.isEnabledFor(logging.DEBUG))
+ stm32pio.util.log_current_exception(self.logger)
if len(args):
self._name = args[0] # use a project path string (as it should be a first argument) as a name
else:
self._name = 'Undefined'
- self._state = { 'INIT_ERROR': True } # pseudo-stage (isn't present in ProjectStage enum)
+ self._state = { 'INIT_ERROR': True } # pseudo-stage
self._current_stage = 'Initializing error'
else:
# Successful initialization. These values should not be used anymore but we "reset" them anyway
@@ -210,14 +223,15 @@ def init_project(self, *args, **kwargs) -> None:
@staticmethod
def at_exit(workers_pool: QThreadPool, logging_worker: LoggingWorker, name: str):
"""
- Instance deconstruction handler meant to be used with weakref.finalize() conforming with the requirement to have
- no reference to the target object (so it is decorated as 'staticmethod')
+ The instance deconstruction handler is meant to be used with weakref.finalize() conforming with the requirement
+ to have no reference to the target object (so it doesn't contain any instance reference and also is decorated as
+ 'staticmethod')
"""
- module_logger.info(f"destroy {name}")
# Wait forever for all the jobs to complete. Currently, we cannot abort them gracefully
workers_pool.waitForDone(msecs=-1)
logging_worker.stopped.set() # post the event in the logging worker to inform it...
logging_worker.thread.wait() # ...and wait for it to exit, too
+ module_logger.info(f"destroyed {name} ProjectListItem")
@Property(bool)
@@ -225,6 +239,16 @@ def fromStartup(self) -> bool:
"""Is this project is here from the beginning of the app life?"""
return self._from_startup
+ @Property('QVariant')
+ def config(self) -> dict:
+ """Inner project's ConfigParser config converted to the dictionary (QML JS object)"""
+ return {
+ section: {
+ key: value for key, value in self.project.config.items(section)
+ } if self.project is not None else {} for section in ['app', 'project']
+ }
+
+ nameChanged = Signal()
@Property(str, notify=nameChanged)
def name(self) -> str:
"""Human-readable name of the project. Will evaluate to the absolute path if it cannot be instantiated"""
@@ -233,6 +257,7 @@ def name(self) -> str:
else:
return self._name
+ stateChanged = Signal()
@Property('QVariant', notify=stateChanged)
def state(self) -> dict:
"""
@@ -253,6 +278,7 @@ def state(self) -> dict:
else:
return self._state
+ stageChanged = Signal()
@Property(str, notify=stageChanged)
def currentStage(self) -> str:
"""
@@ -282,7 +308,8 @@ def actionStartedSlot(self, action: str):
def actionFinishedSlot(self, action: str, success: bool):
"""Pass the corresponding signal from the worker, perform related tasks"""
if not success:
- self.workers_pool.clear() # clear the queue - stop further execution
+ # Clear the queue - stop further execution (cancel planned tasks if an error had happened)
+ self.workers_pool.clear()
self.actionFinished.emit(action, success)
# Currently, this property should be reset AFTER emitting the 'actionFinished' signal (because QML will query it
# when the signal will be handled in StateMachine) (probably, should be resolved later as it is bad to be bound
@@ -297,7 +324,7 @@ def qmlLoaded(self):
@Slot(str, 'QVariantList')
- def run(self, action: str, args: list):
+ def run(self, action: str, args: List[Any]):
"""
Asynchronously perform Stm32pio actions (generate, build, etc.) (dispatch all business logic).
@@ -319,18 +346,18 @@ def run(self, action: str, args: list):
class Worker(QObject, QRunnable):
"""
Generic worker for asynchronous processes: QObject + QRunnable combination. First allows to attach Qt signals,
- second is compatible with QThreadPool.
+ second is compatible with the QThreadPool
"""
started = Signal(str, arguments=['action'])
finished = Signal(str, bool, arguments=['action', 'success'])
- def __init__(self, func: Callable[[list], Optional[int]], args: list = None, logger: logging.Logger = None,
- parent: QObject = None):
+ def __init__(self, func: Callable[[List[Any]], Optional[int]], args: List[Any] = None,
+ logger: logging.Logger = None, parent: QObject = None):
"""
Args:
- func: function to run. It should return 0 or None to call to be considered successful
+ func: function to run. It should return 0 or None for the call to be considered successful
args: the list of positional arguments. They will be unpacked and passed to the function
logger: optional logger to report about the occurred exception
parent: Qt object
@@ -351,9 +378,7 @@ def run(self):
result = self.func(*self.args)
except Exception:
if self.logger is not None:
- # Print format is: "ExceptionName: message"
- self.logger.exception(traceback.format_exception_only(*(sys.exc_info()[:2]))[-1],
- exc_info=self.logger.isEnabledFor(logging.DEBUG))
+ stm32pio.util.log_current_exception(self.logger)
result = -1
if result is None or (type(result) == int and result == 0):
@@ -375,10 +400,10 @@ def run(self):
class ProjectsList(QAbstractListModel):
"""
QAbstractListModel implementation - describe basic operations and delegate all main functionality to the
- ProjectListItem.
+ ProjectListItem
"""
- duplicateFound = Signal(int, arguments=['duplicateIndex'])
+ goToProject = Signal(int, arguments=['indexToGo'])
def __init__(self, projects: List[ProjectListItem] = None, parent: QObject = None):
"""
@@ -387,13 +412,18 @@ def __init__(self, projects: List[ProjectListItem] = None, parent: QObject = Non
parent: QObject to be parented to
"""
super().__init__(parent=parent)
+
self.projects = projects if projects is not None else []
+ self.workers_pool = QThreadPool(parent=self)
+ self.workers_pool.setMaxThreadCount(1) # only 1 active worker at a time
+ self.workers_pool.setExpiryTimeout(-1) # tasks wait forever for the available spot
+
@Slot(int, result=ProjectListItem)
def get(self, index: int):
"""
Expose the ProjectListItem to the GUI QML side. You should firstly register the returning type using
- qmlRegisterType or similar.
+ qmlRegisterType or similar
"""
if index in range(len(self.projects)):
return self.projects[index]
@@ -405,98 +435,145 @@ def data(self, index: QModelIndex, role=None):
if role == Qt.DisplayRole or role is None:
return self.projects[index.row()]
- def addProject(self, project: ProjectListItem):
+ def _saveInSettings(self) -> None:
"""
- Append already formed ProjectListItem to the projects list
+ Get correct projects and save them to Settings. Intended to be run in a thread
"""
- self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
- self.projects.append(project)
- self.endInsertRows()
- @Slot('QStringList')
- def addProjectByPath(self, str_list: list):
+ # Wait for all projects to be loaded (project.init_project is finished), whether successful or not
+ while not all(project.name != 'Loading...' for project in self.projects):
+ pass
+
+ settings.beginGroup('app')
+ settings.remove('projects') # clear the current saved list
+
+ settings.beginWriteArray('projects')
+ # Only correct ones (inner Stm32pio instance has been successfully constructed)
+ projects_to_save = [project for project in self.projects if project.project is not None]
+ for idx, project in enumerate(projects_to_save):
+ settings.setArrayIndex(idx)
+ # This ensures that we always save paths in pathlib form
+ settings.setValue('path', str(project.project.path))
+ settings.endArray()
+
+ settings.endGroup()
+ module_logger.info(f"{len(projects_to_save)} projects have been saved to Settings") # total amount
+
+ def saveInSettings(self) -> None:
+ """Spawn a thread to wait for all projects and save them in background"""
+ w = Worker(self._saveInSettings, logger=module_logger)
+ self.workers_pool.start(w)
+
+ def each_project_is_duplicate_of(self, path: str) -> Iterator[bool]:
"""
- Create, append to the end and save in QSettings a new ProjectListItem instance with a given QUrl path (typically
- is sent from the QML GUI).
+ Returns generator yielding an answer to the question "Is current project is a duplicate of one represented by a
+ given path?" for every project in this model, one by one.
+
+ Logic explanation: At a given time some projects (e.g., when we add a bunch of projects, recently added ones)
+ can be not instantiated yet so we cannot extract their project.path property and need to check before comparing.
+ In this case, simply evaluate strings. Also, samefile will even raise, if the given path doesn't exist.
"""
+ for list_item in self.projects:
+ try:
+ yield (list_item.project is not None and list_item.project.path.samefile(pathlib.Path(path))) or \
+ path == list_item.name # simply check strings if a path isn't available
+ except OSError:
+ yield False
- if len(str_list) > 1:
- for path_str in str_list:
- self.addProjectByPath([path_str])
- return
- elif len(str_list) == 0:
- module_logger.warning("No path were given")
- return
+ def addListItem(self, path: str, list_item_kwargs: Mapping[str, Any] = None, go_to_this: bool = False) -> None:
+ """
+ Create and append to the list tail a new ProjectListItem instance. This doesn't save in QSettings, it's an up to
+ the caller task (e.g. if we adding a bunch of projects, it make sense to store them once in the end).
- path_qurl = QUrl(str_list[0])
- if path_qurl.isLocalFile():
- path = path_qurl.toLocalFile()
- elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix
- path = str_list[0] # just use a source string
+ Args:
+ path: path as string
+ list_item_kwargs: keyword arguments passed to the ProjectListItem constructor
+ go_to_this: should we jump to the new project in GUI
+ """
+
+ if list_item_kwargs is not None:
+ list_item_kwargs = dict(list_item_kwargs) # shallow copy, dict makes it mutable
else:
- module_logger.error(f"Incorrect path: {str_list[0]}")
- return
+ list_item_kwargs = {}
- # When we add a bunch of projects (or in the general case, too) recently added ones can be not instantiated yet
- # so we need to check
- duplicate_index = next((idx for idx, list_item in enumerate(self.projects) if list_item.project is not None and
- list_item.project.path.samefile(pathlib.Path(path))), -1)
+ duplicate_index = next((idx for idx, is_duplicated in enumerate(self.each_project_is_duplicate_of(path))
+ if is_duplicated), -1)
if duplicate_index > -1:
+ # Just added project is already in the list so abort the addition
module_logger.warning(f"This project is already in the list: {path}")
- self.duplicateFound.emit(duplicate_index) # notify the GUI
+
+ # If some parameters were provided, merge them
+ proj_params = list_item_kwargs.get('project_kwargs', {}).get('parameters', {})
+ if len(proj_params):
+ self.projects[duplicate_index].logger.info(f"updating parameters from the CLI... {proj_params}")
+ self.projects[duplicate_index].run('save_config', [proj_params])
+
+ self.goToProject.emit(duplicate_index) # jump to the existing one
return
+ # Insert given path into the constructor args (do not use dict.update() as we have list value that we also want
+ # to "merge")
+ if len(list_item_kwargs) == 0:
+ list_item_kwargs = { 'project_args': [path] }
+ elif 'project_args' not in list_item_kwargs or len(list_item_kwargs['project_args']) == 0:
+ list_item_kwargs['project_args'] = [path]
+ else:
+ list_item_kwargs['project_args'][0] = path
+
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
- project = ProjectListItem(project_args=[path], parent=self)
+ # The project is ready to be appended to the model right after the main constructor (wrapper) finished. The
+ # underlying Stm32pio class will be initialized soon later in the dedicated thread
+ project = ProjectListItem(**list_item_kwargs)
self.projects.append(project)
self.endInsertRows()
- settings.beginGroup('app')
- settings.beginWriteArray('projects')
- settings.setArrayIndex(len(self.projects) - 1)
- settings.setValue('path', path)
- settings.endArray()
- settings.endGroup()
+ if go_to_this:
+ self.goToProject.emit(len(self.projects) - 1)
+
+
+ @Slot('QStringList')
+ def addProjectsByPaths(self, paths: List[str]):
+ """QUrl path (typically is sent from the QML GUI)"""
+ if len(paths) == 0:
+ module_logger.warning("No paths were given")
+ return
+ else:
+ for path_str in paths: # convert to strings
+ path_qurl = QUrl(path_str)
+ if path_qurl.isEmpty():
+ module_logger.warning(f"Given path is empty: {path_str}")
+ continue
+ elif path_qurl.isLocalFile(): # file://...
+ path: str = path_qurl.toLocalFile()
+ elif path_qurl.isRelative(): # this means that the path string is not starting with 'file://' prefix
+ path: str = path_str # just use a source string
+ else:
+ module_logger.error(f"Incorrect path: {path_str}")
+ continue
+ self.addListItem(path, list_item_kwargs={ 'parent': self })
+ self.saveInSettings()
+
@Slot(int)
def removeProject(self, index: int):
"""
Remove the project residing on the index both from the runtime list and QSettings
"""
- if index in range(len(self.projects)):
- self.beginRemoveRows(QModelIndex(), index, index)
-
- project = self.projects.pop(index)
- # It allows the project to be deconstructed (i.e. GC'ed) very soon, not at the app shutdown time
- project.deleteLater()
-
- self.endRemoveRows()
-
- settings.beginGroup('app')
-
- # Get current settings ...
- settings_projects_list = []
- for idx in range(settings.beginReadArray('projects')):
- settings.setArrayIndex(idx)
- settings_projects_list.append(settings.value('path'))
- settings.endArray()
-
- # ... drop the index ...
- settings_projects_list.pop(index)
+ if index not in range(len(self.projects)):
+ return
- # ... and overwrite the list. We don't use self.projects[i].project.path as there is a chance that 'path'
- # doesn't exist (e.g. not initialized for some reason project) but reuse the current values
- settings.remove('projects')
- settings.beginWriteArray('projects')
- for idx, path in enumerate(settings_projects_list):
- settings.setArrayIndex(idx)
- settings.setValue('path', path)
- settings.endArray()
+ self.beginRemoveRows(QModelIndex(), index, index)
+ project = self.projects.pop(index)
+ self.endRemoveRows()
- settings.endGroup()
+ if project.project is not None:
+ # Re-save the settings only if this project was correct and therefore is saved in the settings
+ self.saveInSettings()
+ # It allows the project to be deconstructed (i.e. GC'ed) very soon, not at the app shutdown time
+ project.deleteLater()
@@ -512,20 +589,20 @@ class Settings(QSettings):
'notifications': True
}
- def __init__(self, prefix: str, defaults: dict = None, qs_args: list = None, qs_kwargs: dict = None,
- external_triggers: Dict[str, Callable[[str], Any]] = None):
+ def __init__(self, prefix: str, defaults: Mapping[str, Any] = None, qs_args: List[Any] = None,
+ qs_kwargs: Mapping[str, Any] = None, external_triggers: Mapping[str, Callable[[str], Any]] = None):
"""
Args:
prefix: this prefix will always be added when get/set methods will be called so use it to group some most
- needed preferences under a single name. For example, prefix='app/params' while the list of users is
+ important preferences under a single name. For example, prefix='app/params' while the list of users is
located in 'app/users'
- defaults: dictionary of fallback values (under the prefix mentioned above) that will be used if there is no
+ defaults: mapping of fallback values (under the prefix mentioned above) that will be used if there is no
matching key in the storage
qs_args: positional arguments that will be passed to the QSettings constructor
qs_kwargs: keyword arguments that will be passed to the QSettings constructor
- external_triggers: dictionary where the keys are parameters names (under the prefix) and the values are
+ external_triggers: mapping where the keys are parameters names (under the prefix) and the values are
functions that will be called with the corresponding parameter value as the argument when the parameter
- is going to be set. Itis useful for a setup of the additional actions needed to be performed right after
+ is going to be set. It's useful to setup the additional actions needed to be performed right after
a certain parameter gets an update
"""
@@ -535,7 +612,7 @@ def __init__(self, prefix: str, defaults: dict = None, qs_args: list = None, qs_
super().__init__(*qs_args, **qs_kwargs)
self.prefix = prefix
- defaults = defaults if defaults is not None else self.DEFAULTS
+ defaults = defaults if defaults is not None else Settings.DEFAULTS
self.external_triggers = external_triggers if external_triggers is not None else {}
for key, value in defaults.items():
@@ -546,18 +623,16 @@ def __init__(self, prefix: str, defaults: dict = None, qs_args: list = None, qs_
def clear(self):
super().clear()
-
@Slot(str, result='QVariant')
def get(self, key):
value = self.value(self.prefix + key)
- # Windows registry storage is case insensitive so 'False' is saved as 'false' and we need to handle this
+ # On case insensitive file systems 'False' is saved as 'false' so we need to workaround this
if value == 'false':
value = False
elif value == 'true':
value = True
return value
-
@Slot(str, 'QVariant')
def set(self, key, value):
self.setValue(self.prefix + key, value)
@@ -566,17 +641,34 @@ def set(self, key, value):
self.external_triggers[key](value)
-def main():
- global module_logger
+def parse_args(args: list) -> Optional[argparse.Namespace]:
+ parser = argparse.ArgumentParser(description=inspect.cleandoc('''lala'''))
+
+ # Global arguments (there is also an automatically added '-h, --help' option)
+ parser.add_argument('--version', action='version', version=f"stm32pio v{stm32pio.app.__version__}")
+
+ parser.add_argument('-d', '--directory', dest='path', default=str(pathlib.Path.cwd()),
+ help="path to the project (current directory, if not given, but any other option should be specified then)")
+ parser.add_argument('-b', '--board', dest='board', default='', help="PlatformIO name of the board")
+
+ return parser.parse_args(args) if len(args) else None
+
+
+def main(sys_argv: List[str] = None) -> int:
+ if sys_argv is None:
+ sys_argv = sys.argv[1:]
+
+ args = parse_args(sys_argv)
+
module_log_handler = logging.StreamHandler()
module_log_handler.setFormatter(logging.Formatter("%(levelname)s %(funcName)s %(message)s"))
module_logger.addHandler(module_log_handler)
- module_logger.setLevel(logging.INFO)
+ module_logger.setLevel(logging.INFO) # set this again later after getting QSettings
module_logger.info('Starting stm32pio_gui...')
def qt_message_handler(mode, context, message):
"""
- Register this logging handler for the Qt stuff if your plarform doesn't provide the built-in one or if you want to
+ Register this logging handler for the Qt stuff if your platform doesn't provide a built-in one or if you want to
customize it
"""
if mode == QtInfoMsg:
@@ -593,57 +685,55 @@ def qt_message_handler(mode, context, message):
# Apparently Windows version of PySide2 doesn't have QML logging feature turn on so we fill this gap
# TODO: set up for other platforms too (separate console.debug, console.warn, etc.)
- qml_logger = logging.getLogger('qml')
+ qml_logger = logging.getLogger('stm32pio_gui.qml')
if platform.system() == 'Windows':
qml_log_handler = logging.StreamHandler()
qml_log_handler.setFormatter(logging.Formatter("[QML] %(levelname)s %(message)s"))
qml_logger.addHandler(qml_log_handler)
qInstallMessageHandler(qt_message_handler)
-
- # Most Linux distros should be linked with the QWidgets' QApplication instead of the QGuiApplication to enable
- # QtDialogs
+ # Most Linux distros should be "linked" with QWidgets' QApplication instead of QGuiApplication to enable QtDialogs
if platform.system() == 'Linux':
app = QApplication(sys.argv)
else:
app = QGuiApplication(sys.argv)
- # Used as a settings identifier too
+ # These are used as a settings identifier too
app.setOrganizationName('ussserrr')
app.setApplicationName('stm32pio')
app.setWindowIcon(QIcon(str(MODULE_PATH.joinpath('icons/icon.svg'))))
-
global settings
def verbose_setter(value):
+ """Use this to toggle the verbosity of all loggers at once"""
module_logger.setLevel(logging.DEBUG if value else logging.INFO)
- for project in projects_model.projects:
- project.logger.setLevel(logging.DEBUG if value else logging.INFO)
-
- settings = Settings(prefix='app/settings/',
- qs_kwargs={
- 'parent': app
- },
- external_triggers={
- 'verbose': verbose_setter
- })
- # settings.clear() # clear all
- # settings.remove('app/settings')
- # settings.remove('app/projects')
-
- module_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
- qml_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
- # if module_logger.isEnabledFor(logging.DEBUG):
- # module_logger.debug("App QSettings:")
- # for key in settings.allKeys():
- # module_logger.debug(f"{key}: {settings.value(key)} (type: {type(settings.value(key))})")
+ qml_logger.setLevel(logging.DEBUG if value else logging.INFO)
+ projects_logger.setLevel(logging.DEBUG if value else logging.INFO)
+ formatter.verbosity = stm32pio.util.Verbosity.VERBOSE if value else stm32pio.util.Verbosity.NORMAL
+
+ settings = Settings(prefix='app/settings/', qs_kwargs={ 'parent': app },
+ external_triggers={ 'verbose': verbose_setter })
+
+ # Use "singleton" real logger for all projects just wrapping it into the LoggingAdapter for every project
+ projects_logger = logging.getLogger('stm32pio_gui.projects')
+ projects_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
+ formatter = stm32pio.util.DispatchingFormatter(
+ general={
+ stm32pio.util.Verbosity.NORMAL: logging.Formatter("%(levelname)-8s %(message)s"),
+ stm32pio.util.Verbosity.VERBOSE: logging.Formatter(
+ f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s")
+ })
+ projects_logger_handler.setFormatter(formatter)
+ projects_logger.addHandler(projects_logger_handler)
+
+ verbose_setter(settings.get('verbose')) # set initial verbosity settings based on the saved state
settings.beginGroup('app')
- projects_paths = []
+ restored_projects_paths: List[str] = []
for index in range(settings.beginReadArray('projects')):
settings.setArrayIndex(index)
- projects_paths.append(settings.value('path'))
+ restored_projects_paths.append(settings.value('path'))
settings.endArray()
settings.endGroup()
@@ -654,17 +744,10 @@ def verbose_setter(value):
qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings')
projects_model = ProjectsList(parent=engine)
- boards = []
boards_model = QStringListModel(parent=engine)
- engine.rootContext().setContextProperty('Logging', {
- logging.getLevelName(logging.CRITICAL): logging.CRITICAL,
- logging.getLevelName(logging.ERROR): logging.ERROR,
- logging.getLevelName(logging.WARNING): logging.WARNING,
- logging.getLevelName(logging.INFO): logging.INFO,
- logging.getLevelName(logging.DEBUG): logging.DEBUG,
- logging.getLevelName(logging.NOTSET): logging.NOTSET
- })
+ engine.rootContext().setContextProperty('appVersion', stm32pio.app.__version__)
+ engine.rootContext().setContextProperty('Logging', stm32pio.util.logging_levels)
engine.rootContext().setContextProperty('projectsModel', projects_model)
engine.rootContext().setContextProperty('boardsModel', boards_model)
engine.rootContext().setContextProperty('appSettings', settings)
@@ -674,33 +757,52 @@ def verbose_setter(value):
main_window = engine.rootObjects()[0]
- # Getting PlatformIO boards can take long time when the PlatformIO cache is outdated but it is important to have
- # them before the projects list restoring, so we start a dedicated loading thread. We actually can add other
- # start-up operations here if there will be need to. Use the same Worker to spawn the thread at pool.
-
+ # Getting PlatformIO boards can take a long time when the PlatformIO cache is outdated but it is important to have
+ # them before the projects list is restored, so we start a dedicated loading thread. We actually can add other
+ # start-up operations here if there will be a need to. Use the same Worker class to spawn the thread at the pool
def loading():
- nonlocal boards
boards = ['None'] + stm32pio.util.get_platformio_boards('platformio')
-
- def loaded(_, success):
boards_model.setStringList(boards)
- projects = [ProjectListItem(project_args=[path], from_startup=True, parent=projects_model) for path in projects_paths]
- for p in projects:
- projects_model.addProject(p)
- main_window.backendLoaded.emit() # inform the GUI
+
+ def loaded(_: str, success: bool):
+ try:
+ # Qt objects cannot be parented from the different thread so we restore the projects list in the main thread
+ for path in restored_projects_paths:
+ projects_model.addListItem(path, go_to_this=False, list_item_kwargs={
+ 'from_startup': True,
+ 'parent': projects_model
+ })
+
+ # At the end, append (or jump to) a CLI-provided project, if there is one
+ if args is not None:
+ list_item_kwargs = {
+ 'from_startup': True,
+ 'parent': projects_model
+ }
+ if args.board:
+ list_item_kwargs['project_kwargs'] = { 'parameters': { 'project': { 'board': args.board } } } # pizdec konechno...
+ projects_model.addListItem(str(pathlib.Path(args.path)), go_to_this=True,
+ list_item_kwargs=list_item_kwargs)
+ projects_model.saveInSettings()
+ except Exception:
+ stm32pio.util.log_current_exception(module_logger)
+ success = False
+
+ main_window.backendLoaded.emit(success) # inform the GUI
loader = Worker(loading, logger=module_logger)
loader.finished.connect(loaded)
QThreadPool.globalInstance().start(loader)
-
return app.exec_()
-# Globals
-module_logger = logging.getLogger(__name__) # use it as a console logger for whatever you want to, typically not
- # related to the concrete project
+# [necessary] globals
+module_logger = logging.getLogger(f'stm32pio_gui.{__name__}') # use it as a console logger for whatever you want to,
+ # typically not related to the concrete project
+projects_logger_handler = BuffersDispatchingHandler() # a storage of the buffers for the logging messages of all
+ # current projects (see its docs for more info)
settings = QSettings() # placeholder, will be replaced in main()
diff --git a/stm32pio_gui/icons/LICENSE b/stm32pio_gui/icons/LICENSE
index 17ec736..5e98f5c 100644
--- a/stm32pio_gui/icons/LICENSE
+++ b/stm32pio_gui/icons/LICENSE
@@ -3,5 +3,6 @@ Icons by
- Flat Icons
- Google
- Pixel Perfect
+ - Freepik
from www.flaticon.com
diff --git a/stm32pio_gui/icons/edit.svg b/stm32pio_gui/icons/edit.svg
new file mode 100644
index 0000000..8f94ea6
--- /dev/null
+++ b/stm32pio_gui/icons/edit.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stm32pio_gui/icons/trash-bin.svg b/stm32pio_gui/icons/trash-bin.svg
new file mode 100644
index 0000000..3a6c3a0
--- /dev/null
+++ b/stm32pio_gui/icons/trash-bin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stm32pio_gui/main.qml b/stm32pio_gui/main.qml
index 5e08c4f..fa5ca46 100644
--- a/stm32pio_gui/main.qml
+++ b/stm32pio_gui/main.qml
@@ -1,5 +1,6 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
+import QtQml.Models 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
import QtQuick.Dialogs 1.3 as Dialogs
@@ -23,8 +24,19 @@ ApplicationWindow {
/*
Notify the front about the end of an initial loading
*/
- signal backendLoaded()
- onBackendLoaded: loadingOverlay.close()
+ signal backendLoaded(bool success)
+ onBackendLoaded: {
+ loadingOverlay.close();
+ if (!success) {
+ backendLoadingErrorDialog.open();
+ }
+ }
+ Dialogs.MessageDialog {
+ id: backendLoadingErrorDialog
+ title: 'Warning'
+ text: "There was an error during the initialization of the Python backend. Please see the terminal output for more details"
+ icon: Dialogs.StandardIcon.Warning
+ }
Popup {
id: loadingOverlay
visible: true
@@ -79,7 +91,7 @@ ApplicationWindow {
}
Item { Layout.preferredWidth: 140 } // spacer
Text {
- Layout.preferredWidth: 250 // Detected recursive rearrange. Aborting after two iterations
+ Layout.preferredWidth: 250 // TODO: a cause of "Detected recursive rearrange. Aborting after two iterations"
wrapMode: Text.Wrap
color: 'dimgray'
text: "Get messages about completed project actions when the app is in the background"
@@ -122,18 +134,19 @@ ApplicationWindow {
standardButtons: Dialogs.StandardButton.Close
ColumnLayout {
Rectangle {
- width: 250
- height: 100
+ width: 280
+ height: aboutDialogTextArea.implicitHeight
TextArea {
+ id: aboutDialogTextArea
width: parent.width
- height: parent.height
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
textFormat: TextEdit.RichText
horizontalAlignment: TextEdit.AlignHCenter
verticalAlignment: TextEdit.AlignVCenter
- text: `2018 - 2020 © ussserrr
+ text: `ver. ${appVersion}
+ 2018 - 2020 © ussserrr
GitHub
Powered by Python3, PlatformIO, Qt for Python, FlatIcons and other awesome technologies`
onLinkActivated: {
@@ -157,8 +170,6 @@ ApplicationWindow {
QML-side implementation details to the backend we define this helper function that counts and stores
a number of widgets currently loaded for each project in model and informs the Qt-side right after all
necessary components become ready.
-
- TODO: should be remade to use Python id() as a unique identifier, see TODO.md
*/
readonly property var initInfo: ({})
function setInitInfo(projectIndex) {
@@ -174,9 +185,17 @@ ApplicationWindow {
}
}
+ Connections {
+ target: projectsModel
+ onGoToProject: projectsListView.currentIndex = indexToGo
+ }
function removeCurrentProject() {
const indexToRemove = projectsListView.currentIndex;
indexToRemove === 0 ? projectsListView.incrementCurrentIndex() : projectsListView.decrementCurrentIndex();
+
+ // Need to manually unload the dynamic component to prevent annoying "TypeError: Cannot read property 'XXX' of null" messages
+ projectsWorkspaceView.children[indexToRemove].sourceComponent = undefined;
+
projectsModel.removeProject(indexToRemove);
}
@@ -225,11 +244,11 @@ ApplicationWindow {
}
onDropped: {
if (drop.urls.length) {
- // We need to convert to the array of strings as typeof(drop.urls) === 'object'
- projectsModel.addProjectByPath(Object.keys(drop.urls).map(u => drop.urls[u]));
+ // We need to convert to an array of strings till typeof(drop.urls) === 'object'
+ projectsModel.addProjectsByPaths(Object.keys(drop.urls).map(u => drop.urls[u]));
} else if (drop.text) {
- // Wrap into the array for consistency
- projectsModel.addProjectByPath([drop.text]);
+ // Wrap into an array for consistency
+ projectsModel.addProjectsByPaths([drop.text]);
} else {
console.log("Incorrect drag'n'drop event");
}
@@ -264,17 +283,23 @@ ApplicationWindow {
highlightMoveDuration: 0 // turn off animations
highlightMoveVelocity: -1
- model: projectsModel // backend-side
- delegate: Component {
+ model: DelegateModel {
/*
- (See setInitInfo docs) One of the two main widgets representing the project. Use Loader component
- as it can give us the relible timestamp of all its children loading completion (unlike Component.onCompleted)
+ Use DelegateModel as it has a feature to always preserve specified list items in memory so we can store an actual state
+ directly in the delegate
*/
- id: listViewDelegate
- Loader {
- onLoaded: setInitInfo(index)
+ model: projectsModel // backend-side
+ delegate: Loader {
+ /*
+ (See setInitInfo docs) One of the two main widgets representing the project. Use Loader component
+ as it can give us the relible timestamp of all its children loading completion (unlike Component.onCompleted)
+ */
+ onLoaded: {
+ setInitInfo(index);
+ DelegateModel.inPersistedItems = 1;
+ }
sourceComponent: RowLayout {
- property bool initLoading: true // initial waiting for the backend-side TODO: do not store state in the delegate!
+ property bool initLoading: true // initial waiting for the backend-side
readonly property ProjectListItem project: projectsModel.get(index)
Connections {
target: project
@@ -321,8 +346,14 @@ ApplicationWindow {
}
if (Qt.colorEqual(projectCurrentStage.color, 'darkgray')) {
projectCurrentStage.color = 'black';
+ }
+ if (runningOrFinished.currentIndex === 1) { // TODO: ugly
runningOrFinished.visible = false;
}
+ } else {
+ if (Qt.colorEqual(projectCurrentStage.color, 'black')) {
+ projectCurrentStage.color = 'darkgray';
+ }
}
}
}
@@ -388,7 +419,6 @@ ApplicationWindow {
y: parent.y
width: parent.width
height: parent.height
- enabled: !parent.initLoading
onClicked: projectsListView.currentIndex = index
}
}
@@ -398,7 +428,7 @@ ApplicationWindow {
Labs.FolderDialog {
id: addProjectFolderDialog
currentFolder: Labs.StandardPaths.standardLocations(Labs.StandardPaths.HomeLocation)[0]
- onAccepted: projectsModel.addProjectByPath([folder])
+ onAccepted: projectsModel.addProjectsByPaths([folder])
}
footerPositioning: ListView.OverlayFooter
footer: Rectangle { // Probably should use Pane but need to override default window color then
@@ -409,11 +439,6 @@ ApplicationWindow {
RowLayout {
id: listFooter
anchors.centerIn: parent
- Connections {
- target: projectsModel
- // Just added project is already in the list so abort the addition and jump to the existing one
- onDuplicateFound: projectsListView.currentIndex = duplicateIndex
- }
Button {
text: 'Add'
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
@@ -456,549 +481,575 @@ ApplicationWindow {
Repeater {
// Use similar to ListView pattern (same projects model, Loader component)
model: projectsModel
- delegate: Component {
- Loader {
- property int projectIndex: index // binding so will be automatically updated on change
- onLoaded: setInitInfo(index)
- /*
- Use another one StackLayout to separate Project initialization "screen" and Main one
- */
- sourceComponent: StackLayout {
- id: mainOrInitScreen
- currentIndex: -1 // at widget creation we do not show main nor init screen
+ delegate: Loader {
+ property int projectIndex: index // binding so will be automatically updated on change
+ onLoaded: setInitInfo(index)
+ /*
+ Use another one StackLayout to separate Project initialization "screen" and Main one
+ */
+ sourceComponent: StackLayout {
+ id: mainOrInitScreen
+ currentIndex: -1 // at widget creation we do not show main nor init screen
- Layout.fillWidth: true
- Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.fillHeight: true
- readonly property ProjectListItem project: projectsModel.get(index)
+ readonly property ProjectListItem project: projectsModel.get(index)
- /*
- State retrieving procedure is relatively expensive (many IO operations) so we optimize it by getting the state
- only in certain situations (see Component.onCompleted below) and caching a value in the local varible. Then, all
- widgets can pick up this value as many times as they want while not abusing the real property getter. Such a subscription
- can be established by the creation of a local reference to the cache and listening to the change event like this:
-
- property var stateCachedNotifier: stateCached
- onStateCachedNotifierChanged: {
- // use stateCached there
- }
- */
- signal handleState()
- property var stateCached: ({})
- onHandleState: {
- if (mainWindow.active && // the app got foreground
- projectIndex === projectsWorkspaceView.currentIndex && // only for the current list item
- !projectIncorrectDialog.visible && // on macOS, there is an animation effect so this property isn't updated
- // immediately and the state can be retrieved several times and some flaws
- // may appear. Workaround - is to have a dedicated flag and update it
- // manually but this isn't very elegant solution
- project.currentAction === ''
- ) {
- const state = project.state;
- stateCached = state;
+ /*
+ State retrieving procedure is relatively expensive (many IO operations) so we optimize it by getting the state
+ only in certain situations (see Component.onCompleted below) and caching a value in the local varible. Then, all
+ widgets can pick up this value as many times as they want while not abusing the real property getter. Such a subscription
+ can be established by the creation of a local reference to the cache and listening to the change event like this:
- project.stageChanged(); // side-effect: update the stage at the same time
- }
- }
- Component.onCompleted: {
- // Several events lead to a single handler
- project.stateChanged.connect(handleState); // the model has notified about the change
- projectsWorkspaceView.currentIndexChanged.connect(handleState); // the project was selected in the list
- mainWindow.activeChanged.connect(handleState); // the app window has got (or lost, filter in the handler) the focus
+ property var stateCachedNotifier: stateCached
+ onStateCachedNotifierChanged: {
+ // use stateCached there
+ }
+ */
+ signal handleState()
+ property var stateCached: ({})
+ onHandleState: {
+ if (mainWindow.active && // the app got foreground
+ projectIndex === projectsWorkspaceView.currentIndex && // only for the current list item
+ !projectIncorrectDialog.visible && // on macOS, there is an animation effect so this property isn't updated
+ // immediately and the state can be retrieved several times and some flaws
+ // may appear. Workaround - is to have a dedicated flag and update it
+ // manually but this isn't very elegant solution
+ project.currentAction === ''
+ ) {
+ const state = project.state;
+ stateCached = state;
+
+ project.stageChanged(); // side-effect: update the stage at the same time
}
+ }
+ Component.onCompleted: {
+ // Several events lead to a single handler
+ project.stateChanged.connect(handleState); // the model has notified about the change
+ projectsWorkspaceView.currentIndexChanged.connect(handleState); // the project was selected in the list
+ mainWindow.activeChanged.connect(handleState); // the app window has got (or lost, filter in the handler) the focus
+ }
- Connections {
- target: project
- // Currently, this event is equivalent to the complete initialization of the backend side of the project
- onNameChanged: {
- const state = project.state;
- const completedStages = Object.keys(state).filter(stateName => state[stateName]);
- if (completedStages.length === 1 && completedStages[0] === 'EMPTY') {
- setupScreenLoader.active = true;
- mainOrInitScreen.currentIndex = 0; // show init dialog
- } else {
- mainOrInitScreen.currentIndex = 1; // show main view
+ Connections {
+ target: project
+ // Currently, this event is equivalent to the complete initialization of the backend side of the project
+ onNameChanged: {
+ const state = project.state;
+ stateCached = state;
+ const completedStages = Object.keys(state).filter(stateName => state[stateName]);
+ if (completedStages.length === 1 && completedStages[0] === 'EMPTY') {
+ setupScreenLoader.active = true;
+ mainOrInitScreen.currentIndex = 0; // show init dialog
+ } else {
+ const config = project.config;
+ if (Object.keys(config['project']).length && !config['project']['board']) {
+ project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO ' +
+ 'project creation. You can set it in "stm32pio.ini" file in the project directory',
+ Logging.WARNING);
}
+ mainOrInitScreen.currentIndex = 1; // show main view
}
}
+ }
- // property bool projectIncorrectDialogIsOpen: false
- Dialogs.MessageDialog {
- id: projectIncorrectDialog
- visible: Object.keys(stateCached).length && !stateCached['INIT_ERROR'] && !stateCached['EMPTY']
- text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
- The project will be removed from the app. It will not affect any real content`
- icon: Dialogs.StandardIcon.Critical
- onAccepted: removeCurrentProject()
- }
+ Dialogs.MessageDialog {
+ id: projectIncorrectDialog
+ visible: Object.keys(stateCached).length && [
+ 'LOADING', // ignore transitional state
+ 'INIT_ERROR', // we have another view for this state, skip
+ 'EMPTY' // true if .ioc file is present, false otherwise
+ ].every(key => !stateCached[key])
+ text: `The project was modified outside of the stm32pio and .ioc file is no longer present.
+ The project will be removed from the app. It will not affect any real content`
+ icon: Dialogs.StandardIcon.Warning
+ onAccepted: removeCurrentProject()
+ }
- /*
- Index: 0. Project initialization "screen"
+ /*
+ Index: 0. Project initialization "screen"
- Prompt a user to perform initial setup
- */
- Loader {
- id: setupScreenLoader
- active: false
- sourceComponent: Column {
- Text {
- text: "To complete initialization you can provide the PlatformIO name of the board"
- padding: 10
- }
- Row {
- padding: 10
- spacing: 10
- ComboBox {
- id: board
- width: 200
- editable: true
- model: boardsModel // backend-side (simple string model)
- textRole: 'display'
- onAccepted: focus = false
- onActivated: focus = false
- onFocusChanged: {
- if (focus) {
- selectAll();
- } else {
- if (find(editText) === -1) {
- editText = textAt(0); // should be 'None' at index 0
- }
+ Prompt a user to perform initial setup
+ */
+ Loader {
+ id: setupScreenLoader
+ active: false
+ sourceComponent: Column {
+ Text {
+ text: "To complete initialization you can provide the PlatformIO name of the board"
+ padding: 10
+ }
+ Row {
+ padding: 10
+ spacing: 10
+ ComboBox {
+ id: board
+ width: 200
+ editable: true
+ model: boardsModel // backend-side (simple string model)
+ textRole: 'display'
+ onAccepted: focus = false
+ onActivated: focus = false
+ onFocusChanged: {
+ if (focus) {
+ selectAll();
+ } else {
+ if (find(editText) === -1) {
+ editText = textAt(0); // should be 'None' at index 0
}
}
}
- /*
- Trigger full run
- */
- CheckBox {
- id: runCheckBox
- text: 'Run'
- enabled: false
- ToolTip {
- visible: runCheckBox.hovered // not working on Linux (Manjaro LXQt)
- Component.onCompleted: {
- // Form the tool tip text using action names
- const actions = [];
- for (let i = projActionsModel.statefulActionsStartIndex; i < projActionsModel.count; ++i) {
- actions.push(`${projActionsModel.get(i).name} `);
- }
- text = `Do: ${actions.join(' → ')}`;
+ Component.onCompleted: {
+ // Board can be already specified in the config, in this case we should paste it
+ const config = project.config;
+ if (Object.keys(config['project']).length && config['project']['board']) {
+ editText = config['project']['board'];
+ }
+ forceActiveFocus();
+ }
+ }
+ /*
+ Trigger full run
+ */
+ CheckBox {
+ id: runCheckBox
+ text: 'Run'
+ enabled: false
+ ToolTip {
+ visible: runCheckBox.hovered // not working on Linux (Manjaro LXQt)
+ Component.onCompleted: {
+ // Form the tool tip text using action names
+ const actions = [];
+ for (let i = projActionsModel.statefulActionsStartIndex; i < projActionsModel.count; ++i) {
+ actions.push(`${projActionsModel.get(i).name} `);
}
+ text = `Do: ${actions.join(' → ')}`;
}
- Connections {
- target: board
- onFocusChanged: {
- if (!board.focus) {
- if (board.editText === board.textAt(0)) { // should be 'None' at index 0
- runCheckBox.checked = false;
- runCheckBox.enabled = false;
- } else {
- runCheckBox.enabled = true;
- }
+ }
+ Connections {
+ target: board
+ onFocusChanged: {
+ if (!board.focus) {
+ if (board.editText === board.textAt(0)) { // should be 'None' at index 0
+ runCheckBox.checked = false;
+ runCheckBox.enabled = false;
+ } else {
+ runCheckBox.enabled = true;
}
}
}
}
- CheckBox {
- id: openEditor
- text: 'Open editor'
- ToolTip {
- text: "Start the editor specified in the Settings after the completion"
- visible: openEditor.hovered // not working on Linux (Manjaro LXQt)
- }
+ }
+ CheckBox {
+ id: openEditor
+ text: 'Open editor'
+ ToolTip {
+ text: "Start the editor specified in the Settings after the completion"
+ visible: openEditor.hovered // not working on Linux (Manjaro LXQt)
}
}
- Button {
- text: 'OK'
- topInset: 15
- leftInset: 10
- topPadding: 20
- leftPadding: 18
- onClicked: {
- // All 'run' operations will be queued by the backend
- project.run('save_config', [{
- 'project': {
- 'board': board.editText === board.textAt(0) ? '' : board.editText
- }
- }]);
- if (board.editText === board.textAt(0)) {
- project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO ' +
- 'project creation. You can set it in "stm32pio.ini" file in the project directory',
- Logging.WARNING);
- }
-
- if (runCheckBox.checked) {
- for (let i = projActionsModel.statefulActionsStartIndex + 1; i < projActionsModel.count; ++i) {
- project.run(projActionsModel.get(i).action, []);
- }
+ }
+ Button {
+ text: 'OK'
+ topInset: 15
+ leftInset: 10
+ topPadding: 20
+ leftPadding: 18
+ onClicked: {
+ // All 'run' operations will be queued by the backend
+ project.run('save_config', [{
+ 'project': {
+ 'board': board.editText === board.textAt(0) ? '' : board.editText
}
+ }]);
+ if (board.editText === board.textAt(0)) {
+ project.logAdded('WARNING STM32 PlatformIO board is not specified, it will be needed on PlatformIO ' +
+ 'project creation. You can set it in "stm32pio.ini" file in the project directory',
+ Logging.WARNING);
+ }
- if (openEditor.checked) {
- project.run('start_editor', [settings.get('editor')]);
+ if (runCheckBox.checked) {
+ for (let i = projActionsModel.statefulActionsStartIndex + 1; i < projActionsModel.count; ++i) {
+ project.run(projActionsModel.get(i).action, []);
}
+ }
- mainOrInitScreen.currentIndex = 1; // go to main screen
- setupScreenLoader.sourceComponent = undefined; // destroy init screen
+ if (openEditor.checked) {
+ project.run('start_editor', [settings.get('editor')]);
}
+
+ mainOrInitScreen.currentIndex = 1; // go to main screen
+ setupScreenLoader.sourceComponent = undefined; // destroy init screen
}
}
}
+ }
+
+ /*
+ Index: 1. Main "screen"
+ */
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
/*
- Index: 1. Main "screen"
+ Show this or action buttons
*/
- ColumnLayout {
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- /*
- Show this or action buttons
- */
- Text {
- id: initErrorMessage
- visible: stateCached['INIT_ERROR'] ? true : false // explicitly convert to boolean
- padding: 10
- text: "The project cannot be initialized "
- color: 'indianred'
- }
+ Text {
+ id: initErrorMessage
+ visible: stateCached['INIT_ERROR'] ? true : false // explicitly convert to boolean
+ padding: 10
+ text: "The project cannot be initialized "
+ color: 'indianred'
+ }
- /*
- The core widget - a group of buttons mapping all main actions that can be performed on the given project.
- They also serve the project state displaying - each button indicates a stage associated with it:
- - green (and green glow): done
- - yellow: in progress right now
- - red glow: an error has occured during the last execution
- */
- RowLayout {
- id: projActionsRow
- visible: stateCached['INIT_ERROR'] ? false : true
- Layout.fillWidth: true
- Layout.bottomMargin: 7
- z: 1 // for the glowing animation
- Repeater {
- model: ListModel {
- id: projActionsModel
- readonly property int statefulActionsStartIndex: 2
- ListElement {
- name: 'Clean'
- action: 'clean'
- tooltip: "WARNING: this will delete ALL content of the project folder \
- except the current .ioc file and clear all logs"
- }
- ListElement {
- name: 'Open editor'
- action: 'start_editor'
- margin: 15 // margin to visually separate first 2 actions as they don't represent any stage
- }
- ListElement {
- name: 'Initialize'
- stageRepresented: 'INITIALIZED' // the project stage this button is representing
- action: 'save_config'
- }
- ListElement {
- name: 'Generate'
- stageRepresented: 'GENERATED'
- action: 'generate_code'
- }
- ListElement {
- name: 'Init PlatformIO'
- stageRepresented: 'PIO_INITIALIZED'
- action: 'pio_init'
- }
- ListElement {
- name: 'Patch'
- stageRepresented: 'PATCHED'
- action: 'patch'
- }
- ListElement {
- name: 'Build'
- stageRepresented: 'BUILT'
- action: 'build'
- }
+ /*
+ The core widget - a group of buttons mapping all main actions that can be performed on the given project.
+ They also serve the project state displaying - each button indicates a stage associated with it:
+ - green (and green glow): done
+ - yellow: in progress right now
+ - red glow: an error has occured during the last execution
+ */
+ RowLayout {
+ id: projActionsRow
+ visible: stateCached['INIT_ERROR'] ? false : true
+ Layout.fillWidth: true
+ Layout.bottomMargin: 7
+ z: 1 // for the glowing animation
+ Repeater {
+ model: ListModel {
+ id: projActionsModel
+ readonly property int statefulActionsStartIndex: 2
+ ListElement {
+ name: 'Clean'
+ action: 'clean'
+ icon: './icons/trash-bin.svg'
+ tooltip: "WARNING: this will delete ALL content of the project folder \
+ except the current .ioc file and clear all logs"
+ }
+ ListElement {
+ name: 'Open editor'
+ action: 'start_editor'
+ icon: './icons/edit.svg'
+ margin: 15 // margin to visually separate first 2 actions as they don't represent any stage
+ }
+ ListElement {
+ name: 'Initialize'
+ stageRepresented: 'INITIALIZED' // the project stage this button is representing
+ action: 'save_config'
+ tooltip: "Saves the current configuration to the config file stm32pio.ini "
+ }
+ ListElement {
+ name: 'Generate'
+ stageRepresented: 'GENERATED'
+ action: 'generate_code'
+ }
+ ListElement {
+ name: 'Init PlatformIO'
+ stageRepresented: 'PIO_INITIALIZED'
+ action: 'pio_init'
}
- delegate: Button {
- text: model.name
- Layout.rightMargin: model.margin
- property bool shouldBeHighlighted: false // highlight on mouse over
- property bool shouldBeHighlightedWhileRunning: false // distinguish actions picked out for the batch run
- property int buttonIndex: -1
+ ListElement {
+ name: 'Patch'
+ stageRepresented: 'PATCHED'
+ action: 'patch'
+ }
+ ListElement {
+ name: 'Build'
+ stageRepresented: 'BUILT'
+ action: 'build'
+ }
+ }
+ delegate: Button {
+ text: model.name
+ Layout.rightMargin: model.margin
+ property bool shouldBeHighlighted: false // highlight on mouse over
+ property bool shouldBeHighlightedWhileRunning: false // distinguish actions picked out for the batch run
+ property int buttonIndex: -1
+ Component.onCompleted: {
+ buttonIndex = index;
+ background.border.color = 'dimgray';
+ }
+ display: model.icon ? AbstractButton.IconOnly : AbstractButton.TextOnly
+ icon.source: model.icon || ''
+ ToolTip {
+ visible: mouseArea.containsMouse
Component.onCompleted: {
- buttonIndex = index;
- background.border.color = 'dimgray';
- }
- ToolTip {
- visible: mouseArea.containsMouse
- Component.onCompleted: {
- if (model.tooltip) {
- text = model.tooltip;
- } else {
- this.destroy();
- }
+ text = '';
+ if (model.icon) {
+ text += model.name;
}
- }
- onClicked: {
- // JS array cannot be attached to a ListElement (at least in a non-hacky manner) so we fill arguments here
- const args = [];
- switch (model.action) {
- case 'start_editor':
- args.push(settings.get('editor'));
- break;
- case 'clean':
- log.clear();
- break;
- default:
- break;
+ if (model.tooltip) {
+ text += text ? ` ${model.tooltip}` : model.tooltip;
+ }
+ if (!model.icon && !model.tooltip) {
+ this.destroy();
}
- project.run(model.action, args);
}
- /*
- As the button reflects relatively complex logic it's easier to maintain using the state machine technique.
- We define states and allowed transitions between them, all other stuff is managed by the DSM framework.
- You can find the graphical diagram somewhere in the docs
- */
- DSM.StateMachine {
- initialState: main // start position
- running: true // run immediately
- DSM.State {
- id: main
- initialState: normal
- DSM.SignalTransition {
- targetState: disabled
- signal: project.actionStarted
- }
- DSM.SignalTransition {
- targetState: highlighted
- signal: shouldBeHighlightedChanged
- guard: shouldBeHighlighted // go only if...
- }
- onEntered: {
- enabled = true;
- palette.buttonText = 'black';
- }
- DSM.State {
- id: normal
- DSM.SignalTransition {
- targetState: stageFulfilled
- signal: stateCachedChanged
- guard: stateCached[model.stageRepresented] ? true : false // explicitly convert to boolean
- }
- onEntered: {
- palette.button = 'lightgray';
- }
- }
- DSM.State {
- id: stageFulfilled
- DSM.SignalTransition {
- targetState: normal
- signal: stateCachedChanged
- guard: stateCached[model.stageRepresented] ? false : true
- }
- onEntered: {
- palette.button = 'lightgreen';
- }
- }
- DSM.HistoryState {
- id: mainHistory
- defaultState: normal
- }
+ }
+ onClicked: {
+ // JS array cannot be attached to a ListElement (at least in a non-hacky manner) so we fill arguments here
+ const args = [];
+ switch (model.action) {
+ case 'start_editor':
+ args.push(settings.get('editor'));
+ break;
+ case 'clean':
+ log.clear();
+ break;
+ default:
+ break;
+ }
+ project.run(model.action, args);
+ }
+ /*
+ As the button reflects relatively complex logic it's easier to maintain using the state machine technique.
+ We define states and allowed transitions between them, all other stuff is managed by the DSM framework.
+ You can find the graphical diagram somewhere in the docs
+ */
+ DSM.StateMachine {
+ initialState: main // start position
+ running: true // run immediately
+ DSM.State {
+ id: main
+ initialState: normal
+ DSM.SignalTransition {
+ targetState: disabled
+ signal: project.actionStarted
+ }
+ DSM.SignalTransition {
+ targetState: highlighted
+ signal: shouldBeHighlightedChanged
+ guard: shouldBeHighlighted // go only if...
+ }
+ onEntered: {
+ enabled = true;
+ palette.buttonText = 'black';
}
DSM.State {
- // Activates/deactivates additional properties (such as color or border) on some conditions
- // (e.g. some action is currently running), see onEntered, onExited
- id: disabled
+ id: normal
DSM.SignalTransition {
- targetState: mainHistory
- signal: project.actionFinished
+ targetState: stageFulfilled
+ signal: stateCachedChanged
+ guard: stateCached[model.stageRepresented] ? true : false // explicitly convert to boolean
}
onEntered: {
- enabled = false;
- palette.buttonText = 'darkgray';
- if (project.currentAction === model.action) {
- palette.button = 'gold';
- }
- if (shouldBeHighlightedWhileRunning) {
- background.border.width = 2;
- }
- }
- onExited: {
- // Erase highlighting if this action is last in the series or at all
- if (project.currentAction === model.action &&
- shouldBeHighlightedWhileRunning &&
- (buttonIndex === (projActionsModel.count - 1) ||
- projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false)
- ) {
- for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) {
- projActionsRow.children[i].shouldBeHighlightedWhileRunning = false;
- projActionsRow.children[i].background.border.width = 0;
- }
- }
+ palette.button = 'lightgray';
}
}
DSM.State {
- id: highlighted
+ id: stageFulfilled
DSM.SignalTransition {
- targetState: mainHistory
- signal: shouldBeHighlightedChanged
- guard: !shouldBeHighlighted
+ targetState: normal
+ signal: stateCachedChanged
+ guard: stateCached[model.stageRepresented] ? false : true
}
onEntered: {
- palette.button = Qt.lighter('lightgreen', 1.2);
- palette.buttonText = 'dimgray';
+ palette.button = 'lightgreen';
}
}
+ DSM.HistoryState {
+ id: mainHistory
+ defaultState: normal
+ }
}
- /*
- Detect modifier keys using overlayed MouseArea:
- - Ctrl (Cmd): start the editor after the action(s)
- - Shift: batch actions run
- */
- MouseArea {
- id: mouseArea
- anchors.fill: parent
- hoverEnabled: true
- property bool ctrlPressed: false
- property bool ctrlPressedLastState: false
- property bool shiftPressed: false
- property bool shiftPressedLastState: false
- function shiftHandler() {
- // manage the appearance of all [stateful] buttons prior this one
- for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) {
- projActionsRow.children[i].shouldBeHighlighted = shiftPressed;
+ DSM.State {
+ // Activates/deactivates additional properties (such as color or border) on some conditions
+ // (e.g. some action is currently running), see onEntered, onExited
+ id: disabled
+ DSM.SignalTransition {
+ targetState: mainHistory
+ signal: project.actionFinished
+ }
+ onEntered: {
+ enabled = false;
+ palette.buttonText = 'darkgray';
+ if (project.currentAction === model.action) {
+ palette.button = 'gold';
+ }
+ if (shouldBeHighlightedWhileRunning) {
+ background.border.width = 2;
}
}
- onClicked: {
- if (shiftPressed && buttonIndex >= projActionsModel.statefulActionsStartIndex) {
+ onExited: {
+ // Erase highlighting if this action is last in the series or at all
+ if (project.currentAction === model.action &&
+ shouldBeHighlightedWhileRunning &&
+ (buttonIndex === (projActionsModel.count - 1) ||
+ projActionsRow.children[buttonIndex + 1].shouldBeHighlightedWhileRunning === false)
+ ) {
for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) {
- projActionsRow.children[i].shouldBeHighlighted = false;
- projActionsRow.children[i].shouldBeHighlightedWhileRunning = true;
- }
- for (let i = projActionsModel.statefulActionsStartIndex; i < buttonIndex; ++i) {
- project.run(projActionsModel.get(i).action, []);
+ projActionsRow.children[i].shouldBeHighlightedWhileRunning = false;
+ projActionsRow.children[i].background.border.width = 0;
}
}
- parent.clicked(); // pass the event to the underlying button though all work can be done in-place
- if (ctrlPressed && model.action !== 'start_editor') {
- project.run('start_editor', [settings.get('editor')]);
- }
}
- onPositionChanged: {
- ctrlPressed = mouse.modifiers & Qt.ControlModifier; // bitwise AND
- if (ctrlPressedLastState !== ctrlPressed) {
- ctrlPressedLastState = ctrlPressed;
- }
-
- shiftPressed = mouse.modifiers & Qt.ShiftModifier; // bitwise AND
- if (shiftPressedLastState !== shiftPressed) { // reduce a number of unnecessary shiftHandler() calls
- shiftPressedLastState = shiftPressed;
- shiftHandler();
- }
+ }
+ DSM.State {
+ id: highlighted
+ DSM.SignalTransition {
+ targetState: mainHistory
+ signal: shouldBeHighlightedChanged
+ guard: !shouldBeHighlighted
}
onEntered: {
- if (model.action !== 'start_editor') {
- let preparedText = `Ctrl -click to open the editor specified in the Settings
- after the operation`;
- if (buttonIndex >= projActionsModel.statefulActionsStartIndex) {
- preparedText +=
- `, Shift -click to perform all actions prior this one (including).
- Ctrl -Shift -click for both`;
- }
- statusBar.text = preparedText;
- }
+ palette.button = Qt.lighter('lightgreen', 1.2);
+ palette.buttonText = 'dimgray';
}
- onExited: {
- statusBar.text = '';
-
- ctrlPressed = false;
- ctrlPressedLastState = false;
-
- if (shiftPressed || shiftPressedLastState) {
- shiftPressed = false;
- shiftPressedLastState = false;
- shiftHandler();
+ }
+ }
+ /*
+ Detect modifier keys using overlayed MouseArea:
+ - Ctrl (Cmd): start the editor after the action(s)
+ - Shift: batch actions run
+ */
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ property bool ctrlPressed: false
+ property bool ctrlPressedLastState: false
+ property bool shiftPressed: false
+ property bool shiftPressedLastState: false
+ function shiftHandler() {
+ // manage the appearance of all [stateful] buttons prior this one
+ for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) {
+ projActionsRow.children[i].shouldBeHighlighted = shiftPressed;
+ }
+ }
+ onClicked: {
+ if (shiftPressed && buttonIndex >= projActionsModel.statefulActionsStartIndex) {
+ for (let i = projActionsModel.statefulActionsStartIndex; i <= buttonIndex; ++i) {
+ projActionsRow.children[i].shouldBeHighlighted = false;
+ projActionsRow.children[i].shouldBeHighlightedWhileRunning = true;
+ }
+ for (let i = projActionsModel.statefulActionsStartIndex; i < buttonIndex; ++i) {
+ project.run(projActionsModel.get(i).action, []);
}
}
+ parent.clicked(); // pass the event to the underlying button though all work can be done in-place
+ if (ctrlPressed && model.action !== 'start_editor') {
+ project.run('start_editor', [settings.get('editor')]);
+ }
}
- Connections {
- target: project
- onActionStarted: {
- glow.visible = false;
+ onPositionChanged: {
+ ctrlPressed = mouse.modifiers & Qt.ControlModifier; // bitwise AND
+ if (ctrlPressedLastState !== ctrlPressed) {
+ ctrlPressedLastState = ctrlPressed;
}
- onActionFinished: {
- if (action === model.action) {
- if (success) {
- glow.color = 'lightgreen';
- } else {
- glow.color = 'lightcoral';
- }
- glow.visible = true;
-
- if (settings.get('notifications') && !mainWindow.active) {
- sysTrayIcon.showMessage(
- success ? 'Success' : 'Error', // title
- `${project.name} - ${model.name}`, // text
- success ? Labs.SystemTrayIcon.Information : Labs.SystemTrayIcon.Warning, // icon
- 5000 // ms
- );
- }
+
+ shiftPressed = mouse.modifiers & Qt.ShiftModifier; // bitwise AND
+ if (shiftPressedLastState !== shiftPressed) { // reduce a number of unnecessary shiftHandler() calls
+ shiftPressedLastState = shiftPressed;
+ shiftHandler();
+ }
+ }
+ onEntered: {
+ if (model.action !== 'start_editor') {
+ let preparedText = `Ctrl -click to open the editor specified in the Settings
+ after the operation`;
+ if (buttonIndex >= projActionsModel.statefulActionsStartIndex) {
+ preparedText +=
+ `, Shift -click to perform all actions prior this one (including).
+ Ctrl -Shift -click for both`;
}
+ statusBar.text = preparedText;
+ }
+ }
+ onExited: {
+ statusBar.text = '';
+
+ ctrlPressed = false;
+ ctrlPressedLastState = false;
+
+ if (shiftPressed || shiftPressedLastState) {
+ shiftPressed = false;
+ shiftPressedLastState = false;
+ shiftHandler();
}
}
- /*
- Blinky glowing
- */
- RectangularGlow {
- id: glow
- visible: false
- anchors.fill: parent
- cornerRadius: 25
- glowRadius: 20
- spread: 0.25
- onVisibleChanged: visible ? glowAnimation.start() : glowAnimation.complete()
- SequentialAnimation {
- id: glowAnimation
- loops: 3
- onStopped: glow.visible = false
- OpacityAnimator {
- target: glow
- from: 0
- to: 1
- duration: 1000
+ }
+ Connections {
+ target: project
+ onActionStarted: {
+ glow.visible = false;
+ }
+ onActionFinished: {
+ if (action === model.action) {
+ if (success) {
+ glow.color = 'lightgreen';
+ } else {
+ glow.color = 'lightcoral';
}
- OpacityAnimator {
- target: glow
- from: 1
- to: 0
- duration: 1000
+ glow.visible = true;
+
+ if (settings.get('notifications') && !mainWindow.active) {
+ sysTrayIcon.showMessage(
+ success ? 'Success' : 'Error', // title
+ `${project.name} - ${model.name}`, // text
+ success ? Labs.SystemTrayIcon.Information : Labs.SystemTrayIcon.Warning, // icon
+ 5000 // ms
+ );
}
}
}
}
+ /*
+ Blinky glowing
+ */
+ RectangularGlow {
+ id: glow
+ visible: false
+ anchors.fill: parent
+ cornerRadius: 25
+ glowRadius: 20
+ spread: 0.25
+ onVisibleChanged: visible ? glowAnimation.start() : glowAnimation.complete()
+ SequentialAnimation {
+ id: glowAnimation
+ loops: 3
+ onStopped: glow.visible = false
+ OpacityAnimator {
+ target: glow
+ from: 0
+ to: 1
+ duration: 1000
+ }
+ OpacityAnimator {
+ target: glow
+ from: 1
+ to: 0
+ duration: 1000
+ }
+ }
+ }
}
}
+ }
- Rectangle {
- Layout.fillWidth: true
- Layout.fillHeight: true
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
- ScrollView {
- anchors.fill: parent
- TextArea {
- id: log
- readOnly: true
- selectByMouse: true
- wrapMode: Text.WordWrap
- font.pointSize: 10 // different on different platforms, Qt's bug
- font.weight: Font.DemiBold
- textFormat: TextEdit.RichText
- Connections {
- target: project
- onLogAdded: {
- if (level === Logging.WARNING) {
- log.append('' + message + ' ');
- } else if (level >= Logging.ERROR) {
- log.append('' + message + ' ');
- } else {
- log.append('
' + message + ' ');
- }
+ ScrollView {
+ anchors.fill: parent
+ TextArea {
+ id: log
+ readOnly: true
+ selectByMouse: true
+ wrapMode: Text.WordWrap
+ font.pointSize: 10 // different on different platforms, Qt's bug
+ font.weight: Font.DemiBold
+ textFormat: TextEdit.RichText
+ Connections {
+ target: project
+ onLogAdded: {
+ if (level === Logging.WARNING) {
+ log.append('' + message + ' ');
+ } else if (level >= Logging.ERROR) {
+ log.append('' + message + ' ');
+ } else {
+ log.append('' + message + ' ');
}
}
}
diff --git a/tests/test.py b/tests/test.py
index d449e18..91270e1 100755
--- a/tests/test.py
+++ b/tests/test.py
@@ -1,6 +1,6 @@
"""
-Common preparations for all test suites. Use this as a source of constants for test cases. Find the tests themself at
-concrete files
+Common preparations for all test suites. Use this as a source of constants for test cases. Find the tests themselfs at
+the concrete files
NOTE: make sure the test project tree is clean before running the tests!
@@ -14,6 +14,7 @@
"""
import inspect
+import logging
import pathlib
import shutil
import sys
@@ -27,13 +28,14 @@
if not TEST_PROJECT_PATH.joinpath('stm32pio-test-project.ioc').is_file():
raise FileNotFoundError("No test project is present")
-# Gently ask a user running tests to remove all irrelevant files from the TEST_PROJECT_PATH
+# Gently ask a user running tests to remove all irrelevant files from the TEST_PROJECT_PATH as they can interfere with
+# execution
if len(list(TEST_PROJECT_PATH.iterdir())) > 1:
raise Warning(f"There are extrinsic files in the test project directory '{TEST_PROJECT_PATH}'. Please persist only "
- "the .ioc file")
+ "the .ioc file and restart")
-# Make sure you have F0 framework installed (try to run code generation from STM32CubeMX manually at least once before
-# proceeding)
+# Make sure you have F0 framework installed (both for PlatformIO and CubeMX) (try to run a code generation and build
+# manually at least once before proceeding)
TEST_PROJECT_BOARD = 'nucleo_f031k6'
# Instantiate a temporary folder on every test suite run. It is used across all the tests and is deleted on shutdown
@@ -53,9 +55,7 @@
class CustomTestCase(unittest.TestCase):
- """
- These pre- and post-tasks are common for all test cases
- """
+ """These pre- and post-tasks are common for all test cases"""
def setUp(self):
"""
diff --git a/tests/test_cli.py b/tests/test_cli.py
index d95a5cc..7e672b7 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -9,6 +9,7 @@
import stm32pio.app
import stm32pio.lib
import stm32pio.settings
+import stm32pio.util
# Provides test constants
from tests.test import *
@@ -16,8 +17,8 @@
class TestCLI(CustomTestCase):
"""
- Some tests to mimic the behavior of end-user tasks (CLI commands such as 'new', 'clean', etc.). Run main function
- passing the arguments to it but sometimes even run as subprocess (to capture actual STDOUT/STDERR output)
+ Some tests to mimic the behavior of end-user tasks (CLI commands such as 'new', 'clean', etc.). Run the main
+ function passing the arguments to it but sometimes even run as subprocess (to capture actual STDOUT/STDERR output)
"""
def test_clean(self):
@@ -31,10 +32,12 @@ def test_clean(self):
# Clean ...
if case == '--quiet':
- return_code = stm32pio.app.main(sys_argv=['clean', case, '-d', str(FIXTURE_PATH)])
+ return_code = stm32pio.app.main(sys_argv=['clean', case, '-d', str(FIXTURE_PATH)],
+ should_setup_logging=False)
else:
with unittest.mock.patch('builtins.input', return_value=case):
- return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(FIXTURE_PATH)])
+ return_code = stm32pio.app.main(sys_argv=['clean', '-d', str(FIXTURE_PATH)],
+ should_setup_logging=False)
self.assertEqual(return_code, 0, msg="Non-zero return code")
@@ -59,15 +62,16 @@ def test_new(self):
"""
Successful build is the best indicator that all went right so we use '--with-build' option here
"""
- return_code = stm32pio.app.main(sys_argv=['new', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD,
- '--with-build'])
+ return_code = stm32pio.app.main(
+ sys_argv=['new', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD, '--with-build'],
+ should_setup_logging=False)
self.assertEqual(return_code, 0, msg="Non-zero return code")
# .ioc file should be preserved
self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file")
def test_generate(self):
- return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)])
+ return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)], should_setup_logging=False)
self.assertEqual(return_code, 0, msg="Non-zero return code")
for directory in ['Inc', 'Src']:
@@ -79,94 +83,87 @@ def test_generate(self):
# .ioc file should be preserved
self.assertTrue(FIXTURE_PATH.joinpath(f"{FIXTURE_PATH.name}.ioc").is_file(), msg="Missing .ioc file")
- def test_incorrect_path_should_log_error(self):
+ def test_should_log_error(self):
"""
- We should see an error log message and non-zero return code
+ We should see an error log message and a non-zero return code
"""
- path_not_exist = pathlib.Path('path_some_uniq_name/does/not/exist')
-
- with self.assertLogs(level='ERROR') as logs:
- return_code = stm32pio.app.main(sys_argv=['init', '-d', str(path_not_exist)])
- self.assertNotEqual(return_code, 0, msg="Return code should be non-zero")
- # Actual text may vary and depends on OS and system language so we check only for a part of path string
- self.assertTrue(next((True for message in logs.output if 'path_some_uniq_name' in message.lower()), False),
- msg="'ERROR' logging message hasn't been printed")
-
- def test_no_ioc_file_should_log_error(self):
+ with self.subTest(error="Incorrect path"):
+ path_not_exist = pathlib.Path('path_some_uniq_name/does/not/exist')
+ with self.assertLogs(level='ERROR') as logs:
+ return_code = stm32pio.app.main(sys_argv=['init', '-d', str(path_not_exist)],
+ should_setup_logging=False)
+ self.assertNotEqual(return_code, 0, msg="Return code should be non-zero")
+ # Actual text varies for different OSes and system languages so we check only for a part of the string
+ self.assertTrue(next((True for msg in logs.output if 'path_some_uniq_name' in msg.lower()), False),
+ msg="'ERROR' logging message hasn't been printed")
+
+ with self.subTest(error="No .ioc file"):
+ dir_with_no_ioc_file = FIXTURE_PATH.joinpath('dir.with.no.ioc.file')
+ dir_with_no_ioc_file.mkdir(exist_ok=False)
+ with self.assertLogs(level='ERROR') as logs:
+ return_code = stm32pio.app.main(sys_argv=['init', '-d', str(dir_with_no_ioc_file)],
+ should_setup_logging=False)
+ self.assertNotEqual(return_code, 0, msg="Return code should be non-zero")
+ self.assertTrue(next((True for msg in logs.output if FileNotFoundError.__name__ in msg), False),
+ msg="'ERROR' logging message hasn't been printed")
+
+ def test_verbosity(self):
"""
- We should see an error log message and non-zero return code
- """
- dir_with_no_ioc_file = FIXTURE_PATH.joinpath('dir.with.no.ioc.file')
- dir_with_no_ioc_file.mkdir(exist_ok=False)
-
- with self.assertLogs(level='ERROR') as logs:
- return_code = stm32pio.app.main(sys_argv=['init', '-d', str(dir_with_no_ioc_file)])
- self.assertNotEqual(return_code, 0, msg="Return code should be non-zero")
- self.assertTrue(next((True for message in logs.output if FileNotFoundError.__name__ in message), False),
- msg="'ERROR' logging message hasn't been printed")
-
- def test_verbose(self):
- """
- Capture the full output. Check for both 'DEBUG' logging messages and STM32CubeMX CLI output. Verbose logs format
- should match such a regex:
+ Capture the full output. Check both the app logging messages and STM32CubeMX CLI output. Completely isolate
+ runs by using subprocess
+ Verbose logs format should match such a regex:
^(?=(DEBUG|INFO|WARNING|ERROR|CRITICAL) {0,4})(?=.{8} (?=(build|pio_init|...) {0,26})(?=.{26} [^ ]))
+
+ Non-verbose:
+ ^(?=(INFO) {0,4})(?=.{8} ((?!( |build|pio_init|...))))
"""
# inspect.getmembers() is great but it triggers class properties to execute leading to the unwanted code
# execution
methods = dir(stm32pio.lib.Stm32pio) + ['main']
- buffer_stdout, buffer_stderr = io.StringIO(), io.StringIO()
- with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(buffer_stderr):
- return_code = stm32pio.app.main(sys_argv=['-v', 'new', '-d', str(FIXTURE_PATH), '-b', TEST_PROJECT_BOARD])
+ with self.subTest(verbosity_level=stm32pio.util.Verbosity.NORMAL):
+ result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', str(FIXTURE_PATH)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
- self.assertEqual(return_code, 0, msg="Non-zero return code")
- # stderr and not stdout contains the actual output (by default for the logging module)
- self.assertEqual(len(buffer_stdout.getvalue()), 0,
- msg="Process has printed something directly into STDOUT bypassing logging")
- self.assertIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output hasn't been enabled on STDERR")
-
- # Inject all methods' names in the regex. Inject the width of field in a log format string
- regex = re.compile("^(?=(DEBUG) {0,4})(?=.{8} (?=(" + '|'.join(methods) + ") {0," +
- str(stm32pio.settings.log_fieldwidth_function) + "})(?=.{" +
- str(stm32pio.settings.log_fieldwidth_function) + "} [^ ]))", flags=re.MULTILINE)
- self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1,
- msg="Logs messages doesn't match the format")
-
- # The snippet of the actual STM32CubeMX output
- self.assertIn("Starting STM32CubeMX", buffer_stderr.getvalue(), msg="STM32CubeMX has not printed its logs")
-
- def test_non_verbose(self):
- """
- Capture the full output. We should not see any 'DEBUG' logging messages or STM32CubeMX CLI output. Logs format
- should match such a regex:
+ self.assertEqual(result.returncode, 0, msg="Non-zero return code")
+ # stderr and not stdout contains the actual output (by default for the logging module)
+ self.assertNotIn('DEBUG', result.stderr, msg="Verbose logging output has been enabled on stderr")
+ self.assertEqual(len(result.stdout), 0,
+ msg="Entire app output should flow through the logging module")
- ^(?=(INFO) {0,4})(?=.{8} ((?!( |build|pio_init|...))))
- """
+ regex = re.compile("^(?=(INFO) {0,4})(?=.{8} ((?!( |" + '|'.join(methods) + "))))", flags=re.MULTILINE)
+ self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1,
+ msg="Logs messages doesn't match the format")
- # inspect.getmembers is great but it triggers class properties leading to the unacceptable code execution
- methods = dir(stm32pio.lib.Stm32pio) + ['main']
+ # The snippet of the actual STM32CubeMX output
+ self.assertNotIn('Starting STM32CubeMX', result.stderr, msg="STM32CubeMX has printed its logs")
- buffer_stdout, buffer_stderr = io.StringIO(), io.StringIO()
- with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(buffer_stderr):
- return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)])
+ with self.subTest(verbosity_level=stm32pio.util.Verbosity.VERBOSE):
+ result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, '-v', 'new', '-d', str(FIXTURE_PATH),
+ '-b', TEST_PROJECT_BOARD], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ encoding='utf-8')
- self.assertEqual(return_code, 0, msg="Non-zero return code")
- # stderr and not stdout contains the actual output (by default for the logging module)
- self.assertNotIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output has been enabled on stderr")
- self.assertEqual(len(buffer_stdout.getvalue()), 0, msg="All app output should flow through the logging module")
+ self.assertEqual(result.returncode, 0, msg="Non-zero return code")
+ # stderr and not stdout contains the actual output (by default for the logging module)
+ self.assertEqual(len(result.stdout), 0,
+ msg="Process has printed something directly into STDOUT bypassing logging")
+ self.assertIn('DEBUG', result.stderr, msg="Verbose logging output hasn't been enabled on STDERR")
- regex = re.compile("^(?=(INFO) {0,4})(?=.{8} ((?!( |" + '|'.join(methods) + "))))", flags=re.MULTILINE)
- self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1,
- msg="Logs messages doesn't match the format")
+ # Inject all methods' names in the regex. Inject the width of field in a log format string
+ regex = re.compile("^(?=(DEBUG) {0,4})(?=.{8} (?=(" + '|'.join(methods) + ") {0," +
+ str(stm32pio.settings.log_fieldwidth_function) + "})(?=.{" +
+ str(stm32pio.settings.log_fieldwidth_function) + "} [^ ]))", flags=re.MULTILINE)
+ self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1,
+ msg="Logs messages doesn't match the format")
- # The snippet of the actual STM32CubeMX output
- self.assertNotIn('Starting STM32CubeMX', buffer_stderr.getvalue(), msg="STM32CubeMX has printed its logs")
+ # The snippet of the actual STM32CubeMX output
+ self.assertIn("Starting STM32CubeMX", result.stderr, msg="STM32CubeMX has not printed its logs")
def test_init(self):
"""
- Check for config creation and parameters presence
+ Check for the config creation and parameters presence
"""
result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH),
'-b', TEST_PROJECT_BOARD], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -179,19 +176,20 @@ def test_init(self):
config.read(str(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name)))
for section, parameters in stm32pio.settings.config_default.items():
for option, value in parameters.items():
- with self.subTest(section=section, option=option, msg="Section/key is not found in saved config file"):
+ with self.subTest(section=section, option=option,
+ msg="Section/key is not found in the saved config file"):
self.assertIsNotNone(config.get(section, option, fallback=None))
self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD,
msg="'board' has not been set")
def test_status(self):
"""
- Test the output returning by the app on a request to the 'status' command
+ Test the app output returned as a response to the 'status' command
"""
buffer_stdout = io.StringIO()
with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(None):
- return_code = stm32pio.app.main(sys_argv=['status', '-d', str(FIXTURE_PATH)])
+ return_code = stm32pio.app.main(sys_argv=['status', '-d', str(FIXTURE_PATH)], should_setup_logging=False)
self.assertEqual(return_code, 0, msg="Non-zero return code")
@@ -206,4 +204,4 @@ def test_status(self):
self.assertGreater(match.start(), last_stage_pos, msg="The order of stages is messed up")
last_stage_pos = match.start()
- self.assertEqual(matches_counter, len(stm32pio.lib.ProjectStage) - 1)
+ self.assertEqual(matches_counter, len(stm32pio.lib.ProjectStage) - 1) # UNDEFINED stage should not be printed
diff --git a/tests/test_integration.py b/tests/test_integration.py
index eaf98ac..390bac6 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -17,7 +17,7 @@ class TestIntegration(CustomTestCase):
def test_rebase_project(self):
"""
Test the portability of projects: they should stay totally valid after moving to another path (same as renaming
- the parent part of the path). If we will not meet any exceptions, we should consider the test passed.
+ the parent part of the path). If we will not meet any exceptions, we should consider the test passed
"""
project_before = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
project_before.save_config()
@@ -26,14 +26,14 @@ def test_rebase_project(self):
shutil.move(str(project_before.path), new_path)
project_after = stm32pio.lib.Stm32pio(new_path, parameters={'project': {'board': TEST_PROJECT_BOARD}})
- project_after.generate_code()
- project_after.pio_init()
- project_after.patch()
- project_after.build()
+ self.assertEqual(project_after.generate_code(), 0)
+ self.assertEqual(project_after.pio_init(), 0)
+ self.assertEqual(project_after.patch(), None)
+ self.assertEqual(project_after.build(), 0)
def test_config_priorities(self):
"""
- Test the compliance with priorities when reading the parameters
+ Test the compliance with the priorities when reading the parameters
"""
# Sample user's custom patch value
config_parameter_user_value = inspect.cleandoc('''
@@ -61,7 +61,7 @@ def test_config_priorities(self):
project.pio_init()
project.patch()
- # Actually, we can parse platformio.ini via configparser but this is simpler in our case
+ # Actually, we can parse the platformio.ini via the configparser but this is simpler in our case
after_patch_content = FIXTURE_PATH.joinpath('platformio.ini').read_text()
self.assertIn(config_parameter_user_value, after_patch_content,
msg="User config parameter has not been prioritized over the default one")
@@ -77,9 +77,7 @@ def test_build(self):
project.pio_init()
project.patch()
- result = project.build()
-
- self.assertEqual(result, 0, msg="Build failed")
+ self.assertEqual(project.build(), 0, msg="Build failed")
def test_regenerate_code(self):
"""
@@ -119,28 +117,43 @@ def test_current_stage(self):
"""
Go through the sequence of states emulating the real-life project lifecycle
"""
- project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY)
-
- project.save_config()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.INITIALIZED)
- project.generate_code()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.GENERATED)
+ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
- project.pio_init()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.PIO_INITIALIZED)
+ for method, expected_stage in [(None, stm32pio.lib.ProjectStage.EMPTY),
+ ('save_config', stm32pio.lib.ProjectStage.INITIALIZED),
+ ('generate_code', stm32pio.lib.ProjectStage.GENERATED),
+ ('pio_init', stm32pio.lib.ProjectStage.PIO_INITIALIZED),
+ ('patch', stm32pio.lib.ProjectStage.PATCHED),
+ ('build', stm32pio.lib.ProjectStage.BUILT),
+ ('clean', stm32pio.lib.ProjectStage.EMPTY),
+ ('pio_init', stm32pio.lib.ProjectStage.UNDEFINED)]:
+ if method is not None:
+ getattr(project, method)()
+ self.assertEqual(project.state.current_stage, expected_stage)
+ if expected_stage != stm32pio.lib.ProjectStage.UNDEFINED:
+ self.assertTrue(project.state.is_consistent)
+ else:
+ # Should be UNDEFINED when the project is messed up (pio_init() after clean())
+ self.assertFalse(project.state.is_consistent)
+
+ def test_users_files_preservation(self):
+ """
+ Check that custom user's files and folders will remain untouched throughout all the steps of the project
+ """
- project.patch()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.PATCHED)
+ users_file = FIXTURE_PATH.joinpath('some_users_file.txt')
+ users_file_content = "Sample content that any human can put into a text file"
+ users_file.write_text(users_file_content)
+ users_dir = FIXTURE_PATH.joinpath('some_users_directory')
+ users_dir.mkdir()
- project.build()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.BUILT)
+ def check_preservation():
+ self.assertTrue(all(item in FIXTURE_PATH.iterdir() for item in [users_file, users_dir]))
+ self.assertIn(users_file_content, users_file.read_text())
- project.clean()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY)
+ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
- # Should be UNDEFINED when the project is messed up
- project.pio_init()
- self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.UNDEFINED)
- self.assertFalse(project.state.is_consistent)
+ for method in ['save_config', 'generate_code', 'pio_init', 'patch', 'build']:
+ getattr(project, method)()
+ check_preservation()
diff --git a/tests/test_unit.py b/tests/test_unit.py
index 03c93bf..52dd9ad 100644
--- a/tests/test_unit.py
+++ b/tests/test_unit.py
@@ -1,3 +1,4 @@
+import collections
import configparser
import inspect
import platform
@@ -34,9 +35,9 @@ def test_generate_code(self):
def test_pio_init(self):
"""
- Consider that existence of 'platformio.ini' file showing a successful PlatformIO project initialization. The
- last one has another traces that can be checked too but we are interested only in a 'platformio.ini' anyway.
- Also, check that it is a correct configparser file and is not empty
+ Consider that the existence of a 'platformio.ini' file showing a successful PlatformIO project initialization.
+ There are other artifacts that can be checked too but we are interested only in a 'platformio.ini' anyway. Also,
+ check that it is a correct configparser.ConfigParser file and is not empty
"""
project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
result = project.pio_init()
@@ -50,8 +51,8 @@ def test_pio_init(self):
def test_patch(self):
"""
- Check that new parameters were added, modified were updated and existing parameters didn't gone. Also, check for
- unnecessary folders deletion
+ Check that the new parameters have been added, modified ones have been updated and existing parameters didn't
+ gone. Also, check for unnecessary folders deletion
"""
project = stm32pio.lib.Stm32pio(FIXTURE_PATH)
@@ -111,7 +112,7 @@ def test_build_should_handle_error(self):
with self.assertLogs(level='ERROR') as logs:
self.assertNotEqual(project.build(), 0, msg="Build error was not indicated")
- # next() - Technique to find something in array, string, etc. (or to indicate that there is no)
+ # next() - Technique to find something in array, string, etc. (or to indicate that there is no of such)
self.assertTrue(next((True for item in logs.output if "PlatformIO build error" in item), False),
msg="Error message does not match")
@@ -121,7 +122,7 @@ def test_start_editor(self):
"""
project = stm32pio.lib.Stm32pio(FIXTURE_PATH)
- editors = {
+ editors = { # some edotors to check
'atom': {
'Windows': 'atom.exe',
'Darwin': 'Atom',
@@ -168,24 +169,31 @@ def test_start_editor(self):
def test_init_path_not_found_should_raise(self):
"""
- Pass non-existing path and expect the error
+ Pass a non-existing path and expect the error
"""
path_does_not_exist_name = 'does_not_exist'
path_does_not_exist = FIXTURE_PATH.joinpath(path_does_not_exist_name)
with self.assertRaisesRegex(FileNotFoundError, path_does_not_exist_name,
- msg="FileNotFoundError was not raised or doesn't contain a description"):
+ msg="FileNotFoundError has not been raised or doesn't contain a description"):
stm32pio.lib.Stm32pio(path_does_not_exist)
def test_save_config(self):
"""
- Explicitly save the config to file and look did that actually happen and whether all the information was
+ Explicitly save the config to a file and look did that actually happen and whether all the information was
preserved
"""
# 'board' is non-default, 'project'-section parameter
project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'project': {'board': TEST_PROJECT_BOARD}})
- project.save_config()
+ # Merge additional parameters
+ retcode = project.save_config({
+ 'project': {
+ 'additional_test_key': 'test_value'
+ }
+ })
+
+ self.assertEqual(retcode, 0, msg="Return code of the method is non-zero")
self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(),
msg=f"{stm32pio.settings.config_file_name} file hasn't been created")
@@ -200,12 +208,18 @@ def test_save_config(self):
self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD,
msg="'board' has not been set")
+ self.assertEqual(config.get('project', 'additional_test_key', fallback="Not found"), 'test_value',
+ msg="Merged config is not present in the saved file")
def test_get_platformio_boards(self):
"""
PlatformIO identifiers of boards are requested using PlatformIO CLI in JSON format
"""
- self.assertIsInstance(stm32pio.util.get_platformio_boards(platformio_cmd='platformio'), list)
+ boards = stm32pio.util.get_platformio_boards(platformio_cmd='platformio')
+
+ self.assertIsInstance(boards, collections.abc.MutableSequence)
+ self.assertGreater(len(boards), 0, msg="boards list is empty")
+ self.assertTrue(all(isinstance(item, str) for item in boards), msg="some list items are not strings")
def test_ioc_file_provided(self):
"""
@@ -216,8 +230,8 @@ def test_ioc_file_provided(self):
shutil.copy(FIXTURE_PATH.joinpath('stm32pio-test-project.ioc'), FIXTURE_PATH.joinpath('42.ioc'))
shutil.copy(FIXTURE_PATH.joinpath('stm32pio-test-project.ioc'), FIXTURE_PATH.joinpath('Abracadabra.ioc'))
- project = stm32pio.lib.Stm32pio(FIXTURE_PATH.joinpath('42.ioc'))
+ project = stm32pio.lib.Stm32pio(FIXTURE_PATH.joinpath('42.ioc')) # pick just one
self.assertTrue(project.ioc_file.samefile(FIXTURE_PATH.joinpath('42.ioc')),
- msg="Provided .ioc file wasn't chosen")
+ msg="Provided .ioc file hasn't been chosen")
self.assertEqual(project.config.get('project', 'ioc_file'), '42.ioc',
msg="Provided .ioc file is not in the config")