From d67e62715edd32d3912e31e72c3e50f82aefadda Mon Sep 17 00:00:00 2001 From: zakstucke <44890343+zakstucke@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:09:56 +0200 Subject: [PATCH] Misc (#42) --- .dockerignore | 196 ++++++++ .github/actions/install-rust/action.yml | 37 ++ .github/workflows/release.yml | 33 +- .github/workflows/tests.yml | 36 +- .gitignore | 77 ++-- .pre-commit-config.yaml | 37 +- .zetch.lock | 23 +- bitbazaar.code-workspace | 34 +- dev_scripts/_scr_setup/setup.sh | 2 +- dev_scripts/docs.sh | 2 +- dev_scripts/initial_setup.sh | 276 +++++++---- dev_scripts/pkg.sh | 2 +- dev_scripts/process.sh | 18 +- dev_scripts/py_rust.sh | 8 +- dev_scripts/run.sh | 49 +- dev_scripts/test.sh | 36 +- dev_scripts/utils.sh | 106 ++++- dev_scripts/zj.sh | 28 ++ js/bun.lockb | Bin 252506 -> 52457 bytes js/package-lock.json | 26 +- js/package.json | 13 +- js/tsconfig.json | 3 +- js/tsconfig.zetch.json | 3 +- opencollector.yaml | 12 - opencollector.yaml.zetch | 23 +- py/bitbazaar/__init__.py | 1 + py/bitbazaar/log/_formatting.py | 2 +- py/pdm.lock | 265 ++++++----- py/pyproject.toml | 5 +- py/tests/test_version.py | 8 + py_rust/.config/nextest.toml | 112 +++++ py_rust/Cargo.lock | 417 ++++++++++------- py_rust/Cargo.toml | 10 +- py_rust/dev_requirements.txt | 6 + py_rust/rustfmt.toml | 2 +- py_rust/src/lib.rs | 5 +- py_rust/src/prelude.rs | 8 +- py_rust/tests/test_basic.py | 9 - py_rust/tests/test_version.py | 8 + rust/.config/nextest.toml | 10 +- rust/Cargo.lock | 589 +++++++++++++++++++++--- rust/Cargo.toml | 101 ++-- rust/benches/bench_setup_test.rs | 25 + rust/bitbazaar/lib.rs | 2 + rust/bitbazaar/prelude.rs | 7 +- rust/rustfmt.toml | 3 +- zetch.config.toml | 13 +- 47 files changed, 1903 insertions(+), 785 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/actions/install-rust/action.yml create mode 100755 dev_scripts/zj.sh create mode 100644 py/tests/test_version.py create mode 100644 py_rust/.config/nextest.toml create mode 100644 py_rust/dev_requirements.txt delete mode 100644 py_rust/tests/test_basic.py create mode 100644 py_rust/tests/test_version.py create mode 100644 rust/benches/bench_setup_test.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..68e52332 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,196 @@ +Dockerfile +Dockerfile.* + +# Ignore private folder and any file with private in the name: +**/**/private/ +*private* +# Except for zetch files: +!*private*.zetch.* +!*private*.*.zetch + +**/**/process_data/ +**/**/logs/ + +# Tempate files +.cop.*.yml + +scratch_space.ipynb + +**/**/node_modules/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Files flagged for ignore: which have .gitignore. inside them: +*.gitignore.* +*_gitignore.* + +# Celery: +celerybeat-schedule.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Pdm: +.pdm-python + +# Distribution / packaging +.Python +sds/sworker/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# leptos build files: +**/**/static/rsite/ +# Compiled css: +**/**/static/css/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +**/**/htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +**/**/coverage/ +nosetests.xml +coverage.xml +*.cover +*.py,cover +**/**/.hypothesis/ +**/**/.pytest_cache/ +**/**/.ruff_cache/ +**/**/cover/ +**/**/.nox/ +*.prof +prof/ +**/**/perf_profiles/ +**/**/test-results/ +**/**/playwright-report/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +**/**/instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# PyBuilder +**/**/.pybuilder/ +**/**/target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +**/**/profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +**/**/__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +**/**/.env +**/**/.venv +**/**/env/ +**/**/venv/ +**/**/ENV/ +**/**/env.bak/ +**/**/venv.bak/ + +# mkdocs local build directory: +**/**/site/ +**/**/docs/js_ref/ +**/**/docs/rust_ref/ +.staticrypt.json + +# Vscode internals +**/**/.vscode/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mypy +**/**/.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +**/**/.pyre/ + +# pytype static type analyzer +**/**/.pytype/ + +# Cython debug symbols +**/**/cython_debug/ + +# Pycharm +**/**/.idea/ + +# Mac stuff +.DS_Store + +**/**/template_test_build/ + +# User diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml new file mode 100644 index 00000000..4929c732 --- /dev/null +++ b/.github/actions/install-rust/action.yml @@ -0,0 +1,37 @@ +name: Setup Rust +description: "Installs latest stable rust, and sets up sscache for caching." +inputs: + qa: + description: "Whether things like cargo-hack need installing." + required: false + default: "false" + test: + description: "Whether things like nextest need installing." + required: false + default: "false" +runs: + using: composite + steps: + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.4 + - name: Set Rust caching env vars + shell: bash + run: | + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + # Really this would need to be run as a post task to be of any use (to actually see hits), + # but post in composite action not currently supported: + # https://github.com/actions/runner/issues/1478 + # - name: Run sccache stat for check + # shell: bash + # run: ${SCCACHE_PATH} --show-stats + - name: "Install cargo-hack, used for feature checking in pre-commit." + if: ${{ inputs.qa == 'true' }} + uses: taiki-e/install-action@cargo-hack + - name: Install nextest + if: ${{ inputs.test == 'true' }} + uses: taiki-e/install-action@nextest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4d7c835..d0f44e58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,6 @@ jobs: with: token: ${{ secrets.VERSION_BOT_PAT }} - uses: actions/setup-python@v4 - - uses: ./.github/actions/install-pre-commit - name: Install zetch run: pip install zetch @@ -69,9 +68,9 @@ jobs: - name: Update Python version if: ${{ inputs.py_release }} run: | - zetch put zetch.config.toml context.static.PY_VERSION.value ${{ inputs.py_version }} + zetch put zetch.config.toml context.static.PY_VERSION ${{ inputs.py_version }} - # Js project + # Js project - name: Install Bun, no npm should be needed if: ${{ inputs.js_release }} uses: oven-sh/setup-bun@v1 @@ -85,28 +84,22 @@ jobs: - name: Update JS version if: ${{ inputs.js_release }} run: | - zetch put zetch.config.toml context.static.JS_VERSION.value ${{ inputs.js_version }} - - - uses: dtolnay/rust-toolchain@stable - if: ${{ inputs.py_rust_release }} || ${{ inputs.rust_release }} - with: - components: rustfmt, clippy + zetch put zetch.config.toml context.static.JS_VERSION ${{ inputs.js_version }} - - name: Install cargo-hack, used for feature checking in pre-commit. + - uses: ./.github/actions/install-rust if: ${{ inputs.py_rust_release }} || ${{ inputs.rust_release }} - uses: taiki-e/install-action@cargo-hack - - name: Update Rust-backed Python version if: ${{ inputs.py_rust_release }} run: | - zetch put zetch.config.toml context.static.PY_RUST_VERSION.value ${{ inputs.py_rust_version }} + zetch put zetch.config.toml context.static.PY_RUST_VERSION ${{ inputs.py_rust_version }} - name: Update Rust version if: ${{ inputs.rust_release }} run: | - zetch put zetch.config.toml context.static.RUST_VERSION.value ${{ inputs.rust_version }} + zetch put zetch.config.toml context.static.RUST_VERSION ${{ inputs.rust_version }} + - uses: ./.github/actions/install-pre-commit - name: add and format added files with pre-commit # Running on staged change only as that's all that's needed, || true as don't want it to fail, just modify run: | @@ -160,13 +153,7 @@ jobs: with: node-version: '20' - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ./rust -> target - cache-on-failure: 'true' - cache-all-crates: 'true' + - uses: ./.github/actions/install-rust - name: Build docs run: | @@ -334,9 +321,7 @@ jobs: ref: ${{ needs.commit_versions.outputs.new-sha }} - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - + - uses: ./.github/actions/install-rust - name: upload to Crates.io run: | cd rust diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4fbba383..1c697e63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,6 @@ jobs: with: python-version: "3.12" cache: pip - - uses: ./.github/actions/install-pre-commit # Python project - name: Set up PDM @@ -62,12 +61,11 @@ jobs: cd ./js bun install - - uses: dtolnay/rust-toolchain@stable + - uses: ./.github/actions/install-rust with: - components: rustfmt, clippy - - name: "Install cargo-hack, used for feature checking in pre-commit." - uses: taiki-e/install-action@cargo-hack + qa: true + - uses: ./.github/actions/install-pre-commit - name: Run QA run: | ./dev_scripts/test.sh qa @@ -96,13 +94,9 @@ jobs: with: node-version: "20" - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/install-rust with: - workspaces: "./rust -> target" - cache-on-failure: "true" - cache-all-crates: "true" + qa: true - name: Install dependencies run: | @@ -198,15 +192,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/install-rust with: - workspaces: "./py_rust -> target" - cache-on-failure: "true" - cache-all-crates: "true" - - name: Install nextest - uses: taiki-e/install-action@nextest + test: true - uses: actions/setup-python@v4 with: @@ -243,15 +231,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/install-rust with: - workspaces: "./rust -> target" - cache-on-failure: "true" - cache-all-crates: "true" - - name: Install nextest - uses: taiki-e/install-action@nextest + test: true - name: Run tests (linux) if: matrix.os == 'ubuntu-latest' diff --git a/.gitignore b/.gitignore index f8f250cb..0d204723 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,22 @@ # Ignore private folder and any file with private in the name: -private/ +**/**/private/ *private* # Except for zetch files: !*private*.zetch.* !*private*.*.zetch -process_data/ -logs/ +**/**/process_data/ +**/**/logs/ # Tempate files .cop.*.yml scratch_space.ipynb -node_modules/ +**/**/node_modules/ + +# These are backup files generated by rustfmt +**/*.rs.bk # Files flagged for ignore: which have .gitignore. inside them: *.gitignore.* @@ -52,6 +55,10 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +# leptos build files: +**/**/static/rsite/ +# Compiled css: +**/**/static/css/ # PyInstaller # Usually these files are written by a python script from a template @@ -64,25 +71,27 @@ pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports -htmlcov/ +**/**/htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache -coverage/ +**/**/coverage/ nosetests.xml coverage.xml *.cover *.py,cover -.hypothesis/ -.pytest_cache/ -.ruff_cache/ -cover/ -.nox/ +**/**/.hypothesis/ +**/**/.pytest_cache/ +**/**/.ruff_cache/ +**/**/cover/ +**/**/.nox/ *.prof prof/ -perf_profiles/ +**/**/perf_profiles/ +**/**/test-results/ +**/**/playwright-report/ # Translations *.mo @@ -95,21 +104,21 @@ db.sqlite3 db.sqlite3-journal # Flask stuff: -instance/ +**/**/instance/ .webassets-cache # Scrapy stuff: .scrapy # PyBuilder -.pybuilder/ -target/ +**/**/.pybuilder/ +**/**/target/ # Jupyter Notebook .ipynb_checkpoints # IPython -profile_default/ +**/**/profile_default/ ipython_config.py # pyenv @@ -125,7 +134,7 @@ ipython_config.py #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ +**/**/__pypackages__/ # Celery stuff celerybeat-schedule @@ -135,22 +144,22 @@ celerybeat.pid *.sage.py # Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +**/**/.env +**/**/.venv +**/**/env/ +**/**/venv/ +**/**/ENV/ +**/**/env.bak/ +**/**/venv.bak/ # mkdocs local build directory: -site/ -docs/js_ref/ -docs/rust_ref/ +**/**/site/ +**/**/docs/js_ref/ +**/**/docs/rust_ref/ .staticrypt.json # Vscode internals -.vscode/ +**/**/.vscode/ # Spyder project settings .spyderproject @@ -160,25 +169,25 @@ docs/rust_ref/ .ropeproject # mypy -.mypy_cache/ +**/**/.mypy_cache/ .dmypy.json dmypy.json # Pyre type checker -.pyre/ +**/**/.pyre/ # pytype static type analyzer -.pytype/ +**/**/.pytype/ # Cython debug symbols -cython_debug/ +**/**/cython_debug/ # Pycharm -.idea/ +**/**/.idea/ # Mac stuff .DS_Store -template_test_build/ +**/**/template_test_build/ # User diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7145c20b..731e54f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/zakstucke/zetch - rev: v0.0.10 + rev: v0.0.16 hooks: - id: zetch @@ -48,6 +48,8 @@ repos: args: # Ignore don't error on specific words that always fail: (foo,bar,baz...) - -L=crate + - -L=numer + - -L=anull # Ruff: linting and formatting for python code: - repo: https://github.com/astral-sh/ruff-pre-commit @@ -76,23 +78,22 @@ repos: - id: cargo-fmt name: cargo-fmt description: "Format files with cargo fmt." - entry: cargo fmt - language: rust + language: system types: [rust] + entry: cargo fmt args: [--manifest-path=./rust/Cargo.toml, --] - - id: cargo-hack-check - name: cargo-check-each-feature - description: Check the package for errors. - entry: cargo hack check - language: rust + - id: cargo-rust-check + name: cargo-rust-check + description: Check the rust package for errors using cargo check on needed targets and features. + entry: ./dev_scripts/test.sh cargo_rust_check + language: system types: [rust] - args: [--manifest-path=./rust/Cargo.toml, --each-feature, --no-dev-deps] pass_filenames: false - id: cargo-clippy name: cargo-clippy description: Lint rust sources entry: cargo clippy - language: rust + language: system args: ["--manifest-path", "./rust/Cargo.toml", "--all-features", "--", "-D", "warnings"] types: [rust] pass_filenames: false @@ -104,22 +105,22 @@ repos: name: cargo-fmt description: "Format files with cargo fmt." entry: cargo fmt - language: rust + language: system types: [rust] args: [--manifest-path=./py_rust/Cargo.toml, --] - - id: cargo-hack-check - name: cargo-check-each-feature - description: Check the package for errors. - entry: cargo hack check - language: rust + - id: cargo-py-rust-check + name: cargo-py-rust-check + description: Check the rust-backed python package for errors using cargo check on needed targets + and features. + entry: ./dev_scripts/test.sh cargo_py_rust_check + language: system types: [rust] - args: [--manifest-path=./py_rust/Cargo.toml, --each-feature, --no-dev-deps] pass_filenames: false - id: cargo-clippy name: cargo-clippy description: Lint rust sources entry: cargo clippy - language: rust + language: system args: ["--manifest-path", "./py_rust/Cargo.toml", "--all-features", "--", "-D", "warnings"] types: [rust] pass_filenames: false diff --git a/.zetch.lock b/.zetch.lock index 08a6f165..d46d3c47 100644 --- a/.zetch.lock +++ b/.zetch.lock @@ -1,23 +1,24 @@ { - "version": "0.0.10", + "version": "0.0.16", "files": { - "docs/CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97", - "js/tsconfig.zetch.json": "fb5d57b825bb3c2f6dd4254bf939f2444e52946622a7f93b91e3acb75876ebbc", - "docs/index.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", "CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97", - "py/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", - "docs/CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", - "rust/README.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", + "LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "README.zetch.md": "5e44429bf29ed38799d08bbf375435dd58516002c8dcf7f6f5cf47628cc29182", + "docs/CODE_OF_CONDUCT.zetch.md": "bf106326ffc75f5167cfde27c997c77c6b97c843a9e392b564355d0e70e50b97", + "docs/CONTRIBUTING.zetch.md": "bace46dc064746b54cf472eba960d934d705c2f83120b865a4b47032ff1552c5", + "docs/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", + "docs/index.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", + "js/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "js/README.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", + "js/tsconfig.zetch.json": "20974867286079e9cff0945f28efa3d77b49a7d75ab4f7106969c44239df97c6", + "opencollector.yaml.zetch": "7bc57cf34798098de165251bee40b14905e46189e781f38597ed5debb1dfd4f7", + "py/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "py/README.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", "py_rust/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "docs/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", "py_rust/README.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", "rust/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "js/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b", - "opencollector.yaml.zetch": "678a691ae64d7f9893e8799ea657842fe051b3fcce4da568969d8de070a29393" + "rust/README.zetch.md": "0dd2c30854a3e110c37b08c068c09e80f6e459017f123930a9be5ed08b34fece", + "rust/pkg/LICENSE.zetch.md": "d2c12e539d357957b950a54a5477c3a9f87bd2b3ee707be7a4db7adaf5aacc2b" } } \ No newline at end of file diff --git a/bitbazaar.code-workspace b/bitbazaar.code-workspace index bbf8db99..9fb521a6 100644 --- a/bitbazaar.code-workspace +++ b/bitbazaar.code-workspace @@ -15,6 +15,14 @@ ], "rust-analyzer.rustfmt.extraArgs": [], "rust-analyzer.cargo.features": "all", // Enable all features in cargo.toml for type hinting + // Use a separate target dir to prevent it messing with other processes (used to cause locks etc): + // https://github.com/rust-lang/rust-analyzer/issues/6007 + "rust-analyzer.server.extraEnv": { + "CARGO_TARGET_DIR": "target/analyzer" + }, + "rust-analyzer.check.extraArgs": [ + "--target-dir=target/analyzer" + ], "biome.lspBin": "/usr/local/bin/biome", // Path to the biome binary installed in initial_setup.sh "editor.codeActionsOnSave": { @@ -99,29 +107,6 @@ "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" ], - // For the tailwind extension if using: - // Note: biome class sorting should be possible soon: https://github.com/biomejs/biome/issues/1274 - "tailwindCSS.experimental.classRegex": [ - // Allows tailwind autocomplete to work in "classnames" function and "cx" shorthand for it. - // https://github.com/tailwindlabs/tailwindcss/issues/7553 - ["(?:classnames|cx)\\(([^)]*)\\)", "\"([^\"]*)\""], - ["(?:classnames|cx)\\(([^)]*)\\)", "'([^']*')"], - ["(?:classnames|cx)\\(([^)]*)\\)", "`([^`]*`)"], - - // Also allow it to work in any variable starting with Classes: - "Classes=\"([^\"]*)", //
- "Classes = \"([^\"]*)", // *.Classes = "..." - "Classes={\"([^\"}]*)", // - "Classes={`([^`}]*)", // - "Classes: \"([^\"]*)", // - "Classes: `([^`]*)" // - ], - "tailwindCSS.files.exclude": [ - "**/.git/**", - "**/node_modules/**", - "**/venv/**", - "**/.venv/**" - ], "scss.lint.unknownAtRules": "ignore", "search.useIgnoreFiles": false, // Otherwise, things in .gitignore will not be searchable, its better to exclude them manually: "notebook.output.textLineLimit": 100, @@ -141,7 +126,8 @@ "**/.git/**": true, "**/ipynb_checkpoints/**": true, "**/.ipynb": true, - "**/target/**": true + "**/target/**": true, + "**/prof/**": true }, "files.watcherExclude": { "**/.venv/**": true, diff --git a/dev_scripts/_scr_setup/setup.sh b/dev_scripts/_scr_setup/setup.sh index ee7f2032..629484a3 100755 --- a/dev_scripts/_scr_setup/setup.sh +++ b/dev_scripts/_scr_setup/setup.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # If no function name provided, print a list of all the functions: if [ $# -eq 0 ]; then diff --git a/dev_scripts/docs.sh b/dev_scripts/docs.sh index 404f2d25..a373ac78 100755 --- a/dev_scripts/docs.sh +++ b/dev_scripts/docs.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # Exit on error # Builds the nested js site: diff --git a/dev_scripts/initial_setup.sh b/dev_scripts/initial_setup.sh index 4fc49932..5fb98b3a 100755 --- a/dev_scripts/initial_setup.sh +++ b/dev_scripts/initial_setup.sh @@ -1,8 +1,50 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e +# Useful for platform matching, can use like: +# if is_arm; then +# echo "arm" +# else +# echo "not arm" +# fi +is_arm() { + if [ "$(uname -m)" == "arm64" ] || [ "$(uname -m)" == "aarch64" ]; then + return 0 # Return true + else + return 1 # Return false + fi +} + + +_ensure_zellij () { + target_version="0.40.1" + old_version=$(./dev_scripts/utils.sh match_substring 'zellij (.*)' "$(zellij --version 2>&1)" || echo "") + if [ "$old_version" != "$target_version" ]; then + echo "Installing zelliji version $target_version..." + + if [ "$(uname)" == "Darwin" ]; then + plat="apple-darwin" + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + plat="unknown-linux-musl" + fi + + if is_arm; then + arch="aarch64" + else + arch="x86_64" + fi + + curl -L https://github.com/zellij-org/zellij/releases/download/v$target_version/zellij-$arch-$plat.tar.gz -o zellij.tar.gz -f + tar -xzf zellij.tar.gz + rm zellij.tar.gz + chmod +x zellij + sudo mv zellij /usr/local/bin + echo "zellij version $target_version installed!" + fi +} + # Pass in the version number _install_yaml_fmt () { echo "Installing yamlfmt version $1..." @@ -21,99 +63,120 @@ _install_yaml_fmt () { } -_install_openobserve() { - echo "Installing openobserve version $1..." - - # os lowercase: - OS=$(uname -s | tr '[:upper:]' '[:lower:]') - ARCH=$(uname -m) +_ensure_go () { + if ! command -v go > /dev/null 2>&1; then + echo "go toolchain not found, installing..." + go_version="1.22.3" + if is_arm; then + arch="arm64" + else + arch="amd64" + fi + if [ "$(uname)" == "Darwin" ]; then + plat="darwin" + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + plat="linux" + fi - echo "Downloading openobserve version $1 for ${OS}-${ARCH}..." - curl -L https://github.com/openobserve/openobserve/releases/download/v$1/openobserve-v$1-${OS}-${ARCH}.tar.gz -o openobserve.tar.gz -f - tar -xzf openobserve.tar.gz - rm openobserve.tar.gz - chmod +x openobserve - sudo mv openobserve /usr/local/bin + curl -L https://go.dev/dl/go${go_version}.${plat}-${arch}.tar.gz -o go_src -f + sudo tar -C /usr/local -xzf go_src + rm go_src + echo "export GOPATH=~/go" >> ~/.profile && source ~/.profile + echo "Setting PATH to include golang binaries" + echo "export PATH='$PATH':/usr/local/go/bin:$GOPATH/bin" >> ~/.profile && source ~/.profile + fi } _ensure_openobserve() { - req_ver="$1" - - if [[ -z "$req_ver" ]]; then - echo "openobserve version not provided!" - exit 1 - fi + target_version="0.10.5" + old_version=$(./dev_scripts/utils.sh match_substring 'openobserve v(.*)' "$(openobserve --version 2>/dev/null)" || echo "") - if version=$(openobserve --version 2>/dev/null); then - # Will be "openobserve v$ver", make sure starts with "openobserve v" and remove that: - if [[ ! "$version" =~ ^openobserve\ v ]]; then - echo "openobserve version not found in expected format, expected 'openobserve vx.x.x', got '$version'!" - exit 1 - fi + if [ "$old_version" != "$target_version" ]; then + echo "Installing openobserve version $target_version..." - # Strip prefix: - version=${version#openobserve v} - - if [[ "$version" == "$req_ver" ]]; then - echo "openobserve already installed with correct version $version!" + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + if is_arm; then + ARCH="arm64" else - echo "openobserve incorrect version, upgrading to $version..." - _install_openobserve $req_ver + ARCH="amd64" fi - else - _install_openobserve $req_ver - fi -} - -_install_otlp_collector () { - echo "Installing otlp_collector version $1..." - # os lowercase: - OS=$(uname -s | tr '[:upper:]' '[:lower:]') - ARCH=$(uname -m) - - # If ARCH == aarch64, replace with arm64: - if [ "${ARCH}" == "aarch64" ]; then - ARCH="arm64" + curl -L https://github.com/openobserve/openobserve/releases/download/v$target_version/openobserve-v$target_version-${OS}-${ARCH}.tar.gz -o openobserve.tar.gz -f + tar -xzf openobserve.tar.gz + rm openobserve.tar.gz + chmod +x openobserve + sudo mv openobserve /usr/local/bin fi - - echo "Downloading otlp_collector version $1 for ${OS}-${ARCH}..." - curl -L https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v$1/otelcol-contrib_$1_${OS}_${ARCH}.tar.gz \ - -o otlp_collector.tar.gz -f - tar -xzf otlp_collector.tar.gz - rm otlp_collector.tar.gz - # Comes out as otelcol-contrib: - mv otelcol-contrib otlp_collector - chmod +x otlp_collector - sudo mv otlp_collector /usr/local/bin } -_ensure_otlp_collector() { - req_ver="$1" - - if [[ -z "$req_ver" ]]; then - echo "otlp_collector version not provided!" - exit 1 - fi - - if version=$(otlp_collector --version 2>/dev/null); then - # Will be "otelcol-contrib version $ver", make sure starts with "otelcol-contrib version " and remove that: - if [[ ! "$version" =~ ^otelcol-contrib\ version\ ]]; then - echo "otlp_collector version not found in expected format, expected 'otelcol-contrib version x.x.x', got '$version'!" - exit 1 - fi - - # Strip prefix: - version=${version#otelcol-contrib version } - - if [[ "$version" == "$req_ver" ]]; then - echo "otlp_collector already installed with correct version $version!" +# We don't use the default released binary as it's 250MB! +# Instead, we compile a custom one that's only 22MB. +# We manage this by removing a bunch of features we don't need. +# For custom compilation docs, see https://opentelemetry.io/docs/collector/custom-collector/ +# For a full list of components to add, see https://github.com/open-telemetry/opentelemetry-collector/blob/main/cmd/otelcorecol/builder-config.yaml +_ensure_otlp_collector () { + target_version="0.100.0" + install_path="$HOME/compiled_otlp_collector" + build_path="$install_path/build" + active_version_path="$install_path/active_version.txt" + + # If active_version_path file doesn't exist, or doesn't contain target version, need to install/reinstall: + if [ ! -f $active_version_path ] || [ "$(cat $active_version_path)" != "$target_version" ]; then + echo "otlp_collector version $target_version needs installing..." + + # We're compiling the otlp go project from src, hence need go: + _ensure_go + + # Remove old artifacts: + rm -rf $install_path + mkdir -p $install_path + cd $install_path + + if is_arm; then + arch="arm64" else - echo "otlp_collector incorrect version, upgrading to $version..." - _install_otlp_collector $req_ver + arch="amd64" fi - else - _install_otlp_collector $req_ver + if [ "$(uname)" == "Darwin" ]; then + plat="darwin" + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + plat="linux" + fi + + # Install the builder: + curl --proto '=https' --tlsv1.2 -fL -o ocb \ + https://github.com/open-telemetry/opentelemetry-collector/releases/download/cmd%2Fbuilder%2Fv${target_version}/ocb_${target_version}_${plat}_${arch} + chmod +x ocb + + # Write the builder config yaml file the builder needs, this specifies which components we're actually going to build: + printf "%s\n" "dist:" \ + " name: otelcol-dev" \ + " description: Basic OTel Collector distribution for Developers" \ + " output_path: ./otelcol-dev" \ + " otelcol_version: ${target_version}" \ + "" \ + "exporters:" \ + " - gomod: go.opentelemetry.io/collector/exporter/otlphttpexporter v${target_version}" \ + "" \ + "processors:" \ + " - gomod: go.opentelemetry.io/collector/processor/batchprocessor v${target_version}" \ + " - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v${target_version}" \ + "" \ + "receivers:" \ + " - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v${target_version}" \ + "" > builder-config.yaml + + # Run the builder: + go env + ./ocb --config builder-config.yaml --verbose + + # Make the binary executable: + chmod +x otelcol-dev/otelcol-dev + # Move the outputted binary to /usr/local/bin and rename to "otlp_collector" + sudo mv otelcol-dev/otelcol-dev /usr/local/bin/otlp_collector + + # Update the active version so won't re-install next time unless version changes: + echo $target_version > $active_version_path fi } @@ -159,6 +222,35 @@ _ensure_biome() { fi } +_install_cargo_hack () { + # Get host target + host=$(rustc -Vv | grep host | sed 's/host: //') + # Download binary and install to $HOME/.cargo/bin + curl --proto '=https' --tlsv1.2 -fsSL https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-$host.tar.gz | tar xzf - -C "$HOME/.cargo/bin" +} + +_ensure_cargo_hack () { + if version=$(./dev_scripts/utils.sh match_substring 'cargo-hack (.*)' "$(cargo hack --version)"); then + echo "cargo-hack already installed with version $version" + else + echo "cargo-hack not installed, installing..." + _install_cargo_hack + fi +} + +_ensure_gnuplot () { + if command -v gnuplot > /dev/null 2>&1; then + echo "gnuplot already installed" + else + echo "gnuplot could not be found, installing..." + if [ "$(uname)" == "Darwin" ]; then + brew install gnuplot + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + sudo apt-get install -y gnuplot + fi + fi +} + initial_setup () { # Install useful local directories (might be unused): mkdir -p ./process_data @@ -172,9 +264,14 @@ initial_setup () { pipx install zetch fi + # Make sure zellij installed and correct version: + _ensure_zellij + # Make sure openobserve is installed for dev open telemetry logging: - _ensure_openobserve "0.8.0" - _ensure_otlp_collector "0.94.0" + _ensure_openobserve + + # Make sure otlp collector is installed as the interface between our processes and openobserve: + _ensure_otlp_collector # Make sure biome is installed for linting and formatting various files: _ensure_biome "1.5.3" @@ -200,13 +297,21 @@ initial_setup () { _install_yaml_fmt $yamlfmt_req_ver fi + # Make sure nextest is installed: cargo install cargo-nextest --locked + # Make sure cargo-hack is installed: + _ensure_cargo_hack + # Make sure gnuplot installed for criterion benchmarks: + _ensure_gnuplot # Install pre-commit if not already: pipx install pre-commit || true pre-commit install + # Make sure pdm global cache being used to speed up installs: + pdm config install.cache on + echo "Setting up docs..." cd docs # Effectively simulating pdm init but won't modify upstream pyproject.toml or use existing active venv: @@ -223,13 +328,6 @@ initial_setup () { pdm install -G:all cd .. - echo "Setting up js..." - cd js - npm i - cd .. - - - echo "Setting up rust backed python project..." ./dev_scripts/py_rust.sh ensure_venv diff --git a/dev_scripts/pkg.sh b/dev_scripts/pkg.sh index e9404ff4..eeaddc2c 100755 --- a/dev_scripts/pkg.sh +++ b/dev_scripts/pkg.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e diff --git a/dev_scripts/process.sh b/dev_scripts/process.sh index 60e5aac3..75169031 100755 --- a/dev_scripts/process.sh +++ b/dev_scripts/process.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e @@ -104,25 +104,23 @@ list() { # Terminate a process and all of its child processes terminate() { local parent_pid="$1" - local IS_CHILD=$2 # Terminate the child processes of the parent PID local child_pids=$(pgrep -P "$parent_pid") for pid in $child_pids; do - terminate "$pid" "true" + terminate "$pid" done # Terminate the parent PID if ps -p "$parent_pid" > /dev/null; then - if [ "$IS_CHILD" = "true" ]; then - echo "Terminating child: $parent_pid" - else - echo "Terminating root: $parent_pid" - fi - # Or true to not error if the process is already dead: - kill -9 "$parent_pid" > /dev/null 2>&1 || true + # The || true to ignore when the pid is already dead: + # Send SIGTERM and SIGHUP, then if it's still alive after 15 seconds, send SIGKILL + kill -15 "$parent_pid" 2>/dev/null || true + kill -1 "$parent_pid" 2>/dev/null || true + { sleep 15; kill -9 "$parent_pid" 2>/dev/null || true; } & fi } + # Has to come at the end of these files: source ./dev_scripts/_scr_setup/setup.sh "$@" \ No newline at end of file diff --git a/dev_scripts/py_rust.sh b/dev_scripts/py_rust.sh index bd4d8af8..77b2a57e 100755 --- a/dev_scripts/py_rust.sh +++ b/dev_scripts/py_rust.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e @@ -21,10 +21,8 @@ ensure_venv () { source ./py_rust/.venv/bin/activate fi - ./dev_scripts/utils.sh py_install_if_missing typing-extensions - ./dev_scripts/utils.sh py_install_if_missing maturin - ./dev_scripts/utils.sh py_install_if_missing pyright - ./dev_scripts/utils.sh py_install_if_missing pytest + # Install any dev requirements that aren't managed by maturin: + pip install -r ./py_rust/dev_requirements.txt } diff --git a/dev_scripts/run.sh b/dev_scripts/run.sh index 2b8743bd..4aee27da 100755 --- a/dev_scripts/run.sh +++ b/dev_scripts/run.sh @@ -1,49 +1,26 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e -# Prep for running top-level services -_prep () { - # A custom env version may have been used before, reset zetch to make sure not the case. - zetch - - # Start open telemetry collector and openobserve in the background: - ./dev_scripts/run.sh collector - ./dev_scripts/run.sh oo +# Debug+prod prep for running top-level services +_shared_run_prep () { } -# Starts the open telemetry collector in the background to collect open telemetry data -collector () { - if [ "$(./dev_scripts/utils.sh in_ci)" = "true" ]; then - echo "In CI, not starting open telemetry collector." - else - prefix="otlp_col_" - - # Stop any current open observer processes: - ./dev_scripts/process.sh stop $prefix - - # Run the process: - ./dev_scripts/process.sh start "${prefix}bitbazaar" "otlp_collector --config $(pwd)/opencollector.yaml" - fi -} - -# Starts the openobserve server in the background to look at dev logs/traces/metrics +# Starts the openobserve server: oo () { - if [ "$(./dev_scripts/utils.sh in_ci)" = "true" ]; then - echo "In CI, not starting openobserver." - else - prefix="oo_" - - # Stop any current open observer processes: - ./dev_scripts/process.sh stop $prefix + ZO_ROOT_USER_EMAIL="dev@dev.com" ZO_ROOT_USER_PASSWORD="pass" \ + ZO_DATA_DIR="$(pwd)/process_data/openobserve" \ + openobserve +} - ZO_ROOT_USER_EMAIL="dev@dev.com" ZO_ROOT_USER_PASSWORD="pass" \ - ZO_DATA_DIR="$(pwd)/process_data/openobserve" \ - ./dev_scripts/process.sh start "${prefix}bitbazaar" "openobserve" - fi +# Open telemetry collector: +collector () { + # Make sure stopped before starting again (zj SIGHUP doesn't seem to be respected by otlp_collector) + pkill -9 otlp_collector &> /dev/null || true + otlp_collector --config $(pwd)/opencollector.yaml } # Has to come at the end of these files: diff --git a/dev_scripts/test.sh b/dev_scripts/test.sh index 0861e2c0..612f5e70 100755 --- a/dev_scripts/test.sh +++ b/dev_scripts/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e @@ -8,13 +8,13 @@ all () { ./dev_scripts/test.sh qa echo "Python..." - ./dev_scripts/test.sh py + ./dev_scripts/test.sh py -n auto echo "Javascript..." ./dev_scripts/test.sh js echo "Python Rust..." - ./dev_scripts/test.sh py_rust + ./dev_scripts/test.sh py_rust -n auto echo "Rust..." ./dev_scripts/test.sh rust @@ -60,17 +60,13 @@ qa () { } py () { - ./dev_scripts/run.sh collector # Needed for open telemetry tests in bitbazaar - cd ./py/ # Check for COVERAGE=False/false, which is set in some workflow runs to make faster: if [[ "$COVERAGE" == "False" ]] || [[ "$COVERAGE" == "false" ]]; then echo "COVERAGE=False/false, not running coverage" pdm run pytest $@ else - pdm run coverage run --parallel -m pytest $@ - pdm run coverage combine - pdm run coverage report + pdm run pytest --cov=./bitbazaar/ $@ fi cd .. } @@ -103,17 +99,35 @@ py_rust () { fi # Have to specify to compile in debug mode (meaning it will use the install_debug call above) - cargo nextest run --cargo-profile dev --all-features + cargo nextest run --all-features python -m pytest $@ deactivate cd .. } +# Used internally by pre-commit: +cargo_py_rust_check () { + # This will go through and check with no features, each feature on it's own, and all features respectively. + # Note: won't do unnecessary checks, e.g. if no features will only run cargo check once. + cargo hack check --manifest-path=./py_rust/Cargo.toml --each-feature +} + rust () { - ./dev_scripts/run.sh collector # Needed for open telemetry tests in bitbazaar - cargo nextest run --cargo-profile dev --manifest-path ./rust/Cargo.toml --all-features $@ + cargo nextest run --manifest-path ./rust/Cargo.toml --all-features $@ +} + +rust_bench () { + + cargo bench --manifest-path ./rust/Cargo.toml --all-features $@ +} + +# Used internally by pre-commit: +cargo_rust_check () { + # This will go through and check with no features, each feature on it's own, and all features respectively using cargo hack. + # Note: won't do unnecessary checks, e.g. if no features in this project will only run cargo check once. + cargo hack check --manifest-path=./rust/Cargo.toml --each-feature } docs () { diff --git a/dev_scripts/utils.sh b/dev_scripts/utils.sh index 858cf762..37601610 100755 --- a/dev_scripts/utils.sh +++ b/dev_scripts/utils.sh @@ -1,12 +1,79 @@ -#!/bin/bash +#!/usr/bin/env bash # Stop on error: set -e -# Run commands in parallel. E.g. run_parallel "sleep 1" "sleep 1" "sleep 1" -run_parallel () { - # --halt now,fail=1 stops all processes if any of the error - parallel --ungroup -j 0 --halt now,fail=1 ::: "$@" +pre_till_commit () { + ./dev_scripts/test.sh pre_till_success + git commit "$@" +} + +# Run commands in parallel. E.g. run_endless_parallel "sleep 1" "sleep 1" "sleep 1" +# - if any exit, exit all. Because this is for endless parallelism, if something goes down, the whole thing should. +# Originally used gnu-parallel line below, but caused problems in prod and with child processes: +# parallel --ungroup -j 0 --halt now,done=1 ::: "$@" +run_endless_parallel () { + # 4.3 needed for wait -n: + ensure_bash_version + + # Store each pid, so can kill all and their children if one fails: + local pids=() + local succeeded_pids=() + + # Called after error and on ctrl-c to kill any remaining processes: + kill_unfinished() { + # Terminate any that didn't succeed. + # That script will send sigterm/hup first, + # then 15 seconds later kill if still active. + for pid in "${pids[@]}"; do + if [[ ! ${succeeded_pids[@]} =~ $pide ]]; then + ./dev_scripts/process.sh terminate "$pid" + fi + done + } + + # Make sure to still kill background processes if e.g. ctrl-c is pressed: + on_external_kill() { + kill_unfinished + exit 1 + } + trap 'on_external_kill' INT + + # Fire off each command in the background: + for cmd in "$@"; do + eval "$cmd" & pid=$! + pids+=($pid) + done + + for cmd in "$@"; do + # Disable exit on error temporarily, would break the inside block: + set +e + # Wait for ANY PID to finish + # The || true is needed because we call "set -e" on all our scripts. + wait -n + exit_status=$? + finished_pid=$! + # Re-enable exit on error: + set -e + + if [ $exit_status -eq 0 ]; then + succeeded_pids+=($finished_pid) + fi + + # In both cases of successful exit and not, + # kill all remaining PID's and return with the code of the original. + # Find the command so we can print its exit code: + finished_cmd="" + for i in "${!pids[@]}"; do + if [ "${pids[i]}" -eq "$finished_pid" ]; then + finished_cmd="${@:i:i+1}" + break + fi + done + echo "Cmd exited with code=$exit_status: \"$finished_cmd\". Forcefully exiting remaining commands..." + kill_unfinished + return $exit_status + done } py_install_if_missing () { @@ -63,5 +130,34 @@ match_substring () { anypython ./dev_scripts/_internal/match_substring.py "$1" "$2" } +# Return a random id to use +rand_id () { + echo $(openssl rand -hex 3) +} + +run_in_new_terminal () { + if [ "$(uname)" == "Darwin" ]; then + osascript -e "tell application \"Terminal\" to do script \"cd $(pwd); $1\"" + else + x-terminal-emulator -e "$1" + fi +} + +docker_stop_all_containers () { + sudo docker stop $(sudo docker ps -a -q) 2>/dev/null || true +} + +docker_delete_all_containers () { + sudo docker rm -vf $(sudo docker ps -a -q) 2>/dev/null || true +} + +docker_prune_volumes () { + sudo docker volume prune +} + +docker_delete_all_images () { + sudo docker rmi -f $(sudo docker images -a -q) 2>/dev/null || true +} + # Has to come at the end of these files: source ./dev_scripts/_scr_setup/setup.sh "$@" diff --git a/dev_scripts/zj.sh b/dev_scripts/zj.sh new file mode 100755 index 00000000..b87d3e98 --- /dev/null +++ b/dev_scripts/zj.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Stop on error: +set -e + +wait_for_session () { + # Wait for session to be available, checking every 0.01 seconds, after 2 seconds break with error: + found=false + for i in {1..200}; do + if zellij list-sessions | grep -q $1; then + found=true + # If i isn't 1, meaning literally just been created, + # wait an extra 0.05 seconds to make sure everything is ready: + if [ "$i" != 1 ]; then + sleep 0.05 + fi + break + fi + sleep 0.01 + done + if [ "$found" = false ]; then + echo "Session $1 not found after 2 seconds" + exit 1 + fi +} + +# Has to come at the end of these files: +source ./dev_scripts/_scr_setup/setup.sh "$@" diff --git a/js/bun.lockb b/js/bun.lockb index be720c30c61ead270d1de15f29c3dbeda762749a..53710a20c1e03d817c4167b23c10ddb146bc01d0 100755 GIT binary patch delta 10437 zcmeHNeOOf0y5Ao&G8-I43JvmB@d;+JKFhLL5bEgXfH>YefSAvBL0$R`$>n0%j zr3Q%lA ?dV@H)Nx#wp^hj*;}cMd_H2gIc^(kxf&S=19qlgwIh>dH zVN`t<)G p|$kYAbM^X6{ zKbrDS6Xej(B~ZtC@&)Y354!(7$fF%G;A37Jf;`6I3n1oa4UIPdu|E$G p2b( zQZPCk5dDMo7sQ^=4)cOK+VKu{4GMA%XE{I}$7KcB6Hp1TWBh>V*H;(^@|yv%zlt8$ z1wi!U1l_NqPWiVJ>gdlxKwMXI0nwjzFt5D;&udWnFn{19i;YPE#m99)N1kF%`FaP3 z{4-B~w{Cv7UR;C2{oO%tG2|7FhuD8MiOL6$$yEI90CD{0H0}m@^gD
gGdjjJ4ziU$C%moyNdNz$|fEcHBbblh~!TjHBNl=ffH|kRUSVBAM$Mx;%8_XIHb@Y3dK8ppZ8$AgSmSFT?K=faa zmj4cVAoS69wW;~^31$alT!unF=93^Gj<3#$;@N{7Yz5IT0Fh^HLX9)r)fcCQWde2d z&txhU*IYo13utxqb@gCRhI+?30mShS1Vs4}z#9T61&H}k0{SsOmqUEek16KVyexz| z=I6WFRQokR^y7dXHBP%ZR2*tSUK-kuLjPdEBpR0jqF#1zXoxq&Nek-AQ2%MqV!@J% zKIcIB6X3{V4Trh~AnMO@qRyp7fGB4O?Kp2FUc;b{c4Po?yuAT&K6Zh=j&Xq<*ixcj z&0?`s0nY)V{_TL+9t;ReD%uiI32-bR^1NUsPMR`e?wXw$wNa0bxsv4un-9377 6rq2lsgA?^j93} zIPP?)qkqAG7`KKQ6yKcYl|mhP{h>Y{a5L1^09&mo{pSGHp`HPV{x1TAEg;&0#!+;8 zn-w*VGC-7f2R&l}_ds0^Fa{9)Fo1TPA4jNT9CZLOZsLF{fCr~je%8*U;xT$Liv>$O zx)AE<=NjloKeo{IAVAbR0}%Bo0OGj20pdD%Xi4dF10KdL7wYK$dO(zS2E=%&({h N$)j;2Alg@eap5?>gB<#Kk49~pS3ix?vl>tv`pp1! zP#*FgsL|5}sQmZE+aOra8L?EpCegUaJA@6*EONgzGLFh$5kQRJo7L2KZvbLm9|x2H z+)TGG0z^4mK+GF0z=42rfVjU10HXYhRg|8 =x?n z>%p3oK&|_sfc=0c4k!Wma|6B40b>0DAogFR+mF%h+X1DZJ&vv~1jO^lfo?YiM4md` zuK?H=>Y{Z0OFYGY0f=&yfVi#-0nzSWx_=X0C-+si`KkR+b_=!c-GW@**}*JU8eM-1 zC<^?sDU_TaJ7_+89O#605om{Vx8s&fCY9m~Kzko(_jC;oS%~w)4h{D93} Y_6}Nv^BNKm;))xxUyz@-AI!AKHcEd$U`INz!0zH;Ulz-D zJLOkzK;)D950FPc-T|VW5bqFQws!=Z9maNd1^s>*v_F72jsSml2pR%w-tH=%{ )5EJ~i}DM+X0Eo~>| z_j*9oH)I#($06uPfA&Hh{r;!Fzw?v$Mebv_Z>8p++z ZisAFDip!>=B|IhO@8v0Rg5g_WHc#y*1$u9so96x!k zarX*X;OznTa%@)*cnk~FIz;JHp;7oSb)J%ZboCF1=UR6!ZttgDz%IsvJokC~2SWn- zvJ4?UxPHzYq2? o)v*i08IniYO$QW) zez$-CUpMf}BfziYMj?srch0Bu&7=|bG?m3(t|8?9aY2A@KoG3E1kjJ?k{h53pgvt6 zb&9&*6$C$Ue>@H`=nL5YG<6Prg?`*`C!e9@>Y mm&r|!k2B19D1)+T;;4>Hpo>OIjg8-+MQv8r$d>Uu54nPOSH8|8g2*S!* zdXcgp=pE!50vo1Z1WK`3PM4^84e?_8 yy9^<{>(Q zaSM0#^F_Tm<&=G&0C;WSPdx{_g|k?}z8#OnEbsaL0YPlYNAIAH4SCEJN`6%Z<&V3o zJE~y?fnJP{;#JC@OlZS#B?97pxe^fVLtcc0!_C>0YgGFvK=j)i`Y}FpX?a6H%uC3D z4n1@lv(#@;dh`HM|BLHXyp~l`c01%$yo1T}%_3+=Iafg3Z@68*cEH1RIq(*RD**eT zUAkUPBb*z->=4|iRs387S*!?XN4x%jIBpj}Tz3g~sC+U3y*O^jjgGudxl74c1EL&x zud_HH6kgS^Sf}q%yrY2VkDqr4SnJ?)*y*8~6n)}mdc5b;0v(MVZ_M{ut)FZup}f3g zPG8{>k2m*gKj7LbWGt`s gjy0qkW%mv5LEp(RZp@()KLTM^&PIdK{YnzSU%D zx9yKAof~srlz42EJixCzOVlO2cV(986KUB=lH(6wv>P;kh{>P}f$P*ZRJjR`ZjJf+ zVvhPZvlTs)O^#?-XMB+zXfJcbIXQXu!Ot^xu3q-)mTT++8`&tM)$hv8Luc6-96P*8 zskx%xExWt5Pg@c dx)h3gu zWf`T H+;MaFiF&r>+3W 057P2h8jjR)TQgC#yVLa}3R-Elx#zOhY_d$d8oXwvZ?&n|9{r64 ziOc)PzKeVEe!2b6eTsbgTlz%nk9Rt3O!Th&nV0cxfTaID(aHXOC8wD^{G8x%I C|k7u2+0J3K 5 kTO_+fe4lZ6AOTx_t~dd58O=d&k{&t~0t9-}s0%JIQJc^k5h&HAzA#w?35 z=XVaQxFF%SP)S=)L~PQ{IXRVPA9uVcUO2q^%`$U|SJ`XD?%fj%d^t0s+l*OhCtvQ% zJ-I~w#K4l;McV0;E$(>Q#lMrDYhhe=r6F&r%k+hpj~b+ ysz= zrylO*v?NDk?$TZg-|w6A8{1xwn|2}l?xbgno=h^YcsFjFbeQtQYQYmdzPYT_y)($X z^77QnI)Wh^bA8=fEjNqmJu=rH-aGoD ywc*2VZ_wy1^~Jt<{j(*;;RajJoHOqI=7@HdMBc0srn>8d*O))6 z+?G}$z0j{fq{sG+R~1*h+&Nh0$Fhw%_2oln$Y+-7KI*6I+_LDa;f{mNC*u;+gkDVA zDVlmJH2#d<4_&MB0 7Z#g;JE;A@*L$_=hbk^J8TsLjoP=qq zwv Axysg;k753X+0;%o?71|ac8|@ z(cUX>yUkj>_T?6tPa?Hi4bze~_HJ-J++90l?_Jj9kBfFrT^HHCZ%9GrHj}M6cjx3> zTz7O;% OjddEz#_ju+?+Y z cjpis(qt9 z>juPLPAi(T|M?@kNe4HmoO d3v&A!;*H$C+ z-+I)&{uCEEUB)D4(W?jivZ8uh8U{t$%xR9kGE;e2+g78p?~`?}EL^)Kf7&jgiKU+t zC5{xN?d#uLGsw+Zd%%IkJm z+&)|6+J+S@^&07li@Q&h-p#w8VA}X<>sFgF7p6<~dpUhXoas4zi38r!M`lM3E!%4| ze2|ZT^cTOk`==?aRV^w|H5fPImYAVQwV3wXNn!`u4~A}464gAl&i0$6$FT~juaZ5* z-rqH_oMky9KB?&QcCngO1r_m)K9Q@0H7D$7v^Ljitg~*MTI*}9eD36_xe>e1wn{!Z z9I!OW_~Aa~Yt9dwRV^~5*QiIur?x~Ld(vFGByjuvtG8QHj^2}wl5Yt}y&<+VZhFeh zrmTJTm!lkm*O<1S*rOEn-S@(e84h7CCkK>0s;q3UYt(XeEgxqgJbsgV#JTj$C5;Ez zdophA$iFDluRZ;>*Z`JiQR&;ng^#4#6^x6I+}_YDr?OI0V7pi3)k!B}Ti@MZc+}-? z37?!pg7DNCDW5)$Ep9nquT-Te9Isn=)inM5>m`LY(OTzLKQ2j^{kdS;GDH47ZR*R_ zgjC`}3y1QX_V5c+el{qoq||?cm*Up8i?^o7oz)I} T!WRr)R^JIyuGbcGN*?s-eIGsz?28JYFd(U5dW50Okn?>J!$NoGN zknC$5U3F}nxj^A$zDu{*58I}0i?_D?_IUNPiKEt EFnO>?feK6ZKW<=MsK{cm4f#tJKvtiP9CfA-|H#E4UV9~2EzPZ@~{KeHL~?u?VE z@X@z1C3}<{$FEqEm-@5vo`rMqz-#_{4}X}O^hjr)ea%mH#;T%-otABr-|c#+5cX8# zV{@ChQd7jbDRVs+3r!kyJJoGU_GX*!ER~$LH&fpqdU8sD<-&SV$^MwRso}%-_$#6X z$Gbni+%UrWTxR%(4IakQ4rNs`^Hr@TmX{e?l%IP(B=hjJ$h#ggOMSDC*%=;b50RQJ z?%gY9qKcmLxT6!S9=euHysEKpRrSP*5#o0>KhJbZZ?Ii)@w1iFfTh(!wTISht1huw z_((CVR$sh0#og?hm6hWMaqBtuD`pq) E z@_yx@o68q&s&D&lAnmDbpXi?NmUMFHAg_1ZR_L!#nHaTctF=P;y@UH^mmh817umcl zPNZgqz~a{?r;mJ|^x@n!<68f5>mT x5suRCmAZsqWBYGM~}DJN-8gmD!UcePG_n(D=)Dm&!YAkC%Uox@v-qMvc5(XVN?F_E-NkG($LMrByEbh|)ZNn(?`GO D8sUhki_FyfSQ^5CX|&BqpK^~oson>f7oROIPax4jbY%k$ig zR7)=}o7JazYrMj4>mkF!#Jf4}?`GX&&BU+E?3=$W-Z3dQVD< y4sM{MNWQfrDNy;+kRgTS8j0bU&SOllm?!CM)sDcizsQ_cor= zSMEDvJm0$KSvRx#3@#n<+VX&jw^NYr@a;SIFZFpRH1t;W3Qv= !RpHNbpP zt@&*=i+Cq7C1YFN ( zmV9K%&n2(ISmxt;&8ZH%u($lQM?$xt3CG5lR Uz^ zHro8KHtf}|&9dzu#v5<+I1?T`QGnDViY1;&h)cF52n~6a6};!ZpI_bnqPAPBm&~0z zc;3e(nZQ{$=G5B-Ei1D2zQEspJJC+_fJ?kMU&;|DBSn#v?G20U#tn{-vZ%NfT-<)w zdWtZ)|KGDE<4t$DdHN&XKWPf?-+XfBQs2TyxyKJ)_1o}ymt~7d_Gr5*o4yVA^(3FB zY8DGE>G|4jux{r1uSCz}JS``M-fQblHVgOb*KPGGm1*v)LLQY_eLT?fLhZo9pK6J- z7v8Q9j5a;3;UKYIRq>zx{?1S07rBp3j-Ar;*vE-)$o;@S?*sDg&YT{dAn&kPjK69{ zeTB!4q0ys9#j@Qbe-@kMdOgTbR*tUp^k{wjiq-eD$h+b7%@YT#l^^J2v+4Z#hmteJ zA66R<`XQ<_Cg#E|vqbfl$~olx|L1vH!6#m2dEnL8!3s5sCBh1)?D@7=n|$68_&fPY zQ*z}Lrq2CNo@?fptaTcsJLa^4Y|6mF3fAkQ1f8~NuixWuBzAOPNPOj5kM;Spy&q+~ zBl-9xv~A_A#!Y$L-cMc z4pu2YWz90#@i=E^|1)2%x87(z(W+gd?v$UZxF=5Px_a<~k5!xBiFkeSR?n&2Nyf43 z$C&VgX5!JQ&&YiBJ~dxs$dzR; z_stzSr_8hT;FRiu@z+1*mQ*B8J~Hf6epQjueb?>XEO+K8?kwziBfO8;{b4f88dHkY zf^U}Ew9jf&bG`F$sne+}ix+iL8x`^%|GYQGhW}NXj+yiPS1Wc2RIDza^IcJK%7=n# zE4zDb`FiC3@kYptwFY(q7Bj{38di Z+FRkYD^n|9db&6h2 zy^gD$2v%L`a8zgx+k2&NV1mVjW|PmG`Ol7Nbu)^+tGF _~vM7$)VygiZ)j#e+ua(5}vei`t`u&>*Rg1wPyBGD9n@%+JD{{({w z8-s1D(t>Z8gvON}t}34S!#{F>)>rS ~>^! zg@w@eSBlGx`((r>Rm9xtognGtdg}C6Me=;3*YoGhh+(&Htohau >6*G+|R6--{ Im;P>Ew!xSO> z7~t#iz&{6kGvE__occlJd%}PXc(BhFWU;33z|R7{2@m{w9{DN|u<1P54+lOk{ulFT zUqpyE|LuUUN{=7c6sPNo v0sc6!kMl<6gt^cB?lX}sr}-!cONrw`_|ovpR|Bw5>YOSd@`1pg$pgOx_*%e6 zyYTxPjtjB>4fvCIus oGV zKev1*;2Y5Xb9(Q9gCh2K03X*c;ga^>4xH{Fa;mUsHGq$GOSri8-yQhO@pHNci2XgZ z{qA5JeoxJDA$$?IX`BK6BN27L@1i*>gzpY~%s(XI{>$z7cK}}p_{hVH=!K7+79#f; z__%*!T>>s{<0s#j`rae1J=jXQ=Gy`v_b;G!a509Q6ypC?;6vU-BL{uQF>q1{e+GPM z!iJ8Y01zRaIVyyo34C+lleNd~{58;gG>SU7jlUTT81Fy3I{yj4$MuV2=cbVHHvk{= z59^$c9qkbQcvvvV$9d;Q2jMRSzAo^kfsgBlw8O_v3z54Ed?VoF{zck4CDNTlb|8G2 z1^1t>#?K1)dcY_8II)c~M1CXi@%+ZT$Mpx>8ApZi?*m^S_!xhTA-C&q0DOsx#1B4l z&G!Wo-oK(h7(?P0eC)IkxwEu=^qW)PQ8D534WQBqkh;N!T_2hz?df20ic{tmf# z;U36IA$H<`Pwu~vhdMYZgkK7LBOb;t1Ydrg1bm{8(>WmWdNiN-&1nu3ei-n{{Uayi zCNhMdPus`6hxkBbIzz(mJA_()q|T`hB5w_Rj317hQ!e^N_-lcW_YW9%Y~wb6%7JeH zd~R$r?Tg4#@2}7{H#&&_^MS8J+sAuw(vCJdD@1M&@W%ijx$r81<3jj7<*4 J $H< ITmDwyll(_6xBLgdH=z9oI;#i% z;GmH4OOK$=U$l$&@7%^uUzx?4M%zan@H&;FLhNq?z6ryB(*7qUM6L$-c>jU&=sUOm z4;=L?{-~4N_}KxU 0H>dr_F%ZB0Gz=oQ9r(PA{~qwk`enw2Q~MIC)bB648vl8~H)f2# z_n!>Y{bc-mfIpit{~Xf)it{%f`BrfAJDp*l#NkhV5dYJG@5tbDn?G&9pTh&+3Jy=Q z{?I49kLRQi|91hOx&FD`zg`0$_dkpqUfix<`LTarKb-iEGQ|G{z{mG5q>g7OCxyso z0p9@l=r{V$E&n<2@%;hG8>075J`g!sc=^fPzezi%{9xdlGRDs>{|xYP{>i*^JAZuR z|2}@Ahg1KxfzQkMHv*sRA2 MZ3>_2Fmi~~M)T8JF4>tA5 g!9^1IJUj}?g!4ChWfY0szIbI9? z?Lfx(|KRpt-NgSi;FJ8tPHy+VyTCUF`=}3}9k?C8G#JPE=ah#&Vj=$9(tNW1In_h> z8-S1J7v^49_K!QjC(m! -hd2Y;0@N<=?+*HPZw2e=8*ZJ#>GaKWLxZ{c|$#;T1wh{$cEi-oF|9 zmkN<%1E1`l|04F=Cc@7JKIU&%<3B);H~SvIpTn@vZTt#>5C3)9|44l*|8W271sA7< z#P1^TVF`8FKEDC={ZouT?w^>)oD^a|5crO?edKd{{$2$>Gya^|Mj2vX$&k1BdjOx! z@Lz&M2Z;T0;KLC9ia)pUlQ8;q|BkUEdjI4Gkuw24?w?& iq-eFWx`k8s?-Beh}~>1^!yU z-0}-(KI+4ZBj;&Z^s^oKyzC#A@bcdh{6}3lcih^~0KO4p{KBY+gF@o>eKvLfjQOVs z7q|BH;o-xF_8() _uT`WQr{P8E@9|S&xpd){9|KQet z{<$oc8|^=*u}9~KeGdj7$H1-s4}tGO+sC;RMCDK}LVG`GkKC<0T{FpJDb@PxC$Vp1 z|M&Obm zw93Dkl71PE`JrdZ$JiokYGA`2IZDALq o5MH$i!^4}9D|x;lO#HjCxO13wn{Ho#{=!M&TT3#@lmh};9< z^D_U_J%7a?&mY_aI4Q*bci>0UR?0elkwuADzQ!7SEF+J0B^_dM|3c<^5d7M~A;&+Yoz z0DQOw>hk{i9{9Z6KRSi}{r*i5_5e-_$-gtekK$qc6JhfN6 &xM zXg;ogF-`^#GGzRpflv0&uH-K}?APB (@E**ZrF)80Pl=_d4*I`#-nw*Mr5!j6b*j z9|S%x&%eq`{vJPW{l5r&ID$LkkLyo@lgR@aGJi9{d3aUVW&VEPuVl &5 LC1q ztA4$|<#g?$3xw|md^OrWr?#2=T;Qt%A8m6QJ0{;Gj>WR)fu9L{M;`d!f$z)%-+m2m z{+|Fo{0b_%tMTi#mN)->fX_?(E&|_!2mh7VQNMrg>iDC8Kbr^pw}8(}{FT@97XLWl zPXzzT*{g_)gM&iW?^WO%0H3T~(*7qEL{282TEDn|;l=I#?ErlAAMN#n3&+7pA@ d9|3$`?4JidFYy!I%v=0yfp5;k_%ndd z%lKacpO^8EOXkgf1n~7>{CMw%X9zh*a6NQZ$o_pA_?o~c=Lc!;jEj9lZsZo~_xE`I zV ;sU9M?i?&bBJ#NSU7WlY+h~M1uHB)|lfBR29Gy94E zA;8D+qg{+4xAwDvPo6)JOY|~r{o7|E_Y(LMz&@Ej%mYpe;SWot<{#^5pIiUkfREqb zAifK6VjN_M{S&|+PxHk% (HaADs5^jR<$s wO z(ogK41wNiXB!1k^U$?DP{E0qp{Wk!<1=vTwaPCMPvEErB_O1b64fv$aZTuy-{rdhb z+T+B+GXBlbJ I31Y03YX{Qy%e)@L$mOInCRD@@3Ph^Ruh` z`M}5Z!)f0A(|!f;Cjq~!_W9xE**M_i`9)$vY;=Z1HX8UC|E}7<4tzX62%nszo!Q1d zV!zi8YX8Lh4{i$K8vq~U*VXt(0ACOIxNi}iocKXxPxENM1^BprNc=dljWWdk=uGPQ zAIFb6I9*4C?+^SbV4uXFTYe7kasJ7?b2<)UUo7j_-|v9?e~k-2cUA~r{~!F$ILto6 z4+cK2Us7kvboQCZUj)7Z5A!Fq^Vj<41A};RQiy#$;N$ZHr*S8`34bT>G5@-{ejWke z0Qjhv=p;5eLn1qJ*RT6G%t1JdI4Xqi1$^fI!x0&T2tN<_IRBj9w-Eh={|)%K|02ID z^QW?#+JA{YqW@1w AU5HSqO-FMy3uIMqS;Bll4G z(^dNmfIk`Ri-2vSv%`@;{2;Qsfo~0b6z|IXeF8qoA5O=PwupU)y}!oKY{$=?6~a#k zz83gTY Er+je@G7X z_di|bF9E(0*vGl!rV#t*fRF2^tK;u?fO`HwJ~uuP`%{6B=NC6z94Fz&0H68(g3}lf z{z>3t{BZr@9FTSRCq0D!4fwp=f9W6mwSLehxAy;EkN*-FKc4>rFm}8+EhPSDfRFiu z`v%!VIMG4)t-vSGZ)6YShHrN07oVFPCiY{1kNL-q@8}=lm(hHzbDBF$zVu=0{W rR#{>e7>6Z`-D`%{0v|KKzSNc?#H_oueQ{OLjd;@ZU=CVJsx zr-jHp0zTgVpfG-aiPLpJ`2COk`u%lpRKj5)d<)>u1p9b?kv2|L5ZO}T i zCvoi8^K)1D*1$*qQ6D!kB=(DekMoc5>qqOLUF;MHzZLi 8udBT;Q7nA7zoxt^ePEkK;#ucn`xZ|G^3B|No3@AGy8Y;-nD& zN99rf{uOdb8#jDA;A8%BGj`NZZ0!X;$v^ag+wngGKJH(f^2peT{Q>#E_CIcP5WX+) z$@oz>x9dL}_(t^nbIL_K#QqxwAKN)Cgs*mz8b8S&Zuwro*9ZTRHv}%s15OIDzn`{` zvFG&JAN3P{4e)&!^H19U m)4zUy*_HUm0)HMI|E}170eo8?_*Q3r{rllvvA+%YdOX;# z2EGLke8WP@KIRRsK{7A!vC~52Rsi1=?4$6wP8omiB>ZL``IF94e?NuVhyL9}r!``~ z5%?2n|3!eo?fOwDqP{ =mXJ%@#w4&|A&-N`GY)c @MqKZ@jDEh7Gl4! ?_J<)!uXMgZKVBAI*8nai`4#y>rUZMBt}2sZvws* z9e?zJ({~_*{~Y+Z|B-!HkYS^ @xNK4daKlxwY>Fe0+XH9_l3X03SOo#Qrhhs{tQlk2*MwA>r2o zAM>}X{_}}a|9&MpD}+Dj((&&%gJWkC8_JfB6lf#u1c`l_au!Pu>~p&B|C8?se7yf6 zaU*((-0zUc9S1(f59>JpM9=Sdq@D1e10UnZP41u$!XI2g{r-&PF1Pt(1$+kM~ZT za+&rw0w2!JF8hCh_8;eun;4Mse+E82zY{K}V<7xVSAOL$j*HVm_(8zO{Riuu+D03M ze-!v*fRF1A*B~qhjtb#F1-=pRN#2q6KPe${YFDZK2j#Ji({)7nLBPlNpJeP}e= z6aFFK8v?&CZNDFv{ASud;lr~4M}^p*e(l%(k7FQt^d}{Rza01m;6KXufs46^VcYKt z;g I1RA9{9L^1_2k>F1PW&0et-ak2r9#5WQ6G_dki8&`oOpL))b7_pDz% zM0O_d$^9> !e+>YN2_$CZKxAVUj_~iW)nCG|<|Gxtt_g^w@Zuf89yVUy|eC|fy zxxIhL1U{ZWUFA0bALGYq+(>}P_|@)F-#;h!AEf_JNcd5}SLeb0P2iLHBRc hA6iJF2q>4 zurLw*iR);k5dB#L7vfsF9TDZ?;llO}aA9F0wkLG7Qi%38!iBueaAAKkTv(Wh?OWhN zOo0mvBJ#Fk1xja#d60=hP%_~{Jy~#JLB#q_xX_>7aAEm3#Qwb k?e3=Q3PaIz!~c<7LOXfMdRc2gh9p z-T}n^2XJ9rtKq_ei1jBl)&OEb#P&M4hQlQSPM|-10O3EDIE{U=0)>faM+(|eeh@7u zL*rl?hX7(c<> PZf{46nbo+Eb^v4nqc{X%?Hr;PW*X;p?px+G;`5tsX8xRW X$$DVA>k zH^i=0@B`;*9iSNCR$88k*tCsqXCm6opxc?)6WX)sb|zxee)xfy13xfM2LVyf5p1Lp z$9 YP#PoUd10a1@0-H(VL4d^-((Y_(wj)-+5 zy3RywH->h!V@l&RKop%pqa}@2G}-{7{A@tv+tKZFX>_3bo#}oTy4@8J=f#t*djVoz z1Oejs7XhNa#WaQiqMb-U_>Z*&exO_oU0(r+{j2DD99>@rh;iCTw =&Wyy=fGqQJltpG)e;EIHUmKKh{9_f%XOiqF!0Lu85sb{tfY6q$>2| zvxOEd_kR=7gbr r6!c zD!LsJ>#ON{XNV$eY58@uJR aY@?j|jVi2Pf09TDrd0nyL9G~TD%9{^%OM7f7FR?+Q<*j`Q7nTYpG zwRAfo>aU~gh**C~<10Ya^P28wBDOcsa&KsS2Z;JxX?aA{_nEFEBJT@bN9+Z40hm;j z6U51-5!bN@)KR_{-HwP zmisqEzf$RbMC7H>bwvD_PS^hpk-v-XXCms^O}GCW;<)$H{Y*sOK4`~r _md+6UJWum4(DI0=zm&!@x*ZYQF9PCxJpjb6NALsn zJi*Fu5c{9ea JL(7rNU zSeQ5z>i@kLqt@|%@5MUO v+!p_g;*O&wua5s5t!hUJO^qzrSb0eU|w?4CS!?-+M6@ zYaRv%3L@?||GgKZ_E+ZnFpLk@F+Tsj7o+0y-+QtD-izUVhz7hz`0u^gfA7VZ@5ykV z{QvL0Si4v|Cj9@&0{Zw^lwPK4frJM;D@K-0UZ1^emB(YT75Sdyde1cES4}857cYJI z@PPP{7jkt*KRg@xy7FPelC0xm?WT_%dev^(Gb*{&D&clE^_>k$FU|!i0tv@|`j<%c z`*h~GbkenmiMsPd0_>m6_~dlfK4Q|2cdEtbt4#F9eR(p+yU;Gq&p>+AoAF27dXF~C zol#x?!ZKMMzwLfK-yy^< z^1c1)Bb(G3Q$8(!qu9@#Ej0UvgyxVNfuDcw4Op4bc#ff$el1DqHM^LwXo-{l)o1=r zeIzF)X|ktE8oYh_r2nvPi4)c*$7UR;YN=N{<|6Pp{IOj4L*37VmL@B@JbQ4_-eszq z=!-YY7 FLL1rbiyjuzAYIWsi481`_)-~ zi1W$0n)PeWzZ_w(+N^Aa>{Z_Z{TH~2=;L=JNF4ATHz@*U^GkQu$;J;zd*iOxW9*%g z(|5HbW=GZq#(#+OnK<@UhG#zi?um_2R+>FBwyYVGENNTc=GNEsOHGZRjp^W~b+!z> z_zs^GfvvH=tKa$U`uXBjjQHG3sUy60 $3iSV#~KR{t9?0O`fi8CS6!|~rQV-*GveOzb&jV}FH_%Hq4eTA zX;K6h^==qy@pkI_?TcIQPioM*bZ3N6!u+Hn;hkI7ct`j7`buP7#xtF=mW6iu>8q;B z=X|d9Pi@(1S+Xu8sIYFgWFRwt@w*kI2#7{Hj(VgzDB^+M^Y}R`Vi8m3%!|HM&Q9y` zEn~*<&Ao+&EZG;AFjx3pishsHSygt831#;kB@cZ`yg98md-U;r)OTqpf0=(LB+y4? z%ueV0N5_1oZ`IgpYWQ)2r&{}o+V>3^Dy2II%(N3SOP(}tl;ZAj`7^HUs2u!Z+m*LJ zE@qy%C-Oyg{N86FJGL|C4S)AVih$a&qNQ 9jO =)=yp`)FiR5#qOr@Q%|k_>Ek>qB2#}RRk@kYaSZ!BY5M`Wf>ZUE z#yh`i>>D?Wp%?$|h!lZa_HQq{)_>3ao|q|YTPJJ&_U8xV6~VWqTf)@$c@@`1Zce{> z=)|(U14@r*mMc!aX_|Uwg3~62RP)#G?rhLmP~5=Ki{H5-MPOQ=@mX`1Bz(VVb33WN zw$Jg?4odwtYueZLIJd8L#h7)=_%t> Mgzl4@OzltSoQvxw(Z8voT 3Y^iVK`k`KVEj!%O zWkT;7 2#U$zInlZsOi+#dp_T1?-h`B%D?|MiP znD#pF+jY74>WuNPE|1(1S6LV$Ft#o!$^Z4Cx*3j^HJ{y|@MoXu=ee?}NA=@z{y{dA zKA#i4bJa *q#3UwVw|n4l=<1@E?;@+=PO&@ zxClxqZEz0riuqtTFlEn_Ek&<#Oa$I7)qfpuJEq|hyW5w(J{krZuNB5Ju!CT`{H<2#ie_c=1Y&NXqjjGUgog#j4@y2hLr9d zeeRv?VG;Ze9@!@)nR*8~HJ!L9X4w+eE%uZ7M8EEuuAcU<@9`gLkaE+UqEXXl%ZjXR zL+bdXAH29!v`{5q@+Y56rOw5=!=p-f{ahG6r3XVVKEsnDP;f6}ZLV0+mf7aLnvcFe zWaE3+Otb#y`L+WsL(0-t->ms+D*CzSJk|>f!IC nkA9O|>;(r-DD`t)F6CcSID(;f08?*xX7He0V<(Os{>k)cQPq>rQ~P_}s!Q89 zeeR)Py(<$P>kJ(4y^;NpaY69CNb1(s%3Q^PxA{wR9k*<4&j@Os959TbcOX-*Pqbc! z(y(nJj> AlkNmrg~_(sO#2`I+yZo}t%&?7W6=pLfqX6}ntH;Zt4@ORcZR0xpy&?sRLk z{iz(vJm+PYdJjhIlp5^3B{Q+r{fgWep_^jQehRe>w_Bg2tRS{8Td(JAnIGNrt !uP=x6?{O%^n-@#11O_R!49}3!gjniYv84t_}n|e$x zdefqLISV3vZG#8*n2_Q$AZO>6Y0pMyt`fU{+G@+;N {_qim{ljYv2^D6fqaGMEhKeiOWvmqe;G2r zr%tHr8Mcd5&gWXqA5BG<%XdF09`Gd8%f~iuPaiG*kaiR1zAneqTRk$fVWs|_mrp#$ z*v_n$GrZxjda%Zf(z?_;7q{OoY#Eu9b|Ul2ny05VpV{i0+b8wwGf(4=UY*60m)S3G zYd^@8WB4o2)H`?D8I#_IhgWE2iCmofVJGWQ-i{){6?I=CW|iu%5nc6YUAWg8sTHeK zf-4k%&bu0y@o`k3?Ww}^k2L3{m|v0Dh2JSA^QOSm>nawrZBSColg&=aC%;6mTBf}5 za_HU7IwpBN+rDPkYWP?8pQ?OBqwTi-UB_mNqVCl%)_mywH0!qJvEoDKGj-a{8G03& zdP6n6N=zRmi&m@{VjVJ2d8&r1xJ3V*KXp@X`nBGj-rAOV)32xgP mp}r+qQaX-d{F)<(X-kpY_f(bvrtycC&+# z?cyHadZjN^p3%>bwL~%fRQvjk;`S?xjNS8Evn=XI&Nj~bS}4WPi@&QUML Qkp2w1EAq>64h*Cg-3t@5I7NTY6!XFy Av(j;}dogH-AcXo*3A?0$r zhT7WK`6~ zL2*VXsXtZqC?rfBW|P=|}j_ikx|U*1^$bvD$58Yt<)r9x(Kh z?^~dHfdcV6kIp{2etw*5P|Al(j}Q8cDZ8^gYwQZ+j7_eQ)>HdFv>E%X?&sH6krAh3 zhYo2n@?5=iZD@^he1k!+{#Lpz_ZfOeG5xikB)#|bmgM8|i37%4<|_qn>2IZ@+23^S znbK9xhXw_AYg?>aQ`zSJ`gM8ElveiX{FLOa``0wiDwLY+KCb`5uy+i-qnUd9y{HtH z>Fc+5XU{ MHelQo@A4%Ky(&z-(#amFH^%f|P-$)YN%fPb*iy5yT8q)6g {_-!V+RRufp(y3IwS2OqukAxpt`_jZTGrC#^SKb%{l`%}NSi$igJni4xrW=7K2 zGxwTDc#mm&sMLBOY}?HJC#R);Fq7HC(5uGOJN5h{?H6e~osLf}-zyvBJW@2#NZ#RX zm_)40+|6E7NBd?z?`awLX@=hOEh}fcmL8mP=4Z={<0gvnF_(V4_FmYhjiGleQ*W5l zfl?*^%?2rh)!jsow!Ch*>gp4JVfl?vF$q0U`O|Og4a%I2-m|M#OvxOZt2W2X%G2fJ zrID()?2oEF3~2Ud?!V)hdP9c|T%B7Zq-CJ&-^a)JW2wEbk>;)q_gvadFSq}={pgX( z%VOOFx>xVtTv9%I!+y&!vp~ c^_<$9>#nz5pLBK5 z*Y*iNMhFL}rh0x9 2lGaP!Iq*Dg=;lAgc1jP+4l zKZ~JPovHU?_~?k|dcCg6o%DS3Wl)Wld1Q0BR-@-|cD3r&St0i?cnz}MbyqI2Xmiej zygS|#_P%Tu^eN%9t4iTh?77`WZ9GG-22=0AU3&HL-*448FY%I}q&B;c*Flwy?zhcu z)v*pX%2#~~YS?^w*61ewm18qT4tSXE+ N7u0^yuRqyk7Fm+EFWq^$I+p^e*P^ z*04M2B73w)Og$o<#?U*FsrPXDtz8xs3m@(snR_H=*Ft@T1#fec&E`&?c2oI|%l %CQ=iA+_>b z^Rn%l+6=w$Z!r8-1acCi&kkI2ta`(c&ufn@y_A05s;E8W-cISjgO!_`+Q;;A{QOqA zn!lwu`f1Tq_3z{BtF~?sJntl`Ea&L4 5)O- D8;{HGO}Mi2(`D<6Z=W*a0RI-iUqv8#@5hLcmqw%Sn++8fJzRYBv+|+l zBURo?gLeA;DEHoxvQzk2l6zeyd%D-x9Ro*H$iArD|LJ|@Ojcyci+)*;^O^7EwExly zaTqppy;glzh(XUBtA$BBWoJiA%=Q}pwoL1)ZcXc=kOLF5bLC&oOt!eDH0JEe)Vbl^ z9mm8)9or=Txu$rUq-R6KZic@)Ouaq%J|CZb_Eo`tnUR~UJ-h1)YN jS8(Pn#mCRTe zWF%3WvWa WNG&H{yT$r17Xvme-ZCm&o69-GX81>r4@K>Ly_vlWS`}ITgq`#P!k1+@|RM&E; z(Re*u(!}Tf;ag8b-=A7sIk&o>cR}H;`)#u_epI<97q33emwL_ro&Lff&B3Ma47~ _Hn6 zW0KR~oiki&X!cbjr^SbPKWWI+J4<_@;{JigcWU0RUvnX3z2BPRGtYgNmW=J2yrJ^* zk7Gx4BcDiJ>bHN&!6lM|jZ@?fO`dsevRGlpL!bTwHU>AeG2bg2G4<*- z*@M4X?wH$S7gt~FY@xLeOII|ioojylXzw{b{*MdyJ-RL}y6T7grJ>RNKHR9f(W2UG zp2*R=4$_P`7&G-Y&A$~9HvNL@!?7Au!{oF_jNIR6)dzc}(d(ba3@&k)R^51Sf#25_ zckPFx<@(vCW=g#uYul^E{KZg-$3jLQZ}t~s=rv*L^*)#Gw{pimc2?L{;cf5v1p3x| zkN!SlqUN)eX&)!32+k|~(fh=+PiCW|j)x8L89y#O|D UCd{_H7i~=S~zWp>SkMMM07O!JNqbZ%(vGoxD># z=#k8jlEe{1_b1&L^ Bdw!bzWBuu~C$`6xO;^#|*z9vQCg(wPt=yJr zJ++lijc4YWIa6=3cQ^5Ok;(P_PS0MU@Z3gM<$tkqS5a9#-^0L>4ke_!ySuvuLAtw3 zIwYjKySpT%8$`Mrq(izT1SH?D7rtwL_y2u!o~zH=YtB69%$Yqi)1zTXI63Kar|0Ne z^!{$|tNVl)R0jxk)uQ_+>-9&2o78akMW|k|Ep+T- mfFl98#0{20dC6dNYR 3Pev2LC2$Z6eGe6D<*qv5ru9}8-7NYw zrU;YeyPP@2=#e|Z$(yjD#WssscK%fRIbZ7i$pO?h_Xi+f1<*|mDtqLx^^ADx?h5ME z>r&XEK6aOnyIXo5@zIwdm6w{oE>o1#RjtiFJHJHbk78~7F1po1D{`$X?E=|(G6{~W z|E&xE%R^JQd9Gs^Lke(mu(I^3wMTw_h9jfrHEGp@h8WMD=k2)`Sk3H7-P5>o*=H4* zgcej{2y*;fwI<$0pHdD36-Er?`vJ^1j6Rlt)v(g_f!L;x{MoTokbj)?`>e!a8)ANX zq*_{U+r9x6;*NaY9!&D|t5g}ztM=SNZfzo%2uFXNW9^VI!2P@D^ 7uie z@V2BJaFzd||6QTigK|EcqO 2lEO8?%(q!{sna6!+_OiVPo}X9f2ss>nUD0A`-77 zo(hXE*dNDP4=0jv$B-y!hO`#H24&$Su&)lh3xM$$(V)j_aScr;Ma%);^Q!;Q|E|!9 zYhh2F0u@orScBN`?MI)#_f+%spVOq)B892{mN?nq((%nG&pK0@()o;;HF}{X?R!TC zx61|@$u~NQTpK_E P8>~HPN|3196(J9fW#r&R=7syuwbOSP_ zIpIl3mR6sy2gu&P 91hHF}i!ed6jdZ z3m=pHGDjE2_ID5Zzj*>p(A{06b~UL84=ULmUbIX!vQ{2{U09C&IuP#ucurBP#)1s* zGPb_Yt=~VuYBYC4C}KV!aMFOwXCv3doq9cy4jfniJ$K|^Kugwrdgcvwm=$y2{8acO zG3)K{qZhlD9?H4f>Yc`2HuEu+aGDiU7(Pu5v!o?V@N=qKMd~NMb>fjq-_oFL>`owG z?SJThSLpk%rzf(^b0Uk&`XR2>kxZ1oe>Muf*K5$KI`G+{>b}sj-KM$bJZ7vf#4O25 zksQ&cma&d-fY_TtuEc6EBM1K%I-r|^?b~e-nYaOG9@NvMW@G7P5SqLg!A5Sawwl )UFW Bpv$hTytCaCYaN@qQZ-;Kw!@Vg`q`$#LEL0Ss*9C)hWhRFn7 zvd7M!(JG7>3Q$`ed j=tS)Jk@m5?n>vrVB919iGD)tXW$x}9B zZM8 XisCL?+CrAmFA`xS@x7T_9#ZZa|5 z%Y1uqhP)13bQomP$sAE7?v*IydE`i`Jg2blTSgE!BZZjZZpA@LOamqFz%vUz0!6 zc#(EmCCk|B_5-d7=vJDo;F$y!^q@u-G25)eDf6JzuZ-yIo8mVKh$lAjhGQ{T!dP)= z@sExDv{bx}Ox@2S8EL%oVa&jruK&>0M+vy5pes5=Mr!7vrGKO;ufv^g)?9ilLc9xm zTY8gG9M-sj^XoJHf~EoL!9+F-ljzJ!I2NLPn~~#-ujp3R-coj=e>UKnf$m;8`8*~W z%{Bu(xtzua|F9eAA9C#B{M2cyrVz2m<~V`J4#6i8*%*f340sk|lJXdzkosM3ZCPbP zML92?lm4E$@$Y$N{y**>X-)j2YMjcqHii`BR})T}L40WBm3W2>@@do-3eAv}4#M7~ z@A6rl7ZW1TMb}=d!JOZtwh^4AJH0zg@s|W#3()O+XGg1gNRp=O%;x_QVdf9MZfoGH ztD`q#mrakJ?!jeOztYQIahqpAZpxNqtD#XK+vhC9hklP C}ugOTZ^^?n#qE5wZFe3b_5E}n21 zv@5G&){7N5!w)d@X81`@ zld@>|LHVQeGUbUp-C`{NkohN^=}FTy@H@a7bkFjI!d_zjw7~oxme}8=f!6IiPA`U~ zG+Cbss8;zfb- ziYg;h*5ADw|N4V1=&}j9VAP(bj9~g;?-Q2TkF?Pcj_f*aYq@-zg1wYg+fcE|u||Mw z2$*1~x5j<5>ofMD^6-S+7)D~BdOrQaN*!?RKsPqh({WHpy{E>|i{@UG(m8$IJngh% znp(my*nGYYw&x%QHwXJh)cv-&+^$J8%@>X*(CgYiY~Qb0wkiFxcLs1jg06c1 9-17(rqTd}nT9-sTd{V}=WLg1(f0i<)viNA+=(H(wgAojQl{-)h&tvvRcBOfQ zWx|3&-+=krgRXnE4wo{XJN}{lFD|P_y_AA-jhyC~9SaM!s>BM;njr6%|0++NtY4z$ zD=-b_caD$`e<_+!II|L2o%GOh8Fd5sI)JV$zU{jTsE>%UFpbSc5c2THo>i}N*hdAV z7; Ef*J3G z>pvcv6V*ia>znUjy>mK^6`Cm`-=Pj`8N%9;UGn6(ZYK+@Grb1slMi_(Eln&vcR|7X znY MCrm*#n zsfFBkf?V)}?E3U^M!Jf&H}^Tx_=?2x(>^2S9!;SK2h4|Md$8W^0=gFnadi0*GR>cI z2{GmfE#qn_?c}PPW_%&ts3hH1Yz-GeD-u=|p`(Pb%fAlYLbkWd-SaDA2XmG-C!eA< zc!KqASJ36!gq9UgVr!Mr-5;c9x&N4Ww!Xl)fxpc;CnLV4uPFQ8KA=O3=o%sKB4pdJ zl`$26t1ywAg`fY;ZaO2G#TEl_-`qf#!|IqG=IWgdSD7SP=p~Xxg6NmI85@K2wCCL9 z0@{PJgtwZ|H>DQt9L-K&j*%mOVJ_9w*dV!X8NyXC^`7eA0IoaedeWr*av?Ihfm@`b zUTUtO-#Iw29$6yL5v+uYvu;r)i4(n%wy(s#DWZ#SQ!K0efH~~#KF4le&e34Rp*FGx z_FoUsRd?+V3+@ Fn~GHA# 3fp2!eU(E&d2~Vu zVNEmL_Jv)hq?qczVzBUQAt2vRpv#p91=nvPpggvuhuPsu)ccyDS4thGfG{Aoz{Bg^ zI#wmK5S<5wiP1 !y6@*^h__K2! z_wN}~|DKax|Iq)g&~vB_FiJ*p_b%nDe4d`(UgyXtlJxZNV3_%pqgJ;Z^0+nm!v|4O z4A{?vW{RSu1eM-FYpcCn+#!0NCR{Sh$^))9=*FtDn+(NrT(lHMne%(vO(Z$KA1X(2 zCPDQw_I_E+%8)6{#cn@b-^4CVZe_A;{*n9V>cG*s*ItJy?M7gh5S%~r0o`M5#Uzra z&}_AW<~nK-B~NO7c4sJ>p3=~-$=96%8$QM*)p5VJG|a-vh09(VYcGCI$lUB0Ia9a6 zHtj_6z5>7BK7+13tll>fM5YWg%;*R+%zK5$YVJ1jn6BL(8NLiX`n;%4qvM3Hb;=u^ z 4C5n{j<=h82M+I`dKdba2a$=*jAZ`I|Ye7gj>R_om-J^uH@~ z!}}xyp0CkoU%Y>}r`oMXu}9e`8rpZ5LFTtbebkSwh}G~(Og6dYDEt|)5aQwd@%KHc zW1nV6SUT%fOx7Mo6OgYz=q9RhkP?it#VjQU5Rc LE)kQrJlB) zXeFa+?cDCbvlqW2?%2lR(AoP{X0H?(v^(rCpNaNssOJPlXkQ>LKH*eBTN?syAn1+} zi&byqWeAj9X*N-2`^>5Xi3-=ST32sqI)Bq{xAU?0W 3HD*zwRZxj3gi627zuPeGlD~&5ah!l)Q@-eC+i`OtC(Dr1cBueyh5KxLx_zjU+k4 zv6b&|{jw?2GZLRA*PCa}5G<^HZR3LS3FpDOUNGp=aXA%PcadK_5X49)9l;VwCVjB( z5|@|^Myg=!s8Da;r)4o5YmuRDxu^a;D{|1_u4*!z&Ei@iU|YdeD(?>Vvk=g2&2D{a zgh-a+^&vv06{0tL_3A4JmXU!LrGH4RY!G#YQU1K_oa_D8_S9iEouJ8c<}TggxoR6? z-^S3oarX>ZFAN1;31P32mcHg`q>C_!fW#tS=N1wJv!HmJn>VD52vjr8q*%H0gLMnL zD8#a^XZAH35Sy1uh@=u@G0yJG@V72t{|y6OzHiS7mls^Aao7CH!^_m{sI?CZ8hm4n z-RZwi$>qB6?5y0NsQ$oUJUiR{7H(<%n)TL }O+nJbb?1G*vw zgMJbrTGv!n$ZyyC_s5)JW^MPm?t$<5NYLeCdO|MsKR1`G& (*xHF9L(Z`@KNihf CrGK{WV@h#31bJHn- za%nppXy0hiMVNXUHKW*x)m|OgCi!)f(NASO>Pncw-{9?Q7qez13ZxOvpp@}=|BtFL zoXeuxpZVJK;j2~{4?F5?%!7~6+5tBPbQ7-i`uJJRd7PDe_|2Uz-8mZHE6!4>&Jnor z7}h FOLzM=;=G9L`R0;iRvmC-LDy`KgL3bQ_rdrl z*6E(wt)j8+tFpu!>?@=9@*KqV&s;&aL^a+TlcdB~s5|E7<@j+V;lVTqZXtt*M&oI@ zUT%OJ2fBS8TgaWO9B&)oxK%01f0%oHP$Ayy?rW7shB#9hzLgw+I17_`Sr=gcSSYvk zWl!~w$RDZ*!fq0X{3OCv4L^FojR#$OhAx#B+B-FeOokD`F!g03v$c`KG#l2`#+&-F zmenxUj*ld3u$~CFy-mFzM%60QGd|d!*L{qJNd7o$lD7&2xCx+JHfr0_WKQ} >D9MxsI jU5>gYNgCWzX7Qv(#GO?~G|1 zhc#;!e<5FL)a6G*mD!JP;l0Ww-B=c_M(2BEoA8Nk8^c M<01$r2%x)z;$^G&2C) z6ws~FklE?*51Si|#`bbBS+GtE)$C2Br}yXdUDeDU_vKp>Rqw!weRK-#N$rB*)h_@2 z(Ui-QNj_@$MTaYpOb{HWrh;yu1Wuxc793hl; 9oa24bS#H5LA@xCF790GD`t5S{!Z76E8`mTZN-;%ekVQTj;HHDF z2zhY|tZZX3&5dOmOixcRCmzA#Ft+nzy_&I$P ^Xn5SO8fQ=;8NilqpV9bnb#T>{)J(6t?t zr-9%6;rL2choGM;4Jtg|In(t6%<{72Ph55b3xy|w;MBKbB?+%=LjMexm1Qu}{os$k z(^N7Qkcy- `=JrK0ujt_e7{oFP+U zf i90BNxbC_JFn43chm2@z1Qad%>$N z1*~pZ4?OzL<&)+LFy2>j@&r#aA55v>AU~fkaLx0v;Cg?hs>Qv7LvI21b>xEX^=eT8 z=O<+vj}%1ScQJa+D7jN<2nIfg@G}B|@Xm|5B0sXAriMM6SVz7;=f0 <@XMt8bFx)m%sD|9h)vP4%8Fa4Uw`bJ5^DqLlA4DeJAy2IK0IkV^kV z>UVXT1D0lPolCU=-^#^qPIeDxEMKh35uhFNLAR1QPIVSR`z5O3j-pw ) z@h?|HNt{1Sa%^d?xp@=IJ`jEiXKC)$wJowQjOb6`vE9xoV9>fvG*{KW(*MaMpa~IZ z&(-l6uL-Q17J_cC26XGw%pKn-0^|dlP|@T8RZ^->#f;amLqgQo2~+a!`}{Zaw!vF* z#M2=NHnAr`PBu0+zV7#rvib;-C!a5Xb|?bfq)N;e_2Z(CB`~7Wja}o)HX_heH(Dv? znYM4jHhF_!AJwK3HVM}n`s+GJFvAB~UfK8XNSNrhyNo?K>xsJ^0d6tq>N~)9q7bnk zLLL?)4OTLVF<{ntpG5v}*CCi+)mE1` @yh$s%q&?P^9#g=q1E+`? zguzht8gNTMH?M3ZNH?C;Q?>QvF$|q*NU9~f<&I>Btqoham)X?h+k)yX3(2_}RCR~Y zIa=J=^VG+CnFa+f20_Fmj$_T1G{7wd-T%Y&Twg%<|8PB58R(AJ_*9aKC~B|7P>S1I zTGJ?v8+I5rDZWu6&{_>~KymcsTGi1@g3NI#m}_Od^E(Kh-NnjoT7eN^2-7!Bdixe= zhjP%BM1vF>dWbl`u+Km=8igUGv%*+9iG6nCRh_2oVi(S!Hh_`b;51L?d*iS_fV3Hp zoI3KP;JtN4vN0fykWUG&1E~OAmy@nlq?EO)8qOwZvTfXAZjvaQNR O=G-?liO4M zxf!=et)7J#2wTkGXAZd4p! z=$X&umtYMy)JisDh! z4xd^Ryi0{{Iqu5&roIF|x;?!fn-docx~yg4F&5jAt}7Kv0rIT@UCQ^T9xmfCMqH|- z!4rD|Xx=? uP@^K`$M)GkNy)8<~9(P2QUgV_3zg3?!` zBBnU{GZ=KFw`2u|%wIghc$@&Y4s_819ZKEi9ySPy6)0bs<1jZlKUmTa8~rY87FVEF zbN^x*PE_M5M`$~cJ?xaqaX}%Xslk4r#=qbc5FV qic_^`JXEUb^kETBaL 0>J9U^} Q4CWi}qkuzWVI&pp~1}mb+ts zV&M}h=Uh}aa2U>cJQnu%O!j~CeT|@7u!fcG()k(A)Gw4s@GPESiBl6rC}Vs1u&?kF z;ry?v_KGRdg@(R0LaP(6>q&K|SCb5fI_JOG*tDKI=DHbY0k;Ws8Q7c!m7r0tKaa+; z-#T!ht+DBSqlF*{(T?5O4Iwl})LeNAsyU{p$v8Sw2_5LDP(A$Z$=a5O=xvQZK??B* z)^VCaH^_4+=EnB(Y-LAL=@N%#YSq;@d=hlAt{L;!d7tx8BB-Pju;7gAa_GN{jvZ{A zz|40S#qu9EISGo(p{P_Bfb*#>pu4YICP?0s&kE1|!)fQFD>Onm1kzl9A&>Sk9zMvQ z{HLXs@J(Lk$OY6zCenlbz1zm(k7|4Q1*;GCWZ^q+J}m?7& MZ%4u$eK!`aqnNvckd$w)`SM#ZLc64ddTOvK!O;S& zzqNtx1XchtH6hb(^sdzSP9QZ_p?uJfgIfbz;t8jf+l+@4lHdI3@q3O%-@+N&Je^=+ z ?%q N2m8|bd1dvR7!7+*m#!t(AREUeB6 zr5{7TI#Mau6jGJiJ{LGb?N;L=Sj7z?`l2nYpw51{LxwrxN;nE>dR~eL-7x{U-JmOo z8BDa3w6=LFl6aoAz)v|$)U0{koLJ5G-C5xA#8c$YQo47UbU(=jSv|}V*~J2`(uBWn zJN`ot$D)4dYlC>e{SLY{Rw#qJCTvT72mLo^emodx5)2>GErMF~OG9`az8;UN+;iP_ zDNYX;qzH@8(o ul!~kP2M^+#b-SyZ6|iKGrajp#I=j$M~*&8)sdr zlGs26n-+c2Ry}R7{IHda^ZgI~+u=XRjhXVMjkb&rN;5`J!{&W^^=|Q0fZGeYuJAJz z&x2Mp)9lgx7*^+d(ou_EtR=R|bGOT~S`E1Z+r7|xQTuJwAw3tT&&90per}<$r&>u3 zTN2qBt&_jz0k;oyANL~Y!8n`ci0CdTpF{oEg zZpDjW7x*G*5_vJ@hr(okieP5+e7Vloy%4Wu-6f8+v~TdUMHBjXZCS6Y@C`?`nucQh zGel+27P%CV?;z-EZdyHx!YY$7P||sQAAK1uM7~$^Q4U|#uKbAkr^ADQU2~Dm8`~3K zILxC+Fp9P=#Q*8$%_nx3XVGG5ES`GsJwF7xH$M;q3Nq`xrKq> _&8O_nQ&n7Oj+bo#CdU+u&ky!wJp0BnvkMxLVI90r?JtE@VITz|Sqo zNU|Eu3~Hpq`B$IGoeELn6$Xx7e|{a9h@A5(rzdEx)G=@A8PpZCxiaEbq9l(ekXv z=7G_X_ Kq?Jy3y 5yIc-F-6XqHCKch5Iw?da*XZ`MwFzEpNz0h|&}xvh1b}VQlr*!_)XduaT0O z!`u00S~jlx4gYG1qbColA`2De$wgNdD#}VoLseyvyYYS!`rW`bI6j*MUEaucUHR=e z3T~vtlQl@(TDb9&a)jX&k=s?m-^JOpiDC^*R+@)zgQ`m6x Z`aup1>b_}6lOp-SM=#xapVrC z$i}xzbLRKS)z`v^5v`nE(|4}o`!kK|bCJUrg0EijsqVp_sK1EW%H<@`6bTiu5G}`# zP&m8q1MM&ix-%)5unr@pr@7hWJ=IXchb9E4J55vS2Z^WjSA&OtBGpx!Y)bu#tS+R; za#N<2YqqMI>fka3g?&c1#5o2Pw*hw!bg$z+ZGTVChcTb1H7p(Br^hyoXDvC#7kpz! z^LSTsZUz<0_lLI;^)PEuC{HMGHc`&}yI3eAL&naigg87*yg%U1gDzd7h$I9zInr}9 z3rE{Lrdr$9UAoOzCHv_!PaNBBjNS3mC1I?)j5qd3M8>U)yt>4}vCE(H$VbQLODmx@ zX>I{`0d#GGVMU8$iF3_)U2(ErHF+VdiZYRH>T@eR4d+1#)tiY!ns##abl}B}G1Y_$ z$-yYLxYHL+ z+qqNb7O?xGj|a(|u|36)zTZVD96Sa;YtNPSxT@Y4FuZ;&nqJ#VQ{~U#%Q`?(J60h5 z2(-fz=*~apRR@hGd9@w$G#zTABfqdoAv9nP5;=`B7H*T)W M@p!>Eal!Yqi$IA5OHLO=QD_Or~CZkU^avs*{^H+}@ z)HA-ipJST144t}P@Srj8*>h7TaAkIiGoC~GzX}SO4uJDJE1)~7$HHslJ)B{!Z;~e! z^PrXAmw|<9Xd-gV#5H$WNgEtQKalcg#IFSFpek;w>@lLMNrHPVfLrZ`wkg-dhTjCp zcNKIYG AiBDQighx%|$kW-o^uM{#M%}R?rH0SH?RGI@1*0VN1 zxAlE{{IBU0?2*(HPL+zo9|cUEWT#QD^&lHH1iy-Qw9 @S)SC3e^)Wv6I=<2 z>o;w3p0Ng_*c-)=#t@SHToD{2%cr=7u-J-AOr>Hg>A{@gb7o?y{C#%?vL6a?e}b-S z_!bc%HB}&Sj+E_zRY{N*HH4`RGw(!u`s#JhGisJpSBEEYyMF8Ok$PUWVDHGw(>ik7 zwitH4p01D6Cs=pD-3Hxt5h?j8(Zs2#uzPGHMv^Hk%I_RFN<6vckcqLFENHTXtr#~V zdkq*D{?yw_yzo-PfvvDR!`(LDa|!}Sq_PeGcL#K9AEa$Ek|?8(>lcig)q--YnDsNO zkVw8ev+sXxfb#oBy Dc z6uxf=a8E!tUWcIU6Aura=LMCEn+!CqX4}h9;l_JJ7}MbjpV+4bA}x*@`e^lG0qvwU zeJMj;i+PO4#wV0o^>=1IGO(;*{o)jKbz05BP|knXD_yw|tXCPAPQ|fi3nF~)wRK-% zL>+H|x0oB<`KtaNyG1dRUta7%1y_0a#vb0hxKcrKBEx{t9?16$bXTTL1!_%lgC^0c zrqOH5bdEC7N6qR^w{vDSId5TWCj^<&B 2;-*J57StaIJjd71r{;8|-FabDj{qr?#E+9q VqK8OQ1fd(X#+DOSM!1-ccu EvSrazI zd@0V0eUxkzD P7b4ly+-{kHck739G#Eu=_8$Bn zUxMz@+D1MH{7u>V-JYcygzptGeWDfTC*4MW%q $_`%bK~GGNR9MA7BippzMP^9c)8kDzghb1fHY&`%*3Q!T&Rko>UaCT*iG9x_ z3YnzeSX`FHmC3uangz7O4d@!Y=a;6Et07ZUZ`njuvtpE3>LpK4^dw{M(sJ4(PYTXH zs+Vmz*nxv4*g-<8P8m^`e}6Pg+51QM#;%YuFc+*t-h%E#Gm-S~E!<#X_j}r}Mu_}P z@r2R^(6MweKL)=*%zjVxl;ghqQKo(LE^w*`qdx0TD3PCKNtY0Vl~9A*)kfG9knbJn z4&EQ(3_8c|FPQ&|G1R74fOr159Q!86P#E?LhtZlJcmEQEuwF_JY|iU~7+a@S2$nn1 zbHO&<$!<%pFv>-|7Qp=jx=g=R2F~$CDJY!DVpR4TKQ4`9zR_cWbCE8*)N!lOJLWjh zk7G0*s#@=u?@$s5-mUx|zvf1*Gvmb4wI=MDHvqWzpxgcZkOP@@sRv@i{Kbi}+ZW0( z_~5IJS%a2XihT7sw1d&>(#hHhT?`ZT%&+RO )eDul*S~W*%1h?nT!Q102hgSM z8Ol5oH YD0N+MU-J{_{#^T{PHADOVI#nFXLo9-ixD;c zi5o7M;YQ`5rQri1tCdFFd9%-9?rGF0xntVe{CnQ!e7+^seya922e2+&;QGF2(8bcG zn9alC${EnGMi5NV6<%3#8(aTYF$pU=+Dq`ZxPWf3%OSTc$=wx3yRQ;T);rq)F_EN% z`2lU#szcI02OO8YfG%NT9rvP8^~yf)`^C`KxBZ?Y_B`~N$#;0}C?=}N<;U%wX?Qzh z?+CRu+b!+IJTD)8!=s1a8eu)HlN;30LEr)H``^UW{|{*O+j(Q{T^OCN^A0unO`3k> z60_E!ig@|ye191w0y0AGFYeD{%YK_I-`1Ql3OP1Uo7C1$_srgq{csgMdUjF<+*hF6 zQ04KpI^6V2z_zD19pmj#jT%MShu7K=zs%npx2nUll;?E?vGi55FFcdp-bA@YJz+>1 z>yO;iRJ6C~NEJGQ|6@qdMHS2qD3MD)Q4iZl<50UfeDnw)vL@iMm^;nukg@S-Yzm8M zepl9#qAG{rfJ9eQYP*gwDyho~YxYFKUVb1P1LXU+FaHZ@F=rJUF5az(JbVIpFLd0_ zuZCsy%Re%G2j`U^q)$-Q%2Hvh^g@ig7Ad#gTsuZ|=bIIUO(rjgfYsK;d|RKY-NaTzrRB!byV2`#7wIcvIX)>Rd}K z{_G7~yNn<+EuWw_IKT7v_rHJ+zM@^kgPAETTHoi@u!CSQoLTx|B3f|lrhidsb`BB2 za#ormF`f)%bO!$kgHF*lrE0!g9*Lu>7XvTQ`yvheZh-~eXu{!K?{>!wk37j(3 V;^h$wZGzNFen-bki!Ke6$F- I`cJDWqi~m{w=2PK8cNWH?^oJvLXZnY@`IROEJDU42r-P9N+zi5F%(3@5 z4+z+w7!%ogO&`Cdz40Qj!4nmHd4q&y wfFJ5!zD?MHL|5 zzkTChK$rJ7kom0HLu1%}rSN3?@7P5+Ra-bJHP4jzwhze5Hy9a9{$!9;&)ljv30<*h zyXbdXUfEyvWOmx#8M44TwJ8EF66g{T;1#gD^r<>-lQEaRDj%S9Yn0wSkLHZgrr583 z<3}t($mnzk{rf~KLi;0o)0cI%`Oy|8;&&ITyW59ieCXhK^6%Q1e*t|2cPtF=#kX_s zpA~L?>#5bSQJqI~_NS$fp `g)iO_U%iGMovA?`>-Q@Zgqqn3F%e&-R2{$QJI@IAw`u06fA00*_NmKI zJ5Yx{H67Q!dlG(sFu;6vJ|f&Y1GED&=*}+5Hy1M~b-rx)>7bG&+v(CPE=(LLBWY{h zY;-)-G^J#XqDM9n{JzuJVLOb=>c+i^|K!BA0(Cb}Wu67WtpT_wplhbcw$B;!nVq*Y zH=~XRLKtEhmd!a}e;bWiP-+6Hq`H~kheP@g@}v(Id>bDefyN<>M1=0!l(^!okGf`G zisS(o6?EwWI^Wj*X2BH`#&l7IGS##D=8nvqTpL)a$$rtARD+flr>bj5@H(^ M6qJge%L`U XnMf z-%(*B`5bR4k}udIfKpUz>R+j2wo8Vlo?_O$HqLQP?up8U{^E+MHyzdZMU972t1lUx zH$n$pIB0 !^HDjXx*TD)^k&+=O++F0+xM zwZ%#z&p0fyi0#iUM#EO rYv9XoP< x=JPPhnZ)|Gowsn} z%aN3vLFv?Iqkdr#6%>hDnn|6$ 8`IfuN2=eIZ9v7Q`}CV+A5*FM*Ym6j=DYDZSQ8_G{i~c&K`~SE$R@g>*0c~ z74^n8`w+QKlZepeodFH<*zeybJS0X4fpf_^dq+K4jNV%M;Sgi)EW*;el$rt?S&m|Z zxcZoaC@c}v8^xoXK)!#+KK}yRF#Ce8LY4*k&l4MNP8`~5Y{Vb)z(QDjZf~c)SoT5U zfqFUz-zlF^Nzn%tqXP&(h->AYx%lW)CiQ3I^XbLEIsSVO;e)O* Po zO8~lKUTim%KbD;s;L#LY*S6_=WF&s)t%h$d6sG(COx{>(G>0TC%J`aWrXS{@g;anp zVfXs;FsaeGE)&A352+%!zM2qpX?PVSCMua?Xy&xY-l-kD`by-|*k4l+=34#m`)0KI zPfqLj&nAza#= K8d#tt*VJ{IJ${JZll$pZJfrKVuAK>FM4e43w! zb1q)$V+ZC}l^V}NlKAYLF}**UmS=u6L;(5zom>4E(BHFBNL?IrVeM0=a7qeQ2U0%4 zQVdi@dL`_Kh;M(wqTrVx58N8=2&d6IWXkjZEu&RnuqaM(vEvL$ Lp;}6eQ*5s)j+vIQd7~phx;xk$~|dj;& 4$q5Irl-&U#oL= z$ekx_$omE?C7gVRx?|o8fA7`5eE-(e{{=KYe*#QT(O^|JK9|hAnc;hzokU?C%*0lC zCY6gNUj=;MN(Ai#9$|K2%}_eSKPU4L)GyT#XJTtRc1Fx6&8mOLWdGd1Yis`nG^_ic z HTIcLhDW{}zsIis*QpUwl|71+h7Y@0C!p+`{p z7u&`~l}*G7rvaA^ba(H>>hvsKeLdUjkZ2rCwr>Qw=qxLc5ZH(aYDomXXk1jq^+5$y zCY%Nit&CC9rgLn)QtWbJyiokOyf6Ra0)7wutrh(XXfY9uZcDs8#*hYcr2_M|gy)&@ zhwCF5`ylnN$4wIb6-|=GSEIeOgyhJJSC+zNr|F1Bl+e ki~M%^(ktk}?81t% Jmt-AS-)RQHlNfxHe*SGN>aVV9rm+gl-T8YY2 Kj5%`JSH>;x4(kf_Qa`E6g_7bf2_$}WW5sOGyn z#ongNlL?;Ie>ju3c+R+))%8{cZIsD>WvK>u`PBLED=3{mQ2q+DmBio#9->{-1pA zTVZx2DvFQ7zb(u(*>hrDeFzlf0u;Og0hbkYU3nhYBvqJEp0is*%b zSSB4u5%F}n&UH &yMHTvF)Sz>eVUm~D5z#|YT2^I0q+un-<$ zHuMS#0mmg=pgWD2DV*m?aNzYa6fNx+r((MG)HL0I=RwPC?^vQYf8+9<<`XtOL!cxi zi8pKLA>MtWxP;U^Bf=p3_Yt$JU%o)Tf9E3p1$0MjLrq95$yor`wjiB?TDtpdHJq3t z9 2_xa_yoV`DXm$Ka>D#JT+wUGfKqnA zEmo8!{<}t=0A@wtq&=w8y{YlK6LmZ&-=(&y{FBfB={VQ-oLxx0qHEx zIk%T%o~fZOJt7CVyr5fzR;Ll^|K;%HL6SM7Z{qR` d?&HEJrmk)GHY=l)1kV{;M9)ESC&HK7Ht0DU7 z!0mWo-Ov!2J3j;~Iw4OtMm5nq&n&B-X-)(q(9qatF$7-($<}-eW!C`z7k}3R{|o4g zH|L*TLE)ZyxN UOWuM2=~Gc}b3F04;VZBU<%zRHhQB`a4ejS{*W9lzJXWQG!zJ5QUt zq|W*F#)H}chp+W#QN4>bvc^Mcu@CY{2+|Y(_JM!zH$l+l<{%L3SQhz%hmMGz8Av)} zKu)gt5%t@GrayD*;w*fRk0sXok{%rv=-LwrXv8^GLFQg3N2@|nnNX*5)bsy2M@tBF z&p$54iJ*KZLsfV-&g_Gm^eQXQb))3W51)a&Qe7JimNksjf6`i`eX^!&-p@fF4Z9 7-d3XFk3BsIF)BWBdfx60{5%-_D!Zx)v}ZG_4Mo&oOP zciq2$F0Q0T7k>)WV+{y=7%IPquWqI{lCNbI!c5CHJ^w8Xtv6waqCOcar{$-F$>3kk zYHgKB^&3fPh61&Ugtwoo0dU1YH|Y*aRxyF1u6e*n{JmbfBm^0IBa<>~fjfj#_y1$< zE~Bb?zK4Mmmy`~X?(UEjq(r*AyFoyZZs~66M!LJZ8$ponmPVS#`}NOS=Xw9z>#n7* zK5T}WIWv3CJx8^Ir!vQ6OvnX-fNg4sRyyU6xg5bsN<-NY#re4(t%hZT2XH