From 23d5d48f099cbbb23616004fae975a486dc7bfbf 2023-02-06 10:02:06 From: Maor Kleinberger Date: 2023-02-06 10:02:06 Subject: [PATCH] Merge branch 'main' into create_app_session_for_debugger_prompt --- diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 52f3e79..c7fa22c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -29,11 +29,13 @@ jobs: pip install mypy pyflakes flake8 - name: Lint with mypy run: | + set -e mypy -p IPython.terminal mypy -p IPython.core.magics mypy -p IPython.core.guarded_eval mypy -p IPython.core.completer - name: Lint with pyflakes run: | + set -e flake8 IPython/core/magics/script.py flake8 IPython/core/magics/packaging.py diff --git a/IPython/core/application.py b/IPython/core/application.py index 26c0616..e0a8174 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -123,9 +123,8 @@ class ProfileAwareConfigLoader(PyFileConfigLoader): return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path) class BaseIPythonApplication(Application): - - name = u'ipython' - description = Unicode(u'IPython: an enhanced interactive Python shell.') + name = "ipython" + description = "IPython: an enhanced interactive Python shell." version = Unicode(release.version) aliases = base_aliases @@ -311,7 +310,7 @@ class BaseIPythonApplication(Application): except OSError as e: # this will not be EEXIST self.log.error("couldn't create path %s: %s", path, e) - self.log.debug("IPYTHONDIR set to: %s" % new) + self.log.debug("IPYTHONDIR set to: %s", new) def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS): """Load the config file. @@ -401,7 +400,7 @@ class BaseIPythonApplication(Application): self.log.fatal("Profile %r not found."%self.profile) self.exit(1) else: - self.log.debug(f"Using existing profile dir: {p.location!r}") + self.log.debug("Using existing profile dir: %r", p.location) else: location = self.config.ProfileDir.location # location is fully specified @@ -421,7 +420,7 @@ class BaseIPythonApplication(Application): self.log.fatal("Profile directory %r not found."%location) self.exit(1) else: - self.log.debug(f"Using existing profile dir: {p.location!r}") + self.log.debug("Using existing profile dir: %r", p.location) # if profile_dir is specified explicitly, set profile name dir_name = os.path.basename(p.location) if dir_name.startswith('profile_'): @@ -468,7 +467,7 @@ class BaseIPythonApplication(Application): s = self.generate_config_file() config_file = Path(self.profile_dir.location) / self.config_file_name if self.overwrite or not config_file.exists(): - self.log.warning("Generating default config file: %r" % (config_file)) + self.log.warning("Generating default config file: %r", (config_file)) config_file.write_text(s, encoding="utf-8") @catch_config_error diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 578e783..aba4f90 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -91,7 +91,13 @@ class DisplayHook(Configurable): # some uses of ipshellembed may fail here return False - sio = _io.StringIO(cell) + return self.semicolon_at_end_of_expression(cell) + + @staticmethod + def semicolon_at_end_of_expression(expression): + """Parse Python expression and detects whether last token is ';'""" + + sio = _io.StringIO(expression) tokens = list(tokenize.generate_tokens(sio.readline)) for token in reversed(tokens): diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index b1a770b..66ceee0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2367,6 +2367,14 @@ class InteractiveShell(SingletonConfigurable): kwargs['local_ns'] = self.get_local_scope(stack_depth) with self.builtin_trap: result = fn(*args, **kwargs) + + # The code below prevents the output from being displayed + # when using magics with decodator @output_can_be_silenced + # when the last Python token in the expression is a ';'. + if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False): + if DisplayHook.semicolon_at_end_of_expression(magic_arg_s): + return None + return result def get_local_scope(self, stack_depth): @@ -2420,6 +2428,14 @@ class InteractiveShell(SingletonConfigurable): with self.builtin_trap: args = (magic_arg_s, cell) result = fn(*args, **kwargs) + + # The code below prevents the output from being displayed + # when using magics with decodator @output_can_be_silenced + # when the last Python token in the expression is a ';'. + if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False): + if DisplayHook.semicolon_at_end_of_expression(cell): + return None + return result def find_line_magic(self, magic_name): @@ -3200,6 +3216,7 @@ class InteractiveShell(SingletonConfigurable): # Execute the user code interactivity = "none" if silent else self.ast_node_interactivity + has_raised = await self.run_ast_nodes(code_ast.body, cell_name, interactivity=interactivity, compiler=compiler, result=result) diff --git a/IPython/core/logger.py b/IPython/core/logger.py index e3cb233..99e7ce2 100644 --- a/IPython/core/logger.py +++ b/IPython/core/logger.py @@ -198,7 +198,16 @@ which already exists. But you must first start the logging process with odata = u'\n'.join([u'#[Out]# %s' % s for s in data.splitlines()]) write(u'%s\n' % odata) - self.logfile.flush() + try: + self.logfile.flush() + except OSError: + print("Failed to flush the log file.") + print( + f"Please check that {self.logfname} exists and have the right permissions." + ) + print( + "Also consider turning off the log with `%logstop` to avoid this warning." + ) def logstop(self): """Fully stop logging and close log file. diff --git a/IPython/core/magic.py b/IPython/core/magic.py index cedba61..4f9e4e5 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -257,7 +257,8 @@ def _function_magic_marker(magic_kind): return magic_deco -MAGIC_NO_VAR_EXPAND_ATTR = '_ipython_magic_no_var_expand' +MAGIC_NO_VAR_EXPAND_ATTR = "_ipython_magic_no_var_expand" +MAGIC_OUTPUT_CAN_BE_SILENCED = "_ipython_magic_output_can_be_silenced" def no_var_expand(magic_func): @@ -276,6 +277,16 @@ def no_var_expand(magic_func): return magic_func +def output_can_be_silenced(magic_func): + """Mark a magic function so its output may be silenced. + + The output is silenced if the Python code used as a parameter of + the magic ends in a semicolon, not counting a Python comment that can + follow it. + """ + setattr(magic_func, MAGIC_OUTPUT_CAN_BE_SILENCED, True) + return magic_func + # Create the actual decorators for public use # These three are used to decorate methods in class definitions diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index da7f780..7b558d5 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -37,6 +37,7 @@ from IPython.core.magic import ( magics_class, needs_local_scope, no_var_expand, + output_can_be_silenced, on_off, ) from IPython.testing.skipdoctest import skip_doctest @@ -1194,6 +1195,7 @@ class ExecutionMagics(Magics): @no_var_expand @needs_local_scope @line_cell_magic + @output_can_be_silenced def time(self,line='', cell=None, local_ns=None): """Time execution of a Python statement or expression. diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index f3fa246..f64f1bc 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -468,9 +468,9 @@ class OSMagics(Magics): string. Usage:\\ - %set_env var val: set value for var - %set_env var=val: set value for var - %set_env var=$val: set value for var, using python expansion if possible + :``%set_env var val``: set value for var + :``%set_env var=val``: set value for var + :``%set_env var=$val``: set value for var, using python expansion if possible """ split = '=' if '=' in parameter_s else ' ' bits = parameter_s.split(split, 1) diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index 0f3fff6..2a69453 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -54,7 +54,7 @@ class PylabMagics(Magics): If you are using the inline matplotlib backend in the IPython Notebook you can set which figure formats are enabled using the following:: - In [1]: from IPython.display import set_matplotlib_formats + In [1]: from matplotlib_inline.backend_inline import set_matplotlib_formats In [2]: set_matplotlib_formats('pdf', 'svg') @@ -65,9 +65,9 @@ class PylabMagics(Magics): In [3]: %config InlineBackend.print_figure_kwargs = {'bbox_inches':None} - In addition, see the docstring of - `IPython.display.set_matplotlib_formats` and - `IPython.display.set_matplotlib_close` for more information on + In addition, see the docstrings of + `matplotlib_inline.backend_inline.set_matplotlib_formats` and + `matplotlib_inline.backend_inline.set_matplotlib_close` for more information on changing additional behaviors of the inline backend. Examples diff --git a/IPython/core/magics/script.py b/IPython/core/magics/script.py index 9fd2fc6..e0615c0 100644 --- a/IPython/core/magics/script.py +++ b/IPython/core/magics/script.py @@ -210,7 +210,7 @@ class ScriptMagics(Magics): async def _handle_stream(stream, stream_arg, file_object): while True: - line = (await stream.readline()).decode("utf8") + line = (await stream.readline()).decode("utf8", errors="replace") if not line: break if stream_arg: diff --git a/IPython/core/release.py b/IPython/core/release.py index 0416637..3b7dff0 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,7 +16,7 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 9 +_version_minor = 10 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" diff --git a/IPython/core/shellapp.py b/IPython/core/shellapp.py index 180f8b1..29325a0 100644 --- a/IPython/core/shellapp.py +++ b/IPython/core/shellapp.py @@ -278,7 +278,7 @@ class InteractiveShellApp(Configurable): ) for ext in extensions: try: - self.log.info("Loading IPython extension: %s" % ext) + self.log.info("Loading IPython extension: %s", ext) self.shell.extension_manager.load_extension(ext) except: if self.reraise_ipython_extension_failures: diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 509dd66..e64b959 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -416,6 +416,65 @@ def test_time(): with tt.AssertPrints("hihi", suppress=False): ip.run_cell("f('hi')") + +# ';' at the end of %time prevents instruction value to be printed. +# This tests fix for #13837. +def test_time_no_output_with_semicolon(): + ip = get_ipython() + + # Test %time cases + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456") + + with tt.AssertNotPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456;") + + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456 # Comment") + + with tt.AssertNotPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456; # Comment") + + with tt.AssertPrints(" 123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%time 123000+456 # ;Comment") + + # Test %%time cases + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456\n\n\n") + + with tt.AssertNotPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456;\n\n\n") + + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456 # Comment\n\n\n") + + with tt.AssertNotPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456; # Comment\n\n\n") + + with tt.AssertPrints("123456"): + with tt.AssertPrints("Wall time: ", suppress=False): + with tt.AssertPrints("CPU times: ", suppress=False): + ip.run_cell("%%time\n123000+456 # ;Comment\n\n\n") + + def test_time_last_not_expression(): ip.run_cell("%%time\n" "var_1 = 1\n" diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index c867b55..b5fc148 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -4,6 +4,7 @@ import asyncio import os import sys from warnings import warn +from typing import Union as UnionType from IPython.core.async_helpers import get_asyncio_loop from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC @@ -49,6 +50,10 @@ from .pt_inputhooks import get_inputhook_name_and_func from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook from .ptutils import IPythonPTCompleter, IPythonPTLexer from .shortcuts import create_ipython_shortcuts +from .shortcuts.auto_suggest import ( + NavigableAutoSuggestFromHistory, + AppendAutoSuggestionInAnyLine, +) PTK3 = ptk_version.startswith('3.') @@ -183,7 +188,10 @@ class TerminalInteractiveShell(InteractiveShell): 'menus, decrease for short and wide.' ).tag(config=True) - pt_app = None + pt_app: UnionType[PromptSession, None] = None + auto_suggest: UnionType[ + AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None + ] = None debugger_history = None debugger_history_file = Unicode( @@ -376,18 +384,27 @@ class TerminalInteractiveShell(InteractiveShell): ).tag(config=True) autosuggestions_provider = Unicode( - "AutoSuggestFromHistory", + "NavigableAutoSuggestFromHistory", help="Specifies from which source automatic suggestions are provided. " - "Can be set to `'AutoSuggestFromHistory`' or `None` to disable" - "automatic suggestions. Default is `'AutoSuggestFromHistory`'.", + "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and " + ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, " + " or ``None`` to disable automatic suggestions. " + "Default is `'NavigableAutoSuggestFromHistory`'.", allow_none=True, ).tag(config=True) def _set_autosuggestions(self, provider): + # disconnect old handler + if self.auto_suggest and isinstance( + self.auto_suggest, NavigableAutoSuggestFromHistory + ): + self.auto_suggest.disconnect() if provider is None: self.auto_suggest = None elif provider == "AutoSuggestFromHistory": self.auto_suggest = AutoSuggestFromHistory() + elif provider == "NavigableAutoSuggestFromHistory": + self.auto_suggest = NavigableAutoSuggestFromHistory() else: raise ValueError("No valid provider.") if self.pt_app: @@ -462,6 +479,8 @@ class TerminalInteractiveShell(InteractiveShell): tempfile_suffix=".py", **self._extra_prompt_options() ) + if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory): + self.auto_suggest.connect(self.pt_app) def _make_style_from_name_or_cls(self, name_or_cls): """ @@ -560,23 +579,39 @@ class TerminalInteractiveShell(InteractiveShell): get_message = get_message() options = { - 'complete_in_thread': False, - 'lexer':IPythonPTLexer(), - 'reserve_space_for_menu':self.space_for_menu, - 'message': get_message, - 'prompt_continuation': ( - lambda width, lineno, is_soft_wrap: - PygmentsTokens(self.prompts.continuation_prompt_tokens(width))), - 'multiline': True, - 'complete_style': self.pt_complete_style, - + "complete_in_thread": False, + "lexer": IPythonPTLexer(), + "reserve_space_for_menu": self.space_for_menu, + "message": get_message, + "prompt_continuation": ( + lambda width, lineno, is_soft_wrap: PygmentsTokens( + self.prompts.continuation_prompt_tokens(width) + ) + ), + "multiline": True, + "complete_style": self.pt_complete_style, + "input_processors": [ # Highlight matching brackets, but only when this setting is # enabled, and only when the DEFAULT_BUFFER has the focus. - 'input_processors': [ConditionalProcessor( - processor=HighlightMatchingBracketProcessor(chars='[](){}'), - filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() & - Condition(lambda: self.highlight_matching_brackets))], - } + ConditionalProcessor( + processor=HighlightMatchingBracketProcessor(chars="[](){}"), + filter=HasFocus(DEFAULT_BUFFER) + & ~IsDone() + & Condition(lambda: self.highlight_matching_brackets), + ), + # Show auto-suggestion in lines other than the last line. + ConditionalProcessor( + processor=AppendAutoSuggestionInAnyLine(), + filter=HasFocus(DEFAULT_BUFFER) + & ~IsDone() + & Condition( + lambda: isinstance( + self.auto_suggest, NavigableAutoSuggestFromHistory + ) + ), + ), + ], + } if not PTK3: options['inputhook'] = self.inputhook @@ -647,7 +682,7 @@ class TerminalInteractiveShell(InteractiveShell): self.alias_manager.soft_define_alias(cmd, cmd) - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super(TerminalInteractiveShell, self).__init__(*args, **kwargs) self._set_autosuggestions(self.autosuggestions_provider) self.init_prompt_toolkit_cli() diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index df4648b..6280bce 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -156,7 +156,7 @@ frontend_flags['i'] = ( flags.update(frontend_flags) aliases = dict(base_aliases) -aliases.update(shell_aliases) +aliases.update(shell_aliases) # type: ignore[arg-type] #----------------------------------------------------------------------------- # Main classes and functions @@ -180,7 +180,7 @@ class LocateIPythonApp(BaseIPythonApplication): class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): name = u'ipython' description = usage.cl_usage - crash_handler_class = IPAppCrashHandler + crash_handler_class = IPAppCrashHandler # typing: ignore[assignment] examples = _examples flags = flags diff --git a/IPython/terminal/shortcuts.py b/IPython/terminal/shortcuts.py deleted file mode 100644 index 6ca91ec..0000000 --- a/IPython/terminal/shortcuts.py +++ /dev/null @@ -1,608 +0,0 @@ -""" -Module to define and register Terminal IPython shortcuts with -:mod:`prompt_toolkit` -""" - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - -import warnings -import signal -import sys -import re -import os -from typing import Callable - - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER -from prompt_toolkit.filters import (has_focus, has_selection, Condition, - vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) -from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.bindings import named_commands as nc -from prompt_toolkit.key_binding.vi_state import InputMode, ViState - -from IPython.utils.decorators import undoc - -@undoc -@Condition -def cursor_in_leading_ws(): - before = get_app().current_buffer.document.current_line_before_cursor - return (not before) or before.isspace() - - -# Needed for to accept autosuggestions in vi insert mode -def _apply_autosuggest(event): - """ - Apply autosuggestion if at end of line. - """ - b = event.current_buffer - d = b.document - after_cursor = d.text[d.cursor_position :] - lines = after_cursor.split("\n") - end_of_current_line = lines[0].strip() - suggestion = b.suggestion - if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): - b.insert_text(suggestion.text) - else: - nc.end_of_line(event) - -def create_ipython_shortcuts(shell): - """Set up the prompt_toolkit keyboard shortcuts for IPython""" - - kb = KeyBindings() - insert_mode = vi_insert_mode | emacs_insert_mode - - if getattr(shell, 'handle_return', None): - return_handler = shell.handle_return(shell) - else: - return_handler = newline_or_execute_outer(shell) - - kb.add('enter', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - ))(return_handler) - - def reformat_and_execute(event): - reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) - event.current_buffer.validate_and_handle() - - @Condition - def ebivim(): - return shell.emacs_bindings_in_vi_insert_mode - - kb.add( - "escape", - "enter", - filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), - )(reformat_and_execute) - - kb.add("c-\\")(quit) - - kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) - )(previous_history_or_previous_completion) - - kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) - )(next_history_or_next_completion) - - kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions) - )(dismiss_completion) - - kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer) - - kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) - - supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP')) - kb.add('c-z', filter=supports_suspend)(suspend_to_bg) - - # Ctrl+I == Tab - kb.add('tab', filter=(has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - & cursor_in_leading_ws - ))(indent_buffer) - kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) - )(newline_autoindent_outer(shell.input_transformer_manager)) - - kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) - - @Condition - def auto_match(): - return shell.auto_match - - def all_quotes_paired(quote, buf): - paired = True - i = 0 - while i < len(buf): - c = buf[i] - if c == quote: - paired = not paired - elif c == "\\": - i += 1 - i += 1 - return paired - - focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER) - _preceding_text_cache = {} - _following_text_cache = {} - - def preceding_text(pattern): - if pattern in _preceding_text_cache: - return _preceding_text_cache[pattern] - - if callable(pattern): - - def _preceding_text(): - app = get_app() - before_cursor = app.current_buffer.document.current_line_before_cursor - return bool(pattern(before_cursor)) - - else: - m = re.compile(pattern) - - def _preceding_text(): - app = get_app() - before_cursor = app.current_buffer.document.current_line_before_cursor - return bool(m.match(before_cursor)) - - condition = Condition(_preceding_text) - _preceding_text_cache[pattern] = condition - return condition - - def following_text(pattern): - try: - return _following_text_cache[pattern] - except KeyError: - pass - m = re.compile(pattern) - - def _following_text(): - app = get_app() - return bool(m.match(app.current_buffer.document.current_line_after_cursor)) - - condition = Condition(_following_text) - _following_text_cache[pattern] = condition - return condition - - @Condition - def not_inside_unclosed_string(): - app = get_app() - s = app.current_buffer.document.text_before_cursor - # remove escaped quotes - s = s.replace('\\"', "").replace("\\'", "") - # remove triple-quoted string literals - s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s) - # remove single-quoted string literals - s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s) - return not ('"' in s or "'" in s) - - # auto match - @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("()") - event.current_buffer.cursor_left() - - @kb.add("[", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("[]") - event.current_buffer.cursor_left() - - @kb.add("{", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) - def _(event): - event.current_buffer.insert_text("{}") - event.current_buffer.cursor_left() - - @kb.add( - '"', - filter=focused_insert - & auto_match - & not_inside_unclosed_string - & preceding_text(lambda line: all_quotes_paired('"', line)) - & following_text(r"[,)}\]]|$"), - ) - def _(event): - event.current_buffer.insert_text('""') - event.current_buffer.cursor_left() - - @kb.add( - "'", - filter=focused_insert - & auto_match - & not_inside_unclosed_string - & preceding_text(lambda line: all_quotes_paired("'", line)) - & following_text(r"[,)}\]]|$"), - ) - def _(event): - event.current_buffer.insert_text("''") - event.current_buffer.cursor_left() - - @kb.add( - '"', - filter=focused_insert - & auto_match - & not_inside_unclosed_string - & preceding_text(r'^.*""$'), - ) - def _(event): - event.current_buffer.insert_text('""""') - event.current_buffer.cursor_left(3) - - @kb.add( - "'", - filter=focused_insert - & auto_match - & not_inside_unclosed_string - & preceding_text(r"^.*''$"), - ) - def _(event): - event.current_buffer.insert_text("''''") - event.current_buffer.cursor_left(3) - - # raw string - @kb.add( - "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") - ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("()" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) - - @kb.add( - "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") - ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("[]" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) - - @kb.add( - "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") - ) - def _(event): - matches = re.match( - r".*(r|R)[\"'](-*)", - event.current_buffer.document.current_line_before_cursor, - ) - dashes = matches.group(2) or "" - event.current_buffer.insert_text("{}" + dashes) - event.current_buffer.cursor_left(len(dashes) + 1) - - # just move cursor - @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)")) - @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]")) - @kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}")) - @kb.add('"', filter=focused_insert & auto_match & following_text('^"')) - @kb.add("'", filter=focused_insert & auto_match & following_text("^'")) - def _(event): - event.current_buffer.cursor_right() - - @kb.add( - "backspace", - filter=focused_insert - & preceding_text(r".*\($") - & auto_match - & following_text(r"^\)"), - ) - @kb.add( - "backspace", - filter=focused_insert - & preceding_text(r".*\[$") - & auto_match - & following_text(r"^\]"), - ) - @kb.add( - "backspace", - filter=focused_insert - & preceding_text(r".*\{$") - & auto_match - & following_text(r"^\}"), - ) - @kb.add( - "backspace", - filter=focused_insert - & preceding_text('.*"$') - & auto_match - & following_text('^"'), - ) - @kb.add( - "backspace", - filter=focused_insert - & preceding_text(r".*'$") - & auto_match - & following_text(r"^'"), - ) - def _(event): - event.current_buffer.delete() - event.current_buffer.delete_before_cursor() - - if shell.display_completions == "readlinelike": - kb.add( - "c-i", - filter=( - has_focus(DEFAULT_BUFFER) - & ~has_selection - & insert_mode - & ~cursor_in_leading_ws - ), - )(display_completions_like_readline) - - if sys.platform == "win32": - kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) - - focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode - - @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) - def _(event): - _apply_autosuggest(event) - - @kb.add("c-e", filter=focused_insert_vi & ebivim) - def _(event): - _apply_autosuggest(event) - - @kb.add("c-f", filter=focused_insert_vi) - def _(event): - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - b.insert_text(suggestion.text) - else: - nc.forward_char(event) - - @kb.add("escape", "f", filter=focused_insert_vi & ebivim) - def _(event): - b = event.current_buffer - suggestion = b.suggestion - if suggestion: - t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next((x for x in t if x), "")) - else: - nc.forward_word(event) - - # Simple Control keybindings - key_cmd_dict = { - "c-a": nc.beginning_of_line, - "c-b": nc.backward_char, - "c-k": nc.kill_line, - "c-w": nc.backward_kill_word, - "c-y": nc.yank, - "c-_": nc.undo, - } - - for key, cmd in key_cmd_dict.items(): - kb.add(key, filter=focused_insert_vi & ebivim)(cmd) - - # Alt and Combo Control keybindings - keys_cmd_dict = { - # Control Combos - ("c-x", "c-e"): nc.edit_and_execute, - ("c-x", "e"): nc.edit_and_execute, - # Alt - ("escape", "b"): nc.backward_word, - ("escape", "c"): nc.capitalize_word, - ("escape", "d"): nc.kill_word, - ("escape", "h"): nc.backward_kill_word, - ("escape", "l"): nc.downcase_word, - ("escape", "u"): nc.uppercase_word, - ("escape", "y"): nc.yank_pop, - ("escape", "."): nc.yank_last_arg, - } - - for keys, cmd in keys_cmd_dict.items(): - kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd) - - def get_input_mode(self): - app = get_app() - app.ttimeoutlen = shell.ttimeoutlen - app.timeoutlen = shell.timeoutlen - - return self._input_mode - - def set_input_mode(self, mode): - shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) - cursor = "\x1b[{} q".format(shape) - - sys.stdout.write(cursor) - sys.stdout.flush() - - self._input_mode = mode - - if shell.editing_mode == "vi" and shell.modal_cursor: - ViState._input_mode = InputMode.INSERT - ViState.input_mode = property(get_input_mode, set_input_mode) - - return kb - - -def reformat_text_before_cursor(buffer, document, shell): - text = buffer.delete_before_cursor(len(document.text[:document.cursor_position])) - try: - formatted_text = shell.reformat_handler(text) - buffer.insert_text(formatted_text) - except Exception as e: - buffer.insert_text(text) - - -def newline_or_execute_outer(shell): - - def newline_or_execute(event): - """When the user presses return, insert a newline or execute the code.""" - b = event.current_buffer - d = b.document - - if b.complete_state: - cc = b.complete_state.current_completion - if cc: - b.apply_completion(cc) - else: - b.cancel_completion() - return - - # If there's only one line, treat it as if the cursor is at the end. - # See https://github.com/ipython/ipython/issues/10425 - if d.line_count == 1: - check_text = d.text - else: - check_text = d.text[:d.cursor_position] - status, indent = shell.check_complete(check_text) - - # if all we have after the cursor is whitespace: reformat current text - # before cursor - after_cursor = d.text[d.cursor_position:] - reformatted = False - if not after_cursor.strip(): - reformat_text_before_cursor(b, d, shell) - reformatted = True - if not (d.on_last_line or - d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() - ): - if shell.autoindent: - b.insert_text('\n' + indent) - else: - b.insert_text('\n') - return - - if (status != 'incomplete') and b.accept_handler: - if not reformatted: - reformat_text_before_cursor(b, d, shell) - b.validate_and_handle() - else: - if shell.autoindent: - b.insert_text('\n' + indent) - else: - b.insert_text('\n') - return newline_or_execute - - -def previous_history_or_previous_completion(event): - """ - Control-P in vi edit mode on readline is history next, unlike default prompt toolkit. - - If completer is open this still select previous completion. - """ - event.current_buffer.auto_up() - - -def next_history_or_next_completion(event): - """ - Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit. - - If completer is open this still select next completion. - """ - event.current_buffer.auto_down() - - -def dismiss_completion(event): - b = event.current_buffer - if b.complete_state: - b.cancel_completion() - - -def reset_buffer(event): - b = event.current_buffer - if b.complete_state: - b.cancel_completion() - else: - b.reset() - - -def reset_search_buffer(event): - if event.current_buffer.document.text: - event.current_buffer.reset() - else: - event.app.layout.focus(DEFAULT_BUFFER) - -def suspend_to_bg(event): - event.app.suspend_to_background() - -def quit(event): - """ - On platforms that support SIGQUIT, send SIGQUIT to the current process. - On other platforms, just exit the process with a message. - """ - sigquit = getattr(signal, "SIGQUIT", None) - if sigquit is not None: - os.kill(0, signal.SIGQUIT) - else: - sys.exit("Quit") - -def indent_buffer(event): - event.current_buffer.insert_text(' ' * 4) - -@undoc -def newline_with_copy_margin(event): - """ - DEPRECATED since IPython 6.0 - - See :any:`newline_autoindent_outer` for a replacement. - - Preserve margin and cursor position when using - Control-O to insert a newline in EMACS mode - """ - warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " - "see `newline_autoindent_outer(shell)(event)` for a replacement.", - DeprecationWarning, stacklevel=2) - - b = event.current_buffer - cursor_start_pos = b.document.cursor_position_col - b.newline(copy_margin=True) - b.cursor_up(count=1) - cursor_end_pos = b.document.cursor_position_col - if cursor_start_pos != cursor_end_pos: - pos_diff = cursor_start_pos - cursor_end_pos - b.cursor_right(count=pos_diff) - -def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: - """ - Return a function suitable for inserting a indented newline after the cursor. - - Fancier version of deprecated ``newline_with_copy_margin`` which should - compute the correct indentation of the inserted line. That is to say, indent - by 4 extra space after a function definition, class definition, context - manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``. - """ - - def newline_autoindent(event): - """insert a newline after the cursor indented appropriately.""" - b = event.current_buffer - d = b.document - - if b.complete_state: - b.cancel_completion() - text = d.text[:d.cursor_position] + '\n' - _, indent = inputsplitter.check_complete(text) - b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False) - - return newline_autoindent - - -def open_input_in_editor(event): - event.app.current_buffer.open_in_editor() - - -if sys.platform == 'win32': - from IPython.core.error import TryNext - from IPython.lib.clipboard import (ClipboardEmpty, - win32_clipboard_get, - tkinter_clipboard_get) - - @undoc - def win_paste(event): - try: - text = win32_clipboard_get() - except TryNext: - try: - text = tkinter_clipboard_get() - except (TryNext, ClipboardEmpty): - return - except ClipboardEmpty: - return - event.current_buffer.insert_text(text.replace("\t", " " * 4)) diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py new file mode 100644 index 0000000..7ec9a28 --- /dev/null +++ b/IPython/terminal/shortcuts/__init__.py @@ -0,0 +1,670 @@ +""" +Module to define and register Terminal IPython shortcuts with +:mod:`prompt_toolkit` +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import os +import re +import signal +import sys +import warnings +from typing import Callable, Dict, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER +from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions +from prompt_toolkit.filters import has_focus as has_focus_impl +from prompt_toolkit.filters import ( + has_selection, + has_suggestion, + vi_insert_mode, + vi_mode, +) +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.bindings import named_commands as nc +from prompt_toolkit.key_binding.bindings.completion import ( + display_completions_like_readline, +) +from prompt_toolkit.key_binding.vi_state import InputMode, ViState +from prompt_toolkit.layout.layout import FocusableElement + +from IPython.terminal.shortcuts import auto_match as match +from IPython.terminal.shortcuts import auto_suggest +from IPython.utils.decorators import undoc + +__all__ = ["create_ipython_shortcuts"] + + +@undoc +@Condition +def cursor_in_leading_ws(): + before = get_app().current_buffer.document.current_line_before_cursor + return (not before) or before.isspace() + + +def has_focus(value: FocusableElement): + """Wrapper around has_focus adding a nice `__name__` to tester function""" + tester = has_focus_impl(value).func + tester.__name__ = f"is_focused({value})" + return Condition(tester) + + +@undoc +@Condition +def has_line_below() -> bool: + document = get_app().current_buffer.document + return document.cursor_position_row < len(document.lines) - 1 + + +@undoc +@Condition +def has_line_above() -> bool: + document = get_app().current_buffer.document + return document.cursor_position_row != 0 + + +def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings: + """Set up the prompt_toolkit keyboard shortcuts for IPython. + + Parameters + ---------- + shell: InteractiveShell + The current IPython shell Instance + for_all_platforms: bool (default false) + This parameter is mostly used in generating the documentation + to create the shortcut binding for all the platforms, and export + them. + + Returns + ------- + KeyBindings + the keybinding instance for prompt toolkit. + + """ + # Warning: if possible, do NOT define handler functions in the locals + # scope of this function, instead define functions in the global + # scope, or a separate module, and include a user-friendly docstring + # describing the action. + + kb = KeyBindings() + insert_mode = vi_insert_mode | emacs_insert_mode + + if getattr(shell, "handle_return", None): + return_handler = shell.handle_return(shell) + else: + return_handler = newline_or_execute_outer(shell) + + kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))( + return_handler + ) + + @Condition + def ebivim(): + return shell.emacs_bindings_in_vi_insert_mode + + @kb.add( + "escape", + "enter", + filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), + ) + def reformat_and_execute(event): + """Reformat code and execute it""" + reformat_text_before_cursor( + event.current_buffer, event.current_buffer.document, shell + ) + event.current_buffer.validate_and_handle() + + kb.add("c-\\")(quit) + + kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( + previous_history_or_previous_completion + ) + + kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( + next_history_or_next_completion + ) + + kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))( + dismiss_completion + ) + + kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer) + + kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) + + supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP")) + kb.add("c-z", filter=supports_suspend)(suspend_to_bg) + + # Ctrl+I == Tab + kb.add( + "tab", + filter=( + has_focus(DEFAULT_BUFFER) + & ~has_selection + & insert_mode + & cursor_in_leading_ws + ), + )(indent_buffer) + kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))( + newline_autoindent_outer(shell.input_transformer_manager) + ) + + kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) + + @Condition + def auto_match(): + return shell.auto_match + + def all_quotes_paired(quote, buf): + paired = True + i = 0 + while i < len(buf): + c = buf[i] + if c == quote: + paired = not paired + elif c == "\\": + i += 1 + i += 1 + return paired + + focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER) + _preceding_text_cache: Dict[Union[str, Callable], Condition] = {} + _following_text_cache: Dict[Union[str, Callable], Condition] = {} + + def preceding_text(pattern: Union[str, Callable]): + if pattern in _preceding_text_cache: + return _preceding_text_cache[pattern] + + if callable(pattern): + + def _preceding_text(): + app = get_app() + before_cursor = app.current_buffer.document.current_line_before_cursor + # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603 + return bool(pattern(before_cursor)) # type: ignore[operator] + + else: + m = re.compile(pattern) + + def _preceding_text(): + app = get_app() + before_cursor = app.current_buffer.document.current_line_before_cursor + return bool(m.match(before_cursor)) + + _preceding_text.__name__ = f"preceding_text({pattern!r})" + + condition = Condition(_preceding_text) + _preceding_text_cache[pattern] = condition + return condition + + def following_text(pattern): + try: + return _following_text_cache[pattern] + except KeyError: + pass + m = re.compile(pattern) + + def _following_text(): + app = get_app() + return bool(m.match(app.current_buffer.document.current_line_after_cursor)) + + _following_text.__name__ = f"following_text({pattern!r})" + + condition = Condition(_following_text) + _following_text_cache[pattern] = condition + return condition + + @Condition + def not_inside_unclosed_string(): + app = get_app() + s = app.current_buffer.document.text_before_cursor + # remove escaped quotes + s = s.replace('\\"', "").replace("\\'", "") + # remove triple-quoted string literals + s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s) + # remove single-quoted string literals + s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s) + return not ('"' in s or "'" in s) + + # auto match + for key, cmd in match.auto_match_parens.items(): + kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))( + cmd + ) + + # raw string + for key, cmd in match.auto_match_parens_raw_string.items(): + kb.add( + key, + filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"), + )(cmd) + + kb.add( + '"', + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(lambda line: all_quotes_paired('"', line)) + & following_text(r"[,)}\]]|$"), + )(match.double_quote) + + kb.add( + "'", + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(lambda line: all_quotes_paired("'", line)) + & following_text(r"[,)}\]]|$"), + )(match.single_quote) + + kb.add( + '"', + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(r'^.*""$'), + )(match.docstring_double_quotes) + + kb.add( + "'", + filter=focused_insert + & auto_match + & not_inside_unclosed_string + & preceding_text(r"^.*''$"), + )(match.docstring_single_quotes) + + # just move cursor + kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))( + match.skip_over + ) + kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))( + match.skip_over + ) + kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))( + match.skip_over + ) + kb.add('"', filter=focused_insert & auto_match & following_text('^"'))( + match.skip_over + ) + kb.add("'", filter=focused_insert & auto_match & following_text("^'"))( + match.skip_over + ) + + kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\($") + & auto_match + & following_text(r"^\)"), + )(match.delete_pair) + kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\[$") + & auto_match + & following_text(r"^\]"), + )(match.delete_pair) + kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*\{$") + & auto_match + & following_text(r"^\}"), + )(match.delete_pair) + kb.add( + "backspace", + filter=focused_insert + & preceding_text('.*"$') + & auto_match + & following_text('^"'), + )(match.delete_pair) + kb.add( + "backspace", + filter=focused_insert + & preceding_text(r".*'$") + & auto_match + & following_text(r"^'"), + )(match.delete_pair) + + if shell.display_completions == "readlinelike": + kb.add( + "c-i", + filter=( + has_focus(DEFAULT_BUFFER) + & ~has_selection + & insert_mode + & ~cursor_in_leading_ws + ), + )(display_completions_like_readline) + + if sys.platform == "win32" or for_all_platforms: + kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) + + focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode + + # autosuggestions + @Condition + def navigable_suggestions(): + return isinstance( + shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory + ) + + kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))( + auto_suggest.accept_in_vi_insert_mode + ) + kb.add("c-e", filter=focused_insert_vi & ebivim)( + auto_suggest.accept_in_vi_insert_mode + ) + kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept) + kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word) + kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_token + ) + kb.add( + "escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER) & emacs_insert_mode + )(auto_suggest.discard) + kb.add( + "up", + filter=navigable_suggestions + & ~has_line_above + & has_suggestion + & has_focus(DEFAULT_BUFFER), + )(auto_suggest.swap_autosuggestion_up(shell.auto_suggest)) + kb.add( + "down", + filter=navigable_suggestions + & ~has_line_below + & has_suggestion + & has_focus(DEFAULT_BUFFER), + )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest)) + kb.add( + "up", filter=has_line_above & navigable_suggestions & has_focus(DEFAULT_BUFFER) + )(auto_suggest.up_and_update_hint) + kb.add( + "down", + filter=has_line_below & navigable_suggestions & has_focus(DEFAULT_BUFFER), + )(auto_suggest.down_and_update_hint) + kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_character + ) + kb.add("c-left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_and_move_cursor_left + ) + kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.accept_and_keep_cursor + ) + kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))( + auto_suggest.backspace_and_resume_hint + ) + + # Simple Control keybindings + key_cmd_dict = { + "c-a": nc.beginning_of_line, + "c-b": nc.backward_char, + "c-k": nc.kill_line, + "c-w": nc.backward_kill_word, + "c-y": nc.yank, + "c-_": nc.undo, + } + + for key, cmd in key_cmd_dict.items(): + kb.add(key, filter=focused_insert_vi & ebivim)(cmd) + + # Alt and Combo Control keybindings + keys_cmd_dict = { + # Control Combos + ("c-x", "c-e"): nc.edit_and_execute, + ("c-x", "e"): nc.edit_and_execute, + # Alt + ("escape", "b"): nc.backward_word, + ("escape", "c"): nc.capitalize_word, + ("escape", "d"): nc.kill_word, + ("escape", "h"): nc.backward_kill_word, + ("escape", "l"): nc.downcase_word, + ("escape", "u"): nc.uppercase_word, + ("escape", "y"): nc.yank_pop, + ("escape", "."): nc.yank_last_arg, + } + + for keys, cmd in keys_cmd_dict.items(): + kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd) + + def get_input_mode(self): + app = get_app() + app.ttimeoutlen = shell.ttimeoutlen + app.timeoutlen = shell.timeoutlen + + return self._input_mode + + def set_input_mode(self, mode): + shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) + cursor = "\x1b[{} q".format(shape) + + sys.stdout.write(cursor) + sys.stdout.flush() + + self._input_mode = mode + + if shell.editing_mode == "vi" and shell.modal_cursor: + ViState._input_mode = InputMode.INSERT # type: ignore + ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore + return kb + + +def reformat_text_before_cursor(buffer, document, shell): + text = buffer.delete_before_cursor(len(document.text[: document.cursor_position])) + try: + formatted_text = shell.reformat_handler(text) + buffer.insert_text(formatted_text) + except Exception as e: + buffer.insert_text(text) + + +def newline_or_execute_outer(shell): + def newline_or_execute(event): + """When the user presses return, insert a newline or execute the code.""" + b = event.current_buffer + d = b.document + + if b.complete_state: + cc = b.complete_state.current_completion + if cc: + b.apply_completion(cc) + else: + b.cancel_completion() + return + + # If there's only one line, treat it as if the cursor is at the end. + # See https://github.com/ipython/ipython/issues/10425 + if d.line_count == 1: + check_text = d.text + else: + check_text = d.text[: d.cursor_position] + status, indent = shell.check_complete(check_text) + + # if all we have after the cursor is whitespace: reformat current text + # before cursor + after_cursor = d.text[d.cursor_position :] + reformatted = False + if not after_cursor.strip(): + reformat_text_before_cursor(b, d, shell) + reformatted = True + if not ( + d.on_last_line + or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() + ): + if shell.autoindent: + b.insert_text("\n" + indent) + else: + b.insert_text("\n") + return + + if (status != "incomplete") and b.accept_handler: + if not reformatted: + reformat_text_before_cursor(b, d, shell) + b.validate_and_handle() + else: + if shell.autoindent: + b.insert_text("\n" + indent) + else: + b.insert_text("\n") + + newline_or_execute.__qualname__ = "newline_or_execute" + + return newline_or_execute + + +def previous_history_or_previous_completion(event): + """ + Control-P in vi edit mode on readline is history next, unlike default prompt toolkit. + + If completer is open this still select previous completion. + """ + event.current_buffer.auto_up() + + +def next_history_or_next_completion(event): + """ + Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit. + + If completer is open this still select next completion. + """ + event.current_buffer.auto_down() + + +def dismiss_completion(event): + """Dismiss completion""" + b = event.current_buffer + if b.complete_state: + b.cancel_completion() + + +def reset_buffer(event): + """Reset buffer""" + b = event.current_buffer + if b.complete_state: + b.cancel_completion() + else: + b.reset() + + +def reset_search_buffer(event): + """Reset search buffer""" + if event.current_buffer.document.text: + event.current_buffer.reset() + else: + event.app.layout.focus(DEFAULT_BUFFER) + + +def suspend_to_bg(event): + """Suspend to background""" + event.app.suspend_to_background() + + +def quit(event): + """ + Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise. + + On platforms that support SIGQUIT, send SIGQUIT to the current process. + On other platforms, just exit the process with a message. + """ + sigquit = getattr(signal, "SIGQUIT", None) + if sigquit is not None: + os.kill(0, signal.SIGQUIT) + else: + sys.exit("Quit") + + +def indent_buffer(event): + """Indent buffer""" + event.current_buffer.insert_text(" " * 4) + + +@undoc +def newline_with_copy_margin(event): + """ + DEPRECATED since IPython 6.0 + + See :any:`newline_autoindent_outer` for a replacement. + + Preserve margin and cursor position when using + Control-O to insert a newline in EMACS mode + """ + warnings.warn( + "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " + "see `newline_autoindent_outer(shell)(event)` for a replacement.", + DeprecationWarning, + stacklevel=2, + ) + + b = event.current_buffer + cursor_start_pos = b.document.cursor_position_col + b.newline(copy_margin=True) + b.cursor_up(count=1) + cursor_end_pos = b.document.cursor_position_col + if cursor_start_pos != cursor_end_pos: + pos_diff = cursor_start_pos - cursor_end_pos + b.cursor_right(count=pos_diff) + + +def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: + """ + Return a function suitable for inserting a indented newline after the cursor. + + Fancier version of deprecated ``newline_with_copy_margin`` which should + compute the correct indentation of the inserted line. That is to say, indent + by 4 extra space after a function definition, class definition, context + manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``. + """ + + def newline_autoindent(event): + """Insert a newline after the cursor indented appropriately.""" + b = event.current_buffer + d = b.document + + if b.complete_state: + b.cancel_completion() + text = d.text[: d.cursor_position] + "\n" + _, indent = inputsplitter.check_complete(text) + b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False) + + newline_autoindent.__qualname__ = "newline_autoindent" + + return newline_autoindent + + +def open_input_in_editor(event): + """Open code from input in external editor""" + event.app.current_buffer.open_in_editor() + + +if sys.platform == "win32": + from IPython.core.error import TryNext + from IPython.lib.clipboard import ( + ClipboardEmpty, + tkinter_clipboard_get, + win32_clipboard_get, + ) + + @undoc + def win_paste(event): + try: + text = win32_clipboard_get() + except TryNext: + try: + text = tkinter_clipboard_get() + except (TryNext, ClipboardEmpty): + return + except ClipboardEmpty: + return + event.current_buffer.insert_text(text.replace("\t", " " * 4)) + +else: + + @undoc + def win_paste(event): + """Stub used when auto-generating shortcuts for documentation""" + pass diff --git a/IPython/terminal/shortcuts/auto_match.py b/IPython/terminal/shortcuts/auto_match.py new file mode 100644 index 0000000..46cb1bd --- /dev/null +++ b/IPython/terminal/shortcuts/auto_match.py @@ -0,0 +1,104 @@ +""" +Utilities function for keybinding with prompt toolkit. + +This will be bound to specific key press and filter modes, +like whether we are in edit mode, and whether the completer is open. +""" +import re +from prompt_toolkit.key_binding import KeyPressEvent + + +def parenthesis(event: KeyPressEvent): + """Auto-close parenthesis""" + event.current_buffer.insert_text("()") + event.current_buffer.cursor_left() + + +def brackets(event: KeyPressEvent): + """Auto-close brackets""" + event.current_buffer.insert_text("[]") + event.current_buffer.cursor_left() + + +def braces(event: KeyPressEvent): + """Auto-close braces""" + event.current_buffer.insert_text("{}") + event.current_buffer.cursor_left() + + +def double_quote(event: KeyPressEvent): + """Auto-close double quotes""" + event.current_buffer.insert_text('""') + event.current_buffer.cursor_left() + + +def single_quote(event: KeyPressEvent): + """Auto-close single quotes""" + event.current_buffer.insert_text("''") + event.current_buffer.cursor_left() + + +def docstring_double_quotes(event: KeyPressEvent): + """Auto-close docstring (double quotes)""" + event.current_buffer.insert_text('""""') + event.current_buffer.cursor_left(3) + + +def docstring_single_quotes(event: KeyPressEvent): + """Auto-close docstring (single quotes)""" + event.current_buffer.insert_text("''''") + event.current_buffer.cursor_left(3) + + +def raw_string_parenthesis(event: KeyPressEvent): + """Auto-close parenthesis in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) if matches else "" + event.current_buffer.insert_text("()" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def raw_string_bracket(event: KeyPressEvent): + """Auto-close bracker in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) if matches else "" + event.current_buffer.insert_text("[]" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def raw_string_braces(event: KeyPressEvent): + """Auto-close braces in raw strings""" + matches = re.match( + r".*(r|R)[\"'](-*)", + event.current_buffer.document.current_line_before_cursor, + ) + dashes = matches.group(2) if matches else "" + event.current_buffer.insert_text("{}" + dashes) + event.current_buffer.cursor_left(len(dashes) + 1) + + +def skip_over(event: KeyPressEvent): + """Skip over automatically added parenthesis. + + (rather than adding another parenthesis)""" + event.current_buffer.cursor_right() + + +def delete_pair(event: KeyPressEvent): + """Delete auto-closed parenthesis""" + event.current_buffer.delete() + event.current_buffer.delete_before_cursor() + + +auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces} +auto_match_parens_raw_string = { + "(": raw_string_parenthesis, + "[": raw_string_bracket, + "{": raw_string_braces, +} diff --git a/IPython/terminal/shortcuts/auto_suggest.py b/IPython/terminal/shortcuts/auto_suggest.py new file mode 100644 index 0000000..3bfd6d5 --- /dev/null +++ b/IPython/terminal/shortcuts/auto_suggest.py @@ -0,0 +1,378 @@ +import re +import tokenize +from io import StringIO +from typing import Callable, List, Optional, Union, Generator, Tuple, Sequence + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.key_binding import KeyPressEvent +from prompt_toolkit.key_binding.bindings import named_commands as nc +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion +from prompt_toolkit.document import Document +from prompt_toolkit.history import History +from prompt_toolkit.shortcuts import PromptSession +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) + +from IPython.utils.tokenutil import generate_tokens + + +def _get_query(document: Document): + return document.lines[document.cursor_position_row] + + +class AppendAutoSuggestionInAnyLine(Processor): + """ + Append the auto suggestion to lines other than the last (appending to the + last line is natively supported by the prompt toolkit). + """ + + def __init__(self, style: str = "class:auto-suggestion") -> None: + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + is_last_line = ti.lineno == ti.document.line_count - 1 + is_active_line = ti.lineno == ti.document.cursor_position_row + + if not is_last_line and is_active_line: + buffer = ti.buffer_control.buffer + + if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line: + suggestion = buffer.suggestion.text + else: + suggestion = "" + + return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) + else: + return Transformation(fragments=ti.fragments) + + +class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): + """ + A subclass of AutoSuggestFromHistory that allow navigation to next/previous + suggestion from history. To do so it remembers the current position, but it + state need to carefully be cleared on the right events. + """ + + def __init__( + self, + ): + self.skip_lines = 0 + self._connected_apps = [] + + def reset_history_position(self, _: Buffer): + self.skip_lines = 0 + + def disconnect(self): + for pt_app in self._connected_apps: + text_insert_event = pt_app.default_buffer.on_text_insert + text_insert_event.remove_handler(self.reset_history_position) + + def connect(self, pt_app: PromptSession): + self._connected_apps.append(pt_app) + # note: `on_text_changed` could be used for a bit different behaviour + # on character deletion (i.e. reseting history position on backspace) + pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position) + pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss) + + def get_suggestion( + self, buffer: Buffer, document: Document + ) -> Optional[Suggestion]: + text = _get_query(document) + + if text.strip(): + for suggestion, _ in self._find_next_match( + text, self.skip_lines, buffer.history + ): + return Suggestion(suggestion) + + return None + + def _dismiss(self, buffer, *args, **kwargs): + buffer.suggestion = None + + def _find_match( + self, text: str, skip_lines: float, history: History, previous: bool + ) -> Generator[Tuple[str, float], None, None]: + """ + text : str + Text content to find a match for, the user cursor is most of the + time at the end of this text. + skip_lines : float + number of items to skip in the search, this is used to indicate how + far in the list the user has navigated by pressing up or down. + The float type is used as the base value is +inf + history : History + prompt_toolkit History instance to fetch previous entries from. + previous : bool + Direction of the search, whether we are looking previous match + (True), or next match (False). + + Yields + ------ + Tuple with: + str: + current suggestion. + float: + will actually yield only ints, which is passed back via skip_lines, + which may be a +inf (float) + + + """ + line_number = -1 + for string in reversed(list(history.get_strings())): + for line in reversed(string.splitlines()): + line_number += 1 + if not previous and line_number < skip_lines: + continue + # do not return empty suggestions as these + # close the auto-suggestion overlay (and are useless) + if line.startswith(text) and len(line) > len(text): + yield line[len(text) :], line_number + if previous and line_number >= skip_lines: + return + + def _find_next_match( + self, text: str, skip_lines: float, history: History + ) -> Generator[Tuple[str, float], None, None]: + return self._find_match(text, skip_lines, history, previous=False) + + def _find_previous_match(self, text: str, skip_lines: float, history: History): + return reversed( + list(self._find_match(text, skip_lines, history, previous=True)) + ) + + def up(self, query: str, other_than: str, history: History) -> None: + for suggestion, line_number in self._find_next_match( + query, self.skip_lines, history + ): + # if user has history ['very.a', 'very', 'very.b'] and typed 'very' + # we want to switch from 'very.b' to 'very.a' because a) if the + # suggestion equals current text, prompt-toolkit aborts suggesting + # b) user likely would not be interested in 'very' anyways (they + # already typed it). + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle back to beginning + self.skip_lines = 0 + + def down(self, query: str, other_than: str, history: History) -> None: + for suggestion, line_number in self._find_previous_match( + query, self.skip_lines, history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle to end + for suggestion, line_number in self._find_previous_match( + query, float("Inf"), history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + + +# Needed for to accept autosuggestions in vi insert mode +def accept_in_vi_insert_mode(event: KeyPressEvent): + """Apply autosuggestion if at end of line.""" + buffer = event.current_buffer + d = buffer.document + after_cursor = d.text[d.cursor_position :] + lines = after_cursor.split("\n") + end_of_current_line = lines[0].strip() + suggestion = buffer.suggestion + if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): + buffer.insert_text(suggestion.text) + else: + nc.end_of_line(event) + + +def accept(event: KeyPressEvent): + """Accept autosuggestion""" + buffer = event.current_buffer + suggestion = buffer.suggestion + if suggestion: + buffer.insert_text(suggestion.text) + else: + nc.forward_char(event) + + +def discard(event: KeyPressEvent): + """Discard autosuggestion""" + buffer = event.current_buffer + buffer.suggestion = None + + +def accept_word(event: KeyPressEvent): + """Fill partial autosuggestion by word""" + buffer = event.current_buffer + suggestion = buffer.suggestion + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + buffer.insert_text(next((x for x in t if x), "")) + else: + nc.forward_word(event) + + +def accept_character(event: KeyPressEvent): + """Fill partial autosuggestion by character""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion and suggestion.text: + b.insert_text(suggestion.text[0]) + + +def accept_and_keep_cursor(event: KeyPressEvent): + """Accept autosuggestion and keep cursor in place""" + buffer = event.current_buffer + old_position = buffer.cursor_position + suggestion = buffer.suggestion + if suggestion: + buffer.insert_text(suggestion.text) + buffer.cursor_position = old_position + + +def accept_and_move_cursor_left(event: KeyPressEvent): + """Accept autosuggestion and move cursor left in place""" + accept_and_keep_cursor(event) + nc.backward_char(event) + + +def _update_hint(buffer: Buffer): + if buffer.auto_suggest: + suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + + +def backspace_and_resume_hint(event: KeyPressEvent): + """Resume autosuggestions after deleting last character""" + current_buffer = event.current_buffer + + def resume_hinting(buffer: Buffer): + _update_hint(buffer) + current_buffer.on_text_changed.remove_handler(resume_hinting) + + current_buffer.on_text_changed.add_handler(resume_hinting) + nc.backward_delete_char(event) + + +def up_and_update_hint(event: KeyPressEvent): + """Go up and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_up(count=event.arg) + _update_hint(current_buffer) + + +def down_and_update_hint(event: KeyPressEvent): + """Go down and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_down(count=event.arg) + _update_hint(current_buffer) + + +def accept_token(event: KeyPressEvent): + """Fill partial autosuggestion by token""" + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + prefix = _get_query(b.document) + text = prefix + suggestion.text + + tokens: List[Optional[str]] = [None, None, None] + substrings = [""] + i = 0 + + for token in generate_tokens(StringIO(text).readline): + if token.type == tokenize.NEWLINE: + index = len(text) + else: + index = text.index(token[1], len(substrings[-1])) + substrings.append(text[:index]) + tokenized_so_far = substrings[-1] + if tokenized_so_far.startswith(prefix): + if i == 0 and len(tokenized_so_far) > len(prefix): + tokens[0] = tokenized_so_far[len(prefix) :] + substrings.append(tokenized_so_far) + i += 1 + tokens[i] = token[1] + if i == 2: + break + i += 1 + + if tokens[0]: + to_insert: str + insert_text = substrings[-2] + if tokens[1] and len(tokens[1]) == 1: + insert_text = substrings[-1] + to_insert = insert_text[len(prefix) :] + b.insert_text(to_insert) + return + + nc.forward_word(event) + + +Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] + + +def _swap_autosuggestion( + buffer: Buffer, + provider: NavigableAutoSuggestFromHistory, + direction_method: Callable, +): + """ + We skip most recent history entry (in either direction) if it equals the + current autosuggestion because if user cycles when auto-suggestion is shown + they most likely want something else than what was suggested (otherwise + they would have accepted the suggestion). + """ + suggestion = buffer.suggestion + if not suggestion: + return + + query = _get_query(buffer.document) + current = query + suggestion.text + + direction_method(query=query, other_than=current, history=buffer.history) + + new_suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = new_suggestion + + +def swap_autosuggestion_up(provider: Provider): + def swap_autosuggestion_up(event: KeyPressEvent): + """Get next autosuggestion from history.""" + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, provider=provider, direction_method=provider.up + ) + + swap_autosuggestion_up.__name__ = "swap_autosuggestion_up" + return swap_autosuggestion_up + + +def swap_autosuggestion_down( + provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] +): + def swap_autosuggestion_down(event: KeyPressEvent): + """Get previous autosuggestion from history.""" + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, + provider=provider, + direction_method=provider.down, + ) + + swap_autosuggestion_down.__name__ = "swap_autosuggestion_down" + return swap_autosuggestion_down diff --git a/IPython/terminal/tests/test_interactivshell.py b/IPython/terminal/tests/test_interactivshell.py index 68dbe37..01008d7 100644 --- a/IPython/terminal/tests/test_interactivshell.py +++ b/IPython/terminal/tests/test_interactivshell.py @@ -7,11 +7,25 @@ import sys import unittest import os +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory + from IPython.core.inputtransformer import InputTransformer from IPython.testing import tools as tt from IPython.utils.capture import capture_output from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context +from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory + + +class TestAutoSuggest(unittest.TestCase): + def test_changing_provider(self): + ip = get_ipython() + ip.autosuggestions_provider = None + self.assertEqual(ip.auto_suggest, None) + ip.autosuggestions_provider = "AutoSuggestFromHistory" + self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory) + ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory" + self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory) class TestElide(unittest.TestCase): @@ -24,10 +38,10 @@ class TestElide(unittest.TestCase): ) test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""]) - expect_stirng = ( + expect_string = ( os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c" ) - self.assertEqual(_elide(test_string, ""), expect_stirng) + self.assertEqual(_elide(test_string, ""), expect_string) def test_elide_typed_normal(self): self.assertEqual( diff --git a/IPython/terminal/tests/test_shortcuts.py b/IPython/terminal/tests/test_shortcuts.py new file mode 100644 index 0000000..309205d --- /dev/null +++ b/IPython/terminal/tests/test_shortcuts.py @@ -0,0 +1,318 @@ +import pytest +from IPython.terminal.shortcuts.auto_suggest import ( + accept, + accept_in_vi_insert_mode, + accept_token, + accept_character, + accept_word, + accept_and_keep_cursor, + discard, + NavigableAutoSuggestFromHistory, + swap_autosuggestion_up, + swap_autosuggestion_down, +) + +from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.document import Document +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory + +from unittest.mock import patch, Mock + + +def make_event(text, cursor, suggestion): + event = Mock() + event.current_buffer = Mock() + event.current_buffer.suggestion = Mock() + event.current_buffer.text = text + event.current_buffer.cursor_position = cursor + event.current_buffer.suggestion.text = suggestion + event.current_buffer.document = Document(text=text, cursor_position=cursor) + return event + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"), + ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"), + ], +) +def test_accept(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + accept(event) + assert buffer.insert_text.called + assert buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion", + [ + ("", "def out(tag: str, n=50):"), + ("def ", "out(tag: str, n=50):"), + ], +) +def test_discard(text, suggestion): + event = make_event(text, len(text), suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + discard(event) + assert not buffer.insert_text.called + assert buffer.suggestion is None + + +@pytest.mark.parametrize( + "text, cursor, suggestion, called", + [ + ("123456", 6, "123456789", True), + ("123456", 3, "123456789", False), + ("123456 \n789", 6, "123456789", True), + ], +) +def test_autosuggest_at_EOL(text, cursor, suggestion, called): + """ + test that autosuggest is only applied at end of line. + """ + + event = make_event(text, cursor, suggestion) + event.current_buffer.insert_text = Mock() + accept_in_vi_insert_mode(event) + if called: + event.current_buffer.insert_text.assert_called() + else: + event.current_buffer.insert_text.assert_not_called() + # event.current_buffer.document.get_end_of_line_position.assert_called() + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def "), + ("d", "ef out(tag: str, n=50):", "ef "), + ("de ", "f out(tag: str, n=50):", "f "), + ("def", " out(tag: str, n=50):", " "), + ("def ", "out(tag: str, n=50):", "out("), + ("def o", "ut(tag: str, n=50):", "ut("), + ("def ou", "t(tag: str, n=50):", "t("), + ("def out", "(tag: str, n=50):", "("), + ("def out(", "tag: str, n=50):", "tag: "), + ("def out(t", "ag: str, n=50):", "ag: "), + ("def out(ta", "g: str, n=50):", "g: "), + ("def out(tag", ": str, n=50):", ": "), + ("def out(tag:", " str, n=50):", " "), + ("def out(tag: ", "str, n=50):", "str, "), + ("def out(tag: s", "tr, n=50):", "tr, "), + ("def out(tag: st", "r, n=50):", "r, "), + ("def out(tag: str", ", n=50):", ", n"), + ("def out(tag: str,", " n=50):", " n"), + ("def out(tag: str, ", "n=50):", "n="), + ("def out(tag: str, n", "=50):", "="), + ("def out(tag: str, n=", "50):", "50)"), + ("def out(tag: str, n=5", "0):", "0)"), + ("def out(tag: str, n=50", "):", "):"), + ("def out(tag: str, n=50)", ":", ":"), + ], +) +def test_autosuggest_token(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_token(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "d"), + ("d", "ef out(tag: str, n=50):", "e"), + ("de ", "f out(tag: str, n=50):", "f"), + ("def", " out(tag: str, n=50):", " "), + ], +) +def test_accept_character(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_character(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion, expected", + [ + ("", "def out(tag: str, n=50):", "def "), + ("d", "ef out(tag: str, n=50):", "ef "), + ("de", "f out(tag: str, n=50):", "f "), + ("def", " out(tag: str, n=50):", " "), + # (this is why we also have accept_token) + ("def ", "out(tag: str, n=50):", "out(tag: "), + ], +) +def test_accept_word(text, suggestion, expected): + event = make_event(text, len(text), suggestion) + event.current_buffer.insert_text = Mock() + accept_word(event) + assert event.current_buffer.insert_text.called + assert event.current_buffer.insert_text.call_args[0] == (expected,) + + +@pytest.mark.parametrize( + "text, suggestion, expected, cursor", + [ + ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0), + ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4), + ], +) +def test_accept_and_keep_cursor(text, suggestion, expected, cursor): + event = make_event(text, cursor, suggestion) + buffer = event.current_buffer + buffer.insert_text = Mock() + accept_and_keep_cursor(event) + assert buffer.insert_text.called + assert buffer.insert_text.call_args[0] == (expected,) + assert buffer.cursor_position == cursor + + +def test_autosuggest_token_empty(): + full = "def out(tag: str, n=50):" + event = make_event(full, len(full), "") + event.current_buffer.insert_text = Mock() + + with patch( + "prompt_toolkit.key_binding.bindings.named_commands.forward_word" + ) as forward_word: + accept_token(event) + assert not event.current_buffer.insert_text.called + assert forward_word.called + + +def test_other_providers(): + """Ensure that swapping autosuggestions does not break with other providers""" + provider = AutoSuggestFromHistory() + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + event = Mock() + event.current_buffer = Buffer() + assert up(event) is None + assert down(event) is None + + +async def test_navigable_provider(): + provider = NavigableAutoSuggestFromHistory() + history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"]) + buffer = Buffer(history=history) + + async for _ in history.load(): + pass + + buffer.cursor_position = 5 + buffer.text = "very" + + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + + event = Mock() + event.current_buffer = buffer + + def get_suggestion(): + suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + return suggestion + + assert get_suggestion().text == "_c" + + # should go up + up(event) + assert get_suggestion().text == "_b" + + # should skip over 'very' which is identical to buffer content + up(event) + assert get_suggestion().text == "_a" + + # should cycle back to beginning + up(event) + assert get_suggestion().text == "_c" + + # should cycle back through end boundary + down(event) + assert get_suggestion().text == "_a" + + down(event) + assert get_suggestion().text == "_b" + + down(event) + assert get_suggestion().text == "_c" + + down(event) + assert get_suggestion().text == "_a" + + +async def test_navigable_provider_multiline_entries(): + provider = NavigableAutoSuggestFromHistory() + history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"]) + buffer = Buffer(history=history) + + async for _ in history.load(): + pass + + buffer.cursor_position = 5 + buffer.text = "very" + up = swap_autosuggestion_up(provider) + down = swap_autosuggestion_down(provider) + + event = Mock() + event.current_buffer = buffer + + def get_suggestion(): + suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + return suggestion + + assert get_suggestion().text == "_c" + + up(event) + assert get_suggestion().text == "_b" + + up(event) + assert get_suggestion().text == "_a" + + down(event) + assert get_suggestion().text == "_b" + + down(event) + assert get_suggestion().text == "_c" + + +def create_session_mock(): + session = Mock() + session.default_buffer = Buffer() + return session + + +def test_navigable_provider_connection(): + provider = NavigableAutoSuggestFromHistory() + provider.skip_lines = 1 + + session_1 = create_session_mock() + provider.connect(session_1) + + assert provider.skip_lines == 1 + session_1.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 0 + + session_2 = create_session_mock() + provider.connect(session_2) + provider.skip_lines = 2 + + assert provider.skip_lines == 2 + session_2.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 0 + + provider.skip_lines = 3 + provider.disconnect() + session_1.default_buffer.on_text_insert.fire() + session_2.default_buffer.on_text_insert.fire() + assert provider.skip_lines == 3 diff --git a/IPython/tests/test_shortcuts.py b/IPython/tests/test_shortcuts.py deleted file mode 100644 index 42edb92..0000000 --- a/IPython/tests/test_shortcuts.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest -from IPython.terminal.shortcuts import _apply_autosuggest - -from unittest.mock import Mock - - -def make_event(text, cursor, suggestion): - event = Mock() - event.current_buffer = Mock() - event.current_buffer.suggestion = Mock() - event.current_buffer.cursor_position = cursor - event.current_buffer.suggestion.text = suggestion - event.current_buffer.document = Mock() - event.current_buffer.document.get_end_of_line_position = Mock(return_value=0) - event.current_buffer.document.text = text - event.current_buffer.document.cursor_position = cursor - return event - - -@pytest.mark.parametrize( - "text, cursor, suggestion, called", - [ - ("123456", 6, "123456789", True), - ("123456", 3, "123456789", False), - ("123456 \n789", 6, "123456789", True), - ], -) -def test_autosuggest_at_EOL(text, cursor, suggestion, called): - """ - test that autosuggest is only applied at end of line. - """ - - event = make_event(text, cursor, suggestion) - event.current_buffer.insert_text = Mock() - _apply_autosuggest(event) - if called: - event.current_buffer.insert_text.assert_called() - else: - event.current_buffer.insert_text.assert_not_called() - # event.current_buffer.document.get_end_of_line_position.assert_called() diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 2eac5e6..cef4319 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -18,11 +18,6 @@ from warnings import warn from IPython.utils.decorators import undoc from .capture import CapturedIO, capture_output -# setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr -devnull = open(os.devnull, "w", encoding="utf-8") -atexit.register(devnull.close) - - class Tee(object): """A class to duplicate an output stream to stdout/err. diff --git a/docs/autogen_shortcuts.py b/docs/autogen_shortcuts.py index db7fe8d..f8fd17b 100755 --- a/docs/autogen_shortcuts.py +++ b/docs/autogen_shortcuts.py @@ -1,45 +1,98 @@ +from dataclasses import dataclass +from inspect import getsource from pathlib import Path +from typing import cast, Callable, List, Union +from html import escape as html_escape +import re + +from prompt_toolkit.keys import KEY_ALIASES +from prompt_toolkit.key_binding import KeyBindingsBase +from prompt_toolkit.filters import Filter, Condition +from prompt_toolkit.shortcuts import PromptSession from IPython.terminal.shortcuts import create_ipython_shortcuts -def name(c): - s = c.__class__.__name__ - if s == '_Invert': - return '(Not: %s)' % name(c.filter) - if s in log_filters.keys(): - return '(%s: %s)' % (log_filters[s], ', '.join(name(x) for x in c.filters)) - return log_filters[s] if s in log_filters.keys() else s +@dataclass +class Shortcut: + #: a sequence of keys (each element on the list corresponds to pressing one or more keys) + keys_sequence: list[str] + filter: str -def sentencize(s): - """Extract first sentence - """ - s = s.replace('\n', ' ').strip().split('.') - s = s[0] if len(s) else s - try: - return " ".join(s.split()) - except AttributeError: - return s +@dataclass +class Handler: + description: str + identifier: str -def most_common(lst, n=3): - """Most common elements occurring more then `n` times - """ - from collections import Counter - c = Counter(lst) - return [k for (k, v) in c.items() if k and v > n] +@dataclass +class Binding: + handler: Handler + shortcut: Shortcut -def multi_filter_str(flt): - """Yield readable conditional filter - """ - assert hasattr(flt, 'filters'), 'Conditional filter required' - yield name(flt) +class _NestedFilter(Filter): + """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`.""" + + filters: List[Filter] + + +class _Invert(Filter): + """Protocol reflecting non-public prompt_toolkit's `_Invert`.""" + + filter: Filter + + +conjunctions_labels = {"_AndList": "and", "_OrList": "or"} +ATOMIC_CLASSES = {"Never", "Always", "Condition"} + + +def format_filter( + filter_: Union[Filter, _NestedFilter, Condition, _Invert], + is_top_level=True, + skip=None, +) -> str: + """Create easily readable description of the filter.""" + s = filter_.__class__.__name__ + if s == "Condition": + func = cast(Condition, filter_).func + name = func.__name__ + if name == "": + source = getsource(func) + return source.split("=")[0].strip() + return func.__name__ + elif s == "_Invert": + operand = cast(_Invert, filter_).filter + if operand.__class__.__name__ in ATOMIC_CLASSES: + return f"not {format_filter(operand, is_top_level=False)}" + return f"not ({format_filter(operand, is_top_level=False)})" + elif s in conjunctions_labels: + filters = cast(_NestedFilter, filter_).filters + conjunction = conjunctions_labels[s] + glue = f" {conjunction} " + result = glue.join(format_filter(x, is_top_level=False) for x in filters) + if len(filters) > 1 and not is_top_level: + result = f"({result})" + return result + elif s in ["Never", "Always"]: + return s.lower() + else: + raise ValueError(f"Unknown filter type: {filter_}") + + +def sentencize(s) -> str: + """Extract first sentence""" + s = re.split(r"\.\W", s.replace("\n", " ").strip()) + s = s[0] if len(s) else "" + if not s.endswith("."): + s += "." + try: + return " ".join(s.split()) + except AttributeError: + return s -log_filters = {'_AndList': 'And', '_OrList': 'Or'} -log_invert = {'_Invert'} class _DummyTerminal: """Used as a buffer to get prompt_toolkit bindings @@ -48,49 +101,121 @@ class _DummyTerminal: input_transformer_manager = None display_completions = None editing_mode = "emacs" + auto_suggest = None -ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings - -dummy_docs = [] # ignore bindings without proper documentation - -common_docs = most_common([kb.handler.__doc__ for kb in ipy_bindings]) -if common_docs: - dummy_docs.extend(common_docs) +def create_identifier(handler: Callable): + parts = handler.__module__.split(".") + name = handler.__name__ + package = parts[0] + if len(parts) > 1: + final_module = parts[-1] + return f"{package}:{final_module}.{name}" + else: + return f"{package}:{name}" + + +def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]: + """Collect bindings to a simple format that does not depend on prompt-toolkit internals""" + bindings: List[Binding] = [] + + for kb in prompt_bindings.bindings: + bindings.append( + Binding( + handler=Handler( + description=kb.handler.__doc__ or "", + identifier=create_identifier(kb.handler), + ), + shortcut=Shortcut( + keys_sequence=[ + str(k.value) if hasattr(k, "value") else k for k in kb.keys + ], + filter=format_filter(kb.filter, skip={"has_focus_filter"}), + ), + ) + ) + return bindings + + +INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}} + + +def format_prompt_keys(keys: str, add_alternatives=True) -> str: + """Format prompt toolkit key with modifier into an RST representation.""" + + def to_rst(key): + escaped = key.replace("\\", "\\\\") + return f":kbd:`{escaped}`" + + keys_to_press: list[str] + + prefixes = { + "c-s-": [to_rst("ctrl"), to_rst("shift")], + "s-c-": [to_rst("ctrl"), to_rst("shift")], + "c-": [to_rst("ctrl")], + "s-": [to_rst("shift")], + } + + for prefix, modifiers in prefixes.items(): + if keys.startswith(prefix): + remainder = keys[len(prefix) :] + keys_to_press = [*modifiers, to_rst(remainder)] + break + else: + keys_to_press = [to_rst(keys)] -dummy_docs = list(set(dummy_docs)) + result = " + ".join(keys_to_press) -single_filter = {} -multi_filter = {} -for kb in ipy_bindings: - doc = kb.handler.__doc__ - if not doc or doc in dummy_docs: - continue + if keys in INDISTINGUISHABLE_KEYS and add_alternatives: + alternative = INDISTINGUISHABLE_KEYS[keys] - shortcut = ' '.join([k if isinstance(k, str) else k.name for k in kb.keys]) - shortcut += shortcut.endswith('\\') and '\\' or '' - if hasattr(kb.filter, 'filters'): - flt = ' '.join(multi_filter_str(kb.filter)) - multi_filter[(shortcut, flt)] = sentencize(doc) - else: - single_filter[(shortcut, name(kb.filter))] = sentencize(doc) + result = ( + result + + " (or " + + format_prompt_keys(alternative, add_alternatives=False) + + ")" + ) + return result if __name__ == '__main__': here = Path(__file__).parent dest = here / "source" / "config" / "shortcuts" - def sort_key(item): - k, v = item - shortcut, flt = k - return (str(shortcut), str(flt)) - - for filters, output_filename in [ - (single_filter, "single_filtered"), - (multi_filter, "multi_filtered"), - ]: - with (dest / "{}.csv".format(output_filename)).open( - "w", encoding="utf-8" - ) as csv: - for (shortcut, flt), v in sorted(filters.items(), key=sort_key): - csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v)) + ipy_bindings = create_ipython_shortcuts(_DummyTerminal(), for_all_platforms=True) + + session = PromptSession(key_bindings=ipy_bindings) + prompt_bindings = session.app.key_bindings + + assert prompt_bindings + # Ensure that we collected the default shortcuts + assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings) + + bindings = bindings_from_prompt_toolkit(prompt_bindings) + + def sort_key(binding: Binding): + return binding.handler.identifier, binding.shortcut.filter + + filters = [] + with (dest / "table.tsv").open("w", encoding="utf-8") as csv: + for binding in sorted(bindings, key=sort_key): + sequence = ", ".join( + [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence] + ) + if binding.shortcut.filter == "always": + condition_label = "-" + else: + # we cannot fit all the columns as the filters got too complex over time + condition_label = "ⓘ" + + csv.write( + "\t".join( + [ + sequence, + sentencize(binding.handler.description) + + f" :raw-html:`
` `{binding.handler.identifier}`", + f':raw-html:`{condition_label}`', + ] + ) + + "\n" + ) diff --git a/docs/environment.yml b/docs/environment.yml index afb5eff..9961253 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -3,14 +3,14 @@ channels: - conda-forge - defaults dependencies: - - python=3.8 - - setuptools>=18.5 + - python=3.10 + - setuptools - sphinx>=4.2 - - sphinx_rtd_theme>=1.0 + - sphinx_rtd_theme - numpy - - nose - testpath - matplotlib + - pip - pip: - docrepr - prompt_toolkit diff --git a/docs/source/_images/autosuggest.gif b/docs/source/_images/autosuggest.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee105489432284124c225cd13cdf6787e39250b9 GIT binary patch literal 137790 zc$^fNWmppo7lya7!RXNqqZ>v@No{m@cSs1*U5-(sM@orwOM@UaN?KB-R8T}vloUZw zzW2L-o$LHQPh3Y!M^jGTg#)Yv`V9a85(YCx$i-v zl}4u&#_#~hM9$2_183rqW|H<|Qo}MClCa7~vdPeK(7`!a`8Y(~IqoNM5;5|SbMi1s z^5}wjo*43!IrCI`^2Vw1VvTr9jQOl3_(~q|Re16v9t*JO3&`^eAZY{)#RbW@1S$Ci zm5BsnodpwJg*-HbiMfR7;KJNK!oD^lWSk;=;UbcxBKp!IrZ%D^oT4x_QAT@F7CKRO z7ts=ZF%nKOQeH7i88J$2F&ILOUSFJ8K%87doXSg_UR7KwRXoxYK`o7-WkxU=AR^Qe zg&q>K!4jJhlEhq+2GCt>I^Sz0h9Dn2DCC0(318<&$a@hw+f zD0ksHuW7y@A+z9VcTxM>GNfRch4Rz&Kjlx$%U_>XsteZE);2XZHya2xKW}NblWza| zr=zp8b78M*aHAXT-*a*MqObQwe_x-oOn;=+;M0`h!9jd`)2O%H_{8}5%8zQ_6EJJCpXscfdxCJAEz{|$TPT$a z{f5_Ig?V*Z1`VH`1t$B#gWJQ*Z&{_J7}--R4ZWbf#GZNK>?To zO@|y*33*EZj@H6!6ojfsBQb5PUJ2PJgtzjCs6f0+ymDVYa@^# zP>ZpjNlE8sBhs@jb+q2A>DpxIPm?aV*~fA%9tZ@qV`z(J_r`0$gIlntf3~?`^bIEj zMr=%U(fpmP@4m1;Lu*R-_Q=@l;b?VRBwo$BHIPlhxG8Hi$i?Jha;QXMA^=JiW|QOv zbsHp#K#7kviB`*g$segL9NVU4ATGMv^xSq&9Yibwkk%(9S_)8qIA}&Gr{Nb*rg95`NN$!did&IE1G~B>x=<%Pt%pdK%~Ct z1f`0S=Bg90rP2-v&YS^4F}TxXAXorwHUALX!Di|)hbz~ zGaGRr2czq`KoWZN`Wx&fAW_*}0%E-6Mss$R*tU;P3+VpLJlZfmPzS?0B)YNhA0#7w zhP)I$sboEaCc#Xnw3{XtLQN1LW@qOF2EeEd0*izNT1h72T-gh zF!rj!FPSDjn&>x-z;-}LI43zG-651&LW}>! zk_dWvmdKpkEj-fAA}-bain6t7bk>Z2GH=I@$Xx2#C*L6;=&w~1r_-^K$u0!5xdCST zaKhTI9N7(;GM(rQUsf)T8Z3r$Xp}NG<}=$Ze2OdJSEBVv0>>5pmFzbvO4Qp!Lo0lO z>={+s#J#fyeH(}GAPt-ji6Mzc0DTpE?){|gM-QA~jz|wnsXEa^lR9@SgC=}fr`QO@ zjRUB=>8PIp@nAhwP%UBm(6^rSCjbGo2&7%9O#Yc zt!FTd3E7%g)yy!&0!gQzf6j+(6=^JbOC^GDfb?VdB5H}F8#?Gxmb8ScoMmG7a>)Gf z5S3mN9wBpm_Ex+#?MpP4!`rv^Om5N|r@LDhoC#ZbL?0 zE!^cu%`r}Q-ux8ANNX(sMFbd18|D|tq^~)B{{&LaJ=o*qi<|-P0QWQCdtZ2M3X$xc z=NUT?{qO5gHMwqD1OpWx8eLzl-8mCEEq6M38fMi8NF!JC&`@D>JEHTP;;AsSz=Q=Q^|K&X<*_u}rZo;BOm}XAf zS=}UVbj}c9?~G6tFhtLH!z^q|PJh|{*HlzVDd~%WL7}%!-liad*qDF_qFAH>McIVr zlVow}hPA5_J!9#?NY$heD9S;ba2RrT)sIjAEa$zU6;E09Szx^@@(kjy^7;nKd(RRo zJE-%tAwF_pn>G{m(#NsgmSn=ECK(%l98c^(enk?yGbP$^LK36x3#T0?LQESI$GIoi z5>>7$neI|VTURYiQAC@Fk?DAb%jGVYU#d2Vstxvt5C{25XyF}TtT$zF=%aV4FZs}+ z!IZ$CV)z1@v%L_m;8~xz&C#!K{6(ML&Sf*mNg^4o_rI+InN-&W2aaGF&j6&STM<)I zfYSm}_XVcuGs<_GJ<=9=T%w*Ab9d}l?BJ(lMRWH@s`25R>q?x+$5=YYQ+bh)G1eX^ zn{rxCl4C`P``H&Qsm5ihNivImONRKcA zBmf?L5)ICK9PMd2Z1osFg`moT^x+>qqVTJ7$%^u;kRU*pA$f%cC8wTPN=npp%CXEtl$RcvT1wx+J-w4?XuN#N zK5W4=v3`Q+c!|tuDP%?#M#vYs93Bd#^34fIYQ-npM`TLo_&$)0b1Mq-@R9^qLR^<5 zH+x~*9x!owH)>#t3V#ZON&|of_@az82=x`EU?9c};tmhy9l?Z9f8qly{aT>=zVMe$ zkGgP3LJBUW4i}XSOsL0M(|hH(gOehnT5@qzOaZ^7<=F)wT5^~uLXhtnQV1*E0`kIy zNjDH#pPggLa3T0fi;BNFU*cPhpp}=E(4o;w`^mcbIy1MeqtuyIP6lPbjqMfV^^>ifroC58D zyiqMDa48r=5ys$6uV54iUNeGSLYYZQ^aZ)YHL-huf(4xT7ye<$^)pNfb##@l;R+A> z6Sv`;Oz*oxm~S}51yM;|VqwQC=JFY#6%G}+^GAa+-57R@a33>u4PwaRi zQwjNT;8CzxKB`2+MqtU4h8Coe@SrD3t?3)Lsk6#;ViU7!ZdMYC>V--ps=0Tw*^-No z5)B-cD;0fM!|@jNr;88L4Yp8KlSypq(wX7Y)C5jF5) zIp;;p%?tE>);N~FM7uurv7Y48zBCrrgn_=So4%*ZeYpGm`J_y_cKt=A{jq`lC0qUF zbM$35{Z;p0p2qjpM89nKE>UMk`|2Dto%gbxWq^k2rFgt9qC&T`bl_z(tou8)z&Ovn z)rx`pgXLQTVmVprdV{mVFJIXW%-sxjZVfKoA6g@&Ua=cmFCC%}9NOF(`Y;FEzO#c( zN5ej+XK4W3BrS(O4h$!<4A&<kd{yuz`Ug>o(;t0VrL=7-C zjS%|2xJ_?tO^4ay8i|(iq_=pA7(8uD53&6yW6Jb*UM>UdR7CF5o$yA(5C`xggP>Pc~weIF_AhP^2j+qk4YxlA=;rK;wxO zjF>=1#IK!X{eM{SbXm(enK9|DEcHmPrq7zDKdn}O%N11Lh^kd(x_g%nesT@yI3DbE8_k|9&kU#715W}zi!_urUp zXG|mSnQMFzwy+{Ijgedk_c@4JxeJ^?o(?#N#*Mg2sIWqMG(+_6Dun;>LigRLzE(K9 z&`_dbxsP^S+UKVtF3dDt42IkBoi+*Q`0uP)Kt5C}@ zxS7{k#;%>MOb^2J!_6|@ujBEsy5xsVuO7A%`sBiWw76G$lYL&&Yn@(p;Vt>cG<}`x zh1H3|h)#un;mG~oZzeXas0F?f-pVlZ45+dv6%v~5RE5!2Q}(ezFll)XB)E}FB4lL~sbmBv+ z+xFxw^pJ&j>nE|K8r0iFE?_k_NZos39*LOA@R*se5B08M=)2z<3nVR1n~D}B5t{g1 zz@cO=;Gp)&FG{;_qd!=<)CJbOYx(hE2u-c71 zwfS4e+z@t=3G4BoOobD_eGk+5v1UmAhD#=+!YpH|D5FhAvionwhG^y*qZIFg58J)W zld`?AEGh<(ccM|l-@Jwo^oC=^wjflmFLt0mRhNeEKGpBwI*0~Rkv=Q8ly8R3q1;241C0pJai2d4WNJTuq5~(wi z?K;xi`u&QOYP_CucV`RiB6EeGg9byqPF6v?2NzsK-<&Cq5fm9e-4T8;-F^veukr`z z@^3TcJ-6j9BF`?opIt?+-4tunhd_TJC_)0tJajX<*pDeqzU#vA$3Dk@_3(Gi$5inl z2*?b5BGF~c;>QY#f|8xu8Q@cQT358>#;{3aU>0( zK!WLtr6K|1fvw{28<&lXnj zq+i9Ue|$VYc}A_bJ>fwsV&kx(z!3WE= zX5>mfe}1Js@3?RNysYxF(I)E0jk={`WT&O01n+n|u~_U2uz$VN$A7W6*T1A%{638s&*Ew?kK+`vT&{ge$-m57=3}_= zgM{88(L}Qbv@yvNQ?v2qY4A5|OlFW_de!{|bH#&ED1Qr#OYl!b3Wvh?y#`SsyBLXA z?C0INMsi=eyIvPgOLyI8el&4sqQy>Gf|?6ZJkl(BRs}z=ORm|e)_-?-eqm;;Mi;u) z^rza0gx4AElFZ372BM2Ak;kT4o%GkX%DPH=GKYT1`IL1kiEQ#}lWKm2`z4Q%Y8 zx2uMk>(==kW~9$Y9C*PNFl!SQR*tcdOHRQL+R4R1n+6`Pjt2s2X3;kb^vIxNULmIx z<~X@eK759CpCkeFeHA}@Mc>&4%rsu4S|3ahmHL0Cde`TgrLUcKKFb~IRh2ID=ipZV zJ%t5}b3nWpwpA{XTY(I7cgU_^2seJ_{HGJsfgp|JXIGu}I!=>`;6B+9$cPv(E!B;g z-Vw#^zNC=c#wSe(^wxUj@V24}m}1c0$An`86lK}DmTq3er$;)8B<2@6yrWU>&u@MX z{`SC;74;cj5?eBJ{8X z(fSt}{ALLlC6-krnZ3@LD(D5|=?M_9HMvgM>=(ei82o-0dJ>T;vgb2FwhwZ$Er%+*0GLIW9*~MU?7*w6tWy!rA`TDM6yYS z8xQwmRKl}gI_|{z!uHW#wb`~Ufl8fn)6F{#DsMRx15fdGjypw~*oIWg-X?A#l6SIw zf8B|nGw~oOFr!IcCy8u4t1^TeLzuy)fP@x}YnF-lXNG2<7`={Es2-pGb^5RudAO8mu{SS&F$Tg ztVAy{=y*pqYk0=_fSuemvB!Wfy{H0PQl^ncoho4I?&XU70MJFsHPhK8)}VMAiFFIz zSib6i#g+uVCW!6iu42uQ?`kwq2&CacszFt9J{hIZt3O@XaU!%RhVg#OO&tv3S2Wst zioqwcV0e_U>P9q{qykS9T~gA(c~E*cTzy9%47q4+>wdR*b5A`t9+t$L zVXR|Vh|R=%@%k3`wdlGScej*%rbDD#ZZsA2wch(7Ts$|s%5Kt6s6!ACTWXcnRM>{O z;&uJeI4df~gFMDrxGh9GUevwIa6O&9ULn$cR%H}R5_lX#W9oE;+B3}xAnN21N{vD% zG>~kF_!k+=I}U<;9vIIvmVjC9HOdQAcliIg=+ z;V2xEXb|C<6oKFHzTg?3ugoBl+SO%3;0YYHQ@OX74lVm8nc_N@5ViH z$x)w;R70Q4n~ExmH?*^J?%@|7Vc8BF91$rt^TM$$7DfBr?6>;F2EoV@s-S(D=Hdmv za%|KE6@?sojo(!Tjuw=X+&R;3=3khgFn^i_7TPDljFkkv4eJ-$8|8jUqfWWMBj0t{ zGmh~pk}b;Z75BwF46?Z>V}9S_2)l$A@DS9BW*%D7yk<__8TD`AOFUTEL@~x(&~Yhw zkNvt{j4u?Z?jm&OXP9{NgwLd<-_qOTaqD`W@}+G8nUeM4=BpAy-VbZsO5fybN#fuS z8Kxw3lgL&}rrClA-v9NhD(Dw#Fr{l=vp=>RlDBA{{f#_WI8Mv|BR7L2xnp-76A!%& zm4%2wpEvz_kglJ}@zV@S!^oW%uve}4OUMeRYzZ^)w}P5sC(2<2faz|3s_R)AYT` zs4It*gIzpL{Z#dNWX$;Iz{8OK?&UT{&CyTevsbUkm0L;#SOtao`8yAVOo~@;OISY_ zu5lak7uwz5===0`@oV?8t4zU;Ujvhh;zeeV*B}z>RvN*3N^v}9_cOSDAa2irP86W6 zN-`6TreFCz#1B4!bArS+BUqSSs#)1{UkSdQI(doP^PlC!h|MpP@d?Y*y_u#;64~Pq zw?C9<`XZi60n$%THXFFoaLh4)ilmb+0`=5O90(wdrJm{R zm4^!Fk_rkW5ZXvPHwl1mu|y%zZ5?hwRs1EM?HSeROE3K0=Sp4}{uxkkR}cyfsq%Nl zl9yp&=BgLM2?R5eTL;MPE&OF4mk1YYT1j?-*K-d6<-c|)KHssq9hHlhpoC!gxgwBi zQSRs=oWF%kuunc#AX!)yDvU@9NF5bbRFl?MYe1u3Re|oz`jVkDO}9bmkBU>hphR6= z$w)J`Htn=pFOmdCwYZ8D*6x6 zvn*~kTv#;`-!)tnHQn_!ALwhk*=u^nXnJL6^80Ff50Z&8ChD!LwI6_dYgCM`L56aY z9H)u%&6>fATGf@BFU|N-%bGWGf_V|punUH8GU5>(uyG|*ogD7>x>m@o7MhhgwCsMt z688Reegg`NaZil`6Ayh$OE*^Rl1lloI*|=ayURYLsdY}1C)3Wp)jnd>?yn{t5U9A- zo=$-4Oy6Kr_DDj%5qh8r6@7afur069sw%$saoalO+apqvlLZqL`8){!+0?>ql29SS zu40f@1EFXPi6Aw_Cj`YFe6oB{x22mbr)8jGFso7@+ggULUQ4SX8>37w2)c#dk1kI- z(7JA)ZOPE<-%ESGmT`A-KNVU}P|4x$N<(})NIHWfKg3ZI=TIT}Xs(}CfkuQjXhc#m z>is#a8#(MVN^q*&T|K=7?Oa~(T>ez*kE?k<)L^2!)Z$D{tMd6&TOuR+!`OBD$O;HW zF)c0?!_uA;elTbv0wr69QJlhH9kX{e>Lid#!L12{_vXq6OeBwThcSKmtT)955;OfR zhTqrHUQT2@?s|cZD6nP{MLlFpJS>Q$Ds>Sobkiv`5UdNf)%TuZj9I4li!Tm{f7z<` zBtK7dJeVOOhrxMQE_;JfWsf2MhQXez;f;jOX?|*=;9^QZaTFrOf0rn_J@Hq3svny0 zp#V`FPWL>Y_(Gk2mz4@ntzbOeMK-rY{$t5#0%L$6%!_2GRfCl;>sM3`f1fvDi%mNk zB$G>Ejw~yQm~Yn%W@`hM_F9v6&WLoIl!A&gdLv8wT*&>;*gh~-d?79y1&U6L5=}Ba zp)!L`>-5Ytmd&Lm&ZmNeA?0_A#7~_WpRPzO#g8>(&iJ^K*m#n#k7kIz^@BP~M=%FK zg`ynmX;xDEnhwFzE|aljfjKc}s<%|;sC#RK{cIvjq!F;hDLlLB(!9VrdmOfwjm^Ro zlFB|rcDs@ak9r1Z;`q~1iNRM2e6N##(|K2!>4lnCovr3XRd7gFg+*}@S5+vqSF}y= z`Fw&aaM8fHsvs-vG`f{^4>?z71UhE9nVX)mj6Jgn#YV?;KE#o+&q`J*Wd3EsQ!&vN z&qxOSs`DCSCS^j2>ZYolRGAK0#S~!8-WaA?SFZ+D|H5+5Db&hUS%hy_D=RG99!w_* z@S-;74|GK}o9Jw-R_{V{hIDK#W4}GuR}eKkt9htooKDxh9Hnd1REGKWwl=CDvcXz< z8f-faan*)|?{Jq3=|-~V_#g<~u!IzJlcG@@MNcuSPOwxKy=AZg=-|YaKS<^?~S=VZEC{U z^$BhDF9=1E*xa=+L8cnb)3Z8j1)G}Vx>IRX-84lo9s+wI-h@zAR2OuPM1$AcvP ze%y%d-;vQ0C1&ZCzR&jO1)I-q*-Oh>)*D(rDTuxMu}PL!w&<#t%GjFB{d^xKO7U6v z?TXXDgDvtZ(XBDjl@-zKq*^Ug_V3h=w+@DL5@PeY(4kFHK{>Gcc&R>3|84=cGtqiZ zkbumvVr!#t=LgFFs^FL_`@|G`D%5tKxy$y;{zp!my#AY;PM1F`yGyq8|IWnOXb?D| zc3wF|tadxZr=93VA`vf|>mZq++OcUYA&GWnAa!62&isj%6eLyP z|5?e#t#Kz(*IFyd2~>d53=2=&xqT2d@y*S!PjaKk+6LKUMWdgKNtrRR@3GdpFxyK> z=XI&SeIakQCsz%45Zuz|LPi`~nQc1nY zU)J6&rYa=9@+@2JZ@ZsbF2zv6Qd>DLmpT45A#W*B3U8TtkPuK@T0B#dz&^y7TSP{E zIJxoI^04n)vdh6nVqS`wGp7*NyC{FALx%#iOH?$EO)0idaK0}PdcW`7Jf!?#i22u% z)q&<$dZXhHhd+Ny6Bry}WbI7Tvy|Fo?#v}Zx zOMZPvYDt4Vg#uc5!hoDqAFmJ8%f2DS#EXPa6Ik+t8_ch1i2T)|H`Ncf33Zy|C=7h@ zcXYn*vBLQ7eD%lUNJ6qU3Vj{o7IKnqyz3mE9-O-8fu+1fvhs||O8L=dfIfPR9vMP8 z`#|(u2|rp%KMO$xVaN1YP#tNE^lZ!g*wDF*KY&a#84imP7~4`En|S@`PI>A*JBMIP zopl|V@C0fgUM^UDSG;Chm1jKkuRn3yw+lKc*~Ia1-1vOMxL@D6qOZ2+g<4k32~uQ& zSmo4BqB{rHh32Usq9WY;QwtjzgUov~5R7=j?3@&^Dw(?pyu;6PkEXc#P>wCK7MKb@J=kO@{V|MK7%shr7+o1e700|V3&k!>r4&^Tb5XeOm7hMSifJU{V?rNOS6WJh9RUlP6fMOcW zu9_;O4J5^*N82&vUTAhX5@C2eQ9FP%0PuM#kShW}g^y!}14)_O zHr@7Gc6^89-zSy0)S`diM*bZ+^$;rK54Y7~!B}VaX>~!b6Zlvn34^5X!3pOCmYUzR z#p{&oRR*(c(9yvu_cV@6D_6w(jAu5V(X+$1k-CN;&KGB|Ny8*IZo@U=pB9LJBXW^>vLW*d%9>Zoy(SCTyrp z>TT~)L7L2)Or%a!uiVuL>!GKH&!g@Mt9N9FNjrs<>>CW&P7?F=~#@({c^t@gp5873U&?HIG`M|gm_mCS{e^Z6<6?jr#|HvEHw-KB*)jiswMtjI{ydo85OXQ0Zf-KT`Q}V!TfG z2WHoF6>c+$jK)~raS0yV)vS-b&qFlaY0CDP?JbYxlzcU$l})WqX)%5 zhL`Aa23N^GFWkRVyja$A%K0p~VEy^)%8FMt>Rs?N-0uz9d^Z39j%WX(V&cpSRAP0{ z!y71P4(3mBcfX0fhK$@z91;YPGg3~_Ce^t=2xAw<(y8wN=9ho!Tvz7(~)hm)~{iGXCn#$9#e+B~p6 zTw(k$CjelKW@kow8?Z2HP7P&K zf0=mV?FLW`f2!2^=qFNT+^4BKt&zhGUtzx(iP%_caF71}@US`d;&*8}sF5)bMf&Qi+G`I4J^6X>H-GqOqcjm_qxX zKMAMXjVpLgDaIbvN!_j!@zDyt4$SQ;+s}WOu2~I%4>}W(+IZgoIAa(t1TMcQ!%~Id z0H4X=K!^Y`s%j^34*&rC6+u#tRrjZP0U-hMdeHfc!sj~8j|>|@B-()a#za-5ePSL1 z3k=j3fX*T=1`+pt0?5$UR8oFXRp-T+WZI&uNlaI0xwx|wwvXWAa~UXX1!=H!b--6e zA=A7j^Kl7NPYnzL~IxlZuHeK_Q{ct zQS;XOv%>DWAJ1RcEF~ZHRFXZ0hy~>}+Xw#(>%ZUJ6kIL*{*bw|e{D!CtgHF`566j@ z+keF(#)TcfM1>FTD~U&~4y>Iuy%_o#D;`rh_2Fve#qj0O@yq@6kAKfzjQs1#jJp%w z0n+s0xrp22X495 z{xa8af@EnkkPs{zU-h7Whl`;|V6+{c+%xk@PTyEc{=68#D&7Q7srkmStZ2y6ZU#%qtx{ zjF~AJeW7%!!TBB<7@oR0$aePQV3`9xSIzwcqOXNC|1!QZOu6R|7;M}oRO$0yd`ita zW=!0l!^y|-@70~$c9JhU2_+F;kcJVtNq|)aYvMl9K}WR!4M9PTA`;kaR;VlDnkG%p7;YGSbWdZibdMIh;>88(Y!fFS9h(UpJB4QYewt)C#vsS zAcxT(NrNizABRqoRa`z8M0_8Y8a}Nwh#_V;zM3(MdyeC;+!JoQzFUy8+AX_NJW#vC z&7}`t)LF?K8nyj-L!|Jl<+0L{&C#Fr)!|D}`OuHxw&*{f;(ia`4Mj#%-tN+H42+2@ zpXFt5e-<9Oe*JjxD+}H3bCr=lZ||7SbtFHRJB{3Y2)DY_s&aq%c;t59$>d54bbQ`5 z@>f)MP^UNE@0BCpg3UnhJW^q+n%cWJ;l<(fcmL`4V;tu1*hc@$Vqf8 z-u&%@2X8r(U0=GpJQQ(N_p$-W*?`oGtL4(QoIfG;k^CnXTzjefu6Y9cRU|!}i$zuQ zSnA>Y*8rw>)qSp7cvqGgr2IC)BSCe;Q#D2C822}>domQ_KQqLY!QJm&Y8bh-;BK_m zqr%RXMuFs8==vv+l+huaFh1^{v?;sBET5 zp2}gzrBx~AlIWK5AuY7DRHFm}#)LZnV(kSw4IqvC+H*xT;kJ4FQ6;H@xhmfpuWgdH z0{$Hw%#yG6!kmyJpFnX9iMp*+hN{cAavPQt3R+HFzE8}8LIfc14Pi+paE(=o$v~=P zM^?>(kfa}d3l7>eZY>&i>f}Vz8jfokC3_l(;(ROMN%UUsGX|FK+6u+|4yl zy$B^DX}q@G4PSrJJL1vEaPq%>mGu|4%$X82?-s0>LTR8C+On?3GmVKLir6!B$R}cQ zF{sNhiNU7E?xe(JBuxRZXyYZT50q8{&~Sk}J{wCIW9oABTB@~;HuLaSatKr+77Jgl z^({$uFa?9ez(eG04d5A^;+q^E;s()tu#QgD_Drc4@e^yv{6bB95&33B&g1sfQdX?Q zYn`Wzo~BJY5k)!m7c7ObU3qe|AthXYdTvQ=UhZyUel5cuBamO17*#T{RCJIHH&_MErx~iR&6B^k zH+ZWG&?uOEkIr1jk-uU*2yQa?;cT!ahm{Bue7{DX(<_+G4E%USzT@7o6JVDAPf%Ci zTSql(??~?pYf>P>7zO25Kd3)O)F0ro4lA-w0LI@34UZU&b2Q_h0~&r#a3v}?sT&`3 zwrknF+MN=hJO>Ch>o#IBV9+Zeg5j+e<%#O2*_BO{H|wz165kTmDAjhdAN*Vd|E2tX zv;xqvVdeE=5`+=_N+AF?YfM-?;o;HrSR#~HCyF(O7I-$t`GgNOX=yvZcG&^j?)0PV z{clMruVJ`ty=$^%Wr>nD<+};yd&@Lz6|@H0bnhwXZIbA1ggV1=HuL#3%lB=HbBZf+ zm`5Rc{U$nprs#IiP+tHcAgyAD%I0jD>xv3ayyAsvo~|;xFSbpzZ%>Tbfi{U`BEkHT zD~4!l1c`>=BX6PSD+Jvu!WvZ~JyfDS8q&KflQHJvjSJ$CRYb>vgpI1CsF~D|sq4Ik zx@4Y}X8gTft~0N^XK@*_@vGBuKMqoxwI+6uxJPIYHnOVc>U*ooO!+E6^9Zv1BsN+d zLZ&!VuqrBFU2w^rYA;C~i(EvzF;hFo{Ef1nt`b^}utEFVBXxIo4L^WZ6p!H{ zt&!-2Q9myVN@r4h$G@K3T2TVt6;wBmee0Fqx|8{>u;QCnyB%%F8>`yS4jC3w`EO8V z76(7YX5PMvjsN^`-ooL%h2swkr$~Y0wAS?!h7;QfSL9na`9gPtM1I#oj~^DEm40V_ z{-}&r`Fs8vE%ZJ}w2w)f@4e5!O?tmT@teAxv(2XgR16Zm;(x=AZ)D29z4gC|YWsJ2 zj5%$)BSgdl+W`y#kEz;${Oz%v?I78cXo+?LEkqQu9c&d4VbTuqY!7p3M-F~Y4BXgu zT}OH`rZldnRIX#0JdhRbX~ji^BZ!9qh{;;tK|zD;jTy(E6XZl2D{MJ9PH{3K3Azt$ z*d5-yh3W#GJEB5;CQ9BwfOE0xWJ^(KaccYqZC)|VGf;Or0OYzjsnkI=SR4S@C<9VC zUox>K6IE26J_B~pk4R_)1hTM|6e>(+NtDpa*&1GL)_&397?*&HQ&Z)w)+cS!WZSlr ze`!2DQ5>_a>bK<%c7YppYEDa}Z`-#1uwA`o?%+M^;PvS^M2wh73a0qX5Wem5ZtUg+ z^w_ZU#%}cro_-t+>MQ6J9bbpLCALI$ygZeB{wIiB<#h0H8nn(b{G(&|bPEqGljMht z_MeIx1%WNgUei+!+>DBhd z(Q~r4JCWHHsZ{6}V~VGNWE#tyPghRC*|+DWs=@ciJ4l)yZE#&|m^z`PE|kKb2F5(i z#jo4->hxl8htADf(^-V!D4rocJ%&Z(orVhgAcjmG)DS@ng0MZuP6i zxW~e?r`oy4GW^#KXOC4%sC;0L^^<$40Ubo8J+?P+>ld0vt@rE+tnIpzsit}yUJx>T z+;=#>=WQ){bav1A-4}Zz8J8O}8wMHIy9+^n88;&%;M=B4K(PDcMRBt*H>WTPv0kEw z!7|l%KK)T)o>o#G*J&ZF6TI-fAM$9t+UIMSI%}`r z@7}*-z5Ybu11G%!H!^_?FZ?)Ug9dto#AUr@Uj)0!glL7k8Of^oa(ZXFFnw5J!ia@( zKMUmtQx8^t7n)$*cLS~O>dtAhvy*8T5*mOK;M7$yk6-*sl0~^!%gB6+n$zSlbmc8i zwtnQwZ(I0l^Y{7gZ{$I^T1Aymr)zaYweB>R=w{Ue7f}rVGYktj!Hy15NNvM7daHSr zJ=`iO`5C3?CVd&6JpLhhXdzwFF*ZInE9 zKUL>lRo8WdBD&xcTSaZS*eX4KBA%A@Dh2`3Joug-zksh7tB^*bNW_Q^D%R0ImO|E& zZiK;Q_TFj~%Qi;txme4`B*vpK!kX?##U+7Gj2KIbB-M-&KTnlL_mq(fRi1r*P4GCr zK~-f{QNJde9Vs7Hk60i zSDn3FzBFunj9cd?ip9@$`%*-ZYBV{jWyH@q=|z8g2~dSRDNyd6X_T}*FwzHDS#@8D z;vIY7DYaLs#ktG=VEYHKpPawox`0sp;DVNCK7;F|n9jK`Ptlb{&zfC}`ANAfw3YCK zCT0)$*L3X;Hpz5vODSLM7$0g>-vnrTQ=VDV{^&rv+AvE+Mhsp>4E~mLk(9lxVtLJD zS?&2`5PGcpeOJP3)=;6TrenJaF?uK8F&O`FxSvu)#8f-2Or-8(`$O#)=)sgjcQLQ; zpK|;0rt+Zr6)3%W+*_P@=D2v5xYG5*B+&h?^g>8k|Wr-a_a{x_XVOFfKz*IA|_tL^_wX$XG=CJykA~ zG=@us?R!FSh~9nocW->eqIRX#M4F&KnX_Sw<65id1DT6)hx_45i$D2$(;ojnH`fp3 zuG{-ETd#>|ISL_8! z{Z9~ouagaj3Q&0cw-7W8X7o*ov_z6Mj^s@n4y`%1H-Qg4m^TQTegGO3v>zE)oL~Tz z)EGJtjK#0a!xiLXflZ zF#L#t@(|xgAQwZWPY4PjH=J;abv&bxC~&w!x~gEi-1f6t1iciwdsau?fCL%@HYbJq ziez4f)I_@$1Jg>Nj%k?PQIjDLRehnMmY;(WU5eOsA^3>u0BC*g5P`!d<$Rc`wE^Og zRj|oTT`_d-Azd~0teKiO@Oo;gQ+;8gjwRkbUzWjv)i_8JI8JA+WnPk-w*IcBhitYY1N$sCm&b{FM9Lg|*{k)IQ;Dh{d>MZWO=+_j7#i^!Qhk@}f6i4MHd09u5 z_yOxj)h(M@$F)79>&NvY)Y-opjnFrKH7}cHpR{_)Zk)9L$jd(MdHDu&PzCr&@7mOrFC4tO`!Cnwbs0iCe31Mxc`aS*hOm#I^gHAO+MK87# zxgbv2f62tImwO#-ul+H2#r8#w*SzOeu6Kdr8xmehbJqbN3`OM?V4Q`P3xHJ35PE^h zsNaj=M`m~1<;P@H003qi&Z+??9k<=Lzh~Zj`mu#y^Y=wl+BNmP8nZ#npYDm4n8``B z&VXa+figgfV2Xgr+Exn>b=F4g+`LRrN>lc{o@c8kA%<% zPKP8s^Og`VW9;%#@!!0RTI-(Q79LhB@L-h&Ns;_U zRTlx|9B5g)O1%&f2|||Xk5>f}?(^tLu*3!Vr7tqR7FnOCKjOFhO^iJ=F2$F;``%(z zq(a_cT@yWo#7@cMP}$)~BV$v!RB-uFRmE2;`vJI(31h)#csq^d}5;n>I_T>~xL|E3$N3dr52MJROY> z4fWE$EZ6!jR|#I+(+psG)CH4ruHE~A-0;X6qL&RVP%bBT*~l7`Y$&Wyh7=}D$(nj8 zIcg~~4Q2y%ZDD99_KIRc^J!!)6*cP)Iva+o%qPuhCdG7O*o5o!zgz2;Ph3(v4f?u? z()(MOI3kUdHmS)w+K5hsn25jpv>fG&R5|sis48DXZvGs^1@uB3&K?DlcZ*#*`%D;_ zer{OlKGZp@o9i;Y?bXvg2|o)sRAT+JLf-eMgVEglY8>wBi=nEbW8jEA>JA-4^)!Z9_C+^%7-Zr!oc%T3b+LN8|VNgKrkI)z-P#+Cm{_Qu8-jF z2jKK@E0DjV4IP^tS@ue}ZeF$(Q1M?^@&40d)y{+d{*ynhIw+)ZKpSOuo@sqa*L$sc z2F>Za&4$!KaDy`-oF^a~eJ>$|EbHl4d$IpFBt+KxDxLy0;$m#cdQaRel~C$(cCHI< z1+W{ml23bQ(<*4N9DVT2{l!L?EmC{GVqS|^pAoH`1PYGRIWp~63UuD`6MaTi0zSrb ztXoqQknqx(3NJYq!;3$2CAQ5WMaHxo-mQei<`G8OqPs$X8}(@bI_<&0hMlX_?3>rB zhWA~ZQY`oIbX(_8Vrxu$pWvp8+YH*APFleiK#Ja15&Tq$$Iq`{L^;jUdo?0*?d1?i zIOhp(TAE%R@@Ik_nl_x9_}}6PFza~C2vm;t z=#d6axjON&hj@&d)t3}-kLE%?YC8K&*8w5wI9qc@&F5%Jvz$~XXbd%2-w5z-&kd1l zxci~_D1>^C>IB#gw!nD;V#3D-WkO2c_XP8V47^sf8bloP4xo7={&^=>4PvndBjI^s zX~Oa;Sl`UxR9D z-b`VhYLU=rTmv$b1liD_+9IUx(V*Vfpgf+Zt|6qk)S$RcqRE}7L1=(cG->0LXt6ax z_(XJN=#(G}*4-fDw-!W?=30~axVy9%s^-9>A5A1~bVcZ7%{zpA3WVksB##n7QZ|@z z=IjyusNz9c3R4Vk8XYD9WQD=RrX%>;17ys>nBjv!aS5c|LY$Zd)}^DxLz$oGPnpI?jrWePu+76~&QA4?Oze2Rcl6W<%HtsY= zi$J1WL4wG^WR#X<)JS9%Js=2On5>ZpWMM$o2nMlimd&JN*%`sJm!#AbkX;Ox_w|r- zfI%W)kX}0QBS7BsN?vVAKK)82R6s7eSs~Gb3%d}P6^UZbLcsy1q^JG152mmosXRsk zNw$#KFXY09z0;<}SlOYhN2270Q50H$MlZo>!DKKCaGoSM4(YF!Nev-Uyw1lV8^wzo zp!zAPL7u8XxvW7$s!7vA&77*qv8>7FsZkV6v@XHnc11}~sx7Ufc^s_4;;FGZ0D9bh zEv}<&q@%6ksbS$st*)apzx%erf?8o&$4E!dy+tq6LgIpmYe8G3$QBb1K)ETP3h|)G z-yy&90KeVAGv5Ir(sn>cS87EAlHS@RB>N(fbW~nC#@^SOFP1gRf{9%CvBX>S7?vSq z*Ctz@#*&_p1P>jD9m;i5%^#lByPl?p%cjn$M6Nn!JEk93D zobw&ZW(x!UC4*aSFzpT=pQKvi4tXP~I)p@3#atgx(2U$uv0zDd;7VzX#CBu}Vw+}b z*9xK1(Vb7#S|v5}_q6F)vMCX;9TU(~6Et7bvH1kEu@SJ3^spakwkK^db<}x-(P|$o z@HX_u;nTG;<%k5`N(p;D$wGnUi&leA7T^Uhf>Qv|!i`}8oe>udqMVN$hHUduNVZMD zF*^;C*9t+rBXgx$hOCpCER#Brk~)UN-ap;EFY=;>@7VhbIcRt|7{J~?ti0c9(W6U$ zH?yl-m*#d$ru^B~a(d=R-c)w=QhOky9O?jh){QKM&$ z=|$bF=V9h$Z+D{yCR6G2@_bc^oIF6axn!9sXtmhtg|TBLEkxn5Dz#{Vf0g__5ev|JdF}T~8UAV@ zU*A*>&sN_Cz0ekz&kJ(*BvAMZVYghL(4zLx3V2B0%dl6g?-V^lr}XSQGBnim^k+l# zjX(~YF9U5d0_Rr(&j6olbRr*aKLuydqUwjPAwf)EQhgPGMqW`4215pSDCL%d2tmH& zaE&+Z8rXWYd>KJ3@UX6zt~KqtY?)D9Yc}8xTT0RZS&&D|T}nrW**6Od${WhH^!T&G za4WczgFYB1IKds{^2gigqduBccw&o(?9P?vJe@(bFx4~?q`UxHA*sYG5>1>I?MoJm z5V?!85(*~OhtVso*sUu zZE%$cf#s*HsSsyU$o%yi)3=@XHS-qC;ukJdTg!GJ%h_2j%x$lbUk&XuaDA1sBy2M?N_qXR$}wgxrCJLcY5iyK_Z4!(6<6v;y?MJqM4uk$}cJ05q03? z{c97*z3X9?1HYg06NPq}p&sUjTUBQ9XRv0AA7^`KeIG@20fpvrX6j;L?##X2rXFRM zC&hRQI8729xC4qC07Y1kYCjl$ZZXUZ79UGVcc<_d%aTnMgqo#8q?)182FhC=kY}n*?FX-A*7foVcS(YgO{QWEe0B?Mly+iIxyGEUx>k^bs8&n-Ah`1a_<|e zK$`Oz)gMQi!Yh%-wOVxiLAnp*5j*6k59DrieHUH5hi<)k50oyQ>9_i26JpA^!pdcE zTSQtrrAP%rr=AY3gr5}&@n}mm2#)lEP?5M6-6u)FP(Edyix9u!%E&Z&~%gdI>+0C@XGO+GDhjV|$z zuX`K3t1r8Njtcw@B!x`}jS7qqXfMFGWS0ka~a7C!_B zA3EB0JVyH-C6xlE(ENrf=oV(ab}kxErRZdiEX}MTNzWIIn2?T!@4juIo8syk-GA(a z==7|ur6PSx^b0Tu6{jG8P{e$lThyKK%#3{U{*3u`p|4YcIBNvq+&xJtoSd0%M||zS zF(5f32_fr%L>XivWm$j99-?{F;G)=2G|8Aj8rc|_`4&7#m zN3i}m9eX-KFMGQp(B>(I`F#jRtw}PI6B?@yWqEu)t!tDq&->c zrT4naAC}E!b%M156&O4WY-mpHy!QNi&5W+EFQ~V!pAYGt%#ci14SdeU-W?p0U&KF; zlbh|lSei%=OwNA3rF$(?^wD;vAcLbfc#72Y8&b#O(}!irMH&IBuOAnGn40R*)N8yR z9{BVl^8UTv=MJ<^vz_K&{ARanpPDe}SCB08B)opDgT>)ratFgtP(=fPyu18zHii1# zQM^TRaZRFrUx9qZ<4KH4S>n-rC6lR~Mx6ne{H4>G0tm33GmU_=Q6eH`yKGPHVU&5nMT9 zk?suZ;rcJ?OnW@`RlYBc7D5cS#<66gTn^e?G4~7j$? zD-+LsdO^O|g1Ai1(1v029UzI>d(5XBbK3@;8ybWR)*2ZF7xtG$^Y?d1EKpZcL;Vdq z($u9<#x75vxk!dhL2~|2IZ?)LP1ccC38JUtR*A2lf!0u|6_O=&SxD-VrphbQW$h{L zaB9sXU(XzKE8Hlk*in1vdMLSQ>xPqM?|6O?GPRIiFoql*g&AI;K4m7UsXcvmSjXW` zB<=(oWo}zRIb~#;4JO@ZvQ~b=cs&5@i(r%hfyyZj6_Iy;b>_~Mr z-A3~K^%~pf3~)qX`X=-V_NAG0rI;D!%^A{Um41Wv2i2hU=&Jm%-8MU!x`czWLi!)A zman*o9IJD%NcH4Wxp@5?TGw5soZB`7vAGfIQofPaK1#{^jIhFYBc!PoxuWIie@4&@ zgs<0E9Wxv9*b2EX<2zn|0UobaD1Tti){jd+}yo)tA?;0He_192CBB*aZNhB>!W zf5LGar%zOFRLxADs_;=xyWR6WuGBl~3b%+hv%F!8m{E7T?tEX}bl+7PLN$tE*FcVc z!ZAoe|LVL_K0VVTH%K){Z&8jii`*!Bbm_x{=_gz-xMd=)LlP@X1z%^r+=Pps*~Ikf z;G4*C_t}QVA9^)jUzAsB2bP`T;ipTP^O;nq^U1T8CWP7G^V z@4xu&=Y2X|n8+^tcv{94nMGZ3GVm%NF`{jC3!0=NyW*@p7OJ ze`a;J=RFQl6Kp^4%rwGr+Vf|=n%c+f6*HRJwW91}wyC4i)7F-=krR*R=&{qwUx-tc z=k%y4w&Q`fm+=qFA1}a&@IKX^?x*uJQ%Y|qTxOG@JS6<6epGgKQ5JqIAx&M;vkshq zUxc4|U6)0%4)H#|GBVyowkkujnfrvcp=XOH)m{?s>N6~+<&3k?p2YgX@k8?0SnG6x zDMEEW>|X`=pMb-($;HUM5;HfVEsY*3>TgeYq^G&*$p~-a!_^#3r0vu5#ad2f-7WOP z?<1tP96rR1QxLDH0;QuS&OaDDs3FKN$R!R=ctoVCiy&uFV~0BCB!|M3x5@3T=_Nxb zry+&B#Rv7_lH-zbzi=g~F((wHH1+k2x6WKvo5Ka&QqVK4BQ=5DGVGe4a;%G4zLO5w zH1Gx$ah5P(OEn{{L?!rEmvFt0R(fBtje2?hT{G??)@+ZSeDHBZJD)J#UYX(5I*%k> z4GHI0-r-cdt+%l9*16N7I2_cunRnjGozfd7f>QX7yU(m{yoEF zimlkj#g~vdxyUAMRc-|4To9JFJXTPI$ed=a8CgokfVi9vER)nrA_Q8+zZWrjI2rKM zJ)7#JTz+GR)7xCb-hR4VZU!+iA3?$xPR_KisL`pCdOG`oc*vf?3~R!1x}XSt}x00u2TNwYW)+e9@M-)hOy(%vo#X?YOASN=HHrJ+*co>KWftd;sx?c%iV z6tS+X#d<#&q=h#Egzc`|Oe9m|=v+mi(~<2~m|DB89K}gKB3+2(RUgLnOHO+`SQBIg ziZl7eL*|GxF4fu)e?20@)`@z7`<*hkqs;D2wqmfj^n0%iG@%s@f&ijDS~Ooq`w9_o zTf}H{`N`fpv5NJj-n5j49!CW?6x?hVH`uU^JRd93D?q-{9;L zJj?+#Xp6no;&w^O^nvNIQeY6$chHp2s6cH){QJ1?RQ!W@EcDWpkAOW|Y~OJi?#84< zacg9w;V}i5#+1*T)=b&OI9}$%47G7TI30bbj8FN}!1EElrNu$?+)bIYIHGjrzB5iP zP1(y|E#sgCI3Bf4xo2_v;>W(T0jEuc2@K(^sD5)H+yVvo&Id9Ke)G{^fcd1j4suGO z3oza2&oAN~#Ksm4n5^ZR%HH4}smB`1=Tf&+sKp;?S5df6nzU4z;U4Qpb}lv6w$!|j zcMMt(UF|+?sr%&IV}jxjALedt7-ITm#o@m;?IM(zeDcd)$$x$63ubdw{E3sJ{|04; zP+K4FsVmLK#x8eT$1JX+Tao{_UoLH#%O|HkQ~sM$;V*m6;?DwB=Qr<9+xieHuL4m5 zwozXQPoTP-hj9e#q)7=6(coP~odL-vg2RVjBwWOyI1o)_hK;_#`wdkp!%1O9<7B1# z4XX<}10uX4Z|2qX1fG#8)00jVmK^deG z5*ijB5g8R76B`$wkO)mmPJyMSrDwzi|36r9(xPjbwxLummJNwwB&w7Gtt z%$*|OkV@73$@oh?f*T+ZfA@B}tF3PQMer0TZ^K5NLBbwMAkf%0dTT0!-*I!=p!8c7 z`#gwmPyhTYQ_z!QCTo_*_6Ti@#L+5qDM^1kt>C*W4Cutf6A6BIh+Ny*m%r88zvLX* z&2jckM?myM&8A3dw9f2@m3L3rrw$MhXI5_pnQal%4t^tH$=%MwIA_(q-lvBg2wxyZQ{uWNdR8Kfw#x>9~aZ8FWu@ z-f>!AJVxeBr%44Ui7kM=EQsZqDPO`|XlaB-lMjp`E2l8Fk*h*fjOKvBYp@}qW9pli zjn8XO7Z8G6Ujc*qV6j(Wl_0SCgHHv+)(hYKtURvm?hQj@e-w}=Ag$YWpOSoli64z?hlWHtlP-Nv%J)B;BRsP5CFZk~oc+#P?&wnmEz8h` zi9WS#((H{qz(>P6KuN70S4Y36c*f%9Nh8lu)TWRJG-h69OzX;%j(}e}GMMeSo)A?j z@_r^u_#o!d=pnjPUaEg{?NrLP!dway;={%C1C$%(ON-Dee7BM!i5w>~#rC__r_o{t z`v+hqm3zTeUd>sX>mejxj-r6+Hmr1&4ijzUSaYwIk-0298Wkc{Dwj&r2*)6?%%-Ee znpl&%(Rwi5y)~e8SV25rS?&E1_i;Bq^Zg1A9>fgMqDL8TaPvMF2TyX;8G1JqDvN@0 zhHnI@pRt9Q&aLZ#NB*Svd_bUe|E7(YOG>AMA~7@gSFo#DfVV%Bw%%Hu;00|qwfYC1 zkH3EvDD#bJMR;*TwRbrB$Y^+1IY4pt`Kw(q<^+bA&5*8)`ba0b=y@=OAu_3YFuwU! zxc+_l2vO@Y`VAv0foA&{`I!l7}?S3{BjmXT=BkyuhNeW{j}tB>2_Llp-M+VZjc7rpoa zk|F(hkp-W&)oE47^$plncpR7e{hK_pHlv9n)O?B@n}`A6BkyPKw1b@@LVzy#UZ^*K ziqWwcfjm_04qKxP!ACL|kdfez0hrH8HW1enAI5(?OHRpAkmHlMx>Q=x$d^#QZm3&E z_p3Xcdc#5_r;x9TwO*+86iD}WuAXW`rQlmS zuVi8F7+%6iv=B>457w4y(gw+S>0R$MEaAw<7g@ZIbl0e@t2fS$kt*565<#o9Bd1pl z(0C_BxH_Jn@E~O0K9f)xOEW7#7G~^+b2Dm4(Dxy+7HcZ~b0In1WBjscug5zv@U;vF z;%P1^v_0&Pi?8@fX^F$!rEbw~1b^?(8Q-cWLo*eBBRQMpSHrk2DdH>CZG5S;)2S5| z1W`RLD zn6;!W79CqNl8es9`l_ilzP!$KL|a$ApI$#VZG8H7{(n%wSs%Cc7egwJN_7 zaCsxZ6Eb_r#^y1A9gFFz;ty`c$4K{gQmnD0S{>hOeOvVDn?CtLDX1Tms_`Xs*3;3a zJ&FhS-AQ~StjwwNb$2Mu1p%rTR5p?cT7Zl3jLnorkWaM9>e2OQ2P@k*+5xW^fkA!s zzNhfVmLpiw^^QtGccNZoIy}|{VbS!$S05UZ7yji=leXY)gna;5P*h{NdI3b<_jl&_ zpwTE`_wlQ(3K!-amJI*!r0{N=sLWh2aZ!%GrzuXnpRp+ezOVI<^$A=SzsBOmT5pIA z{dQr9HQcRCzq3Q=pn?z?43fUb{vj#p4rC6{E~eG{{tBij7v+D-+{X~cAyc9JDd_X( zy!Z{MR(01FYUVaQCT7GKT7EWZd6mt)}@0GQy;E$GZ`C6^>U;AH(5f)BM+EKqL zWfg!PD%2*~?XMRroNKLh=W}KO^|0K+t z&>9EPhv}0)3)!2}DQ&4X`gkNSAkSBLYbTbeH-7Mq=bM`XkyG#4hs}rQ+kX7s^GLPF zT}s5=xLWV;^x4OsVu(L;iM>~4YEMT-h{>aW|&+{5u=*c%asI zzdHMT(}j3CPwac#S3}%yAfE4L`<`!S5zh|@1OOC-EF6TYAB64`1k4D+Y7fGJ2jSfX z5rBe;go8=+gUNh?K^egm?ZH&=VCuVIT2KhRa0sJ*2(wQJYeoordk7~yg!?Xp2NcRD z94eq6D&!L?oDnM89x4tGeSH@y0Sc264wKOjlk*8v$OzP^4^xJRsosUDgTmD_!nB1y zY5IigXM`*1ha2AoDwQK+5ky!DM_B7e*!o0RmLqEtpqaoUj>ys2U6Gs2u|4!7Jr%H< zB(R&!BfT=P1MRT`4*C)~4sJOITnwkNnBBzOQ5 zLk_Ti+ecmLC(Z}Ov23GhwIvd)1w7s19NEX8_#~lp#H0H{!!w{m<~TzVxEbv@lb|H# z!vJDTC{lFNIwxJYD^?)|4EW{NS&yM zc9h2{9;UiF_|IlUtFl2m%&|*&unN_&F6^USYa5Q8VPH3(Y5lZl~@j#Ckp7%P%h_A(uNB60(WoiTw45y46J1z_=H zgc)Sy{lS^aPc^MaeXp3+4fdXeN1wIFi|}OH20^KLGUM~pnlo{FqT*WhlY79qKDXI9 zz)ZA5Y**1gWZ+2GBmc?BLw;+s1jf}+jEb;FPsv1~tpKDbVopuu^a0~q4Ki>uGf=V$ zj$86_4w8XK@rC)h-+TeRL7$1(^S(P26jl^)uA3fD|-aOC{G@FT%yJ6SB2>J$v|+5updA~y{Je%;~~0mHYnP__cIGYzD!n$EJbV)2v;++ zm{+7QLnK?AJ+92>b45nBVP~l@S}Y6@v&fTZ$CfgBj+EgG@Hda0>W>vtjL!Bc4LeM8 z5-s;_MkPHjEf9uzh^E9NT+8Zd3)0%7lY!-L-sOe)Rd}IBmjFvc98#OlkwjdPZ2=V| z_hmW4xaGnrEcw|P>y@~BrP)VGv6GoQA_cZb(dB-hM`_Dz*QrvS;Rlc~M;)zrw51EMiQotcF1sscqT%GhHjMN@_VxpNe?9mv&7LAXnE6}oGg z>M=26l{NF{(IxO28iPE2_S&0Hx&_19C7!BP_B;~Nl2WX?k|^A1+LS8x;@hnH>jqS6 zg6f~_*;COqYwOWxlQs8m>v=J2vAP;=lj`uX^N#Q907?zWVli;nM))}npJE+ws)4%; z-0#=;@UHNWLG1k`xCF-9I3ygWB^op88b$HYC>2Y!;Ma(zR2FiWbafZ6d>l{tCvT}A z=ki^Jv{6g$PAQxRmo_?0Nvvq|pc#E4UXm?p|Gwr9-l&`1>O4>n3&NcUiZXj>u@H;0 zqKpc%kL;ywxvFdpI3{1f!d2yH^)PDlo{ID|Nbjq!s$Or4!zQoKK=}=9GZpKwQ-moh zRfis8$40frZ*-_!gxiBU3;iRg^O3KQI?9hp41wX%i=CR=o%Ii$jsC4=*`2LbBvsj6 z9oPUQr2ho>?xJ8K69S0;Drf|_|6mt<2P+RjLjr(=l<5G${s5{B1;U_cvr(#_zf3{^JJ5s_R3}kWPSsIewj$|fQr1vLgS9y+vr?yLrCz;NCX*o9br3SY?e9GvD|^lb3+E)& zM}P#ErUH#BK$uY2mAX&ygpcw;nQpgQky1KOd99#v(jfDde~tj{f9r7r7Up~&HTv} zR(yt;{^X@o_U(1|2l z&xrM@ZP|sWmViO&tbp?F1(qsWGVS|!7f(bPMbHETzYC+8tZS}%xg1U9Op4FV7VYlD zj)I1K05)<#_g<+|p9@wqy71H)ji027*Zs?i)fycH%}rV^k`ZdvlP{J)Z6Swwk2;{Mwf@9$3p1OLkpj~a$7kq?ner2kijg$O+20JmMJxBj;b z3$z!MiZmBNB;fH2UX%T|A6_P}`_c4TZJCx+zW- zYNmH;b+%NjmQFuccfa$X?rgH3pKR{XDw}Q7tVm!o6mni~5#0D4u(4?JwbD9qJ>1tl zcQYg4;a-Kpi(9wJ`#rU7dV6lyl*xWairCc$>()pPtKMquTAj`iOeMTr9|4Er>=Ccp z*z}*)^R?evwC+GT-4N7Y>$}8bIrZ~i+)PMW&pU0pTdf$U z!A96^ir%i)#EZS@`9{@reFl%TRGKtta4#KA$~qOMVe>t@{Wu2lfL&qa7@e5hYH8vKwQKv~FUW%rV=$1bApZOAL}d)2U~Eu#s%Cm-gUHM!**Pyq9n*jhc!PfEm(oMU_?i6 ztuX5F053hupu^rKNT&B~D-Z^+$_7NDK*8m00{ zx#qR_^<|Y>1h7sb&N7YtYrNdR=Cv^C$z~*qbFU7z@R@lmUA#8cpQI0pyCk)2XO6rRKE9m3`aa)m;} z+Dm7;J1+O9MAWixIao>vG45?E6Dkd~gW zJiTgbwyzI?AZ*aXkiJO`M)^=8`vHSd6Y#e+q@3`E$62#HEo1uJ4utV(j+Z)=D1vo2 ziV=%>JG^9OY{y|Z^lPDX~B=ToBCux-Y7bMQS;;#uZpGpr3i(c(`jcBXyQnHO*Sp9_?g9o=SwFQ6?OYf61UM| zKiB7md_)@STbosyFFN}{{!gZ*MDn)_iU);pLRkh5ADRinz3q^X4oWg?*)!;Vr&>hN zry5osmKUdWrdgMkRvuMa3F@;{)sG_BkE@$k-W}Jp|0CIWb97wSkIwO{ewgg9P#9;f z`qelsbo{GnPLAWGc}dstqy=tUb<+CP>-eN?JA&i1eJ{=NwBw+(>a_D$>+xyV`6$O( z_tlEyS2FUnhi(kb^_6Pzfx{7^O1Kfhj?=Ka}h>Q8>)?7n@zhT9e8 z#;!Yeod|ue`^59|WO~S6Y^&>*yNAm%h3oOWm*ma#&G?YZ@b8((7rU43$J{+D%kMva zT<^8sy1Dr(e|e8-3Z#2}`fi2S^QTAatnYr%@$&;5z0U$<1zNh>;IoYELzjvQm=&hE ze-$+_#Io(p!3^H;8lL{r=u#snbDG}504L7 zCEkAcIy#8!q8>P_j{bzW;vb@y+xPv1vVq#s+Iv%@9{O59j&Bd66FxE*r4Nfg0g$&+ zR@Q|K8Es(U!Uu7g8Ne?d^Pd{W<<^}p-XEgmqm3v?P~Xf&jtLhXrCqe2wOk}NtQ9_v zt_&lsC&g)q7t!Cc4zCy!LZKdUCqTO%5wbI=xvIB}QX9+Jv(cwy(`>g&lot=Z^o*d5Y$Pf|!2`XwZ!6CHE8cpej=L9Bl7DO)^S z*!=ho8?Qs00f3ljSO9W(Gdec5Je#>a%=FU(tfECWOF(@mA-ehpi5PGRD73fXYo2KP za@~-&GtFY+R)J8$rr^WJ0G3N&P|V4BsCsNv?}Od*k@>c@J`%r=rCe%HQ97CR4J?3Cbi?aWHaf%eFsoja26>;ss!7E?%p+CZy~V=uB$087B}$ zW=GukluKq(Ta^kzaxvty+D4hA%eMUO)yg_knY01rQIuB9krfJ7a9G7}N@G5b@)C4m96esTN9E7-lF&H@r$)^m;jb|FVf}QBi6b%N6xG5sEIBfCnCcbp8u*H|4 z3zY|tvm$!0-9rUyY?j~RPE)w1v3{&^4gYPhrfF4`{PfNSw@Y}9Pdb#P{n)ik3C85p z-_T@ZBa~WY^z*k@E8JWT-AL)%@B8BRwd(@wc;Cgr!qyIW)C-}!0?F;S%iVW2idNtK zjcbXT+$VwcycU^8%N44%UAz$+NP(h<4c!{k;3QkdW-9*Ql$dnW9^)Y(YLM|4f29jPV-1EwIbrgJ=RDG}Z@ML#N9Lt zb*(WvP^(M2E-Tx`qMq2wL8bC;tM^}D{+Si1YRVUM+m|Az_|IWX_+Kk3=wJK)qJMvU z#}hLc38f$lD_5tNjKSo>8;(*hl1w0c6@pXop;$hWw$6UHEq=P6E&q;`k}X_K#4ilp z_AoVZY5cF9{56UFUi`}(=FDMEUFOMDY0im^xWRpsZc%IVqbjD6;-q}((9NN+X!`w&{mg;64TPysQaNx^0mOgJJ7w1vWdVG(Z; z(cWkpA>qAIvfGcDG|UItg2N{IFJaGNSIrJ2tWpI^`} z7Z7qEgD9E?x3BMV>elumrA0~DAVEph3n<9{hh_c`%lv=GGXJ-xS?*skO7xEyt^8Mv z289B#+2cZZcBDdZ@n465G$yGOKy=p((hU|Q0ug^fPjf?&bg-F#8x@GKUxLA2p;iEt zWImb9>4+7^wpctI{L+qZ2^`L%nnfL4d3ES!H(ey1QfQo|RjEL&3Yn{WNUbEg9Be~vk@o&O$l1b6=f<^=W>Mt?R4JU=4nj#01v=bZ|rw$mk! zgbd!;2;CEjR_qh3zcFWLDC!3VFS0~Tmbb$yvg$uEhu;wo{Nux3U&_l^YL(9B!$p%< z4}u_ymg9e54sIj=-ZFrI0$ICMIbAFsd68PRyZz#OrjC-FHuKv|AtlG6 zTFcX#d7r4x92Md#m&_y4$-!ck*(`O*@4ldjD0z6|7no$+#_`{n6L{J#h^#>3&d8Wg z;Or-N zoWP3xcTFeQA_PEs;~E)^!Yr&#sqxi*FG+#9LX*S+CXG)gi)@a{7$w8IC3#)rlPAH; ze)uEZJOZpiB2Sx=$f>8DBoBcud5;7<4O`eS#C=qd548MQv7aZzHy>^AGc0A6ML&6- zGHF3$pYC&<(9uDtMnKYA!Ap5EU(Npub4*-G)aALZm$dbqJ(pGOaDOfvd!Gm{8Aj$ZF0r$k->lh})!eK* zeXf!6upD!?B4N+riRJRy&wa_&G>4Fi-Spe1Z|4fZK2_rGV|FX~9z}@6u(|?0X z5YO;u*yV``7n%za(BD^K>y5hm%bHHHCuJ|$?I3u=Jlrc%Rf8uff1MCPb86jkFue@ zEaxEv&+?L2sOR9*Mbk$ipZ+s8*WhT!ZJsz@oOiA+;4fAASat4_$YjvaO%CI`)5teO z6hokVf4NIWcpvG#x)9gW_7-9f?Q1WVn;8Ly+pU?G!LiY|X z%^i-7jH{9w&UZ)nSVwOUG)4!Nj!Zs7hKu=oaId>5@rc6Mv~r!hrhqxOCGhnyBq`qT zFHlNu7>20UCqz-rKt$~N@h0aI>5Lg@TYTaqso#TZ9ChelR%D93CU6C6Z__v*j7l2* z7xvz=Ev~;!7RBAY8wu`C;}W!Sg1bZG4#5Hh*Cqji2MF#VxCVl2;}A3icejuLVTb%> zo{@8A&djyR#u7k<)xq^MN51Zu-YrFKj#4m<8@QKR8(zNaRqi`F`xnFI?b3@;f9Fl zIa7GVD34vx*d)#P=KyKu;DXq`$|4?{GgXCEO|F|>3PvNQ2Py9#b=i$Q+oCZn5;DH4 zh^VT&s4AzUh|!iAEik5S@3bNNpBMQ$EsO$?(ta_D)dFhlJ$dpP(N$s3{4iJu#-O@Kbz+*5t(7EFPK_zuRs!WZw3@Vw))R4Zd;FW;h%EvYuX|FrZO zLu|`g(7L{J1KQt_O(P3JIL?Tu4PD~T$tqcla8h;+lFRh>>kNwpgy zIe`T)Dk+$h7vI5VYfD7gwfoBE$7r8<6&j(SKVbAvJuC_^dC73_Ym*PY(7Xb?3v=GL zR_-=kxo#E^=HRccS%UF;CTxLLY~|BL90t9^Cqb+pO>Ko$oN2)5(F4~Ia^s22W3PGh zNbF~L2FI;>49cg$kcg(tu>a(+{|7ki zuhUfei|Y~ormCO+VNj0&8dSHCj>M+q3|CNJmyOk6x2$A#-{^Wp#UF{pA~*659FIk_ zzRMj;#Fs#w=Y&*d2H*uYG-xGNL|NhiI{?FS8e{yO`;Gh39tMPw@Qv;@Ql@-{nhO-p^6{kis10pHa zmrfS^@zj=S#YtmIWGgk4&sOOF;?zHQSf#4Wdtxb-8!H#;@Y<x|UjA(z zjLg}6`O`W$?fDc*_-GyQ=!QoAlH}7{Y6T6i9&^eqp2H#0!?G;XU?n1sH1P`4(bfjX zko$dYDL(F6yhUZxgO$;?Zzc-CkrRx+$uT&9&1~$W-z+#dAI5@aGDyK?cqsC9ti8=A zBW?Hcfy^2&0Urc`fqsm2wBLRsi3jCtMFdCp(Xd@{3cW zZ>L$N+Ha>o%!RfxoErDG-@5g)>|}X;ezXoAId#DG-p;$=N9!P*#9=otnz8)RI@sSW zNC30$6(;LC>=mWklu`(>q%)Vt|-g7wdi)jArb2|essd{`swxbj#$>_nnY?mc9-PM92x zUWYXtEt{6gbi_mfr$cHSYw%2uv;AM3`VI@kH;pwqI$0mU2bz}M@B~e(%cxGsqH=)v zCz*S&I55(o=hKRE8t@|{o0YIxeYQY;z7e-k=oBm*n~#t8(Qy#ud!sVi2pXlIaU&&; zKuw;wQ{lzixp(WYhdoGR4m=5?IA3kKNgCmbW|Dw$4P&W#E~yQ0_&rl*?mQpyRfaWv zr9{N3T@AmbZRLuglYs4wP~SH%y=fXF1b8iF`}Jc#9JLIDM>#d`7Cm_UI2pNA@#L&* zbGcLX^sL1KIaq!C#!l`T@Zz}j$;$2eCvrZOQ(->D?@I$wPqc_qW$IvGNM#T{-*b+S7kd8-p|GeT{;`9oeR ziGJM1*Gm7Z*tYOK@v$3vuj%!!?Nik4LjXRzDY70j!FkQ0jjxf}&SIMAjfrc}6gAPh8`?-0k zP~_cvk@Yc;9Rkd5X-MXx6EO{stm^QP>4o6qzJ+K6H4e&90ZV19C<>sCVdlIugZ*yJw$vQhIOrlIk3 zB}6x_LTAr#jv8miZbE(aBuL{E6K!g~h8S3TNI++BA68t_<^W;yowGyZs4FJ4dV>FU zwEDsMO|j^$!C1M70d>{qTH?+F$CTKfg=5WP&F5@mUZU|iir((!_Z$z${`GbscWBO) zm2-iiK=ys5t#lmN_$8^yUZiX8EOCJ?|Ip`S&N5)F|BPm8*N%>4P)*I( zLpA-_+uoxWAvWgsnj2kK+NV)c0benoWaJdr9~jx7{dF0QoxsmrF^XBx>7n^1ki>`h zC@gHslcY3emqV2oD$|+S*aP6=6X+W;;j;M9-Wgv`@9#pN*x4F<+R4AHc_d;vvGl@Ui7Hd=%+{aTS{O3 z+1T8pdhGtTZ5b0C?SG2*27k@a{^nJ`@m@yZ7v6{OR{tlxq({8>%o+q4Mt; z8X?=t4?$b=E^$)jKcfQV<0-K%bvxs^GI6Hm-)mMUOSK;Hp1&tVZ@U^}?%1$e1nePU z&~Wg!&F_mZK5vrxaF+Jk%Gv=KrUQV+<2A5M_rsn@YOZ5BY__(2J6Y|Sdx-alcr6(8 zqoaL8-Ua^*v~w1CtNU7n_XFM-3Od^B5Ii&U>ce z6atlKN?b$XymOKP6P-@h4HD>ehG0o_Cct-MGojF-#RPoZFbl&0(B%H25eS3COYoW~ z!0|1#;8?;(yjNgE?<7t-O+1p)+~}fIG>*JT=yR;td}-Ss zQIia&t7x{wuAgK2iO@Nuta5+jy$fVgS)woi?JeUS=s^Y?ZA5zs7CuAEcaV6GmZ(LP zQf!bI?<{~|i|aXIUm^{`i`sw0`}dj}hl^}ddY!Cn?_agAsaCZ$E}B;t@LD6ib?FxF z_5LY)W?#Cc_Qwn@sjz`aRxZDj-Tw)kvM>2FN>w~V%pyC}%(9yMo5`|lJ!Ya0?L>M` zHGP}>EI5=%8umq>gpr{Y)No!?G?W1dag6bgA1(4k!>=fl%>f6TjSB6kxJHn+Mzcpz zjb%dK!!2i45UuPZs*r00)Sy4*Tb~>7|7MP`+Tz^7lK50}8NHKQB#PsDJ}}9a>gf&r zzzQwmWM)EVC>oC({lQQZ)l9dkJKEE`!-p_M>GUy*#1c(0`Rlc4rk|9!n?oe8L>t#q z37g=0jvR?YcGqzvrVh`?gqi7xknvm7r=e@SZ;o=W@dLKK z2vvpdU{=`4i+tf?H>U$!FLl<9h$6_#%f7Le#E-Mr z!B~aVsEUKq!1IJ?l|ow5p+T_7d13;zke*3#NXhU#DgDmkQea3C)8_mQ=|Lg$1H!Q8 zwi7k0dN8Zr(6Danc}lf$5k{f)u%RwH2CGmPyHnSQ>2(rGT`n?5z}|>4*)&N%sF=&+ zEY9YqCE2Io2n09V5eGHQw1i+VD5q=G#hVbgip3;UAT{QpW(Ay)Wa6J{QFD+D0}Kg; z;CtDQ`hy+OS%Z7|1~VS8AeVLTVCpD*sa?=ul4QWj9V3VcW6YWz5eQ2tVLDROc)F(b zZaE8?9V8O>%AY8`SJsBtvqr^M;v(ZXd>6IUU^3MZ^zO@Msl2U2oCS$zA^lZ^VpP|3 zGWgP)6Wie(CvZA`%qvY`i$N`A4>McjO^J9sitMvhWC`@Ea(P+@aQyycroT=ZopZU? zTlCrAfe^SIUi#SCMm2?saUoXCPpnhcsa_>w;uSnlsWDsG`f|eO73KzrdYL%W#mW#? z(+daP4$jMx5Xwqx4Xt@|j5k$6#0OgMhUVLkb;`%pQFNFD7N%ZBmabxL8^36oDdyDu zki!q=Ed;8^Jb*vfH#flKdRSm1tPNH4kZsG+#rbwdI>77u3LVc87prKt>;4nFD6|5laY|AF1Rs%zS20-H5c=3zMg;^+Myj_J>^Qcu@3X-?7k($_oKy3!|By(h@#}`uV z!fL+5-t|#qUa2~g=w518C0Vlv1+U@l6$xiP1b^NZedBJQ>+oaNz(5*VBztUaTd#MY zzrRq6L&w9=u$w5dq56I3sJ8@hd6W04`@x;f$l)zHOKr!sNaDSnG1n~&i=Rm(&0W|0&Gqt46*}jvDz7(vhc z&1zaxff9oK{j+1=67;q0qnec;1Q^<^-IkVL@bYY%F0g7jqx*ZscM!w*aQj%&j6yJH!K|C7^~e~w1~ zIU4=%M3v|No`BB%7bl>}zR07w;K&~f0FVL%9HWII#aoC*PzdJdn&esMR)PgRFV8}v zN5ZC;RfZe<1*LeDES^~KyrIST`OH3W#&S1x*q_0{sR8zz9|n3to*rDLBkIcO$YQkT zQf_(;QdVoYQ5+?yEA&@s9;WAQ^g(A?F?LK@tqNC36@G?aI<>x`)~SHNfi&l5b4_KT zgb$S}r$@?gFOh&eyhv8vvx?$5YGSpv&Wq$65=5m&7Lxz^T3Px}GiRTO;XiWr4c&@m^A-NZ+5a)ayQ@i=od5*d>Lu_@byzL;zK;`&X+|7%nw4A{p^{>iO)~AQ&^h+aQ0U|)|?tf zf}wB;`TeKH35jR>Td>yO&OR=KQ##Iiq=r(D=snNXW3|oyNC*Vg;xXLj#E;V+nid;1HLZ=r^4;13b}$VGa%Pwex7&!D;x2D*eve zWg4}m21DTF(IpoH5gCwr*K2^u?Nn^OgcIr7two?gLNF!fWG`@@Zu>HiGr<{mMG4;R zUW`UWNU#{I3ELolB%B}k!A%w{P@5k0HcBoF#An@9&Y zgnjun2#M0dhC39^LXPQfen8z?#|=4#CKwHW%~|L76w*JUNIb523qtfAq7_Mh2lY%L z(&%g%XVTNw92f9Hdw>tdzu#&ydR0%^PZx|N*NrPQEGLD;TcH^{;fZ@CBTpPQn7Q|; zN;uTsdK(Uve0P?46hYFzM9?ZCtnp?>TK%2Cdg(?s!u`-v<2xLrDv52*a3|fwv9*Yr z>EBaGGwt^!wZVpC0{YuB!#NBcvnt(%P*lmP#)a~o)^|r3rIPynTRg^1+yRv)je%#GcJ&xl0MUJ-s1!b?5J-ng9Z z&5{OBpnt(cr-tP6sMB%z99_+6;!s30r;;Udv@~$Tikdf92#{w3i#2e>!#hW-BuNO7 zPSRYiA5)Stc#dw;VIMb$fUj)o|sl&x1YB$QVsmG4gaxM-->NZ+yu zetBs|qbR<*TrxpgX1o#G3%Be@TE~Vd+n*S}Q+0bBqI28S<(%qQOf3$pv3YBq9P^q# z=9F5_&=I|h;VJAwvgT#z_63C4&$#F=M3dbe85n%HJP`HG+=IM!*akbsF5Z|xe*NQ| z>%+aKj#0v7Jh!kco;S8DEFQgvi<`;_!wNX@{kYN}|7fR3%aCqM-^^vfJoUqBA`?Aqo~aWz-X5 z53N$rIn_$B)3PYh=-pTWJbH}EihexYUc_E83(2kYDB(i)*hN(+S}V6apE+ub@=cc% zgjt5-m>a;NkcYwds7mbKG)WTdY_8_`J3EL7$eQpkb35Maw%wp`HAMR_MMsuc644Z&1_)=LWrO8;s%Y3rYQn29g=wmOhotdp(yKjOK((1i#vfxM&|8R!ho-fP^=l0i>d5{bPWNcO1`WR76#CliP1Lz~sTi*X?SN~?r3*`|01DMiN0&kmdR@j(~&7Uk6&*0heRqJzNrkgq+Q zbWK0wPhZN!nS3K4>-Y+(^3Coyw<5Eu8Sgv_dd{{+fQZt$q~qwbCHfWH)0d3uRix91 zvL)eCIu|q8(GWAsxS4jo!#l7h%8K0xvYTcmsODi-^+LbCW3CNV%`L$KYlDWI4#2DC z?BZCt$@#LcDO&a5)yC4s*4S*HcB2Px5|0)B#oWV)8wqAQE~Xjc?F|eg$0IW;0Lnk& z4M^vA%|aC?;U4jQe_q6uDJr(fFZyE~yh56Wu4w=&{j$twl<=#X%xsL>smZ%*Ok@hU&w3~g zyvF2~(>NB$J6jF)VM+UT*1psJ*Qe}IY;<+L?WHhekj*#IBM&Xn`cI7l6$cJzxB;DC zPJafYLj&P721RC8sVeOe-#dEVzF_G1+3>n3D+-b9ZWT??mrkW^(eU8KsIYq1%uiMK zETrW{VB-n951wWtzcHVqop!F5jDF`pjQmH5~4G95`=lb5BR0NVsRGa*eGXM zzqVKw9bZ_lF7E3;dm;Bz$>7%8aIem^Zpdn6-cF@whwW?S;x{US3C;W?Ox+*tbCrgl zbHw(a`>?Mx!M^o-${xH-J6y8DzRs36+4p{V$V=wrO+&qKmOFqw$s+n4JiT}x>+ED* zR^x|~CURNrindu6g6(jaF+S{l!>9R4e?T+!ic&&!(Q5qLho9}&IcnE?xKE8{AE?M- zyQk=vYIpl|*637168y}c0=7@6E;3$9>>g!wt<4!d+#LbM&s9(CY^J|_PwtDnt2u+N z?)cyAl8S6@eG1%SYpWmGHjLT{tYt82V>0MB*V|5eIcpnmP8ujMXYp+dG^P_W8etv_ z#@@8WvKN6eh=iO*pNCvRY}gx(8wIhTJ0Y`Cfsc)1H$5ibP!?uAHZ(m~ z9o-JeAWF0_Yz40gw-B4+FwyxiM+O@e1dfA zdK%0TKd-g*bs}wib*OR?IG7^iOSC16q}`a2-G!n&^rN;R@=jzgT$-a^p#2v@`2Q4I z{}fvPmoq3F@PEan*Z!lpG`PibKrJHzA71wRRH+$&7lRKxiBM)n4#kg8-@D^l+uOLoNb3|HVCqqY14v(+^!J~hvoJFNQq`6AK zA-R|Ar#MA7$gC5LkZJIyU?@a-8zgP5&NLq6WrOh%5gkuj!DwV%wt^(@DIHvMCUI$z z23-x>i1qUqni%>D(I24^)@TWMZxygvI5N^`Cf=JIM;|Y=Kuu}%C`drGEmhM|$Y$$Q zr7xOv<2zm|&!i{#_6Ps#tDZslw^|G*ytg_}CKBU6Nm);=_oO8l{@X+dg@wewN`&-r zhH=i)$Y_FroeGr)TEl6Wl(y74f`m;>x*7J%%n~)%10Fs|G=tkDotMMjZS4I!EVFa%k5E@hr8?e5Md5)o3Jm~GtE7(t4 zdt;i7KPEzYzt8f0dr$TBMXGO4*=KBEgU8*n6}pwf#~$3L+v+l^LOT;YrqZt#B8{Az z+HKb&$ZYJ$+`l(#4iLNacE1*S4u>a)>jfh1Bdqx)r2yK*WVqDU>d)oK2~&3T=px|= zA^`Edg=-12B=+=Ly@?F!L`K?ga9O0$W|otjJ|aGgfg|!|(BXuy4ad_6XNe$@!tmS7 zuq!TORLmdVXOeA6Qlr zQ{8TH1*gRa-o~#I?~L8io-U;`Pc(*l-8xD?x;$(UcG*msNIcO&V#JiQ)mALQLvFno zj@n-0kX32dc#mYdXUy98O(iiav|e&gP*%c*eDPQM)n7vrhQcJpb8b&zpgF8${kzpL zRUy=*x@ZIK;3Q8$m?YttX#2GvMEabn8J7{^Mb})esVuw>-8}YG^yp*CT9wVi2P83t zq&48HVV4-gxdxk4U4TodksU;plM3hD{@6TW0%m{6)1$CUMi0hAd+B_$79A(+n{15+ z&nTs7+y&Wu#WkcaYsm@KHB4hV{=Sn-+Td54m*(1?#^L?CwCVU@?7_qGkUJ~SM#vE*z>tCj#&^7)>P? zfpoC*adX|HL&T)eg<*IaN(TGS93okhbelgNqFxIuAGCCe-wqKW?nBgWDtDPA{ND}{ zPYi2<^iH&bT$St}`k--8j`I(FK-!ib$GeyhgCP-z>+&F-c)#O8%fQiWGF;~9UWa_41uI6D}vMKdmm2@ete6%75SH4Fu3s?(KES- z*Vb=HeA^>!qCSFb2n}Zb!!o^Ru$afhKHQq~;;mR-5r;dvDujKdz3#f(q#_JK^B5_jxaT|$(tnI@cu1RJU zC!4z5e(rwicWc9b-AQ@Wa_ACITA%T#@n!GV@5-T2=SP>d&Haz}ruIZd>Me z85bz!?Q2=l(*Zb-n8oXcKgT;=z23QGlIvrgC`p)*4c!lY^8mv73F4y4rLpeJ|1&6Wc>E3cqVB)t}8jT)rFj6Y*n4}k^hWX>Y7>Y&Z z;A3imbRS}gx2Oa&!Ui7VuN%?xwm>RI^T8dH*`L%q;SC*i$5=Ps6kS^LMph%Zu3xvn zF*}1iG8bV$E0XRABwIxmqy8M79LI=b=jWn>BaM@fbzT3tTX;gWZ*cj(&bEp}1FF|o zK`u(iyg)Zi>?En$|U${d+<6OtTK{b#U`lZ@h|SFjSdSo>Z+Yn zm#hs!bKo9oO~Ex7%rx7ujvH7u7K9}+Ym2|mub2MVCT#<2ib2c(2I=5Sc@Nu>AWs60 zFz=>DNNP*hUB!Cos^^cint&^WfvUi~Qi$FX`Fv^T?nC|9&J@fw`C zi!)cfAbxGv|At5DZKO!l;?kzQxZ{T3G>+BOC;D;uZuDK$Xnc+vQPR){!IH3(1{e-WB~biz>nm zfT`eyUdwJ=IM?#VT@D{eg-Vyd_^yKO#8R*A2PQ!eV4J4F8mCI}JcRs96#;H-hRFe@ za1Qyd4rraQMD00~za%naq=-wJ%zH_$=6(D>PlMfm?&lkJ~G|DQzv z|3G2Vh+GeK4v&T^HU3UZ3ECx#Ccve{cp$6lODZ2ac4A^^lvU){}M4x(U?rrrtjTjT57RmvoobY8!&_Bez4T+c|c;1RK3dUehk{T z^=UfW7<|JNfL-S2x!#we{7x;~)3rI0Xfkrh#o)ttcCK@#c60C$Nk3 z*f=bBqt=B+2pbc@5`=M!;?-TK@&+k%xqhk-;2Zy|aVYjo{K`v(ci@0*W1xk!ZU01& zA?{<1D8;l&9ZcxuN1Q(PCfNQ6&-k0_PE65dw_XLhFJn_GT;jaLVz-AL{ZhCl^^*tm z@1IW_Pg8(IHI(o9W+|8g8YT^yMxRd~`;PL=v&D%Ez#weUs~&s#j{?)qO%F#r%i<$} ztP{^svCh8uv9b$`J|px^dU?s3z=vF}Q2SstCl?OtQWPxs^3ClckG$GP{n>y2M2YLEM$Joq1D z7Z8pKuI1KUEgqO(#2*REJP9mqx4p;apRo%I5<19wxL-NQ{nTl*!Od#ABQZ+%J~?R=7&R?*d;cut?~<6BAzW7Y#Ud~HEwy)=v^GT zMt7>Gh2Q}X{ijT|F3`6k7CSRio!FXm6i26DAC}!^g#6uO`0K1zPkqZK_^~MwdQFeQ zZC)L=AifAP+W5H}-Q!230fiPO{}j;W-R5}}chUR>CKOYL7s{KQgs?dD`Lh^4EkTrX z00TSC^X{aSB-gHkO%BJ@5z*sX&DuN2g}yNCwYx^*fKwrnyza1}fR;6rGK4%2plYpo zWZi`X;ByDprbkRrwjOTh`+o<`m=M0*0@;#J+N(T2KYz<0TEy)N=o$C$=M7)3w=D#t zj|p}VTHix$Su;}ALa10X%8c_zaWbDpexA;QTH5lb2|rz&#Ib)?w@b@Y&TQatkxVse zyqC27%0D`}@HKEtTC24_rE12ayvJC^f|WVfeYCi!!gycJ}+5)u}(f0PqVt;4*P~|60lwh=H#0mRDoAzQ)t`VE&J`dKxFa zBzUnmMyr327=&{0XIq`?1ZdBOr;)+xTtxjx(l+cwDTA*XpPD6@!Rn8;x)d>=iwYxX z|94wGP=n7lBuB8VaP)DGlBOf$V2!3u#24?4Xg2n-#k0yM4J}66BsO((_?_d|vGx~g zeeQ>c4_B6f>fR@hZS_wYXB3Ay@d5VM;qCXgHy4;sd}Gv7q@*dlOoNa}?5Ld^@izgu za8#Smf>FWDYvEmM4M6(H8^J+hw3Hg82eE= zazrLrQ-e_={f|qzFm!au!R(2;6n5sQ!FUuXzpz7{v3S-+L)LyPMc)Io6>Q+Wx0QzO zjQ$Hdyjg61VF&tVhC}0C@oVdk|37Nh{KAeZ#hH{p+UixU8;vh!T7O{&*Yo4gMsH9t zDBL7FZudt{KK(>lcI`mIb#m)M!>#P>#uBUy#<&JkBw;WmBPg?wSXG|&0ezr61Jt%y zBYkvfPQ2`Oxnd*3>>pG{*c&ILm0^vY=heMJuJy9WS(1FCeB<1RqkI!s9H2=VQgKy~ z(ydthxZ17CC`emR9GhR?_Wjl8c{>?wff>mj7_W_F*2My;-^2xzrWr^g}1s#f`1yesUejWzjK^`nvBIEyv1Cx%)9+|DJkyk zUD)x+dEx6<{`(alR{VKC4CKIg>tyP%);ac%#ibp%_%1iQji@eQe4aT0yD&jlk;}?9 z)AlPEvkdmM)Dq|7x4WlR_a~{I2HR65Et^FuejUtT_Hsy{emB7NzVckUk8Bht_ImxY zbtC&^u&Gv*c@>dq9uXEB^>Lhk9j;Cx_cn*L9c2vHd)RG>cfAo*uz=%3cA2wSj~YKz z1H(eim8E(JwTsh?4kfHUrC^uki8ao0BWYpoV^?eHBpE)5s792;+PTK+h_j42*h`5+jbRi(jml(Rz@gU6t)GQQBFx=I1Yj25v)}<3c^ORvM?P)Z>O!=< z%Rz}NhNjqaqN1L5b|`7^DP?iv)(Zs#q<4A|vsMx7&1nwqqWd^F5#hWLfh$nmE=)#V zTjhBgx)@YhBbf9Xf!j`WPq;WlkaPr-m?RF#Zug$JI>0FTEo)rlCjixXO6v>bmE=TW zBe)uTnPnLk95+-YJ6_T!z7z~L?%f*9omiiu-#vM4(JncLouaVxcHVMO+)ltHN<|BN zhL!jbMnOH`8#w`?!aBp$KU_n!sYYh`O75e}*e{Zn?7|C7H0Fc*UxLrCxjvS>fPW5j1Ckl^M?8UIjvAZ5D zHtM|i!N{cYhCbq)Ze^YQ47c-5tyaW)zoPTx3`e|XC33_C!a@pjyTEHEgFok=(bi*zkmK=sVd%^1Xx%Q z4lw7zGTSf3M^kePwS`wG=-?=YQLwHydw;!Es(RkG<=UoVv{cPMPV#~~93g>RpG=9U zJF|#uu0wat)o!EvLz~PxNR0D=_BwE!EdnqztVU*f(p6T;Y<%#x0psiEz9tCDRb(Rc zeu~Vvc+?Q2E~v@Ys@B46!l)eMU_|G0G&rwbyI(H4!4&3LU=Y@?&2ca-SzQ0nl4Vkp z_zB*|IoxkZRaYtycK03wiqPtCyl1!o+-wp_8=T0l%l@RY!%Zg$;N9kU*Fp@bR_z&n z31aZn3b4Z<_nVAA24-jEi7<-#O)U*`#udO%jx_3Y$Q;$%8U%a=PaBYP;_#Pgq}eJK zh!|nqTsW7kNp3aHzBkIL4mA2`CmIDLj}MC651KqM(=sIb2Dy3?2-B4!&@H+sIKUY-mXNvZNX2KN#<0XrQq5P4#+3n3!^x*737386tUF@pajp^LulqmYJfU z6@+Ygxd5(bU$YLIcAWgh2po>T4k_$VeN;Mi{0<;@5ctwAPt|I6XtX}$U02p;AK8*> zbxb;=pKT)Q!trztGlUc*l zHCTPO8oc!B)nr}+64j})nK^FT{D9hZ?c4i9$iVLB4@J$5ZTClRuiKV))O=b$w3}QE zx2^0ECkDg)3l-6T|7$nbpH##=`CoQ(B@ih=r~ch;t{eQ#{zQbLT>BX`f{~}eD!Q`h zWsf@4qtPim8msRObHQ>5QmINZo1HO&;raCXAe$h(pRg~4IX{9g42NTwh^4SXRX9If z$;%;`H4Mmstyl-srGu<5+s}+ME!7BOer~|@Doo51#%3Xy|D`e8o)u7mG$D3hs%bTT zGB|AJCbQTdBwNfYIIY*BO_rNBLMZp^F3|kG3w!_eF7$-r(XeEZ_2uMLO;kjSF9`WV2F$X$g|mJF_iuvBDIhcIs5 z)uAv{+T^J%j$9hF*GVeP)-j2rH5^nQ4ag1;SLj~W6|z!TE5~WR6095qK>i`(N&V~U zdH>p|{f|X+Zj0o{h^MSwHEm%e8K2TJSwwzcC_sjDlX-GuAWZLYo@H8tUM2}&H3#Xm zNaGW)wQ)zdu*R?7VMu-DSm4R@EaORhdQD7SsdSF`ORCC+53le`aPnvy(5QFSD-B-% zxU=8HECjzq;pYvHhDdWnqF`GuF1JM`htR`0?j>PF$hJJOxY2602#T$s4Pz+LJ^D zA}x4pshJ%0kps)%qZ0JkiA%otAeS{F5qhXXv`jR2Q!`01fC$ledEyEvj?$`Vi7LCQ zlk0L)qsRtNFW}uG3}XNU^O1}g(X>+ca8roUNZF}+=8kJj*?4-nYn!IA@CcpW$M&8;!LR{1`ROIukkvj(^cN!k&O*i@;={bYO*p6xHA5Snac>RFy{ zR&jK?k4Z1k4<@tunc~+0YoE(eOPa6H1_w{l4bhSyDxw@7aKm4J@?6^9dP}l0R`fWj zg5%s?`jIR0tbFr6?#2rwuXhQfTL=M4Nr7$gC-ZW*naUXvAP2|r~K^TaeX zPZsgYN*(}Co{R*%9E?nebZ(!;rR;$3)O{9=jbav)KqJ&E0~SM`m7?HZV31Y@p?6vv zOAr;~)1KeVXaepV8Q)_0T`^`Md=4j;^WyZ<$qAV1CHhzQ^)ajc-+7Q}d30;62GEA0 z8J`vRMi*;5<>jL3zY}`1mX!Ika5Ibflzun|zKK}E*csLG& z(CPsHZ^23HrlE*}J76hsGXkoX8;WJSTzPrG{Or{e+GG4~L}{&sITNBbcyn2|o_N7< z!=G+ZIsS*@KIGKzi%=z_vX8DOv<2Y36@3%0HlqBj>Gc-s4E zyE9unk71rU8n_;0AD|1COEHiV@4G;rTNN>pwud6EG}x$$W?jz6tt(Q?_SiYfQfDJGnW7C9p&iGgAn}=oa{aa z@q=Ga!Ut1cQAi6}hQGl-Y_|?qex_5XBcq$*Fj0n86q}+_$%?n>P)yplpP+|sp_lu< z2Ib$>jAj0#LsFgB!~9q?o}b8-Q^4VKfC<%DCC8Ftx=<)iZOA6j;3gS=pRkrqW-%Bx z-aTjNNk+2WM^b;Yu9`*WGko~{j7cp|=0!JFOfrK;@?Yyf!c5MYfitCvG_sJ`^O^IV z^0}wMQ0T|d$cnrmZxrlMHr+MJYK`IgL;|@H9XK->uoa`s3=pn4fyTJ}TIEaR@1lgA zdBJenZmEfWvl=l7u>b?UT~>KbK%>wuF^iXHEq=_J(!>o~uBESHf3EfD~;7KspIv$U=*LB0d z?ydClsE$0YXJrY3ABG=(1b+NP-a9Ex@p&^(v^8~o02x9)Ld3lNfQFA37L7TP0PU2V zv~)n>D&v5)K(`Lje5%}9KLtDV<)f;0mpneOQegC6zvx*PE>-V7=DzTG6B1M`G| z`98tYGc@{eAWOaHT6v%6bigVA1~{!GjYuM0sf;ONtIzO~l=Nwp7^|{!KZgLB-YUfM zm1I1EG|&2zXnKil+@6!rhmkPIk+xxns7DVz$H4cqybr-smzpe0eI@`8XXua4Q4@~; z_N@09kCUPI*mYHjCyA((rg72#g-c1Y2FpTQqM0eScN(en<&vhVvI)H#80=Mp8di^d zOC$3Po?@zp!UVIEKqBSx0~m5R5IJ%lxdRh`A~C&>AjT<;wPK61t`_z+j>Tz=-HdPSx<)HOK)$9-IYLOawnBR^Xnb<#P|hFM~~=6E^Q*knn<4> zPMO^z97;Rsmz)Ef_0uC7fX917YT&T&McOP6C0O76=G(G-6mN2=G#OAM5)fOjuU!}& zv_-Cw<-Mq_Fn|nXRUmlo{f~#?@yo43@P8bJ@BjWV@c#R`ZJ~ZO{ij5HEimA3&(Ckt zS}K{9X6jGR&vB6=ny;icS3zc9rPYl^t56MI%vdP7RG~nNV>OjZ!Yd)Btgo@qM zLvG~d>y?yrvfAxPwI{ayaT?r^^kSA`loo&6h0l~cCk;l#aGx4|7xrC5zf~{vCK952 z`snMuF{t+DZIF?F^I2(M%rl)T!PdhaRBW6;H6Otx=s+Us!^6tb?b6ANAGz!Po#R>G z>O@2In>%6Yn zL<*U8;3~0wS|Dtc*(BmK|Gc&T{?Jw48_Dt;gCur=J^E-VRw8_jA4g2{E=iPN+KA@a zCWcV-AI*#r9_Pi@uce34i&HI92N-=K#C;3W@fNI?qHU;r^DP_L#Z566_4B5SM32kL5SV%O`Om^k`QC9fe#$Y4R{rExnNuf|JD@A0Q1S_5VD}({2 ztgukULMPj#Z;*=Tjje@MA2^rno&$gx>^{@w0<5`pu|KS<7I6`tD?Z_~DmpaV9LZ6<$mP%Wn(P{5ES^yMFno|mDbao&udxTqELWL?UfNry ziDwqS^Sj3{N%X#|UT_;2wQ55vJHnq=Tx0pR65t9naxQC#?{}To`sRGI7C9@T9}z8h ze6vCNKhweeh_~?ae`Pn&C=?RRGyXJ2)}&dG>6Eh2a-o#q*Svo|U_d}$9y3qu&>QB0 z`i%d@-dlLZxvtxuxK`nA!7aEG2*KS6F2UX1T?#MU-4on`h2UB^1PyK>fe;)*LZ`@D zYwvx=8MjZL(fi&ry4RmjHNNkA=X{^}{3Z`e)*nOl^D=g63-xl^N)0Pg`RVBfv$wU- z!ALBI;h4KkXNBqvUH>v}SxAoUz=r~Kc`eya<1?;6iDw+upg$9h`nTU^P7rpR?V z-OnQ}t{4~$-UjV?O3Ac2n-t6Da7By})#$xq1|TV}6o zJ?oJj<$Chv_UWq;9Vs)q06`mD&o`^>tMPA7cHTF>nWc|gK0IFQ@(K%p@NotAq_$%4 zISBEd?Y{d6dx(C2yL;$XW%bb7P4fN3)3`v*<0OFRrgktb`M|#WS{tU2=0Wo|(8yzA z(S-klT@m6UuEh>45v9{%e8dxbP2xjbZzE#0V5hl!Eyq(@M0xkBM?p`))I79ru88r8@M_qG-Y9rlXr#W@0|OUYcy}7KW0=2NvPR z6pWVI7M7?7hBm}i^m%UWquYf7tuzNkCllW3pncxkg*>0*Rx1@8%e@`l%cV@RlB|xD z{S2NRh}_-Qe?ce>XuH3u72TJG($=Kk>^o!bGrVMH|;HNND5D6$88F5;qX4eH_e_)i9f_&UD81}99x!S_#T_aq4EoX9FAb=`J`}01HvTSzX{Oy|4vo;|K{Rx zRn{=J1+nbuqd8{JF(Rr;L@-v}UtK`bd#7PCQBV6q^{#kUR;MtDUqc(nudgjL#NuJ% z@x29Hvgn*?{s8=_?RAydd2NZZ##8cg@t~-`coth$%qiJ{ZPE|=p2h^&L?f9A=70kI zpz&>V+x<14P5c4SSZdWrdXEM6a+~*EuEQ5!LR+QgbSb(X0Y9-1Zw0#?T|zXl;6njD zKAJSv1`1xp8>)H3fGU|Mk6l18SK3BayhZ#Vs?`?6k9HiIMfra4tVu?uu+UXnBFme3 z{E#o(z~2lQeR6YISBsY>(l)#wrE$Y>J-*4Ip+AVPsZa8#Ctnoo~5f;cFgPcH#3U~9z#gBANp96T=)N+8Ma$cpX^ zene`2qTohX|CGUg9=#-{3&c}L$&%4j*1DSb44L};Ic>RhWabEsR&@NHvFVqPA(Ge( z#taH@4B5PFWk%uEp>>h2_H<}`J_!?8c<2E{R9cjWq-zX@*_M*TomB(lv3W8dJdPcM zoL`oxaJb8ULwK6Au%JS&*(O_*X@)xUR63p$O%#8DlhKl9_2Y-c)G@hUB3GDF{^K&s zkC7uddYSa4csgQmmVQ=~i^s1vPZ-pCBCD%T@F!3azUpFqsvr{U$p)=7JVLUa4b|kH zuTHz9W5ZL!1;-*^&JCG-x$Ll$!v5aR(T%)rB}u%$!nra4L=BN}|3pQk)2l(f9g*3X zWP(6!YDB#&S}pFMGY;2l|0)%8aB(C+9cPzq64QdgRcw=hgNK2^} zMY{zg28&XC%>~i?6H~xU-%!;9auzm5O!^x?L+9IJ3PYUzTCkTfbNTdT_L^52oKiIX z2LBzonsNLMJhpXXgRTcnTt@|4xHYtRygD&eiv1RhW8qVF*5m94!{RbX7F%UMQz832 zSI8PpM(NDhyiNa?8>1~3_aDvTv^2>LBDef@+$fuJin;LoMxv0NT+RO{ORv%A$M`nE zyL^?WZ{NQ&B=ekidXX%?8Baw1RO6zmIOYx>2DT{ly*V=f0SyK9iYQX{XoZzKk@${l z6L*G`vcGg?LM`(md(Dh&a1e}1FB(b`Gm2#xkTbNrjqlm#hOZGEaa4Enk%{_PyAI)p zA!|6Zp!MUrAlzPq@COw2f&oq}7}iemnX!5SrlnrMeF_D2TsQjz;L)!d=-6s0r{RMy$eO~%e3a!2 zdsFdN>T|7+aH6|PX(iYYn=1!He0S05BUoX{fQzr`AN(} zvWalsK3WRVa85FugE2v5R2$Bvlma!GJm_>&yBFt`5OkGRi;knJXGmy&258z=3_wA} zff<`dqUG&Ne=#7-ZMCavstm%C)`7TOAi_{B?W-yo&V5kFr4iG zd(-k z8hNepmt<1bn*F2}F$uvp3hQUt{4|U4@-MNOaC(!IXuhO{;uZU?->qDCW2t3ma>Z`^ zyt{r6jc+3>Zo-k?^q?r$hY~_Ik%Vq~u@dUT85FnB^l$ozChH^lAzK)JH~o|t4ImlC zZJeB&0Y>G97;VTlLFdgNM?yoKmEsP`>dlbAWJ7`%WQXGW&9Ep&BREQNmzwl;L{_;m zIRmmwFLXPqn$Vb9p}5Dae>2Qq zI43ma9w;6>TfLq1o@~m$h8&1|znu!kXf8xnI%JjEng%I17ZXk#N^^HigFn=lFerVJ zfAVZLv-Pw{voKiM?{2OTqoq>j*alyg3N1_7BfO+A6c^G*P@mvYZNrJKH!c35aWbOT zp~%kgJ2l~Zlq>egZ|FLVo3pQ#`NDL8sMeA@#M23$%|&9zwi-etOOsbT!T}}XOR7go4$~_-fA2NkLepbE- zUj5-{Jq1UASr9S6ploMw-m%zz(^4Px+}?%o9)3itZRBD6)LO=WoAKf8V2xY+x47@7 zJN=;cVW49;IPre}lC`~`D&b7Kj_d>Lbql)@+n3C7VG_g_A%q;!=jAEd2U7yK1DeVn zg=YSTl9;VCkE;)1E8_xfbHhK~y>hw^UOxDY^J1K5dVM4s>bc?ubi0SA{5a`|@Evn{ zcUFl0?%46bcMVqAt#jwc>FWi*8VAG6*lUu{QcpZLw=ld)Uq}$kYqM?htIQ>9bbb)A zx?N~u^QlG8KN}V8c!%leJ=vL`2dHbUA;xq!O5DC`i$bhdaPl1^r6C@r6#kg?^KLtX z>uUaI`+`S6@V?OdTT8}>nGNMzGnglvK%mRiB*nsJyu1 z{EF!A_3*>a+`Cn3Ooh_qjVg4J$BevB46A@Vz!A_pNcN}NSGj5D}*8wY_! z?fZ??ioJA=d^MVqD&NtPZLUWU2-g#IzAnDyYJi zEI9;Ud)~_g3UUC#7-J{*?VE4>$h>3Qm*Rf(+HVm=zsrDUIE9_hg!h?7sXvVz%5<3F z0OopwP7dOBb*PGo;`O}(-$#Y?AjgIY#4fcuY>oKt=_LMah)uecXia=T4qjV|hb4A|U-m%E1QRWj-^c=&M1k)P{0McNZp))b zh!WSY9Nf05hpmU9B*>{J14jbeZAwQ20tGmc{FC2j z>u_Gw#0~Fc&S)TH(~}T2Rb7slLpQY&Iha#6c^#Cp6$vzo22P!NJ{3$eXCl5iOylxN z)jX*V4eq@~r%mfVx z=7~4hlzQeZ3EEN=!p+L%tuN!I4_QkSZ))!f=I`s~AL_b+uJXPR<89sMe?|ST6`(;2 zkU{V!|7ij8f6YcfP5N9(Z;l$lQTWS$*oeP4p>)b&LWF#I|I7*1&QL*@*1Tz{{)-c; zpKgN6#)D9)n8oMvQ`YAXA`d!+;>}ian;`DhQ`|E796sr04z@qLcGdykoY%tRVUon= zP)th1%mp5~;GY-k4Sg|kPI2;>!{iy_e%pvJ&TEzohkV08XxkjDy8yD^>`a=iycqWhBZxe$qTV26;Yq2t=fSsG>-mEQWCh+Oat9#Kmm7*%{qu}7sm8lx1U*!wQ5PfOBw8&$6@o2ityN7Ut|L9ahk@%%vG*)`?V|q~^=OWx=4`Pwja{&hf?EpItkb3yEL)R6HBke!tHC=E-KSc)g<2kLyn+?Eas3;YH=R4#VmK-M3R-iO>xf z7C`PUxcL>Y1aFET*c&S_gSjrz3!NgdyWn>Vsm@ZKaFaBO!akekMR2Ep!(Zw_)~n|o z8p(%)F^&}8?Eyr}4so%`_ARWbQ1`=&A}WmZjT412Fx-d&tU#Ukt*4O@MHKJ_zn;uH z4sKxc7AAg_EHM?%H&764A$HQpz*`pQXyCsiOa1p&*WX)R{~b=A)~|;~;LnFf^3R6` zdQxrm{W_`W-HFQjLjF9dWa7}$0eF?v!__GaCKKwTu5{HIOlHNuPAURceF3+Xv9xim z5;U3&?@a{Om8M@Ol}K1?DpPaCOpHhbA8Yo4^<1Ue_!#dSE!&w?&XO`?bE)F-VrKnu ztuK0(2ji?90TJPxD0<5|dh`fjT59=E(}rxib>NvCGnxn?P`=4E58MqcLNRd*`;vKEB)XDQ-(WbfD$W&KUB zw$B5&J6_r$0>j3BB-2HY9_H+Fa_u}waj&XdDC)-)OVV6hQm-p{LR|NF7c|wD@A-T` z`SN;!nwj!#2f4X{_`7p2_xP;BgYYNPV#ej{Hxgt#G&S|4&YNAqF{ZxTGHm&!vk5_t znuq0yB8o93))7iP(c3kCD>4mtZJ%{~E*E&37_^0Wmx9we)?YUUHoUAO{h9V{?u$(4 z{Ggee|H*3&;gjobnHM5g7dcMPJ3g6dh^)u@KD^bmhhM*+m+5H?K~^uJq~y8DVAf=c zW?es5H_C1#{4uj1n5tlWC2b`_5{>+V5CPLw8if?HOHI6_Gh~ld*}W`S=}2fmY$ZF= zq-JO-FH8(Y^T%#&VUkXPwots1+=xlH!9vwZ5O&Y~esVR3$SkHUV4`dmtvo$f?1NV7vC(!Ghn z_a&6@awB3SjfoCq(GmpF6d?kQNnrm{S{k`gIlkTGf6hkz%K`S+w}={nQWdJ2OE}0X z#Y6s3&EJaAm~cue1vtmkM^hMVCW@ud>BbB=9D2h)JzcEJc^~;Bw9L3r6dznD2 zt!6EoLLEQU{HfVyzEC~(S(+Y?<)|`|E{+B2w3T8C6YkUWEH2CO%q-1u$f26GQxd&C zQhb(Hv=J449%Ag*>i5B#9jSuc+Df^CZhAG}d#%>r1;8q_E}U7I^mk)nu68ZeCWRzM z-=!HeHja+-1Xv8UXSp?ZFm$27V%;rU_oTWNuvPnBPJHN(1C~U(FB`7Bl1iC$xH~hM z$cgy%b-BngTsBNu9s1bFbu>R+g8Wc{vB+(79Hi^{;*$aI=iKcIm7pJrx?kuqU`wYcZJ{z2aE{Vyta~)=rKQ?*PF$=x!OAmkaN_s1 zC~6h#G2rpA741{l)yT;hN6?8h#RV8Sr)g{ad=An9aVg7~aoH~LesB@uS}02V+Cq;; zi%TLw9dC=xtA!z+i#*CB-*K3uT=S#22Ub;Gpp?t1#X^fqq8f%oRY?$;DsG0Ddpr{i z7}ZMS(<0K36m=nvk8=BI3POHD`T7uRHNjv8CsCG1;%CpXl=VE|JY{JfyX%D&1T-D!U(%UDP!nKzHF@6IT*b!0ll0rv zrgVFx)#>hm$mmp+%hHYq?+Mk*>WYE6CzEis$2A77P5WH zQWc;%CEcysjlS`D9vvkwIPEztMDxZZEpIJ7hs)KMJSvFypREkj^flv9Fqv%nT?7#q zxcWw)A_P*axw`mspRCq){uF1y=cPoCDji%>4Rh+YN*_DxSJ6alaz9r$cb)iT+r>5h z^U4694Z(owmrJke0>p6TzP`JFLHJWsBEzs4|1z>te`I?w zA8|%vp=vnH2AQYEVt*E1NQD8orQ5xYi)CcVB%aNWfOm$Ysgb3>xti`sC8I0l$7PL=_A|}4Ffa@W)JkZsHggqfGW721 zydmMJs0h^@rb%|%Ns4*m0mOr{=uNM%xsEEa1mMh|OhcRNgm#|N};)s$8tAep{dV(HM{IJxa^|7M%2#jMH z()JTX7dU>f`n{?`JW|e^m4Ptmw33k~C`3g>>LAG?Bgr4lE^9{GM3P$O^eEv4gqa`t z2C$TyMakjNu&s?133$b2NWByhKzLpT4ccPTvKez|1c7wnxE;Jwh8hUrZ5tpoLw8hi zX#*-LJawYB0em+=^Bg7oXr)%X@;8nmd*M4G(GX+`^YCbk0h8fO^A%1ImL3oRCqX;Q zIn`Lp(pp?jWwTqHwar);wow zPH@Mx;3y@!V1gnpBTX1Rq>Cj+FtEjA9pAdN)yS5>$7iNDK`TU4Ct>}Y^HCns59whX z^X3_8Vl?$)rqXA$9C@mUsgw~~32MK-I4w<9bQVOM`)~}gu zGzl>e`oh`X%Ax9M$e$qH5}KXQ>a+X2gG;ZI{eJcx{Qlvr`Ky#8zM_*DDXKwA7xLdw z@xRT|{}i(X{`ED)fBYIQ{1a51o@az3rY{Nz6nRP1usINck7j5Sq4jsS&gBkTCJ&EU0u(KF7O|OK|HpeBItSN856HEP$ z&U1>No8T9Vpc=t;(#9l3YiOMrbP}lc(ZP^V#U-cjJ&_1HSw}1lg-Eo&B8_0x;iHwR z1jkAHdm|Ptq=8ldsk4Kz8;R*y1>{>U$N3qZKYo8 zNCK@JX&}*mANKLzdIghy{hQAJ8MYjnwE+Km1&O{D{oh=YMwZIrCPV&X)}j)UjrXrv z3)^1y+(SuN%7%?XX`Y%t$`h^UUO)HQLkfS3#YNP6OPelYY|$oe`|@Bcuiowvl?N?T zdM;1ZM->i2cXi_P8BAO&ZpXn+s^UVf9vCQ;cq}Bh$y4EzOb{&7*pqa-JdwgHgA_(r zxc$yr^sA}Y1u{t~xBI0R+ozFZgOk|v9e(C3zk?Q%zd}p=F+29A(;P+pp=-VzvJtZk zEhO&_P+?`1)Y$|V4dcLQs_XPPzbZ@v0xvi+@M(pOLl80M8C3Blg(<~D1;EB3NDQ*D zFu07e-o5Z7!Nyc#y;VcxVyB}r1px$x-y~58>_=dc)Gvmm1uw7~b47V>X$A;!0uI*` zMUj|x!2sR{SOiRGW7uGNa03E9wJ=Pdmc}7+pQsXK7#5k-zOWZf&Dew*Rfc}l98a3M zYB!1aRCU@DsVr+Z8OXD|A+A{J44aH8Tn`JcVDBMCcaN8n;f+VEE<^ZMY?~(OamD+c z7?VYuMq8R#P}PFZAU0u-_bWVY*#?7r!eHa@Ou(sX89YPtgGFzebSv{TA+>05Z#)$S z1GtQWEy>2fi}~dqB1wHuzpJM4t-MGn*)vt8;8adz-_*wy@7Esvft+xsr6|B^V}vUH zdmHn1euDYWuc@0?KEN{|5He>6v2_mMD1q%xM4fRiYAUzIbx%Mj9xGuvaK}Q^*duFU z(O8HJ;UGe5+5=B%Gt-~}3d=e5t+qb|~5Vr8d47{0%}obe5Ph{qyRLN`@a`gzLX@!|236eU`)XdK*W z@YPkO3-NRO50PLoOmz;Ftd+A?hl|yl73OJttt;dU(F8jz!QB-0(t}hK)CNSZE0$R{ zx2!Z5fgLC+Z%ACmWNQkctQ-~%=4|>&C5LGh2__GGF#NB7Sj=|~uILLtOrM&t>bK#X zHC6rV6A@K{cVEtW#@uzckaKb4}dLS#TCOwKahH>5sJMxHNYx@+zxw{M?Ri{JY2}XP_ zkcX?{1@esx!x$`(L`IO`dh=#*gKP5mXLJ^}U8Ucb~YdmL4^)yPdNo3^4 z)b;4l=$@&X9nqDKbB@a&ycUlHq?TfN%Z9)=5U#=OX5Kd2crnn3rQ+~N!IZj+P!Z`# z#HmIwoPLArHXwi*;Xo9RF2UI9osqnoiDc%HPc4UH%LP*_vJ$oT6}Pocnmf^%WdioA z({9#%!0sz4+H%b!mCTtBN-1K1Cli!mDVb({bm2I;yNZ?Kgv^<IkpLyKpGt`Xv?J8T3ovSK@@O7S63Y`Kl9yL7Vf zK9G$-AY@i&4B3ZPhMk^ALdoIvT=}!hs;52`cOv2{NwP)2C&{uj#bKnCP&^3YsC-GX z5);vunu*dIy~VK)tsR&Dz=K@YLbDdvV~gE)mvzCvUOQxkr9P6YdXQ4JDdG6i(6g%s zaD25ngTnHd-c@7fM71UV`0}LhRZ}6b##%;UWj6b&xl*YHnzi`Qan({EUt@2ju)6ZF za@E>4@tCz(zq@+d1FUt9Qdrw2xo#U#s&&m6U)z6n-98;(>t3Po@ksBwV`-w+^X>S@ z6W{C3O<v7e+N>SLqvK9h8 z%pCYZgu;`Im754n^%3C=!X|oH$!|0kqKVH@iwac!}B*T;6F%=(kOVb8F52&l`uF{(=J@8oO?{% z62uT4B<|WS3(M)Yp=p?=_>=~KH3$5thDBp`)PXtUxc37=eegNUzM>Vn-BSOPNhy$= z!8Q)=GUSQsw`^o!7vGf5q`Pu6D_7T+@aJVhe~QS`2mgJyvaIRMq3og!vI9w(%o!X{ z9#8;Z2sAp59Y?mOF=KwQ67zv*dQuZsQFVzlr z#Q8s)YrfrYJ(tm&?dFT*bia?|Zn~Qtob+r>s5u01>&=%0@-_QCKjzRTSzS@~Ds2le z0kE&mwDWmH53@ShRNN7d>0GKVOnmhiUKt|Px(b(4vUQQ^TpyB^eq9%5>3;ov>x<2K zJm;4aRc`$$Y67piQPyK~+Wxf9Jm*UA7fW8f zm%&kKiA1gaobE)nMS*zBm}6FCNyd%8z(sbR-0eEa9>>qkLj+>Dkm?z!9`3%T&dCx+2bk{goiG1Pt!# zN9CIV>*pk=S9f#qoUxq}re6)$8{@13&9w-Bo^^zDCgdoaLVL0bFn=H`I$y(sLcffH z?&GQy&wI^&UWSDDt>t_?7be@hYMc6TOe(g=q7ZmVF%^O~vgx}-y1~*Y)77N(97^!d@ykS)D49pRLe3jXUkx1suLwl6=5L@#BB4bii+R|g&7E# zFZBRs=wA+6MS(_e6gFYStRu}HkbQ7vLmU}F79)UZq{!&ZXu*)E1RW>;C9f}0VfrZM zVv)dd5O501!Oa_#y%a+Y8(v-+B}L@^vK-L5H>nZXFYD z9e6ivM{j8wulxYK6ZRo9 zfxZE>oC$!SBy16xQb{K~**C%4M^BfHKWa;)p^BL9u{sk-xUfznk#<3nLw^qrtsY4P zL}MwZ!-I$7#&6=sQNk`FBh45by1}OC%V3Hu1q@GPYQbcreOPAEs3%#;Xi2ZWdT6oP zXmQ*obJ=K}mnHLk!eT+rN)i08Bn$pFw0|4gf8X>kHCp0N3+7!9434rOidL8UGpC}X zHebqxxzi($be#341>0*%9=Jz=HI7Q>bB9{6Xg2bx0-@y^Qh9gA&XO-hXnWtj9STgBoVYRkyqg(e~ zpNG7b?Z-~<#bd+!x11~OUyu^>(D69e2mMGNJg#+Yck9EMkd^H5xYZ{D1>MT;1Rb|3 zpCu;qO%S%&E>zQv^Ty4&s;$VVUB8R>v;90B8E}Be)j?xuKegk z;Ii_Ajn%^<#XDQCk98jnE!hmdU%grPn)1Wi+Gh`fX3N7nF!UUHu;ck?H`~K%j>hu03Izqi$2vodpTlR>3!3|*(u`yH@ zn;}&B)|7k zxp|eQdTK2ts}cF_M8%fCBkw}DBXVmB{eT9Y{kYp2+yud6<4+0cev|zPImWFm6^>GhkA&1c`MHD(-F89KCP-kkUc zE_57c*naDD=y{j^rrPh6Z!y;>gzC#mC(bjLH4ndbjl_W)bz7HLL}msr-Ip4cbVe4F z${S7nLg_xR2$;&4I#W)uoDT~KUB!>XG*iW}4KKsFPSGgcrIHP)Oao%!vB}*3s7M@VS$f#Jw<=QOG8mg$P{IrP4HNQ!zm4x zEL3SkEz)`Lyb*z=v54fJ_p%ib(I~arwUGHlh+L>bxemew4t=&XE3tIaGAKbg4Gcr9 z26CpQ4nuXKrZz*^>&f|<&f>zX$}}6QE^KNaQ$GeNdf5Uvl0mlbP5S9=KwcTKt6B{I zl<%1yw|rWfpq+1od1dL)8}iZa8YU_Ep=cPKr|y{oF8>DL%Kz%0fubp};Qu>lzSjnF zjfD!bEN1_);(+QE!M{`-%;m6?{;)AWt@=l}t(Xl6i{n~xwD?!JEenyoh3VV^EXw2r z#kxYnZK9}raORTe3!TBZPmLgL4QB5Veh_~$^`cxK&U~ymIERG9A2u^7oHicJJ(ghm z7{B0(M9;QV>TEq;ZL32gGq9XA8E=2AoAVy~Sd9FG&+akYR)F3mX(himJJSS3^YNAj zT9}SMoG5p9-JaVbUX{>L2;E(u?u=vJzl%@*+;i}GW9t3G$R`Wc{aO2;kKs0oW>S;V z_6%qVw)MIwNHSz77{{`5D{`=1E6PK{g>f51n=g%JV#%Vt9mCR!LL?SB?J^i~+itxR zFW{*;ZC3(J-ANQhs!CM=fz<}dAgEQl$%=u}=45HS(7L&wglUPIkKr~bn$I}$|2Wq5 zKce}D@CN@x^Nl;SY}W1{x9z6;IkX*e?T5f2ll9bg9(O-=>bmF~KY4exAN#fYih1+v zdzhz%;vEQeWqqh+J?ip(=={~r{aDZv06>3n{UDJQV4Q_?YWv$T#T-{GYxA^tBWpd1 z!ReS0s~RlBwBk908zOoR;Ne^JZ<-KL!Bv{#RKazd=G5Sxn2}YHn1F`cc=%RS!+^Zg z98Gb=@J-b%S`#uuAQ@xGzD^4Li&T7 z+vlYy(q=XH9xNRnVc^XWd01E5PU~)R=WfdP?eBZ(PLu5i`C<8ShouZnai6AHn%-j7 z$Rxg89AC2cK8{YYZyZfLSHAt)VLvX*Cn76Iw?UJ|z>t3+? z_t6-^Oz=Om2zMYhKi8d9f7^l7fyH2GN5KQQ=Hw;|#_?$@VwI;tQ|_U5t;ZTuNx*=i=UL@m*W#ULI*rh&j=s0_i~~>fMW(j(!%CXOV(xAtm^C9J z|3E~?=%Yram!*b@)fQ%;r-?BsS%!QdY!&vVj15j5g?A|jB=SOee8HXaAzbp#zj=I& zGQW9zC|d_dhXagk@Gw?6%PX$wJ%~tWsr0(y08}3#(iKyJWW{AzoPx(drpuDiO_xQ}iIs}watOnG795iVeD;zU*Qzn_YauwvkU?YwnP^>SXFN2H ze)bXVVKZ9HL`(R+$u%urToIM5Q-B;Gs2s3HOpPokt9An5d>9ud+9s@D>PLncqbu)j zf-DOqafNh6nQ)pi{oN0q?8wa|FpA<08c1Jz55K^&bxffl+a!;u8qTYkaPWV*zJ8}Q zlWw|e;cO;-!b2pu5rXOFcCiOD7}=*zo%ejLh&gJgkRDHd?@WTYX1F^3r=>GcYf3)e8-GP zULfg|&d@i|%F?VBOo=>=-7DDasP_r%!PHV#a*H-g)(`VQ(!#tqoH&PXXSuYT-8CE@ zTsc9)j4<_uJEg`o(mHRtkLwk3NE{1_^ew9A-^N`AGww>qTNx@=Ehnrpe%hreD3&#p! zXxfXg(yJa^@!W5}buQd-2s-xFTU7^ow=RWI;epqB((8+`*X;ue=y}nDBuI$}K9b+G zn$-OyY}m#%VuFCR+|pHjurTN(H*8;(>k_?XA7bTB;EyvueLE!moOQ0ap*@j{(|w$d ziBpYaewBjQR(5D8S;)$eL}Iy;(1L`g*mpT^yWyd9@|Yzy@jjlzP6DFn*@zbByw8|O z-C_5kN^gBntIFY1!AWAIk-S6IoFkmRG?a648|_3Sma)EJbX z1{ii7{zmgjA3UP@E}CyFTF8UCJ_zG{D7~m*wF=tjeyark7K-NEsxn+t-|-%e=hu_a zvG}HTwa!VuEnDNv0`7(m_htLX1|L@8hsb9QyEnjy6_)a!Pi4G{*my;%SEV*URI7VCdK$=`knbh3XI=zpOS|Et?4 z8U(qhazzu2od034I#WV#L(o-)_19n(x_uJFOy}JclgB0PZ28w<)$p_XXVDx(BwjeY z^qOBD{!yT_tGq?R9dekj17t-6{ubyv!!odolHPk5rF~J@5|6zLQ-#C|%AoS3qGuNA zlz`vcCuR?barH3<9ZD9z2dhi)q*RS_^zu0ZWtuX#;!Dma8`F<{(AA<6ZR5>MwPk8L zOm3904`-%Le{7$i+MFb_r6cm$@+@B&P)#jD1^SC1*`Jaoq)Nz2oU$+6!_x|a0DOm= zVb#&vNF?&)^M)o>tSDQNWS)mxdTvtK6d`H`ItD&Z$}4{fbZUH4e{LxgNv}v+kZG*I ze&tRALo^{`1TD!DD4yiZdN)ay9(B_%47}9uFh-ZUoBC2~)YLB+1@?(pq-b$AUfC_~ zo~;&Cpnq>!R3S5Gp^_rCNHw7hq#KixAyzMj3iLaIHD{OfnF!+eX{HE^ALiw{1yF%* zSwc=5PT)$VzFYE3pxfvrQNLU<@Q!Ku1QqC-704lSPaMmk0$rl$A)A?mcN-Nd(8rRi zQ9@cOYyTgxvrm2r^xy34la4Q~pa1Gzu>1P%-?V*lkEH?KJ}DC+1b&>BWl(CKQ4D%? z2QV%3YAOyM3pyIW%MW^96PJs6PbsgK%zwFqIcuTv-|k@5Y53T4&HerJ>yOU;+pW;; z6W};Y&=mE}X6SEw5ZcCCpP6^H6Zo(8Als_DeLwe?JvdBL(eU|{_RAg|RsXUFpX-O& zg#O7MSV^388d0eJj5~zjISF%T&;;F{@r98M-uOkB*%&~zL%T0L~^W!dFJ?ZLP2 zH}*`Tx4+y0{LNiBX1@1!>i~NY+r?r5)EyK*{DiuLhanAc6MbMLs8L#7kT%>{~CTr2HB0IF>^VLc8Z_^b#51v}#+a z?GVM>$;M#vZOTD!$>0ZD?poJ)ic761A^K;Y8E=5KGU>+M3sO78!9W%VppAY%MO_X= z|BI>5_h%5Mon{UET2Fo)VFrrD!0bE+)E~{;?+b@q7cd_TAueyRT!XHkvS@=b|Din&L7k&3^|e4^hwQAfI;i?=~V(}|}s z3JCXVk;nnBAXMfa>)hmPuL)$3@~jZ%35 z5to(U;(q)q%3FCKA+^jN9ldt%3PM%vbI-y7uT9pBso z9d4F`ytlx= zkkH5BhKL#%E$nRs*KAkXNk|(oC=+=dKHr?+drns#2fi4e7?>64Np`&CPpAQ!k7s{i z<`8dkdLBb6v)VKn!1lUKe@AmeXZ0iBRcq3ZZF6(Ift?R*wNCu;Ms^y7+>!2`*K0@K zQ#qcimeMd*hM)bB`A1R`hV*aI4Ovq>H{tUyG7jE3IJ~^uM0XG#cYB59(ycSvXwXr&ABH0Du#`U@}`#NHHG2`{oG3x?39$+NibT3%YJ{a&rww!dA+*6)yqXY767UYEJO{t-$72RYe1 z*Yk$#c!f*&cm1HAx_+Yd^P)1SfZh4$`ow`Iq7O&pww22F+Ms7A`tJiyB*xa@nMF}( zy#f?6dcvxtj&DX(y3aDE?)QZXZoTjW&HH739O*yYElqV_y?r0>DMiO-Q)UHT3J&gX zPw=-V`2V>l_-{S?P!|D&GPO`L|HA-;N+Jvs`I)~!lF48=A@;LW(|r{h*?0gIGk5)X zVItLwtCA*K`7|Lg0G64CZY+&Y8l%`|z7j(&L>eHUH8)Zq#l!)NxS(T!K`Uy+kscBa zg3LxLOb;N?N|ubLaAn9xOGp=DrGQ1xcIB)#lb>4R)!;`rZ6>Fy#UV`SVD&_U&{Q^C zP;^ZG`*V*!e%`-*iUB4ezN5ML#}s3+7<`vBGkduAk10mB$#A;IZENjslcd1IrrYHI zJAG>MF7N>3`J+h!)AZwivfT+wF{bR-mV7;6^e&S?4sonow$@W)`!s10F+;#ssD zL(A!uiQl4W%0|MVQoS>OET?;_B3%C@k2x%y@s~;J@ECz0^5KXK$6kDflD?iK@^y-e zN>8U!Jr*?wFg=Y#sEqTyxjFy5v7!F6=NQI4YMRbrerO{Eg+8;;$5c!^R~(a>aVs3p zC?n79N(_ez22`yLUD5Z}H>iuS49C-hDWnpQ2SE#ljF3j@0>ibWiCxw>Gw?XC4tEm6 zr07z`Lw*2>%yKtO%k9UO`WLiYhQv?Q6Fm4 z67{zM+&M0E)@D^j7dEohKF(oif6vH!z&Sb#+V1%2Sd<_Dm1jnn=9f>AFgu^%lPNi$ zoKuvDpPIiX+_=V6m3X_+A_wsZc)jkH^_>-##bX zgh|~r{5rz$&Ma8feq3N>PjRFlMI2~3Jxv7r5U|xp*ykLHqDYD2bQ5Q>JB2R)xd_i= zyERWRA)>pj9~RGe0Nz>Lm&d@AtIf0ZHP9)>x>*k2qg5On{e`f7Bm`BuVNji{5SyZT z3#XmF>Ghr{3Wj0{rOI}zO}z_ex@@c>x)|{c1+BhzSWtCW)dWWU)7&V z!c$HS&QX~lo(ksE7c|TQ3F-{(zaGyP7Q=hNjV&-r_|GWYT-pzR;q+Q);ZV zw>0#uxrK|ym!a^JkyHl)CSzKS9G;m%$enr>MtseZG1A3k$5PVk&4!?wC;SY8i(@PJ zq?Ln1+{GD-lh1gnDuJy0X>z=B4a&2HBtOa>-xKqgrN%YYK0nY>^IL+aR*M{3BWDdt z>V#95Xc~gCaVCAOVz1cm#hshGq1bA$pNxvH-pr8fU5zo`LQ{oA(sF)4uGeCtQ% zFylV@g#2Px-wzg^JA%faLvPJu=7|uI6=PpPUe?{@ZO^%437*hj_dbwqxOl~gzA6Ru zqMn$%2Mldt5JKRY8LcAuquIUk4hPgl8)7sd+v8Ps1M@5maTb8!zq)Du-iZ1;K=wbf z6!_QEbOzH6iwSS@hrB`%{g>sQ|E0V_Bg_2f?%zjwCFe>LE9}lWbwIdr$*04)NS>HaGx3pr?3Rm!B9$> z{3aj~r(81Zq0`c2{a)^Q84`(g+hB&IN=#P3kdyxy5_yiD#%W3`pGv3J%(ETe*#?zY zbDu)WliWU9lAy{xF89P>ONpJQ7{B1^6T^M5my{8{g5E(F9A$s+Jw>soD^Pjg9gzJK zWikGqJWwnM{E=ZZ7=fTtjV2g{O3FBt82h8UJ26yVX~|;I##7=Ti#ar#Ad|pkT5W-7 z(?LdF^w2x#tI^?K@@hyR8^ra&dMDm*2DFnPe07tSAOcv&f z5`oGqo&Px@kqW0jd@yK2$EqPP>>STbasJb2oQE<8k5 z!V^66&WcLIPc(3W+@rb12+n8~2Fo~r7hx`3V?6-w--?N62NE}cD>27C_^6oZOWvH# z_%{IL2AXHXk#T5cz23LGTIukVxKlm7M_CtS#sQP_VPh-tW2r=FH z`a9Z-SZE}6IF2 zxvvO*RO-#1VXY+}>y+G2)|>lq=pKln&Fs0F`%>sxLhPlq4VZ165KX);`4~)O@f4p9 zgLbVIN3ATFbAK9I_7#}yu@k1GW70iQ0JE4~1mHmy(IkE}>}3mrC1_2MmoV%OW;~^E zYIQ^l;4sE?xfL_F)ryN~B=SOYCqg2`2>AdKdI( zaH=N-kZF~{2M?|=sm#77(4p(&AbJGFM7}UR`;e{s=<`+NSNSR6WfYvLGV7X?EN?m7 zi?w}sGYJ2nZzQTLmh=K?dh=b{fN4LG^5_ek?C3)`t`Mur>?#W5Q zgA&Cq?z=xD+Qnr=-^u`abj#(Y95kw9?7P-^l+=t$Usk;n^!&obRHOImB({)IxVv@h zwGn(Wxy)wM0+WeT4Jo%vm4ZJjP}D{+I9FMz=+7xuP%gO_^j^~$^CsUt3?JEfv(17I z2#1SYhJl(T3%LG+Ypd^>!2Ds1#qO?>v&Bu*;n#trBfT>%HS4qfABZjXUd#*l)sK9K zCfi9m-gQ$S_`D0eUq$vDAwZO3v^V2%ce(#vK+g&q1z@~+rHZmL76L@4t!u@RDGN>l zE`;M9w-IB(V83zcH!l58$0d{+MRc7uF8f1l0|0;YA~{Jcfn?N-g;|klyK>>FLW~6eYM0&rDXmQ&SLW|^ zm?k3MVQ*0~`gI^danl&uSFgy;V^+4lJyZQic)d=bRrut=P_2!e)!~p@o zB@`)8#%bx=fU{lA&pM*GhwK0;5Da#b-=-VvqP{ImC`zl>QQkA5*r5UCpf2yM$1UXj zNXiY{MRxUZl6fMLoE=Cgaw1b$m_(f*ZkUXtl0XER99z+^+SpYwKs<~dGhV64chGdS zHE^H*0*kk-m5JfS?7ILhIhurwDN^885HUOnq@`Kblp@dAKKCRV6JTlfc5S)BjWAbxL7 ze8phz#la_c&npCf)jvjW?TIEVSxc zTU#zp7l%2Mj)Qv~;{67OH8i%rA;se*_g`QO_(LN;x_JGVshuAmBj?%n;V-ty!so~Z znb)m<oD3&OREpMg2B}* z81(5D#@7|}K)ZrL1?KJ;Hf`tAGaF2fD;Sj5u%|}BU=#@o2GeM%{|1A3e}h5NAGRVJ z*tZ5fv=U`8mxlZd-rG`t!F(8+FUV6N60g+p=ro3bpUd&qt0-hDtMj5@;VlgcZEKrx zU)qzBV->Z2-k7keDPdr}%)JaUX7!?0QOU!bJk4&D61Mh~S<_`6>>)94r8q>Xur4cX zGj<+c!{7)qB5KcioO*g)YeC-shCUN-uA;0fxR>X#SWTXG(b-Ux?=sIr^dE>*0sblW91l7a=;+@8gseY#>j#zDBC*`6ph%U~BPB zJ~iRY(uqPC;p_FC^x+)tu`3u1zv)@5i>M~IRkaIWr#89s=CcPb1Kd%U#YpYZkX|nJ znNyEcioT0r?n6bLh$&2irxZ82g`ro4C+GT8BoAJIek5e6MzgJ!A8kG4RM9S}K7&?`&Y;Hy{ZqSN+`Y`URM-hKKr#o{bFRbUJ6?oz;H#>hxUOU;?7|LC^) zZqmF!z_j-IgG4(?_ZM!O1LnJE@m1<^2myiuqdg++yI-zgu2JIAw_W<%F8ytn{-&beRP_IIyY&AqSJW!#=SEc&b3S2)oHISDN2$``4Sb*~imXs0 znLkbTdP$)3a{)p$li=>Hb(C^8$v5#TSpXip!JySt=h2Qq$h2$ILx6xv1T&(Pvx9Q< zzi{E@Qo#GBlfAWXhq>bznz=_agCeAf&CNg0j_)!@>pr;x!zJ9*`77pgM99OXJ~G+@ zp8249lziz~FEYcyA!uqvi{W$)Shr9_2z1H^Gco$pqnIz=KT_2xr`k%z6}g~8^i30+lKb%sx zX6o%~forF<2NFUs*vlGEH`vFCa!PshIw}VKa7xk4yCvqY1v1YE(0%}+$S)35WEs@x zWmXv9@4ea@O>TYi-M;%SNA$=6(7f^MNYD2e{@4$KzE?rVEWZ zt1o;FU+h)*B|DR^IWV?K<3nao^YgDU!ZMGi334}&=HJlTAp3^3vm_TQsAMTNvuQJr zzQDJK8o#&PpRyjMEh?I21a^l50m;mW(*WlyDz}t&hR=%5 zoa3}umn>K@ghTKkX2qF_V62;rHxk%kQqw@%!?G$09kc~}3mn(Ts(2tDw*1sQS&uJi zsis0Sztmjk+v7Hx8IYsWU7FEEp-_sZxL9A_TjaTI@44A`qVc_KNT1h;kVb<%LC9QY z1?%o4LLt{Zj6BMDRaH1}Fx1*Kk6qNdTXv?JBUaWMIy9eHM5hTX$4G~&0QZ@H> zu@@C#6sk2w8KidOJ9jz#4a+go_5h5>VbCRNDd(32FVbw;#J@g*v*|Cq;Po`$Mi^dd zc@f#9Zf&F?1JVfu`;%Vo%~B(;AOSSAgkgW|K=mTL!cha`Me8z?ym$sYOUF99*qIZD zt53_PU?Zmmb9F(YUl%a7m&<>XnG{=6#~E)Thv#nhcRcRUz3SxLvtTY!Tkc+4P(=p& zO)jKYLj(H-dA2}4mRLCV7+*9zgZ$2S-6swsAucB6l=GR{;jrQLes_iUP#_Ib5%n7*rFjs z!CZk#xlr6>&0%$U*s_3zkJzfHOHs#&gDBL=k4Nbp5U98ad!dyKXXU89t z(Kvag`If#ZLu23#L_1efX8Sb9u*Hk*AHwJU^=8Z0@|0?C!dC*k-JcJ0w9WSL$^uue z@DFFa#h3SE%@zJ(-K=W^1(b!Bhb?GwAGcHe1^@mKCCC8OS0AX?09x=V1_H%wX!|U= z`8w?pKq|4Tk86%XG!CCNvUg{xJJPmc%1N&P(GzW(L(MhLj;9cd{{|$&i3!VP{X4W!w7qaQWj)Tw|IqZJ>DQ~+{bLDahImlk%-A%5C7HG(9|=R8s)&R2Vo;%IMk|{p`V<;2#^j+u6&Vt@_BcU#>X zRjr!^!RUWFqpmgFs%MBxt+9T>S^fP1D)4rGagA{h*LuvBBJ^vF zYbOvU80-Ma(xFmsR~Sd77gFB+x4>H+F!lT+DYmSz2eiKo3H^m}g5yYy4kCr5y^d>) zV@h(_9eI*XfZI{onz+xaA^vl#>1gRiO!Lpv_4_@tPXz@>O|k7o#?-|5YWQ}?cn&6- z^`((BMi{Q0lV&|;Uj6t2d^HVr@1A%!U-8}C_Kb_l-X5;GFXz#stG}7glxEUq-DlmS zP4Sk%YmAd>(in;KX--)ho{c;DqVq+)ddAtpEmnw37Q<^%{t5q+JBWC``_oVM;vRob zseD`cseG8MQtW~t zHht5<8kL<$=MRQ=KHv1lnWH(MVh~+??_=FNTMsYXmepT>V)Pl}(BS`k34n*l{Q0=H z!Djik!vJ9V(}hY~LCJ0Rz3=8TL}{(SaDk_gP{e+dUn1(eY+5EogEo1^Bmh=j| zW_H(_0_hjhv5#8?Dtr}H)sQyD1bJ*ReS=}V+33BUQ%AfXZ#pP@Qs;} zF&Fz)ZU~6`Y;-rETcVp+#_|%&t41e_Jf>SDxVWFRe-A+^Ss=|5G3@tNCq@##s0-(w zOsI)I^L!p=k0o`7%*7rU^k%9n#iQPqGvP6nrjx=@zD}HnzF(fkS5Xdop=SyE55)D0 zwa5~6?pcP=EEBv^kXus^j83+hfE2HM#vRTwPt2Fe$C9D#Q~p?-l!ZQ+)?L;{@FA-e zp+&e(Duoz*h4&#BF2b!@O>^gM0e_*@9GCv!j80B~SAnq-x7cBHoI}yGVvmRSl#jUY zq={A78Ryy4j^mAW2kYB(?@*ayywFYAs(!P8e{W^dzIw<&^TFp_Ea8*}#s01Rw5hc2 zmEJyMrKNI<#1|%8yTA!6cm0VfE_xoevV_>D?++_a$3D9Jc=*oy#JXy`vsBF(FK)Qs zpss^&t7%5X%;Y==p?SxROeZCz>SVHdVUT58k9Qfg(Re-# z3_8*e;OMr#3)mZ&|9P5fv|nB1k1b^h_%A9*{7px{>F9UJ?RUuScgXE`$nAH??RUuS zcgXGk(U9A3Nc;_n|EcLW6kd^E+qo-$vS0`fF{LRro0WVxke)gALD#b^C;=Py`B;$! z7>X~79+?BwZ4m>D(g=8DFx zl|^PQXRwD8t=OJxIL@CtnZC{#HCBSDys0XjL=)MlMeJI1Y(nsI=x- zG1tx&K{DsB($%HBCJBu@MBj-vK%^)~5nDW>u4GZ;vSwo1mAA*0FJ)2Z7VXr3_bDV~ z$GcwPw2%pAcie93n1`HfBx53-@RdJ;eXe~td>Hp+N%cz?G1h;&CHAuIe)es$i*Gy7_$xTrAD3<6T-Wpspi% zru9P6B$+xAME4{r`;|~QIa+UPub$xbnt*gk#GWPJvq}45FO;1X#g4^lAZ@USlnzl7 z4I59px@%5osHA)DM1eFZ|2OU{_N;`b^h!rMZuGqpk5gru-nYz+N z&55^|xqeO)?+Cz|ZhtOA3aoE`UyOQ8Y%%`mT8GaQiCKrgHET+^UWQcpbui@q(57Jz z&g$^>GNgv;kr5p{My-to)G{Q`uSBkYdz2hYwC1iR1B)Rkf$e$>T2Y!QfuSF(Q>bM~ zSsdH#ae%qYO(;%c3){y=)YazqXAw<2AC>A9W881peMWVyw>VE#m{BTGw4+&PJjG!z zS_t-+R-v-S!4*JnZr6Z8$7^1T71w!K~q3{xWz zLzKu{{bRC={_cf64#X}{6*U>)&R9x%B zcFaai2HJ)bLYVReb~Irmx>bun1c4@MGI(p-`3kXWjG7G8616_IU4H-mADs;TtGd?T zqnOVuFhrRxp^>Y9x*67W$QyYFzHZOD#lw`COz=Uy+w0?w`N3MeXgbzTK&OK_OeD1 zmT2d4?|&w~&ahH7JtiAVVmc>9ck6b;?nJP;3I$frQ#? z&mDb_c?3EZju&P*+dzWMZ!XqxyuB#`-zIt_(u&PE_$BqJo7C43+5le$Ze9|$^^v1| z*usERlE$a09>Ur=?@voVkLp7O0{Bt$(8(I{-hki+t#IsFcjtW#=>GUq$z|lS4jOp$ zs3jFUSt9eGtVgW2EAGjBr4m26gv{>AQqbB>#Xo`odBX7tf`JGod zpOzZz=(ZkXO_fq_Pqf;)0gmEe- z#E1q;4s*GW$V!Mp1R!RA2Z z^UWNK7*&x_N(u(C=Q_&a2SC9&$!3ct#wFAKcItn%n3L$+>W>Vmzo8xmNp|phGZ)4(0+Odv+BzxuT$Q^NlSKB}> zfn5wTmRNV)nX%JEFxaHNY4JR($f0yyy`9Z9QL&|nmQ%I{HJxf@@AmND$YY^MkQ}vx zKJ#gDUd2lPDph%Uz!r$lAWUAT{6ktW{2R-vLa{`nRDV*TFa2kwB#-{)i-QX8A24=K z@u`}2(6iflRyX6neRy}u@0-SBMsyUdhHRK*RZnAB&G`{qbJ-wT_`>QsoFU8Hzd zykfeTEN6^pKCEN$m?Lt!+1wFq5WGI&s`%shPA{H7U8b-Zg>dxjRxoC1qD{EfAXS?t z_heFI>H4M@N7JI~kmrtv3yxK5&?9Y_*O!k&-(J3LCgjyC}cEC za&+_lGB;ud?`1c+^z7aFZkj=L2Cuqw|NAbnXW*^%v#w5_w{ALu#tS`pBKMyg&owfi zv~GsYehCmK1Qz-4`0Vrc%aW@}crX0+`4a8rb=UfH#&hxc zjg#Vb)qg!Ad<8~Rs3kPlaQWk(gAptQM5PwHw8PXHOh#qUSyoHnCIM-M zn9bds5E{8Wyh$8ZnB53xcv@=d*bT#x9lvPulBgh9D%@$jK*K;y9q)EYRFP&f)D=@) zZN`1&RvD$mQjNb#LiT)X$VaY9R`DK5p4>@uNlc%vuYf$*t1UsuKX5#QSI0wheLVkk zM-KpjP*BBVQMNiRLnwxk^AI(q!I>c}I4vT~y(COZiXKZ1wGi`%bPq$Xo zX?|FTX8QvUz!%NKj4+MAFUGz)x3;TuWB3=&t+mXmEdoxi7CSb=q!>&_VPNY^Vxt^N zi{?egX-Cu+U`rZK=LCvlQV5~R)~`>nQw+6$3}PB5vB|SI+7>JukJ`6q-lUoo3FXvE zFZ8$&f%<}qv@?_(Qk=$>qmu>nQWqv9^_>i$CI#PJ^soj8ISrC@OQN8guw*@~DCx+m zjoC_Xpcp!nf~A;?G8l zp^-wSWk36qOI4Lw`?-BEX_)D}-X&Cpq^eq-rLw3&%|9s`kJ*iU;*@@%a8EI7$PQ|@ zQC4cL>?+S?b|E1kp7$8{JCcEff0|Y)@?;MrOZqTUviW?eTj5h`%h=bSa0)P5AW1J; z?}){*5cxmi$y8k(F6|$O`_G=)tHY(dI$UkYuV*$=$mZ&qC4OwXCTQE4K&~FHm_D|o zmJG3YRx*X7b2Shm&(T=fh7ig+4a+(cMMYO5u_}6)M1eSnC^VTu zwCei@{#ASR7aEBX5JBkxlFL+hbaV{#>i*zO>rup zqwk`(_AYh*JcnUvD7$|OjTfcWIiI$)6-nYrt*2w-|J(pD9pGELjhDd_EShR&Jf^he z65R~;VLLv@n+#H_q{*qIgT<>4;>!rUKZw_Pktu2$dJ9f%MQ5dxY=!wy$io)XF6Gw5 zf|5>Jc!oTgy#AaencXp*M42;9M~Q;ZkTD9tTaF1J_gA$34`tMTucY|BlH&i>l@zG= z6UFuWX$yd+2)=qh!2md9>>|B_(d`k0gfjZn>ngmyw7Sr8G%l^_8YrtZ0{!GbePZ!JrB)&gLu;9?%DvSR;&iZ80G|d4&K(1t} zSf0yHkGS)(aUKba0yV#Jj>l4OJpC;Os`Sidy`UkA|1pB$-_L*fkMF}D=MPH!|2lu= z$eCm(n^j^6mj{;U4E2g`I@Cj-{~HNMf3lo9|87}vz77k_&=#FLqyU+k4(Np}k48RA zk%I#c>M)8K(+f=KYNANHa01oMqwPzBKk(3S+1sZi;8_-ZP&R&o{C4^rNfr7KIa&Hx zbRCp_k%RcbBc4rCb?@xN>60OTdpWMaoah=Qe|tSHuFbC(ycMI2R!lA86VGkN!;zB7 zhx0MYFua!%yEZC`@0BjtqeeE|mH_{O%|ZXUr(S$@>PCNt4{-iH>l|(RtBlInG5l-}x5di%!7J|8W5k7C8CktG3bQAwa*RXo8;`aZI;bxB zWF9wma`vUBdcF0{A(vd)GB%-pPuk)eoDzwW7%wjYYKosd!1)gJw>ETayLjRgJU-i= z&dP@epcyzWhEdx~q{3**MncF9HGYHwX<^#HtBH64h9v>F48r2r=*Gn3H{S`wf^4jU z9{1Bv!6`W-Ezz*S`!Fk|LDvx&oKhlCj+nSyhe$d!4LgwBFB#)DJxH|RwxF#<(c-Th zDJ!o2GC8kTTL_7MWEmQz`~#SL6tO!Z5BHWloFfWkyOW`SCGVB?iVi*%h)c%&sOS~3 zOIZ*W&y;tVf0v?x3FZ5Zy*>p)x^`mOBTHuXjO*|L4W4Zq5JU9Ry`4e@)*6ZvrI`?n zQTf>t4M)AKaGZ)67T9Jj^}0tplX$tVlRVhYeOd9=jn;N&jXZZWEZsG#yWSaU`eE$n zvk#^z{9{dCKQFnK7~Q(bv7}g49IS(+-O(R^Vo#?AsAKZTV>}nrFH&fJ@MfOWgLH#M z@nRQYL2gjg%K=b<=)DF}((B$91NkTPi@r6Y+(t=^6$Th2Q5^%44Bi67CtGKVE$z^} zbs*zf+D1^r_eLWMuC*sRLx2rGbcMV63T*!QeLsf8*u|?tLCR%~yOX9qn-A4NE{&LA zWZjrw$cQ*&jF%-S5at+9G}<=-RPR_dQE(5r)TXA>!naN!d9Wi)1U7kEdb|`X z-b{i?fa77EN9-OW*6jQ#cJDKRSwj0e@(a^iI8CVK={Kg00k=-)1J_W{U5L#vvx)j> zkJNPN`terN(bgF-fJ`SRL&|h0gk0biO!*Uuc-|Y}BjIF*o*}CD+io%z6HP-ihXjDa zm%6f}6bVh^1QX4L?=DWcFz(On1X0R5%;==O$J|pj+-U7ekxUhx$0d8EY{UU(lfwmQ zz<@IMvPF3j@{ARbxL(zQn}^0b!PMvR<$3VZAe??*yta9)lKeCm*Ee?wveTju9|FbE z5Z08OiF=RX5&+gUJP9}%R;W8%`-2UgR~n__WrkRz9vZ-G8$!Vzn%2gC<406;5a3-3 zh?&w+P6(nAi38jPz;y8Rxuybv4XT3QC8t7c>CwW8sz9?|i+y+tbqlhQGghH=4Rn8ZdTgkW@5CPJ=< zQ8DeYrd2_-D~>|8X~F)TI@l~XFx_7=Ym!GI@I6IxgM*Oy{61icAXU78GSWfEOojI? zFaqOVmq@L9)FfPpVJNsmMYx@{b4s<6kVFy(?@`~N+haXW44MMxv}rP~g*Mt#&TwNo zYoH!797KFpAocaua9SuCuNWPNnkwiLeZsx<)lbE^?}872vug!Ro6FeJyrVB4-`LLv ziS|inB@RE$QZ(FogS(%mn!vLo$FS$5O2OPl82&&chf}jyjRmTMp4FMgmLN~-l`QYl z?U^qzSgbXyC#sex%!T%;SUW;(B8TRnNLBN-ZfgHT!Ht7r?YP(ag=!yOn;g708hmX~ z+yCM1n@`OWa5GPo*O9$YFEeO^zC-2H$$Wd4ujCf7GZM8;qwVh<`WjsOf8d7q6on{(g=) z)lN0gZ*e`w^n9tgg}Ph7GCR;A_^73sOn?vJ7QACvSylO_S;aq8ub7!2K|x==5v?d)^Q*Z@7>!DAUq$!lZ} z^3C9~XT-woe%f~ISDhr2vBH}24CBr3=G)kl>vr2YFQhC~wk2C?#j1;co)g#sPV<_d z)r8fQEF6p(BjW-@D%j$j?KnDydAEe#ZRYfpExg0^(Zi`RmUwh*n+Mb zOUr}EmwGRUSfzIyQQtX`i7!kCA?{tR@7I}K0#{0a6^z=R#|<3}BBf}uy?w@schD?Y zKP~O02#pBK+=tCKuDcHtjM1NObADX}U~32B$}9vCZ@k}Tpbm@v5U@bb;Wz7XTj~z{ z<-WD^8K!T1(=v-dbg-d+ci1Z*BK<&#t|u}AvDl48N67iYRCLOs+tS~k%K2jO)J^vZ zcEeqi8HYnCM5R!kC30zSbK$7B?$gi|m=3>9~7 zAAiw9%Td7)5udSga|o?QiKyCweN9&0yU<>1j{iQEQ6$} z@9(Y#r+s`1Rkbqg4MD>0iLZsZ*O6*Tggc7gmyZlrDhij1B=kEbQ0-MvqmBqZ22u1z zXd8v=rCRrwMYcuw`(M2bWsCKG_fP)}d#fL&O>NKj{hGJ-%$rlBue-RujWfh4mdh^%0r82T`=Dch z0tJ`ZOF>HyCH3fmq7ZyWt)^qkAWdF(qh_m+a0+Fbk~M#MOA1Qev^+uUrt5i2{gT30 zsUNmU1yi)JQVPW&)6+EqFs&$)|3HPkhb>sgIcROm%%6ZVwUzK_Eg@Yb>ib_&y$sZ! zVI~V8c^uAA5CSwt;s^-_j-9Y--U3N98@pEYOevgW~BCA zVqJZ?P9S&(kQ)n$F8YGHx0k1jZ;n`iX<|CEJ`sWj2IJ_e>3m7LzPHE8hl7TNdB>PF zcCZ>pF47|LI_&*zgcKmAP5;1vAsv+IjS73KE0fG)_484E$uQ!^PXvH7`KrnrhQ1jM{NBJ4n?$VbN5O4`9(h+f|h8h*=a5it~K3y;C zq*OO3vjn3e9r9r%dV~KBk&fDp-2uTK-aVwqN0+@piLWJlLsCZ@d&4knzWotJD%br{ zRgTjAF%9v}{c&xTO;2lmLrwUEQHayQq`t$ZXcBrcqIJqP)%9@Nu~4%HZ`FVEFixk) zy`8sa!nJm;3Mr4z=QDqqH85Xu{pY-h-28;MOj~x~VCw6f&qDB5`Qyb%mVky`Mv;s2lVhd(^#@b6 z_wnZge74^UED+mV9uKrcUtG)=%laQ|=F*;jXK!gc2@Z&;od21V-9+XqhjSi-@Hjsr zIJDaKr)IT0B8r4{aJf@=Q)7ujYYA3w?S!^xTLOS-a$C4tPYx_8=IWMdJZB%D*k=*#mKa79vT*KAB>rZiL*!k zM)jN+FLaym)$+n#GZsuN!1_DN4Q@ODcGakf1s=fqPz9;HsCTiNE0Xr#kId)5@Y zYLX)!XYsfqBuZ~VfZQEmb5WkszmT=#of^wj(&{&+QE1`^Cgj4?OX*GniZkg9Ebvt( z2YNx6KJ;A|25H>I8h%h$$trSoYqUhWGDAShhJmPc z^x5O&+_+Y=?X5ly7%FC-=BQ+N<9SRCSvYQ{5|^crp3zit0iy(rDBSAyl9Ld)dx}gw zBKrAS0zZvjo;BwO$Jn^7{p?N!dhI(AYB?s)xhrxN*9vr6xQF1yJ&xb?{Czt5J+xjw z_4{(?T=M5YHq9XJ}@zZq4Zjjs#&dbP9=qaPIN94x6(gx+)!H zdAsl66lw;*v$l2`1D|HHm&SXqr@GKZi%w%o9G@&1sHHtC)A+e+CF}Pc=V57hH)97~N=jWB9fUIwpwDM*%hjsj1e5k~^sxoON>(A1!!8dP=wNZxp zZSp**r6rx^Po)XVe{=uofbn7m5iK$;+opP8?$Fy7mqC!vGk*Hh8hc7Z!5RN!J4e%_ z)7nOExPY&cq?(8MR`V(wy%=R|oC8v@@iJ~lMXhlGZJYvLhwmVf-9OX#`1oy}FxJv3 z&myS}W-Y$fwrA@-DLU)}PQ`6eAL@Hu z<%w+Q7N{PHD17KKk?Jxt22s5C9x(2tVVPe6vvwq{f&tgTPRjbk&AaN0K=7%NK%=p= zwK=3POL3Qb_*IDURWu63_^|rw3@jD`RKD3T3J<8tdl&8~ zz;ik#hW3y3i2X7gX|6xDmrB=yJ`T!oJbstm7NJ9_7CQ0|7>-6^y%;1!&;^K|!xBw`hV`LD?80dW7>f;b;lzkXD z3b$wt#yt|u+-V=CB@B~#Mvzf4OPH#uE@_>&Xu~Dpo(T~)7I6wC7Rjx7pQ=L6qD+1C zmVh?fn4Rn1O@24`NAX-bOvlN)nDWU_Ul&t}o4QH$KWD%w=3Fs-33DIYV*`iJEx$l#XZpSP8OaehvAC$9_*OP8)Z^vMjL2Lh)DY{mE#q|CV(nstJW7Km;S!6eyq-7%M6GuuH6 zy9S*QupLD*-(Mj zaw2A!LSF(oc&`fzrW2mhhDvJ3zzo2^N2}6nD;X{|>A7LR1gNNBB4_l9eo znLp!BkO0w~rte~eO0xk2HXR}mm(h0zZ8hVGpc=GamH)A^u?cIx5P-7-xmMx~j6r{z zx(nj1>LB%9ud@+F3ch0m^eOaVGCtnwv=NO#&qNTxb`WpO&-d>9%xO}1gRw)fKXv!c z`-5p80gAOZvX%Y{|FH-8_x$^N{{1&!y7?zR8pS_3d52|@8ur_W6*D*6UQ z{O!8y(><#iIGS+KK&R7=t;wFR_)8ui3{G~$66S_`O`FYFC?yTKp6*%=z6hfg?%q!~ zn>I`O)IC=L{XQ|0i+x#0^ikM+^Qp17>^D=<^~|Nh+fRRL7;muk9x$63&p_|b?*PSL z`t>#0Pnt~w!;&8lk|-x>F5+^&66#~s`~0wS<$1DG zsAnPAt@2E9_eewyh~zV|`$RNyl>5*m&wsR`J!(A~;BC=}$WO85OGSAiAU$(d1V7i< z`xd$G0hex%{K$?Jz>&v8=#dQif#B z!6BzNn!6u$>&r9;KaJA+?9clQ*HwQuSC~7RpAxxRUx;eU-qDDuXBzv#`8rN`oac@b!A$c)a+5*oIHIm2~XjLz^Vz zR@0ZfneEn|WPG*ePMNdOkt!J%%|Y4ACUpWRb>ZyS75Ma41BXGt_geMTgQ4!@F+El0 zhpd<9bdM0hw@H|6L&?vMhA%HK@Ga%o6I;+%iT_C)vI@|Q=6qez_A3qc>mjBr$pLm! zgz7LWlN^VU(Fg0U`f_8ynB}6#D59l&SFl2pKv3!voUt#e!J1-%3uvQXl8jCRJ;>9sYlo{ew^3*Q^XdD z^`1|nbwt6DTx?~HWJ$^Z@705y@6=7j0@UfFucZqvO-b_AC>Cw45?153<$KHZ?P(&eD+&%fB5X^FFh6VJ3}bW%vxbCiY|c` zPndL=+A$!i<(=g0b}-`rhOT7*zKBplJCM4FI_%CPCTeokJC5zce4s-it1xYn+LUn3 z32$92DM2Rmdn5^wQ-qYub&BNbX;XoL3eq18?ioZb1me=u2Wf=KFHn)Hl1wJW$yww* zh~t6XDR>c1w}su7&K7ou%|tm+L3t6BCFaP6TDo-mmYScIAQGx#gG?Tn29K>T&sv$ z<}n=Pci}_&MkSdh?0JO=LJnujo;EnARx6nYz$VxF=9f3#R>P{qxMy3b=XZY0xcav43vK3_g#Xcr>HHJt008Gh$Se`7YRX3#dbO81sJB!znI%+Uf} zy`Riqn!F#A`*?!1sv{HV9*-3SG1D4ZPoZ2c8^Z7p-JSt}HN@FRpFg)`#`ry)7#L2*E;Wb!M=Fj9Px z@AuJ{L!@Yg;oOSBw76aq(;FPkq!~L9T}OpIoUw2y0nU^D8`%U7oQZ52+k&aM|0l0H zUj3o^x^x*etQY>$#TQzh{>!Dyy>MJSQc8ZQ5rBNOT&N~${%#BICy?SVum$P$Q;~oK zduL(7{fTUviXuia}j&_#CkizlxxRnQF=xE|#q}&NCe4`)D_x zl;QGs*#Q{Oooi7`Sy1}O^dgWz&!`O0`f=OhZ4fqKuV+=q*Lh_U{D%FK|+z1Qc+Pp z2X+0{j^o?w-TU3|yN~Z%f6rgfGuLxp*L_}RFveW8VtXX@(~ay}0PV<6>S!xCi@urD4$f>9ZSHk!#&r)Ye{M;>A!tJVDho|Xq$!B5PR zm%}RjfZMr}!ZZ_K?Lj3gyEvw=M`ewc13b(;!AH}12x_{eGz`9nGNv;EDUs` z74ejkvT~b+pnWx5t$pm_X5E*qd@2-giB*CiNJ5Dib9Rir$8e2t04J;@ z^FZ8t>b{%d59Z(_ir}ZG9PES9d_M1y98WIpjz8-jqR>|nio&xkjX4H7CKC<3c=9=F z>m#&wWS$V|xX6%WsB-dr_Wq}_7dNt}4%Sc37Vlo#0VM$P#Ar4r9-WDHg6W*FvC}T2 zx-15M&l!MFC0@q1gZSo^> z;+!$%D`P1rZ7rH+$L=X=_&7CIomH{6lJTKR5h0JF3hnqBJFUESS~jYbJ)$X+THZ0E z5M0Kod}~}M_XJU?UdC-WHg3>$lG%`0#_Mov!esd*s{>8E8=Elydh)OjTrL=M3uR4n znvGO1hdmra*-M`0OeL0!RNR_$HaX2*LY0fPqltH?d28Sb@zGn)J#$aZdWFQ+b-7bulAs-14{CCm|Rw%3;J4_K?cHkr(7E@(Zt?L{!RA~{d>EOYm67;Vy6 zK_9_9cfBv6&ufP{v6c?PyIvXuE8B3Nu0g>O!AiLWJ3Zg-qcR=8%x=@;thTDkj|X?> zUBEVlAEAihdb>?EON>dXr^=IcUXN?ud@o<@O{O>uB~V{KpHAA|bMtYxiG=S3nwphLQgtb92_$8p z-Z}6_Tn?Fc)MUTkr=2XorQ#A_T2uCJKPeO9Te730 z>DyMVb zTY&FZkCzW|67}KRWUm8yLVaQ7^b36<5~DAZ+jZa?&KGdMrQQ{+&dlJ@5R1Tt@s5wy z5|7@}RrZCSX!`4I)9+uvKhC96N9z+?Cg^%GS2z2AD!Wc-(3C6=9XZMwP(FlA_K4EZ zUEcUmWwHE0o0^4+L0py$RTye2dw=bPIFDJHsdf6dpjGZd>^l|fuc`(x$x=!D_`lvy zwD2egxrUQ2h z_RLdiR#Nhvo^47W1Y7WkW)-d4`Y#VXydA7+5_sP39x^$z<-a^jLpa@=dA7^fvg`La zz5W^l?HuxC11fO8XLj{d`}-%a-aP-dWfA#t4$!k9b?yF0mgvd7q#MHL3Ci#9KVVfm z>bv|<)gK_s^XWA8bN8J$pdeR4o8GR=o(l=g;GMx6V^K;E-njJs*tXfcd{Gz_J{WXr z7z$dyf2kZYH|lR!0seB`WBZ{O{bz#_LH%QgP-=0~+RqPORoKI$&3nsCn6pjZMcL0C zg^aBELOa6At^Ah+33u6Sm{j!j${e)y%@x`#D=2~nLv47^BZx>t5TS%)S`lKL`cs(^ zk{x<-lzKf!A>4S8I!jD)xP+|Ywzpd&CLt|OKi6R4ugP>A`|AO;w#V`hb|BO4oS6dUUw8!xUC+7TX( zdmd|t3HXme`rn9*?!QE2!u4B1J3L8b5i#1>A?u1Ug$Rj)aq%D(66>L8LZ(;ulbQ!{8y)iQD48sc~9}IS+TI4BICQH|m;h^=H zI&Y)6pUsRP3<5U}o?T|5pa} zi<}1L!>nE%22d?`QW%3dS+S`Q zwgn2=?!Z!6dGn(%z2$p`8&M2qq+?Q(5FnQdG2FkT%&FkA#mSUbWGW&`y)gVXUt?e- zkozlgpg2KrJfKJxeC9E1&urEg25cNxCQrQ@kf;-LH^jZq-sdVS(}3Rn9FSNT*uU5L z>;s{$=XrMlKL^#vv+@o3c!;+98-&Qh;RX25iAb|#HEi_fL?o#4QkHPdG904g9X6PH zjh=&qoHO<3fK2QN2#4sU6;a*Xn3IdvC&?LGkyG5Z))Y>z?yi+1bNwr1kVMWPI~~GAc|%fohSQ@@4)2c3r0631nNy~*HX%gx z=9$reLM8!=MrzW{w6i|^)j(s)YbR@>S`=v>*Gk&jVHO9U)(CG+;~=qHwc+0K1e)iY za(vULR${pS*oL|XL;3GMrc_JxJqbcbU1kLdvk;$Vy3!>9=*Ya!J&HzCuAy&5n=HzJ z+txc+(b|^eraTk*5M(0@GLK0)>kRn|!-PW77-pu9$V_cqvy)sJtA<$q48CJ=5q3h> zgmSf&T)F0;{7Pp-(UrhxL!#_TV1xkDe%dmsGBB@W9TXl{3vNduzpJ)$MGnR?fgvve zQFsJIf@hZ0Q5q6~kqDivfeN~(k_5WD1n_8q^&9|Mjc@1rQ3Ninn4^)9$T&s&uuYdE4Gy?Q&v~YFC2ZdAhh2O z7@ju%;F^&ytA>YBWWPS~TDU?5XJ?!wSBLVg3BKdWMoKJ@q+W9%{ zl$b`Geivq04>=quQEx1mX;Ic2iu9+Or5;1ab)fi1OxVLs-za2a9S5)WppwtdLr$DFEGpI=2?f-Y)I z2os6C#G$fbXd#f`+ouTu0&(zkWjUjB)ST?`i9!mSw4;5Eu5LDC6bZyoz0pkpPtL>2 z@QCS!36?%7{&nzmRkzk{%C~M^ZkAG7w03i1{Nr4Uujj~kUSxXymjvGEnwR=h^8kO6 z@JL`79x2?IT4{|+)0<2+XR`_nh{lJCsUsUp`oPjXu1jm$ETL#5pqaA)pFh)19W>W5 zU80eyp|~`+^p@hLXF?+5Q=Q6uRW53WnU;Wzn9MXbD$SiU;k2np#ff(;t*M(j<@Ca5 z$WGz-tD5KjUGv_h#^k(!)Z4e7Ct1JCRE24q?RG1{>ldH+4#P0CmBt|u$;>7$y_UHo^cmk2!oNAQ?F`WaO%#Hv_ML z?4vQ|$G&^>tP9Qn(>_^e#g?`^GMiJ4L$2WOpP9oA&yJJtWje*bv z=*AEN(EmQDTi^|aa^YQMyOQOpR`MM3*jG?lAJ0O*1o=oKi?T~Mr5N2#WXx~|r4VRy z*u_pQYsg>8g?cZWD`xZtuW@)22LQ%xIFdM)PSuz?s+vUVG|QgOrdQyGj##7tB6kQrbbY(z?II1_TXlR|oJ?k})~XR)3F6t28h! zZ8!l{sl7BKNz2{!EZ*Q4 zc%s?Zs_lb^Uv52jrxF+%Zpg+YD5u;A9Egy5uL%p29Xjg4ql&px5I5pXhWbl@ z$2*dnA)f_rJ+2J&v8s!sc%k86`{M~Ei0I>YwVoGW&T4ijvlVqs=M#B7$Jz-*?!PQW z@qzk0GVrA_j+D3;J9OZon5t}Ps>6)z5QCSXa{$S$R7|t%_2}6#JlU+HG10j^8;J;PoJWFZVN~CrMKG{y zO^P$7^YD$O=P3(yObI|L3z^+BGyPU?WP7OC<^sD~GI@*w#*7M8LK*RcC6BAG>Q^^f z2}{)~Q!Tw~_z@o;UEzp)g+6sAB&k5LME?P%0w|+tXG0B_0s(jZIWvytIq(`cQ)Cc) zoe2j)U9F2fwPdO(n@{fHkn2iVrRx~{d!xJ^5>3I}5V#dP_|lp>`l5+>DB$D`uWL`-tZQmUF& z#S}`p=-1gF$g?y)5EZ*t!ZzSR3)_M(S-sfdnaubL69(mDHOV)~7r?4o7D6zmrxfZm zW#y9*-)?B^h-B{0B;mBr2e7mj5BxmTfh-Eq_No9l`{CdmHq}6+Ara}*m@2-J1k6o_ zA>94=@^0<=vzG~RMH;ytT=py_i>^OMf%;DF&GSZ`FnP{omO+lK0XF%>po<;Bv!Q!- zs&_O&Czyam-1bakY1%SPTB5sK9mmgww=F2$ANbD8o;|v~kEIV)o0_X1j*G`}I zpDhytp!)ekJOJZ}_E*vpkcc(Ap=cnAh@Rh;nsi+;go0cX<*#MV$(OfkrAS>?Iu`yC zi8nUmQYx1rsGS}skDW6SE~rL>datU@5iek466t?rJD5aANTy-p;RG+Z5w-yVF}|XJ z8M~&VpkMZ zIsro%Dp)pGMe_w8^1qTdX3BMsQ7%jH--J{EOi!@>MMy;jj+y*lbl$(^jr~ox^e0dD zk8bJ8iq;}V=Mr?KTN(sLG5@0Tno%eesCPEKn-Ipbxw4{tQK;}+xAfbJ_9}0zK%@K0 zigs^x^0!aKHO;3TS0NSXys^{wQzdFwd9r&~R@O?Y&qwIIv0ikZ z?CIWbpNOk<6{X`^*p(G6SZo`uTSA9a{PKxF=gInRum4(CUD0_t(Rs4GZRn5+bl%vN z6|MMI<>pnMY>F%n*V|M@dCK8Gd1GU?XQ2oQc}W7^7tfgwTD&*L93FRGaBUYx#GCBn#&Yuw;h<9`s@3Iv)v+fE5VKCg}E#kYzjH)O&vz1i3w49-zHDIzk<>BRs;N$U7$P zXYbkNS5Mo}$KQ);MD@+w_j;~k(dPBYxB4~z2#}cD8zsbYkq83`q@69ODyTa>;u?h> z^~nvYw+f9hy6!b9dmVhTMj1Fe%mSX|^A*Z@NqeKlSTA+GC&9gZ;x_kmzdjk}-K=Yz z)71@Df78dW>-;RjFvRFJ-ll}_Na9|w@0&v9NB-#w*{|d;Be6tiU&2 zlu|XwiucbAce9!6D>$#^rz$WY99;!b5p)28<-$GCPOSvqblc(Fi{X8dXSnKrIC?7I zCwO~@@-I$W_%nDWn~563pUsaFQ+=40KA8A~ChC3uMbr}quq7VfG@42_OieefqGa#I z8F?3&ZM`CJbiMLY+Zuwq7Ab`C09Zf-p?HaNwd~hraC4`47Y7+8@S=PD511k@r9#1( zemCrDpBTQ_yF;WW8%ieY$W#xgKHFdOPC|}!Xoa|(H=(ra{DW^}XE%he$ z5mqr}#CG*=hD#M+$5nkc7{ zEG8KhN(q&GOrR-JOlrrgbQ}`JC6PWA$v-bQV~F6fN&J-^&`G85qqnnBuyGfvWxU(?VgU3YjVllrzIoZZoP}>R=3< z{fO})-ejd_)>@e3YDVr3;vKIra4K$TV0d8}Ha)Dl%s_K6lcKHi4?Wt&1-)Es2Ip z6wXzqx`0?CrJdc*UTsn3lyBwNqV9h6qABWyRYP5^KO4R>S6Gs5$H%&GnrzeeT@|Lt zF)A*47jr5CGEe!E8ozJkR)j7_(vt^GEu3$Z2*;P6%a`g)_&>b!YMgJzUcXLof|@?=z)ZZYn@Day4@nezh9$xy4K$F_G|cZMT9+!g)EmV==d> zk>9PRJ|kn|vS+nfb*-QLp}NH7v#VCqTPOcE3=bFTk=juBv!AF$GmZEo=(Liv{&>j%B!oaoza&Fii78Kl6lo?ICUUKXy+kTS3o}&o$FL6 z{@cuHIIWIKBNJ*ZS}cUt40}EjooUy15~QaurY#xXgfPrVT@i4j~oNLO?Pn5MbCMmmLsZ zrJv@rEz!c3?o9&ULgyh>IoY2~ZDDaJ9uEPjha}$n>;dunT)~>VDJ43NmqM}L7CLjL zEVJuhrUGx9NRr--Rm65ow{vqwD@PLaD;`HnI#&gc&DTS@xvR>#vGD+oL1Wypdtnq@N;+#|9*S?7~l(tu=|#v+F$jok`mwZyE_8Iq4vbxOH?^pUe6H>o6<1nzjU zg!3^eJpv%(>Ie3G*MZ|?LbL`4Y`6Qp>c zlf2eE2JHsbw7zYa#r;^4bmivQGcFmmSWCclWIxd%wgmRjn6^8cbSV(z#h_jUGOQ#c zNk7oD!XI;kgyqmIry3ANnji7nG0f#8M9G4SNiL-}2AX7)qVM-l^oTXFWe499DWffh zRH!H&Eh$Me358SDv!`3%QU%?tEv6n-8HvdTy2fyq&@3WH#L&!$#1gvqDx;Fi$I1Sv z5{A#n(Hm$>B5)}q?k%J|%}HvkdMPv27!oddl9rlS%F1zTOx5Hh9nFlmJ~pNqc=A7z z84)D`(oVF7bRILe_2^yuXcyJ^Ju{pXzwhyef~wGFXLW{u_Oc%lQ{r>47Kh6SZT306o50O9Xy znj^H}0HJKIT#2`T(7*WHlNs0kNC~#KtTU@p{;ACs8Z9JF5Nea(Qqv-e<#bQYv~CX3 zJQ7-M=(IPpAxXt^4cU)xFJuHrq#)x!hA|f&zdp1s>$2o?Q|Q=RIjN6&O8VcyW&P(p z`k(jc|EEgEFr{W&myO1umru<0u$GIXq%}arPFs)0k#(c3 zk4u?k?MMtRX7F@a6!@4vs#THpk@3M^DW(Onnj_{`s`4e>b4(Rx zq9eb%SPrI10vT`dB0a|$^W^UBM$X}-rb?Kn9_l%hXhB5m+==+`kPZu_cXCXkKx+fC zMQUh6ZWWc6&9>FAnH-T^xp1*bO*WQh4FsDRj?>&kQ^KPHebhE0HjBxon&4VY8J;iK zyBG#~b`i`zJd4Ty0)YA7^&bEKRHVNjT>eiI)1LzRhuPX?6Op(!@`3-FtqmIHI$fLk z7i{e)QT6|2+f(35 zn$DznZ`?8Yv|{?s_!aym$zK{`Ta5VSIbZM)@eBTvznw9?@x?n2=6JTja0P!seNOpf z1IHO(BCp`DmXp;~8VoYnD8bq875vrvxl2w~`%gxz=_D04c96^k8vc6EbyMo=;Wr=} z{tEQ&m#ySe*1GAfClpHpM8jX?43&2%{77t(zK$X%^igQ|%b&6s9_`|$*r-Sqfp;AZ zf5qO3v-P$by{n%+otG&6X`99IJM;{l!~``;wSl7Hukxt34jO2-wvNmWbEc7fRoU$% zH#Gb;V~Y)d=TmRxIF?s!<+`@*{Ey3MRda>>QM0>uZM5;Mb{&oI2y`AUsL^+Fa~A$l zvwx@whiEUk9Nqh6nj^B`FnDE}LwqxTVz|vTs8OKvV&nLiXU-XeE6g4L$}^{BziDD> zh}R1>J?DBVGOLv#EGl`b{>L*15_B@%%!l^OnafXv-M%^b%QI)rQ1}#vn)0Ccy7J7K zg0r}_IjRb;obWbWB{3DDbKj%6&!W=%5SGI)@5comcb}Rm z-w6Kr_La}~p_fv%{s+Yo`@yG^4}T=WDjpd9`0k0%&^$C;)7x`!II;41?P21KMw091 z;A70$bg`VG6t7P4f)&XH8H|A!Xrk(Q@xv})U}~6}m*wz+a{nvfRQQn;9H+YtTy*O& zLQq-_OYQo=rG9DvHfjy@xDos{fFn!@Y;ABkW8XZ)^dz#KmYyiDV$jx9!&U3sBa(^^ z3g!h@Uu9QQfD%R=0gXbGM+ zdr60K_G}N;c7mPxrCHbvcQQnqNCN<7^)#V%=gH=-5=qf-8-j%PM}n!E|~xb^-P93Rxa5yvkU^2sR)(?*ADibdwmO1VV+c@P(WkgTq{!Ppv0ryNMm3xc(>H!=k0@{ z37hy#uU3|@L`$aHpeLS!7G=a>h^zK8W{RRwchZu`voxAC(P%}V3DdAX za@yIcM}R2Wrc~&RO>5rTr+u-7y49A>EdA8+GkB_etTOKgS;qO$)~<)$O_Vhy*54qwe-&%;%X6;uuUHdph-&2Azlk-$ z!%s~Mv!G!EOUy79i{D#D2$hh%9mygk{dy{;gZ|a>-3r;Vdc8d6huKFHwa<@VchCu_8Ko7q@{@zC$S2{ zdRc=m>3+z;yQ8ny%tUV>sEg zcJ>fQ@(Q^}A_1$sIj3)*bIQd1h_uDi&VlKpz$Vm?ovG~a+ju*q72q6x-4u4wmrpb) zL~v;_;!n!bNCB#&?url~-$004A3p|T+17km(soN{2}vqK$}o!r!E=PpE&8g;)%kXu zE;>Hys|Z<#I-OLZ!Ye4N?XA~&@_6REl9k$u?fQAHmW6Xy0Y)9k=VD$E9%j1ySkE#7 zVimh5g*mxmuJb%xVn+U!MPYFf#=jzs%v9Ba2!!+H<%h6vp@EF9%jn^`gqFUJ^XFx- znC|_}+9z+AF00SRL=+)liY2WuY??|&3*di82*E#Z=>NQ-|8KpaqeI7tuRie3zXC!C zxf8>+)&{}}ugR-(9y1Qdlk&ixatg7GB$)jGhyVO$=@N)~MZ)KhB&_M7k&1txkHsO+ z<;eJj9HteYB^SMmkTWW?R}VML=eH8Kdpl8Mh{0B?dXiFf)BG8xjvS8cEc`ZsrX4kq zvow$R0YmVsaz`owJB(UE=3$g-R+0fGx0lCSdaCFhn)sc2W6PZ46@mN6d+V8FSwcJ_ z8^bDtFCJgV#QUFeAOGuBcK?<2LC75$`R~8VXp&4W-C-1C0@1f=(is*_D9lU~zHe9o z$_+~bD1Dhhs>my9yOZrNvTu<^q7OTptHUnYyx8V#Jc3gvQPJ4bj!2&6*2GYwLR;-4 zIr-8qx;YiA+W5#8IyNudOt#BwO|$0+fr-o?O!(0#uZ}MbB>nl;Oza zF9c}kknoBC6&ph)5?QI9!)=fANaKnCl`TNDudJ++B5sHDPRs8^6QHmZcK^OYchLl> zA0T{f#3J^$R_HU^b~FL%v#VmoBz;6^>FZ7%(@r!2>i$#3Q6vSIb}98`T+`3V@c>PL zBJRUGh27gnKECUan!3Axy7%VpSF&xnq!vBe2iTO3mX>f?Ockgj_ENeVE*b%Htuq*< za8$veb%LM?PzE^-PO`JPb{M`qVBB}_{v#%;k&~g1kfVv9G`M-_;@v3Vp{tW>#`L^G7 z!w!uAnNJ*aNGRRu$N0<7aEH4=vE}W8nl6O8q@w*go}FW{13qVGE;>O()?EBORI z@O4);{?4Tt72w&j%dx7Oe;>ZcwDjijDC146BqWQ1ji(4*9(Yn@x`l31WKIj~6eT#S zt|2lmsE~k3VX@zQOIxqu7{0xETO*S&K#-Rlxa-?o-@Z2j#*!LGT^zI?OsPSX)~9%F+WLFD>yCC( zCiuR*MRea#KN+FMy8p8I1?~5*Byk#_)^T5*eEZ-*{6z9Rp#8>=AMeOzPGHSgbl-4d zNnU5}yMD92Ge~n?y6Yw3gHeUm^YO)-2pO!<7M4PsiguxlM;iANHyL(&9Hfua?uNg! z!8t{c_PuxV2*nY}fk7O*4oUC&tj}J%8adr2RUKtgT}Z z1AN?Iq3BBONWe;rpOaXjaR41xi54KMLOx%Ao|cO0$M7T>k*=Ho_sA|X5FLdX7rG!^ zf@o%?+rZcAb%N_6SI-n1o065lW~qg*cHvBF!dljlPU%ogMmc;72Fuj`AR1)6Q=&dd zFejXVM%Z`+99xt?W_>ZtPixibnq?i-c3wtP;&?f(8^W7eZObOvKhmG4QSzwsybt3f zw6`0&0XXC3=0l1~%s&)QG~!nMzeyG6i9c%nG+5U-|P~ zIhF_Y-+*8Iw-y}ax5w*qxDXe~_Es;3_EpxTZ+k2Kh-c13?M@Q2{Zec+BUkLahw~ZYu~LqYi+!^b zYrD^@clAyA2}yZ`SuxRkcGfX9=6gB|_G^zu$gkBHei1Xocry(5e~|a)TKsJDd@%HW zzgp7Q8>BrzF%LhUR7+R9?5B~h1t{u9mOf%qKLr_hz<6V^J>-M&4` z9j5<|o!IX-&-G`~b7fon54U;Kv8H!Q5&xboPko}(zqI8^BjYnVA>feIzEa$&&;7-7 zX00G&{esTAGba`6|41oo(DIw-{H7N&&j5M(OL51^t9Muj=|eAi&}}{~+%7tQX2*kh zm;m~o#pm;H^4W@rmkO8X_)0UR%zJ<<^L&`RC9?Gw&)I!iRE6isYWh8DyZW9xwt?io zc+LU%VgPNWaRdi5&h5*mC#;cC1jOQ&smzbi!c$iP#Fg+A{A!l~ZZGla7tfi45Ftmb zqc;sCh6|>J+mlK7V_*~Obzoqr+4?BC4-dUdg&3bTz2G1)yN7r4;&nva1MDlFa~qx< z77svnNR}v{ahRMIxi|&kGA+a)kbbSNBp;QuRitobf!Ag>jT_JXm3kAs=&`e-3bnM6 z6OqKDQIr;&!Xn1!W1tFeL2?#a$knhtMR8p= zv_BW@rcD^sDu?|_>eY5mISJh8%X@U2uhtM*$kHz}UOA#xG{g?TRP<24gR;Zq31#P5&9(IxZdpAjm_4M&g|9fblZv2&*gk%{m+)(^IKHsUV!BgUSiZHvv9*MCX56;dHk?O}4d zSdTCuovjutz;zj~ne8rpIK$~PX zgyhXLBbeVllH-ZAeK@J)JQl}m6!*20sVuar+ve%ngx`nnpNZ`3-QhYdjok9urmK?Y z`|TfYC~U@*Zhb@B@_avk|K#$=#b-463|t7nRq4Z|ISR#7FT|um_Tfk#g^?r{VsohU zgG`RXsZoVEX!2R$Q3NBn2yM$lmU|S*i6);R2PnIaq68C*zz!;dG|NZP;;14bv@Orq zqnIo5S&YgMGtF_Vih42GL*x)T4^Ini%cC;PZE_rMges^rOV}ZD^u~#HO6Z)9n_gk)-^gc9DikUhDPf7MKE;LMXuFzh zlr8%&yPAw#R1~Khld3k_u14kEFT0vTzw|^lUrzihyP6J^9e+{(g!u`KrVqS@wyUw$ zDajr!y|Swzr)E-UO5A#oRfwq~jg>v71s7``1KSDOKvCQF;yFbVX7YpiPt`|Yh5cX$ zd7a!NMMBZKxC!IFfr4ga#SIDkOI4jL7*&PdJ8r`&b>9#`*0DOL%T|tB(C%qb+qo_0 zCBBvgo;Ni$pQDu2N6n}}2n<~JUglchtvKuS;_|a7CH=VRM0ms_j-!F@8>+K&k!Mh% zc?U(mz?lct-epy;JA#QqVCN%G=wWS@&TTK<^ejZh4_-T2fq3m=nnCKmAh?v>@^o2X zE3d}<4hLKO3__>1T48us;`;LZlP*zStB-Gugl|nhZt~7_e737^@fB=0gt&i~aNn4E zd_wa^X1x!P+dL-M;2Ha7*^0Nr9iWGNuF-XfKRvyCZR;5p$m)Pp=Q=X~8 zXUspLmhnlix{`Mo145!VeJQr^^KXSaJ#I|2_f>7}qo1&|0I4FX�Qp>fAm$gI6R! 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 v documentation". #html_title = None @@ -327,6 +325,10 @@ texinfo_documents = [ modindex_common_prefix = ['IPython.'] +def setup(app): + app.add_css_file("theme_overrides.css") + + # Cleanup # ------- # delete release info to avoid pickling errors from sphinx diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst index 99d4068..0a37b85 100644 --- a/docs/source/config/custommagics.rst +++ b/docs/source/config/custommagics.rst @@ -139,13 +139,26 @@ Accessing user namespace and local scope ======================================== When creating line magics, you may need to access surrounding scope to get user -variables (e.g when called inside functions). IPython provide the +variables (e.g when called inside functions). IPython provides the ``@needs_local_scope`` decorator that can be imported from ``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope`` can also be applied to cell magics even if cell magics cannot appear at local scope context. +Silencing the magic output +========================== + +Sometimes it may be useful to define a magic that can be silenced the same way +that non-magic expressions can, i.e., by appending a semicolon at the end of the Python +code to be executed. That can be achieved by decorating the magic function with +the decorator ``@output_can_be_silenced`` that can be imported from +``IPython.core.magics``. When this decorator is used, IPython will parse the Python +code used by the magic and, if the last token is a ``;``, the output created by the +magic will not show up on the screen. If you want to see an example of this decorator +in action, take a look on the ``time`` magic defined in +``IPython.core.magics.execution.py``. + Complete Example ================ diff --git a/docs/source/config/shortcuts/index.rst b/docs/source/config/shortcuts/index.rst index 4103d92..e361ec2 100755 --- a/docs/source/config/shortcuts/index.rst +++ b/docs/source/config/shortcuts/index.rst @@ -4,28 +4,23 @@ IPython shortcuts Available shortcuts in an IPython terminal. -.. warning:: +.. note:: - This list is automatically generated, and may not hold all available - shortcuts. In particular, it may depend on the version of ``prompt_toolkit`` - installed during the generation of this page. + This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ + between installations depending on the ``prompt_toolkit`` version. -Single Filtered shortcuts -========================= - -.. csv-table:: - :header: Shortcut,Filter,Description - :widths: 30, 30, 100 - :delim: tab - :file: single_filtered.csv +* Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession. +* Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously. +* Hover over the ⓘ icon in the filter column to see when the shortcut is active.g +.. role:: raw-html(raw) + :format: html -Multi Filtered shortcuts -======================== .. csv-table:: - :header: Shortcut,Filter,Description - :widths: 30, 30, 100 + :header: Shortcut,Description and identifier,Filter :delim: tab - :file: multi_filtered.csv + :class: shortcuts + :file: table.tsv + :widths: 20 75 5 diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index e1d4574..2f743ea 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -2,6 +2,54 @@ 8.x Series ============ +.. _version 8.9.0: + +IPython 8.9.0 +------------- + +Second release of IPython in 2023, last Friday of the month, we are back on +track. This is a small release with a few bug-fixes, and improvements, mostly +with respect to terminal shortcuts. + + +The biggest improvement for 8.9 is a drastic amelioration of the +auto-suggestions sponsored by D.E. Shaw and implemented by the more and more +active contributor `@krassowski `. + +- ``right`` accepts a single character from suggestion +- ``ctrl+right`` accepts a semantic token (macos default shortcuts take + precedence and need to be disabled to make this work) +- ``backspace`` deletes a character and resumes hinting autosuggestions +- ``ctrl-left`` accepts suggestion and moves cursor left one character. +- ``backspace`` deletes a character and resumes hinting autosuggestions +- ``down`` moves to suggestion to later in history when no lines are present below the cursors. +- ``up`` moves to suggestion from earlier in history when no lines are present above the cursor. + +This is best described by the Gif posted by `@krassowski +`, and in the PR itself :ghpull:`13888`. + +.. image:: ../_images/autosuggest.gif + +Please report any feedback in order for us to improve the user experience. +In particular we are also working on making the shortcuts configurable. + +If you are interested in better terminal shortcuts, I also invite you to +participate in issue `13879 +`__. + + +As we follow `NEP29 +`__, next version of +IPython will officially stop supporting numpy 1.20, and will stop supporting +Python 3.8 after April release. + +As usual you can find the full list of PRs on GitHub under `the 8.9 milestone +`__. + + +Thanks to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + .. _version 8.8.0: IPython 8.8.0 @@ -11,11 +59,11 @@ First release of IPython in 2023 as there was no release at the end of December. This is an unusually big release (relatively speaking) with more than 15 Pull -Requests merge. +Requests merged. Of particular interest are: - - :ghpull:`13852` that replace the greedy completer and improve + - :ghpull:`13852` that replaces the greedy completer and improves completion, in particular for dictionary keys. - :ghpull:`13858` that adds ``py.typed`` to ``setup.cfg`` to make sure it is bundled in wheels. @@ -24,7 +72,7 @@ Of particular interest are: believe this also needs a recent version of Traitlets. - :ghpull:`13865` makes the ``inspector`` class of `InteractiveShell` configurable. - - :ghpull:`13880` that remove minor-version entrypoints as the minor version + - :ghpull:`13880` that removes minor-version entrypoints as the minor version entry points that would be included in the wheel would be the one of the Python version that was used to build the ``whl`` file. @@ -48,8 +96,8 @@ IPython 8.7.0 Small release of IPython with a couple of bug fixes and new features for this -month. Next month is end of year, it is unclear if there will be a release close -the new year's eve, or if the next release will be at end of January. +month. Next month is the end of year, it is unclear if there will be a release +close to the new year's eve, or if the next release will be at the end of January. Here are a few of the relevant fixes, as usual you can find the full list of PRs on GitHub under `the 8.7 milestone @@ -73,29 +121,29 @@ IPython 8.6.0 Back to a more regular release schedule (at least I try), as Friday is already over by more than 24h hours. This is a slightly bigger release with a -few new features that contain no less then 25 PRs. +few new features that contain no less than 25 PRs. We'll notably found a couple of non negligible changes: The ``install_ext`` and related functions have been removed after being deprecated for years. You can use pip to install extensions. ``pip`` did not -exists when ``install_ext`` was introduced. You can still load local extensions +exist when ``install_ext`` was introduced. You can still load local extensions without installing them. Just set your ``sys.path`` for example. :ghpull:`13744` -IPython now have extra entry points that that the major *and minor* version of -python. For some of you this mean that you can do a quick ``ipython3.10`` to +IPython now has extra entry points that use the major *and minor* version of +python. For some of you this means that you can do a quick ``ipython3.10`` to launch IPython from the Python 3.10 interpreter, while still using Python 3.11 as your main Python. :ghpull:`13743` -The completer matcher API have been improved. See :ghpull:`13745`. This should +The completer matcher API has been improved. See :ghpull:`13745`. This should improve the type inference and improve dict keys completions in many use case. -Tanks ``@krassowski`` for all the works, and the D.E. Shaw group for sponsoring +Thanks ``@krassowski`` for all the work, and the D.E. Shaw group for sponsoring it. The color of error nodes in tracebacks can now be customized. See -:ghpull:`13756`. This is a private attribute until someone find the time to -properly add a configuration option. Note that with Python 3.11 that also show -the relevant nodes in traceback, it would be good to leverage this informations +:ghpull:`13756`. This is a private attribute until someone finds the time to +properly add a configuration option. Note that with Python 3.11 that also shows +the relevant nodes in traceback, it would be good to leverage this information (plus the "did you mean" info added on attribute errors). But that's likely work I won't have time to do before long, so contributions welcome. @@ -108,7 +156,7 @@ This mostly occurs in teaching context when incorrect values get passed around. The ``?``, ``??``, and corresponding ``pinfo``, ``pinfo2`` magics can now find -objects insides arrays. That is to say, the following now works:: +objects inside arrays. That is to say, the following now works:: >>> def my_func(*arg, **kwargs):pass @@ -117,7 +165,7 @@ objects insides arrays. That is to say, the following now works:: If ``container`` define a custom ``getitem``, this __will__ trigger the custom -method. So don't put side effects in your ``getitems``. Thanks the D.E. Shaw +method. So don't put side effects in your ``getitems``. Thanks to the D.E. Shaw group for the request and sponsoring the work. @@ -143,17 +191,17 @@ an bug fixes. Many thanks to everybody who contributed PRs for your patience in review and merges. -Here is a non exhaustive list of changes that have been implemented for IPython +Here is a non-exhaustive list of changes that have been implemented for IPython 8.5.0. As usual you can find the full list of issues and PRs tagged with `the 8.5 milestone `__. - - Added shortcut for accepting auto suggestion. The End key shortcut for + - Added a shortcut for accepting auto suggestion. The End key shortcut for accepting auto-suggestion This binding works in Vi mode too, provided ``TerminalInteractiveShell.emacs_bindings_in_vi_insert_mode`` is set to be ``True`` :ghpull:`13566`. - - No popup in window for latex generation w hen generating latex (e.g. via + - No popup in window for latex generation when generating latex (e.g. via `_latex_repr_`) no popup window is shows under Windows. :ghpull:`13679` - Fixed error raised when attempting to tab-complete an input string with @@ -269,12 +317,12 @@ IPython 8.3.0 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on - the info object when frontend provide it. This has been backported to 7.33 + the info object when frontend provides it. This has been backported to 7.33 - :ghpull:`13624`, fixed :kbd:`End` key being broken after accepting an auto-suggestion. - - :ghpull:`13657` fix issue where history from different sessions would be mixed. + - :ghpull:`13657` fixed an issue where history from different sessions would be mixed. .. _version 8.2.0: @@ -292,8 +340,8 @@ IPython 8.2 mostly bring bugfixes to IPython. - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498` -I am still trying to fix and investigate :ghissue:`13598`, which seem to be -random, and would appreciate help if you find reproducible minimal case. I've +I am still trying to fix and investigate :ghissue:`13598`, which seems to be +random, and would appreciate help if you find a reproducible minimal case. I've tried to make various changes to the codebase to mitigate it, but a proper fix will be difficult without understanding the cause. @@ -322,7 +370,7 @@ IPython 8.1.0 ------------- IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and -Update a few behavior that were problematic with the 8.0 as with many new major +updates a few behaviors that were problematic with the 8.0 as with many new major release. Note that beyond the changes listed here, IPython 8.1.0 also contains all the @@ -373,8 +421,8 @@ We want to remind users that IPython is part of the Jupyter organisations, and thus governed by a Code of Conduct. Some of the behavior we have seen on GitHub is not acceptable. Abuse and non-respectful comments on discussion will not be tolerated. -Many thanks to all the contributors to this release, many of the above fixed issue and -new features where done by first time contributors, showing there is still +Many thanks to all the contributors to this release, many of the above fixed issues and +new features were done by first time contributors, showing there is still plenty of easy contribution possible in IPython . You can find all individual contributions to this milestone `on github `__. @@ -435,7 +483,7 @@ IPython 8.0 IPython 8.0 is bringing a large number of new features and improvements to both the user of the terminal and of the kernel via Jupyter. The removal of compatibility -with older version of Python is also the opportunity to do a couple of +with an older version of Python is also the opportunity to do a couple of performance improvements in particular with respect to startup time. The 8.x branch started diverging from its predecessor around IPython 7.12 (January 2020). @@ -444,7 +492,7 @@ This release contains 250+ pull requests, in addition to many of the features and backports that have made it to the 7.x branch. Please see the `8.0 milestone `__ for the full list of pull requests. -Please feel free to send pull requests to updates those notes after release, +Please feel free to send pull requests to update those notes after release, I have likely forgotten a few things reviewing 250+ PRs. Dependencies changes/downstream packaging @@ -459,8 +507,8 @@ looking for help to do so. - minimal Python is now 3.8 - ``nose`` is not a testing requirement anymore - ``pytest`` replaces nose. - - ``iptest``/``iptest3`` cli entrypoints do not exists anymore. - - minimum officially support ``numpy`` version has been bumped, but this should + - ``iptest``/``iptest3`` cli entrypoints do not exist anymore. + - the minimum officially ​supported ``numpy`` version has been bumped, but this should not have much effect on packaging. diff --git a/setup.cfg b/setup.cfg index de327ab..d196214 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ install_requires = matplotlib-inline pexpect>4.3; sys_platform != "win32" pickleshare - prompt_toolkit>=3.0.11,<3.1.0 + prompt_toolkit>=3.0.30,<3.1.0 pygments>=2.4.0 stack_data traitlets>=5