diff --git a/codeanalyzer/__main__.py b/codeanalyzer/__main__.py index d7f4ab3..51f227f 100644 --- a/codeanalyzer/__main__.py +++ b/codeanalyzer/__main__.py @@ -7,7 +7,7 @@ from codeanalyzer.utils import _set_log_level, logger from codeanalyzer.config import OutputFormat from codeanalyzer.schema import model_dump_json -from codeanalyzer.options import AnalysisOptions, EmitTarget +from codeanalyzer.options import AnalysisOptions, EmitTarget, ShardStrategy def main( @@ -83,9 +83,16 @@ def main( help="Neo4j database name (default: server default). [env: NEO4J_DATABASE]", ), ] = None, - using_codeql: Annotated[ - bool, typer.Option("--codeql/--no-codeql", help="Enable CodeQL-based analysis.") - ] = False, + analysis_level: Annotated[ + int, + typer.Option( + "-a", + "--analysis-level", + help="Analysis depth: 1=symbol table+Jedi call graph, 2=+PyCG call graph.", + min=1, + max=2, + ), + ] = 1, using_ray: Annotated[ bool, typer.Option("--ray/--no-ray", help="Enable Ray for distributed analysis."), @@ -137,6 +144,78 @@ def main( verbosity: Annotated[ int, typer.Option("-v", count=True, help="Increase verbosity: -v, -vv, -vvv") ] = 0, + pycg_shard: Annotated[ + bool, + typer.Option( + "--pycg-shard/--no-pycg-shard", + help=( + "Shard PyCG call-graph analysis by Python package (level 2 only). " + "When the project exceeds the 500-file ceiling, PyCG is run " + "independently per top-level package with cross-package imports " + "treated as ghost nodes. Without this flag, projects over the " + "ceiling fall back to Jedi-only edges." + ), + ), + ] = False, + pycg_shard_ceiling: Annotated[ + int, + typer.Option( + "--pycg-shard-ceiling", + help=( + "Maximum files per shard when --pycg-shard is active (default 100). " + "Shards exceeding this limit are skipped; their call edges are " + "omitted from the call graph (Jedi edges for those packages are " + "still included). Lower values are safer for packages with deep " + "class hierarchies or heavy import graphs." + ), + min=1, + ), + ] = 100, + pycg_shard_timeout: Annotated[ + int, + typer.Option( + "--pycg-shard-timeout", + help=( + "Per-shard wall-clock timeout in seconds when --pycg-shard is " + "active (default 120). A shard that exceeds this limit is skipped " + "gracefully. PyCG's fixpoint is bimodal: it either converges " + "quickly or diverges indefinitely, so the timeout acts as a final " + "safety net after the file-count ceiling. Set to 0 to disable. " + "POSIX only (macOS / Linux); ignored on Windows." + ), + min=0, + ), + ] = 120, + pycg_shard_strategy: Annotated[ + ShardStrategy, + typer.Option( + "--pycg-shard-strategy", + help=( + "How --pycg-shard groups files (level 2 only). 'jedi' (default) " + "partitions the Jedi module-dependency graph (SCC + Louvain) so " + "tightly-coupled modules co-compute and few call edges are " + "severed between shards; import cycles are never split. " + "'package' uses the legacy one-shard-per-package-directory " + "grouping." + ), + ), + ] = ShardStrategy.JEDI, + pycg_max_iter: Annotated[ + int, + typer.Option( + "--pycg-max-iter", + help=( + "Cap on PyCG's fixpoint passes per shard/project (level 2; " + "default 50). PyCG iterates until its points-to state stops " + "changing, but its access-path domain has no convergence bound, " + "so heavy metaclass/mixin code (e.g. an ORM) can loop with each " + "pass costing seconds. The cap returns a sound-but-incomplete " + "call graph instead of looping until the timeout kills it. " + "Set to -1 for PyCG's unbounded run-to-convergence behaviour." + ), + min=-1, + ), + ] = 50, ): options = AnalysisOptions( input=input, @@ -148,7 +227,7 @@ def main( neo4j_user=neo4j_user, neo4j_password=neo4j_password, neo4j_database=neo4j_database, - using_codeql=using_codeql, + analysis_level=analysis_level, using_ray=using_ray, rebuild_analysis=rebuild_analysis, skip_tests=skip_tests, @@ -157,6 +236,11 @@ def main( cache_dir=cache_dir, clear_cache=clear_cache, verbosity=verbosity, + pycg_shard=pycg_shard, + pycg_shard_ceiling=pycg_shard_ceiling, + pycg_shard_timeout=pycg_shard_timeout, + pycg_shard_strategy=pycg_shard_strategy, + pycg_max_iter=pycg_max_iter, ) _set_log_level(options.verbosity) @@ -230,7 +314,7 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat): app = typer.Typer( callback=main, name="canpy", - help="Static Analysis on Python source code using Jedi, CodeQL and Tree sitter.", + help="Static Analysis on Python source code using Jedi, PyCG and Tree sitter.", invoke_without_command=True, no_args_is_help=True, add_completion=False, diff --git a/codeanalyzer/core.py b/codeanalyzer/core.py index 9b5f538..151446e 100644 --- a/codeanalyzer/core.py +++ b/codeanalyzer/core.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Any, Dict, Optional, Union, List +import time + import ray from codeanalyzer.utils import logger from codeanalyzer.schema import ( @@ -17,13 +19,12 @@ ) from codeanalyzer.schema.py_schema import PyCallEdge from codeanalyzer.semantic_analysis.call_graph import ( + filter_external_edges, jedi_call_graph_edges, merge_edges, resolve_unresolved_constructors, ) -from codeanalyzer.semantic_analysis.codeql import CodeQLLoader -from codeanalyzer.semantic_analysis.codeql.codeql_analysis import CodeQL -from codeanalyzer.semantic_analysis.codeql.codeql_exceptions import CodeQLExceptions +from codeanalyzer.semantic_analysis.pycg import PyCG, PyCGExceptions from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayError from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder from codeanalyzer.utils import ProgressBar @@ -54,7 +55,7 @@ def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, s class Codeanalyzer: - """Core functionality for CodeQL analysis. + """Core static analysis engine for Python projects. Args: options (AnalysisOptions): Analysis configuration options containing all necessary parameters. @@ -64,16 +65,13 @@ def __init__(self, options: AnalysisOptions) -> None: self.options = options self.project_dir = Path(options.input).resolve() self.skip_tests = options.skip_tests - self.using_codeql = options.using_codeql + self.analysis_level = options.analysis_level self.rebuild_analysis = options.rebuild_analysis self.no_venv = options.no_venv self.cache_dir = ( options.cache_dir.resolve() if options.cache_dir is not None else self.project_dir ) / ".codeanalyzer" self.clear_cache = options.clear_cache - self.db_path: Optional[Path] = None - self.codeql_bin: Optional[Path] = None - self.codeql_packs_dir: Optional[Path] = None self.virtualenv: Optional[Path] = None self.using_ray: bool = options.using_ray self.file_name: Optional[Path] = options.file_name @@ -85,6 +83,7 @@ def _cmd_exec_helper( capture_output: bool = True, check: bool = True, suppress_output: bool = False, + log_on_failure: bool = True, ) -> subprocess.CompletedProcess: """ Runs a subprocess with real-time output streaming to the logger. @@ -94,7 +93,10 @@ def _cmd_exec_helper( cwd: Working directory to run the command in. capture_output: If True, retains and returns the output. check: If True, raises CalledProcessError on non-zero exit. - suppress_output: If True, silences log output. + suppress_output: If True, silences per-line debug output. + log_on_failure: If False, suppresses the error-level log on + non-zero exit (use when the caller handles the exception and + will emit its own diagnostic). Returns: subprocess.CompletedProcess @@ -125,9 +127,10 @@ def _cmd_exec_helper( if check and returncode != 0: error_output = "\n".join(output_lines) - logger.error(f"Command failed with exit code {returncode}: {' '.join(cmd)}") - if error_output: - logger.error(f"Command output:\n{error_output}") + if log_on_failure: + logger.error(f"Command failed with exit code {returncode}: {' '.join(cmd)}") + if error_output: + logger.error(f"Command output:\n{error_output}") raise subprocess.CalledProcessError(returncode, cmd, output=error_output) return subprocess.CompletedProcess( @@ -248,13 +251,22 @@ def _uv_bin() -> Optional[str]: def _install_into_venv(self, venv_python: Path, args: List[str]) -> None: """Install packages into the target venv, preferring uv for speed (parallel downloads + a shared global cache) and falling back to the venv's own pip - when uv is unavailable.""" + when uv is unavailable. + + Raises ``subprocess.CalledProcessError`` on failure; callers in + ``__enter__`` catch this and warn-and-continue so a single failing + package (e.g. a C extension that needs system libs) does not abort the + entire analysis. + """ uv = self._uv_bin() if uv: cmd = [uv, "pip", "install", "--python", str(venv_python), *args] else: cmd = [str(venv_python), "-m", "pip", "install", *args] - self._cmd_exec_helper(cmd, cwd=self.project_dir, check=True) + self._cmd_exec_helper( + cmd, cwd=self.project_dir, check=True, + suppress_output=True, log_on_failure=False, + ) def __enter__(self) -> "Codeanalyzer": # If no virtualenv is provided, try to create one using requirements.txt or pyproject.toml @@ -287,21 +299,32 @@ def __enter__(self) -> "Codeanalyzer": for dep_file, _ in dependency_files: if (self.project_dir / dep_file).exists(): logger.info(f"Installing dependencies from {dep_file}") - self._install_into_venv( - venv_python, - ["--upgrade", "-r", str(self.project_dir / dep_file)], - ) + try: + self._install_into_venv( + venv_python, + ["--upgrade", "-r", str(self.project_dir / dep_file)], + ) + except subprocess.CalledProcessError as exc: + logger.warning( + f"Dependency installation from {dep_file} failed " + f"(exit {exc.returncode}) — continuing without it. " + "Jedi type resolution may be incomplete." + ) # Handle Pipenv files if (self.project_dir / "Pipfile").exists(): logger.info("Installing dependencies from Pipfile") - # Note: This would require pipenv to be installed - self._install_into_venv(venv_python, ["pipenv"]) - self._cmd_exec_helper( - ["pipenv", "install", "--dev"], - cwd=self.project_dir, - check=True, - ) + try: + self._install_into_venv(venv_python, ["pipenv"]) + self._cmd_exec_helper( + ["pipenv", "install", "--dev"], + cwd=self.project_dir, + check=True, + ) + except subprocess.CalledProcessError as exc: + logger.warning( + f"Pipenv installation failed (exit {exc.returncode}) — continuing without it." + ) # Handle conda environment files conda_files = ["conda.yml", "environment.yml"] @@ -319,7 +342,13 @@ def __enter__(self) -> "Codeanalyzer": if any((self.project_dir / file).exists() for file in package_definition_files): logger.info("Installing project in editable mode") - self._install_into_venv(venv_python, ["-e", str(self.project_dir)]) + try: + self._install_into_venv(venv_python, ["-e", str(self.project_dir)]) + except subprocess.CalledProcessError as exc: + logger.warning( + f"Editable install failed (exit {exc.returncode}) — " + "continuing without it. Jedi type resolution may be incomplete." + ) else: logger.warning("No package definition files found, skipping editable installation") @@ -331,60 +360,6 @@ def __enter__(self) -> "Codeanalyzer": if not self.no_venv and venv_path.exists(): self.virtualenv = venv_path - if self.using_codeql: - logger.info(f"(Re-)initializing CodeQL analysis for {self.project_dir}") - - # Resolve the CLI binary before anything else uses it: DB build - # below needs it, and so does every subsequent query run. - self.codeql_bin = self._ensure_codeql_bin() - # Download the standard query library pack (idempotent). The - # CLI install ships only the language extractors; the - # ``codeql/python-all`` library pack must be fetched separately. - self.codeql_packs_dir = self._ensure_codeql_packs(self.codeql_bin) - - cache_root = self.cache_dir / "codeql" - cache_root.mkdir(parents=True, exist_ok=True) - self.db_path = cache_root / f"{self.project_dir.name}-db" - self.db_path.mkdir(exist_ok=True) - - checksum_file = self.db_path / ".checksum" - current_checksum = self._compute_checksum(self.project_dir) - - def is_cache_valid() -> bool: - if not (self.db_path / "db-python").exists(): - return False - if not checksum_file.exists(): - return False - return checksum_file.read_text().strip() == current_checksum - - if self.rebuild_analysis or not is_cache_valid(): - logger.info("Creating new CodeQL database...") - - cmd = [ - str(self.codeql_bin), - "database", - "create", - str(self.db_path), - f"--source-root={self.project_dir}", - "--language=python", - "--overwrite", - ] - - proc = subprocess.Popen( - cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE - ) - _, err = proc.communicate() - - if proc.returncode != 0: - raise CodeQLExceptions.CodeQLDatabaseBuildException( - f"Error building CodeQL database:\n{err.decode()}" - ) - - checksum_file.write_text(current_checksum) - - else: - logger.info(f"Reusing cached CodeQL DB at {self.db_path}") - return self def __exit__(self, *args, **kwargs) -> None: @@ -449,24 +424,21 @@ def analyze(self) -> PyApplication: # Build symbol table from cached application if available (if no available, the build a new one) symbol_table = self._build_symbol_table(cached_pyapplication.symbol_table if cached_pyapplication else {}) - # Build the call graph in four steps: - # 1. Run CodeQL (when enabled). Produces resolved edges with - # ``provenance=["codeql"]`` and augments ``PyCallsite``s - # in-place — filling ``callee_signature`` for sites Jedi - # couldn't resolve. - # 2. Heuristic fallback for constructor calls neither Jedi nor - # CodeQL could resolve (commonly classes nested inside - # functions). Walks the symbol table by class short-name + - # scope and writes ``.__init__`` into the site. - # 3. Derive Jedi edges from the now-fully-augmented symbol - # table — these reflect every resolution the symbol table - # contains, regardless of which pass put it there. - # 4. Merge with CodeQL edges; provenance unions for edges both - # backends saw. - codeql_edges = self._get_call_graph(symbol_table, augment_sites=True) resolve_unresolved_constructors(symbol_table) + + # Level 1: Jedi call graph. + t0_jedi = time.perf_counter() jedi_edges = jedi_call_graph_edges(symbol_table) - call_graph = merge_edges(jedi_edges, codeql_edges) + call_graph = list(jedi_edges) + logger.info("✅ Jedi: %d edges in %.1fs", len(call_graph), time.perf_counter() - t0_jedi) + + if self.analysis_level >= 2: + # Level 2: also add PyCG edges. The Jedi edges double as the + # coupling graph that drives coupling-aware PyCG sharding. + pycg_edges = self._get_pycg_call_graph(symbol_table, jedi_edges) + call_graph = merge_edges(call_graph, pycg_edges) + + call_graph = filter_external_edges(call_graph, symbol_table) # Classify call-graph endpoints that are not declared in the symbol table # (imported library / builtin members) once, so the JSON and Neo4j backends @@ -573,7 +545,8 @@ def _build_symbol_table(self, cached_symbol_table: Optional[Dict[str, PyModule]] Dict[str, PyModule]: A dictionary mapping file paths to PyModule objects. """ symbol_table: Dict[str, PyModule] = {} - + t0_st = time.perf_counter() + # Handle single file analysis if self.file_name is not None: single_file = self.project_dir / self.file_name @@ -680,123 +653,44 @@ def _build_symbol_table(self, cached_symbol_table: Optional[Dict[str, PyModule]] if files_from_cache > 0: logger.info(f"Reused {files_from_cache} files from cache, processed {files_processed} new/changed files") - logger.info("✅ Symbol table generation complete.") - return symbol_table - - def _ensure_codeql_packs(self, codeql_bin: Path) -> Path: - """Materialize a qlpack that depends on ``codeql/python-all``. - - The CodeQL CLI install ships only the language extractors — query - library packs (and their transitive dependencies like - ``codeql/concepts``) must be resolved separately. The canonical - way is to declare the dependency in a ``qlpack.yml`` and run - ``codeql pack install`` in that directory; CodeQL writes a - ``codeql-pack.lock.yml`` and downloads everything needed. - - We do this once per project under ``/codeql/qlpack/`` - and return that directory. The query runner then writes its - temporary ``.ql`` file inside this pack — colocation makes - ``import python`` resolve without any ``--additional-packs`` or - ``--search-path`` gymnastics. - """ - pack_dir = self.cache_dir / "codeql" / "qlpack" - pack_dir.mkdir(parents=True, exist_ok=True) - qlpack_yml = pack_dir / "qlpack.yml" - lock_file = pack_dir / "codeql-pack.lock.yml" - - if not qlpack_yml.exists(): - qlpack_yml.write_text( - "name: codeanalyzer-deps\n" - "version: 1.0.0\n" - "dependencies:\n" - ' codeql/python-all: "*"\n' - ) - - if lock_file.exists(): - logger.debug(f"CodeQL pack dependencies already installed in {pack_dir}") - return pack_dir - - logger.info(f"Installing CodeQL pack dependencies in {pack_dir}.") - proc = subprocess.Popen( - [str(codeql_bin), "pack", "install", str(pack_dir)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - _, err = proc.communicate() - if proc.returncode != 0: - raise CodeQLExceptions.CodeQLDatabaseBuildException( - f"Failed to install CodeQL pack dependencies:\n" - f"{(err or b'').decode(errors='replace')}" - ) - return pack_dir - - def _ensure_codeql_bin(self) -> Path: - """Locate (or download) the CodeQL CLI binary into the project cache. - - Resolution order: - 1. An existing binary inside ``/codeql/bin/`` — - reused across runs on the same project. - 2. ``codeql`` already on the user's PATH — picked up verbatim. - 3. Otherwise, download into ``/codeql/bin/``. - - The project-local cache is preferred over PATH so the version we - installed earlier wins over whatever the OS ships — keeps behavior - deterministic when the user has both. - """ - bin_root = self.cache_dir / "codeql" / "bin" - bin_root.mkdir(parents=True, exist_ok=True) - - existing = next( - (p for p in bin_root.rglob("codeql") if p.is_file()), - None, + logger.info( + "✅ Symbol table: %d modules in %.1fs", + len(symbol_table), time.perf_counter() - t0_st, ) - if existing and os.access(existing, os.X_OK): - logger.debug(f"Reusing cached CodeQL CLI at {existing}") - return existing.resolve() - - on_path = shutil.which("codeql") - if on_path: - logger.debug(f"Using CodeQL CLI from PATH at {on_path}") - return Path(on_path) - - logger.info(f"CodeQL CLI not found; downloading into {bin_root}.") - downloaded = CodeQLLoader.download_and_extract_codeql(bin_root) - if not downloaded.exists() or not os.access(downloaded, os.X_OK): - raise FileNotFoundError( - f"CodeQL binary not executable after download: {downloaded}" - ) - return downloaded + return symbol_table - def _get_call_graph( + def _get_pycg_call_graph( self, symbol_table: Dict[str, PyModule], - augment_sites: bool = False, + jedi_edges: List[PyCallEdge], ) -> List[PyCallEdge]: - """Build CodeQL-resolved call edges and optionally augment sites. + """Build PyCG-resolved call edges. - Returns an empty list when CodeQL isn't enabled or the database - isn't available. Edges carry ``provenance=["codeql"]`` — merge - with Jedi-derived edges via ``call_graph.merge_edges``. + Runs PyCG's iterative name-pointer analysis over the whole project + and returns edges with ``provenance=["pycg"]``. Falls back to an + empty list and logs a warning on any failure so the caller can + continue with Jedi-only edges. - When ``augment_sites`` is True, also mutates - ``PyCallable.call_sites`` in the symbol table to backfill - ``callee_signature`` for sites Jedi couldn't resolve. The single - CodeQL query is shared (cached on the ``CodeQL`` instance) so - this costs no extra DB work. + *jedi_edges* are the level-1 call edges; under the ``jedi`` shard + strategy they drive coupling-aware partitioning (see + :func:`shard_planner.plan_shards`). """ - if not self.using_codeql or self.db_path is None: - return [] try: - cq = CodeQL( + pycg = PyCG( self.project_dir, - self.db_path, - codeql_bin=self.codeql_bin, - codeql_packs_dir=self.codeql_packs_dir, + skip_tests=self.skip_tests, + shard=self.options.pycg_shard, + shard_ceiling=self.options.pycg_shard_ceiling, + shard_timeout=self.options.pycg_shard_timeout, + shard_strategy=self.options.pycg_shard_strategy, + max_iter=self.options.pycg_max_iter, + using_ray=self.using_ray, ) - edges = cq.build_call_graph_edges(symbol_table) - if augment_sites: - cq.augment_call_sites(symbol_table) - return edges - except Exception as exc: - logger.warning(f"CodeQL call-graph extraction failed: {exc}") + return pycg.build_call_graph_edges(symbol_table, jedi_edges=jedi_edges) + except PyCGExceptions.PyCGImportError as exc: + logger.warning(f"PyCG not installed — level 2 edges will be Jedi-only: {exc}") + return [] + except PyCGExceptions.PyCGAnalysisError as exc: + logger.warning(f"PyCG analysis failed — level 2 edges will be Jedi-only: {exc}") + logger.debug("PyCG full traceback:", exc_info=True) return [] \ No newline at end of file diff --git a/codeanalyzer/options/__init__.py b/codeanalyzer/options/__init__.py index 127a183..317d42b 100644 --- a/codeanalyzer/options/__init__.py +++ b/codeanalyzer/options/__init__.py @@ -1,3 +1,3 @@ -from .options import AnalysisOptions, EmitTarget, OutputFormat +from .options import AnalysisOptions, EmitTarget, OutputFormat, ShardStrategy -__all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat"] \ No newline at end of file +__all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat", "ShardStrategy"] \ No newline at end of file diff --git a/codeanalyzer/options/options.py b/codeanalyzer/options/options.py index e314c5e..4e8662c 100644 --- a/codeanalyzer/options/options.py +++ b/codeanalyzer/options/options.py @@ -23,6 +23,20 @@ class EmitTarget(str, Enum): SCHEMA = "schema" +class ShardStrategy(str, Enum): + """How ``--pycg-shard`` groups files into shards (level 2 only). + + - ``jedi`` : partition the Jedi module-dependency graph (strongly- + connected-component condensation + Louvain) so tightly- + coupled modules co-compute and few call edges are severed + between shards. Import cycles are never split. + - ``package`` : legacy one-shard-per-package-directory grouping. + """ + + JEDI = "jedi" + PACKAGE = "package" + + @dataclass class AnalysisOptions: input: Path @@ -34,7 +48,7 @@ class AnalysisOptions: neo4j_user: str = "neo4j" neo4j_password: str = "neo4j" neo4j_database: Optional[str] = None - using_codeql: bool = False + analysis_level: int = 1 using_ray: bool = False rebuild_analysis: bool = False skip_tests: bool = True @@ -43,3 +57,8 @@ class AnalysisOptions: cache_dir: Optional[Path] = None clear_cache: bool = False verbosity: int = 0 + pycg_shard: bool = False + pycg_shard_ceiling: int = 100 + pycg_shard_timeout: int = 120 + pycg_shard_strategy: ShardStrategy = ShardStrategy.JEDI + pycg_max_iter: int = 50 diff --git a/codeanalyzer/schema/py_schema.py b/codeanalyzer/schema/py_schema.py index c69e5fb..d58ef91 100644 --- a/codeanalyzer/schema/py_schema.py +++ b/codeanalyzer/schema/py_schema.py @@ -355,7 +355,7 @@ class PyCallEdge(BaseModel): target: str # callee's PyCallable.signature type: Literal["CALL_DEP"] = "CALL_DEP" weight: int = 1 - provenance: List[Literal["jedi", "codeql", "joern"]] = [] + provenance: List[Literal["jedi", "pycg", "joern"]] = [] @builder diff --git a/codeanalyzer/semantic_analysis/call_graph.py b/codeanalyzer/semantic_analysis/call_graph.py index f9fa941..fb5f914 100644 --- a/codeanalyzer/semantic_analysis/call_graph.py +++ b/codeanalyzer/semantic_analysis/call_graph.py @@ -173,7 +173,7 @@ def jedi_call_graph_edges( Edges are coalesced on ``(source, target)``: ``weight`` is the count of matching sites. Provenance is always ``["jedi"]``; combine with - CodeQL-derived edges via ``merge_edges``. + PyCG-derived edges via ``merge_edges``. """ counts: Counter = Counter() for caller in iter_callables_in_symbol_table(symbol_table): @@ -191,7 +191,7 @@ def jedi_call_graph_edges( def resolve_unresolved_constructors(symbol_table: Dict[str, PyModule]) -> int: """Fill in ``PyCallsite.callee_signature`` for unresolved constructor sites. - When both Jedi and CodeQL fail to resolve a constructor call (commonly + When Jedi fails to resolve a constructor call (commonly for classes nested inside functions or methods, where static-analysis points-to is weakest), Jedi still flags the site as ``is_constructor_call=True`` with ``method_name`` set to the class's @@ -246,12 +246,33 @@ def scope_score(c: PyClass, _caller_sig: str = caller.signature) -> int: return resolved +def filter_external_edges( + edges: List[PyCallEdge], + symbol_table: Dict[str, PyModule], +) -> List[PyCallEdge]: + """Remove edges where both source and target are outside the app namespace. + + Edges where an app callable calls a library function (or vice-versa) are + retained; only lib→lib edges are dropped. The app symbol set is built by + walking every callable in the symbol table recursively (including nested + functions and closures via ``inner_callables``) plus every class, so + PyCG-discovered closure nodes are correctly recognised as app symbols. + """ + app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)} + app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table)) + + return [ + e for e in edges + if e.source in app_symbols or e.target in app_symbols + ] + + def merge_edges(*edge_lists: list) -> list: """Merge multiple ``List[PyCallEdge]`` into one. Edges with the same ``(source, target)`` are coalesced: weights sum, provenance is the sorted union. Useful for combining edges produced - by different backends (e.g. Jedi + CodeQL). + by different backends (e.g. Jedi + PyCG). """ by_key: Dict[Tuple[str, str], PyCallEdge] = {} for edges in edge_lists: diff --git a/codeanalyzer/semantic_analysis/codeql/codeql_analysis.py b/codeanalyzer/semantic_analysis/codeql/codeql_analysis.py deleted file mode 100644 index 0b93603..0000000 --- a/codeanalyzer/semantic_analysis/codeql/codeql_analysis.py +++ /dev/null @@ -1,382 +0,0 @@ -################################################################################ -# Copyright IBM Corporation 2025 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -################################################################################ - -"""CodeQL module for analyzing Python code using CodeQL. - -This module provides functionality to create and manage CodeQL databases -for Python projects and execute queries against them. -""" - -from collections import Counter -from pathlib import Path -from typing import Any, Dict, Iterator, List, Tuple, Union - -from pandas import DataFrame - -from codeanalyzer.schema.py_schema import PyCallEdge, PyModule -from codeanalyzer.semantic_analysis.call_graph import iter_callables_in_symbol_table -from codeanalyzer.semantic_analysis.codeql.codeql_query_runner import CodeQLQueryRunner -from codeanalyzer.utils import logger - - -class _CallableResolver: - """Maps a CodeQL endpoint ``(file, start_line, name, arity)`` to a Jedi - ``PyCallable``. - - Resolution ladder: - 1. exact ``(abs_path, start_line)`` — the precise join; - 2. on miss, candidates sharing ``(abs_path, short_name)``: a single - candidate is taken directly; otherwise prefer those whose - parameter count equals the CodeQL positional arity, then the - nearest ``start_line``; - 3. no name match -> ``None`` (caller row skipped / callee becomes - a ghost node). - - Step 2 recovers edges the ``(file, line)`` join silently drops when - CodeQL and Jedi disagree on a definition's start line (e.g. decorator - handling). Jedi's ``parameters`` counts every declared slot (incl. - ``*args``/``**kwargs``/keyword-only) whereas CodeQL's arity is - positional only, so the arity filter is exact for plain signatures - and otherwise yields to the nearest-line tiebreak. - """ - - def __init__(self) -> None: - self._by_loc: Dict[Tuple[str, int], Any] = {} - self._by_name: Dict[Tuple[str, str], List[Any]] = {} - - @staticmethod - def _abs(path: str) -> str: - try: - return str(Path(path).resolve()) - except (OSError, RuntimeError): - return path - - @classmethod - def from_symbol_table( - cls, symbol_table: Dict[str, PyModule] - ) -> "_CallableResolver": - resolver = cls() - for c in iter_callables_in_symbol_table(symbol_table): - abs_path = cls._abs(c.path) - resolver._by_loc[(abs_path, c.start_line)] = c - resolver._by_name.setdefault((abs_path, c.name), []).append(c) - return resolver - - def resolve( - self, file: str, start_line: int, name: str, arity: int - ) -> Any: - exact = self._by_loc.get((file, start_line)) - if exact is not None: - return exact - if not name: - return None - candidates = self._by_name.get((file, name)) - if not candidates: - return None - if len(candidates) == 1: - return candidates[0] - arity_matched = [c for c in candidates if len(c.parameters) == arity] - pool = arity_matched or candidates - return min(pool, key=lambda c: abs(c.start_line - start_line)) - - -class CodeQL: - """A class for building the application view of a Python application using CodeQL. - - Args: - project_dir (str or Path): The path to the root of the Python project. - - Attributes: - db_path (Path): The path to the CodeQL database. - temp_db (TemporaryDirectory or None): The temporary directory object if a temporary database was created. - """ - - def __init__( - self, - project_dir: Union[str, Path], - db_path: Path, - codeql_bin: Union[str, Path, None] = None, - codeql_packs_dir: Union[str, Path, None] = None, - ) -> None: - self.project_dir = project_dir - self.db_path = db_path - self.codeql_bin = codeql_bin - self.codeql_packs_dir = codeql_packs_dir - self._cached_df: "DataFrame | None" = None - - def _query_call_edges(self) -> DataFrame: - """Runs the CodeQL query that emits one row per resolved call site. - - The query is written against CodeQL's Python library (``import python``). - It returns physical location handles for both endpoints so the - downstream post-processor can join into Jedi's existing - ``PyCallable.signature`` space via ``(file_path, start_line)`` — - no signature normalization required. - - Filters: - * Caller must be a ``Function`` (skip module-level / class-body - calls — they have no ``PyCallable`` to anchor to). - * Callee may resolve to anything (in-source or library stub); - non-application callees become **ghost** nodes downstream so - RPC / third-party / framework edges are preserved. - - Returns: - DataFrame: one row per resolved (caller, callee, call-site) - triple. Duplicate ``(caller_file, caller_start_line, - callee_file, callee_start_line)`` tuples represent multiple - call sites in the same caller targeting the same callee and - are coalesced into a single ``PyCallEdge`` (weight = count) - by the post-processor. - """ - query = [ - "/**", - " * @name Python call-graph edges", - " * @description One row per resolved call site: caller, callee,", - " * and the call-expression location.", - " * @kind table", - " * @id py/codeanalyzer/call-graph-edges", - " */", - "import python", - # ``FunctionValue`` / ``ClassValue`` / the ``pointsTo`` predicate - # live in ObjectAPI, which ``import python`` only brings in as a - # private import — they aren't re-exported. Pull them in - # explicitly. - "import semmle.python.objects.ObjectAPI", - "", - # ``Value.getACall()`` is the modern call-resolution API in - # codeql/python-all 7.x — it returns the ``CallNode`` (CFG) - # whose target was resolved to that ``Value``. Cleaner than - # poking at ``pointsTo`` directly. - # ``callee`` is bound to the FunctionValue's scope so the - # endpoint emits the same Function-level facts (name, arity, - # location) the post-processor needs for the name+arity - # fallback when the (file, start_line) join misses. - "from CallNode call, Function caller, FunctionValue calleeVal, Function callee", - "where", - " call.getScope() = caller and", - " callee = calleeVal.getScope() and", - " (", - # Direct function / bound-method call: foo() or obj.foo() - " call = calleeVal.getACall()", - " or", - # Constructor call: A(...) resolves to a ClassValue; the actual - # callee is the class's __init__ (via MRO lookup so subclasses - # without an explicit __init__ still resolve to the inherited one). - " exists(ClassValue clsVal |", - " call = clsVal.getACall() and", - ' clsVal.lookup("__init__") = calleeVal', - " )", - " )", - "select", - # --- Caller endpoint --- (joins to PyCallable: exact by - # (file, start_line), else by (file, name) + arity) - " caller.getLocation().getFile().getAbsolutePath(),", - " caller.getLocation().getStartLine(),", - " caller.getQualifiedName(),", - " caller.getName(),", - " count(caller.getArg(_)),", - # --- Callee endpoint --- (file/line may live in a library stub; - # post-processor classifies as in-source or ghost) - " callee.getLocation().getFile().getAbsolutePath(),", - " callee.getLocation().getStartLine(),", - " calleeVal.getQualifiedName(),", - " callee.getName(),", - " count(callee.getArg(_)),", - # --- Call-site location --- (for PyCallsite augmentation) - " call.getLocation().getStartLine(),", - " call.getLocation().getStartColumn(),", - " call.getLocation().getEndLine(),", - " call.getLocation().getEndColumn()", - # ``is_constructor`` is derived in the post-processor by - # checking whether ``callee_qname`` ends in ``.__init__``; - # avoids QL's restrictive ``if-then-else`` typing here. - ] - if self._cached_df is not None: - return self._cached_df - - query_string = "\n".join(query) - - with CodeQLQueryRunner( - self.db_path, - codeql_bin=self.codeql_bin, - codeql_packs_dir=self.codeql_packs_dir, - ) as runner: - df: DataFrame = runner.execute( - query_string, - column_names=[ - "caller_file", - "caller_start_line", - "caller_qname", - "caller_name", - "caller_arity", - "callee_file", - "callee_start_line", - "callee_qname", - "callee_name", - "callee_arity", - "call_start_line", - "call_start_column", - "call_end_line", - "call_end_column", - ], - ) - self._cached_df = df - return df - - @staticmethod - def _build_callable_resolver( - symbol_table: Dict[str, PyModule], - ) -> _CallableResolver: - """Build the endpoint -> ``PyCallable`` resolver from Jedi. - - Paths are resolved so they match CodeQL's ``getAbsolutePath()`` - regardless of symlinks or the current working directory. - """ - return _CallableResolver.from_symbol_table(symbol_table) - - def _iter_resolved_rows( - self, symbol_table: Dict[str, PyModule] - ) -> "Iterator[Tuple[str, str, Any]]": - """Yield ``(source_sig, target_sig, row)`` for every CodeQL row. - - Rows whose caller can't be matched to a ``PyCallable`` in the - symbol table are skipped. Callee misses fall back to - ``row.callee_qname`` (ghost). Used by both edge construction and - call-site augmentation so a single CodeQL query feeds both. - """ - df = self._query_call_edges() - if df.empty: - return - resolver = self._build_callable_resolver(symbol_table) - - skipped_unknown_caller = 0 - ghost_callees = 0 - for row in df.itertuples(index=False): - caller = resolver.resolve( - row.caller_file, - int(row.caller_start_line), - row.caller_name, - int(row.caller_arity), - ) - if caller is None: - skipped_unknown_caller += 1 - continue - - callee = resolver.resolve( - row.callee_file, - int(row.callee_start_line), - row.callee_name, - int(row.callee_arity), - ) - if callee is not None: - target_sig = callee.signature - else: - target_sig = row.callee_qname - ghost_callees += 1 - - yield caller.signature, target_sig, row - - if skipped_unknown_caller: - logger.debug( - f"CodeQL: skipped {skipped_unknown_caller} rows whose caller " - f"was not in Jedi's symbol table." - ) - if ghost_callees: - logger.debug( - f"CodeQL: {ghost_callees} rows resolved to ghost (external) callees." - ) - - def build_call_graph_edges( - self, symbol_table: Dict[str, PyModule] - ) -> List[PyCallEdge]: - """Run the CodeQL query and turn each row into a ``PyCallEdge``. - - Edges are coalesced on ``(source, target)`` — ``weight`` is the - number of distinct call sites in the caller targeting the callee. - Provenance is always ``["codeql"]``; combine with Jedi-derived - edges via ``call_graph.merge_edges``. - """ - edge_counts: Counter = Counter() - for source_sig, target_sig, _row in self._iter_resolved_rows(symbol_table): - edge_counts[(source_sig, target_sig)] += 1 - - return [ - PyCallEdge( - source=src, - target=dst, - weight=count, - provenance=["codeql"], - ) - for (src, dst), count in edge_counts.items() - ] - - def augment_call_sites(self, symbol_table: Dict[str, PyModule]) -> int: - """Backfill ``PyCallsite.callee_signature`` using CodeQL resolution. - - Walks every CodeQL row, locates the matching ``PyCallsite`` inside - the caller's ``PyCallable.call_sites`` by call-expression line range - (``start_line``, ``end_line``), and fills in ``callee_signature`` - **only when Jedi left it empty**. Existing Jedi-resolved signatures - are kept (Jedi sees lexical context CodeQL can't, e.g. closures). - - Match is by line range — column matching is brittle across the two - tools' 0- vs 1-based conventions. Ambiguity on a single line - (e.g. ``a.b().c()``) resolves to the first matching site, which is - an acceptable approximation given how rarely Jedi misses callees - on chained call lines. - - Returns: - Number of ``PyCallsite`` entries augmented. - """ - resolver = self._build_callable_resolver(symbol_table) - df = self._query_call_edges() - if df.empty: - return 0 - - augmented = 0 - for row in df.itertuples(index=False): - caller = resolver.resolve( - row.caller_file, - int(row.caller_start_line), - row.caller_name, - int(row.caller_arity), - ) - if caller is None: - continue - - callee = resolver.resolve( - row.callee_file, - int(row.callee_start_line), - row.callee_name, - int(row.callee_arity), - ) - resolved_sig = callee.signature if callee is not None else row.callee_qname - - call_start = int(row.call_start_line) - call_end = int(row.call_end_line) - for site in caller.call_sites: - if site.start_line != call_start or site.end_line != call_end: - continue - if not site.callee_signature: - site.callee_signature = resolved_sig - augmented += 1 - break - - if augmented: - logger.debug( - f"CodeQL: augmented {augmented} PyCallsite.callee_signature entries." - ) - return augmented diff --git a/codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py b/codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py deleted file mode 100644 index 3232a46..0000000 --- a/codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py +++ /dev/null @@ -1,12 +0,0 @@ -class CodeQLExceptions: - class CodeQLDatabaseBuildException(Exception): - """Exception raised when there is an error building the CodeQL database.""" - - def __init__(self, message: str) -> None: - super().__init__(message) - - class CodeQLQueryExecutionException(Exception): - """Exception raised when there is an error building the CodeQL database.""" - - def __init__(self, message: str) -> None: - super().__init__(message) diff --git a/codeanalyzer/semantic_analysis/codeql/codeql_loader.py b/codeanalyzer/semantic_analysis/codeql/codeql_loader.py deleted file mode 100644 index dc95e0b..0000000 --- a/codeanalyzer/semantic_analysis/codeql/codeql_loader.py +++ /dev/null @@ -1,91 +0,0 @@ -import os -import platform -import stat -import zipfile -from pathlib import Path - -import requests -from codeanalyzer.utils import logger - - -class CodeQLLoader: - @classmethod - def detect_platform_key(cls) -> str: - system = platform.system() - arch = platform.machine().lower() - - if system == "Linux" and arch in {"x86_64", "amd64"}: - return "codeql-linux64.zip" - elif system == "Darwin" and arch in {"x86_64", "arm64"}: - return "codeql-osx64.zip" - elif system == "Windows" and arch in {"x86_64", "amd64"}: - return "codeql-win64.zip" - else: - return "codeql.zip" # fallback to generic binary if needed - - @classmethod - def get_codeql_download_url(cls, expected_filename: str) -> str: - response = requests.get( - "https://api.github.com/repos/github/codeql-cli-binaries/releases/latest" - ) - response.raise_for_status() - for asset in response.json()["assets"]: - if asset["name"] == expected_filename: - return asset["browser_download_url"] - raise RuntimeError(f"No asset found for filename: {expected_filename}") - - @classmethod - def download_and_extract_codeql(cls, temp_dir: Path) -> Path: - filename = cls.detect_platform_key() - download_url = cls.get_codeql_download_url(filename) - - temp_dir.mkdir(parents=True, exist_ok=True) - archive_path = temp_dir / filename - - logger.info(f"Downloading CodeQL CLI from {download_url}") - with requests.get(download_url, stream=True) as r: - r.raise_for_status() - block_size = 8192 # 8KB - - with open(archive_path, "wb") as f: - for chunk in r.iter_content(chunk_size=block_size): - f.write(chunk) - - extract_dir = temp_dir / filename.replace(".zip", "") - extract_dir.mkdir(exist_ok=True) - - logger.info(f"Extracting CodeQL CLI to {extract_dir}") - # zipfile.extractall drops Unix permissions (the executable bit), so - # we extract entries manually and copy each one's stored mode onto - # the file system. Without this, the CodeQL launcher script can't - # be executed and the next subprocess.Popen raises PermissionError. - with zipfile.ZipFile(archive_path, "r") as zip_ref: - for info in zip_ref.infolist(): - extracted_path = zip_ref.extract(info, extract_dir) - stored_mode = info.external_attr >> 16 - if stored_mode: - os.chmod(extracted_path, stored_mode) - - # Archive is no longer needed once extracted. - try: - archive_path.unlink() - except OSError as exc: - logger.warning(f"Could not remove CodeQL archive {archive_path}: {exc}") - - # rglob("codeql") returns both the launcher file *and* an internal - # directory of the same name (CodeQL ships its own runtime under - # ``codeql/codeql/``); insist on a regular file so we never bind to - # the directory. - codeql_bin = next( - (p for p in extract_dir.rglob("codeql") if p.is_file()), - None, - ) - if not codeql_bin: - raise FileNotFoundError("CodeQL binary not found in extracted contents.") - - # Belt-and-suspenders: ensure the binary is executable even if the - # archive entry's mode was zero (some older zip producers omit it). - st = codeql_bin.stat() - codeql_bin.chmod(st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - return codeql_bin.resolve() diff --git a/codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py b/codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py deleted file mode 100644 index 17eb368..0000000 --- a/codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py +++ /dev/null @@ -1,185 +0,0 @@ -################################################################################ -# Copyright IBM Corporation 2025 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -################################################################################ - -"""Backend module for CodeQL query execution. - -This module provides functionality to run CodeQL queries against CodeQL databases -and process the results. -""" - -import shlex -import subprocess -import tempfile -from pathlib import Path -from typing import List - -import pandas as pd -from pandas import DataFrame - -from codeanalyzer.semantic_analysis.codeql.codeql_exceptions import CodeQLExceptions - - -class CodeQLQueryRunner: - """A class for executing CodeQL queries against a CodeQL database. - - This class provides a context manager interface for executing CodeQL queries - and handling temporary resources needed during query execution. - - Args: - database_path (str): The path to the CodeQL database. - codeql_bin (str | Path | None): Absolute path to the CodeQL CLI - binary. When ``None``, falls back to whatever ``codeql`` is on - ``PATH``. - - Attributes: - database_path (Path): The path to the CodeQL database. - codeql_bin (str): Resolved binary path or the literal ``"codeql"``. - temp_file_path (Path): The path to the temporary query file. - csv_output_file (Path): The path to the CSV output file. - temp_bqrs_file_path (Path): The path to the temporary bqrs file. - temp_qlpack_file (Path): The path to the temporary qlpack file. - - Raises: - CodeQLQueryExecutionException: If there is an error executing the query. - """ - - def __init__(self, database_path: str, codeql_bin=None, codeql_packs_dir=None): - self.database_path: Path = Path(database_path) - self.codeql_bin: str = str(codeql_bin) if codeql_bin else "codeql" - self.codeql_packs_dir = ( - Path(codeql_packs_dir) if codeql_packs_dir is not None else None - ) - self.temp_file_path: Path = None - - def __enter__(self): - """Context entry that prepares paths to execute a CodeQL query. - - The ``.ql`` file is written **inside the prepared qlpack - directory** (``codeql_packs_dir``) so ``import python`` resolves - against that pack's installed dependencies — no - ``--additional-packs`` or ``--search-path`` needed. The CSV / - BQRS output files live in ``tempfile`` because they're transient - per-query artifacts. - """ - # CSV and BQRS files are transient per-query — fine in /tmp. - csv_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".csv") - bqrs_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".bqrs") - self.csv_output_file = Path(csv_file.name) - self.temp_bqrs_file_path = Path(bqrs_file.name) - csv_file.close() - bqrs_file.close() - - # The .ql file MUST live inside the prepared qlpack so its - # ``import python`` resolves via that pack's lock file. Writing - # outside the pack means CodeQL falls back to a default - # search-path that doesn't include downloaded library packs. - if self.codeql_packs_dir is None: - raise RuntimeError( - "CodeQLQueryRunner requires codeql_packs_dir — the directory " - "of an installed qlpack that depends on codeql/python-all." - ) - ql_file = tempfile.NamedTemporaryFile( - "w", delete=False, suffix=".ql", dir=str(self.codeql_packs_dir) - ) - self.temp_file_path = Path(ql_file.name) - ql_file.close() - - return self - - def execute(self, query_string: str, column_names: List[str]) -> DataFrame: - """Writes the query to the temporary file and executes it against the specified CodeQL database. - - Args: - query_string (str): The CodeQL query string to be executed. - column_names (List[str]): The list of column names for the CSV the CodeQL produces when we execute the query. - - Returns: - dict: A dictionary containing the resulting DataFrame. - - Raises: - RuntimeError: If the context manager is not entered using the 'with' statement. - CodeQLQueryExecutionException: If there is an error executing the query. - """ - if not self.temp_file_path: - raise RuntimeError("CodeQLQueryRunner not entered using 'with' statement.") - - # Write the query to the temp file so we can execute it. - self.temp_file_path.write_text(query_string) - - # The .ql file sits inside the qlpack directory whose lock file - # already resolves ``codeql/python-all`` and its transitive - # dependencies. ``codeql query run`` auto-discovers the enclosing - # qlpack — no extra flags required. - codeql_query_cmd = shlex.split( - f"{shlex.quote(self.codeql_bin)} query run {self.temp_file_path} " - f"--database={self.database_path} " - f"--output={self.temp_bqrs_file_path}", - posix=False, - ) - - call = subprocess.Popen( - codeql_query_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - _, err = call.communicate() - if call.returncode != 0: - raise CodeQLExceptions.CodeQLQueryExecutionException( - f"Error executing query: {(err or b'').decode(errors='replace')}" - ) - - # Convert the bqrs file to a CSV file - bqrs2csv_command = shlex.split( - f"{shlex.quote(self.codeql_bin)} bqrs decode --format=csv --output={self.csv_output_file} {self.temp_bqrs_file_path}", - posix=False, - ) - - # Read the CSV file content and cast it to a DataFrame - - call = subprocess.Popen( - bqrs2csv_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - _, err = call.communicate() - if call.returncode != 0: - raise CodeQLExceptions.CodeQLQueryExecutionException( - f"Error decoding bqrs: {(err or b'').decode(errors='replace')}" - ) - else: - return pd.read_csv( - self.csv_output_file, - header=None, - names=column_names, - skiprows=[0], - ) - - def __exit__(self, exc_type, exc_val, exc_tb): - """Clean up resources used by the CodeQL analysis. - - Args: - exc_type: The exception type if an exception was raised in the context, otherwise None. - exc_val: The exception instance if an exception was raised in the context, otherwise None. - exc_tb: The traceback if an exception was raised in the context, otherwise None. - - Note: - Deletes the temporary files created during the analysis, including the temporary file path, - the CSV output file, and the temporary QL pack file. - """ - if self.temp_file_path and self.temp_file_path.exists(): - self.temp_file_path.unlink() - - if self.csv_output_file and self.csv_output_file.exists(): - self.csv_output_file.unlink() - - if self.temp_bqrs_file_path and self.temp_bqrs_file_path.exists(): - self.temp_bqrs_file_path.unlink() diff --git a/codeanalyzer/semantic_analysis/pycg/__init__.py b/codeanalyzer/semantic_analysis/pycg/__init__.py new file mode 100644 index 0000000..4b66290 --- /dev/null +++ b/codeanalyzer/semantic_analysis/pycg/__init__.py @@ -0,0 +1,20 @@ +################################################################################ +# Copyright IBM Corporation 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +from codeanalyzer.semantic_analysis.pycg.pycg_analysis import PyCG +from codeanalyzer.semantic_analysis.pycg.pycg_exceptions import PyCGExceptions + +__all__ = ["PyCG", "PyCGExceptions"] diff --git a/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py b/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py new file mode 100644 index 0000000..e82c639 --- /dev/null +++ b/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py @@ -0,0 +1,1054 @@ +################################################################################ +# Copyright IBM Corporation 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""PyCG-based call graph construction for analysis level 2. + +PyCG (Apache-2.0, ICSE 2021) uses iterative inter-procedural name-pointer +analysis to produce a call graph with ~99% precision and ~69% recall on +micro-benchmarks. Its dotted namespace format (``module.Class.method``) +aligns directly with the ``PyCallable.signature`` space used by the symbol +table, so no name translation is needed for in-source callees. + +Callees not found in the symbol table are treated as ghost nodes — the same +convention used by :func:`call_graph.to_digraph`. + +**Sharding** (``shard=True``) runs PyCG independently per Python package +root instead of over the entire project. This keeps each shard under the +500-file ceiling by bounding PyCG's recursive import-following to the +package boundary. Cross-shard imports become ghost nodes (same quality as +Jedi-only edges for those call sites). Edge names are normalised back to +project-relative dotted paths so they align with the symbol table. +""" + +# Python 3.13 compatibility: PyCG installs a custom import hook and calls +# importlib.invalidate_caches() during analysis. In Python 3.13, that call +# triggers lazy loading of importlib.metadata → json → json.decoder, which +# re-enters PyCG's hook before its import graph is ready. Pre-importing +# these modules at import time ensures they're already in sys.modules when +# PyCG's hook is active, preventing the re-entrant ImportManagerError. +import importlib.metadata # noqa: F401 +import importlib.util # noqa: F401 +import contextlib +import json # noqa: F401 +import shutil +import signal +import tempfile +import time + +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Union + + +@contextlib.contextmanager +def _shard_timeout(seconds: int) -> Generator[None, None, None]: + """Context manager that raises ``TimeoutError`` if the body runs longer than *seconds*. + + Uses SIGALRM on POSIX (macOS / Linux). On platforms without SIGALRM + (Windows) the context manager is a no-op — shards can still be bounded + by the file-count ceiling. + + Must be called from the main thread (SIGALRM restriction). + """ + if seconds <= 0 or not hasattr(signal, "SIGALRM"): + yield + return + + def _handler(signum: int, frame: object) -> None: + raise TimeoutError(f"shard timed out after {seconds}s") + + old_handler = signal.signal(signal.SIGALRM, _handler) + signal.alarm(seconds) + try: + yield + finally: + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + +from codeanalyzer.schema.py_schema import PyCallEdge, PyModule +from codeanalyzer.semantic_analysis.call_graph import iter_callables_in_symbol_table +from codeanalyzer.semantic_analysis.pycg.pycg_exceptions import PyCGExceptions +from codeanalyzer.semantic_analysis.pycg.shard_planner import plan_shards +from codeanalyzer.utils import ProgressBar, logger + + +def _materialize_shard_root( + files: List[str], + project_dir: Path, +) -> Tuple[Path, List[str]]: + """Build a temporary symlink mini-project for a shard; return ``(root, eps)``. + + PyCG bounds its import-following to the ``package`` directory — only + modules whose resolved file lives under that root are followed; everything + else becomes a ghost node (``ImportManager``: ``if self.mod_dir not in + mod.__file__: return``). A coupling-derived shard is an arbitrary set of + files that need not form a directory, so we mirror the project layout into + a temp dir holding symlinks to exactly the shard's files plus the + ``__init__.py`` chain each needs for package resolution. Running PyCG with + this mirror as the package root confines analysis to the shard while + emitting project-relative edge names (so ``prefix=""`` — no rename needed). + + The caller owns the returned *root* and must ``shutil.rmtree`` it. + """ + root = Path(tempfile.mkdtemp(prefix="canpy_pycg_shard_")) + entry_points: List[str] = [] + linked_inits: Set[Path] = set() + for f in files: + src = Path(f).resolve() + try: + rel = src.relative_to(project_dir) + except ValueError: + continue # defensively skip files outside the project + dst = root / rel + dst.parent.mkdir(parents=True, exist_ok=True) + if not dst.exists(): + dst.symlink_to(src) + entry_points.append(str(dst)) + + # Symlink the __init__.py chain from project root down to this file's + # package so PyCG/importlib can resolve the dotted module name. These + # add ~0 analysis cost (usually empty) and keep out-of-shard siblings + # unresolved → ghost nodes. + for i in range(len(rel.parent.parts) + 1): + pkg_rel = Path(*rel.parent.parts[:i]) + real_init = project_dir / pkg_rel / "__init__.py" + link_init = root / pkg_rel / "__init__.py" + if real_init.exists() and link_init not in linked_inits: + link_init.parent.mkdir(parents=True, exist_ok=True) + if not link_init.exists(): + link_init.symlink_to(real_init.resolve()) + linked_inits.add(link_init) + return root, entry_points + + +@contextlib.contextmanager +def _shard_symlink_root( + files: List[str], + project_dir: Path, +) -> Generator[Tuple[Path, List[str]], None, None]: + """Context-manager wrapper around :func:`_materialize_shard_root`. + + Yields ``(root, entry_points)`` and removes the temp tree on exit. + """ + root, entry_points = _materialize_shard_root(files, project_dir) + try: + yield root, entry_points + finally: + shutil.rmtree(root, ignore_errors=True) + + +def _pycg_shard_worker( + entry_points: List[str], + package_dir: str, + prefix: str, + max_iter: int = -1, +) -> List[tuple]: + """Run PyCG on one shard; called in a Ray worker process. + + Returns a list of ``(source, target, weight)`` tuples that the caller + converts to :class:`PyCallEdge` objects. This function is a plain + module-level callable so it can be pickled by Ray without capturing any + class-level state. *max_iter* caps PyCG's fixpoint passes (-1 = unbounded). + """ + import importlib + import sys + + # Python 3.13 compatibility pre-imports (mirroring the top-level block). + import importlib.metadata # noqa: F401 + import importlib.util # noqa: F401 + import json # noqa: F401 + from collections import Counter as _WorkerCounter + + CallGraphGenerator = None + for pkg_name in ("pycg", "PyCG"): + try: + mod = importlib.import_module(pkg_name) + sys.modules.setdefault("pycg", mod) + sys.modules.setdefault("PyCG", mod) + pycg_mod = importlib.import_module(f"{pkg_name}.pycg") + CallGraphGenerator = pycg_mod.CallGraphGenerator + break + except ImportError: + continue + + if CallGraphGenerator is None: + raise RuntimeError("pycg is not installed in Ray worker — run `pip install pycg`") + + _apply_pycg_posonly_patch() + + cg = CallGraphGenerator( + entry_points=entry_points, + package=package_dir, + max_iter=max_iter, + operation="call-graph", + ) + cg.analyze() + + edge_counts = _WorkerCounter() + for src, dst in cg.output_edges(): + if prefix: + src = f"{prefix}.{src}" + dst = f"{prefix}.{dst}" + edge_counts[(src, dst)] += 1 + + return [(src, dst, count) for (src, dst), count in edge_counts.items()] + + +def _apply_pycg_posonly_patch() -> None: + """Monkey-patch PyCG's PreProcessor to handle Python 3.8+ positional-only params. + + PyCG's ``_get_fun_defaults`` computes the default-argument start index as + ``len(node.args.args) - len(node.args.defaults)``. In Python 3.8+, + ``node.args.defaults`` covers the LAST ``len(defaults)`` arguments of + ``posonlyargs + args`` combined, not just ``args``. When any positional- + only argument has a default (e.g. ``def f(a=1, b=2, /):``), the start + index becomes too negative, causing ``IndexError: list index out of range`` + during PyCG's pre-processing pass. + + This function replaces ``PreProcessor._get_fun_defaults`` with a corrected + implementation the first time it is called. Subsequent calls are no-ops. + """ + try: + import sys + preprocessor_mod = sys.modules.get("pycg.processing.preprocessor") \ + or sys.modules.get("PyCG.processing.preprocessor") + if preprocessor_mod is None: + import importlib + for pkg_name in ("pycg", "PyCG"): + try: + preprocessor_mod = importlib.import_module( + f"{pkg_name}.processing.preprocessor" + ) + break + except ImportError: + continue + if preprocessor_mod is None: + return + + PreProcessor = preprocessor_mod.PreProcessor + if getattr(PreProcessor, "_posonly_patched", False): + return + + def _patched_get_fun_defaults(self, node): # type: ignore[override] + defaults = {} + # Combine posonlyargs (Python 3.8+) with regular args so that the + # start index is computed over the full positional parameter list. + all_args = getattr(node.args, "posonlyargs", []) + node.args.args + start = len(all_args) - len(node.args.defaults) + for cnt, d in enumerate(node.args.defaults, start=start): + if not d: + continue + self.visit(d) + if 0 <= cnt < len(all_args): + defaults[all_args[cnt].arg] = self.decode_node(d) + + start = len(node.args.kwonlyargs) - len(node.args.kw_defaults) + for cnt, d in enumerate(node.args.kw_defaults, start=start): + if not d: + continue + self.visit(d) + if 0 <= cnt < len(node.args.kwonlyargs): + defaults[node.args.kwonlyargs[cnt].arg] = self.decode_node(d) + return defaults + + PreProcessor._get_fun_defaults = _patched_get_fun_defaults # type: ignore[method-assign] + PreProcessor._posonly_patched = True # type: ignore[attr-defined] + logger.debug("PyCG: applied positional-only-param default patch (Python 3.8+ fix)") + except Exception: + pass + + +def _import_pycg() -> Any: + """Import PyCG's CallGraphGenerator, trying both 'pycg' and 'PyCG' package names. + + The PyPI distribution installs as ``PyCG/`` (mixed case). Python's importer + is case-sensitive even on macOS HFS+, so we try both names and normalise + ``pycg`` in sys.modules so PyCG's own ``from pycg import utils`` resolves + regardless of which name the finder used first. + + Returns the ``CallGraphGenerator`` class. + Raises ``PyCGExceptions.PyCGImportError`` if neither name is importable. + """ + import importlib + import sys + + for pkg_name in ("pycg", "PyCG"): + try: + mod = importlib.import_module(pkg_name) + sys.modules.setdefault("pycg", mod) + sys.modules.setdefault("PyCG", mod) + pycg_mod = importlib.import_module(f"{pkg_name}.pycg") + return pycg_mod.CallGraphGenerator + except ImportError: + continue + + raise PyCGExceptions.PyCGImportError( + "pycg is not installed — run `pip install pycg`" + ) + + +class _PyCGCallableResolver: + """Maps a PyCG dotted namespace string to a ``PyCallable.signature``. + + PyCG names callables as ``module.Class.method`` relative to the package + root, which is identical to our ``PyCallable.signature`` format. A + direct dict lookup is therefore sufficient; this class exists to hold + the index and make the ghost-node fallback explicit. + """ + + def __init__(self, known: Set[str]) -> None: + self._known = known + + @classmethod + def from_symbol_table( + cls, symbol_table: Dict[str, PyModule] + ) -> "_PyCGCallableResolver": + known = {c.signature for c in iter_callables_in_symbol_table(symbol_table)} + return cls(known) + + def resolve(self, pycg_name: str) -> str: + """Return the canonical signature for *pycg_name*. + + If the name is in the symbol table it is returned verbatim. + Otherwise it is returned as-is so the edge is preserved as a + ghost (external / library) node in the call graph. + """ + return pycg_name + + +class PyCG: + """Thin wrapper around PyCG's ``CallGraphGenerator``. + + Args: + project_dir: Root of the Python project to analyse. + skip_tests: When ``True``, files whose path contains ``test`` or + ``conftest`` are excluded from the entry-point list. + shard: When ``True``, run PyCG independently per Python package + root instead of over the whole project. Required for projects + that exceed the 500-file ceiling. + shard_ceiling: Maximum file count per shard. Shards exceeding this + limit are skipped. Defaults to ``_PYCG_SHARD_CEILING`` (100). + shard_timeout: Per-shard wall-clock timeout in seconds. A shard that + exceeds this limit is skipped. 0 disables the timeout. Defaults + to ``_PYCG_SHARD_TIMEOUT`` (120). POSIX only; no-op on Windows. + """ + + # PyCG's pointer analysis is practical only up to this many files. + # Its per-iteration cost grows super-linearly; on very large projects + # even a single pass can take tens of minutes. + _PYCG_FILE_CEILING: int = 500 + + # Separate, tighter ceiling applied per shard in sharding mode. + # A shard covers one Python package root; PyCG follows imports only + # within that boundary. Even so, packages with deep class hierarchies + # or heavily interconnected imports can cause PyCG's pointer fixpoint + # to diverge well before the whole-project ceiling. 100 files is the + # conservative default; override via --pycg-shard-ceiling. + _PYCG_SHARD_CEILING: int = 100 + + # Per-shard wall-clock timeout (seconds). PyCG's fixpoint is bimodal: + # either it converges in seconds or it diverges and never finishes. + # This timeout acts as a final safety net after the file-count ceiling. + # 120 seconds is generous enough for any legitimately complex shard + # while still catching non-converging ones. Override via + # --pycg-shard-timeout. Set to 0 to disable. + _PYCG_SHARD_TIMEOUT: int = 120 + + # Cap on PyCG's outer fixpoint passes. PyCG runs PostProcessor until the + # def/scope/MRO state stops changing; its abstract domain (field-sensitive + # access paths, no k-limiting or widening) has no ascending-chain bound, so + # on heavy metaclass/mixin code (e.g. an ORM) the def set can balloon into + # the thousands and each O(defs^2) pass costs seconds — convergence, if it + # comes, takes many passes. A finite cap turns "loop until killed" into a + # sound-but-incomplete result that still returns the edges found so far. + # 50 is generous — well-behaved code converges in well under 20 passes — + # while bounding the pathological case. Override via --pycg-max-iter; + # -1 restores PyCG's unbounded run-to-convergence behaviour. + _PYCG_MAX_ITER: int = 50 + + # Iterative decomposition of runaway (timed-out) shards: a shard that the + # wall-clock timeout kills is re-partitioned at half the budget and re-run, + # down to this file-count floor. Below the floor — or for an atomic import + # cycle that won't split — the residue falls back to Jedi-only coverage. + _PYCG_DECOMP_FLOOR: int = 10 + _PYCG_MAX_DECOMP_ROUNDS: int = 6 + + # Directory names that should never be fed to PyCG as entry points, nor + # followed into during import resolution (an in-tree .codeanalyzer venv / + # site-packages lives under project_dir and would otherwise be pulled into + # the package bound and analysed — see _shard_symlink_root). + _SKIP_DIRS: frozenset = frozenset({ + ".codeanalyzer", ".git", "__pycache__", + "venv", ".venv", "virtualenv", "env", ".env", + "node_modules", "dist", "build", ".tox", ".nox", + "site-packages", + }) + + def __init__( + self, + project_dir: Union[str, Path], + skip_tests: bool = True, + shard: bool = False, + shard_ceiling: Optional[int] = None, + shard_timeout: Optional[int] = None, + shard_strategy: str = "jedi", + max_iter: Optional[int] = None, + using_ray: bool = False, + ) -> None: + self.project_dir = Path(project_dir).resolve() + self.skip_tests = skip_tests + self.shard = shard + self.shard_ceiling = ( + shard_ceiling if shard_ceiling is not None else self._PYCG_SHARD_CEILING + ) + self.shard_timeout = ( + shard_timeout if shard_timeout is not None else self._PYCG_SHARD_TIMEOUT + ) + self.max_iter = max_iter if max_iter is not None else self._PYCG_MAX_ITER + # "jedi": partition the Jedi module graph (SCC + Louvain) so coupled + # modules co-compute and few edges are severed (see shard_planner). + # "package": legacy one-shard-per-package-directory grouping. + self.shard_strategy = shard_strategy + self.using_ray = using_ray + self._CallGraphGenerator: Optional[Any] = None + self._resolver: Optional["_PyCGCallableResolver"] = None + + @staticmethod + def _coalesce_edges(edges: List[PyCallEdge]) -> List[PyCallEdge]: + """Sum weights of duplicate ``(source, target)`` pairs across shards.""" + merged: Dict[tuple, PyCallEdge] = {} + for edge in edges: + key = (edge.source, edge.target) + if key in merged: + existing = merged[key] + merged[key] = PyCallEdge( + source=existing.source, + target=existing.target, + weight=existing.weight + edge.weight, + provenance=existing.provenance, + ) + else: + merged[key] = edge + return list(merged.values()) + + # ------------------------------------------------------------------ + # Entry-point collection + # ------------------------------------------------------------------ + + def _collect_entry_points(self) -> List[str]: + """Return absolute paths of project Python files, excluding caches and venvs.""" + paths = [] + for p in self.project_dir.rglob("*.py"): + # Skip any file whose path passes through a filtered directory. + if any(part in self._SKIP_DIRS for part in p.parts): + continue + # Skip test files using exact path-component matching, consistent + # with core.py's _build_symbol_table filter. Substring matching + # (e.g. "/test" in full_path_str) incorrectly excludes files in + # paths like "test/fixtures/..." that are source files, not tests. + rel_parts = p.relative_to(self.project_dir).parts + if self.skip_tests and ( + "test" in rel_parts + or "tests" in rel_parts + or p.stem.startswith("test_") + or p.name.endswith("_test.py") + or p.name == "conftest.py" + ): + continue + paths.append(str(p)) + return paths + + # ------------------------------------------------------------------ + # Package-root helpers for sharding + # ------------------------------------------------------------------ + + @staticmethod + def _find_package_root(file_path: Path, project_dir: Path) -> Path: + """Return the top-level Python package directory that owns *file_path*. + + Walks upward from the file's directory toward *project_dir*, returning + the highest ancestor that still contains an ``__init__.py``. Files + at the project root (no ``__init__.py`` in any parent) are placed in + a shard rooted at *project_dir* itself. + + Examples:: + + project/addons/account/models/res.py → project/addons/account/ + project/src/flask/app.py → project/src/flask/ + project/standalone_script.py → project/ + """ + package_root = file_path.parent + current = file_path.parent + while current != project_dir: + if not (current / "__init__.py").exists(): + break + package_root = current + current = current.parent + return package_root + + @staticmethod + def _package_prefix(pkg_root: Path, project_dir: Path) -> str: + """Dot-separated path from *project_dir* to *pkg_root*. + + This prefix is prepended to PyCG's package-relative edge names so + they become project-relative and align with the symbol table:: + + pkg_root = project/addons/account/ → "addons.account" + pkg_root = project/src/flask/ → "src.flask" + pkg_root = project/ → "" (no prefix needed) + """ + rel = pkg_root.relative_to(project_dir) + return ".".join(rel.parts) + + # ------------------------------------------------------------------ + # Core PyCG runner + # ------------------------------------------------------------------ + + def _ensure_pycg_loaded(self) -> None: + """Import PyCG and apply compatibility patches (idempotent).""" + if self._CallGraphGenerator is not None: + return + self._CallGraphGenerator = _import_pycg() + # Python 3.8+ positional-only-param fix and Python 3.13 import-hook fix. + _apply_pycg_posonly_patch() + + def _run_pycg_batch( + self, + entry_points: List[str], + package_dir: Path, + resolver: "_PyCGCallableResolver", + prefix: str = "", + ) -> List[PyCallEdge]: + """Run PyCG on *entry_points* with *package_dir* as the package root. + + *prefix* is a dot-separated path prepended to every edge name emitted + by PyCG so that shard-relative names become project-relative. Pass + ``""`` when *package_dir* is the project root (names already match). + + Raises ``PyCGExceptions.PyCGAnalysisError`` on any PyCG failure. + """ + assert self._CallGraphGenerator is not None + try: + cg = self._CallGraphGenerator( + entry_points=entry_points, + package=str(package_dir), + max_iter=self.max_iter, + operation="call-graph", + ) + cg.analyze() + except TimeoutError: + raise # propagate directly so _build_sharded logs a clean timeout message + except Exception as exc: + raise PyCGExceptions.PyCGAnalysisError( + f"PyCG analysis failed: {exc}" + ) from exc + + edge_counts: Counter = Counter() + for src, dst in cg.output_edges(): + if prefix: + src = f"{prefix}.{src}" + dst = f"{prefix}.{dst}" + edge_counts[(resolver.resolve(src), resolver.resolve(dst))] += 1 + + return [ + PyCallEdge(source=src, target=dst, weight=count, provenance=["pycg"]) + for (src, dst), count in edge_counts.items() + ] + + # ------------------------------------------------------------------ + # Sharded analysis + # ------------------------------------------------------------------ + + def _build_sharded_planned( + self, + jedi_edges: List[PyCallEdge], + symbol_table: Dict[str, PyModule], + resolver: "_PyCGCallableResolver", + ) -> List[PyCallEdge]: + """Coupling-aware sharding with iterative decomposition of runaways. + + Shards are chosen to *minimise the call edges severed between shards*: + :func:`shard_planner.plan_shards` condenses the Jedi call graph by + strongly-connected component (so import cycles never split) and clusters + it with Louvain so tightly-coupled modules land together. Each shard is + run through PyCG via a symlinked mini-project that bounds analysis to its + files. + + PyCG's fixpoint diverges on heavy metaclass/mixin clusters, and a uniform + ceiling would force *every* shard small (severing many edges) just to tame + the few that run away. Instead we start coarse (low cut, high recall on + healthy code) and **only re-decompose the shards that time out**: each + runaway's files are re-partitioned at half the budget and re-run, down to + a floor. A runaway shard contributes zero edges, so splitting it recovers + almost all of them while paying cut on its internal seams alone. The + residue that still diverges at the floor (or is an atomic cycle that won't + split) falls back to Jedi-only coverage. + """ + self._resolver = resolver + plan = plan_shards( + symbol_table, jedi_edges, budget=self.shard_ceiling, merge_small=True + ) + m = plan.metrics + logger.info( + "PyCG: planned %d shard(s) from Jedi module graph " + "(cut_ratio=%.3f, max_shard=%d files, %d modules)", + int(m["num_shards"]), m["cut_ratio"], + int(m["max_shard_files"]), int(m["modules"]), + ) + + runner = ( + self._run_fileset_shards_ray if self.using_ray + else self._run_fileset_shards_seq + ) + all_edges: List[PyCallEdge] = [] + shards = plan.shards + budget = self.shard_ceiling + converged_total = 0 + irreducible_files = 0 + round_no = 0 + + while shards: + label = "decomposition round %d (budget %d, %d shard(s))" % ( + round_no, budget, len(shards), + ) + logger.info("PyCG: %s", label) + edges, runaways = runner(shards) + all_edges.extend(edges) + converged_total += len(shards) - len(runaways) + if not runaways: + break + + next_budget = max(self._PYCG_DECOMP_FLOOR, budget // 2) + stop_decomposing = ( + round_no >= self._PYCG_MAX_DECOMP_ROUNDS or next_budget >= budget + ) + + next_shards: List[List[str]] = [] + for rf in runaways: + # Re-partition this runaway's files alone, at a tighter budget. + # An atomic cycle (or a lone file) that won't shrink is + # irreducible — accept Jedi-only rather than loop forever. + sub_st = {f: symbol_table[f] for f in rf if f in symbol_table} + if stop_decomposing or len(rf) <= 1: + irreducible_files += len(rf) + continue + sub_plan = plan_shards(sub_st, jedi_edges, budget=next_budget) + if len(sub_plan.shards) <= 1: + # did not actually split (one atomic SCC) — give up on it + irreducible_files += len(rf) + continue + next_shards.extend(sub_plan.shards) + + if not next_shards: + break + logger.info( + "PyCG: %d shard(s) ran away — decomposing into %d sub-shard(s) " + "at budget %d", len(runaways), len(next_shards), next_budget, + ) + shards, budget = next_shards, next_budget + round_no += 1 + + if irreducible_files: + logger.warning( + "PyCG: %d file(s) in irreducibly-divergent shards fall back to " + "Jedi-only coverage", irreducible_files, + ) + + result = self._coalesce_edges(all_edges) + logger.info( + "PyCG: %d edges from %d converged shard(s) over %d round(s) " + "(%d before dedup, Jedi-planned%s)", + len(result), converged_total, round_no + 1, len(all_edges), + ", Ray-parallel" if self.using_ray else "", + ) + return result + + def _run_fileset_shards_seq( + self, shards: List[List[str]], + ) -> Tuple[List[PyCallEdge], List[List[str]]]: + """Run each file-set shard sequentially; return ``(edges, runaways)``. + + A shard that times out or raises is returned in *runaways* (its file + list) for the caller to re-decompose; it contributes no edges. + """ + resolver = self._resolver + edges_all: List[PyCallEdge] = [] + runaways: List[List[str]] = [] + with ProgressBar(len(shards), "Building call graph shards", item_label="shards") as progress: + for files in shards: + try: + with _shard_symlink_root(files, self.project_dir) as (root, eps): + with _shard_timeout(self.shard_timeout): + edges = self._run_pycg_batch(eps, root, resolver, prefix="") + edges_all.extend(edges) + except (TimeoutError, PyCGExceptions.PyCGAnalysisError): + runaways.append(files) + progress.advance() + return edges_all, runaways + + def _run_fileset_shards_ray( + self, shards: List[List[str]], + ) -> Tuple[List[PyCallEdge], List[List[str]]]: + """Ray-parallel variant of :meth:`_run_fileset_shards_seq`. + + Each shard is materialised as a symlink mini-project up front (the trees + must outlive their remote tasks), submitted as a Ray task, and collected + against one wall-clock deadline — Ray workers cannot use SIGALRM, so the + timeout is enforced orchestrator-side. Timed-out/failed shards become + runaways; symlink trees are removed once the batch completes. + """ + import os + import ray + + os.environ.setdefault("RAY_IGNORE_UNHANDLED_ERRORS", "1") + remote_fn = ray.remote(_pycg_shard_worker) + + roots: List[Path] = [] + futures: List[Any] = [] + meta: Dict[Any, List[str]] = {} # ObjectRef -> shard file list + edges_all: List[PyCallEdge] = [] + runaways: List[List[str]] = [] + try: + with ProgressBar(len(shards), "Building call graph shards (parallel)", item_label="shards") as progress: + for files in shards: + root, eps = _materialize_shard_root(files, self.project_dir) + roots.append(root) + fut = remote_fn.remote(eps, str(root), "", self.max_iter) + futures.append(fut) + meta[fut] = files + + deadline = ( + time.perf_counter() + float(self.shard_timeout) + if self.shard_timeout > 0 else None + ) + pending = list(futures) + while pending: + if deadline is not None: + remaining = deadline - time.perf_counter() + if remaining <= 0: + break + else: + remaining = None + + ready, pending = ray.wait(pending, num_returns=1, timeout=remaining) + if not ready: + break + + fut = ready[0] + try: + triples = ray.get(fut) + edges_all.extend( + PyCallEdge(source=s, target=t, weight=w, provenance=["pycg"]) + for s, t, w in triples + ) + except Exception: + runaways.append(meta[fut]) + progress.advance() + + for fut in pending: # exceeded the deadline + ray.cancel(fut, force=True) + runaways.append(meta[fut]) + progress.advance() + finally: + for root in roots: + shutil.rmtree(root, ignore_errors=True) + return edges_all, runaways + + def _build_sharded( + self, + entry_points: List[str], + resolver: "_PyCGCallableResolver", + ) -> List[PyCallEdge]: + """Run PyCG per Python package shard and merge the results. + + Groups entry points by their top-level package root. Each shard + whose size is within ``self.shard_ceiling`` is analysed independently + with its package directory as the PyCG ``package`` root, which limits + recursive import-following to that package boundary. Shards that + exceed the shard ceiling are skipped with a warning (framework modules + with deep mixin hierarchies can cause PyCG's fixpoint to diverge). + + Edge names are normalised to project-relative dotted paths so they + match the symbol table's ``PyCallable.signature`` namespace. + """ + shards: Dict[Path, List[str]] = defaultdict(list) + for ep in entry_points: + pkg_root = self._find_package_root(Path(ep), self.project_dir) + shards[pkg_root].append(ep) + + logger.debug( + "PyCG: sharding %d files into %d package shard(s)", + len(entry_points), len(shards), + ) + + if self.using_ray: + return self._build_sharded_ray(shards) + + all_edges: List[PyCallEdge] = [] + skipped = 0 + with ProgressBar(len(shards), "Building call graph shards", item_label="shards") as progress: + for pkg_root, files in shards.items(): + n = len(files) + pkg_label = str(pkg_root.relative_to(self.project_dir)) or "." + if n > self.shard_ceiling: + logger.warning( + "PyCG shard '%s': %d files exceeds shard ceiling of %d — skipped", + pkg_label, n, self.shard_ceiling, + ) + skipped += 1 + progress.advance() + continue + prefix = self._package_prefix(pkg_root, self.project_dir) + try: + with _shard_timeout(self.shard_timeout): + edges = self._run_pycg_batch(files, pkg_root, resolver, prefix=prefix) + all_edges.extend(edges) + logger.debug( + "PyCG shard '%s': %d edges from %d files", + pkg_label, len(edges), n, + ) + except TimeoutError: + logger.warning( + "PyCG shard '%s' timed out after %ds — skipped", + pkg_label, self.shard_timeout, + ) + skipped += 1 + except PyCGExceptions.PyCGAnalysisError as exc: + logger.warning("PyCG shard '%s' failed — skipped: %s", pkg_label, exc) + skipped += 1 + progress.advance() + + if skipped: + logger.warning( + "PyCG: %d shard(s) were skipped (exceeded %d-file ceiling, " + "%ds timeout, or failed)", + skipped, self.shard_ceiling, self.shard_timeout, + ) + + # Merge duplicate (source, target) pairs that appear in multiple shards. + merged: Dict[tuple, PyCallEdge] = {} + for edge in all_edges: + key = (edge.source, edge.target) + if key in merged: + existing = merged[key] + merged[key] = PyCallEdge( + source=existing.source, + target=existing.target, + weight=existing.weight + edge.weight, + provenance=existing.provenance, + ) + else: + merged[key] = edge + + result = list(merged.values()) + logger.info( + "PyCG: %d edges from %d/%d shard(s) (%d before dedup)", + len(result), len(shards) - skipped, len(shards), len(all_edges), + ) + return result + + def _build_sharded_ray(self, shards: Dict[Path, List[str]]) -> List[PyCallEdge]: + """Ray-parallel variant of the sequential shard loop. + + All eligible shards are submitted as Ray remote tasks simultaneously. + ``ray.wait(timeout=shard_timeout)`` is used to collect results and + cancel stragglers — Ray workers cannot use SIGALRM, so the timeout is + enforced at the orchestrator level instead. + """ + import os + import ray + + # force-cancel kills worker processes; suppress Ray's "worker died + # unexpectedly" noise since the death is intentional here. + os.environ.setdefault("RAY_IGNORE_UNHANDLED_ERRORS", "1") + + remote_fn = ray.remote(_pycg_shard_worker) + futures: List[Any] = [] + meta: Dict[Any, tuple] = {} # ObjectRef -> (pkg_label, n_files) + skipped = 0 + + all_edges: List[PyCallEdge] = [] + with ProgressBar(len(shards), "Building call graph shards (parallel)", item_label="shards") as progress: + for pkg_root, files in shards.items(): + n = len(files) + pkg_label = str(pkg_root.relative_to(self.project_dir)) or "." + if n > self.shard_ceiling: + logger.warning( + "PyCG shard '%s': %d files exceeds shard ceiling of %d — skipped", + pkg_label, n, self.shard_ceiling, + ) + skipped += 1 + progress.advance() + continue + prefix = self._package_prefix(pkg_root, self.project_dir) + fut = remote_fn.remote(files, str(pkg_root), prefix, self.max_iter) + futures.append(fut) + meta[fut] = (pkg_label, n) + + # Collect results one shard at a time so the progress bar ticks per + # completed shard. A single deadline governs the whole batch: tasks + # submitted simultaneously all have the same wall-clock budget. + deadline = ( + time.perf_counter() + float(self.shard_timeout) + if self.shard_timeout > 0 else None + ) + pending = list(futures) + while pending: + if deadline is not None: + remaining = deadline - time.perf_counter() + if remaining <= 0: + break + else: + remaining = None + + ready, pending = ray.wait(pending, num_returns=1, timeout=remaining) + if not ready: + break # deadline reached before any new result + + fut = ready[0] + pkg_label, n = meta[fut] + try: + triples = ray.get(fut) + edges = [ + PyCallEdge(source=s, target=t, weight=w, provenance=["pycg"]) + for s, t, w in triples + ] + all_edges.extend(edges) + logger.debug( + "PyCG shard '%s': %d edges from %d files (Ray)", + pkg_label, len(edges), n, + ) + except Exception as exc: + logger.warning("PyCG shard '%s' failed — skipped: %s", pkg_label, exc) + skipped += 1 + progress.advance() + + # Cancel any shards that did not complete before the deadline. + for fut in pending: + pkg_label, _ = meta[fut] + logger.warning( + "PyCG shard '%s' timed out after %ds — skipped", + pkg_label, self.shard_timeout, + ) + ray.cancel(fut, force=True) + skipped += 1 + progress.advance() + + if skipped: + logger.warning( + "PyCG: %d shard(s) were skipped (exceeded %d-file ceiling, " + "%ds timeout, or failed)", + skipped, self.shard_ceiling, self.shard_timeout, + ) + + merged: Dict[tuple, PyCallEdge] = {} + for edge in all_edges: + key = (edge.source, edge.target) + if key in merged: + existing = merged[key] + merged[key] = PyCallEdge( + source=existing.source, + target=existing.target, + weight=existing.weight + edge.weight, + provenance=existing.provenance, + ) + else: + merged[key] = edge + + result = list(merged.values()) + logger.info( + "PyCG: %d edges from %d/%d shard(s) (%d before dedup, Ray-parallel)", + len(result), len(shards) - skipped, len(shards), len(all_edges), + ) + return result + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def build_call_graph_edges( + self, + symbol_table: Dict[str, PyModule], + jedi_edges: Optional[List[PyCallEdge]] = None, + ) -> List[PyCallEdge]: + """Run PyCG and return ``PyCallEdge`` entries with ``provenance=["pycg"]``. + + Edges are coalesced on ``(source, target)`` — ``weight`` equals the + number of times PyCG reports the same (caller, callee) pair (always 1 + per unique pair in PyCG's output). Ghost callees (not in the symbol + table) are preserved so external / library edges appear in the graph. + + Returns an empty list and logs a warning if pycg is not installed or + if the analysis raises an unexpected exception. + + When ``self.shard=True`` and the project exceeds the 500-file ceiling, + PyCG is run per Python package root (see :meth:`_build_sharded`). + When ``self.shard=False`` and the project exceeds the ceiling, PyCG is + skipped and an empty list is returned (Jedi-only fallback). + """ + try: + self._ensure_pycg_loaded() + except PyCGExceptions.PyCGImportError: + raise + + entry_points = self._collect_entry_points() + if not entry_points: + logger.debug("PyCG: no Python files found under %s", self.project_dir) + return [] + + n_files = len(entry_points) + resolver = _PyCGCallableResolver.from_symbol_table(symbol_table) + t0 = time.perf_counter() + + if n_files > self._PYCG_FILE_CEILING: + if self.shard: + if self.shard_strategy == "jedi" and jedi_edges is not None: + logger.info( + "PyCG: starting Jedi-planned sharded analysis (%d files)", + n_files, + ) + edges = self._build_sharded_planned( + jedi_edges, symbol_table, resolver + ) + else: + mode = "Ray-parallel" if self.using_ray else "sequential" + logger.info( + "PyCG: starting per-package sharded analysis (%d files, %s)", + n_files, mode, + ) + edges = self._build_sharded(entry_points, resolver) + else: + logger.warning( + "PyCG: %d entry points exceeds ceiling of %d — " + "skipping pointer analysis (Jedi-only edges will be used). " + "Re-run with --pycg-shard to analyse per package shard.", + n_files, self._PYCG_FILE_CEILING, + ) + return [] + else: + # Small project (≤ ceiling): whole-project analysis. Run inside a + # symlink mini-project mirroring only the (already SKIP_DIRS-filtered) + # entry points, so PyCG's package bound covers project source alone. + # Pointing PyCG at project_dir directly would put an in-tree + # .codeanalyzer venv / site-packages *under* mod_dir, and PyCG would + # follow imports into those dependencies and explode the analysis. + logger.info("PyCG: starting whole-project call graph analysis (%d files)", n_files) + with _shard_symlink_root(entry_points, self.project_dir) as (root, eps): + edges = self._run_pycg_batch(eps, root, resolver, prefix="") + + elapsed = time.perf_counter() - t0 + logger.info("✅ PyCG: %d edges in %.1fs", len(edges), elapsed) + return edges diff --git a/codeanalyzer/semantic_analysis/codeql/__init__.py b/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py similarity index 72% rename from codeanalyzer/semantic_analysis/codeql/__init__.py rename to codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py index c4bfd97..c469ae8 100644 --- a/codeanalyzer/semantic_analysis/codeql/__init__.py +++ b/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py @@ -14,13 +14,10 @@ # limitations under the License. ################################################################################ -""" -CodeQL package -""" -from .codeql_analysis import CodeQL -from .codeql_exceptions import CodeQLExceptions -from .codeql_loader import CodeQLLoader -from .codeql_query_runner import CodeQLQueryRunner +class PyCGExceptions: + class PyCGAnalysisError(Exception): + """Raised when PyCG fails to analyze a project.""" -__all__ = ["CodeQL", "CodeQLQueryRunner", "CodeQLLoader", "CodeQLExceptions"] + class PyCGImportError(ImportError): + """Raised when the pycg package is not installed.""" diff --git a/codeanalyzer/semantic_analysis/pycg/shard_planner.py b/codeanalyzer/semantic_analysis/pycg/shard_planner.py new file mode 100644 index 0000000..a1608dc --- /dev/null +++ b/codeanalyzer/semantic_analysis/pycg/shard_planner.py @@ -0,0 +1,401 @@ +################################################################################ +# Copyright IBM Corporation 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +"""Coupling-aware shard planning for PyCG call-graph construction. + +Sharding splits a project so PyCG can be run on each part independently; any +call edge whose caller and callee land in *different* shards becomes a ghost +node in both, so PyCG never resolves it. The directory + file-count heuristic +is blind to coupling and can sever heavily-interacting modules. This planner +instead partitions the **module dependency graph derived from the Jedi call +graph** (already computed at analysis level 1), so the cuts fall on the weakest +seams. + +Pipeline: + +1. **Module graph** — project the Jedi ``PyCallEdge`` list (callable→callable) + down to a weighted directed graph over *modules*. ``weight(A, B)`` is the + number of Jedi call sites from a callable in module ``A`` to one in ``B``. +2. **SCC condensation** — modules in an import/call cycle must be co-computed + (splitting them breaks both shards), so each strongly-connected component is + collapsed into one indivisible unit via Tarjan's algorithm. +3. **Community detection** — Louvain modularity over the undirected weighted + condensation groups tightly-coupled units; each community is a candidate + shard. +4. **Budget enforcement** — communities over the per-shard file budget are + re-partitioned (higher Louvain resolution, then a greedy first-fit fallback + that guarantees termination); communities under budget are agglomeratively + merged by inter-shard weight to recover cut edges and reduce shard count. + +The result is a list of shards (each a list of file paths) plus a metrics +report whose headline figure is ``cut_ratio`` — the fraction of inter-module +Jedi edge weight severed by the partition. Lower is better; it is the +estimated upper bound on PyCG edges lost to sharding. +""" + +from __future__ import annotations + +import logging +from collections import defaultdict +from dataclasses import dataclass, field +from typing import Dict, Iterator, List, Optional, Set, Tuple + +import networkx as nx + +from codeanalyzer.schema.py_schema import PyCallable, PyCallEdge, PyClass, PyModule + +logger = logging.getLogger(__name__) + + +# ---------------------------------------------------------------------------- +# Symbol-table walking: callable / class signature -> defining file +# ---------------------------------------------------------------------------- + +def _walk_callable_sigs(c: PyCallable) -> Iterator[str]: + yield c.signature + for inner in c.inner_callables.values(): + yield from _walk_callable_sigs(inner) + for inner_cls in c.inner_classes.values(): + yield from _walk_class_sigs(inner_cls) + + +def _walk_class_sigs(cls: PyClass) -> Iterator[str]: + yield cls.signature + for method in cls.methods.values(): + yield from _walk_callable_sigs(method) + for inner in cls.inner_classes.values(): + yield from _walk_class_sigs(inner) + + +def _signature_to_file(symbol_table: Dict[str, PyModule]) -> Dict[str, str]: + """Map every callable/class signature in the project to its defining file. + + Built by walking each module's full nesting tree, recording the file that + actually defines each callable. Files are the partition unit because + ``PyModule.module_name`` is only the file *stem* (``py_file.stem``), which + collides heavily across a real project (every ``__init__.py``, ``models.py`` + …) — keying the module graph by name would collapse unrelated files into + one node and silently drop files from shards. ``file_path`` is unique. + """ + sig_to_file: Dict[str, str] = {} + for module in symbol_table.values(): + for fn in module.functions.values(): + for sig in _walk_callable_sigs(fn): + sig_to_file[sig] = module.file_path + for cls in module.classes.values(): + for sig in _walk_class_sigs(cls): + sig_to_file[sig] = module.file_path + return sig_to_file + + +# ---------------------------------------------------------------------------- +# Result type +# ---------------------------------------------------------------------------- + +@dataclass +class ShardPlan: + """Output of :func:`plan_shards`. + + ``shards`` is what the PyCG executor consumes: each inner list is the set of + project file paths to analyse together as one shard. ``module_shards`` is + the same partition expressed in module names (handy for logging/tests). + """ + + shards: List[List[str]] = field(default_factory=list) + module_shards: List[List[str]] = field(default_factory=list) + metrics: Dict[str, float] = field(default_factory=dict) + + def __str__(self) -> str: + m = self.metrics + return ( + f"ShardPlan({len(self.shards)} shards, " + f"cut_ratio={m.get('cut_ratio', 0):.3f}, " + f"max_shard_files={int(m.get('max_shard_files', 0))}, " + f"oversized={int(m.get('oversized_shards', 0))})" + ) + + +# ---------------------------------------------------------------------------- +# Module dependency graph +# ---------------------------------------------------------------------------- + +def build_module_graph( + symbol_table: Dict[str, PyModule], + jedi_edges: List[PyCallEdge], +) -> nx.DiGraph: + """Project Jedi callable→callable edges onto a weighted file DiGraph. + + Nodes are file paths (the unique, collision-free partition unit — see + :func:`_signature_to_file`); each carries a ``module_name`` attribute for + readable reporting. Every project file is a node (isolated files + included). Edge weight is the summed Jedi weight of cross-file call sites; + intra-file edges and edges touching external/library symbols (no + symbol-table entry) are dropped — they cannot influence the partition. + """ + sig_to_file = _signature_to_file(symbol_table) + + g = nx.DiGraph() + for module in symbol_table.values(): + g.add_node(module.file_path, module_name=module.module_name) + + for edge in jedi_edges: + src = sig_to_file.get(edge.source) + dst = sig_to_file.get(edge.target) + if src is None or dst is None or src == dst: + continue + if g.has_edge(src, dst): + g[src][dst]["weight"] += edge.weight + else: + g.add_edge(src, dst, weight=edge.weight) + return g + + +# ---------------------------------------------------------------------------- +# Partitioning +# ---------------------------------------------------------------------------- + +def _undirected_weighted(g: nx.DiGraph) -> nx.Graph: + """Collapse a directed weighted graph to undirected, summing both directions.""" + h = nx.Graph() + h.add_nodes_from(g.nodes()) + for u, v, w in g.edges(data="weight", default=1): + if h.has_edge(u, v): + h[u][v]["weight"] += w + else: + h.add_edge(u, v, weight=w) + return h + + +def _communities(h: nx.Graph, resolution: float, seed: int) -> List[Set[str]]: + """Weighted community detection, preferring Louvain with a graceful fallback.""" + if h.number_of_nodes() == 0: + return [] + if hasattr(nx.community, "louvain_communities"): + return [ + set(c) + for c in nx.community.louvain_communities( + h, weight="weight", resolution=resolution, seed=seed + ) + ] + # networkx < 3.0 (Python 3.9/3.10 floor): greedy modularity has no seed. + return [set(c) for c in nx.community.greedy_modularity_communities(h, weight="weight")] + + +def _greedy_bin_pack( + units: List[Tuple[frozenset, int]], budget: int +) -> List[Set[str]]: + """First-fit-decreasing pack (unit -> modules, size) into <= budget bins. + + Termination guarantee for the recursive splitter: a community that Louvain + refuses to cut still gets divided here. A single unit larger than the + budget (an atomic SCC — a real import cycle) is emitted on its own; that is + unavoidable without breaking edges. + """ + bins: List[Tuple[Set[str], int]] = [] + for members, size in sorted(units, key=lambda u: u[1], reverse=True): + placed = False + for b in bins: + if b[1] + size <= budget: + b[0].update(members) + bins[bins.index(b)] = (b[0], b[1] + size) + placed = True + break + if not placed: + bins.append((set(members), size)) + return [b[0] for b in bins] + + +def _split_oversized( + h: nx.Graph, + community: Set[str], + unit_size: Dict[str, int], + budget: int, + seed: int, + depth: int = 0, +) -> List[Set[str]]: + """Recursively partition a community whose file count exceeds the budget.""" + size = sum(unit_size[n] for n in community) + if size <= budget or len(community) == 1: + # Fits, or is a single atomic SCC unit that cannot be split further. + return [community] + + sub = h.subgraph(community) + # Escalate resolution so Louvain cuts more aggressively each level. + parts = _communities(sub, resolution=2.0 * (depth + 1), seed=seed) + parts = [p for p in parts if p] + + if len(parts) <= 1: + # Louvain could not divide it (one dense blob) — fall back to bin packing + # so the budget is still honoured. + units = [(frozenset([n]), unit_size[n]) for n in community] + return _greedy_bin_pack(units, budget) + + out: List[Set[str]] = [] + for p in parts: + out.extend(_split_oversized(h, p, unit_size, budget, seed, depth + 1)) + return out + + +def _merge_small( + shards: List[Set[str]], + h: nx.Graph, + unit_size: Dict[str, int], + budget: int, +) -> List[Set[str]]: + """Agglomeratively merge under-budget shards by inter-shard weight. + + Recovers cut edges: merging two shards turns the edges between them back + into intra-shard edges. Greedy — repeatedly merge the heaviest-coupled + pair that still fits the budget — which is enough to mop up the small + fragments Louvain leaves behind without reintroducing oversized shards. + """ + shards = [set(s) for s in shards] + sizes = [sum(unit_size[n] for n in s) for s in shards] + + def pair_weight(a: Set[str], b: Set[str]) -> float: + w = 0.0 + for u in a: + for v in h.adj[u]: + if v in b: + w += h[u][v]["weight"] + return w + + while True: + best: Optional[Tuple[int, int]] = None + best_w = 0.0 + for i in range(len(shards)): + for j in range(i + 1, len(shards)): + if sizes[i] + sizes[j] > budget: + continue + w = pair_weight(shards[i], shards[j]) + if w > best_w: + best_w, best = w, (i, j) + if best is None: + break + i, j = best + shards[i] |= shards[j] + sizes[i] += sizes[j] + del shards[j] + del sizes[j] + + # Final consolidation: first-fit pack any shards that still fit together. + # Merging zero-coupling shards (e.g. isolated leaf modules) costs no cut + # weight and avoids spawning a PyCG process per trivial file. Packing + # never severs an edge, so it is strictly safe; the budget still bounds + # per-shard PyCG divergence risk. + units = [(frozenset(s), sz) for s, sz in zip(shards, sizes)] + return _greedy_bin_pack(units, budget) + + +# ---------------------------------------------------------------------------- +# Public entry point +# ---------------------------------------------------------------------------- + +def plan_shards( + symbol_table: Dict[str, PyModule], + jedi_edges: List[PyCallEdge], + budget: int = 100, + seed: int = 42, + merge_small: bool = True, +) -> ShardPlan: + """Partition a project into coupling-aware PyCG shards. + + Args: + symbol_table: The level-1 symbol table (``file_path -> PyModule``). + jedi_edges: Jedi-provenance call edges from the level-1 call graph. + budget: Maximum number of files per shard. + seed: Determinism seed for Louvain. + merge_small: Agglomeratively merge under-budget shards to cut shard + count and recover edges. + + Returns: + A :class:`ShardPlan`. Shards never silently drop files: every project + module appears in exactly one shard (an atomic SCC larger than *budget* + yields a single oversized shard, flagged in ``metrics``). + """ + g = build_module_graph(symbol_table, jedi_edges) + total_weight = float(sum(w for _, _, w in g.edges(data="weight", default=1))) + + # SCC condensation: each strongly-connected component is one atomic unit. + condensation = nx.condensation(g) # DAG; node attr 'members' = set of modules + mapping: Dict[str, int] = condensation.graph["mapping"] # module -> scc id + unit_members: Dict[int, Set[str]] = { + scc: set(condensation.nodes[scc]["members"]) for scc in condensation.nodes + } + unit_size: Dict[int, int] = {scc: len(m) for scc, m in unit_members.items()} + + # Weighted undirected graph over SCC units. + hu = nx.Graph() + hu.add_nodes_from(condensation.nodes()) + for u, v, w in g.edges(data="weight", default=1): + su, sv = mapping[u], mapping[v] + if su == sv: + continue + if hu.has_edge(su, sv): + hu[su][sv]["weight"] += w + else: + hu.add_edge(su, sv, weight=w) + + # Community detection over units, then budget enforcement. + communities = _communities(hu, resolution=1.0, seed=seed) + unit_shards: List[Set[str]] = [] # sets of SCC ids + for community in communities: + unit_shards.extend( + _split_oversized(hu, community, unit_size, budget, seed) + ) + + if merge_small: + unit_shards = _merge_small(unit_shards, hu, unit_size, budget) + + # Expand SCC units back to file paths (graph nodes are files). + file_shards: List[List[str]] = [] + for units in unit_shards: + files: Set[str] = set() + for scc in units: + files |= unit_members[scc] + if files: + file_shards.append(sorted(files)) + + # Parallel view in module names (file stems) for human-readable reporting. + module_shards = [ + [g.nodes[f].get("module_name", f) for f in files] for files in file_shards + ] + + # Metrics: how much Jedi edge weight does this partition sever? + shard_of: Dict[str, int] = {} + for idx, files in enumerate(file_shards): + for f in files: + shard_of[f] = idx + cut_weight = 0.0 + for u, v, w in g.edges(data="weight", default=1): + if shard_of.get(u) != shard_of.get(v): + cut_weight += w + + sizes = [len(s) for s in module_shards] or [0] + metrics = { + "modules": float(g.number_of_nodes()), + "module_edges": float(g.number_of_edges()), + "total_edge_weight": total_weight, + "cut_weight": cut_weight, + "cut_ratio": (cut_weight / total_weight) if total_weight else 0.0, + "num_shards": float(len(module_shards)), + "max_shard_files": float(max(sizes)), + "oversized_shards": float(sum(1 for s in sizes if s > budget)), + } + + plan = ShardPlan(shards=file_shards, module_shards=module_shards, metrics=metrics) + logger.info("Shard planner: %s", plan) + return plan diff --git a/codeanalyzer/utils/logging.py b/codeanalyzer/utils/logging.py index df4b163..bebbb33 100644 --- a/codeanalyzer/utils/logging.py +++ b/codeanalyzer/utils/logging.py @@ -3,8 +3,11 @@ from rich.console import Console from rich.logging import RichHandler -# Set up base logger with RichHandler -console = Console() +# Logs go to stderr so stdout stays clean for piped output (e.g. --emit json | jq). +# The same console instance is shared with ProgressBar so Rich can coordinate +# live-display updates with log messages without the two consoles stomping on each +# other (which causes progress bars to appear twice when a warning interrupts them). +console = Console(stderr=True) handler = RichHandler(console=console, show_time=True, show_level=True, show_path=False) logger = logging.getLogger("codeanalyzer") diff --git a/codeanalyzer/utils/progress_bar.py b/codeanalyzer/utils/progress_bar.py index c90e70c..79eecb4 100644 --- a/codeanalyzer/utils/progress_bar.py +++ b/codeanalyzer/utils/progress_bar.py @@ -1,7 +1,6 @@ import logging from typing import Optional -from rich.console import Console from rich.progress import ( BarColumn, Progress, @@ -15,14 +14,17 @@ class ProgressBar: def __init__( - self, total_files: int, description: str = "Processing files..." + self, + total_files: int, + description: str = "Processing files...", + item_label: str = "files", ) -> None: self.total_files = total_files self.description = description - self.console = Console(stderr=True) + self.item_label = item_label - logger = logging.getLogger("codeanalyzer") - current_level = logger.getEffectiveLevel() + _logger = logging.getLogger("codeanalyzer") + current_level = _logger.getEffectiveLevel() # Disable progress if logger level is higher than INFO (e.g., WARNING or ERROR) self.disabled = current_level >= logging.ERROR @@ -32,16 +34,22 @@ def __init__( def __enter__(self): if not self.disabled: + # Import the shared console from the logging module so that Rich can + # coordinate log messages and live progress rendering on the same + # console — prevents the bar from appearing twice when a warning is + # printed while the progress is active. + from codeanalyzer.utils.logging import console as _shared_console + self._progress = Progress( SpinnerColumn(spinner_name="dots"), TextColumn("[progress.description]{task.description}"), BarColumn(), TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TextColumn("[blue]{task.completed}/{task.total} files"), + TextColumn(f"[blue]{{task.completed}}/{{task.total}} {self.item_label}"), TimeElapsedColumn(), TimeRemainingColumn(), transient=False, - console=self.console, # <-- Use stderr-safe console + console=_shared_console, ) self._progress.start() self._task_id = self._progress.add_task( diff --git a/pyproject.toml b/pyproject.toml index d7f2514..1afc6db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,9 @@ dependencies = [ "ray==2.0.0; python_version < '3.11'", "ray>=2.10.0,<3.0.0; python_version >= '3.11'", "packaging>=25.0", + # pycg: call graph construction for analysis level 2 (Apache-2.0, ICSE 2021). + # Archived Nov 2023; Python 3.13 compatibility is patched in pycg_analysis.py. + "pycg>=0.0.6", # uv -- installs the analyzed project's deps into the analysis venv quickly. # Shipped as a self-contained binary in its wheel, so it's available wherever # canpy is pip-installed (incl. Docker); core.py falls back to pip without it. diff --git a/test/conftest.py b/test/conftest.py index 9af14d4..eeef0a0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -34,3 +34,33 @@ def whole_applications__xarray() -> Path: def single_functionalities__stuff_nested_in_functions() -> Path: """Returns the path to the 'single_functionalities/stuff_nested_in_functions' directory.""" return Path(__file__).parent.resolve().joinpath("fixtures", "single_functionalities", "stuff_nested_in_functions_test") + + +@pytest.fixture +def single_functionalities__decorators_and_hof() -> Path: + """Decorator and higher-order function patterns fixture.""" + return Path(__file__).parent.resolve().joinpath("fixtures", "single_functionalities", "decorators_and_hof") + + +@pytest.fixture +def single_functionalities__class_hierarchy() -> Path: + """Abstract base class, MRO, super(), @classmethod factory fixture.""" + return Path(__file__).parent.resolve().joinpath("fixtures", "single_functionalities", "class_hierarchy") + + +@pytest.fixture +def single_functionalities__async_patterns() -> Path: + """async/await, async generators, async context managers fixture.""" + return Path(__file__).parent.resolve().joinpath("fixtures", "single_functionalities", "async_patterns") + + +@pytest.fixture +def whole_applications__flask() -> Path: + """Flask 3.0.3 application directory.""" + return Path(__file__).parent.resolve().joinpath("fixtures", "whole_applications", "flask") + + +@pytest.fixture +def whole_applications__requests() -> Path: + """Requests 2.31.0 application directory.""" + return Path(__file__).parent.resolve().joinpath("fixtures", "whole_applications", "requests") diff --git a/test/fixtures/single_functionalities/async_patterns/main.py b/test/fixtures/single_functionalities/async_patterns/main.py new file mode 100644 index 0000000..4ebe296 --- /dev/null +++ b/test/fixtures/single_functionalities/async_patterns/main.py @@ -0,0 +1,127 @@ +"""Async/await patterns. + +Exercises: +- async def + await +- asyncio.gather (concurrent tasks) +- Async generator (async def + yield) +- async for loop consuming an async generator +- Async context manager (__aenter__ / __aexit__) +- async with block +- Nested awaits / task composition +""" +import asyncio +from typing import AsyncGenerator, List + + +# --------------------------------------------------------------------------- +# 1. Basic async function +# --------------------------------------------------------------------------- + +async def fetch_data(url: str) -> str: + await asyncio.sleep(0) + return f"data:{url}" + + +async def process_url(url: str) -> str: + raw = await fetch_data(url) + return raw.upper() + + +# --------------------------------------------------------------------------- +# 2. Concurrent tasks with asyncio.gather +# --------------------------------------------------------------------------- + +async def fetch_all(urls: List[str]) -> List[str]: + tasks = [process_url(url) for url in urls] + return await asyncio.gather(*tasks) + + +# --------------------------------------------------------------------------- +# 3. Async generator +# --------------------------------------------------------------------------- + +async def generate_chunks(data: str, size: int) -> AsyncGenerator[str, None]: + for i in range(0, len(data), size): + await asyncio.sleep(0) + yield data[i : i + size] + + +# --------------------------------------------------------------------------- +# 4. async for consuming an async generator +# --------------------------------------------------------------------------- + +async def collect_chunks(data: str, size: int) -> List[str]: + chunks: List[str] = [] + async for chunk in generate_chunks(data, size): + chunks.append(chunk) + return chunks + + +# --------------------------------------------------------------------------- +# 5. Async context manager +# --------------------------------------------------------------------------- + +class AsyncResource: + def __init__(self, name: str): + self.name = name + self._open = False + + async def __aenter__(self) -> "AsyncResource": + await asyncio.sleep(0) + self._open = True + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool: + await asyncio.sleep(0) + self._open = False + return False + + async def read(self) -> str: + if not self._open: + raise RuntimeError("Resource not open") + return f"content of {self.name}" + + +# --------------------------------------------------------------------------- +# 6. async with +# --------------------------------------------------------------------------- + +async def read_resource(name: str) -> str: + async with AsyncResource(name) as res: + return await res.read() + + +# --------------------------------------------------------------------------- +# 7. Task composition +# --------------------------------------------------------------------------- + +async def pipeline(urls: List[str], resource_name: str) -> dict: + fetched = await fetch_all(urls) + chunks = await collect_chunks("hello world async", size=5) + content = await read_resource(resource_name) + return { + "fetched": fetched, + "chunks": chunks, + "content": content, + } + + +# --------------------------------------------------------------------------- +# 8. Driver +# --------------------------------------------------------------------------- + +async def async_main() -> dict: + urls = [ + "http://example.com/a", + "http://example.com/b", + "http://example.com/c", + ] + return await pipeline(urls, resource_name="config.json") + + +def main(): + return asyncio.run(async_main()) + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/single_functionalities/class_hierarchy/main.py b/test/fixtures/single_functionalities/class_hierarchy/main.py new file mode 100644 index 0000000..21a89d0 --- /dev/null +++ b/test/fixtures/single_functionalities/class_hierarchy/main.py @@ -0,0 +1,185 @@ +"""Class hierarchy patterns. + +Exercises: +- Abstract base class (abc.ABC + @abstractmethod) +- Multiple inheritance and MRO +- super() in __init__ and regular methods +- @classmethod as factory +- @staticmethod utility +- __init_subclass__ hook +- Dynamic dispatch / polymorphism +""" +from abc import ABC, abstractmethod +from typing import List + + +# --------------------------------------------------------------------------- +# 1. Abstract base class +# --------------------------------------------------------------------------- + +class Animal(ABC): + _registry: List["Animal"] = [] + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + def __init__(self, name: str): + self.name = name + Animal._registry.append(self) + + @abstractmethod + def speak(self) -> str: + ... + + @classmethod + def create(cls, name: str) -> "Animal": + return cls(name) + + @staticmethod + def kingdom() -> str: + return "Animalia" + + def describe(self) -> str: + return f"{self.name} says: {self.speak()}" + + +# --------------------------------------------------------------------------- +# 2. Concrete subclasses +# --------------------------------------------------------------------------- + +class Dog(Animal): + def __init__(self, name: str): + super().__init__(name) + + def speak(self) -> str: + return "Woof" + + def fetch(self, item: str) -> str: + return f"{self.name} fetches {item}" + + +class Cat(Animal): + def __init__(self, name: str): + super().__init__(name) + + def speak(self) -> str: + return "Meow" + + def purr(self) -> str: + return f"{self.name} purrs" + + +# --------------------------------------------------------------------------- +# 3. Multiple inheritance + MRO +# --------------------------------------------------------------------------- + +class Swimmer(ABC): + @abstractmethod + def swim(self) -> str: + ... + + +class Duck(Animal, Swimmer): + def __init__(self, name: str): + super().__init__(name) + + def speak(self) -> str: + return "Quack" + + def swim(self) -> str: + return f"{self.name} paddles" + + +# --------------------------------------------------------------------------- +# 4. Deep inheritance chain with super() method call +# --------------------------------------------------------------------------- + +class PoliceDog(Dog): + def __init__(self, name: str, badge: int): + super().__init__(name) + self.badge = badge + + def speak(self) -> str: + base_bark = super().speak() + return f"{base_bark} (K9 unit #{self.badge})" + + +class RescuePoliceDog(PoliceDog): + def __init__(self, name: str, badge: int, specialty: str): + super().__init__(name, badge) + self.specialty = specialty + + def speak(self) -> str: + base = super().speak() + return f"{base} [{self.specialty}]" + + +# --------------------------------------------------------------------------- +# 5. @classmethod factory pattern +# --------------------------------------------------------------------------- + +class Config: + def __init__(self, host: str, port: int, debug: bool = False): + self.host = host + self.port = port + self.debug = debug + + @classmethod + def from_dict(cls, d: dict) -> "Config": + return cls( + host=d.get("host", "localhost"), + port=int(d.get("port", 8080)), + debug=bool(d.get("debug", False)), + ) + + @classmethod + def development(cls) -> "Config": + return cls(host="127.0.0.1", port=5000, debug=True) + + @classmethod + def production(cls) -> "Config": + return cls(host="0.0.0.0", port=80, debug=False) + + @staticmethod + def validate_port(port: int) -> bool: + return 1 <= port <= 65535 + + +# --------------------------------------------------------------------------- +# 6. Dynamic dispatch via polymorphism +# --------------------------------------------------------------------------- + +def process_animals(animals: List[Animal]) -> List[str]: + return [a.describe() for a in animals] + + +def make_sound_twice(animal: Animal) -> str: + return f"{animal.speak()} {animal.speak()}" + + +# --------------------------------------------------------------------------- +# 7. Driver +# --------------------------------------------------------------------------- + +def main(): + dog = Dog.create("Rex") + cat = Cat.create("Whiskers") + duck = Duck.create("Donald") + k9 = PoliceDog("Buddy", badge=42) + elite = RescuePoliceDog("Max", badge=99, specialty="avalanche") + + descriptions = process_animals([dog, cat, duck, k9, elite]) + sounds = [make_sound_twice(a) for a in [dog, cat]] + + cfg_dev = Config.development() + cfg_prod = Config.production() + cfg_custom = Config.from_dict({"host": "10.0.0.1", "port": "9090"}) + + valid = Config.validate_port(cfg_dev.port) + kingdom = Animal.kingdom() + + return descriptions, sounds, cfg_dev, cfg_prod, cfg_custom, valid, kingdom + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/single_functionalities/decorators_and_hof/main.py b/test/fixtures/single_functionalities/decorators_and_hof/main.py new file mode 100644 index 0000000..22e8d2d --- /dev/null +++ b/test/fixtures/single_functionalities/decorators_and_hof/main.py @@ -0,0 +1,147 @@ +"""Decorator and higher-order function patterns. + +Exercises: +- Simple function wrapper (functools.wraps) +- Parameterised decorator factory +- Class-based decorator (__call__) +- Higher-order function (function passed as argument) +- Closure / function factory +- Decorator stacking +""" +import functools + + +# --------------------------------------------------------------------------- +# 1. Simple wrapper decorator +# --------------------------------------------------------------------------- + +def log_call(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + return result + return wrapper + + +# --------------------------------------------------------------------------- +# 2. Parameterised decorator factory +# --------------------------------------------------------------------------- + +def repeat(n: int): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = None + for _ in range(n): + result = func(*args, **kwargs) + return result + return wrapper + return decorator + + +# --------------------------------------------------------------------------- +# 3. Class-based decorator +# --------------------------------------------------------------------------- + +class Timer: + def __init__(self, func): + functools.update_wrapper(self, func) + self.func = func + self.call_count = 0 + + def __call__(self, *args, **kwargs): + self.call_count += 1 + return self.func(*args, **kwargs) + + +# --------------------------------------------------------------------------- +# 4. Higher-order functions +# --------------------------------------------------------------------------- + +def apply(func, value): + """Call *func* with *value* and return the result.""" + return func(value) + + +def double(x): + return x * 2 + + +def triple(x): + return x * 3 + + +def compose(f, g): + """Return a new function h(x) = f(g(x)).""" + def h(x): + return f(g(x)) + return h + + +# --------------------------------------------------------------------------- +# 5. Closure / function factory +# --------------------------------------------------------------------------- + +def make_adder(n: int): + def adder(x): + return x + n + return adder + + +def make_multiplier(n: int): + def multiplier(x): + return x * n + return multiplier + + +# --------------------------------------------------------------------------- +# 6. Decorated callables +# --------------------------------------------------------------------------- + +@log_call +def greet(name: str) -> str: + return f"Hello, {name}" + + +@repeat(3) +def say_hello(): + print("hello") + + +@Timer +def compute(x, y): + return x + y + + +@log_call +@repeat(2) +def stacked(value): + return value * 10 + + +# --------------------------------------------------------------------------- +# 7. Driver +# --------------------------------------------------------------------------- + +def main(): + r1 = apply(double, 10) + r2 = apply(triple, 10) + + double_then_triple = compose(triple, double) + r3 = double_then_triple(5) + + add5 = make_adder(5) + mul3 = make_multiplier(3) + r4 = add5(10) + r5 = mul3(10) + + r6 = greet("world") + say_hello() + r7 = compute(2, 3) + r8 = stacked(7) + + return r1, r2, r3, r4, r5, r6, r7, r8 + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/whole_applications/flask/.devcontainer/devcontainer.json b/test/fixtures/whole_applications/flask/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4519826 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "pallets/flask", + "image": "mcr.microsoft.com/devcontainers/python:3", + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.launchArgs": [ + "-X", + "dev" + ] + } + } + }, + "onCreateCommand": ".devcontainer/on-create-command.sh" +} diff --git a/test/fixtures/whole_applications/flask/.devcontainer/on-create-command.sh b/test/fixtures/whole_applications/flask/.devcontainer/on-create-command.sh new file mode 100755 index 0000000..eaebea6 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.devcontainer/on-create-command.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +python3 -m venv --upgrade-deps .venv +. .venv/bin/activate +pip install -r requirements/dev.txt +pip install -e . +pre-commit install --install-hooks diff --git a/test/fixtures/whole_applications/flask/.editorconfig b/test/fixtures/whole_applications/flask/.editorconfig new file mode 100644 index 0000000..2ff985a --- /dev/null +++ b/test/fixtures/whole_applications/flask/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 +max_line_length = 88 + +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] +indent_size = 2 diff --git a/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/bug-report.md b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..0917c79 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Report a bug in Flask (not other projects which depend on Flask) +--- + + + + + + + +Environment: + +- Python version: +- Flask version: diff --git a/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/config.yml b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3f27ac9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Security issue + url: https://github.com/pallets/flask/security/advisories/new + about: Do not report security issues publicly. Create a private advisory. + - name: Questions + url: https://github.com/pallets/flask/discussions/ + about: Ask questions about your own code on the Discussions tab. + - name: Questions on + url: https://discord.gg/pallets + about: Ask questions about your own code on our Discord chat. diff --git a/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/feature-request.md b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..52c2aed --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,15 @@ +--- +name: Feature request +about: Suggest a new feature for Flask +--- + + + + diff --git a/test/fixtures/whole_applications/flask/.github/dependabot.yml b/test/fixtures/whole_applications/flask/.github/dependabot.yml new file mode 100644 index 0000000..fa94b77 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + ignore: + # slsa depends on upload/download v3 + - dependency-name: actions/upload-artifact + versions: '>= 4' + - dependency-name: actions/download-artifact + versions: '>= 4' + groups: + github-actions: + patterns: + - '*' + - package-ecosystem: pip + directory: /requirements/ + schedule: + interval: monthly + groups: + python-requirements: + patterns: + - '*' diff --git a/test/fixtures/whole_applications/flask/.github/pull_request_template.md b/test/fixtures/whole_applications/flask/.github/pull_request_template.md new file mode 100644 index 0000000..eb124d2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/pull_request_template.md @@ -0,0 +1,25 @@ + + + + + diff --git a/test/fixtures/whole_applications/flask/.github/workflows/lock.yaml b/test/fixtures/whole_applications/flask/.github/workflows/lock.yaml new file mode 100644 index 0000000..1677663 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/workflows/lock.yaml @@ -0,0 +1,23 @@ +name: Lock inactive closed issues +# Lock closed issues that have not received any further activity for two weeks. +# This does not close open issues, only humans may do that. It is easier to +# respond to new issues with fresh examples rather than continuing discussions +# on old issues. + +on: + schedule: + - cron: '0 0 * * *' +permissions: + issues: write + pull-requests: write +concurrency: + group: lock +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1 + with: + issue-inactive-days: 14 + pr-inactive-days: 14 + discussion-inactive-days: 14 diff --git a/test/fixtures/whole_applications/flask/.github/workflows/publish.yaml b/test/fixtures/whole_applications/flask/.github/workflows/publish.yaml new file mode 100644 index 0000000..0d5e126 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/workflows/publish.yaml @@ -0,0 +1,73 @@ +name: Publish +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. + - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + - run: python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + path: ./dist + provenance: + needs: [build] + permissions: + actions: read + id-token: write + contents: write + # Can't pin with hash due to how this workflow works. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} + create-release: + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: [provenance] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - name: create release + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* artifact/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + needs: [provenance] + # Wait for approval before attempting to upload to PyPI. This allows reviewing the + # files in the draft release. + environment: + name: publish + url: https://pypi.org/project/Flask/${{ github.ref_name }} + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - uses: pypa/gh-action-pypi-publish@68e62d4871ad9d14a9d55f114e6ac71f0b408ec0 # v1.8.14 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: artifact/ + - uses: pypa/gh-action-pypi-publish@68e62d4871ad9d14a9d55f114e6ac71f0b408ec0 # v1.8.14 + with: + packages-dir: artifact/ diff --git a/test/fixtures/whole_applications/flask/.github/workflows/tests.yaml b/test/fixtures/whole_applications/flask/.github/workflows/tests.yaml new file mode 100644 index 0000000..8795e60 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.github/workflows/tests.yaml @@ -0,0 +1,59 @@ +name: Tests +on: + push: + branches: + - main + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' +jobs: + tests: + name: ${{ matrix.name || matrix.python }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + include: + - {python: '3.12'} + - {name: Windows, python: '3.12', os: windows-latest} + - {name: Mac, python: '3.12', os: macos-latest} + - {python: '3.11'} + - {python: '3.10'} + - {python: '3.9'} + - {python: '3.8'} + - {name: PyPy, python: 'pypy-3.10', tox: pypy310} + - {name: Minimum Versions, python: '3.12', tox: py-min} + - {name: Development Versions, python: '3.8', tox: py-dev} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install tox + - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} + typing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt + - name: cache mypy + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: ./.mypy_cache + key: mypy|${{ hashFiles('pyproject.toml') }} + - run: pip install tox + - run: tox run -e typing diff --git a/test/fixtures/whole_applications/flask/.gitignore b/test/fixtures/whole_applications/flask/.gitignore new file mode 100644 index 0000000..62c1b88 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.gitignore @@ -0,0 +1,10 @@ +.idea/ +.vscode/ +.venv*/ +venv*/ +__pycache__/ +dist/ +.coverage* +htmlcov/ +.tox/ +docs/_build/ diff --git a/test/fixtures/whole_applications/flask/.pre-commit-config.yaml b/test/fixtures/whole_applications/flask/.pre-commit-config.yaml new file mode 100644 index 0000000..8289161 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +ci: + autoupdate_schedule: monthly +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.5 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/test/fixtures/whole_applications/flask/.readthedocs.yaml b/test/fixtures/whole_applications/flask/.readthedocs.yaml new file mode 100644 index 0000000..865c685 --- /dev/null +++ b/test/fixtures/whole_applications/flask/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.12' +python: + install: + - requirements: requirements/docs.txt + - method: pip + path: . +sphinx: + builder: dirhtml + fail_on_warning: true diff --git a/test/fixtures/whole_applications/flask/CHANGES.rst b/test/fixtures/whole_applications/flask/CHANGES.rst new file mode 100644 index 0000000..082c706 --- /dev/null +++ b/test/fixtures/whole_applications/flask/CHANGES.rst @@ -0,0 +1,1565 @@ +Version 3.0.3 +------------- + +Released 2024-04-07 + +- The default ``hashlib.sha1`` may not be available in FIPS builds. Don't + access it at import time so the developer has time to change the default. + :issue:`5448` +- Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in + the ``Flask`` concrete class. :pr:`5270` + + +Version 3.0.2 +------------- + +Released 2024-02-03 + +- Correct type for ``jinja_loader`` property. :issue:`5388` +- Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options. + :issue:`5391` + + +Version 3.0.1 +------------- + +Released 2024-01-18 + +- Correct type for ``path`` argument to ``send_file``. :issue:`5230` +- Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344` +- Session data is untagged without relying on the built-in ``json.loads`` + ``object_hook``. This allows other JSON providers that don't implement that. + :issue:`5381` +- Address more type findings when using mypy strict mode. :pr:`5383` + + +Version 3.0.0 +------------- + +Released 2023-09-30 + +- Remove previously deprecated code. :pr:`5223` +- Deprecate the ``__version__`` attribute. Use feature detection, or + ``importlib.metadata.version("flask")``, instead. :issue:`5230` +- Restructure the code such that the Flask (app) and Blueprint + classes have Sans-IO bases. :pr:`5127` +- Allow self as an argument to url_for. :pr:`5264` +- Require Werkzeug >= 3.0.0. + + +Version 2.3.3 +------------- + +Released 2023-08-21 + +- Python 3.12 compatibility. +- Require Werkzeug >= 2.3.7. +- Use ``flit_core`` instead of ``setuptools`` as build backend. +- Refactor how an app's root and instance paths are determined. :issue:`5160` + + +Version 2.3.2 +------------- + +Released 2023-05-01 + +- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. +- Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes. + + +Version 2.3.1 +------------- + +Released 2023-04-25 + +- Restore deprecated ``from flask import Markup``. :issue:`5084` + + +Version 2.3.0 +------------- + +Released 2023-04-25 + +- Drop support for Python 3.7. :pr:`5072` +- Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, + itsdangerous>=2.1.2, click>=8.1.3. +- Remove previously deprecated code. :pr:`4995` + + - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and + ``_request_ctx_stack`` objects are removed. ``top`` still exists to give + extensions more time to update, but it will be removed. + - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` + property are removed. + - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, + ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` + are removed. + - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and + ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. + - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators + are removed. + - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the + corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. + - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. + - Calling setup methods on blueprints after registration is an error instead of a + warning. :pr:`4997` + +- Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them + directly from ``markupsafe`` instead. :pr:`4996` +- The ``app.got_first_request`` property is deprecated. :pr:`4997` +- The ``locked_cached_property`` decorator is deprecated. Use a lock inside the + decorated function if locking is needed. :issue:`4993` +- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The + ``signals_available`` attribute is deprecated. :issue:`5056` +- Signals support ``async`` subscriber functions. :pr:`5049` +- Remove uses of locks that could cause requests to block each other very briefly. + :issue:`4993` +- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. + :pr:`4947` +- Ensure subdomains are applied with nested blueprints. :issue:`4834` +- ``config.from_file`` can use ``text=False`` to indicate that the parser wants a + binary file instead. :issue:`4989` +- If a blueprint is created with an empty name it raises a ``ValueError``. + :issue:`5010` +- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not + to set the domain, which modern browsers interpret as an exact match rather than + a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. + :issue:`5051` +- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain + matching is in use. :issue:`5004` +- Use postponed evaluation of annotations. :pr:`5071` + + +Version 2.2.5 +------------- + +Released 2023-05-02 + +- Update for compatibility with Werkzeug 2.3.3. +- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. + + +Version 2.2.4 +------------- + +Released 2023-04-25 + +- Update for compatibility with Werkzeug 2.3. + + +Version 2.2.3 +------------- + +Released 2023-02-15 + +- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` +- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` +- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` + + +Version 2.2.2 +------------- + +Released 2022-08-08 + +- Update Werkzeug dependency to >= 2.2.2. This includes fixes related + to the new faster router, header parsing, and the development + server. :pr:`4754` +- Fix the default value for ``app.env`` to be ``"production"``. This + attribute remains deprecated. :issue:`4740` + + +Version 2.2.1 +------------- + +Released 2022-08-03 + +- Setting or accessing ``json_encoder`` or ``json_decoder`` raises a + deprecation warning. :issue:`4732` + + +Version 2.2.0 +------------- + +Released 2022-08-01 + +- Remove previously deprecated code. :pr:`4667` + + - Old names for some ``send_file`` parameters have been removed. + ``download_name`` replaces ``attachment_filename``, ``max_age`` + replaces ``cache_timeout``, and ``etag`` replaces ``add_etags``. + Additionally, ``path`` replaces ``filename`` in + ``send_from_directory``. + - The ``RequestContext.g`` property returning ``AppContext.g`` is + removed. + +- Update Werkzeug dependency to >= 2.2. +- The app and request contexts are managed using Python context vars + directly rather than Werkzeug's ``LocalStack``. This should result + in better performance and memory use. :pr:`4682` + + - Extension maintainers, be aware that ``_app_ctx_stack.top`` + and ``_request_ctx_stack.top`` are deprecated. Store data on + ``g`` instead using a unique prefix, like + ``g._extension_name_attr``. + +- The ``FLASK_ENV`` environment variable and ``app.env`` attribute are + deprecated, removing the distinction between development and debug + mode. Debug mode should be controlled directly using the ``--debug`` + option or ``app.run(debug=True)``. :issue:`4714` +- Some attributes that proxied config keys on ``app`` are deprecated: + ``session_cookie_name``, ``send_file_max_age_default``, + ``use_x_sendfile``, ``propagate_exceptions``, and + ``templates_auto_reload``. Use the relevant config keys instead. + :issue:`4716` +- Add new customization points to the ``Flask`` app object for many + previously global behaviors. + + - ``flask.url_for`` will call ``app.url_for``. :issue:`4568` + - ``flask.abort`` will call ``app.aborter``. + ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used + to customize this aborter. :issue:`4567` + - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` + - ``flask.json`` is an instance of ``JSONProvider``. A different + provider can be set to use a different JSON library. + ``flask.jsonify`` will call ``app.json.response``, other + functions in ``flask.json`` will call corresponding functions in + ``app.json``. :pr:`4692` + +- JSON configuration is moved to attributes on the default + ``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, + ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are + deprecated. :pr:`4692` +- Setting custom ``json_encoder`` and ``json_decoder`` classes on the + app or a blueprint, and the corresponding ``json.JSONEncoder`` and + ``JSONDecoder`` classes, are deprecated. JSON behavior can now be + overridden using the ``app.json`` provider interface. :pr:`4692` +- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated, + the function is built-in to Jinja now. :pr:`4692` +- Refactor ``register_error_handler`` to consolidate error checking. + Rewrite some error messages to be more consistent. :issue:`4559` +- Use Blueprint decorators and functions intended for setup after + registering the blueprint will show a warning. In the next version, + this will become an error just like the application setup methods. + :issue:`4571` +- ``before_first_request`` is deprecated. Run setup code when creating + the application instead. :issue:`4605` +- Added the ``View.init_every_request`` class attribute. If a view + subclass sets this to ``False``, the view will not create a new + instance on every request. :issue:`2520`. +- A ``flask.cli.FlaskGroup`` Click group can be nested as a + sub-command in a custom CLI. :issue:`3263` +- Add ``--app`` and ``--debug`` options to the ``flask`` CLI, instead + of requiring that they are set through environment variables. + :issue:`2836` +- Add ``--env-file`` option to the ``flask`` CLI. This allows + specifying a dotenv file to load in addition to ``.env`` and + ``.flaskenv``. :issue:`3108` +- It is no longer required to decorate custom CLI commands on + ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app + context will already be active at that point. :issue:`2410` +- ``SessionInterface.get_expiration_time`` uses a timezone-aware + value. :pr:`4645` +- View functions can return generators directly instead of wrapping + them in a ``Response``. :pr:`4629` +- Add ``stream_template`` and ``stream_template_string`` functions to + render a template as a stream of pieces. :pr:`4629` +- A new implementation of context preservation during debugging and + testing. :pr:`4666` + + - ``request``, ``g``, and other context-locals point to the + correct data when running code in the interactive debugger + console. :issue:`2836` + - Teardown functions are always run at the end of the request, + even if the context is preserved. They are also run after the + preserved context is popped. + - ``stream_with_context`` preserves context separately from a + ``with client`` block. It will be cleaned up when + ``response.get_data()`` or ``response.close()`` is called. + +- Allow returning a list from a view function, to convert it to a + JSON response like a dict is. :issue:`4672` +- When type checking, allow ``TypedDict`` to be returned from view + functions. :pr:`4695` +- Remove the ``--eager-loading/--lazy-loading`` options from the + ``flask run`` command. The app is always eager loaded the first + time, then lazily loaded in the reloader. The reloader always prints + errors immediately but continues serving. Remove the internal + ``DispatchingApp`` middleware used by the previous implementation. + :issue:`4715` + + +Version 2.1.3 +------------- + +Released 2022-07-13 + +- Inline some optional imports that are only used for certain CLI + commands. :pr:`4606` +- Relax type annotation for ``after_request`` functions. :issue:`4600` +- ``instance_path`` for namespace packages uses the path closest to + the imported submodule. :issue:`4610` +- Clearer error message when ``render_template`` and + ``render_template_string`` are used outside an application context. + :pr:`4693` + + +Version 2.1.2 +------------- + +Released 2022-04-28 + +- Fix type annotation for ``json.loads``, it accepts str or bytes. + :issue:`4519` +- The ``--cert`` and ``--key`` options on ``flask run`` can be given + in either order. :issue:`4459` + + +Version 2.1.1 +------------- + +Released on 2022-03-30 + +- Set the minimum required version of importlib_metadata to 3.6.0, + which is required on Python < 3.10. :issue:`4502` + + +Version 2.1.0 +------------- + +Released 2022-03-28 + +- Drop support for Python 3.6. :pr:`4335` +- Update Click dependency to >= 8.0. :pr:`4008` +- Remove previously deprecated code. :pr:`4337` + + - The CLI does not pass ``script_info`` to app factory functions. + - ``config.from_json`` is replaced by + ``config.from_file(name, load=json.load)``. + - ``json`` functions no longer take an ``encoding`` parameter. + - ``safe_join`` is removed, use ``werkzeug.utils.safe_join`` + instead. + - ``total_seconds`` is removed, use ``timedelta.total_seconds`` + instead. + - The same blueprint cannot be registered with the same name. Use + ``name=`` when registering to specify a unique name. + - The test client's ``as_tuple`` parameter is removed. Use + ``response.request.environ`` instead. :pr:`4417` + +- Some parameters in ``send_file`` and ``send_from_directory`` were + renamed in 2.0. The deprecation period for the old names is extended + to 2.2. Be sure to test with deprecation warnings visible. + + - ``attachment_filename`` is renamed to ``download_name``. + - ``cache_timeout`` is renamed to ``max_age``. + - ``add_etags`` is renamed to ``etag``. + - ``filename`` is renamed to ``path``. + +- The ``RequestContext.g`` property is deprecated. Use ``g`` directly + or ``AppContext.g`` instead. :issue:`3898` +- ``copy_current_request_context`` can decorate async functions. + :pr:`4303` +- The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to + load command entry points. :issue:`4419` +- Overriding ``FlaskClient.open`` will not cause an error on redirect. + :issue:`3396` +- Add an ``--exclude-patterns`` option to the ``flask run`` CLI + command to specify patterns that will be ignored by the reloader. + :issue:`4188` +- When using lazy loading (the default with the debugger), the Click + context from the ``flask run`` command remains available in the + loader thread. :issue:`4460` +- Deleting the session cookie uses the ``httponly`` flag. + :issue:`4485` +- Relax typing for ``errorhandler`` to allow the user to use more + precise types and decorate the same function multiple times. + :issue:`4095, 4295, 4297` +- Fix typing for ``__exit__`` methods for better compatibility with + ``ExitStack``. :issue:`4474` +- From Werkzeug, for redirect responses the ``Location`` header URL + will remain relative, and exclude the scheme and domain, by default. + :pr:`4496` +- Add ``Config.from_prefixed_env()`` to load config values from + environment variables that start with ``FLASK_`` or another prefix. + This parses values as JSON by default, and allows setting keys in + nested dicts. :pr:`4479` + + +Version 2.0.3 +------------- + +Released 2022-02-14 + +- The test client's ``as_tuple`` parameter is deprecated and will be + removed in Werkzeug 2.1. It is now also deprecated in Flask, to be + removed in Flask 2.1, while remaining compatible with both in + 2.0.x. Use ``response.request.environ`` instead. :pr:`4341` +- Fix type annotation for ``errorhandler`` decorator. :issue:`4295` +- Revert a change to the CLI that caused it to hide ``ImportError`` + tracebacks when importing the application. :issue:`4307` +- ``app.json_encoder`` and ``json_decoder`` are only passed to + ``dumps`` and ``loads`` if they have custom behavior. This improves + performance, mainly on PyPy. :issue:`4349` +- Clearer error message when ``after_this_request`` is used outside a + request context. :issue:`4333` + + +Version 2.0.2 +------------- + +Released 2021-10-04 + +- Fix type annotation for ``teardown_*`` methods. :issue:`4093` +- Fix type annotation for ``before_request`` and ``before_app_request`` + decorators. :issue:`4104` +- Fixed the issue where typing requires template global + decorators to accept functions with no arguments. :issue:`4098` +- Support View and MethodView instances with async handlers. :issue:`4112` +- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095` +- Fix registering a blueprint twice with differing names. :issue:`4124` +- Fix the type of ``static_folder`` to accept ``pathlib.Path``. + :issue:`4150` +- ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``. + :issue:`4157` +- Correctly handle raising deferred errors in CLI lazy loading. + :issue:`4096` +- The CLI loader handles ``**kwargs`` in a ``create_app`` function. + :issue:`4170` +- Fix the order of ``before_request`` and other callbacks that trigger + before the view returns. They are called from the app down to the + closest nested blueprint. :issue:`4229` + + +Version 2.0.1 +------------- + +Released 2021-05-21 + +- Re-add the ``filename`` parameter in ``send_from_directory``. The + ``filename`` parameter has been renamed to ``path``, the old name + is deprecated. :pr:`4019` +- Mark top-level names as exported so type checking understands + imports in user projects. :issue:`4024` +- Fix type annotation for ``g`` and inform mypy that it is a namespace + object that has arbitrary attributes. :issue:`4020` +- Fix some types that weren't available in Python 3.6.0. :issue:`4040` +- Improve typing for ``send_file``, ``send_from_directory``, and + ``get_send_file_max_age``. :issue:`4044`, :pr:`4026` +- Show an error when a blueprint name contains a dot. The ``.`` has + special meaning, it is used to separate (nested) blueprint names and + the endpoint name. :issue:`4041` +- Combine URL prefixes when nesting blueprints that were created with + a ``url_prefix`` value. :issue:`4037` +- Revert a change to the order that URL matching was done. The + URL is again matched after the session is loaded, so the session is + available in custom URL converters. :issue:`4053` +- Re-add deprecated ``Config.from_json``, which was accidentally + removed early. :issue:`4078` +- Improve typing for some functions using ``Callable`` in their type + signatures, focusing on decorator factories. :issue:`4060` +- Nested blueprints are registered with their dotted name. This allows + different blueprints with the same name to be nested at different + locations. :issue:`4069` +- ``register_blueprint`` takes a ``name`` option to change the + (pre-dotted) name the blueprint is registered with. This allows the + same blueprint to be registered multiple times with unique names for + ``url_for``. Registering the same blueprint with the same name + multiple times is deprecated. :issue:`1091` +- Improve typing for ``stream_with_context``. :issue:`4052` + + +Version 2.0.0 +------------- + +Released 2021-05-11 + +- Drop support for Python 2 and 3.5. +- Bump minimum versions of other Pallets projects: Werkzeug >= 2, + Jinja2 >= 3, MarkupSafe >= 2, ItsDangerous >= 2, Click >= 8. Be sure + to check the change logs for each project. For better compatibility + with other applications (e.g. Celery) that still require Click 7, + there is no hard dependency on Click 8 yet, but using Click 7 will + trigger a DeprecationWarning and Flask 2.1 will depend on Click 8. +- JSON support no longer uses simplejson. To use another JSON module, + override ``app.json_encoder`` and ``json_decoder``. :issue:`3555` +- The ``encoding`` option to JSON functions is deprecated. :pr:`3562` +- Passing ``script_info`` to app factory functions is deprecated. This + was not portable outside the ``flask`` command. Use + ``click.get_current_context().obj`` if it's needed. :issue:`3552` +- The CLI shows better error messages when the app failed to load + when looking up commands. :issue:`2741` +- Add ``SessionInterface.get_cookie_name`` to allow setting the + session cookie name dynamically. :pr:`3369` +- Add ``Config.from_file`` to load config using arbitrary file + loaders, such as ``toml.load`` or ``json.load``. + ``Config.from_json`` is deprecated in favor of this. :pr:`3398` +- The ``flask run`` command will only defer errors on reload. Errors + present during the initial call will cause the server to exit with + the traceback immediately. :issue:`3431` +- ``send_file`` raises a ``ValueError`` when passed an ``io`` object + in text mode. Previously, it would respond with 200 OK and an empty + file. :issue:`3358` +- When using ad-hoc certificates, check for the cryptography library + instead of PyOpenSSL. :pr:`3492` +- When specifying a factory function with ``FLASK_APP``, keyword + argument can be passed. :issue:`3553` +- When loading a ``.env`` or ``.flaskenv`` file, the current working + directory is no longer changed to the location of the file. + :pr:`3560` +- When returning a ``(response, headers)`` tuple from a view, the + headers replace rather than extend existing headers on the response. + For example, this allows setting the ``Content-Type`` for + ``jsonify()``. Use ``response.headers.extend()`` if extending is + desired. :issue:`3628` +- The ``Scaffold`` class provides a common API for the ``Flask`` and + ``Blueprint`` classes. ``Blueprint`` information is stored in + attributes just like ``Flask``, rather than opaque lambda functions. + This is intended to improve consistency and maintainability. + :issue:`3215` +- Include ``samesite`` and ``secure`` options when removing the + session cookie. :pr:`3726` +- Support passing a ``pathlib.Path`` to ``static_folder``. :pr:`3579` +- ``send_file`` and ``send_from_directory`` are wrappers around the + implementations in ``werkzeug.utils``. :pr:`3828` +- Some ``send_file`` parameters have been renamed, the old names are + deprecated. ``attachment_filename`` is renamed to ``download_name``. + ``cache_timeout`` is renamed to ``max_age``. ``add_etags`` is + renamed to ``etag``. :pr:`3828, 3883` +- ``send_file`` passes ``download_name`` even if + ``as_attachment=False`` by using ``Content-Disposition: inline``. + :pr:`3828` +- ``send_file`` sets ``conditional=True`` and ``max_age=None`` by + default. ``Cache-Control`` is set to ``no-cache`` if ``max_age`` is + not set, otherwise ``public``. This tells browsers to validate + conditional requests instead of using a timed cache. :pr:`3828` +- ``helpers.safe_join`` is deprecated. Use + ``werkzeug.utils.safe_join`` instead. :pr:`3828` +- The request context does route matching before opening the session. + This could allow a session interface to change behavior based on + ``request.endpoint``. :issue:`3776` +- Use Jinja's implementation of the ``|tojson`` filter. :issue:`3881` +- Add route decorators for common HTTP methods. For example, + ``@app.post("/login")`` is a shortcut for + ``@app.route("/login", methods=["POST"])``. :pr:`3907` +- Support async views, error handlers, before and after request, and + teardown functions. :pr:`3412` +- Support nesting blueprints. :issue:`593, 1548`, :pr:`3923` +- Set the default encoding to "UTF-8" when loading ``.env`` and + ``.flaskenv`` files to allow to use non-ASCII characters. :issue:`3931` +- ``flask shell`` sets up tab and history completion like the default + ``python`` shell if ``readline`` is installed. :issue:`3941` +- ``helpers.total_seconds()`` is deprecated. Use + ``timedelta.total_seconds()`` instead. :pr:`3962` +- Add type hinting. :pr:`3973`. + + +Version 1.1.4 +------------- + +Released 2021-05-13 + +- Update ``static_folder`` to use ``_compat.fspath`` instead of + ``os.fspath`` to continue supporting Python < 3.6 :issue:`4050` + + +Version 1.1.3 +------------- + +Released 2021-05-13 + +- Set maximum versions of Werkzeug, Jinja, Click, and ItsDangerous. + :issue:`4043` +- Re-add support for passing a ``pathlib.Path`` for ``static_folder``. + :pr:`3579` + + +Version 1.1.2 +------------- + +Released 2020-04-03 + +- Work around an issue when running the ``flask`` command with an + external debugger on Windows. :issue:`3297` +- The static route will not catch all URLs if the ``Flask`` + ``static_folder`` argument ends with a slash. :issue:`3452` + + +Version 1.1.1 +------------- + +Released 2019-07-08 + +- The ``flask.json_available`` flag was added back for compatibility + with some extensions. It will raise a deprecation warning when used, + and will be removed in version 2.0.0. :issue:`3288` + + +Version 1.1.0 +------------- + +Released 2019-07-04 + +- Bump minimum Werkzeug version to >= 0.15. +- Drop support for Python 3.4. +- Error handlers for ``InternalServerError`` or ``500`` will always be + passed an instance of ``InternalServerError``. If they are invoked + due to an unhandled exception, that original exception is now + available as ``e.original_exception`` rather than being passed + directly to the handler. The same is true if the handler is for the + base ``HTTPException``. This makes error handler behavior more + consistent. :pr:`3266` + + - ``Flask.finalize_request`` is called for all unhandled + exceptions even if there is no ``500`` error handler. + +- ``Flask.logger`` takes the same name as ``Flask.name`` (the value + passed as ``Flask(import_name)``. This reverts 1.0's behavior of + always logging to ``"flask.app"``, in order to support multiple apps + in the same process. A warning will be shown if old configuration is + detected that needs to be moved. :issue:`2866` +- ``RequestContext.copy`` includes the current session object in the + request context copy. This prevents ``session`` pointing to an + out-of-date object. :issue:`2935` +- Using built-in RequestContext, unprintable Unicode characters in + Host header will result in a HTTP 400 response and not HTTP 500 as + previously. :pr:`2994` +- ``send_file`` supports ``PathLike`` objects as described in + :pep:`519`, to support ``pathlib`` in Python 3. :pr:`3059` +- ``send_file`` supports ``BytesIO`` partial content. + :issue:`2957` +- ``open_resource`` accepts the "rt" file mode. This still does the + same thing as "r". :issue:`3163` +- The ``MethodView.methods`` attribute set in a base class is used by + subclasses. :issue:`3138` +- ``Flask.jinja_options`` is a ``dict`` instead of an + ``ImmutableDict`` to allow easier configuration. Changes must still + be made before creating the environment. :pr:`3190` +- Flask's ``JSONMixin`` for the request and response wrappers was + moved into Werkzeug. Use Werkzeug's version with Flask-specific + support. This bumps the Werkzeug dependency to >= 0.15. + :issue:`3125` +- The ``flask`` command entry point is simplified to take advantage + of Werkzeug 0.15's better reloader support. This bumps the Werkzeug + dependency to >= 0.15. :issue:`3022` +- Support ``static_url_path`` that ends with a forward slash. + :issue:`3134` +- Support empty ``static_folder`` without requiring setting an empty + ``static_url_path`` as well. :pr:`3124` +- ``jsonify`` supports ``dataclass`` objects. :pr:`3195` +- Allow customizing the ``Flask.url_map_class`` used for routing. + :pr:`3069` +- The development server port can be set to 0, which tells the OS to + pick an available port. :issue:`2926` +- The return value from ``cli.load_dotenv`` is more consistent with + the documentation. It will return ``False`` if python-dotenv is not + installed, or if the given path isn't a file. :issue:`2937` +- Signaling support has a stub for the ``connect_via`` method when + the Blinker library is not installed. :pr:`3208` +- Add an ``--extra-files`` option to the ``flask run`` CLI command to + specify extra files that will trigger the reloader on change. + :issue:`2897` +- Allow returning a dictionary from a view function. Similar to how + returning a string will produce a ``text/html`` response, returning + a dict will call ``jsonify`` to produce a ``application/json`` + response. :pr:`3111` +- Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands + registered with a blueprint will be available as a group under the + ``flask`` command. :issue:`1357`. +- When using the test client as a context manager (``with client:``), + all preserved request contexts are popped when the block exits, + ensuring nested contexts are cleaned up correctly. :pr:`3157` +- Show a better error message when the view return type is not + supported. :issue:`3214` +- ``flask.testing.make_test_environ_builder()`` has been deprecated in + favour of a new class ``flask.testing.EnvironBuilder``. :pr:`3232` +- The ``flask run`` command no longer fails if Python is not built + with SSL support. Using the ``--cert`` option will show an + appropriate error message. :issue:`3211` +- URL matching now occurs after the request context is pushed, rather + than when it's created. This allows custom URL converters to access + the app and request contexts, such as to query a database for an id. + :issue:`3088` + + +Version 1.0.4 +------------- + +Released 2019-07-04 + +- The key information for ``BadRequestKeyError`` is no longer cleared + outside debug mode, so error handlers can still access it. This + requires upgrading to Werkzeug 0.15.5. :issue:`3249` +- ``send_file`` url quotes the ":" and "/" characters for more + compatible UTF-8 filename support in some browsers. :issue:`3074` +- Fixes for :pep:`451` import loaders and pytest 5.x. :issue:`3275` +- Show message about dotenv on stderr instead of stdout. :issue:`3285` + + +Version 1.0.3 +------------- + +Released 2019-05-17 + +- ``send_file`` encodes filenames as ASCII instead of Latin-1 + (ISO-8859-1). This fixes compatibility with Gunicorn, which is + stricter about header encodings than :pep:`3333`. :issue:`2766` +- Allow custom CLIs using ``FlaskGroup`` to set the debug flag without + it always being overwritten based on environment variables. + :pr:`2765` +- ``flask --version`` outputs Werkzeug's version and simplifies the + Python version. :pr:`2825` +- ``send_file`` handles an ``attachment_filename`` that is a native + Python 2 string (bytes) with UTF-8 coded bytes. :issue:`2933` +- A catch-all error handler registered for ``HTTPException`` will not + handle ``RoutingException``, which is used internally during + routing. This fixes the unexpected behavior that had been introduced + in 1.0. :pr:`2986` +- Passing the ``json`` argument to ``app.test_client`` does not + push/pop an extra app context. :issue:`2900` + + +Version 1.0.2 +------------- + +Released 2018-05-02 + +- Fix more backwards compatibility issues with merging slashes between + a blueprint prefix and route. :pr:`2748` +- Fix error with ``flask routes`` command when there are no routes. + :issue:`2751` + + +Version 1.0.1 +------------- + +Released 2018-04-29 + +- Fix registering partials (with no ``__name__``) as view functions. + :pr:`2730` +- Don't treat lists returned from view functions the same as tuples. + Only tuples are interpreted as response data. :issue:`2736` +- Extra slashes between a blueprint's ``url_prefix`` and a route URL + are merged. This fixes some backwards compatibility issues with the + change in 1.0. :issue:`2731`, :issue:`2742` +- Only trap ``BadRequestKeyError`` errors in debug mode, not all + ``BadRequest`` errors. This allows ``abort(400)`` to continue + working as expected. :issue:`2735` +- The ``FLASK_SKIP_DOTENV`` environment variable can be set to ``1`` + to skip automatically loading dotenv files. :issue:`2722` + + +Version 1.0 +----------- + +Released 2018-04-26 + +- Python 2.6 and 3.3 are no longer supported. +- Bump minimum dependency versions to the latest stable versions: + Werkzeug >= 0.14, Jinja >= 2.10, itsdangerous >= 0.24, Click >= 5.1. + :issue:`2586` +- Skip ``app.run`` when a Flask application is run from the command + line. This avoids some behavior that was confusing to debug. +- Change the default for ``JSONIFY_PRETTYPRINT_REGULAR`` to + ``False``. ``~json.jsonify`` returns a compact format by default, + and an indented format in debug mode. :pr:`2193` +- ``Flask.__init__`` accepts the ``host_matching`` argument and sets + it on ``Flask.url_map``. :issue:`1559` +- ``Flask.__init__`` accepts the ``static_host`` argument and passes + it as the ``host`` argument when defining the static route. + :issue:`1559` +- ``send_file`` supports Unicode in ``attachment_filename``. + :pr:`2223` +- Pass ``_scheme`` argument from ``url_for`` to + ``Flask.handle_url_build_error``. :pr:`2017` +- ``Flask.add_url_rule`` accepts the ``provide_automatic_options`` + argument to disable adding the ``OPTIONS`` method. :pr:`1489` +- ``MethodView`` subclasses inherit method handlers from base classes. + :pr:`1936` +- Errors caused while opening the session at the beginning of the + request are handled by the app's error handlers. :pr:`2254` +- Blueprints gained ``Blueprint.json_encoder`` and + ``Blueprint.json_decoder`` attributes to override the app's + encoder and decoder. :pr:`1898` +- ``Flask.make_response`` raises ``TypeError`` instead of + ``ValueError`` for bad response types. The error messages have been + improved to describe why the type is invalid. :pr:`2256` +- Add ``routes`` CLI command to output routes registered on the + application. :pr:`2259` +- Show warning when session cookie domain is a bare hostname or an IP + address, as these may not behave properly in some browsers, such as + Chrome. :pr:`2282` +- Allow IP address as exact session cookie domain. :pr:`2282` +- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through + ``SERVER_NAME``. :pr:`2282` +- Auto-detect zero-argument app factory called ``create_app`` or + ``make_app`` from ``FLASK_APP``. :pr:`2297` +- Factory functions are not required to take a ``script_info`` + parameter to work with the ``flask`` command. If they take a single + parameter or a parameter named ``script_info``, the ``ScriptInfo`` + object will be passed. :pr:`2319` +- ``FLASK_APP`` can be set to an app factory, with arguments if + needed, for example ``FLASK_APP=myproject.app:create_app('dev')``. + :pr:`2326` +- ``FLASK_APP`` can point to local packages that are not installed in + editable mode, although ``pip install -e`` is still preferred. + :pr:`2414` +- The ``View`` class attribute + ``View.provide_automatic_options`` is set in ``View.as_view``, to be + detected by ``Flask.add_url_rule``. :pr:`2316` +- Error handling will try handlers registered for ``blueprint, code``, + ``app, code``, ``blueprint, exception``, ``app, exception``. + :pr:`2314` +- ``Cookie`` is added to the response's ``Vary`` header if the session + is accessed at all during the request (and not deleted). :pr:`2288` +- ``Flask.test_request_context`` accepts ``subdomain`` and + ``url_scheme`` arguments for use when building the base URL. + :pr:`1621` +- Set ``APPLICATION_ROOT`` to ``'/'`` by default. This was already the + implicit default when it was set to ``None``. +- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode. + ``BadRequestKeyError`` has a message with the bad key in debug mode + instead of the generic bad request message. :pr:`2348` +- Allow registering new tags with ``TaggedJSONSerializer`` to support + storing other types in the session cookie. :pr:`2352` +- Only open the session if the request has not been pushed onto the + context stack yet. This allows ``stream_with_context`` generators to + access the same session that the containing view uses. :pr:`2354` +- Add ``json`` keyword argument for the test client request methods. + This will dump the given object as JSON and set the appropriate + content type. :pr:`2358` +- Extract JSON handling to a mixin applied to both the ``Request`` and + ``Response`` classes. This adds the ``Response.is_json`` and + ``Response.get_json`` methods to the response to make testing JSON + response much easier. :pr:`2358` +- Removed error handler caching because it caused unexpected results + for some exception inheritance hierarchies. Register handlers + explicitly for each exception if you want to avoid traversing the + MRO. :pr:`2362` +- Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374` +- Template auto reloading will honor debug mode even even if + ``Flask.jinja_env`` was already accessed. :pr:`2373` +- The following old deprecated code was removed. :issue:`2385` + + - ``flask.ext`` - import extensions directly by their name instead + of through the ``flask.ext`` namespace. For example, + ``import flask.ext.sqlalchemy`` becomes + ``import flask_sqlalchemy``. + - ``Flask.init_jinja_globals`` - extend + ``Flask.create_jinja_environment`` instead. + - ``Flask.error_handlers`` - tracked by + ``Flask.error_handler_spec``, use ``Flask.errorhandler`` + to register handlers. + - ``Flask.request_globals_class`` - use + ``Flask.app_ctx_globals_class`` instead. + - ``Flask.static_path`` - use ``Flask.static_url_path`` instead. + - ``Request.module`` - use ``Request.blueprint`` instead. + +- The ``Request.json`` property is no longer deprecated. :issue:`1421` +- Support passing a ``EnvironBuilder`` or ``dict`` to + ``test_client.open``. :pr:`2412` +- The ``flask`` command and ``Flask.run`` will load environment + variables from ``.env`` and ``.flaskenv`` files if python-dotenv is + installed. :pr:`2416` +- When passing a full URL to the test client, the scheme in the URL is + used instead of ``PREFERRED_URL_SCHEME``. :pr:`2430` +- ``Flask.logger`` has been simplified. ``LOGGER_NAME`` and + ``LOGGER_HANDLER_POLICY`` config was removed. The logger is always + named ``flask.app``. The level is only set on first access, it + doesn't check ``Flask.debug`` each time. Only one format is used, + not different ones depending on ``Flask.debug``. No handlers are + removed, and a handler is only added if no handlers are already + configured. :pr:`2436` +- Blueprint view function names may not contain dots. :pr:`2450` +- Fix a ``ValueError`` caused by invalid ``Range`` requests in some + cases. :issue:`2526` +- The development server uses threads by default. :pr:`2529` +- Loading config files with ``silent=True`` will ignore ``ENOTDIR`` + errors. :pr:`2581` +- Pass ``--cert`` and ``--key`` options to ``flask run`` to run the + development server over HTTPS. :pr:`2606` +- Added ``SESSION_COOKIE_SAMESITE`` to control the ``SameSite`` + attribute on the session cookie. :pr:`2607` +- Added ``Flask.test_cli_runner`` to create a Click runner that can + invoke Flask CLI commands for testing. :pr:`2636` +- Subdomain matching is disabled by default and setting + ``SERVER_NAME`` does not implicitly enable it. It can be enabled by + passing ``subdomain_matching=True`` to the ``Flask`` constructor. + :pr:`2635` +- A single trailing slash is stripped from the blueprint + ``url_prefix`` when it is registered with the app. :pr:`2629` +- ``Request.get_json`` doesn't cache the result if parsing fails when + ``silent`` is true. :issue:`2651` +- ``Request.get_json`` no longer accepts arbitrary encodings. Incoming + JSON should be encoded using UTF-8 per :rfc:`8259`, but Flask will + autodetect UTF-8, -16, or -32. :pr:`2691` +- Added ``MAX_COOKIE_SIZE`` and ``Response.max_cookie_size`` to + control when Werkzeug warns about large cookies that browsers may + ignore. :pr:`2693` +- Updated documentation theme to make docs look better in small + windows. :pr:`2709` +- Rewrote the tutorial docs and example project to take a more + structured approach to help new users avoid common pitfalls. + :pr:`2676` + + +Version 0.12.5 +-------------- + +Released 2020-02-10 + +- Pin Werkzeug to < 1.0.0. :issue:`3497` + + +Version 0.12.4 +-------------- + +Released 2018-04-29 + +- Repackage 0.12.3 to fix package layout issue. :issue:`2728` + + +Version 0.12.3 +-------------- + +Released 2018-04-26 + +- ``Request.get_json`` no longer accepts arbitrary encodings. + Incoming JSON should be encoded using UTF-8 per :rfc:`8259`, but + Flask will autodetect UTF-8, -16, or -32. :issue:`2692` +- Fix a Python warning about imports when using ``python -m flask``. + :issue:`2666` +- Fix a ``ValueError`` caused by invalid ``Range`` requests in some + cases. + + +Version 0.12.2 +-------------- + +Released 2017-05-16 + +- Fix a bug in ``safe_join`` on Windows. + + +Version 0.12.1 +-------------- + +Released 2017-03-31 + +- Prevent ``flask run`` from showing a ``NoAppException`` when an + ``ImportError`` occurs within the imported application module. +- Fix encoding behavior of ``app.config.from_pyfile`` for Python 3. + :issue:`2118` +- Use the ``SERVER_NAME`` config if it is present as default values + for ``app.run``. :issue:`2109`, :pr:`2152` +- Call ``ctx.auto_pop`` with the exception object instead of ``None``, + in the event that a ``BaseException`` such as ``KeyboardInterrupt`` + is raised in a request handler. + + +Version 0.12 +------------ + +Released 2016-12-21, codename Punsch + +- The cli command now responds to ``--version``. +- Mimetype guessing and ETag generation for file-like objects in + ``send_file`` has been removed. :issue:`104`, :pr`1849` +- Mimetype guessing in ``send_file`` now fails loudly and doesn't fall + back to ``application/octet-stream``. :pr:`1988` +- Make ``flask.safe_join`` able to join multiple paths like + ``os.path.join`` :pr:`1730` +- Revert a behavior change that made the dev server crash instead of + returning an Internal Server Error. :pr:`2006` +- Correctly invoke response handlers for both regular request + dispatching as well as error handlers. +- Disable logger propagation by default for the app logger. +- Add support for range requests in ``send_file``. +- ``app.test_client`` includes preset default environment, which can + now be directly set, instead of per ``client.get``. +- Fix crash when running under PyPy3. :pr:`1814` + + +Version 0.11.1 +-------------- + +Released 2016-06-07 + +- Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from + working. :pr:`1872` + + +Version 0.11 +------------ + +Released 2016-05-29, codename Absinthe + +- Added support to serializing top-level arrays to ``jsonify``. This + introduces a security risk in ancient browsers. +- Added before_render_template signal. +- Added ``**kwargs`` to ``Flask.test_client`` to support passing + additional keyword arguments to the constructor of + ``Flask.test_client_class``. +- Added ``SESSION_REFRESH_EACH_REQUEST`` config key that controls the + set-cookie behavior. If set to ``True`` a permanent session will be + refreshed each request and get their lifetime extended, if set to + ``False`` it will only be modified if the session actually modifies. + Non permanent sessions are not affected by this and will always + expire if the browser window closes. +- Made Flask support custom JSON mimetypes for incoming data. +- Added support for returning tuples in the form ``(response, + headers)`` from a view function. +- Added ``Config.from_json``. +- Added ``Flask.config_class``. +- Added ``Config.get_namespace``. +- Templates are no longer automatically reloaded outside of debug + mode. This can be configured with the new ``TEMPLATES_AUTO_RELOAD`` + config key. +- Added a workaround for a limitation in Python 3.3's namespace + loader. +- Added support for explicit root paths when using Python 3.3's + namespace packages. +- Added ``flask`` and the ``flask.cli`` module to start the + local debug server through the click CLI system. This is recommended + over the old ``flask.run()`` method as it works faster and more + reliable due to a different design and also replaces + ``Flask-Script``. +- Error handlers that match specific classes are now checked first, + thereby allowing catching exceptions that are subclasses of HTTP + exceptions (in ``werkzeug.exceptions``). This makes it possible for + an extension author to create exceptions that will by default result + in the HTTP error of their choosing, but may be caught with a custom + error handler if desired. +- Added ``Config.from_mapping``. +- Flask will now log by default even if debug is disabled. The log + format is now hardcoded but the default log handling can be disabled + through the ``LOGGER_HANDLER_POLICY`` configuration key. +- Removed deprecated module functionality. +- Added the ``EXPLAIN_TEMPLATE_LOADING`` config flag which when + enabled will instruct Flask to explain how it locates templates. + This should help users debug when the wrong templates are loaded. +- Enforce blueprint handling in the order they were registered for + template loading. +- Ported test suite to py.test. +- Deprecated ``request.json`` in favour of ``request.get_json()``. +- Add "pretty" and "compressed" separators definitions in jsonify() + method. Reduces JSON response size when + ``JSONIFY_PRETTYPRINT_REGULAR=False`` by removing unnecessary white + space included by default after separators. +- JSON responses are now terminated with a newline character, because + it is a convention that UNIX text files end with a newline and some + clients don't deal well when this newline is missing. :pr:`1262` +- The automatically provided ``OPTIONS`` method is now correctly + disabled if the user registered an overriding rule with the + lowercase-version ``options``. :issue:`1288` +- ``flask.json.jsonify`` now supports the ``datetime.date`` type. + :pr:`1326` +- Don't leak exception info of already caught exceptions to context + teardown handlers. :pr:`1393` +- Allow custom Jinja environment subclasses. :pr:`1422` +- Updated extension dev guidelines. +- ``flask.g`` now has ``pop()`` and ``setdefault`` methods. +- Turn on autoescape for ``flask.templating.render_template_string`` + by default. :pr:`1515` +- ``flask.ext`` is now deprecated. :pr:`1484` +- ``send_from_directory`` now raises BadRequest if the filename is + invalid on the server OS. :pr:`1763` +- Added the ``JSONIFY_MIMETYPE`` configuration variable. :pr:`1728` +- Exceptions during teardown handling will no longer leave bad + application contexts lingering around. +- Fixed broken ``test_appcontext_signals()`` test case. +- Raise an ``AttributeError`` in ``helpers.find_package`` with a + useful message explaining why it is raised when a :pep:`302` import + hook is used without an ``is_package()`` method. +- Fixed an issue causing exceptions raised before entering a request + or app context to be passed to teardown handlers. +- Fixed an issue with query parameters getting removed from requests + in the test client when absolute URLs were requested. +- Made ``@before_first_request`` into a decorator as intended. +- Fixed an etags bug when sending a file streams with a name. +- Fixed ``send_from_directory`` not expanding to the application root + path correctly. +- Changed logic of before first request handlers to flip the flag + after invoking. This will allow some uses that are potentially + dangerous but should probably be permitted. +- Fixed Python 3 bug when a handler from + ``app.url_build_error_handlers`` reraises the ``BuildError``. + + +Version 0.10.1 +-------------- + +Released 2013-06-14 + +- Fixed an issue where ``|tojson`` was not quoting single quotes which + made the filter not work properly in HTML attributes. Now it's + possible to use that filter in single quoted attributes. This should + make using that filter with angular.js easier. +- Added support for byte strings back to the session system. This + broke compatibility with the common case of people putting binary + data for token verification into the session. +- Fixed an issue where registering the same method twice for the same + endpoint would trigger an exception incorrectly. + + +Version 0.10 +------------ + +Released 2013-06-13, codename Limoncello + +- Changed default cookie serialization format from pickle to JSON to + limit the impact an attacker can do if the secret key leaks. +- Added ``template_test`` methods in addition to the already existing + ``template_filter`` method family. +- Added ``template_global`` methods in addition to the already + existing ``template_filter`` method family. +- Set the content-length header for x-sendfile. +- ``tojson`` filter now does not escape script blocks in HTML5 + parsers. +- ``tojson`` used in templates is now safe by default. This was + allowed due to the different escaping behavior. +- Flask will now raise an error if you attempt to register a new + function on an already used endpoint. +- Added wrapper module around simplejson and added default + serialization of datetime objects. This allows much easier + customization of how JSON is handled by Flask or any Flask + extension. +- Removed deprecated internal ``flask.session`` module alias. Use + ``flask.sessions`` instead to get the session module. This is not to + be confused with ``flask.session`` the session proxy. +- Templates can now be rendered without request context. The behavior + is slightly different as the ``request``, ``session`` and ``g`` + objects will not be available and blueprint's context processors are + not called. +- The config object is now available to the template as a real global + and not through a context processor which makes it available even in + imported templates by default. +- Added an option to generate non-ascii encoded JSON which should + result in less bytes being transmitted over the network. It's + disabled by default to not cause confusion with existing libraries + that might expect ``flask.json.dumps`` to return bytes by default. +- ``flask.g`` is now stored on the app context instead of the request + context. +- ``flask.g`` now gained a ``get()`` method for not erroring out on + non existing items. +- ``flask.g`` now can be used with the ``in`` operator to see what's + defined and it now is iterable and will yield all attributes stored. +- ``flask.Flask.request_globals_class`` got renamed to + ``flask.Flask.app_ctx_globals_class`` which is a better name to what + it does since 0.10. +- ``request``, ``session`` and ``g`` are now also added as proxies to + the template context which makes them available in imported + templates. One has to be very careful with those though because + usage outside of macros might cause caching. +- Flask will no longer invoke the wrong error handlers if a proxy + exception is passed through. +- Added a workaround for chrome's cookies in localhost not working as + intended with domain names. +- Changed logic for picking defaults for cookie values from sessions + to work better with Google Chrome. +- Added ``message_flashed`` signal that simplifies flashing testing. +- Added support for copying of request contexts for better working + with greenlets. +- Removed custom JSON HTTP exception subclasses. If you were relying + on them you can reintroduce them again yourself trivially. Using + them however is strongly discouraged as the interface was flawed. +- Python requirements changed: requiring Python 2.6 or 2.7 now to + prepare for Python 3.3 port. +- Changed how the teardown system is informed about exceptions. This + is now more reliable in case something handles an exception halfway + through the error handling process. +- Request context preservation in debug mode now keeps the exception + information around which means that teardown handlers are able to + distinguish error from success cases. +- Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. +- Flask now orders JSON keys by default to not trash HTTP caches due + to different hash seeds between different workers. +- Added ``appcontext_pushed`` and ``appcontext_popped`` signals. +- The builtin run method now takes the ``SERVER_NAME`` into account + when picking the default port to run on. +- Added ``flask.request.get_json()`` as a replacement for the old + ``flask.request.json`` property. + + +Version 0.9 +----------- + +Released 2012-07-01, codename Campari + +- The ``Request.on_json_loading_failed`` now returns a JSON formatted + response by default. +- The ``url_for`` function now can generate anchors to the generated + links. +- The ``url_for`` function now can also explicitly generate URL rules + specific to a given HTTP method. +- Logger now only returns the debug log setting if it was not set + explicitly. +- Unregister a circular dependency between the WSGI environment and + the request object when shutting down the request. This means that + environ ``werkzeug.request`` will be ``None`` after the response was + returned to the WSGI server but has the advantage that the garbage + collector is not needed on CPython to tear down the request unless + the user created circular dependencies themselves. +- Session is now stored after callbacks so that if the session payload + is stored in the session you can still modify it in an after request + callback. +- The ``Flask`` class will avoid importing the provided import name if + it can (the required first parameter), to benefit tools which build + Flask instances programmatically. The Flask class will fall back to + using import on systems with custom module hooks, e.g. Google App + Engine, or when the import name is inside a zip archive (usually an + egg) prior to Python 2.7. +- Blueprints now have a decorator to add custom template filters + application wide, ``Blueprint.app_template_filter``. +- The Flask and Blueprint classes now have a non-decorator method for + adding custom template filters application wide, + ``Flask.add_template_filter`` and + ``Blueprint.add_app_template_filter``. +- The ``get_flashed_messages`` function now allows rendering flashed + message categories in separate blocks, through a ``category_filter`` + argument. +- The ``Flask.run`` method now accepts ``None`` for ``host`` and + ``port`` arguments, using default values when ``None``. This allows + for calling run using configuration values, e.g. + ``app.run(app.config.get('MYHOST'), app.config.get('MYPORT'))``, + with proper behavior whether or not a config file is provided. +- The ``render_template`` method now accepts a either an iterable of + template names or a single template name. Previously, it only + accepted a single template name. On an iterable, the first template + found is rendered. +- Added ``Flask.app_context`` which works very similar to the request + context but only provides access to the current application. This + also adds support for URL generation without an active request + context. +- View functions can now return a tuple with the first instance being + an instance of ``Response``. This allows for returning + ``jsonify(error="error msg"), 400`` from a view function. +- ``Flask`` and ``Blueprint`` now provide a ``get_send_file_max_age`` + hook for subclasses to override behavior of serving static files + from Flask when using ``Flask.send_static_file`` (used for the + default static file handler) and ``helpers.send_file``. This hook is + provided a filename, which for example allows changing cache + controls by file extension. The default max-age for ``send_file`` + and static files can be configured through a new + ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, which is used + in the default ``get_send_file_max_age`` implementation. +- Fixed an assumption in sessions implementation which could break + message flashing on sessions implementations which use external + storage. +- Changed the behavior of tuple return values from functions. They are + no longer arguments to the response object, they now have a defined + meaning. +- Added ``Flask.request_globals_class`` to allow a specific class to + be used on creation of the ``g`` instance of each request. +- Added ``required_methods`` attribute to view functions to force-add + methods on registration. +- Added ``flask.after_this_request``. +- Added ``flask.stream_with_context`` and the ability to push contexts + multiple times without producing unexpected behavior. + + +Version 0.8.1 +------------- + +Released 2012-07-01 + +- Fixed an issue with the undocumented ``flask.session`` module to not + work properly on Python 2.5. It should not be used but did cause + some problems for package managers. + + +Version 0.8 +----------- + +Released 2011-09-29, codename Rakija + +- Refactored session support into a session interface so that the + implementation of the sessions can be changed without having to + override the Flask class. +- Empty session cookies are now deleted properly automatically. +- View functions can now opt out of getting the automatic OPTIONS + implementation. +- HTTP exceptions and Bad Request errors can now be trapped so that + they show up normally in the traceback. +- Flask in debug mode is now detecting some common problems and tries + to warn you about them. +- Flask in debug mode will now complain with an assertion error if a + view was attached after the first request was handled. This gives + earlier feedback when users forget to import view code ahead of + time. +- Added the ability to register callbacks that are only triggered once + at the beginning of the first request with + ``Flask.before_first_request``. +- Malformed JSON data will now trigger a bad request HTTP exception + instead of a value error which usually would result in a 500 + internal server error if not handled. This is a backwards + incompatible change. +- Applications now not only have a root path where the resources and + modules are located but also an instance path which is the + designated place to drop files that are modified at runtime (uploads + etc.). Also this is conceptually only instance depending and outside + version control so it's the perfect place to put configuration files + etc. +- Added the ``APPLICATION_ROOT`` configuration variable. +- Implemented ``TestClient.session_transaction`` to easily modify + sessions from the test environment. +- Refactored test client internally. The ``APPLICATION_ROOT`` + configuration variable as well as ``SERVER_NAME`` are now properly + used by the test client as defaults. +- Added ``View.decorators`` to support simpler decorating of pluggable + (class-based) views. +- Fixed an issue where the test client if used with the "with" + statement did not trigger the execution of the teardown handlers. +- Added finer control over the session cookie parameters. +- HEAD requests to a method view now automatically dispatch to the + ``get`` method if no handler was implemented. +- Implemented the virtual ``flask.ext`` package to import extensions + from. +- The context preservation on exceptions is now an integral component + of Flask itself and no longer of the test client. This cleaned up + some internal logic and lowers the odds of runaway request contexts + in unittests. +- Fixed the Jinja2 environment's ``list_templates`` method not + returning the correct names when blueprints or modules were + involved. + + +Version 0.7.2 +------------- + +Released 2011-07-06 + +- Fixed an issue with URL processors not properly working on + blueprints. + + +Version 0.7.1 +------------- + +Released 2011-06-29 + +- Added missing future import that broke 2.5 compatibility. +- Fixed an infinite redirect issue with blueprints. + + +Version 0.7 +----------- + +Released 2011-06-28, codename Grappa + +- Added ``Flask.make_default_options_response`` which can be used by + subclasses to alter the default behavior for ``OPTIONS`` responses. +- Unbound locals now raise a proper ``RuntimeError`` instead of an + ``AttributeError``. +- Mimetype guessing and etag support based on file objects is now + deprecated for ``send_file`` because it was unreliable. Pass + filenames instead or attach your own etags and provide a proper + mimetype by hand. +- Static file handling for modules now requires the name of the static + folder to be supplied explicitly. The previous autodetection was not + reliable and caused issues on Google's App Engine. Until 1.0 the old + behavior will continue to work but issue dependency warnings. +- Fixed a problem for Flask to run on jython. +- Added a ``PROPAGATE_EXCEPTIONS`` configuration variable that can be + used to flip the setting of exception propagation which previously + was linked to ``DEBUG`` alone and is now linked to either ``DEBUG`` + or ``TESTING``. +- Flask no longer internally depends on rules being added through the + ``add_url_rule`` function and can now also accept regular werkzeug + rules added to the url map. +- Added an ``endpoint`` method to the flask application object which + allows one to register a callback to an arbitrary endpoint with a + decorator. +- Use Last-Modified for static file sending instead of Date which was + incorrectly introduced in 0.6. +- Added ``create_jinja_loader`` to override the loader creation + process. +- Implemented a silent flag for ``config.from_pyfile``. +- Added ``teardown_request`` decorator, for functions that should run + at the end of a request regardless of whether an exception occurred. + Also the behavior for ``after_request`` was changed. It's now no + longer executed when an exception is raised. +- Implemented ``has_request_context``. +- Deprecated ``init_jinja_globals``. Override the + ``Flask.create_jinja_environment`` method instead to achieve the + same functionality. +- Added ``safe_join``. +- The automatic JSON request data unpacking now looks at the charset + mimetype parameter. +- Don't modify the session on ``get_flashed_messages`` if there are no + messages in the session. +- ``before_request`` handlers are now able to abort requests with + errors. +- It is not possible to define user exception handlers. That way you + can provide custom error messages from a central hub for certain + errors that might occur during request processing (for instance + database connection errors, timeouts from remote resources etc.). +- Blueprints can provide blueprint specific error handlers. +- Implemented generic class-based views. + + +Version 0.6.1 +------------- + +Released 2010-12-31 + +- Fixed an issue where the default ``OPTIONS`` response was not + exposing all valid methods in the ``Allow`` header. +- Jinja2 template loading syntax now allows "./" in front of a + template load path. Previously this caused issues with module + setups. +- Fixed an issue where the subdomain setting for modules was ignored + for the static folder. +- Fixed a security problem that allowed clients to download arbitrary + files if the host server was a windows based operating system and + the client uses backslashes to escape the directory the files where + exposed from. + + +Version 0.6 +----------- + +Released 2010-07-27, codename Whisky + +- After request functions are now called in reverse order of + registration. +- OPTIONS is now automatically implemented by Flask unless the + application explicitly adds 'OPTIONS' as method to the URL rule. In + this case no automatic OPTIONS handling kicks in. +- Static rules are now even in place if there is no static folder for + the module. This was implemented to aid GAE which will remove the + static folder if it's part of a mapping in the .yml file. +- ``Flask.config`` is now available in the templates as ``config``. +- Context processors will no longer override values passed directly to + the render function. +- Added the ability to limit the incoming request data with the new + ``MAX_CONTENT_LENGTH`` configuration value. +- The endpoint for the ``Module.add_url_rule`` method is now optional + to be consistent with the function of the same name on the + application object. +- Added a ``make_response`` function that simplifies creating response + object instances in views. +- Added signalling support based on blinker. This feature is currently + optional and supposed to be used by extensions and applications. If + you want to use it, make sure to have ``blinker`` installed. +- Refactored the way URL adapters are created. This process is now + fully customizable with the ``Flask.create_url_adapter`` method. +- Modules can now register for a subdomain instead of just an URL + prefix. This makes it possible to bind a whole module to a + configurable subdomain. + + +Version 0.5.2 +------------- + +Released 2010-07-15 + +- Fixed another issue with loading templates from directories when + modules were used. + + +Version 0.5.1 +------------- + +Released 2010-07-06 + +- Fixes an issue with template loading from directories when modules + where used. + + +Version 0.5 +----------- + +Released 2010-07-06, codename Calvados + +- Fixed a bug with subdomains that was caused by the inability to + specify the server name. The server name can now be set with the + ``SERVER_NAME`` config key. This key is now also used to set the + session cookie cross-subdomain wide. +- Autoescaping is no longer active for all templates. Instead it is + only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside + templates this behavior can be changed with the ``autoescape`` tag. +- Refactored Flask internally. It now consists of more than a single + file. +- ``send_file`` now emits etags and has the ability to do conditional + responses builtin. +- (temporarily) dropped support for zipped applications. This was a + rarely used feature and led to some confusing behavior. +- Added support for per-package template and static-file directories. +- Removed support for ``create_jinja_loader`` which is no longer used + in 0.5 due to the improved module support. +- Added a helper function to expose files from any directory. + + +Version 0.4 +----------- + +Released 2010-06-18, codename Rakia + +- Added the ability to register application wide error handlers from + modules. +- ``Flask.after_request`` handlers are now also invoked if the request + dies with an exception and an error handling page kicks in. +- Test client has not the ability to preserve the request context for + a little longer. This can also be used to trigger custom requests + that do not pop the request stack for testing. +- Because the Python standard library caches loggers, the name of the + logger is configurable now to better support unittests. +- Added ``TESTING`` switch that can activate unittesting helpers. +- The logger switches to ``DEBUG`` mode now if debug is enabled. + + +Version 0.3.1 +------------- + +Released 2010-05-28 + +- Fixed a error reporting bug with ``Config.from_envvar``. +- Removed some unused code. +- Release does no longer include development leftover files (.git + folder for themes, built documentation in zip and pdf file and some + .pyc files) + + +Version 0.3 +----------- + +Released 2010-05-28, codename Schnaps + +- Added support for categories for flashed messages. +- The application now configures a ``logging.Handler`` and will log + request handling exceptions to that logger when not in debug mode. + This makes it possible to receive mails on server errors for + example. +- Added support for context binding that does not require the use of + the with statement for playing in the console. +- The request context is now available within the with statement + making it possible to further push the request context or pop it. +- Added support for configurations. + + +Version 0.2 +----------- + +Released 2010-05-12, codename J?germeister + +- Various bugfixes +- Integrated JSON support +- Added ``get_template_attribute`` helper function. +- ``Flask.add_url_rule`` can now also register a view function. +- Refactored internal request dispatching. +- Server listens on 127.0.0.1 by default now to fix issues with + chrome. +- Added external URL support. +- Added support for ``send_file``. +- Module support and internal request handling refactoring to better + support pluggable applications. +- Sessions can be set to be permanent now on a per-session basis. +- Better error reporting on missing secret keys. +- Added support for Google Appengine. + + +Version 0.1 +----------- + +Released 2010-04-16 + +- First public preview release. diff --git a/test/fixtures/whole_applications/flask/CODE_OF_CONDUCT.md b/test/fixtures/whole_applications/flask/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f4ba197 --- /dev/null +++ b/test/fixtures/whole_applications/flask/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at report@palletsprojects.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/test/fixtures/whole_applications/flask/CONTRIBUTING.rst b/test/fixtures/whole_applications/flask/CONTRIBUTING.rst new file mode 100644 index 0000000..fed4497 --- /dev/null +++ b/test/fixtures/whole_applications/flask/CONTRIBUTING.rst @@ -0,0 +1,238 @@ +How to contribute to Flask +========================== + +Thank you for considering contributing to Flask! + + +Support questions +----------------- + +Please don't use the issue tracker for this. The issue tracker is a tool +to address bugs and feature requests in Flask itself. Use one of the +following resources for questions about using Flask or issues with your +own code: + +- The ``#questions`` channel on our Discord chat: + https://discord.gg/pallets +- Ask on `Stack Overflow`_. Search with Google first using: + ``site:stackoverflow.com flask {search term, exception message, etc.}`` +- Ask on our `GitHub Discussions`_ for long term discussion or larger + questions. + +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent +.. _GitHub Discussions: https://github.com/pallets/flask/discussions + + +Reporting issues +---------------- + +Include the following information in your post: + +- Describe what you expected to happen. +- If possible, include a `minimal reproducible example`_ to help us + identify the issue. This also helps check that the issue is not with + your own code. +- Describe what actually happened. Include the full traceback if there + was an exception. +- List your Python and Flask versions. If possible, check if this + issue is already fixed in the latest releases or the latest code in + the repository. + +.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example + + +Submitting patches +------------------ + +If there is not an open issue for what you want to submit, prefer +opening one for discussion before working on a PR. You can work on any +issue that doesn't have an open PR linked to it or a maintainer assigned +to it. These show up in the sidebar. No need to ask if you can work on +an issue that interests you. + +Include the following in your patch: + +- Use `Black`_ to format your code. This and other tools will run + automatically if you install `pre-commit`_ using the instructions + below. +- Include tests if your patch adds or changes code. Make sure the test + fails without your patch. +- Update any relevant docs pages and docstrings. Docs pages and + docstrings should be wrapped at 72 characters. +- Add an entry in ``CHANGES.rst``. Use the same style as other + entries. Also include ``.. versionchanged::`` inline changelogs in + relevant docstrings. + +.. _Black: https://black.readthedocs.io +.. _pre-commit: https://pre-commit.com + + +First time setup using GitHub Codespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`GitHub Codespaces`_ creates a development environment that is already set up for the +project. By default it opens in Visual Studio Code for the Web, but this can +be changed in your GitHub profile settings to use Visual Studio Code or JetBrains +PyCharm on your local computer. + +- Make sure you have a `GitHub account`_. +- From the project's repository page, click the green "Code" button and then "Create + codespace on main". +- The codespace will be set up, then Visual Studio Code will open. However, you'll + need to wait a bit longer for the Python extension to be installed. You'll know it's + ready when the terminal at the bottom shows that the virtualenv was activated. +- Check out a branch and `start coding`_. + +.. _GitHub Codespaces: https://docs.github.com/en/codespaces +.. _devcontainer: https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers + +First time setup in your local environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure you have a `GitHub account`_. +- Download and install the `latest version of git`_. +- Configure git with your `username`_ and `email`_. + + .. code-block:: text + + $ git config --global user.name 'your name' + $ git config --global user.email 'your email' + +- Fork Flask to your GitHub account by clicking the `Fork`_ button. +- `Clone`_ your fork locally, replacing ``your-username`` in the command below with + your actual username. + + .. code-block:: text + + $ git clone https://github.com/your-username/flask + $ cd flask + +- Create a virtualenv. Use the latest version of Python. + + - Linux/macOS + + .. code-block:: text + + $ python3 -m venv .venv + $ . .venv/bin/activate + + - Windows + + .. code-block:: text + + > py -3 -m venv .venv + > .venv\Scripts\activate + +- Install the development dependencies, then install Flask in editable mode. + + .. code-block:: text + + $ python -m pip install -U pip + $ pip install -r requirements/dev.txt && pip install -e . + +- Install the pre-commit hooks. + + .. code-block:: text + + $ pre-commit install --install-hooks + +.. _GitHub account: https://github.com/join +.. _latest version of git: https://git-scm.com/downloads +.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git +.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address +.. _Fork: https://github.com/pallets/flask/fork +.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork + +.. _start coding: + +Start coding +~~~~~~~~~~~~ + +- Create a branch to identify the issue you would like to work on. If you're + submitting a bug or documentation fix, branch off of the latest ".x" branch. + + .. code-block:: text + + $ git fetch origin + $ git checkout -b your-branch-name origin/2.0.x + + If you're submitting a feature addition or change, branch off of the "main" branch. + + .. code-block:: text + + $ git fetch origin + $ git checkout -b your-branch-name origin/main + +- Using your favorite editor, make your changes, `committing as you go`_. + + - If you are in a codespace, you will be prompted to `create a fork`_ the first + time you make a commit. Enter ``Y`` to continue. + +- Include tests that cover any code changes you make. Make sure the test fails without + your patch. Run the tests as described below. +- Push your commits to your fork on GitHub and `create a pull request`_. Link to the + issue being addressed with ``fixes #123`` in the pull request description. + + .. code-block:: text + + $ git push --set-upstream origin your-branch-name + +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _create a fork: https://docs.github.com/en/codespaces/developing-in-codespaces/using-source-control-in-your-codespace#about-automatic-forking +.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request + +.. _Running the tests: + +Running the tests +~~~~~~~~~~~~~~~~~ + +Run the basic test suite with pytest. + +.. code-block:: text + + $ pytest + +This runs the tests for the current environment, which is usually +sufficient. CI will run the full suite when you submit your pull +request. You can run the full test suite with tox if you don't want to +wait. + +.. code-block:: text + + $ tox + + +Running test coverage +~~~~~~~~~~~~~~~~~~~~~ + +Generating a report of lines that do not have test coverage can indicate +where to start contributing. Run ``pytest`` using ``coverage`` and +generate a report. + +If you are using GitHub Codespaces, ``coverage`` is already installed +so you can skip the installation command. + +.. code-block:: text + + $ pip install coverage + $ coverage run -m pytest + $ coverage html + +Open ``htmlcov/index.html`` in your browser to explore the report. + +Read more about `coverage `__. + + +Building the docs +~~~~~~~~~~~~~~~~~ + +Build the docs in the ``docs`` directory using Sphinx. + +.. code-block:: text + + $ cd docs + $ make html + +Open ``_build/html/index.html`` in your browser to view the docs. + +Read more about `Sphinx `__. diff --git a/test/fixtures/whole_applications/flask/LICENSE.txt b/test/fixtures/whole_applications/flask/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/whole_applications/flask/README.md b/test/fixtures/whole_applications/flask/README.md new file mode 100644 index 0000000..8ab2d59 --- /dev/null +++ b/test/fixtures/whole_applications/flask/README.md @@ -0,0 +1,65 @@ +# Flask + +Flask is a lightweight [WSGI][] web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around [Werkzeug][] +and [Jinja][], and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +[WSGI]: https://wsgi.readthedocs.io/ +[Werkzeug]: https://werkzeug.palletsprojects.com/ +[Jinja]: https://jinja.palletsprojects.com/ + + +## Installing + +Install and update from [PyPI][] using an installer such as [pip][]: + +``` +$ pip install -U Flask +``` + +[PyPI]: https://pypi.org/project/Flask/ +[pip]: https://pip.pypa.io/en/stable/getting-started/ + + +## A Simple Example + +```python +# save this as app.py +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" +``` + +``` +$ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + + +## Contributing + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the [contributing guidelines][]. + +[contributing guidelines]: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst + + +## Donate + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate diff --git a/test/fixtures/whole_applications/flask/docs/Makefile b/test/fixtures/whole_applications/flask/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/test/fixtures/whole_applications/flask/docs/_static/debugger.png b/test/fixtures/whole_applications/flask/docs/_static/debugger.png new file mode 100644 index 0000000..7d4181f Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/_static/debugger.png differ diff --git a/test/fixtures/whole_applications/flask/docs/_static/flask-horizontal.png b/test/fixtures/whole_applications/flask/docs/_static/flask-horizontal.png new file mode 100644 index 0000000..a0df2c6 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/_static/flask-horizontal.png differ diff --git a/test/fixtures/whole_applications/flask/docs/_static/flask-vertical.png b/test/fixtures/whole_applications/flask/docs/_static/flask-vertical.png new file mode 100644 index 0000000..d1fd149 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/_static/flask-vertical.png differ diff --git a/test/fixtures/whole_applications/flask/docs/_static/pycharm-run-config.png b/test/fixtures/whole_applications/flask/docs/_static/pycharm-run-config.png new file mode 100644 index 0000000..ad02554 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/_static/pycharm-run-config.png differ diff --git a/test/fixtures/whole_applications/flask/docs/_static/shortcut-icon.png b/test/fixtures/whole_applications/flask/docs/_static/shortcut-icon.png new file mode 100644 index 0000000..4d3e6c3 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/_static/shortcut-icon.png differ diff --git a/test/fixtures/whole_applications/flask/docs/api.rst b/test/fixtures/whole_applications/flask/docs/api.rst new file mode 100644 index 0000000..1aa8048 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/api.rst @@ -0,0 +1,717 @@ +API +=== + +.. module:: flask + +This part of the documentation covers all the interfaces of Flask. For +parts where Flask depends on external libraries, we document the most +important right here and provide links to the canonical documentation. + + +Application Object +------------------ + +.. autoclass:: Flask + :members: + :inherited-members: + + +Blueprint Objects +----------------- + +.. autoclass:: Blueprint + :members: + :inherited-members: + +Incoming Request Data +--------------------- + +.. autoclass:: Request + :members: + :inherited-members: + :exclude-members: json_module + +.. attribute:: request + + To access incoming request data, you can use the global `request` + object. Flask parses incoming request data for you and gives you + access to it through that global object. Internally Flask makes + sure that you always get the correct data for the active thread if you + are in a multithreaded environment. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + The request object is an instance of a :class:`~flask.Request`. + + +Response Objects +---------------- + +.. autoclass:: flask.Response + :members: + :inherited-members: + :exclude-members: json_module + +Sessions +-------- + +If you have set :attr:`Flask.secret_key` (or configured it from +:data:`SECRET_KEY`) you can use sessions in Flask applications. A session makes +it possible to remember information from one request to another. The way Flask +does this is by using a signed cookie. The user can look at the session +contents, but can't modify it unless they know the secret key, so make sure to +set that to something complex and unguessable. + +To access the current session you can use the :class:`session` object: + +.. class:: session + + The session object works pretty much like an ordinary dict, with the + difference that it keeps track of modifications. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + The following attributes are interesting: + + .. attribute:: new + + ``True`` if the session is new, ``False`` otherwise. + + .. attribute:: modified + + ``True`` if the session object detected a modification. Be advised + that modifications on mutable structures are not picked up + automatically, in that situation you have to explicitly set the + attribute to ``True`` yourself. Here an example:: + + # this change is not picked up because a mutable object (here + # a list) is changed. + session['objects'].append(42) + # so mark it as modified yourself + session.modified = True + + .. attribute:: permanent + + If set to ``True`` the session lives for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to ``False`` (which is the default) the + session will be deleted when the user closes the browser. + + +Session Interface +----------------- + +.. versionadded:: 0.8 + +The session interface provides a simple way to replace the session +implementation that Flask is using. + +.. currentmodule:: flask.sessions + +.. autoclass:: SessionInterface + :members: + +.. autoclass:: SecureCookieSessionInterface + :members: + +.. autoclass:: SecureCookieSession + :members: + +.. autoclass:: NullSession + :members: + +.. autoclass:: SessionMixin + :members: + +.. admonition:: Notice + + The :data:`PERMANENT_SESSION_LIFETIME` config can be an integer or ``timedelta``. + The :attr:`~flask.Flask.permanent_session_lifetime` attribute is always a + ``timedelta``. + + +Test Client +----------- + +.. currentmodule:: flask.testing + +.. autoclass:: FlaskClient + :members: + + +Test CLI Runner +--------------- + +.. currentmodule:: flask.testing + +.. autoclass:: FlaskCliRunner + :members: + + +Application Globals +------------------- + +.. currentmodule:: flask + +To share data that is valid for one request only from one function to +another, a global variable is not good enough because it would break in +threaded environments. Flask provides you with a special object that +ensures it is only valid for the active request and that will return +different values for each request. In a nutshell: it does the right +thing, like it does for :class:`request` and :class:`session`. + +.. data:: g + + A namespace object that can store data during an + :doc:`application context `. This is an instance of + :attr:`Flask.app_ctx_globals_class`, which defaults to + :class:`ctx._AppCtxGlobals`. + + This is a good place to store resources during a request. For + example, a ``before_request`` function could load a user object from + a session id, then set ``g.user`` to be used in the view function. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + .. versionchanged:: 0.10 + Bound to the application context instead of the request context. + +.. autoclass:: flask.ctx._AppCtxGlobals + :members: + + +Useful Functions and Classes +---------------------------- + +.. data:: current_app + + A proxy to the application handling the current request. This is + useful to access the application without needing to import it, or if + it can't be imported, such as when using the application factory + pattern or in blueprints and extensions. + + This is only available when an + :doc:`application context ` is pushed. This happens + automatically during requests and CLI commands. It can be controlled + manually with :meth:`~flask.Flask.app_context`. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + +.. autofunction:: has_request_context + +.. autofunction:: copy_current_request_context + +.. autofunction:: has_app_context + +.. autofunction:: url_for + +.. autofunction:: abort + +.. autofunction:: redirect + +.. autofunction:: make_response + +.. autofunction:: after_this_request + +.. autofunction:: send_file + +.. autofunction:: send_from_directory + + +Message Flashing +---------------- + +.. autofunction:: flash + +.. autofunction:: get_flashed_messages + + +JSON Support +------------ + +.. module:: flask.json + +Flask uses Python's built-in :mod:`json` module for handling JSON by +default. The JSON implementation can be changed by assigning a different +provider to :attr:`flask.Flask.json_provider_class` or +:attr:`flask.Flask.json`. The functions provided by ``flask.json`` will +use methods on ``app.json`` if an app context is active. + +Jinja's ``|tojson`` filter is configured to use the app's JSON provider. +The filter marks the output with ``|safe``. Use it to render data inside +HTML `` + +.. autofunction:: jsonify + +.. autofunction:: dumps + +.. autofunction:: dump + +.. autofunction:: loads + +.. autofunction:: load + +.. autoclass:: flask.json.provider.JSONProvider + :members: + :member-order: bysource + +.. autoclass:: flask.json.provider.DefaultJSONProvider + :members: + :member-order: bysource + +.. automodule:: flask.json.tag + + +Template Rendering +------------------ + +.. currentmodule:: flask + +.. autofunction:: render_template + +.. autofunction:: render_template_string + +.. autofunction:: stream_template + +.. autofunction:: stream_template_string + +.. autofunction:: get_template_attribute + +Configuration +------------- + +.. autoclass:: Config + :members: + + +Stream Helpers +-------------- + +.. autofunction:: stream_with_context + +Useful Internals +---------------- + +.. autoclass:: flask.ctx.RequestContext + :members: + +.. data:: flask.globals.request_ctx + + The current :class:`~flask.ctx.RequestContext`. If a request context + is not active, accessing attributes on this proxy will raise a + ``RuntimeError``. + + This is an internal object that is essential to how Flask handles + requests. Accessing this should not be needed in most cases. Most + likely you want :data:`request` and :data:`session` instead. + +.. autoclass:: flask.ctx.AppContext + :members: + +.. data:: flask.globals.app_ctx + + The current :class:`~flask.ctx.AppContext`. If an app context is not + active, accessing attributes on this proxy will raise a + ``RuntimeError``. + + This is an internal object that is essential to how Flask handles + requests. Accessing this should not be needed in most cases. Most + likely you want :data:`current_app` and :data:`g` instead. + +.. autoclass:: flask.blueprints.BlueprintSetupState + :members: + +.. _core-signals-list: + +Signals +------- + +Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. + +.. _blinker: https://blinker.readthedocs.io/ + +.. data:: template_rendered + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + + Example subscriber:: + + def log_template_renders(sender, template, context, **extra): + sender.logger.debug('Rendering template "%s" with context %s', + template.name or 'string template', + context) + + from flask import template_rendered + template_rendered.connect(log_template_renders, app) + +.. data:: flask.before_render_template + :noindex: + + This signal is sent before template rendering process. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + + Example subscriber:: + + def log_template_renders(sender, template, context, **extra): + sender.logger.debug('Rendering template "%s" with context %s', + template.name or 'string template', + context) + + from flask import before_render_template + before_render_template.connect(log_template_renders, app) + +.. data:: request_started + + This signal is sent when the request context is set up, before + any request processing happens. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + + Example subscriber:: + + def log_request(sender, **extra): + sender.logger.debug('Request context is set up') + + from flask import request_started + request_started.connect(log_request, app) + +.. data:: request_finished + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + + Example subscriber:: + + def log_response(sender, response, **extra): + sender.logger.debug('Request context is about to close down. ' + 'Response: %s', response) + + from flask import request_finished + request_finished.connect(log_response, app) + +.. data:: got_request_exception + + This signal is sent when an unhandled exception happens during + request processing, including when debugging. The exception is + passed to the subscriber as ``exception``. + + This signal is not sent for + :exc:`~werkzeug.exceptions.HTTPException`, or other exceptions that + have error handlers registered, unless the exception was raised from + an error handler. + + This example shows how to do some extra logging if a theoretical + ``SecurityException`` was raised: + + .. code-block:: python + + from flask import got_request_exception + + def log_security_exception(sender, exception, **extra): + if not isinstance(exception, SecurityException): + return + + security_logger.exception( + f"SecurityException at {request.url!r}", + exc_info=exception, + ) + + got_request_exception.connect(log_security_exception, app) + +.. data:: request_tearing_down + + This signal is sent when the request is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender, **extra): + session.close() + + from flask import request_tearing_down + request_tearing_down.connect(close_db_connection, app) + + As of Flask 0.9, this will also be passed an `exc` keyword argument + that has a reference to the exception that caused the teardown if + there was one. + +.. data:: appcontext_tearing_down + + This signal is sent when the app context is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender, **extra): + session.close() + + from flask import appcontext_tearing_down + appcontext_tearing_down.connect(close_db_connection, app) + + This will also be passed an `exc` keyword argument that has a reference + to the exception that caused the teardown if there was one. + +.. data:: appcontext_pushed + + This signal is sent when an application context is pushed. The sender + is the application. This is usually useful for unittests in order to + temporarily hook in information. For instance it can be used to + set a resource early onto the `g` object. + + Example usage:: + + from contextlib import contextmanager + from flask import appcontext_pushed + + @contextmanager + def user_set(app, user): + def handler(sender, **kwargs): + g.user = user + with appcontext_pushed.connected_to(handler, app): + yield + + And in the testcode:: + + def test_user_me(self): + with user_set(app, 'john'): + c = app.test_client() + resp = c.get('/users/me') + assert resp.data == 'username=john' + + .. versionadded:: 0.10 + +.. data:: appcontext_popped + + This signal is sent when an application context is popped. The sender + is the application. This usually falls in line with the + :data:`appcontext_tearing_down` signal. + + .. versionadded:: 0.10 + +.. data:: message_flashed + + This signal is sent when the application is flashing a message. The + messages is sent as `message` keyword argument and the category as + `category`. + + Example subscriber:: + + recorded = [] + def record(sender, message, category, **extra): + recorded.append((message, category)) + + from flask import message_flashed + message_flashed.connect(record, app) + + .. versionadded:: 0.10 + + +Class-Based Views +----------------- + +.. versionadded:: 0.7 + +.. currentmodule:: None + +.. autoclass:: flask.views.View + :members: + +.. autoclass:: flask.views.MethodView + :members: + +.. _url-route-registrations: + +URL Route Registrations +----------------------- + +Generally there are three ways to define rules for the routing system: + +1. You can use the :meth:`flask.Flask.route` decorator. +2. You can use the :meth:`flask.Flask.add_url_rule` function. +3. You can directly access the underlying Werkzeug routing system + which is exposed as :attr:`flask.Flask.url_map`. + +Variable parts in the route can be specified with angular brackets +(``/user/``). By default a variable part in the URL accepts any +string without a slash however a different converter can be specified as +well by using ````. + +Variable parts are passed to the view function as keyword arguments. + +The following converters are available: + +=========== =============================================== +`string` accepts any text without a slash (the default) +`int` accepts integers +`float` like `int` but for floating point values +`path` like the default but also accepts slashes +`any` matches one of the items provided +`uuid` accepts UUID strings +=========== =============================================== + +Custom converters can be defined using :attr:`flask.Flask.url_map`. + +Here are some examples:: + + @app.route('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + pass + +An important detail to keep in mind is how Flask deals with trailing +slashes. The idea is to keep each URL unique so the following rules +apply: + +1. If a rule ends with a slash and is requested without a slash by the + user, the user is automatically redirected to the same page with a + trailing slash attached. +2. If a rule does not end with a trailing slash and the user requests the + page with a trailing slash, a 404 not found is raised. + +This is consistent with how web servers deal with static files. This +also makes it possible to use relative link targets safely. + +You can also define multiple rules for the same function. They have to be +unique however. Defaults can also be specified. Here for example is a +definition for a URL that accepts an optional page:: + + @app.route('/users/', defaults={'page': 1}) + @app.route('/users/page/') + def show_users(page): + pass + +This specifies that ``/users/`` will be the URL for page one and +``/users/page/N`` will be the URL for page ``N``. + +If a URL contains a default value, it will be redirected to its simpler +form with a 301 redirect. In the above example, ``/users/page/1`` will +be redirected to ``/users/``. If your route handles ``GET`` and ``POST`` +requests, make sure the default route only handles ``GET``, as redirects +can't preserve form data. :: + + @app.route('/region/', defaults={'id': 1}) + @app.route('/region/', methods=['GET', 'POST']) + def region(id): + pass + +Here are the parameters that :meth:`~flask.Flask.route` and +:meth:`~flask.Flask.add_url_rule` accept. The only difference is that +with the route parameter the view function is defined with the decorator +instead of the `view_func` parameter. + +=============== ========================================================== +`rule` the URL rule as string +`endpoint` the endpoint for the registered URL rule. Flask itself + assumes that the name of the view function is the name + of the endpoint if not explicitly stated. +`view_func` the function to call when serving a request to the + provided endpoint. If this is not provided one can + specify the function later by storing it in the + :attr:`~flask.Flask.view_functions` dictionary with the + endpoint as key. +`defaults` A dictionary with defaults for this rule. See the + example above for how defaults work. +`subdomain` specifies the rule for the subdomain in case subdomain + matching is in use. If not specified the default + subdomain is assumed. +`**options` the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change to + Werkzeug is handling of method options. methods is a list + of methods this rule should be limited to (``GET``, ``POST`` + etc.). By default a rule just listens for ``GET`` (and + implicitly ``HEAD``). Starting with Flask 0.6, ``OPTIONS`` is + implicitly added and handled by the standard request + handling. They have to be specified as keyword arguments. +=============== ========================================================== + + +View Function Options +--------------------- + +For internal usage the view functions can have some attributes attached to +customize behavior the view function would normally not have control over. +The following attributes can be provided optionally to either override +some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: + +- `__name__`: The name of a function is by default used as endpoint. If + endpoint is provided explicitly this value is used. Additionally this + will be prefixed with the name of the blueprint by default which + cannot be customized from the function itself. + +- `methods`: If methods are not provided when the URL rule is added, + Flask will look on the view function object itself if a `methods` + attribute exists. If it does, it will pull the information for the + methods from there. + +- `provide_automatic_options`: if this attribute is set Flask will + either force enable or disable the automatic implementation of the + HTTP ``OPTIONS`` response. This can be useful when working with + decorators that want to customize the ``OPTIONS`` response on a per-view + basis. + +- `required_methods`: if this attribute is set, Flask will always add + these methods when registering a URL rule even if the methods were + explicitly overridden in the ``route()`` call. + +Full example:: + + def index(): + if request.method == 'OPTIONS': + # custom options handling here + ... + return 'Hello World!' + index.provide_automatic_options = False + index.methods = ['GET', 'OPTIONS'] + + app.add_url_rule('/', index) + +.. versionadded:: 0.8 + The `provide_automatic_options` functionality was added. + +Command Line Interface +---------------------- + +.. currentmodule:: flask.cli + +.. autoclass:: FlaskGroup + :members: + +.. autoclass:: AppGroup + :members: + +.. autoclass:: ScriptInfo + :members: + +.. autofunction:: load_dotenv + +.. autofunction:: with_appcontext + +.. autofunction:: pass_script_info + + Marks a function so that an instance of :class:`ScriptInfo` is passed + as first argument to the click callback. + +.. autodata:: run_command + +.. autodata:: shell_command diff --git a/test/fixtures/whole_applications/flask/docs/appcontext.rst b/test/fixtures/whole_applications/flask/docs/appcontext.rst new file mode 100644 index 0000000..5509a9a --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/appcontext.rst @@ -0,0 +1,147 @@ +.. currentmodule:: flask + +The Application Context +======================= + +The application context keeps track of the application-level data during +a request, CLI command, or other activity. Rather than passing the +application around to each function, the :data:`current_app` and +:data:`g` proxies are accessed instead. + +This is similar to :doc:`/reqcontext`, which keeps track of +request-level data during a request. A corresponding application context +is pushed when a request context is pushed. + +Purpose of the Context +---------------------- + +The :class:`Flask` application object has attributes, such as +:attr:`~Flask.config`, that are useful to access within views and +:doc:`CLI commands `. However, importing the ``app`` instance +within the modules in your project is prone to circular import issues. +When using the :doc:`app factory pattern ` or +writing reusable :doc:`blueprints ` or +:doc:`extensions ` there won't be an ``app`` instance to +import at all. + +Flask solves this issue with the *application context*. Rather than +referring to an ``app`` directly, you use the :data:`current_app` +proxy, which points to the application handling the current activity. + +Flask automatically *pushes* an application context when handling a +request. View functions, error handlers, and other functions that run +during a request will have access to :data:`current_app`. + +Flask will also automatically push an app context when running CLI +commands registered with :attr:`Flask.cli` using ``@app.cli.command()``. + + +Lifetime of the Context +----------------------- + +The application context is created and destroyed as necessary. When a +Flask application begins handling a request, it pushes an application +context and a :doc:`request context `. When the request +ends it pops the request context then the application context. +Typically, an application context will have the same lifetime as a +request. + +See :doc:`/reqcontext` for more information about how the contexts work +and the full life cycle of a request. + + +Manually Push a Context +----------------------- + +If you try to access :data:`current_app`, or anything that uses it, +outside an application context, you'll get this error message: + +.. code-block:: pytb + + RuntimeError: Working outside of application context. + + This typically means that you attempted to use functionality that + needed to interface with the current application object in some way. + To solve this, set up an application context with app.app_context(). + +If you see that error while configuring your application, such as when +initializing an extension, you can push a context manually since you +have direct access to the ``app``. Use :meth:`~Flask.app_context` in a +``with`` block, and everything that runs in the block will have access +to :data:`current_app`. :: + + def create_app(): + app = Flask(__name__) + + with app.app_context(): + init_db() + + return app + +If you see that error somewhere else in your code not related to +configuring the application, it most likely indicates that you should +move that code into a view function or CLI command. + + +Storing Data +------------ + +The application context is a good place to store common data during a +request or CLI command. Flask provides the :data:`g object ` for this +purpose. It is a simple namespace object that has the same lifetime as +an application context. + +.. note:: + The ``g`` name stands for "global", but that is referring to the + data being global *within a context*. The data on ``g`` is lost + after the context ends, and it is not an appropriate place to store + data between requests. Use the :data:`session` or a database to + store data across requests. + +A common use for :data:`g` is to manage resources during a request. + +1. ``get_X()`` creates resource ``X`` if it does not exist, caching it + as ``g.X``. +2. ``teardown_X()`` closes or otherwise deallocates the resource if it + exists. It is registered as a :meth:`~Flask.teardown_appcontext` + handler. + +For example, you can manage a database connection using this pattern:: + + from flask import g + + def get_db(): + if 'db' not in g: + g.db = connect_to_database() + + return g.db + + @app.teardown_appcontext + def teardown_db(exception): + db = g.pop('db', None) + + if db is not None: + db.close() + +During a request, every call to ``get_db()`` will return the same +connection, and it will be closed automatically at the end of the +request. + +You can use :class:`~werkzeug.local.LocalProxy` to make a new context +local from ``get_db()``:: + + from werkzeug.local import LocalProxy + db = LocalProxy(get_db) + +Accessing ``db`` will call ``get_db`` internally, in the same way that +:data:`current_app` works. + + +Events and Signals +------------------ + +The application will call functions registered with :meth:`~Flask.teardown_appcontext` +when the application context is popped. + +The following signals are sent: :data:`appcontext_pushed`, +:data:`appcontext_tearing_down`, and :data:`appcontext_popped`. diff --git a/test/fixtures/whole_applications/flask/docs/async-await.rst b/test/fixtures/whole_applications/flask/docs/async-await.rst new file mode 100644 index 0000000..06a29fc --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/async-await.rst @@ -0,0 +1,131 @@ +.. _async_await: + +Using ``async`` and ``await`` +============================= + +.. versionadded:: 2.0 + +Routes, error handlers, before request, after request, and teardown +functions can all be coroutine functions if Flask is installed with the +``async`` extra (``pip install flask[async]``). This allows views to be +defined with ``async def`` and use ``await``. + +.. code-block:: python + + @app.route("/get-data") + async def get_data(): + data = await async_db_query(...) + return jsonify(data) + +Pluggable class-based views also support handlers that are implemented as +coroutines. This applies to the :meth:`~flask.views.View.dispatch_request` +method in views that inherit from the :class:`flask.views.View` class, as +well as all the HTTP method handlers in views that inherit from the +:class:`flask.views.MethodView` class. + +.. admonition:: Using ``async`` on Windows on Python 3.8 + + Python 3.8 has a bug related to asyncio on Windows. If you encounter + something like ``ValueError: set_wakeup_fd only works in main thread``, + please upgrade to Python 3.9. + +.. admonition:: Using ``async`` with greenlet + + When using gevent or eventlet to serve an application or patch the + runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is + required. + + +Performance +----------- + +Async functions require an event loop to run. Flask, as a WSGI +application, uses one worker to handle one request/response cycle. +When a request comes in to an async view, Flask will start an event loop +in a thread, run the view function there, then return the result. + +Each request still ties up one worker, even for async views. The upside +is that you can run async code within a view, for example to make +multiple concurrent database queries, HTTP requests to an external API, +etc. However, the number of requests your application can handle at one +time will remain the same. + +**Async is not inherently faster than sync code.** Async is beneficial +when performing concurrent IO-bound tasks, but will probably not improve +CPU-bound tasks. Traditional Flask views will still be appropriate for +most use cases, but Flask's async support enables writing and using +code that wasn't possible natively before. + + +Background tasks +---------------- + +Async functions will run in an event loop until they complete, at +which stage the event loop will stop. This means any additional +spawned tasks that haven't completed when the async function completes +will be cancelled. Therefore you cannot spawn background tasks, for +example via ``asyncio.create_task``. + +If you wish to use background tasks it is best to use a task queue to +trigger background work, rather than spawn tasks in a view +function. With that in mind you can spawn asyncio tasks by serving +Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter +as described in :doc:`deploying/asgi`. This works as the adapter creates +an event loop that runs continually. + + +When to use Quart instead +------------------------- + +Flask's async support is less performant than async-first frameworks due +to the way it is implemented. If you have a mainly async codebase it +would make sense to consider `Quart`_. Quart is a reimplementation of +Flask based on the `ASGI`_ standard instead of WSGI. This allows it to +handle many concurrent requests, long running requests, and websockets +without requiring multiple worker processes or threads. + +It has also already been possible to run Flask with Gevent or Eventlet +to get many of the benefits of async request handling. These libraries +patch low-level Python functions to accomplish this, whereas ``async``/ +``await`` and ASGI use standard, modern Python capabilities. Deciding +whether you should use Flask, Quart, or something else is ultimately up +to understanding the specific needs of your project. + +.. _Quart: https://github.com/pallets/quart +.. _ASGI: https://asgi.readthedocs.io/en/latest/ + + +Extensions +---------- + +Flask extensions predating Flask's async support do not expect async views. +If they provide decorators to add functionality to views, those will probably +not work with async views because they will not await the function or be +awaitable. Other functions they provide will not be awaitable either and +will probably be blocking if called within an async view. + +Extension authors can support async functions by utilising the +:meth:`flask.Flask.ensure_sync` method. For example, if the extension +provides a view function decorator add ``ensure_sync`` before calling +the decorated function, + +.. code-block:: python + + def extension(func): + @wraps(func) + def wrapper(*args, **kwargs): + ... # Extension logic + return current_app.ensure_sync(func)(*args, **kwargs) + + return wrapper + +Check the changelog of the extension you want to use to see if they've +implemented async support, or make a feature request or PR to them. + + +Other event loops +----------------- + +At the moment Flask only supports :mod:`asyncio`. It's possible to +override :meth:`flask.Flask.ensure_sync` to change how async functions +are wrapped to use a different library. diff --git a/test/fixtures/whole_applications/flask/docs/blueprints.rst b/test/fixtures/whole_applications/flask/docs/blueprints.rst new file mode 100644 index 0000000..d5cf3d8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/blueprints.rst @@ -0,0 +1,315 @@ +Modular Applications with Blueprints +==================================== + +.. currentmodule:: flask + +.. versionadded:: 0.7 + +Flask uses a concept of *blueprints* for making application components and +supporting common patterns within an application or across applications. +Blueprints can greatly simplify how large applications work and provide a +central means for Flask extensions to register operations on applications. +A :class:`Blueprint` object works similarly to a :class:`Flask` +application object, but it is not actually an application. Rather it is a +*blueprint* of how to construct or extend an application. + +Why Blueprints? +--------------- + +Blueprints in Flask are intended for these cases: + +* Factor an application into a set of blueprints. This is ideal for + larger applications; a project could instantiate an application object, + initialize several extensions, and register a collection of blueprints. +* Register a blueprint on an application at a URL prefix and/or subdomain. + Parameters in the URL prefix/subdomain become common view arguments + (with defaults) across all view functions in the blueprint. +* Register a blueprint multiple times on an application with different URL + rules. +* Provide template filters, static files, templates, and other utilities + through blueprints. A blueprint does not have to implement applications + or view functions. +* Register a blueprint on an application for any of these cases when + initializing a Flask extension. + +A blueprint in Flask is not a pluggable app because it is not actually an +application -- it's a set of operations which can be registered on an +application, even multiple times. Why not have multiple application +objects? You can do that (see :doc:`/patterns/appdispatch`), but your +applications will have separate configs and will be managed at the WSGI +layer. + +Blueprints instead provide separation at the Flask level, share +application config, and can change an application object as necessary with +being registered. The downside is that you cannot unregister a blueprint +once an application was created without having to destroy the whole +application object. + +The Concept of Blueprints +------------------------- + +The basic concept of blueprints is that they record operations to execute +when registered on an application. Flask associates view functions with +blueprints when dispatching requests and generating URLs from one endpoint +to another. + +My First Blueprint +------------------ + +This is what a very basic blueprint looks like. In this case we want to +implement a blueprint that does simple rendering of static templates:: + + from flask import Blueprint, render_template, abort + from jinja2 import TemplateNotFound + + simple_page = Blueprint('simple_page', __name__, + template_folder='templates') + + @simple_page.route('/', defaults={'page': 'index'}) + @simple_page.route('/') + def show(page): + try: + return render_template(f'pages/{page}.html') + except TemplateNotFound: + abort(404) + +When you bind a function with the help of the ``@simple_page.route`` +decorator, the blueprint will record the intention of registering the +function ``show`` on the application when it's later registered. +Additionally it will prefix the endpoint of the function with the +name of the blueprint which was given to the :class:`Blueprint` +constructor (in this case also ``simple_page``). The blueprint's name +does not modify the URL, only the endpoint. + +Registering Blueprints +---------------------- + +So how do you register that blueprint? Like this:: + + from flask import Flask + from yourapplication.simple_page import simple_page + + app = Flask(__name__) + app.register_blueprint(simple_page) + +If you check the rules registered on the application, you will find +these:: + + >>> app.url_map + Map([' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> simple_page.show>, + simple_page.show>]) + +The first one is obviously from the application itself for the static +files. The other two are for the `show` function of the ``simple_page`` +blueprint. As you can see, they are also prefixed with the name of the +blueprint and separated by a dot (``.``). + +Blueprints however can also be mounted at different locations:: + + app.register_blueprint(simple_page, url_prefix='/pages') + +And sure enough, these are the generated rules:: + + >>> app.url_map + Map([' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> simple_page.show>, + simple_page.show>]) + +On top of that you can register blueprints multiple times though not every +blueprint might respond properly to that. In fact it depends on how the +blueprint is implemented if it can be mounted more than once. + +Nesting Blueprints +------------------ + +It is possible to register a blueprint on another blueprint. + +.. code-block:: python + + parent = Blueprint('parent', __name__, url_prefix='/parent') + child = Blueprint('child', __name__, url_prefix='/child') + parent.register_blueprint(child) + app.register_blueprint(parent) + +The child blueprint will gain the parent's name as a prefix to its +name, and child URLs will be prefixed with the parent's URL prefix. + +.. code-block:: python + + url_for('parent.child.create') + /parent/child/create + +In addition a child blueprint's will gain their parent's subdomain, +with their subdomain as prefix if present i.e. + +.. code-block:: python + + parent = Blueprint('parent', __name__, subdomain='parent') + child = Blueprint('child', __name__, subdomain='child') + parent.register_blueprint(child) + app.register_blueprint(parent) + + url_for('parent.child.create', _external=True) + "child.parent.domain.tld" + +Blueprint-specific before request functions, etc. registered with the +parent will trigger for the child. If a child does not have an error +handler that can handle a given exception, the parent's will be tried. + + +Blueprint Resources +------------------- + +Blueprints can provide resources as well. Sometimes you might want to +introduce a blueprint only for the resources it provides. + +Blueprint Resource Folder +````````````````````````` + +Like for regular applications, blueprints are considered to be contained +in a folder. While multiple blueprints can originate from the same folder, +it does not have to be the case and it's usually not recommended. + +The folder is inferred from the second argument to :class:`Blueprint` which +is usually `__name__`. This argument specifies what logical Python +module or package corresponds to the blueprint. If it points to an actual +Python package that package (which is a folder on the filesystem) is the +resource folder. If it's a module, the package the module is contained in +will be the resource folder. You can access the +:attr:`Blueprint.root_path` property to see what the resource folder is:: + + >>> simple_page.root_path + '/Users/username/TestProject/yourapplication' + +To quickly open sources from this folder you can use the +:meth:`~Blueprint.open_resource` function:: + + with simple_page.open_resource('static/style.css') as f: + code = f.read() + +Static Files +```````````` + +A blueprint can expose a folder with static files by providing the path +to the folder on the filesystem with the ``static_folder`` argument. +It is either an absolute path or relative to the blueprint's location:: + + admin = Blueprint('admin', __name__, static_folder='static') + +By default the rightmost part of the path is where it is exposed on the +web. This can be changed with the ``static_url_path`` argument. Because the +folder is called ``static`` here it will be available at the +``url_prefix`` of the blueprint + ``/static``. If the blueprint +has the prefix ``/admin``, the static URL will be ``/admin/static``. + +The endpoint is named ``blueprint_name.static``. You can generate URLs +to it with :func:`url_for` like you would with the static folder of the +application:: + + url_for('admin.static', filename='style.css') + +However, if the blueprint does not have a ``url_prefix``, it is not +possible to access the blueprint's static folder. This is because the +URL would be ``/static`` in this case, and the application's ``/static`` +route takes precedence. Unlike template folders, blueprint static +folders are not searched if the file does not exist in the application +static folder. + +Templates +````````` + +If you want the blueprint to expose templates you can do that by providing +the `template_folder` parameter to the :class:`Blueprint` constructor:: + + admin = Blueprint('admin', __name__, template_folder='templates') + +For static files, the path can be absolute or relative to the blueprint +resource folder. + +The template folder is added to the search path of templates but with a lower +priority than the actual application's template folder. That way you can +easily override templates that a blueprint provides in the actual application. +This also means that if you don't want a blueprint template to be accidentally +overridden, make sure that no other blueprint or actual application template +has the same relative path. When multiple blueprints provide the same relative +template path the first blueprint registered takes precedence over the others. + + +So if you have a blueprint in the folder ``yourapplication/admin`` and you +want to render the template ``'admin/index.html'`` and you have provided +``templates`` as a `template_folder` you will have to create a file like +this: :file:`yourapplication/admin/templates/admin/index.html`. The reason +for the extra ``admin`` folder is to avoid getting our template overridden +by a template named ``index.html`` in the actual application template +folder. + +To further reiterate this: if you have a blueprint named ``admin`` and you +want to render a template called :file:`index.html` which is specific to this +blueprint, the best idea is to lay out your templates like this:: + + yourpackage/ + blueprints/ + admin/ + templates/ + admin/ + index.html + __init__.py + +And then when you want to render the template, use :file:`admin/index.html` as +the name to look up the template by. If you encounter problems loading +the correct templates enable the ``EXPLAIN_TEMPLATE_LOADING`` config +variable which will instruct Flask to print out the steps it goes through +to locate templates on every ``render_template`` call. + +Building URLs +------------- + +If you want to link from one page to another you can use the +:func:`url_for` function just like you normally would do just that you +prefix the URL endpoint with the name of the blueprint and a dot (``.``):: + + url_for('admin.index') + +Additionally if you are in a view function of a blueprint or a rendered +template and you want to link to another endpoint of the same blueprint, +you can use relative redirects by prefixing the endpoint with a dot only:: + + url_for('.index') + +This will link to ``admin.index`` for instance in case the current request +was dispatched to any other admin blueprint endpoint. + + +Blueprint Error Handlers +------------------------ + +Blueprints support the ``errorhandler`` decorator just like the :class:`Flask` +application object, so it is easy to make Blueprint-specific custom error +pages. + +Here is an example for a "404 Page Not Found" exception:: + + @simple_page.errorhandler(404) + def page_not_found(e): + return render_template('pages/404.html') + +Most errorhandlers will simply work as expected; however, there is a caveat +concerning handlers for 404 and 405 exceptions. These errorhandlers are only +invoked from an appropriate ``raise`` statement or a call to ``abort`` in another +of the blueprint's view functions; they are not invoked by, e.g., an invalid URL +access. This is because the blueprint does not "own" a certain URL space, so +the application instance has no way of knowing which blueprint error handler it +should run if given an invalid URL. If you would like to execute different +handling strategies for these errors based on URL prefixes, they may be defined +at the application level using the ``request`` proxy object:: + + @app.errorhandler(404) + @app.errorhandler(405) + def _handle_api_error(ex): + if request.path.startswith('/api/'): + return jsonify(error=str(ex)), ex.code + else: + return ex + +See :doc:`/errorhandling`. diff --git a/test/fixtures/whole_applications/flask/docs/changes.rst b/test/fixtures/whole_applications/flask/docs/changes.rst new file mode 100644 index 0000000..955deaf --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/changes.rst @@ -0,0 +1,4 @@ +Changes +======= + +.. include:: ../CHANGES.rst diff --git a/test/fixtures/whole_applications/flask/docs/cli.rst b/test/fixtures/whole_applications/flask/docs/cli.rst new file mode 100644 index 0000000..a72e6d5 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/cli.rst @@ -0,0 +1,556 @@ +.. currentmodule:: flask + +Command Line Interface +====================== + +Installing Flask installs the ``flask`` script, a `Click`_ command line +interface, in your virtualenv. Executed from the terminal, this script gives +access to built-in, extension, and application-defined commands. The ``--help`` +option will give more information about any commands and options. + +.. _Click: https://click.palletsprojects.com/ + + +Application Discovery +--------------------- + +The ``flask`` command is installed by Flask, not your application; it must be +told where to find your application in order to use it. The ``--app`` +option is used to specify how to load the application. + +While ``--app`` supports a variety of options for specifying your +application, most use cases should be simple. Here are the typical values: + +(nothing) + The name "app" or "wsgi" is imported (as a ".py" file, or package), + automatically detecting an app (``app`` or ``application``) or + factory (``create_app`` or ``make_app``). + +``--app hello`` + The given name is imported, automatically detecting an app (``app`` + or ``application``) or factory (``create_app`` or ``make_app``). + +---- + +``--app`` has three parts: an optional path that sets the current working +directory, a Python file or dotted import path, and an optional variable +name of the instance or factory. If the name is a factory, it can optionally +be followed by arguments in parentheses. The following values demonstrate these +parts: + +``--app src/hello`` + Sets the current working directory to ``src`` then imports ``hello``. + +``--app hello.web`` + Imports the path ``hello.web``. + +``--app hello:app2`` + Uses the ``app2`` Flask instance in ``hello``. + +``--app 'hello:create_app("dev")'`` + The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` + as the argument. + +If ``--app`` is not set, the command will try to import "app" or +"wsgi" (as a ".py" file, or package) and try to detect an application +instance or factory. + +Within the given import, the command looks for an application instance named +``app`` or ``application``, then any application instance. If no instance is +found, the command looks for a factory function named ``create_app`` or +``make_app`` that returns an instance. + +If parentheses follow the factory name, their contents are parsed as +Python literals and passed as arguments and keyword arguments to the +function. This means that strings must still be in quotes. + + +Run the Development Server +-------------------------- + +The :func:`run ` command will start the development server. It +replaces the :meth:`Flask.run` method in most cases. :: + + $ flask --app hello run + * Serving Flask app "hello" + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + +.. warning:: Do not use this command to run your application in production. + Only use the development server during development. The development server + is provided for convenience, but is not designed to be particularly secure, + stable, or efficient. See :doc:`/deploying/index` for how to run in production. + +If another program is already using port 5000, you'll see +``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the +server tries to start. See :ref:`address-already-in-use` for how to +handle that. + + +Debug Mode +~~~~~~~~~~ + +In debug mode, the ``flask run`` command will enable the interactive debugger and the +reloader by default, and make errors easier to see and debug. To enable debug mode, use +the ``--debug`` option. + +.. code-block:: console + + $ flask --app hello run --debug + * Serving Flask app "hello" + * Debug mode: on + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + * Restarting with inotify reloader + * Debugger is active! + * Debugger PIN: 223-456-919 + +The ``--debug`` option can also be passed to the top level ``flask`` command to enable +debug mode for any command. The following two ``run`` calls are equivalent. + +.. code-block:: console + + $ flask --app hello --debug run + $ flask --app hello run --debug + + +Watch and Ignore Files with the Reloader +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using debug mode, the reloader will trigger whenever your Python code or imported +modules change. The reloader can watch additional files with the ``--extra-files`` +option. Multiple paths are separated with ``:``, or ``;`` on Windows. + +.. code-block:: text + + $ flask run --extra-files file1:dirA/file2:dirB/ + * Running on http://127.0.0.1:8000/ + * Detected change in '/path/to/file1', reloading + +The reloader can also ignore files using :mod:`fnmatch` patterns with the +``--exclude-patterns`` option. Multiple patterns are separated with ``:``, or ``;`` on +Windows. + + +Open a Shell +------------ + +To explore the data in your application, you can start an interactive Python +shell with the :func:`shell ` command. An application +context will be active, and the app instance will be imported. :: + + $ flask shell + Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux + App: example [production] + Instance: /home/david/Projects/pallets/flask/instance + >>> + +Use :meth:`~Flask.shell_context_processor` to add other automatic imports. + + +.. _dotenv: + +Environment Variables From dotenv +--------------------------------- + +The ``flask`` command supports setting any option for any command with +environment variables. The variables are named like ``FLASK_OPTION`` or +``FLASK_COMMAND_OPTION``, for example ``FLASK_APP`` or +``FLASK_RUN_PORT``. + +Rather than passing options every time you run a command, or environment +variables every time you open a new terminal, you can use Flask's dotenv +support to set environment variables automatically. + +If `python-dotenv`_ is installed, running the ``flask`` command will set +environment variables defined in the files ``.env`` and ``.flaskenv``. +You can also specify an extra file to load with the ``--env-file`` +option. Dotenv files can be used to avoid having to set ``--app`` or +``FLASK_APP`` manually, and to set configuration using environment +variables similar to how some deployment services work. + +Variables set on the command line are used over those set in :file:`.env`, +which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be +used for public variables, such as ``FLASK_APP``, while :file:`.env` should not +be committed to your repository so that it can set private variables. + +Directories are scanned upwards from the directory you call ``flask`` +from to locate the files. + +The files are only loaded by the ``flask`` command or calling +:meth:`~Flask.run`. If you would like to load these files when running in +production, you should call :func:`~cli.load_dotenv` manually. + +.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + +Setting Command Options +~~~~~~~~~~~~~~~~~~~~~~~ + +Click is configured to load default values for command options from +environment variables. The variables use the pattern +``FLASK_COMMAND_OPTION``. For example, to set the port for the run +command, instead of ``flask run --port 8000``: + +.. tabs:: + + .. group-tab:: Bash + + .. code-block:: text + + $ export FLASK_RUN_PORT=8000 + $ flask run + * Running on http://127.0.0.1:8000/ + + .. group-tab:: Fish + + .. code-block:: text + + $ set -x FLASK_RUN_PORT 8000 + $ flask run + * Running on http://127.0.0.1:8000/ + + .. group-tab:: CMD + + .. code-block:: text + + > set FLASK_RUN_PORT=8000 + > flask run + * Running on http://127.0.0.1:8000/ + + .. group-tab:: Powershell + + .. code-block:: text + + > $env:FLASK_RUN_PORT = 8000 + > flask run + * Running on http://127.0.0.1:8000/ + +These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to +control default command options. + + +Disable dotenv +~~~~~~~~~~~~~~ + +The ``flask`` command will show a message if it detects dotenv files but +python-dotenv is not installed. + +.. code-block:: bash + + $ flask run + * Tip: There are .env files present. Do "pip install python-dotenv" to use them. + +You can tell Flask not to load dotenv files even when python-dotenv is +installed by setting the ``FLASK_SKIP_DOTENV`` environment variable. +This can be useful if you want to load them manually, or if you're using +a project runner that loads them already. Keep in mind that the +environment variables must be set before the app loads or it won't +configure as expected. + +.. tabs:: + + .. group-tab:: Bash + + .. code-block:: text + + $ export FLASK_SKIP_DOTENV=1 + $ flask run + + .. group-tab:: Fish + + .. code-block:: text + + $ set -x FLASK_SKIP_DOTENV 1 + $ flask run + + .. group-tab:: CMD + + .. code-block:: text + + > set FLASK_SKIP_DOTENV=1 + > flask run + + .. group-tab:: Powershell + + .. code-block:: text + + > $env:FLASK_SKIP_DOTENV = 1 + > flask run + + +Environment Variables From virtualenv +------------------------------------- + +If you do not want to install dotenv support, you can still set environment +variables by adding them to the end of the virtualenv's :file:`activate` +script. Activating the virtualenv will set the variables. + +.. tabs:: + + .. group-tab:: Bash + + Unix Bash, :file:`.venv/bin/activate`:: + + $ export FLASK_APP=hello + + .. group-tab:: Fish + + Fish, :file:`.venv/bin/activate.fish`:: + + $ set -x FLASK_APP hello + + .. group-tab:: CMD + + Windows CMD, :file:`.venv\\Scripts\\activate.bat`:: + + > set FLASK_APP=hello + + .. group-tab:: Powershell + + Windows Powershell, :file:`.venv\\Scripts\\activate.ps1`:: + + > $env:FLASK_APP = "hello" + +It is preferred to use dotenv support over this, since :file:`.flaskenv` can be +committed to the repository so that it works automatically wherever the project +is checked out. + + +Custom Commands +--------------- + +The ``flask`` command is implemented using `Click`_. See that project's +documentation for full information about writing commands. + +This example adds the command ``create-user`` that takes the argument +``name``. :: + + import click + from flask import Flask + + app = Flask(__name__) + + @app.cli.command("create-user") + @click.argument("name") + def create_user(name): + ... + +:: + + $ flask create-user admin + +This example adds the same command, but as ``user create``, a command in a +group. This is useful if you want to organize multiple related commands. :: + + import click + from flask import Flask + from flask.cli import AppGroup + + app = Flask(__name__) + user_cli = AppGroup('user') + + @user_cli.command('create') + @click.argument('name') + def create_user(name): + ... + + app.cli.add_command(user_cli) + +:: + + $ flask user create demo + +See :ref:`testing-cli` for an overview of how to test your custom +commands. + + +Registering Commands with Blueprints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your application uses blueprints, you can optionally register CLI +commands directly onto them. When your blueprint is registered onto your +application, the associated commands will be available to the ``flask`` +command. By default, those commands will be nested in a group matching +the name of the blueprint. + +.. code-block:: python + + from flask import Blueprint + + bp = Blueprint('students', __name__) + + @bp.cli.command('create') + @click.argument('name') + def create(name): + ... + + app.register_blueprint(bp) + +.. code-block:: text + + $ flask students create alice + +You can alter the group name by specifying the ``cli_group`` parameter +when creating the :class:`Blueprint` object, or later with +:meth:`app.register_blueprint(bp, cli_group='...') `. +The following are equivalent: + +.. code-block:: python + + bp = Blueprint('students', __name__, cli_group='other') + # or + app.register_blueprint(bp, cli_group='other') + +.. code-block:: text + + $ flask other create alice + +Specifying ``cli_group=None`` will remove the nesting and merge the +commands directly to the application's level: + +.. code-block:: python + + bp = Blueprint('students', __name__, cli_group=None) + # or + app.register_blueprint(bp, cli_group=None) + +.. code-block:: text + + $ flask create alice + + +Application Context +~~~~~~~~~~~~~~~~~~~ + +Commands added using the Flask app's :attr:`~Flask.cli` or +:class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator +will be executed with an application context pushed, so your custom +commands and parameters have access to the app and its configuration. The +:func:`~cli.with_appcontext` decorator can be used to get the same +behavior, but is not needed in most cases. + +.. code-block:: python + + import click + from flask.cli import with_appcontext + + @click.command() + @with_appcontext + def do_work(): + ... + + app.cli.add_command(do_work) + + +Plugins +------- + +Flask will automatically load commands specified in the ``flask.commands`` +`entry point`_. This is useful for extensions that want to add commands when +they are installed. Entry points are specified in :file:`pyproject.toml`: + +.. code-block:: toml + + [project.entry-points."flask.commands"] + my-command = "my_extension.commands:cli" + +.. _entry point: https://packaging.python.org/tutorials/packaging-projects/#entry-points + +Inside :file:`my_extension/commands.py` you can then export a Click +object:: + + import click + + @click.command() + def cli(): + ... + +Once that package is installed in the same virtualenv as your Flask project, +you can run ``flask my-command`` to invoke the command. + + +.. _custom-scripts: + +Custom Scripts +-------------- + +When you are using the app factory pattern, it may be more convenient to define +your own Click script. Instead of using ``--app`` and letting Flask load +your application, you can create your own Click object and export it as a +`console script`_ entry point. + +Create an instance of :class:`~cli.FlaskGroup` and pass it the factory:: + + import click + from flask import Flask + from flask.cli import FlaskGroup + + def create_app(): + app = Flask('wiki') + # other setup + return app + + @click.group(cls=FlaskGroup, create_app=create_app) + def cli(): + """Management script for the Wiki application.""" + +Define the entry point in :file:`pyproject.toml`: + +.. code-block:: toml + + [project.scripts] + wiki = "wiki:cli" + +Install the application in the virtualenv in editable mode and the custom +script is available. Note that you don't need to set ``--app``. :: + + $ pip install -e . + $ wiki run + +.. admonition:: Errors in Custom Scripts + + When using a custom script, if you introduce an error in your + module-level code, the reloader will fail because it can no longer + load the entry point. + + The ``flask`` command, being separate from your code, does not have + this issue and is recommended in most cases. + +.. _console script: https://packaging.python.org/tutorials/packaging-projects/#console-scripts + + +PyCharm Integration +------------------- + +PyCharm Professional provides a special Flask run configuration to run the development +server. For the Community Edition, and for other commands besides ``run``, you need to +create a custom run configuration. These instructions should be similar for any other +IDE you use. + +In PyCharm, with your project open, click on *Run* from the menu bar and go to *Edit +Configurations*. You'll see a screen similar to this: + +.. image:: _static/pycharm-run-config.png + :align: center + :class: screenshot + :alt: Screenshot of PyCharm run configuration. + +Once you create a configuration for the ``flask run``, you can copy and change it to +call any other command. + +Click the *+ (Add New Configuration)* button and select *Python*. Give the configuration +a name such as "flask run". + +Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. + +The *Parameters* field is set to the CLI command to execute along with any arguments. +This example uses ``--app hello run --debug``, which will run the development server in +debug mode. ``--app hello`` should be the import or file with your Flask app. + +If you installed your project as a package in your virtualenv, you may uncheck the +*PYTHONPATH* options. This will more accurately match how you deploy later. + +Click *OK* to save and close the configuration. Select the configuration in the main +PyCharm window and click the play button next to it to run the server. + +Now that you have a configuration for ``flask run``, you can copy that configuration and +change the *Parameters* argument to run a different CLI command. diff --git a/test/fixtures/whole_applications/flask/docs/conf.py b/test/fixtures/whole_applications/flask/docs/conf.py new file mode 100644 index 0000000..25b8f00 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/conf.py @@ -0,0 +1,97 @@ +import packaging.version +from pallets_sphinx_themes import get_version +from pallets_sphinx_themes import ProjectLink + +# Project -------------------------------------------------------------- + +project = "Flask" +copyright = "2010 Pallets" +author = "Pallets" +release, version = get_version("Flask") + +# General -------------------------------------------------------------- + +default_role = "code" +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinxcontrib.log_cabinet", + "sphinx_tabs.tabs", + "pallets_sphinx_themes", +] +autodoc_member_order = "bysource" +autodoc_typehints = "description" +autodoc_preserve_defaults = True +extlinks = { + "issue": ("https://github.com/pallets/flask/issues/%s", "#%s"), + "pr": ("https://github.com/pallets/flask/pull/%s", "#%s"), +} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "werkzeug": ("https://werkzeug.palletsprojects.com/", None), + "click": ("https://click.palletsprojects.com/", None), + "jinja": ("https://jinja.palletsprojects.com/", None), + "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), + "sqlalchemy": ("https://docs.sqlalchemy.org/", None), + "wtforms": ("https://wtforms.readthedocs.io/", None), + "blinker": ("https://blinker.readthedocs.io/", None), +} + +# HTML ----------------------------------------------------------------- + +html_theme = "flask" +html_theme_options = {"index_sidebar_logo": False} +html_context = { + "project_links": [ + ProjectLink("Donate", "https://palletsprojects.com/donate"), + ProjectLink("PyPI Releases", "https://pypi.org/project/Flask/"), + ProjectLink("Source Code", "https://github.com/pallets/flask/"), + ProjectLink("Issue Tracker", "https://github.com/pallets/flask/issues/"), + ProjectLink("Chat", "https://discord.gg/pallets"), + ] +} +html_sidebars = { + "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], + "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], +} +singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} +html_static_path = ["_static"] +html_favicon = "_static/shortcut-icon.png" +html_logo = "_static/flask-vertical.png" +html_title = f"Flask Documentation ({version})" +html_show_sourcelink = False + +# Local Extensions ----------------------------------------------------- + + +def github_link(name, rawtext, text, lineno, inliner, options=None, content=None): + app = inliner.document.settings.env.app + release = app.config.release + base_url = "https://github.com/pallets/flask/tree/" + + if text.endswith(">"): + words, text = text[:-1].rsplit("<", 1) + words = words.strip() + else: + words = None + + if packaging.version.parse(release).is_devrelease: + url = f"{base_url}main/{text}" + else: + url = f"{base_url}{release}/{text}" + + if words is None: + words = url + + from docutils.nodes import reference + from docutils.parsers.rst.roles import set_classes + + options = options or {} + set_classes(options) + node = reference(rawtext, words, refuri=url, **options) + return [node], [] + + +def setup(app): + app.add_role("gh", github_link) diff --git a/test/fixtures/whole_applications/flask/docs/config.rst b/test/fixtures/whole_applications/flask/docs/config.rst new file mode 100644 index 0000000..7828fb9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/config.rst @@ -0,0 +1,721 @@ +Configuration Handling +====================== + +Applications need some kind of configuration. There are different settings +you might want to change depending on the application environment like +toggling the debug mode, setting the secret key, and other such +environment-specific things. + +The way Flask is designed usually requires the configuration to be +available when the application starts up. You can hard code the +configuration in the code, which for many small applications is not +actually that bad, but there are better ways. + +Independent of how you load your config, there is a config object +available which holds the loaded configuration values: +The :attr:`~flask.Flask.config` attribute of the :class:`~flask.Flask` +object. This is the place where Flask itself puts certain configuration +values and also where extensions can put their configuration values. But +this is also where you can have your own configuration. + + +Configuration Basics +-------------------- + +The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and +can be modified just like any dictionary:: + + app = Flask(__name__) + app.config['TESTING'] = True + +Certain configuration values are also forwarded to the +:attr:`~flask.Flask` object so you can read and write them from there:: + + app.testing = True + +To update multiple keys at once you can use the :meth:`dict.update` +method:: + + app.config.update( + TESTING=True, + SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + ) + + +Debug Mode +---------- + +The :data:`DEBUG` config value is special because it may behave inconsistently if +changed after the app has begun setting up. In order to set debug mode reliably, use the +``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the +interactive debugger and reloader by default in debug mode. + +.. code-block:: text + + $ flask --app hello run --debug + +Using the option is recommended. While it is possible to set :data:`DEBUG` in your +config or code, this is strongly discouraged. It can't be read early by the +``flask run`` command, and some systems or extensions may have already configured +themselves based on a previous value. + + +Builtin Configuration Values +---------------------------- + +The following configuration values are used internally by Flask: + +.. py:data:: DEBUG + + Whether debug mode is enabled. When using ``flask run`` to start the development + server, an interactive debugger will be shown for unhandled exceptions, and the + server will be reloaded when code changes. The :attr:`~flask.Flask.debug` attribute + maps to this config key. This is set with the ``FLASK_DEBUG`` environment variable. + It may not behave as expected if set in code. + + **Do not enable debug mode when deploying in production.** + + Default: ``False`` + +.. py:data:: TESTING + + Enable testing mode. Exceptions are propagated rather than handled by the + the app's error handlers. Extensions may also change their behavior to + facilitate easier testing. You should enable this in your own tests. + + Default: ``False`` + +.. py:data:: PROPAGATE_EXCEPTIONS + + Exceptions are re-raised rather than being handled by the app's error + handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG`` + is enabled. + + Default: ``None`` + +.. py:data:: TRAP_HTTP_EXCEPTIONS + + If there is no handler for an ``HTTPException``-type exception, re-raise it + to be handled by the interactive debugger instead of returning it as a + simple error response. + + Default: ``False`` + +.. py:data:: TRAP_BAD_REQUEST_ERRORS + + Trying to access a key that doesn't exist from request dicts like ``args`` + and ``form`` will return a 400 Bad Request error page. Enable this to treat + the error as an unhandled exception instead so that you get the interactive + debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If + unset, it is enabled in debug mode. + + Default: ``None`` + +.. py:data:: SECRET_KEY + + A secret key that will be used for securely signing the session cookie + and can be used for any other security related needs by extensions or your + application. It should be a long random ``bytes`` or ``str``. For + example, copy the output of this to your config:: + + $ python -c 'import secrets; print(secrets.token_hex())' + '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + + **Do not reveal the secret key when posting questions or committing code.** + + Default: ``None`` + +.. py:data:: SESSION_COOKIE_NAME + + The name of the session cookie. Can be changed in case you already have a + cookie with the same name. + + Default: ``'session'`` + +.. py:data:: SESSION_COOKIE_DOMAIN + + The value of the ``Domain`` parameter on the session cookie. If not set, browsers + will only send the cookie to the exact domain it was set from. Otherwise, they + will send it to any subdomain of the given value as well. + + Not setting this value is more restricted and secure than setting it. + + Default: ``None`` + + .. versionchanged:: 2.3 + Not set by default, does not fall back to ``SERVER_NAME``. + +.. py:data:: SESSION_COOKIE_PATH + + The path that the session cookie will be valid for. If not set, the cookie + will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set. + + Default: ``None`` + +.. py:data:: SESSION_COOKIE_HTTPONLY + + Browsers will not allow JavaScript access to cookies marked as "HTTP only" + for security. + + Default: ``True`` + +.. py:data:: SESSION_COOKIE_SECURE + + Browsers will only send cookies with requests over HTTPS if the cookie is + marked "secure". The application must be served over HTTPS for this to make + sense. + + Default: ``False`` + +.. py:data:: SESSION_COOKIE_SAMESITE + + Restrict how cookies are sent with requests from external sites. Can + be set to ``'Lax'`` (recommended) or ``'Strict'``. + See :ref:`security-cookie`. + + Default: ``None`` + + .. versionadded:: 1.0 + +.. py:data:: PERMANENT_SESSION_LIFETIME + + If ``session.permanent`` is true, the cookie's expiration will be set this + number of seconds in the future. Can either be a + :class:`datetime.timedelta` or an ``int``. + + Flask's default cookie implementation validates that the cryptographic + signature is not older than this value. + + Default: ``timedelta(days=31)`` (``2678400`` seconds) + +.. py:data:: SESSION_REFRESH_EACH_REQUEST + + Control whether the cookie is sent with every response when + ``session.permanent`` is true. Sending the cookie every time (the default) + can more reliably keep the session from expiring, but uses more bandwidth. + Non-permanent sessions are not affected. + + Default: ``True`` + +.. py:data:: USE_X_SENDFILE + + When serving files, set the ``X-Sendfile`` header instead of serving the + data with Flask. Some web servers, such as Apache, recognize this and serve + the data more efficiently. This only makes sense when using such a server. + + Default: ``False`` + +.. py:data:: SEND_FILE_MAX_AGE_DEFAULT + + When serving files, set the cache control max age to this number of + seconds. Can be a :class:`datetime.timedelta` or an ``int``. + Override this value on a per-file basis using + :meth:`~flask.Flask.get_send_file_max_age` on the application or + blueprint. + + If ``None``, ``send_file`` tells the browser to use conditional + requests will be used instead of a timed cache, which is usually + preferable. + + Default: ``None`` + +.. py:data:: SERVER_NAME + + Inform the application what host and port it is bound to. Required + for subdomain route matching support. + + If set, ``url_for`` can generate external URLs with only an application + context instead of a request context. + + Default: ``None`` + + .. versionchanged:: 2.3 + Does not affect ``SESSION_COOKIE_DOMAIN``. + +.. py:data:: APPLICATION_ROOT + + Inform the application what path it is mounted under by the application / + web server. This is used for generating URLs outside the context of a + request (inside a request, the dispatcher is responsible for setting + ``SCRIPT_NAME`` instead; see :doc:`/patterns/appdispatch` + for examples of dispatch configuration). + + Will be used for the session cookie path if ``SESSION_COOKIE_PATH`` is not + set. + + Default: ``'/'`` + +.. py:data:: PREFERRED_URL_SCHEME + + Use this scheme for generating external URLs when not in a request context. + + Default: ``'http'`` + +.. py:data:: MAX_CONTENT_LENGTH + + Don't read more than this many bytes from the incoming request data. If not + set and the request does not specify a ``CONTENT_LENGTH``, no data will be + read for security. + + Default: ``None`` + +.. py:data:: TEMPLATES_AUTO_RELOAD + + Reload templates when they are changed. If not set, it will be enabled in + debug mode. + + Default: ``None`` + +.. py:data:: EXPLAIN_TEMPLATE_LOADING + + Log debugging information tracing how a template file was loaded. This can + be useful to figure out why a template was not loaded or the wrong file + appears to be loaded. + + Default: ``False`` + +.. py:data:: MAX_COOKIE_SIZE + + Warn if cookie headers are larger than this many bytes. Defaults to + ``4093``. Larger cookies may be silently ignored by browsers. Set to + ``0`` to disable the warning. + +.. versionadded:: 0.4 + ``LOGGER_NAME`` + +.. versionadded:: 0.5 + ``SERVER_NAME`` + +.. versionadded:: 0.6 + ``MAX_CONTENT_LENGTH`` + +.. versionadded:: 0.7 + ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` + +.. versionadded:: 0.8 + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, + ``SESSION_COOKIE_SECURE`` + +.. versionadded:: 0.9 + ``PREFERRED_URL_SCHEME`` + +.. versionadded:: 0.10 + ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` + +.. versionadded:: 0.11 + ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``, + ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` + +.. versionchanged:: 1.0 + ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See + :doc:`/logging` for information about configuration. + + Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment + variable. + + Added :data:`SESSION_COOKIE_SAMESITE` to control the session + cookie's ``SameSite`` option. + + Added :data:`MAX_COOKIE_SIZE` to control a warning from Werkzeug. + +.. versionchanged:: 2.2 + Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``. + +.. versionchanged:: 2.3 + ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and + ``JSONIFY_PRETTYPRINT_REGULAR`` were removed. The default ``app.json`` provider has + equivalent attributes instead. + +.. versionchanged:: 2.3 + ``ENV`` was removed. + + +Configuring from Python Files +----------------------------- + +Configuration becomes more useful if you can store it in a separate file, ideally +located outside the actual application package. You can deploy your application, then +separately configure it for the specific deployment. + +A common pattern is this:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +This first loads the configuration from the +`yourapplication.default_settings` module and then overrides the values +with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` +environment variable points to. This environment variable can be set +in the shell before starting the server: + +.. tabs:: + + .. group-tab:: Bash + + .. code-block:: text + + $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg + $ flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: Fish + + .. code-block:: text + + $ set -x YOURAPPLICATION_SETTINGS /path/to/settings.cfg + $ flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: CMD + + .. code-block:: text + + > set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg + > flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: Powershell + + .. code-block:: text + + > $env:YOURAPPLICATION_SETTINGS = "\path\to\settings.cfg" + > flask run + * Running on http://127.0.0.1:5000/ + +The configuration files themselves are actual Python files. Only values +in uppercase are actually stored in the config object later on. So make +sure to use uppercase letters for your config keys. + +Here is an example of a configuration file:: + + # Example configuration + SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +Make sure to load the configuration very early on, so that extensions have +the ability to access the configuration when starting up. There are other +methods on the config object as well to load from individual files. For a +complete reference, read the :class:`~flask.Config` object's +documentation. + + +Configuring from Data Files +--------------------------- + +It is also possible to load configuration from a file in a format of +your choice using :meth:`~flask.Config.from_file`. For example to load +from a TOML file: + +.. code-block:: python + + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) + +Or from a JSON file: + +.. code-block:: python + + import json + app.config.from_file("config.json", load=json.load) + + +Configuring from Environment Variables +-------------------------------------- + +In addition to pointing to configuration files using environment +variables, you may find it useful (or necessary) to control your +configuration values directly from the environment. Flask can be +instructed to load all environment variables starting with a specific +prefix into the config using :meth:`~flask.Config.from_prefixed_env`. + +Environment variables can be set in the shell before starting the +server: + +.. tabs:: + + .. group-tab:: Bash + + .. code-block:: text + + $ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" + $ export FLASK_MAIL_ENABLED=false + $ flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: Fish + + .. code-block:: text + + $ set -x FLASK_SECRET_KEY "5f352379324c22463451387a0aec5d2f" + $ set -x FLASK_MAIL_ENABLED false + $ flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: CMD + + .. code-block:: text + + > set FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" + > set FLASK_MAIL_ENABLED=false + > flask run + * Running on http://127.0.0.1:5000/ + + .. group-tab:: Powershell + + .. code-block:: text + + > $env:FLASK_SECRET_KEY = "5f352379324c22463451387a0aec5d2f" + > $env:FLASK_MAIL_ENABLED = "false" + > flask run + * Running on http://127.0.0.1:5000/ + +The variables can then be loaded and accessed via the config with a key +equal to the environment variable name without the prefix i.e. + +.. code-block:: python + + app.config.from_prefixed_env() + app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f" + +The prefix is ``FLASK_`` by default. This is configurable via the +``prefix`` argument of :meth:`~flask.Config.from_prefixed_env`. + +Values will be parsed to attempt to convert them to a more specific type +than strings. By default :func:`json.loads` is used, so any valid JSON +value is possible, including lists and dicts. This is configurable via +the ``loads`` argument of :meth:`~flask.Config.from_prefixed_env`. + +When adding a boolean value with the default JSON parsing, only "true" +and "false", lowercase, are valid values. Keep in mind that any +non-empty string is considered ``True`` by Python. + +It is possible to set keys in nested dictionaries by separating the +keys with double underscore (``__``). Any intermediate keys that don't +exist on the parent dict will be initialized to an empty dict. + +.. code-block:: text + + $ export FLASK_MYAPI__credentials__username=user123 + +.. code-block:: python + + app.config["MYAPI"]["credentials"]["username"] # Is "user123" + +On Windows, environment variable keys are always uppercase, therefore +the above example would end up as ``MYAPI__CREDENTIALS__USERNAME``. + +For even more config loading features, including merging and +case-insensitive Windows support, try a dedicated library such as +Dynaconf_, which includes integration with Flask. + +.. _Dynaconf: https://www.dynaconf.com/ + + +Configuration Best Practices +---------------------------- + +The downside with the approach mentioned earlier is that it makes testing +a little harder. There is no single 100% solution for this problem in +general, but there are a couple of things you can keep in mind to improve +that experience: + +1. Create your application in a function and register blueprints on it. + That way you can create multiple instances of your application with + different configurations attached which makes unit testing a lot + easier. You can use this to pass in configuration as needed. + +2. Do not write code that needs the configuration at import time. If you + limit yourself to request-only accesses to the configuration you can + reconfigure the object later on as needed. + +3. Make sure to load the configuration very early on, so that + extensions can access the configuration when calling ``init_app``. + + +.. _config-dev-prod: + +Development / Production +------------------------ + +Most applications need more than one configuration. There should be at +least separate configurations for the production server and the one used +during development. The easiest way to handle this is to use a default +configuration that is always loaded and part of the version control, and a +separate configuration that overrides the values as necessary as mentioned +in the example above:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +Then you just have to add a separate :file:`config.py` file and export +``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However +there are alternative ways as well. For example you could use imports or +subclassing. + +What is very popular in the Django world is to make the import explicit in +the config file by adding ``from yourapplication.default_settings +import *`` to the top of the file and then overriding the changes by hand. +You could also inspect an environment variable like +``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc +and import different hard-coded files based on that. + +An interesting pattern is also to use classes and inheritance for +configuration:: + + class Config(object): + TESTING = False + + class ProductionConfig(Config): + DATABASE_URI = 'mysql://user@localhost/foo' + + class DevelopmentConfig(Config): + DATABASE_URI = "sqlite:////tmp/foo.db" + + class TestingConfig(Config): + DATABASE_URI = 'sqlite:///:memory:' + TESTING = True + +To enable such a config you just have to call into +:meth:`~flask.Config.from_object`:: + + app.config.from_object('configmodule.ProductionConfig') + +Note that :meth:`~flask.Config.from_object` does not instantiate the class +object. If you need to instantiate the class, such as to access a property, +then you must do so before calling :meth:`~flask.Config.from_object`:: + + from configmodule import ProductionConfig + app.config.from_object(ProductionConfig()) + + # Alternatively, import via string: + from werkzeug.utils import import_string + cfg = import_string('configmodule.ProductionConfig')() + app.config.from_object(cfg) + +Instantiating the configuration object allows you to use ``@property`` in +your configuration classes:: + + class Config(object): + """Base config, uses staging database server.""" + TESTING = False + DB_SERVER = '192.168.1.56' + + @property + def DATABASE_URI(self): # Note: all caps + return f"mysql://user@{self.DB_SERVER}/foo" + + class ProductionConfig(Config): + """Uses production database server.""" + DB_SERVER = '192.168.19.32' + + class DevelopmentConfig(Config): + DB_SERVER = 'localhost' + + class TestingConfig(Config): + DB_SERVER = 'localhost' + DATABASE_URI = 'sqlite:///:memory:' + +There are many different ways and it's up to you how you want to manage +your configuration files. However here a list of good recommendations: + +- Keep a default configuration in version control. Either populate the + config with this default configuration or import it in your own + configuration files before overriding values. +- Use an environment variable to switch between the configurations. + This can be done from outside the Python interpreter and makes + development and deployment much easier because you can quickly and + easily switch between different configs without having to touch the + code at all. If you are working often on different projects you can + even create your own script for sourcing that activates a virtualenv + and exports the development configuration for you. +- Use a tool like `fabric`_ to push code and configuration separately + to the production server(s). + +.. _fabric: https://www.fabfile.org/ + + +.. _instance-folders: + +Instance Folders +---------------- + +.. versionadded:: 0.8 + +Flask 0.8 introduces instance folders. Flask for a long time made it +possible to refer to paths relative to the application's folder directly +(via :attr:`Flask.root_path`). This was also how many developers loaded +configurations stored next to the application. Unfortunately however this +only works well if applications are not packages in which case the root +path refers to the contents of the package. + +With Flask 0.8 a new attribute was introduced: +:attr:`Flask.instance_path`. It refers to a new concept called the +“instance folder”. The instance folder is designed to not be under +version control and be deployment specific. It's the perfect place to +drop things that either change at runtime or configuration files. + +You can either explicitly provide the path of the instance folder when +creating the Flask application or you can let Flask autodetect the +instance folder. For explicit configuration use the `instance_path` +parameter:: + + app = Flask(__name__, instance_path='/path/to/instance/folder') + +Please keep in mind that this path *must* be absolute when provided. + +If the `instance_path` parameter is not provided the following default +locations are used: + +- Uninstalled module:: + + /myapp.py + /instance + +- Uninstalled package:: + + /myapp + /__init__.py + /instance + +- Installed module or package:: + + $PREFIX/lib/pythonX.Y/site-packages/myapp + $PREFIX/var/myapp-instance + + ``$PREFIX`` is the prefix of your Python installation. This can be + ``/usr`` or the path to your virtualenv. You can print the value of + ``sys.prefix`` to see what the prefix is set to. + +Since the config object provided loading of configuration files from +relative filenames we made it possible to change the loading via filenames +to be relative to the instance path if wanted. The behavior of relative +paths in config files can be flipped between “relative to the application +root” (the default) to “relative to instance folder” via the +`instance_relative_config` switch to the application constructor:: + + app = Flask(__name__, instance_relative_config=True) + +Here is a full example of how to configure Flask to preload the config +from a module and then override the config from a file in the instance +folder if it exists:: + + app = Flask(__name__, instance_relative_config=True) + app.config.from_object('yourapplication.default_settings') + app.config.from_pyfile('application.cfg', silent=True) + +The path to the instance folder can be found via the +:attr:`Flask.instance_path`. Flask also provides a shortcut to open a +file from the instance folder with :meth:`Flask.open_instance_resource`. + +Example usage for both:: + + filename = os.path.join(app.instance_path, 'application.cfg') + with open(filename) as f: + config = f.read() + + # or via open_instance_resource: + with app.open_instance_resource('application.cfg') as f: + config = f.read() diff --git a/test/fixtures/whole_applications/flask/docs/contributing.rst b/test/fixtures/whole_applications/flask/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/test/fixtures/whole_applications/flask/docs/debugging.rst b/test/fixtures/whole_applications/flask/docs/debugging.rst new file mode 100644 index 0000000..f6b56ca --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/debugging.rst @@ -0,0 +1,99 @@ +Debugging Application Errors +============================ + + +In Production +------------- + +**Do not run the development server, or enable the built-in debugger, in +a production environment.** The debugger allows executing arbitrary +Python code from the browser. It's protected by a pin, but that should +not be relied on for security. + +Use an error logging tool, such as Sentry, as described in +:ref:`error-logging-tools`, or enable logging and notifications as +described in :doc:`/logging`. + +If you have access to the server, you could add some code to start an +external debugger if ``request.remote_addr`` matches your IP. Some IDE +debuggers also have a remote mode so breakpoints on the server can be +interacted with locally. Only enable a debugger temporarily. + + +The Built-In Debugger +--------------------- + +The built-in Werkzeug development server provides a debugger which shows +an interactive traceback in the browser when an unhandled error occurs +during a request. This debugger should only be used during development. + +.. image:: _static/debugger.png + :align: center + :class: screenshot + :alt: screenshot of debugger in action + +.. warning:: + + The debugger allows executing arbitrary Python code from the + browser. It is protected by a pin, but still represents a major + security risk. Do not run the development server or debugger in a + production environment. + +The debugger is enabled by default when the development server is run in debug mode. + +.. code-block:: text + + $ flask --app hello run --debug + +When running from Python code, passing ``debug=True`` enables debug mode, which is +mostly equivalent. + +.. code-block:: python + + app.run(debug=True) + +:doc:`/server` and :doc:`/cli` have more information about running the debugger and +debug mode. More information about the debugger can be found in the `Werkzeug +documentation `__. + + +External Debuggers +------------------ + +External debuggers, such as those provided by IDEs, can offer a more +powerful debugging experience than the built-in debugger. They can also +be used to step through code during a request before an error is raised, +or if no error is raised. Some even have a remote mode so you can debug +code running on another machine. + +When using an external debugger, the app should still be in debug mode, otherwise Flask +turns unhandled errors into generic 500 error pages. However, the built-in debugger and +reloader should be disabled so they don't interfere with the external debugger. + +.. code-block:: text + + $ flask --app hello run --debug --no-debugger --no-reload + +When running from Python: + +.. code-block:: python + + app.run(debug=True, use_debugger=False, use_reloader=False) + +Disabling these isn't required, an external debugger will continue to work with the +following caveats. + +- If the built-in debugger is not disabled, it will catch unhandled exceptions before + the external debugger can. +- If the reloader is not disabled, it could cause an unexpected reload if code changes + during a breakpoint. +- The development server will still catch unhandled exceptions if the built-in + debugger is disabled, otherwise it would crash on any error. If you want that (and + usually you don't) pass ``passthrough_errors=True`` to ``app.run``. + + .. code-block:: python + + app.run( + debug=True, passthrough_errors=True, + use_debugger=False, use_reloader=False + ) diff --git a/test/fixtures/whole_applications/flask/docs/deploying/apache-httpd.rst b/test/fixtures/whole_applications/flask/docs/deploying/apache-httpd.rst new file mode 100644 index 0000000..bdeaf62 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/apache-httpd.rst @@ -0,0 +1,66 @@ +Apache httpd +============ + +`Apache httpd`_ is a fast, production level HTTP server. When serving +your application with one of the WSGI servers listed in :doc:`index`, it +is often good or necessary to put a dedicated HTTP server in front of +it. This "reverse proxy" can handle incoming requests, TLS, and other +security and performance concerns better than the WSGI server. + +httpd can be installed using your system package manager, or a pre-built +executable for Windows. Installing and running httpd itself is outside +the scope of this doc. This page outlines the basics of configuring +httpd to proxy your application. Be sure to read its documentation to +understand what features are available. + +.. _Apache httpd: https://httpd.apache.org/ + + +Domain Name +----------- + +Acquiring and configuring a domain name is outside the scope of this +doc. In general, you will buy a domain name from a registrar, pay for +server space with a hosting provider, and then point your registrar +at the hosting provider's name servers. + +To simulate this, you can also edit your ``hosts`` file, located at +``/etc/hosts`` on Linux. Add a line that associates a name with the +local IP. + +Modern Linux systems may be configured to treat any domain name that +ends with ``.localhost`` like this without adding it to the ``hosts`` +file. + +.. code-block:: python + :caption: ``/etc/hosts`` + + 127.0.0.1 hello.localhost + + +Configuration +------------- + +The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on +Linux. It may be different depending on your operating system. Check the +docs and look for ``httpd.conf``. + +Remove or comment out any existing ``DocumentRoot`` directive. Add the +config lines below. We'll assume the WSGI server is listening locally at +``http://127.0.0.1:8000``. + +.. code-block:: apache + :caption: ``/etc/httpd/conf/httpd.conf`` + + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass / http://127.0.0.1:8000/ + RequestHeader set X-Forwarded-Proto http + RequestHeader set X-Forwarded-Prefix / + +The ``LoadModule`` lines might already exist. If so, make sure they are +uncommented instead of adding them manually. + +Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded`` +headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically +set by ``ProxyPass``. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/asgi.rst b/test/fixtures/whole_applications/flask/docs/deploying/asgi.rst new file mode 100644 index 0000000..36acff8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/asgi.rst @@ -0,0 +1,27 @@ +ASGI +==== + +If you'd like to use an ASGI server you will need to utilise WSGI to +ASGI middleware. The asgiref +`WsgiToAsgi `_ +adapter is recommended as it integrates with the event loop used for +Flask's :ref:`async_await` support. You can use the adapter by +wrapping the Flask app, + +.. code-block:: python + + from asgiref.wsgi import WsgiToAsgi + from flask import Flask + + app = Flask(__name__) + + ... + + asgi_app = WsgiToAsgi(app) + +and then serving the ``asgi_app`` with the ASGI server, e.g. using +`Hypercorn `_, + +.. sourcecode:: text + + $ hypercorn module:asgi_app diff --git a/test/fixtures/whole_applications/flask/docs/deploying/eventlet.rst b/test/fixtures/whole_applications/flask/docs/deploying/eventlet.rst new file mode 100644 index 0000000..8a718b2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/eventlet.rst @@ -0,0 +1,80 @@ +eventlet +======== + +Prefer using :doc:`gunicorn` with eventlet workers rather than using +`eventlet`_ directly. Gunicorn provides a much more configurable and +production-tested server. + +`eventlet`_ allows writing asynchronous, coroutine-based code that looks +like standard synchronous Python. It uses `greenlet`_ to enable task +switching without writing ``async/await`` or using ``asyncio``. + +:doc:`gevent` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +eventlet provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use eventlet in +your own code to see any benefit to using the server. + +.. _eventlet: https://eventlet.net/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using eventlet, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +Create a virtualenv, install your application, then install +``eventlet``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install eventlet + + +Running +------- + +To use eventlet to serve your application, write a script that imports +its ``wsgi.server``, as well as your app or app factory. + +.. code-block:: python + :caption: ``wsgi.py`` + + import eventlet + from eventlet import wsgi + from hello import create_app + + app = create_app() + wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app) + +.. code-block:: text + + $ python wsgi.py + (x) wsgi starting up on http://127.0.0.1:8000 + + +Binding Externally +------------------ + +eventlet should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of eventlet. + +You can bind to all external IPs on a non-privileged port by using +``0.0.0.0`` in the server arguments shown in the previous section. +Don't do this when using a reverse proxy setup, otherwise it will be +possible to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/gevent.rst b/test/fixtures/whole_applications/flask/docs/deploying/gevent.rst new file mode 100644 index 0000000..448b93e --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/gevent.rst @@ -0,0 +1,80 @@ +gevent +====== + +Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather +than using `gevent`_ directly. Gunicorn and uWSGI provide much more +configurable and production-tested servers. + +`gevent`_ allows writing asynchronous, coroutine-based code that looks +like standard synchronous Python. It uses `greenlet`_ to enable task +switching without writing ``async/await`` or using ``asyncio``. + +:doc:`eventlet` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +gevent provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use gevent in your +own code to see any benefit to using the server. + +.. _gevent: https://www.gevent.org/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +Create a virtualenv, install your application, then install ``gevent``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install gevent + + +Running +------- + +To use gevent to serve your application, write a script that imports its +``WSGIServer``, as well as your app or app factory. + +.. code-block:: python + :caption: ``wsgi.py`` + + from gevent.pywsgi import WSGIServer + from hello import create_app + + app = create_app() + http_server = WSGIServer(("127.0.0.1", 8000), app) + http_server.serve_forever() + +.. code-block:: text + + $ python wsgi.py + +No output is shown when the server starts. + + +Binding Externally +------------------ + +gevent should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of gevent. + +You can bind to all external IPs on a non-privileged port by using +``0.0.0.0`` in the server arguments shown in the previous section. Don't +do this when using a reverse proxy setup, otherwise it will be possible +to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/gunicorn.rst b/test/fixtures/whole_applications/flask/docs/deploying/gunicorn.rst new file mode 100644 index 0000000..c50edc2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/gunicorn.rst @@ -0,0 +1,130 @@ +Gunicorn +======== + +`Gunicorn`_ is a pure Python WSGI server with simple configuration and +multiple worker implementations for performance tuning. + +* It tends to integrate easily with hosting platforms. +* It does not support Windows (but does run on WSL). +* It is easy to install as it does not require additional dependencies + or compilation. +* It has built-in async worker support using gevent or eventlet. + +This page outlines the basics of running Gunicorn. Be sure to read its +`documentation`_ and use ``gunicorn --help`` to understand what features +are available. + +.. _Gunicorn: https://gunicorn.org/ +.. _documentation: https://docs.gunicorn.org/ + + +Installing +---------- + +Gunicorn is easy to install, as it does not require external +dependencies or compilation. It runs on Windows only under WSL. + +Create a virtualenv, install your application, then install +``gunicorn``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install gunicorn + + +Running +------- + +The only required argument to Gunicorn tells it how to load your Flask +application. The syntax is ``{module_import}:{app_variable}``. +``module_import`` is the dotted import name to the module with your +application. ``app_variable`` is the variable with the application. It +can also be a function call (with any arguments) if you're using the +app factory pattern. + +.. code-block:: text + + # equivalent to 'from hello import app' + $ gunicorn -w 4 'hello:app' + + # equivalent to 'from hello import create_app; create_app()' + $ gunicorn -w 4 'hello:create_app()' + + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: sync + Booting worker with pid: x + Booting worker with pid: x + Booting worker with pid: x + Booting worker with pid: x + +The ``-w`` option specifies the number of processes to run; a starting +value could be ``CPU * 2``. The default is only 1 worker, which is +probably not what you want for the default worker type. + +Logs for each request aren't shown by default, only worker info and +errors are shown. To show access logs on stdout, use the +``--access-logfile=-`` option. + + +Binding Externally +------------------ + +Gunicorn should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of Gunicorn. + +You can bind to all external IPs on a non-privileged port using the +``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup, +otherwise it will be possible to bypass the proxy. + +.. code-block:: text + + $ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()' + Listening at: http://0.0.0.0:8000 (x) + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. + + +Async with gevent or eventlet +----------------------------- + +The default sync worker is appropriate for many use cases. If you need +asynchronous support, Gunicorn provides workers using either `gevent`_ +or `eventlet`_. This is not the same as Python's ``async/await``, or the +ASGI server spec. You must actually use gevent/eventlet in your own code +to see any benefit to using the workers. + +When using either gevent or eventlet, greenlet>=1.0 is required, +otherwise context locals such as ``request`` will not work as expected. +When using PyPy, PyPy>=7.3.7 is required. + +To use gevent: + +.. code-block:: text + + $ gunicorn -k gevent 'hello:create_app()' + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: gevent + Booting worker with pid: x + +To use eventlet: + +.. code-block:: text + + $ gunicorn -k eventlet 'hello:create_app()' + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: eventlet + Booting worker with pid: x + +.. _gevent: https://www.gevent.org/ +.. _eventlet: https://eventlet.net/ diff --git a/test/fixtures/whole_applications/flask/docs/deploying/index.rst b/test/fixtures/whole_applications/flask/docs/deploying/index.rst new file mode 100644 index 0000000..4135596 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/index.rst @@ -0,0 +1,79 @@ +Deploying to Production +======================= + +After developing your application, you'll want to make it available +publicly to other users. When you're developing locally, you're probably +using the built-in development server, debugger, and reloader. These +should not be used in production. Instead, you should use a dedicated +WSGI server or hosting platform, some of which will be described here. + +"Production" means "not development", which applies whether you're +serving your application publicly to millions of users or privately / +locally to a single user. **Do not use the development server when +deploying to production. It is intended for use only during local +development. It is not designed to be particularly secure, stable, or +efficient.** + +Self-Hosted Options +------------------- + +Flask is a WSGI *application*. A WSGI *server* is used to run the +application, converting incoming HTTP requests to the standard WSGI +environ, and converting outgoing WSGI responses to HTTP responses. + +The primary goal of these docs is to familiarize you with the concepts +involved in running a WSGI application using a production WSGI server +and HTTP server. There are many WSGI servers and HTTP servers, with many +configuration possibilities. The pages below discuss the most common +servers, and show the basics of running each one. The next section +discusses platforms that can manage this for you. + +.. toctree:: + :maxdepth: 1 + + gunicorn + waitress + mod_wsgi + uwsgi + gevent + eventlet + asgi + +WSGI servers have HTTP servers built-in. However, a dedicated HTTP +server may be safer, more efficient, or more capable. Putting an HTTP +server in front of the WSGI server is called a "reverse proxy." + +.. toctree:: + :maxdepth: 1 + + proxy_fix + nginx + apache-httpd + +This list is not exhaustive, and you should evaluate these and other +servers based on your application's needs. Different servers will have +different capabilities, configuration, and support. + + +Hosting Platforms +----------------- + +There are many services available for hosting web applications without +needing to maintain your own server, networking, domain, etc. Some +services may have a free tier up to a certain time or bandwidth. Many of +these services use one of the WSGI servers described above, or a similar +interface. The links below are for some of the most common platforms, +which have instructions for Flask, WSGI, or Python. + +- `PythonAnywhere `_ +- `Google App Engine `_ +- `Google Cloud Run `_ +- `AWS Elastic Beanstalk `_ +- `Microsoft Azure `_ + +This list is not exhaustive, and you should evaluate these and other +services based on your application's needs. Different services will have +different capabilities, configuration, pricing, and support. + +You'll probably need to :doc:`proxy_fix` when using most hosting +platforms. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/mod_wsgi.rst b/test/fixtures/whole_applications/flask/docs/deploying/mod_wsgi.rst new file mode 100644 index 0000000..23e8227 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/mod_wsgi.rst @@ -0,0 +1,94 @@ +mod_wsgi +======== + +`mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server. +The modern `mod_wsgi-express`_ command makes it easy to configure and +start the server without needing to write Apache httpd configuration. + +* Tightly integrated with Apache httpd. +* Supports Windows directly. +* Requires a compiler and the Apache development headers to install. +* Does not require a reverse proxy setup. + +This page outlines the basics of running mod_wsgi-express, not the more +complex installation and configuration with httpd. Be sure to read the +`mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to +understand what features are available. + +.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ +.. _mod_wsgi: https://modwsgi.readthedocs.io/ +.. _Apache httpd: https://httpd.apache.org/ + + +Installing +---------- + +Installing mod_wsgi requires a compiler and the Apache server and +development headers installed. You will get an error if they are not. +How to install them depends on the OS and package manager that you use. + +Create a virtualenv, install your application, then install +``mod_wsgi``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install mod_wsgi + + +Running +------- + +The only argument to ``mod_wsgi-express`` specifies a script containing +your Flask application, which must be called ``application``. You can +write a small script to import your app with this name, or to create it +if using the app factory pattern. + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import app + + application = app + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import create_app + + application = create_app() + +Now run the ``mod_wsgi-express start-server`` command. + +.. code-block:: text + + $ mod_wsgi-express start-server wsgi.py --processes 4 + +The ``--processes`` option specifies the number of worker processes to +run; a starting value could be ``CPU * 2``. + +Logs for each request aren't show in the terminal. If an error occurs, +its information is written to the error log file shown when starting the +server. + + +Binding Externally +------------------ + +Unlike the other WSGI servers in these docs, mod_wsgi can be run as +root to bind to privileged ports like 80 and 443. However, it must be +configured to drop permissions to a different user and group for the +worker processes. + +For example, if you created a ``hello`` user and group, you should +install your virtualenv and application as that user, then tell +mod_wsgi to drop to that user after starting. + +.. code-block:: text + + $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ + /home/hello/wsgi.py \ + --user hello --group hello --port 80 --processes 4 diff --git a/test/fixtures/whole_applications/flask/docs/deploying/nginx.rst b/test/fixtures/whole_applications/flask/docs/deploying/nginx.rst new file mode 100644 index 0000000..6b25c07 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/nginx.rst @@ -0,0 +1,69 @@ +nginx +===== + +`nginx`_ is a fast, production level HTTP server. When serving your +application with one of the WSGI servers listed in :doc:`index`, it is +often good or necessary to put a dedicated HTTP server in front of it. +This "reverse proxy" can handle incoming requests, TLS, and other +security and performance concerns better than the WSGI server. + +Nginx can be installed using your system package manager, or a pre-built +executable for Windows. Installing and running Nginx itself is outside +the scope of this doc. This page outlines the basics of configuring +Nginx to proxy your application. Be sure to read its documentation to +understand what features are available. + +.. _nginx: https://nginx.org/ + + +Domain Name +----------- + +Acquiring and configuring a domain name is outside the scope of this +doc. In general, you will buy a domain name from a registrar, pay for +server space with a hosting provider, and then point your registrar +at the hosting provider's name servers. + +To simulate this, you can also edit your ``hosts`` file, located at +``/etc/hosts`` on Linux. Add a line that associates a name with the +local IP. + +Modern Linux systems may be configured to treat any domain name that +ends with ``.localhost`` like this without adding it to the ``hosts`` +file. + +.. code-block:: python + :caption: ``/etc/hosts`` + + 127.0.0.1 hello.localhost + + +Configuration +------------- + +The nginx configuration is located at ``/etc/nginx/nginx.conf`` on +Linux. It may be different depending on your operating system. Check the +docs and look for ``nginx.conf``. + +Remove or comment out any existing ``server`` section. Add a ``server`` +section and use the ``proxy_pass`` directive to point to the address the +WSGI server is listening on. We'll assume the WSGI server is listening +locally at ``http://127.0.0.1:8000``. + +.. code-block:: nginx + :caption: ``/etc/nginx.conf`` + + server { + listen 80; + server_name _; + + location / { + proxy_pass http://127.0.0.1:8000/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Prefix /; + } + } + +Then :doc:`proxy_fix` so that your application uses these headers. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/proxy_fix.rst b/test/fixtures/whole_applications/flask/docs/deploying/proxy_fix.rst new file mode 100644 index 0000000..e2c42e8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/proxy_fix.rst @@ -0,0 +1,33 @@ +Tell Flask it is Behind a Proxy +=============================== + +When using a reverse proxy, or many Python hosting platforms, the proxy +will intercept and forward all external requests to the local WSGI +server. + +From the WSGI server and Flask application's perspectives, requests are +now coming from the HTTP server to the local address, rather than from +the remote address to the external server address. + +HTTP servers should set ``X-Forwarded-`` headers to pass on the real +values to the application. The application can then be told to trust and +use those values by wrapping it with the +:doc:`werkzeug:middleware/proxy_fix` middleware provided by Werkzeug. + +This middleware should only be used if the application is actually +behind a proxy, and should be configured with the number of proxies that +are chained in front of it. Not all proxies set all the headers. Since +incoming headers can be faked, you must set how many proxies are setting +each header so the middleware knows what to trust. + +.. code-block:: python + + from werkzeug.middleware.proxy_fix import ProxyFix + + app.wsgi_app = ProxyFix( + app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 + ) + +Remember, only apply this middleware if you are behind a proxy, and set +the correct number of proxies that set each header. It can be a security +issue if you get this configuration wrong. diff --git a/test/fixtures/whole_applications/flask/docs/deploying/uwsgi.rst b/test/fixtures/whole_applications/flask/docs/deploying/uwsgi.rst new file mode 100644 index 0000000..1f9d5ec --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/uwsgi.rst @@ -0,0 +1,145 @@ +uWSGI +===== + +`uWSGI`_ is a fast, compiled server suite with extensive configuration +and capabilities beyond a basic server. + +* It can be very performant due to being a compiled program. +* It is complex to configure beyond the basic application, and has so + many options that it can be difficult for beginners to understand. +* It does not support Windows (but does run on WSL). +* It requires a compiler to install in some cases. + +This page outlines the basics of running uWSGI. Be sure to read its +documentation to understand what features are available. + +.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ + + +Installing +---------- + +uWSGI has multiple ways to install it. The most straightforward is to +install the ``pyuwsgi`` package, which provides precompiled wheels for +common platforms. However, it does not provide SSL support, which can be +provided with a reverse proxy instead. + +Create a virtualenv, install your application, then install ``pyuwsgi``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install pyuwsgi + +If you have a compiler available, you can install the ``uwsgi`` package +instead. Or install the ``pyuwsgi`` package from sdist instead of wheel. +Either method will include SSL support. + +.. code-block:: text + + $ pip install uwsgi + + # or + $ pip install --no-binary pyuwsgi pyuwsgi + + +Running +------- + +The most basic way to run uWSGI is to tell it to start an HTTP server +and import your application. + +.. code-block:: text + + $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app + + *** Starting uWSGI 2.0.20 (64bit) on [x] *** + *** Operational MODE: preforking *** + mounting hello:app on / + spawned uWSGI master process (pid: x) + spawned uWSGI worker 1 (pid: x, cores: 1) + spawned uWSGI worker 2 (pid: x, cores: 1) + spawned uWSGI worker 3 (pid: x, cores: 1) + spawned uWSGI worker 4 (pid: x, cores: 1) + spawned uWSGI http 1 (pid: x) + +If you're using the app factory pattern, you'll need to create a small +Python file to create the app, then point uWSGI at that. + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import create_app + + app = create_app() + +.. code-block:: text + + $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app + +The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The +``--master`` option specifies the standard worker manager. The ``-p`` +option starts 4 worker processes; a starting value could be ``CPU * 2``. +The ``-w`` option tells uWSGI how to import your application + + +Binding Externally +------------------ + +uWSGI should not be run as root with the configuration shown in this doc +because it would cause your application code to run as root, which is +not secure. However, this means it will not be possible to bind to port +80 or 443. Instead, a reverse proxy such as :doc:`nginx` or +:doc:`apache-httpd` should be used in front of uWSGI. It is possible to +run uWSGI as root securely, but that is beyond the scope of this doc. + +uWSGI has optimized integration with `Nginx uWSGI`_ and +`Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using +a standard HTTP proxy. That configuration is beyond the scope of this +doc, see the links for more information. + +.. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html +.. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi + +You can bind to all external IPs on a non-privileged port using the +``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy +setup, otherwise it will be possible to bypass the proxy. + +.. code-block:: text + + $ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. + + +Async with gevent +----------------- + +The default sync worker is appropriate for many use cases. If you need +asynchronous support, uWSGI provides a `gevent`_ worker. This is not the +same as Python's ``async/await``, or the ASGI server spec. You must +actually use gevent in your own code to see any benefit to using the +worker. + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +.. code-block:: text + + $ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app + + *** Starting uWSGI 2.0.20 (64bit) on [x] *** + *** Operational MODE: async *** + mounting hello:app on / + spawned uWSGI master process (pid: x) + spawned uWSGI worker 1 (pid: x, cores: 100) + spawned uWSGI http 1 (pid: x) + *** running gevent loop engine [addr:x] *** + + +.. _gevent: https://www.gevent.org/ diff --git a/test/fixtures/whole_applications/flask/docs/deploying/waitress.rst b/test/fixtures/whole_applications/flask/docs/deploying/waitress.rst new file mode 100644 index 0000000..aeafb9f --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/deploying/waitress.rst @@ -0,0 +1,75 @@ +Waitress +======== + +`Waitress`_ is a pure Python WSGI server. + +* It is easy to configure. +* It supports Windows directly. +* It is easy to install as it does not require additional dependencies + or compilation. +* It does not support streaming requests, full request data is always + buffered. +* It uses a single process with multiple thread workers. + +This page outlines the basics of running Waitress. Be sure to read its +documentation and ``waitress-serve --help`` to understand what features +are available. + +.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ + + +Installing +---------- + +Create a virtualenv, install your application, then install +``waitress``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install waitress + + +Running +------- + +The only required argument to ``waitress-serve`` tells it how to load +your Flask application. The syntax is ``{module}:{app}``. ``module`` is +the dotted import name to the module with your application. ``app`` is +the variable with the application. If you're using the app factory +pattern, use ``--call {module}:{factory}`` instead. + +.. code-block:: text + + # equivalent to 'from hello import app' + $ waitress-serve --host 127.0.0.1 hello:app + + # equivalent to 'from hello import create_app; create_app()' + $ waitress-serve --host 127.0.0.1 --call hello:create_app + + Serving on http://127.0.0.1:8080 + +The ``--host`` option binds the server to local ``127.0.0.1`` only. + +Logs for each request aren't shown, only errors are shown. Logging can +be configured through the Python interface instead of the command line. + + +Binding Externally +------------------ + +Waitress should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of Waitress. + +You can bind to all external IPs on a non-privileged port by not +specifying the ``--host`` option. Don't do this when using a revers +proxy setup, otherwise it will be possible to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/test/fixtures/whole_applications/flask/docs/design.rst b/test/fixtures/whole_applications/flask/docs/design.rst new file mode 100644 index 0000000..066cf10 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/design.rst @@ -0,0 +1,228 @@ +Design Decisions in Flask +========================= + +If you are curious why Flask does certain things the way it does and not +differently, this section is for you. This should give you an idea about +some of the design decisions that may appear arbitrary and surprising at +first, especially in direct comparison with other frameworks. + + +The Explicit Application Object +------------------------------- + +A Python web application based on WSGI has to have one central callable +object that implements the actual application. In Flask this is an +instance of the :class:`~flask.Flask` class. Each Flask application has +to create an instance of this class itself and pass it the name of the +module, but why can't Flask do that itself? + +Without such an explicit application object the following code:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + return 'Hello World!' + +Would look like this instead:: + + from hypothetical_flask import route + + @route('/') + def index(): + return 'Hello World!' + +There are three major reasons for this. The most important one is that +implicit application objects require that there may only be one instance at +the time. There are ways to fake multiple applications with a single +application object, like maintaining a stack of applications, but this +causes some problems I won't outline here in detail. Now the question is: +when does a microframework need more than one application at the same +time? A good example for this is unit testing. When you want to test +something it can be very helpful to create a minimal application to test +specific behavior. When the application object is deleted everything it +allocated will be freed again. + +Another thing that becomes possible when you have an explicit object lying +around in your code is that you can subclass the base class +(:class:`~flask.Flask`) to alter specific behavior. This would not be +possible without hacks if the object were created ahead of time for you +based on a class that is not exposed to you. + +But there is another very important reason why Flask depends on an +explicit instantiation of that class: the package name. Whenever you +create a Flask instance you usually pass it `__name__` as package name. +Flask depends on that information to properly load resources relative +to your module. With Python's outstanding support for reflection it can +then access the package to figure out where the templates and static files +are stored (see :meth:`~flask.Flask.open_resource`). Now obviously there +are frameworks around that do not need any configuration and will still be +able to load templates relative to your application module. But they have +to use the current working directory for that, which is a very unreliable +way to determine where the application is. The current working directory +is process-wide and if you are running multiple applications in one +process (which could happen in a webserver without you knowing) the paths +will be off. Worse: many webservers do not set the working directory to +the directory of your application but to the document root which does not +have to be the same folder. + +The third reason is "explicit is better than implicit". That object is +your WSGI application, you don't have to remember anything else. If you +want to apply a WSGI middleware, just wrap it and you're done (though +there are better ways to do that so that you do not lose the reference +to the application object :meth:`~flask.Flask.wsgi_app`). + +Furthermore this design makes it possible to use a factory function to +create the application which is very helpful for unit testing and similar +things (:doc:`/patterns/appfactories`). + +The Routing System +------------------ + +Flask uses the Werkzeug routing system which was designed to +automatically order routes by complexity. This means that you can declare +routes in arbitrary order and they will still work as expected. This is a +requirement if you want to properly implement decorator based routing +since decorators could be fired in undefined order when the application is +split into multiple modules. + +Another design decision with the Werkzeug routing system is that routes +in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far +with that in that it will automatically redirect to a canonical URL if a route +is ambiguous. + + +One Template Engine +------------------- + +Flask decides on one template engine: Jinja2. Why doesn't Flask have a +pluggable template engine interface? You can obviously use a different +template engine, but Flask will still configure Jinja2 for you. While +that limitation that Jinja2 is *always* configured will probably go away, +the decision to bundle one template engine and use that will not. + +Template engines are like programming languages and each of those engines +has a certain understanding about how things work. On the surface they +all work the same: you tell the engine to evaluate a template with a set +of variables and take the return value as string. + +But that's about where similarities end. Jinja2 for example has an +extensive filter system, a certain way to do template inheritance, +support for reusable blocks (macros) that can be used from inside +templates and also from Python code, supports iterative template +rendering, configurable syntax and more. On the other hand an engine +like Genshi is based on XML stream evaluation, template inheritance by +taking the availability of XPath into account and more. Mako on the +other hand treats templates similar to Python modules. + +When it comes to connecting a template engine with an application or +framework there is more than just rendering templates. For instance, +Flask uses Jinja2's extensive autoescaping support. Also it provides +ways to access macros from Jinja2 templates. + +A template abstraction layer that would not take the unique features of +the template engines away is a science on its own and a too large +undertaking for a microframework like Flask. + +Furthermore extensions can then easily depend on one template language +being present. You can easily use your own templating language, but an +extension could still depend on Jinja itself. + + +What does "micro" mean? +----------------------- + +“Micro” does not mean that your whole web application has to fit into a single +Python file (although it certainly can), nor does it mean that Flask is lacking +in functionality. The "micro" in microframework means Flask aims to keep the +core simple but extensible. Flask won't make many decisions for you, such as +what database to use. Those decisions that it does make, such as what +templating engine to use, are easy to change. Everything else is up to you, so +that Flask can be everything you need and nothing you don't. + +By default, Flask does not include a database abstraction layer, form +validation or anything else where different libraries already exist that can +handle that. Instead, Flask supports extensions to add such functionality to +your application as if it was implemented in Flask itself. Numerous extensions +provide database integration, form validation, upload handling, various open +authentication technologies, and more. Flask may be "micro", but it's ready for +production use on a variety of needs. + +Why does Flask call itself a microframework and yet it depends on two +libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look +over to the Ruby side of web development there we have a protocol very +similar to WSGI. Just that it's called Rack there, but besides that it +looks very much like a WSGI rendition for Ruby. But nearly all +applications in Ruby land do not work with Rack directly, but on top of a +library with the same name. This Rack library has two equivalents in +Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but +from my understanding it's sort of deprecated in favour of WebOb. The +development of WebOb and Werkzeug started side by side with similar ideas +in mind: be a good implementation of WSGI for other applications to take +advantage. + +Flask is a framework that takes advantage of the work already done by +Werkzeug to properly interface WSGI (which can be a complex task at +times). Thanks to recent developments in the Python package +infrastructure, packages with dependencies are no longer an issue and +there are very few reasons against having libraries that depend on others. + + +Thread Locals +------------- + +Flask uses thread local objects (context local objects in fact, they +support greenlet contexts as well) for request, session and an extra +object you can put your own things on (:data:`~flask.g`). Why is that and +isn't that a bad idea? + +Yes it is usually not such a bright idea to use thread locals. They cause +troubles for servers that are not based on the concept of threads and make +large applications harder to maintain. However Flask is just not designed +for large applications or asynchronous servers. Flask wants to make it +quick and easy to write a traditional web application. + + +Async/await and ASGI support +---------------------------- + +Flask supports ``async`` coroutines for view functions by executing the +coroutine on a separate thread instead of using an event loop on the +main thread as an async-first (ASGI) framework would. This is necessary +for Flask to remain backwards compatible with extensions and code built +before ``async`` was introduced into Python. This compromise introduces +a performance cost compared with the ASGI frameworks, due to the +overhead of the threads. + +Due to how tied to WSGI Flask's code is, it's not clear if it's possible +to make the ``Flask`` class support ASGI and WSGI at the same time. Work +is currently being done in Werkzeug to work with ASGI, which may +eventually enable support in Flask as well. + +See :doc:`/async-await` for more discussion. + + +What Flask is, What Flask is Not +-------------------------------- + +Flask will never have a database layer. It will not have a form library +or anything else in that direction. Flask itself just bridges to Werkzeug +to implement a proper WSGI application and to Jinja2 to handle templating. +It also binds to a few common standard library packages such as logging. +Everything else is up for extensions. + +Why is this the case? Because people have different preferences and +requirements and Flask could not meet those if it would force any of this +into the core. The majority of web applications will need a template +engine in some sort. However not every application needs a SQL database. + +As your codebase grows, you are free to make the design decisions appropriate +for your project. Flask will continue to provide a very simple glue layer to +the best that Python has to offer. You can implement advanced patterns in +SQLAlchemy or another database tool, introduce non-relational data persistence +as appropriate, and take advantage of framework-agnostic tools built for WSGI, +the Python web interface. + +The idea of Flask is to build a good foundation for all applications. +Everything else is up to you or extensions. diff --git a/test/fixtures/whole_applications/flask/docs/errorhandling.rst b/test/fixtures/whole_applications/flask/docs/errorhandling.rst new file mode 100644 index 0000000..faca58c --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/errorhandling.rst @@ -0,0 +1,523 @@ +Handling Application Errors +=========================== + +Applications fail, servers fail. Sooner or later you will see an exception +in production. Even if your code is 100% correct, you will still see +exceptions from time to time. Why? Because everything else involved will +fail. Here are some situations where perfectly fine code can lead to server +errors: + +- the client terminated the request early and the application was still + reading from the incoming data +- the database server was overloaded and could not handle the query +- a filesystem is full +- a harddrive crashed +- a backend server overloaded +- a programming error in a library you are using +- network connection of the server to another system failed + +And that's just a small sample of issues you could be facing. So how do we +deal with that sort of problem? By default if your application runs in +production mode, and an exception is raised Flask will display a very simple +page for you and log the exception to the :attr:`~flask.Flask.logger`. + +But there is more you can do, and we will cover some better setups to deal +with errors including custom exceptions and 3rd party tools. + + +.. _error-logging-tools: + +Error Logging Tools +------------------- + +Sending error mails, even if just for critical ones, can become +overwhelming if enough users are hitting the error and log files are +typically never looked at. This is why we recommend using `Sentry +`_ for dealing with application errors. It's +available as a source-available project `on GitHub +`_ and is also available as a `hosted version +`_ which you can try for free. Sentry +aggregates duplicate errors, captures the full stack trace and local +variables for debugging, and sends you mails based on new errors or +frequency thresholds. + +To use Sentry you need to install the ``sentry-sdk`` client with extra +``flask`` dependencies. + +.. code-block:: text + + $ pip install sentry-sdk[flask] + +And then add this to your Flask app: + +.. code-block:: python + + import sentry_sdk + from sentry_sdk.integrations.flask import FlaskIntegration + + sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()]) + +The ``YOUR_DSN_HERE`` value needs to be replaced with the DSN value you +get from your Sentry installation. + +After installation, failures leading to an Internal Server Error +are automatically reported to Sentry and from there you can +receive error notifications. + +See also: + +- Sentry also supports catching errors from a worker queue + (RQ, Celery, etc.) in a similar fashion. See the `Python SDK docs + `__ for more information. +- `Flask-specific documentation `__ + + +Error Handlers +-------------- + +When an error occurs in Flask, an appropriate `HTTP status code +`__ will be +returned. 400-499 indicate errors with the client's request data, or +about the data requested. 500-599 indicate errors with the server or +application itself. + +You might want to show custom error pages to the user when an error occurs. +This can be done by registering error handlers. + +An error handler is a function that returns a response when a type of error is +raised, similar to how a view is a function that returns a response when a +request URL is matched. It is passed the instance of the error being handled, +which is most likely a :exc:`~werkzeug.exceptions.HTTPException`. + +The status code of the response will not be set to the handler's code. Make +sure to provide the appropriate HTTP status code when returning a response from +a handler. + + +Registering +``````````` + +Register handlers by decorating a function with +:meth:`~flask.Flask.errorhandler`. Or use +:meth:`~flask.Flask.register_error_handler` to register the function later. +Remember to set the error code when returning the response. + +.. code-block:: python + + @app.errorhandler(werkzeug.exceptions.BadRequest) + def handle_bad_request(e): + return 'bad request!', 400 + + # or, without the decorator + app.register_error_handler(400, handle_bad_request) + +:exc:`werkzeug.exceptions.HTTPException` subclasses like +:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable +when registering handlers. (``BadRequest.code == 400``) + +Non-standard HTTP codes cannot be registered by code because they are not known +by Werkzeug. Instead, define a subclass of +:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and +register and raise that exception class. + +.. code-block:: python + + class InsufficientStorage(werkzeug.exceptions.HTTPException): + code = 507 + description = 'Not enough storage space.' + + app.register_error_handler(InsufficientStorage, handle_507) + + raise InsufficientStorage() + +Handlers can be registered for any exception class, not just +:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status +codes. Handlers can be registered for a specific class, or for all subclasses +of a parent class. + + +Handling +```````` + +When building a Flask application you *will* run into exceptions. If some part +of your code breaks while handling a request (and you have no error handlers +registered), a "500 Internal Server Error" +(:exc:`~werkzeug.exceptions.InternalServerError`) will be returned by default. +Similarly, "404 Not Found" +(:exc:`~werkzeug.exceptions.NotFound`) error will occur if a request is sent to an unregistered route. +If a route receives an unallowed request method, a "405 Method Not Allowed" +(:exc:`~werkzeug.exceptions.MethodNotAllowed`) will be raised. These are all +subclasses of :class:`~werkzeug.exceptions.HTTPException` and are provided by +default in Flask. + +Flask gives you the ability to raise any HTTP exception registered by +Werkzeug. However, the default HTTP exceptions return simple exception +pages. You might want to show custom error pages to the user when an error occurs. +This can be done by registering error handlers. + +When Flask catches an exception while handling a request, it is first looked up by code. +If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen. +If no handler is registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a +generic message about their code, while other exceptions are converted to a +generic "500 Internal Server Error". + +For example, if an instance of :exc:`ConnectionRefusedError` is raised, +and a handler is registered for :exc:`ConnectionError` and +:exc:`ConnectionRefusedError`, the more specific :exc:`ConnectionRefusedError` +handler is called with the exception instance to generate the response. + +Handlers registered on the blueprint take precedence over those registered +globally on the application, assuming a blueprint is handling the request that +raises the exception. However, the blueprint cannot handle 404 routing errors +because the 404 occurs at the routing level before the blueprint can be +determined. + + +Generic Exception Handlers +`````````````````````````` + +It is possible to register error handlers for very generic base classes +such as ``HTTPException`` or even ``Exception``. However, be aware that +these will catch more than you might expect. + +For example, an error handler for ``HTTPException`` might be useful for turning +the default HTML errors pages into JSON. However, this +handler will trigger for things you don't cause directly, such as 404 +and 405 errors during routing. Be sure to craft your handler carefully +so you don't lose information about the HTTP error. + +.. code-block:: python + + from flask import json + from werkzeug.exceptions import HTTPException + + @app.errorhandler(HTTPException) + def handle_exception(e): + """Return JSON instead of HTML for HTTP errors.""" + # start with the correct headers and status code from the error + response = e.get_response() + # replace the body with JSON + response.data = json.dumps({ + "code": e.code, + "name": e.name, + "description": e.description, + }) + response.content_type = "application/json" + return response + +An error handler for ``Exception`` might seem useful for changing how +all errors, even unhandled ones, are presented to the user. However, +this is similar to doing ``except Exception:`` in Python, it will +capture *all* otherwise unhandled errors, including all HTTP status +codes. + +In most cases it will be safer to register handlers for more +specific exceptions. Since ``HTTPException`` instances are valid WSGI +responses, you could also pass them through directly. + +.. code-block:: python + + from werkzeug.exceptions import HTTPException + + @app.errorhandler(Exception) + def handle_exception(e): + # pass through HTTP errors + if isinstance(e, HTTPException): + return e + + # now you're handling non-HTTP exceptions only + return render_template("500_generic.html", e=e), 500 + +Error handlers still respect the exception class hierarchy. If you +register handlers for both ``HTTPException`` and ``Exception``, the +``Exception`` handler will not handle ``HTTPException`` subclasses +because the ``HTTPException`` handler is more specific. + + +Unhandled Exceptions +```````````````````` + +When there is no error handler registered for an exception, a 500 +Internal Server Error will be returned instead. See +:meth:`flask.Flask.handle_exception` for information about this +behavior. + +If there is an error handler registered for ``InternalServerError``, +this will be invoked. As of Flask 1.1.0, this error handler will always +be passed an instance of ``InternalServerError``, not the original +unhandled error. + +The original error is available as ``e.original_exception``. + +An error handler for "500 Internal Server Error" will be passed uncaught +exceptions in addition to explicit 500 errors. In debug mode, a handler +for "500 Internal Server Error" will not be used. Instead, the +interactive debugger will be shown. + + +Custom Error Pages +------------------ + +Sometimes when building a Flask application, you might want to raise a +:exc:`~werkzeug.exceptions.HTTPException` to signal to the user that +something is wrong with the request. Fortunately, Flask comes with a handy +:func:`~flask.abort` function that aborts a request with a HTTP error from +werkzeug as desired. It will also provide a plain black and white error page +for you with a basic description, but nothing fancy. + +Depending on the error code it is less or more likely for the user to +actually see such an error. + +Consider the code below, we might have a user profile route, and if the user +fails to pass a username we can raise a "400 Bad Request". If the user passes a +username and we can't find it, we raise a "404 Not Found". + +.. code-block:: python + + from flask import abort, render_template, request + + # a username needs to be supplied in the query args + # a successful request would be like /profile?username=jack + @app.route("/profile") + def user_profile(): + username = request.arg.get("username") + # if a username isn't supplied in the request, return a 400 bad request + if username is None: + abort(400) + + user = get_user(username=username) + # if a user can't be found by their username, return 404 not found + if user is None: + abort(404) + + return render_template("profile.html", user=user) + +Here is another example implementation for a "404 Page Not Found" exception: + +.. code-block:: python + + from flask import render_template + + @app.errorhandler(404) + def page_not_found(e): + # note that we set the 404 status explicitly + return render_template('404.html'), 404 + +When using :doc:`/patterns/appfactories`: + +.. code-block:: python + + from flask import Flask, render_template + + def page_not_found(e): + return render_template('404.html'), 404 + + def create_app(config_filename): + app = Flask(__name__) + app.register_error_handler(404, page_not_found) + return app + +An example template might be this: + +.. code-block:: html+jinja + + {% extends "layout.html" %} + {% block title %}Page Not Found{% endblock %} + {% block body %} +

Page Not Found

+

What you were looking for is just not there. +

go somewhere nice + {% endblock %} + + +Further Examples +```````````````` + +The above examples wouldn't actually be an improvement on the default +exception pages. We can create a custom 500.html template like this: + +.. code-block:: html+jinja + + {% extends "layout.html" %} + {% block title %}Internal Server Error{% endblock %} + {% block body %} +

Internal Server Error

+

Oops... we seem to have made a mistake, sorry!

+

Go somewhere nice instead + {% endblock %} + +It can be implemented by rendering the template on "500 Internal Server Error": + +.. code-block:: python + + from flask import render_template + + @app.errorhandler(500) + def internal_server_error(e): + # note that we set the 500 status explicitly + return render_template('500.html'), 500 + +When using :doc:`/patterns/appfactories`: + +.. code-block:: python + + from flask import Flask, render_template + + def internal_server_error(e): + return render_template('500.html'), 500 + + def create_app(): + app = Flask(__name__) + app.register_error_handler(500, internal_server_error) + return app + +When using :doc:`/blueprints`: + +.. code-block:: python + + from flask import Blueprint + + blog = Blueprint('blog', __name__) + + # as a decorator + @blog.errorhandler(500) + def internal_server_error(e): + return render_template('500.html'), 500 + + # or with register_error_handler + blog.register_error_handler(500, internal_server_error) + + +Blueprint Error Handlers +------------------------ + +In :doc:`/blueprints`, most error handlers will work as expected. +However, there is a caveat concerning handlers for 404 and 405 +exceptions. These error handlers are only invoked from an appropriate +``raise`` statement or a call to ``abort`` in another of the blueprint's +view functions; they are not invoked by, e.g., an invalid URL access. + +This is because the blueprint does not "own" a certain URL space, so +the application instance has no way of knowing which blueprint error +handler it should run if given an invalid URL. If you would like to +execute different handling strategies for these errors based on URL +prefixes, they may be defined at the application level using the +``request`` proxy object. + +.. code-block:: python + + from flask import jsonify, render_template + + # at the application level + # not the blueprint level + @app.errorhandler(404) + def page_not_found(e): + # if a request is in our blog URL space + if request.path.startswith('/blog/'): + # we return a custom blog 404 page + return render_template("blog/404.html"), 404 + else: + # otherwise we return our generic site-wide 404 page + return render_template("404.html"), 404 + + @app.errorhandler(405) + def method_not_allowed(e): + # if a request has the wrong method to our API + if request.path.startswith('/api/'): + # we return a json saying so + return jsonify(message="Method Not Allowed"), 405 + else: + # otherwise we return a generic site-wide 405 page + return render_template("405.html"), 405 + + +Returning API Errors as JSON +---------------------------- + +When building APIs in Flask, some developers realise that the built-in +exceptions are not expressive enough for APIs and that the content type of +:mimetype:`text/html` they are emitting is not very useful for API consumers. + +Using the same techniques as above and :func:`~flask.json.jsonify` we can return JSON +responses to API errors. :func:`~flask.abort` is called +with a ``description`` parameter. The error handler will +use that as the JSON error message, and set the status code to 404. + +.. code-block:: python + + from flask import abort, jsonify + + @app.errorhandler(404) + def resource_not_found(e): + return jsonify(error=str(e)), 404 + + @app.route("/cheese") + def get_one_cheese(): + resource = get_resource() + + if resource is None: + abort(404, description="Resource not found") + + return jsonify(resource) + +We can also create custom exception classes. For instance, we can +introduce a new custom exception for an API that can take a proper human readable message, +a status code for the error and some optional payload to give more context +for the error. + +This is a simple example: + +.. code-block:: python + + from flask import jsonify, request + + class InvalidAPIUsage(Exception): + status_code = 400 + + def __init__(self, message, status_code=None, payload=None): + super().__init__() + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = dict(self.payload or ()) + rv['message'] = self.message + return rv + + @app.errorhandler(InvalidAPIUsage) + def invalid_api_usage(e): + return jsonify(e.to_dict()), e.status_code + + # an API app route for getting user information + # a correct request might be /api/user?user_id=420 + @app.route("/api/user") + def user_api(user_id): + user_id = request.arg.get("user_id") + if not user_id: + raise InvalidAPIUsage("No user id provided!") + + user = get_user(user_id=user_id) + if not user: + raise InvalidAPIUsage("No such user!", status_code=404) + + return jsonify(user.to_dict()) + +A view can now raise that exception with an error message. Additionally +some extra payload can be provided as a dictionary through the `payload` +parameter. + + +Logging +------- + +See :doc:`/logging` for information about how to log exceptions, such as +by emailing them to admins. + + +Debugging +--------- + +See :doc:`/debugging` for information about how to debug errors in +development and production. diff --git a/test/fixtures/whole_applications/flask/docs/extensiondev.rst b/test/fixtures/whole_applications/flask/docs/extensiondev.rst new file mode 100644 index 0000000..c9dee5f --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/extensiondev.rst @@ -0,0 +1,303 @@ +Flask Extension Development +=========================== + +.. currentmodule:: flask + +Extensions are extra packages that add functionality to a Flask +application. While `PyPI`_ contains many Flask extensions, you may not +find one that fits your need. If this is the case, you can create your +own, and publish it for others to use as well. + +This guide will show how to create a Flask extension, and some of the +common patterns and requirements involved. Since extensions can do +anything, this guide won't be able to cover every possibility. + +The best ways to learn about extensions are to look at how other +extensions you use are written, and discuss with others. Discuss your +design ideas with others on our `Discord Chat`_ or +`GitHub Discussions`_. + +The best extensions share common patterns, so that anyone familiar with +using one extension won't feel completely lost with another. This can +only work if collaboration happens early. + + +Naming +------ + +A Flask extension typically has ``flask`` in its name as a prefix or +suffix. If it wraps another library, it should include the library name +as well. This makes it easy to search for extensions, and makes their +purpose clearer. + +A general Python packaging recommendation is that the install name from +the package index and the name used in ``import`` statements should be +related. The import name is lowercase, with words separated by +underscores (``_``). The install name is either lower case or title +case, with words separated by dashes (``-``). If it wraps another +library, prefer using the same case as that library's name. + +Here are some example install and import names: + +- ``Flask-Name`` imported as ``flask_name`` +- ``flask-name-lower`` imported as ``flask_name_lower`` +- ``Flask-ComboName`` imported as ``flask_comboname`` +- ``Name-Flask`` imported as ``name_flask`` + + +The Extension Class and Initialization +-------------------------------------- + +All extensions will need some entry point that initializes the +extension with the application. The most common pattern is to create a +class that represents the extension's configuration and behavior, with +an ``init_app`` method to apply the extension instance to the given +application instance. + +.. code-block:: python + + class HelloExtension: + def __init__(self, app=None): + if app is not None: + self.init_app(app) + + def init_app(self, app): + app.before_request(...) + +It is important that the app is not stored on the extension, don't do +``self.app = app``. The only time the extension should have direct +access to an app is during ``init_app``, otherwise it should use +:data:`current_app`. + +This allows the extension to support the application factory pattern, +avoids circular import issues when importing the extension instance +elsewhere in a user's code, and makes testing with different +configurations easier. + +.. code-block:: python + + hello = HelloExtension() + + def create_app(): + app = Flask(__name__) + hello.init_app(app) + return app + +Above, the ``hello`` extension instance exists independently of the +application. This means that other modules in a user's project can do +``from project import hello`` and use the extension in blueprints before +the app exists. + +The :attr:`Flask.extensions` dict can be used to store a reference to +the extension on the application, or some other state specific to the +application. Be aware that this is a single namespace, so use a name +unique to your extension, such as the extension's name without the +"flask" prefix. + + +Adding Behavior +--------------- + +There are many ways that an extension can add behavior. Any setup +methods that are available on the :class:`Flask` object can be used +during an extension's ``init_app`` method. + +A common pattern is to use :meth:`~Flask.before_request` to initialize +some data or a connection at the beginning of each request, then +:meth:`~Flask.teardown_request` to clean it up at the end. This can be +stored on :data:`g`, discussed more below. + +A more lazy approach is to provide a method that initializes and caches +the data or connection. For example, a ``ext.get_db`` method could +create a database connection the first time it's called, so that a view +that doesn't use the database doesn't create a connection. + +Besides doing something before and after every view, your extension +might want to add some specific views as well. In this case, you could +define a :class:`Blueprint`, then call :meth:`~Flask.register_blueprint` +during ``init_app`` to add the blueprint to the app. + + +Configuration Techniques +------------------------ + +There can be multiple levels and sources of configuration for an +extension. You should consider what parts of your extension fall into +each one. + +- Configuration per application instance, through ``app.config`` + values. This is configuration that could reasonably change for each + deployment of an application. A common example is a URL to an + external resource, such as a database. Configuration keys should + start with the extension's name so that they don't interfere with + other extensions. +- Configuration per extension instance, through ``__init__`` + arguments. This configuration usually affects how the extension + is used, such that it wouldn't make sense to change it per + deployment. +- Configuration per extension instance, through instance attributes + and decorator methods. It might be more ergonomic to assign to + ``ext.value``, or use a ``@ext.register`` decorator to register a + function, after the extension instance has been created. +- Global configuration through class attributes. Changing a class + attribute like ``Ext.connection_class`` can customize default + behavior without making a subclass. This could be combined + per-extension configuration to override defaults. +- Subclassing and overriding methods and attributes. Making the API of + the extension itself something that can be overridden provides a + very powerful tool for advanced customization. + +The :class:`~flask.Flask` object itself uses all of these techniques. + +It's up to you to decide what configuration is appropriate for your +extension, based on what you need and what you want to support. + +Configuration should not be changed after the application setup phase is +complete and the server begins handling requests. Configuration is +global, any changes to it are not guaranteed to be visible to other +workers. + + +Data During a Request +--------------------- + +When writing a Flask application, the :data:`~flask.g` object is used to +store information during a request. For example the +:doc:`tutorial ` stores a connection to a SQLite +database as ``g.db``. Extensions can also use this, with some care. +Since ``g`` is a single global namespace, extensions must use unique +names that won't collide with user data. For example, use the extension +name as a prefix, or as a namespace. + +.. code-block:: python + + # an internal prefix with the extension name + g._hello_user_id = 2 + + # or an internal prefix as a namespace + from types import SimpleNamespace + g._hello = SimpleNamespace() + g._hello.user_id = 2 + +The data in ``g`` lasts for an application context. An application +context is active when a request context is, or when a CLI command is +run. If you're storing something that should be closed, use +:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed +when the application context ends. If it should only be valid during a +request, or would not be used in the CLI outside a request, use +:meth:`~flask.Flask.teardown_request`. + + +Views and Models +---------------- + +Your extension views might want to interact with specific models in your +database, or some other extension or data connected to your application. +For example, let's consider a ``Flask-SimpleBlog`` extension that works +with Flask-SQLAlchemy to provide a ``Post`` model and views to write +and read posts. + +The ``Post`` model needs to subclass the Flask-SQLAlchemy ``db.Model`` +object, but that's only available once you've created an instance of +that extension, not when your extension is defining its views. So how +can the view code, defined before the model exists, access the model? + +One method could be to use :doc:`views`. During ``__init__``, create +the model, then create the views by passing the model to the view +class's :meth:`~views.View.as_view` method. + +.. code-block:: python + + class PostAPI(MethodView): + def __init__(self, model): + self.model = model + + def get(self, id): + post = self.model.query.get(id) + return jsonify(post.to_json()) + + class BlogExtension: + def __init__(self, db): + class Post(db.Model): + id = db.Column(primary_key=True) + title = db.Column(db.String, nullable=False) + + self.post_model = Post + + def init_app(self, app): + api_view = PostAPI.as_view(model=self.post_model) + + db = SQLAlchemy() + blog = BlogExtension(db) + db.init_app(app) + blog.init_app(app) + +Another technique could be to use an attribute on the extension, such as +``self.post_model`` from above. Add the extension to ``app.extensions`` +in ``init_app``, then access +``current_app.extensions["simple_blog"].post_model`` from views. + +You may also want to provide base classes so that users can provide +their own ``Post`` model that conforms to the API your extension +expects. So they could implement ``class Post(blog.BasePost)``, then +set it as ``blog.post_model``. + +As you can see, this can get a bit complex. Unfortunately, there's no +perfect solution here, only different strategies and tradeoffs depending +on your needs and how much customization you want to offer. Luckily, +this sort of resource dependency is not a common need for most +extensions. Remember, if you need help with design, ask on our +`Discord Chat`_ or `GitHub Discussions`_. + + +Recommended Extension Guidelines +-------------------------------- + +Flask previously had the concept of "approved extensions", where the +Flask maintainers evaluated the quality, support, and compatibility of +the extensions before listing them. While the list became too difficult +to maintain over time, the guidelines are still relevant to all +extensions maintained and developed today, as they help the Flask +ecosystem remain consistent and compatible. + +1. An extension requires a maintainer. In the event an extension author + would like to move beyond the project, the project should find a new + maintainer and transfer access to the repository, documentation, + PyPI, and any other services. The `Pallets-Eco`_ organization on + GitHub allows for community maintenance with oversight from the + Pallets maintainers. +2. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. + It must provide exactly one package or module named + ``flask_extension_name``. +3. The extension must use an open source license. The Python web + ecosystem tends to prefer BSD or MIT. It must be open source and + publicly available. +4. The extension's API must have the following characteristics: + + - It must support multiple applications running in the same Python + process. Use ``current_app`` instead of ``self.app``, store + configuration and state per application instance. + - It must be possible to use the factory pattern for creating + applications. Use the ``ext.init_app()`` pattern. + +5. From a clone of the repository, an extension with its dependencies + must be installable in editable mode with ``pip install -e .``. +6. It must ship tests that can be invoked with a common tool like + ``tox -e py``, ``nox -s test`` or ``pytest``. If not using ``tox``, + the test dependencies should be specified in a requirements file. + The tests must be part of the sdist distribution. +7. A link to the documentation or project website must be in the PyPI + metadata or the readme. The documentation should use the Flask theme + from the `Official Pallets Themes`_. +8. The extension's dependencies should not use upper bounds or assume + any particular version scheme, but should use lower bounds to + indicate minimum compatibility support. For example, + ``sqlalchemy>=1.4``. +9. Indicate the versions of Python supported using ``python_requires=">=version"``. + Flask itself supports Python >=3.8 as of April 2023, but this will update over time. + +.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask +.. _Discord Chat: https://discord.gg/pallets +.. _GitHub Discussions: https://github.com/pallets/flask/discussions +.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ +.. _Pallets-Eco: https://github.com/pallets-eco diff --git a/test/fixtures/whole_applications/flask/docs/extensions.rst b/test/fixtures/whole_applications/flask/docs/extensions.rst new file mode 100644 index 0000000..4713ec8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/extensions.rst @@ -0,0 +1,48 @@ +Extensions +========== + +Extensions are extra packages that add functionality to a Flask +application. For example, an extension might add support for sending +email or connecting to a database. Some extensions add entire new +frameworks to help build certain types of applications, like a REST API. + + +Finding Extensions +------------------ + +Flask extensions are usually named "Flask-Foo" or "Foo-Flask". You can +search PyPI for packages tagged with `Framework :: Flask `_. + + +Using Extensions +---------------- + +Consult each extension's documentation for installation, configuration, +and usage instructions. Generally, extensions pull their own +configuration from :attr:`app.config ` and are +passed an application instance during initialization. For example, +an extension called "Flask-Foo" might be used like this:: + + from flask_foo import Foo + + foo = Foo() + + app = Flask(__name__) + app.config.update( + FOO_BAR='baz', + FOO_SPAM='eggs', + ) + + foo.init_app(app) + + +Building Extensions +------------------- + +While `PyPI `_ contains many Flask extensions, you may not find +an extension that fits your need. If this is the case, you can create +your own, and publish it for others to use as well. Read +:doc:`extensiondev` to develop your own Flask extension. + + +.. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask diff --git a/test/fixtures/whole_applications/flask/docs/index.rst b/test/fixtures/whole_applications/flask/docs/index.rst new file mode 100644 index 0000000..fc9f914 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/index.rst @@ -0,0 +1,84 @@ +.. rst-class:: hide-header + +Welcome to Flask +================ + +.. image:: _static/flask-horizontal.png + :align: center + +Welcome to Flask's documentation. Get started with :doc:`installation` +and then get an overview with the :doc:`quickstart`. There is also a +more detailed :doc:`tutorial/index` that shows how to create a small but +complete application with Flask. Common patterns are described in the +:doc:`patterns/index` section. The rest of the docs describe each +component of Flask in detail, with a full reference in the :doc:`api` +section. + +Flask depends on the `Werkzeug`_ WSGI toolkit, the `Jinja`_ template engine, and the +`Click`_ CLI toolkit. Be sure to check their documentation as well as Flask's when +looking for information. + +.. _Werkzeug: https://werkzeug.palletsprojects.com +.. _Jinja: https://jinja.palletsprojects.com +.. _Click: https://click.palletsprojects.com + + +User's Guide +------------ + +Flask provides configuration and conventions, with sensible defaults, to get started. +This section of the documentation explains the different parts of the Flask framework +and how they can be used, customized, and extended. Beyond Flask itself, look for +community-maintained extensions to add even more functionality. + +.. toctree:: + :maxdepth: 2 + + installation + quickstart + tutorial/index + templating + testing + errorhandling + debugging + logging + config + signals + views + lifecycle + appcontext + reqcontext + blueprints + extensions + cli + server + shell + patterns/index + web-security + deploying/index + async-await + + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + + +Additional Notes +---------------- + +.. toctree:: + :maxdepth: 2 + + design + extensiondev + contributing + license + changes diff --git a/test/fixtures/whole_applications/flask/docs/installation.rst b/test/fixtures/whole_applications/flask/docs/installation.rst new file mode 100644 index 0000000..aeb00ce --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/installation.rst @@ -0,0 +1,144 @@ +Installation +============ + + +Python Version +-------------- + +We recommend using the latest version of Python. Flask supports Python 3.8 and newer. + + +Dependencies +------------ + +These distributions will be installed automatically when installing Flask. + +* `Werkzeug`_ implements WSGI, the standard Python interface between + applications and servers. +* `Jinja`_ is a template language that renders the pages your application + serves. +* `MarkupSafe`_ comes with Jinja. It escapes untrusted input when rendering + templates to avoid injection attacks. +* `ItsDangerous`_ securely signs data to ensure its integrity. This is used + to protect Flask's session cookie. +* `Click`_ is a framework for writing command line applications. It provides + the ``flask`` command and allows adding custom management commands. +* `Blinker`_ provides support for :doc:`signals`. + +.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ +.. _Jinja: https://palletsprojects.com/p/jinja/ +.. _MarkupSafe: https://palletsprojects.com/p/markupsafe/ +.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ +.. _Click: https://palletsprojects.com/p/click/ +.. _Blinker: https://blinker.readthedocs.io/ + + +Optional dependencies +~~~~~~~~~~~~~~~~~~~~~ + +These distributions will not be installed automatically. Flask will detect and +use them if you install them. + +* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` + commands. +* `Watchdog`_ provides a faster, more efficient reloader for the development + server. + +.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme +.. _watchdog: https://pythonhosted.org/watchdog/ + + +greenlet +~~~~~~~~ + +You may choose to use gevent or eventlet with your application. In this +case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is +required. + +These are not minimum supported versions, they only indicate the first +versions that added necessary features. You should use the latest +versions of each. + + +Virtual environments +-------------------- + +Use a virtual environment to manage the dependencies for your project, both in +development and in production. + +What problem does a virtual environment solve? The more Python projects you +have, the more likely it is that you need to work with different versions of +Python libraries, or even Python itself. Newer versions of libraries for one +project can break compatibility in another project. + +Virtual environments are independent groups of Python libraries, one for each +project. Packages installed for one project will not affect other projects or +the operating system's packages. + +Python comes bundled with the :mod:`venv` module to create virtual +environments. + + +.. _install-create-env: + +Create an environment +~~~~~~~~~~~~~~~~~~~~~ + +Create a project folder and a :file:`.venv` folder within: + +.. tabs:: + + .. group-tab:: macOS/Linux + + .. code-block:: text + + $ mkdir myproject + $ cd myproject + $ python3 -m venv .venv + + .. group-tab:: Windows + + .. code-block:: text + + > mkdir myproject + > cd myproject + > py -3 -m venv .venv + + +.. _install-activate-env: + +Activate the environment +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before you work on your project, activate the corresponding environment: + +.. tabs:: + + .. group-tab:: macOS/Linux + + .. code-block:: text + + $ . .venv/bin/activate + + .. group-tab:: Windows + + .. code-block:: text + + > .venv\Scripts\activate + +Your shell prompt will change to show the name of the activated +environment. + + +Install Flask +------------- + +Within the activated environment, use the following command to install +Flask: + +.. code-block:: sh + + $ pip install Flask + +Flask is now installed. Check out the :doc:`/quickstart` or go to the +:doc:`Documentation Overview `. diff --git a/test/fixtures/whole_applications/flask/docs/license.rst b/test/fixtures/whole_applications/flask/docs/license.rst new file mode 100644 index 0000000..2a445f9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/license.rst @@ -0,0 +1,5 @@ +BSD-3-Clause License +==================== + +.. literalinclude:: ../LICENSE.txt + :language: text diff --git a/test/fixtures/whole_applications/flask/docs/lifecycle.rst b/test/fixtures/whole_applications/flask/docs/lifecycle.rst new file mode 100644 index 0000000..2344d98 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/lifecycle.rst @@ -0,0 +1,168 @@ +Application Structure and Lifecycle +=================================== + +Flask makes it pretty easy to write a web application. But there are quite a few +different parts to an application and to each request it handles. Knowing what happens +during application setup, serving, and handling requests will help you know what's +possible in Flask and how to structure your application. + + +Application Setup +----------------- + +The first step in creating a Flask application is creating the application object. Each +Flask application is an instance of the :class:`.Flask` class, which collects all +configuration, extensions, and views. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + SECRET_KEY="dev", + ) + app.config.from_prefixed_env() + + @app.route("/") + def index(): + return "Hello, World!" + +This is known as the "application setup phase", it's the code you write that's outside +any view functions or other handlers. It can be split up between different modules and +sub-packages, but all code that you want to be part of your application must be imported +in order for it to be registered. + +All application setup must be completed before you start serving your application and +handling requests. This is because WSGI servers divide work between multiple workers, or +can be distributed across multiple machines. If the configuration changed in one worker, +there's no way for Flask to ensure consistency between other workers. + +Flask tries to help developers catch some of these setup ordering issues by showing an +error if setup-related methods are called after requests are handled. In that case +you'll see this error: + + The setup method 'route' can no longer be called on the application. It has already + handled its first request, any changes will not be applied consistently. + Make sure all imports, decorators, functions, etc. needed to set up the application + are done before running it. + +However, it is not possible for Flask to detect all cases of out-of-order setup. In +general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects +from within view functions that run during requests. This includes: + +- Adding routes, view functions, and other request handlers with ``@app.route``, + ``@app.errorhandler``, ``@app.before_request``, etc. +- Registering blueprints. +- Loading configuration with ``app.config``. +- Setting up the Jinja template environment with ``app.jinja_env``. +- Setting a session interface, instead of the default itsdangerous cookie. +- Setting a JSON provider with ``app.json``, instead of the default provider. +- Creating and initializing Flask extensions. + + +Serving the Application +----------------------- + +Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During +development, Flask, through Werkzeug, provides a development WSGI server with the +``flask run`` CLI command. When you are done with development, use a production server +to serve your application, see :doc:`deploying/index`. + +Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The +WSGI server will be told how to access your Flask application object, which is the WSGI +application. Then it will start listening for HTTP requests, translate the request data +into a WSGI environ, and call the WSGI application with that data. The WSGI application +will return data that is translated into an HTTP response. + +#. Browser or other client makes HTTP request. +#. WSGI server receives request. +#. WSGI server converts HTTP data to WSGI ``environ`` dict. +#. WSGI server calls WSGI application with the ``environ``. +#. Flask, the WSGI application, does all its internal processing to route the request + to a view function, handle errors, etc. +#. Flask translates View function return into WSGI response data, passes it to WSGI + server. +#. WSGI server creates and send an HTTP response. +#. Client receives the HTTP response. + + +Middleware +~~~~~~~~~~ + +The WSGI application above is a callable that behaves in a certain way. Middleware +is a WSGI application that wraps another WSGI application. It's a similar concept to +Python decorators. The outermost middleware will be called by the server. It can modify +the data passed to it, then call the WSGI application (or further middleware) that it +wraps, and so on. And it can take the return value of that call and modify it further. + +From the WSGI server's perspective, there is one WSGI application, the one it calls +directly. Typically, Flask is the "real" application at the end of the chain of +middleware. But even Flask can call further WSGI applications, although that's an +advanced, uncommon use case. + +A common middleware you'll see used with Flask is Werkzeug's +:class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look +like it came directly from a client even if it passed through HTTP proxies on the way. +There are other middleware that can handle serving static files, authentication, etc. + + +How a Request is Handled +------------------------ + +For us, the interesting part of the steps above is when Flask gets called by the WSGI +server (or middleware). At that point, it will do quite a lot to handle the request and +generate the response. At the most basic, it will match the URL to a view function, call +the view function, and pass the return value back to the server. But there are many more +parts that you can use to customize its behavior. + +#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. +#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` + dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. +#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and + :data:`.g` available. +#. The :data:`.appcontext_pushed` signal is sent. +#. The :doc:`request context ` is pushed, which makes :attr:`.request` and + :class:`.session` available. +#. The session is opened, loading any existing session data using the app's + :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. +#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` + decorator during application setup. If there is no match, the error - usually a 404, + 405, or redirect - is stored to be handled later. +#. The :data:`.request_started` signal is sent. +#. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. +#. Any :meth:`~.Flask.before_request` decorated functions are called. If any of + these function returns a value it is treated as the response immediately. +#. If the URL didn't match a route a few steps ago, that error is raised now. +#. The :meth:`~.Flask.route` decorated view function associated with the matched URL + is called and returns a value to be used as the response. +#. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` + decorated function that matches the exception class or HTTP error code, it is + called to handle the error and return a response. +#. Whatever returned a response value - a before request function, the view, or an + error handler, that value is converted to a :class:`.Response` object. +#. Any :func:`~.after_this_request` decorated functions are called, then cleared. +#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify + the response object. +#. The session is saved, persisting any modified session data using the app's + :attr:`~.Flask.session_interface`. +#. The :data:`.request_finished` signal is sent. +#. If any step so far raised an exception, and it was not handled by an error handler + function, it is handled now. HTTP exceptions are treated as responses with their + corresponding status code, other exceptions are converted to a generic 500 response. + The :data:`.got_request_exception` signal is sent. +#. The response object's status, headers, and body are returned to the WSGI server. +#. Any :meth:`~.Flask.teardown_request` decorated functions are called. +#. The :data:`.request_tearing_down` signal is sent. +#. The request context is popped, :attr:`.request` and :class:`.session` are no longer + available. +#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. +#. The :data:`.appcontext_tearing_down` signal is sent. +#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer + available. +#. The :data:`.appcontext_popped` signal is sent. + +There are even more decorators and customization points than this, but that aren't part +of every request lifecycle. They're more specific to certain things you might use during +a request, such as templates, building URLs, or handling JSON data. See the rest of this +documentation, as well as the :doc:`api` to explore further. diff --git a/test/fixtures/whole_applications/flask/docs/logging.rst b/test/fixtures/whole_applications/flask/docs/logging.rst new file mode 100644 index 0000000..3958824 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/logging.rst @@ -0,0 +1,183 @@ +Logging +======= + +Flask uses standard Python :mod:`logging`. Messages about your Flask +application are logged with :meth:`app.logger `, +which takes the same name as :attr:`app.name `. This +logger can also be used to log your own messages. + +.. code-block:: python + + @app.route('/login', methods=['POST']) + def login(): + user = get_user(request.form['username']) + + if user.check_password(request.form['password']): + login_user(user) + app.logger.info('%s logged in successfully', user.username) + return redirect(url_for('index')) + else: + app.logger.info('%s failed to log in', user.username) + abort(401) + +If you don't configure logging, Python's default log level is usually +'warning'. Nothing below the configured level will be visible. + + +Basic Configuration +------------------- + +When you want to configure logging for your project, you should do it as soon +as possible when the program starts. If :meth:`app.logger ` +is accessed before logging is configured, it will add a default handler. If +possible, configure logging before creating the application object. + +This example uses :func:`~logging.config.dictConfig` to create a logging +configuration similar to Flask's default, except for all logs:: + + from logging.config import dictConfig + + dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } + }) + + app = Flask(__name__) + + +Default Configuration +````````````````````` + +If you do not configure logging yourself, Flask will add a +:class:`~logging.StreamHandler` to :meth:`app.logger ` +automatically. During requests, it will write to the stream specified by the +WSGI server in ``environ['wsgi.errors']`` (which is usually +:data:`sys.stderr`). Outside a request, it will log to :data:`sys.stderr`. + + +Removing the Default Handler +```````````````````````````` + +If you configured logging after accessing +:meth:`app.logger `, and need to remove the default +handler, you can import and remove it:: + + from flask.logging import default_handler + + app.logger.removeHandler(default_handler) + + +Email Errors to Admins +---------------------- + +When running the application on a remote server for production, you probably +won't be looking at the log messages very often. The WSGI server will probably +send log messages to a file, and you'll only check that file if a user tells +you something went wrong. + +To be proactive about discovering and fixing bugs, you can configure a +:class:`logging.handlers.SMTPHandler` to send an email when errors and higher +are logged. :: + + import logging + from logging.handlers import SMTPHandler + + mail_handler = SMTPHandler( + mailhost='127.0.0.1', + fromaddr='server-error@example.com', + toaddrs=['admin@example.com'], + subject='Application Error' + ) + mail_handler.setLevel(logging.ERROR) + mail_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' + )) + + if not app.debug: + app.logger.addHandler(mail_handler) + +This requires that you have an SMTP server set up on the same server. See the +Python docs for more information about configuring the handler. + + +Injecting Request Information +----------------------------- + +Seeing more information about the request, such as the IP address, may help +debugging some errors. You can subclass :class:`logging.Formatter` to inject +your own fields that can be used in messages. You can change the formatter for +Flask's default handler, the mail handler defined above, or any other +handler. :: + + from flask import has_request_context, request + from flask.logging import default_handler + + class RequestFormatter(logging.Formatter): + def format(self, record): + if has_request_context(): + record.url = request.url + record.remote_addr = request.remote_addr + else: + record.url = None + record.remote_addr = None + + return super().format(record) + + formatter = RequestFormatter( + '[%(asctime)s] %(remote_addr)s requested %(url)s\n' + '%(levelname)s in %(module)s: %(message)s' + ) + default_handler.setFormatter(formatter) + mail_handler.setFormatter(formatter) + + +Other Libraries +--------------- + +Other libraries may use logging extensively, and you want to see relevant +messages from those logs too. The simplest way to do this is to add handlers +to the root logger instead of only the app logger. :: + + from flask.logging import default_handler + + root = logging.getLogger() + root.addHandler(default_handler) + root.addHandler(mail_handler) + +Depending on your project, it may be more useful to configure each logger you +care about separately, instead of configuring only the root logger. :: + + for logger in ( + logging.getLogger(app.name), + logging.getLogger('sqlalchemy'), + logging.getLogger('other_package'), + ): + logger.addHandler(default_handler) + logger.addHandler(mail_handler) + + +Werkzeug +```````` + +Werkzeug logs basic request/response information to the ``'werkzeug'`` logger. +If the root logger has no handlers configured, Werkzeug adds a +:class:`~logging.StreamHandler` to its logger. + + +Flask Extensions +```````````````` + +Depending on the situation, an extension may choose to log to +:meth:`app.logger ` or its own named logger. Consult each +extension's documentation for details. diff --git a/test/fixtures/whole_applications/flask/docs/make.bat b/test/fixtures/whole_applications/flask/docs/make.bat new file mode 100644 index 0000000..922152e --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/test/fixtures/whole_applications/flask/docs/patterns/appdispatch.rst b/test/fixtures/whole_applications/flask/docs/patterns/appdispatch.rst new file mode 100644 index 0000000..f22c806 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/appdispatch.rst @@ -0,0 +1,189 @@ +Application Dispatching +======================= + +Application dispatching is the process of combining multiple Flask +applications on the WSGI level. You can combine not only Flask +applications but any WSGI application. This would allow you to run a +Django and a Flask application in the same interpreter side by side if +you want. The usefulness of this depends on how the applications work +internally. + +The fundamental difference from :doc:`packages` is that in this case you +are running the same or different Flask applications that are entirely +isolated from each other. They run different configurations and are +dispatched on the WSGI level. + + +Working with this Document +-------------------------- + +Each of the techniques and examples below results in an ``application`` +object that can be run with any WSGI server. For development, use the +``flask run`` command to start a development server. For production, see +:doc:`/deploying/index`. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + + @app.route('/') + def hello_world(): + return 'Hello World!' + + +Combining Applications +---------------------- + +If you have entirely separated applications and you want them to work next +to each other in the same Python interpreter process you can take +advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea +here is that each Flask application is a valid WSGI application and they +are combined by the dispatcher middleware into a larger one that is +dispatched based on prefix. + +For example you could have your main application run on ``/`` and your +backend interface on ``/backend``. + +.. code-block:: python + + from werkzeug.middleware.dispatcher import DispatcherMiddleware + from frontend_app import application as frontend + from backend_app import application as backend + + application = DispatcherMiddleware(frontend, { + '/backend': backend + }) + + +Dispatch by Subdomain +--------------------- + +Sometimes you might want to use multiple instances of the same application +with different configurations. Assuming the application is created inside +a function and you can call that function to instantiate it, that is +really easy to implement. In order to develop your application to support +creating new instances in functions have a look at the +:doc:`appfactories` pattern. + +A very common example would be creating applications per subdomain. For +instance you configure your webserver to dispatch all requests for all +subdomains to your application and you then use the subdomain information +to create user-specific instances. Once you have your server set up to +listen on all subdomains you can use a very simple WSGI application to do +the dynamic application creation. + +The perfect level for abstraction in that regard is the WSGI layer. You +write your own WSGI application that looks at the request that comes and +delegates it to your Flask application. If that application does not +exist yet, it is dynamically created and remembered. + +.. code-block:: python + + from threading import Lock + + class SubdomainDispatcher: + + def __init__(self, domain, create_app): + self.domain = domain + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, host): + host = host.split(':')[0] + assert host.endswith(self.domain), 'Configuration error' + subdomain = host[:-len(self.domain)].rstrip('.') + with self.lock: + app = self.instances.get(subdomain) + if app is None: + app = self.create_app(subdomain) + self.instances[subdomain] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(environ['HTTP_HOST']) + return app(environ, start_response) + + +This dispatcher can then be used like this: + +.. code-block:: python + + from myapplication import create_app, get_user_for_subdomain + from werkzeug.exceptions import NotFound + + def make_app(subdomain): + user = get_user_for_subdomain(subdomain) + if user is None: + # if there is no user for that subdomain we still have + # to return a WSGI application that handles that request. + # We can then just return the NotFound() exception as + # application which will render a default 404 page. + # You might also redirect the user to the main page then + return NotFound() + + # otherwise create the application for the specific user + return create_app(user) + + application = SubdomainDispatcher('example.com', make_app) + + +Dispatch by Path +---------------- + +Dispatching by a path on the URL is very similar. Instead of looking at +the ``Host`` header to figure out the subdomain one simply looks at the +request path up to the first slash. + +.. code-block:: python + + from threading import Lock + from wsgiref.util import shift_path_info + + class PathDispatcher: + + def __init__(self, default_app, create_app): + self.default_app = default_app + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, prefix): + with self.lock: + app = self.instances.get(prefix) + if app is None: + app = self.create_app(prefix) + if app is not None: + self.instances[prefix] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(_peek_path_info(environ)) + if app is not None: + shift_path_info(environ) + else: + app = self.default_app + return app(environ, start_response) + + def _peek_path_info(environ): + segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) + if segments: + return segments[0] + + return None + +The big difference between this and the subdomain one is that this one +falls back to another application if the creator function returns ``None``. + +.. code-block:: python + + from myapplication import create_app, default_app, get_user_for_prefix + + def make_app(prefix): + user = get_user_for_prefix(prefix) + if user is not None: + return create_app(user) + + application = PathDispatcher(default_app, make_app) diff --git a/test/fixtures/whole_applications/flask/docs/patterns/appfactories.rst b/test/fixtures/whole_applications/flask/docs/patterns/appfactories.rst new file mode 100644 index 0000000..32fd062 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/appfactories.rst @@ -0,0 +1,118 @@ +Application Factories +===================== + +If you are already using packages and blueprints for your application +(:doc:`/blueprints`) there are a couple of really nice ways to further improve +the experience. A common pattern is creating the application object when +the blueprint is imported. But if you move the creation of this object +into a function, you can then create multiple instances of this app later. + +So why would you want to do this? + +1. Testing. You can have instances of the application with different + settings to test every case. +2. Multiple instances. Imagine you want to run different versions of the + same application. Of course you could have multiple instances with + different configs set up in your webserver, but if you use factories, + you can have multiple instances of the same application running in the + same application process which can be handy. + +So how would you then actually implement that? + +Basic Factories +--------------- + +The idea is to set up the application in a function. Like this:: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + from yourapplication.model import db + db.init_app(app) + + from yourapplication.views.admin import admin + from yourapplication.views.frontend import frontend + app.register_blueprint(admin) + app.register_blueprint(frontend) + + return app + +The downside is that you cannot use the application object in the blueprints +at import time. You can however use it from within a request. How do you +get access to the application with the config? Use +:data:`~flask.current_app`:: + + from flask import current_app, Blueprint, render_template + admin = Blueprint('admin', __name__, url_prefix='/admin') + + @admin.route('/') + def index(): + return render_template(current_app.config['INDEX_TEMPLATE']) + +Here we look up the name of a template in the config. + +Factories & Extensions +---------------------- + +It's preferable to create your extensions and app factories so that the +extension object does not initially get bound to the application. + +Using `Flask-SQLAlchemy `_, +as an example, you should not do something along those lines:: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + db = SQLAlchemy(app) + +But, rather, in model.py (or equivalent):: + + db = SQLAlchemy() + +and in your application.py (or equivalent):: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + from yourapplication.model import db + db.init_app(app) + +Using this design pattern, no application-specific state is stored on the +extension object, so one extension object can be used for multiple apps. +For more information about the design of extensions refer to :doc:`/extensiondev`. + +Using Applications +------------------ + +To run such an application, you can use the :command:`flask` command: + +.. code-block:: text + + $ flask --app hello run + +Flask will automatically detect the factory if it is named +``create_app`` or ``make_app`` in ``hello``. You can also pass arguments +to the factory like this: + +.. code-block:: text + + $ flask --app hello:create_app(local_auth=True) run + +Then the ``create_app`` factory in ``myapp`` is called with the keyword +argument ``local_auth=True``. See :doc:`/cli` for more detail. + +Factory Improvements +-------------------- + +The factory function above is not very clever, but you can improve it. +The following changes are straightforward to implement: + +1. Make it possible to pass in configuration values for unit tests so that + you don't have to create config files on the filesystem. +2. Call a function from a blueprint when the application is setting up so + that you have a place to modify attributes of the application (like + hooking in before/after request handlers etc.) +3. Add in WSGI middlewares when the application is being created if necessary. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/caching.rst b/test/fixtures/whole_applications/flask/docs/patterns/caching.rst new file mode 100644 index 0000000..9bf7b72 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/caching.rst @@ -0,0 +1,16 @@ +Caching +======= + +When your application runs slow, throw some caches in. Well, at least +it's the easiest way to speed up things. What does a cache do? Say you +have a function that takes some time to complete but the results would +still be good enough if they were 5 minutes old. So then the idea is that +you actually put the result of that calculation into a cache for some +time. + +Flask itself does not provide caching for you, but `Flask-Caching`_, an +extension for Flask does. Flask-Caching supports various backends, and it is +even possible to develop your own caching backend. + + +.. _Flask-Caching: https://flask-caching.readthedocs.io/en/latest/ diff --git a/test/fixtures/whole_applications/flask/docs/patterns/celery.rst b/test/fixtures/whole_applications/flask/docs/patterns/celery.rst new file mode 100644 index 0000000..2e9a43a --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/celery.rst @@ -0,0 +1,242 @@ +Background Tasks with Celery +============================ + +If your application has a long running task, such as processing some uploaded data or +sending email, you don't want to wait for it to finish during a request. Instead, use a +task queue to send the necessary data to another process that will run the task in the +background while the request returns immediately. + +`Celery`_ is a powerful task queue that can be used for simple background tasks as well +as complex multi-stage programs and schedules. This guide will show you how to configure +Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use +Celery itself. + +.. _Celery: https://celery.readthedocs.io +.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html + +The Flask repository contains `an example `_ +based on the information on this page, which also shows how to use JavaScript to submit +tasks and poll for progress and results. + + +Install +------- + +Install Celery from PyPI, for example using pip: + +.. code-block:: text + + $ pip install celery + + +Integrate Celery with Flask +--------------------------- + +You can use Celery without any integration with Flask, but it's convenient to configure +it through Flask's config, and to let tasks access the Flask application. + +Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration +and registers tasks. While creating a Flask app, use the following code to create and +configure a Celery app as well. + +.. code-block:: python + + from celery import Celery, Task + + def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: + with app.app_context(): + return self.run(*args, **kwargs) + + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app + +This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from +the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so +that it is seen during each request. The ``Task`` subclass automatically runs task +functions with a Flask app context active, so that services like your database +connections are available. + +.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html + +Here's a basic ``example.py`` that configures Celery to use Redis for communication. We +enable a result backend, but ignore results by default. This allows us to store results +only for tasks where we care about the result. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + celery_app = celery_init_app(app) + +Point the ``celery worker`` command at this and it will find the ``celery_app`` object. + +.. code-block:: text + + $ celery -A example worker --loglevel INFO + +You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's +docs for more information about defining schedules. + +.. code-block:: text + + $ celery -A example beat --loglevel INFO + + +Application Factory +------------------- + +When using the Flask application factory pattern, call the ``celery_init_app`` function +inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which +can be used to get the Celery app from the Flask app returned by the factory. + +.. code-block:: python + + def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + return app + +To use ``celery`` commands, Celery needs an app object, but that's no longer directly +available. Create a ``make_celery.py`` file that calls the Flask app factory and gets +the Celery app from the returned Flask app. + +.. code-block:: python + + from example import create_app + + flask_app = create_app() + celery_app = flask_app.extensions["celery"] + +Point the ``celery`` command to this file. + +.. code-block:: text + + $ celery -A make_celery worker --loglevel INFO + $ celery -A make_celery beat --loglevel INFO + + +Defining Tasks +-------------- + +Using ``@celery_app.task`` to decorate task functions requires access to the +``celery_app`` object, which won't be available when using the factory pattern. It also +means that the decorated tasks are tied to the specific Flask and Celery app instances, +which could be an issue during testing if you change configuration for a test. + +Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will +access whatever the "current app" is, which is a similar concept to Flask's blueprints +and app context. This is why we called ``celery_app.set_default()`` above. + +Here's an example task that adds two numbers together and returns the result. + +.. code-block:: python + + from celery import shared_task + + @shared_task(ignore_result=False) + def add_together(a: int, b: int) -> int: + return a + b + +Earlier, we configured Celery to ignore task results by default. Since we want to know +the return value of this task, we set ``ignore_result=False``. On the other hand, a task +that didn't need a result, such as sending an email, wouldn't set this. + + +Calling Tasks +------------- + +The decorated function becomes a task object with methods to call it in the background. +The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for +more methods. + +A Celery worker must be running to run the task. Starting a worker is shown in the +previous sections. + +.. code-block:: python + + from flask import request + + @app.post("/add") + def start_add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = add_together.delay(a, b) + return {"result_id": result.id} + +The route doesn't get the task's result immediately. That would defeat the purpose by +blocking the response. Instead, we return the running task's result id, which we can use +later to get the result. + + +Getting Results +--------------- + +To fetch the result of the task we started above, we'll add another route that takes the +result id we returned before. We return whether the task is finished (ready), whether it +finished successfully, and what the return value (or error) was if it is finished. + +.. code-block:: python + + from celery.result import AsyncResult + + @app.get("/result/") + def task_result(id: str) -> dict[str, object]: + result = AsyncResult(id) + return { + "ready": result.ready(), + "successful": result.successful(), + "value": result.result if result.ready() else None, + } + +Now you can start the task using the first route, then poll for the result using the +second route. This keeps the Flask request workers from being blocked waiting for tasks +to finish. + +The Flask repository contains `an example `_ +using JavaScript to submit tasks and poll for progress and results. + + +Passing Data to Tasks +--------------------- + +The "add" task above took two integers as arguments. To pass arguments to tasks, Celery +has to serialize them to a format that it can pass to other processes. Therefore, +passing complex objects is not recommended. For example, it would be impossible to pass +a SQLAlchemy model object, since that object is probably not serializable and is tied to +the session that queried it. + +Pass the minimal amount of data necessary to fetch or recreate any complex data within +the task. Consider a task that will run when the logged in user asks for an archive of +their data. The Flask request knows the logged in user, and has the user object queried +from the database. It got that by querying the database for a given id, so the task can +do the same thing. Pass the user's id rather than the user object. + +.. code-block:: python + + @shared_task + def generate_user_archive(user_id: str) -> None: + user = db.session.get(User, user_id) + ... + + generate_user_archive.delay(current_user.id) diff --git a/test/fixtures/whole_applications/flask/docs/patterns/deferredcallbacks.rst b/test/fixtures/whole_applications/flask/docs/patterns/deferredcallbacks.rst new file mode 100644 index 0000000..4ff8814 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/deferredcallbacks.rst @@ -0,0 +1,44 @@ +Deferred Request Callbacks +========================== + +One of the design principles of Flask is that response objects are created and +passed down a chain of potential callbacks that can modify them or replace +them. When the request handling starts, there is no response object yet. It is +created as necessary either by a view function or by some other component in +the system. + +What happens if you want to modify the response at a point where the response +does not exist yet? A common example for that would be a +:meth:`~flask.Flask.before_request` callback that wants to set a cookie on the +response object. + +One way is to avoid the situation. Very often that is possible. For instance +you can try to move that logic into a :meth:`~flask.Flask.after_request` +callback instead. However, sometimes moving code there makes it +more complicated or awkward to reason about. + +As an alternative, you can use :func:`~flask.after_this_request` to register +callbacks that will execute after only the current request. This way you can +defer code execution from anywhere in the application, based on the current +request. + +At any time during a request, we can register a function to be called at the +end of the request. For example you can remember the current language of the +user in a cookie in a :meth:`~flask.Flask.before_request` callback:: + + from flask import request, after_this_request + + @app.before_request + def detect_user_language(): + language = request.cookies.get('user_lang') + + if language is None: + language = guess_language_from_request() + + # when the response exists, set a cookie with the language + @after_this_request + def remember_language(response): + response.set_cookie('user_lang', language) + return response + + g.language = language diff --git a/test/fixtures/whole_applications/flask/docs/patterns/favicon.rst b/test/fixtures/whole_applications/flask/docs/patterns/favicon.rst new file mode 100644 index 0000000..21ea767 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/favicon.rst @@ -0,0 +1,53 @@ +Adding a favicon +================ + +A "favicon" is an icon used by browsers for tabs and bookmarks. This helps +to distinguish your website and to give it a unique brand. + +A common question is how to add a favicon to a Flask application. First, of +course, you need an icon. It should be 16 × 16 pixels and in the ICO file +format. This is not a requirement but a de-facto standard supported by all +relevant browsers. Put the icon in your static directory as +:file:`favicon.ico`. + +Now, to get browsers to find your icon, the correct way is to add a link +tag in your HTML. So, for example: + +.. sourcecode:: html+jinja + + + +That's all you need for most browsers, however some really old ones do not +support this standard. The old de-facto standard is to serve this file, +with this name, at the website root. If your application is not mounted at +the root path of the domain you either need to configure the web server to +serve the icon at the root or if you can't do that you're out of luck. If +however your application is the root you can simply route a redirect:: + + app.add_url_rule('/favicon.ico', + redirect_to=url_for('static', filename='favicon.ico')) + +If you want to save the extra redirect request you can also write a view +using :func:`~flask.send_from_directory`:: + + import os + from flask import send_from_directory + + @app.route('/favicon.ico') + def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + +We can leave out the explicit mimetype and it will be guessed, but we may +as well specify it to avoid the extra guessing, as it will always be the +same. + +The above will serve the icon via your application and if possible it's +better to configure your dedicated web server to serve it; refer to the +web server's documentation. + +See also +-------- + +* The `Favicon `_ article on + Wikipedia diff --git a/test/fixtures/whole_applications/flask/docs/patterns/fileuploads.rst b/test/fixtures/whole_applications/flask/docs/patterns/fileuploads.rst new file mode 100644 index 0000000..304f57d --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/fileuploads.rst @@ -0,0 +1,182 @@ +Uploading Files +=============== + +Ah yes, the good old problem of file uploads. The basic idea of file +uploads is actually quite simple. It basically works like this: + +1. A ``

`` tag is marked with ``enctype=multipart/form-data`` + and an ```` is placed in that form. +2. The application accesses the file from the :attr:`~flask.request.files` + dictionary on the request object. +3. use the :meth:`~werkzeug.datastructures.FileStorage.save` method of the file to save + the file permanently somewhere on the filesystem. + +A Gentle Introduction +--------------------- + +Let's start with a very basic application that uploads a file to a +specific upload folder and displays a file to the user. Let's look at the +bootstrapping code for our application:: + + import os + from flask import Flask, flash, request, redirect, url_for + from werkzeug.utils import secure_filename + + UPLOAD_FOLDER = '/path/to/the/uploads' + ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} + + app = Flask(__name__) + app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +So first we need a couple of imports. Most should be straightforward, the +:func:`werkzeug.secure_filename` is explained a little bit later. The +``UPLOAD_FOLDER`` is where we will store the uploaded files and the +``ALLOWED_EXTENSIONS`` is the set of allowed file extensions. + +Why do we limit the extensions that are allowed? You probably don't want +your users to be able to upload everything there if the server is directly +sending out the data to the client. That way you can make sure that users +are not able to upload HTML files that would cause XSS problems (see +:ref:`security-xss`). Also make sure to disallow ``.php`` files if the server +executes them, but who has PHP installed on their server, right? :) + +Next the functions that check if an extension is valid and that uploads +the file and redirects the user to the URL for the uploaded file:: + + def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + @app.route('/', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return redirect(request.url) + file = request.files['file'] + # If the user does not select a file, the browser submits an + # empty file without a filename. + if file.filename == '': + flash('No selected file') + return redirect(request.url) + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + return redirect(url_for('download_file', name=filename)) + return ''' + + Upload new File +

Upload new File

+ + + +
+ ''' + +So what does that :func:`~werkzeug.utils.secure_filename` function actually do? +Now the problem is that there is that principle called "never trust user +input". This is also true for the filename of an uploaded file. All +submitted form data can be forged, and filenames can be dangerous. For +the moment just remember: always use that function to secure a filename +before storing it directly on the filesystem. + +.. admonition:: Information for the Pros + + So you're interested in what that :func:`~werkzeug.utils.secure_filename` + function does and what the problem is if you're not using it? So just + imagine someone would send the following information as `filename` to + your application:: + + filename = "../../../../home/username/.bashrc" + + Assuming the number of ``../`` is correct and you would join this with + the ``UPLOAD_FOLDER`` the user might have the ability to modify a file on + the server's filesystem he or she should not modify. This does require some + knowledge about how the application looks like, but trust me, hackers + are patient :) + + Now let's look how that function works: + + >>> secure_filename('../../../../home/username/.bashrc') + 'home_username_.bashrc' + +We want to be able to serve the uploaded files so they can be downloaded +by users. We'll define a ``download_file`` view to serve files in the +upload folder by name. ``url_for("download_file", name=name)`` generates +download URLs. + +.. code-block:: python + + from flask import send_from_directory + + @app.route('/uploads/') + def download_file(name): + return send_from_directory(app.config["UPLOAD_FOLDER"], name) + +If you're using middleware or the HTTP server to serve files, you can +register the ``download_file`` endpoint as ``build_only`` so ``url_for`` +will work without a view function. + +.. code-block:: python + + app.add_url_rule( + "/uploads/", endpoint="download_file", build_only=True + ) + + +Improving Uploads +----------------- + +.. versionadded:: 0.6 + +So how exactly does Flask handle uploads? Well it will store them in the +webserver's memory if the files are reasonably small, otherwise in a +temporary location (as returned by :func:`tempfile.gettempdir`). But how +do you specify the maximum file size after which an upload is aborted? By +default Flask will happily accept file uploads with an unlimited amount of +memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` +config key:: + + from flask import Flask, Request + + app = Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 + +The code above will limit the maximum allowed payload to 16 megabytes. +If a larger file is transmitted, Flask will raise a +:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. + +.. admonition:: Connection Reset Issue + + When using the local development server, you may get a connection + reset error instead of a 413 response. You will get the correct + status response when running the app with a production WSGI server. + +This feature was added in Flask 0.6 but can be achieved in older versions +as well by subclassing the request object. For more information on that +consult the Werkzeug documentation on file handling. + + +Upload Progress Bars +-------------------- + +A while ago many developers had the idea to read the incoming file in +small chunks and store the upload progress in the database to be able to +poll the progress with JavaScript from the client. The client asks the +server every 5 seconds how much it has transmitted, but this is +something it should already know. + +An Easier Solution +------------------ + +Now there are better solutions that work faster and are more reliable. There +are JavaScript libraries like jQuery_ that have form plugins to ease the +construction of progress bar. + +Because the common pattern for file uploads exists almost unchanged in all +applications dealing with uploads, there are also some Flask extensions that +implement a full fledged upload mechanism that allows controlling which +file extensions are allowed to be uploaded. + +.. _jQuery: https://jquery.com/ diff --git a/test/fixtures/whole_applications/flask/docs/patterns/flashing.rst b/test/fixtures/whole_applications/flask/docs/patterns/flashing.rst new file mode 100644 index 0000000..8eb6b3a --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/flashing.rst @@ -0,0 +1,148 @@ +Message Flashing +================ + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback they will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it next +request and only next request. This is usually combined with a layout +template that does this. Note that browsers and sometimes web servers enforce +a limit on cookie sizes. This means that flashing messages that are too +large for session cookies causes message flashing to fail silently. + +Simple Flashing +--------------- + +So here is a full example:: + + from flask import Flask, flash, redirect, render_template, \ + request, url_for + + app = Flask(__name__) + app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or \ + request.form['password'] != 'secret': + error = 'Invalid credentials' + else: + flash('You were successfully logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) + +And here is the :file:`layout.html` template which does the magic: + +.. sourcecode:: html+jinja + + + My Application + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block body %}{% endblock %} + +Here is the :file:`index.html` template which inherits from :file:`layout.html`: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Overview

+

Do you want to log in? + {% endblock %} + +And here is the :file:`login.html` template which also inherits from +:file:`layout.html`: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Login

+ {% if error %} +

Error: {{ error }} + {% endif %} +

+
+
Username: +
+
Password: +
+
+

+

+ {% endblock %} + +Flashing With Categories +------------------------ + +.. versionadded:: 0.3 + +It is also possible to provide categories when flashing a message. The +default category if nothing is provided is ``'message'``. Alternative +categories can be used to give the user better feedback. For example +error messages could be displayed with a red background. + +To flash a message with a different category, just use the second argument +to the :func:`~flask.flash` function:: + + flash('Invalid password provided', 'error') + +Inside the template you then have to tell the +:func:`~flask.get_flashed_messages` function to also return the +categories. The loop looks slightly different in that situation then: + +.. sourcecode:: html+jinja + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +This is just one example of how to render these flashed messages. One +might also use the category to add a prefix such as +``Error:`` to the message. + +Filtering Flash Messages +------------------------ + +.. versionadded:: 0.9 + +Optionally you can pass a list of categories which filters the results of +:func:`~flask.get_flashed_messages`. This is useful if you wish to +render each category in a separate block. + +.. sourcecode:: html+jinja + + {% with errors = get_flashed_messages(category_filter=["error"]) %} + {% if errors %} +
+ × +
    + {%- for msg in errors %} +
  • {{ msg }}
  • + {% endfor -%} +
+
+ {% endif %} + {% endwith %} diff --git a/test/fixtures/whole_applications/flask/docs/patterns/index.rst b/test/fixtures/whole_applications/flask/docs/patterns/index.rst new file mode 100644 index 0000000..1f2c07d --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/index.rst @@ -0,0 +1,40 @@ +Patterns for Flask +================== + +Certain features and interactions are common enough that you will find +them in most web applications. For example, many applications use a +relational database and user authentication. They will open a database +connection at the beginning of the request and get the information for +the logged in user. At the end of the request, the database connection +is closed. + +These types of patterns may be a bit outside the scope of Flask itself, +but Flask makes it easy to implement them. Some common patterns are +collected in the following pages. + +.. toctree:: + :maxdepth: 2 + + packages + appfactories + appdispatch + urlprocessors + sqlite3 + sqlalchemy + fileuploads + caching + viewdecorators + wtforms + templateinheritance + flashing + javascript + lazyloading + mongoengine + favicon + streaming + deferredcallbacks + methodoverrides + requestchecksum + celery + subclassing + singlepageapplications diff --git a/test/fixtures/whole_applications/flask/docs/patterns/javascript.rst b/test/fixtures/whole_applications/flask/docs/patterns/javascript.rst new file mode 100644 index 0000000..d58a3eb --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/javascript.rst @@ -0,0 +1,259 @@ +JavaScript, ``fetch``, and JSON +=============================== + +You may want to make your HTML page dynamic, by changing data without +reloading the entire page. Instead of submitting an HTML ``
`` and +performing a redirect to re-render the template, you can add +`JavaScript`_ that calls |fetch|_ and replaces content on the page. + +|fetch|_ is the modern, built-in JavaScript solution to making +requests from a page. You may have heard of other "AJAX" methods and +libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in +modern browsers, although you may choose to use them or another library +depending on your application's requirements. These docs will only focus +on built-in JavaScript features. + +.. _JavaScript: https://developer.mozilla.org/Web/JavaScript +.. |fetch| replace:: ``fetch()`` +.. _fetch: https://developer.mozilla.org/Web/API/Fetch_API +.. |XHR| replace:: ``XMLHttpRequest()`` +.. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest +.. _jQuery: https://jquery.com/ + + +Rendering Templates +------------------- + +It is important to understand the difference between templates and +JavaScript. Templates are rendered on the server, before the response is +sent to the user's browser. JavaScript runs in the user's browser, after +the template is rendered and sent. Therefore, it is impossible to use +JavaScript to affect how the Jinja template is rendered, but it is +possible to render data into the JavaScript that will run. + +To provide data to JavaScript when rendering the template, use the +:func:`~jinja-filters.tojson` filter in a `` + +A less common pattern is to add the data to a ``data-`` attribute on an +HTML tag. In this case, you must use single quotes around the value, not +double quotes, otherwise you will produce invalid or unsafe HTML. + +.. code-block:: jinja + +
+ + +Generating URLs +--------------- + +The other way to get data from the server to JavaScript is to make a +request for it. First, you need to know the URL to request. + +The simplest way to generate URLs is to continue to use +:func:`~flask.url_for` when rendering the template. For example: + +.. code-block:: javascript + + const user_url = {{ url_for("user", id=current_user.id)|tojson }} + fetch(user_url).then(...) + +However, you might need to generate a URL based on information you only +know in JavaScript. As discussed above, JavaScript runs in the user's +browser, not as part of the template rendering, so you can't use +``url_for`` at that point. + +In this case, you need to know the "root URL" under which your +application is served. In simple setups, this is ``/``, but it might +also be something else, like ``https://example.com/myapp/``. + +A simple way to tell your JavaScript code about this root is to set it +as a global variable when rendering the template. Then you can use it +when generating URLs from JavaScript. + +.. code-block:: javascript + + const SCRIPT_ROOT = {{ request.script_root|tojson }} + let user_id = ... // do something to get a user id from the page + let user_url = `${SCRIPT_ROOT}/user/${user_id}` + fetch(user_url).then(...) + + +Making a Request with ``fetch`` +------------------------------- + +|fetch|_ takes two arguments, a URL and an object with other options, +and returns a |Promise|_. We won't cover all the available options, and +will only use ``then()`` on the promise, not other callbacks or +``await`` syntax. Read the linked MDN docs for more information about +those features. + +By default, the GET method is used. If the response contains JSON, it +can be used with a ``then()`` callback chain. + +.. code-block:: javascript + + const room_url = {{ url_for("room_detail", id=room.id)|tojson }} + fetch(room_url) + .then(response => response.json()) + .then(data => { + // data is a parsed JSON object + }) + +To send data, use a data method such as POST, and pass the ``body`` +option. The most common types for data are form data or JSON data. + +To send form data, pass a populated |FormData|_ object. This uses the +same format as an HTML form, and would be accessed with ``request.form`` +in a Flask view. + +.. code-block:: javascript + + let data = new FormData() + data.append("name", "Flask Room") + data.append("description", "Talk about Flask here.") + fetch(room_url, { + "method": "POST", + "body": data, + }).then(...) + +In general, prefer sending request data as form data, as would be used +when submitting an HTML form. JSON can represent more complex data, but +unless you need that it's better to stick with the simpler format. When +sending JSON data, the ``Content-Type: application/json`` header must be +sent as well, otherwise Flask will return a 400 error. + +.. code-block:: javascript + + let data = { + "name": "Flask Room", + "description": "Talk about Flask here.", + } + fetch(room_url, { + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "body": JSON.stringify(data), + }).then(...) + +.. |Promise| replace:: ``Promise`` +.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise +.. |FormData| replace:: ``FormData`` +.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData + + +Following Redirects +------------------- + +A response might be a redirect, for example if you logged in with +JavaScript instead of a traditional HTML form, and your view returned +a redirect instead of JSON. JavaScript requests do follow redirects, but +they don't change the page. If you want to make the page change you can +inspect the response and apply the redirect manually. + +.. code-block:: javascript + + fetch("/login", {"body": ...}).then( + response => { + if (response.redirected) { + window.location = response.url + } else { + showLoginError() + } + } + ) + + +Replacing Content +----------------- + +A response might be new HTML, either a new section of the page to add or +replace, or an entirely new page. In general, if you're returning the +entire page, it would be better to handle that with a redirect as shown +in the previous section. The following example shows how to replace a +``
`` with the HTML returned by a request. + +.. code-block:: html + +
+ {{ include "geology_fact.html" }} +
+ + + +Return JSON from Views +---------------------- + +To return a JSON object from your API view, you can directly return a +dict from the view. It will be serialized to JSON automatically. + +.. code-block:: python + + @app.route("/user/") + def user_detail(id): + user = User.query.get_or_404(id) + return { + "username": User.username, + "email": User.email, + "picture": url_for("static", filename=f"users/{id}/profile.png"), + } + +If you want to return another JSON type, use the +:func:`~flask.json.jsonify` function, which creates a response object +with the given data serialized to JSON. + +.. code-block:: python + + from flask import jsonify + + @app.route("/users") + def user_list(): + users = User.query.order_by(User.name).all() + return jsonify([u.to_json() for u in users]) + +It is usually not a good idea to return file data in a JSON response. +JSON cannot represent binary data directly, so it must be base64 +encoded, which can be slow, takes more bandwidth to send, and is not as +easy to cache. Instead, serve files using one view, and generate a URL +to the desired file to include in the JSON. Then the client can make a +separate request to get the linked resource after getting the JSON. + + +Receiving JSON in Views +----------------------- + +Use the :attr:`~flask.Request.json` property of the +:data:`~flask.request` object to decode the request's body as JSON. If +the body is not valid JSON, or the ``Content-Type`` header is not set to +``application/json``, a 400 Bad Request error will be raised. + +.. code-block:: python + + from flask import request + + @app.post("/user/") + def user_update(id): + user = User.query.get_or_404(id) + user.update_from_json(request.json) + db.session.commit() + return user.to_json() diff --git a/test/fixtures/whole_applications/flask/docs/patterns/jquery.rst b/test/fixtures/whole_applications/flask/docs/patterns/jquery.rst new file mode 100644 index 0000000..7ac6856 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/jquery.rst @@ -0,0 +1,6 @@ +:orphan: + +AJAX with jQuery +================ + +Obsolete, see :doc:`/patterns/javascript` instead. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/lazyloading.rst b/test/fixtures/whole_applications/flask/docs/patterns/lazyloading.rst new file mode 100644 index 0000000..658a1cd --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/lazyloading.rst @@ -0,0 +1,109 @@ +Lazily Loading Views +==================== + +Flask is usually used with the decorators. Decorators are simple and you +have the URL right next to the function that is called for that specific +URL. However there is a downside to this approach: it means all your code +that uses decorators has to be imported upfront or Flask will never +actually find your function. + +This can be a problem if your application has to import quick. It might +have to do that on systems like Google's App Engine or other systems. So +if you suddenly notice that your application outgrows this approach you +can fall back to a centralized URL mapping. + +The system that enables having a central URL map is the +:meth:`~flask.Flask.add_url_rule` function. Instead of using decorators, +you have a file that sets up the application with all URLs. + +Converting to Centralized URL Map +--------------------------------- + +Imagine the current application looks somewhat like this:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + pass + + @app.route('/user/') + def user(username): + pass + +Then, with the centralized approach you would have one file with the views +(:file:`views.py`) but without any decorator:: + + def index(): + pass + + def user(username): + pass + +And then a file that sets up an application which maps the functions to +URLs:: + + from flask import Flask + from yourapplication import views + app = Flask(__name__) + app.add_url_rule('/', view_func=views.index) + app.add_url_rule('/user/', view_func=views.user) + +Loading Late +------------ + +So far we only split up the views and the routing, but the module is still +loaded upfront. The trick is to actually load the view function as needed. +This can be accomplished with a helper class that behaves just like a +function but internally imports the real function on first use:: + + from werkzeug.utils import import_string, cached_property + + class LazyView(object): + + def __init__(self, import_name): + self.__module__, self.__name__ = import_name.rsplit('.', 1) + self.import_name = import_name + + @cached_property + def view(self): + return import_string(self.import_name) + + def __call__(self, *args, **kwargs): + return self.view(*args, **kwargs) + +What's important here is is that `__module__` and `__name__` are properly +set. This is used by Flask internally to figure out how to name the +URL rules in case you don't provide a name for the rule yourself. + +Then you can define your central place to combine the views like this:: + + from flask import Flask + from yourapplication.helpers import LazyView + app = Flask(__name__) + app.add_url_rule('/', + view_func=LazyView('yourapplication.views.index')) + app.add_url_rule('/user/', + view_func=LazyView('yourapplication.views.user')) + +You can further optimize this in terms of amount of keystrokes needed to +write this by having a function that calls into +:meth:`~flask.Flask.add_url_rule` by prefixing a string with the project +name and a dot, and by wrapping `view_func` in a `LazyView` as needed. :: + + def url(import_name, url_rules=[], **options): + view = LazyView(f"yourapplication.{import_name}") + for url_rule in url_rules: + app.add_url_rule(url_rule, view_func=view, **options) + + # add a single route to the index view + url('views.index', ['/']) + + # add two routes to a single function endpoint + url_rules = ['/user/','/user/'] + url('views.user', url_rules) + +One thing to keep in mind is that before and after request handlers have +to be in a file that is imported upfront to work properly on the first +request. The same goes for any kind of remaining decorator. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/methodoverrides.rst b/test/fixtures/whole_applications/flask/docs/patterns/methodoverrides.rst new file mode 100644 index 0000000..45dbb87 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/methodoverrides.rst @@ -0,0 +1,42 @@ +Adding HTTP Method Overrides +============================ + +Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP +methods (such as PATCH). In that case it's possible to "proxy" HTTP +methods through another HTTP method in total violation of the protocol. + +The way this works is by letting the client do an HTTP POST request and +set the ``X-HTTP-Method-Override`` header. Then the method is replaced +with the header value before being passed to Flask. + +This can be accomplished with an HTTP middleware:: + + class HTTPMethodOverrideMiddleware(object): + allowed_methods = frozenset([ + 'GET', + 'HEAD', + 'POST', + 'DELETE', + 'PUT', + 'PATCH', + 'OPTIONS' + ]) + bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() + if method in self.allowed_methods: + environ['REQUEST_METHOD'] = method + if method in self.bodyless_methods: + environ['CONTENT_LENGTH'] = '0' + return self.app(environ, start_response) + +To use this with Flask, wrap the app object with the middleware:: + + from flask import Flask + + app = Flask(__name__) + app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) diff --git a/test/fixtures/whole_applications/flask/docs/patterns/mongoengine.rst b/test/fixtures/whole_applications/flask/docs/patterns/mongoengine.rst new file mode 100644 index 0000000..015e7b6 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/mongoengine.rst @@ -0,0 +1,103 @@ +MongoDB with MongoEngine +======================== + +Using a document database like MongoDB is a common alternative to +relational SQL databases. This pattern shows how to use +`MongoEngine`_, a document mapper library, to integrate with MongoDB. + +A running MongoDB server and `Flask-MongoEngine`_ are required. :: + + pip install flask-mongoengine + +.. _MongoEngine: http://mongoengine.org +.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io + + +Configuration +------------- + +Basic setup can be done by defining ``MONGODB_SETTINGS`` on +``app.config`` and creating a ``MongoEngine`` instance. :: + + from flask import Flask + from flask_mongoengine import MongoEngine + + app = Flask(__name__) + app.config['MONGODB_SETTINGS'] = { + "db": "myapp", + } + db = MongoEngine(app) + + +Mapping Documents +----------------- + +To declare a model that represents a Mongo document, create a class that +inherits from ``Document`` and declare each of the fields. :: + + import mongoengine as me + + class Movie(me.Document): + title = me.StringField(required=True) + year = me.IntField() + rated = me.StringField() + director = me.StringField() + actors = me.ListField() + +If the document has nested fields, use ``EmbeddedDocument`` to +defined the fields of the embedded document and +``EmbeddedDocumentField`` to declare it on the parent document. :: + + class Imdb(me.EmbeddedDocument): + imdb_id = me.StringField() + rating = me.DecimalField() + votes = me.IntField() + + class Movie(me.Document): + ... + imdb = me.EmbeddedDocumentField(Imdb) + + +Creating Data +------------- + +Instantiate your document class with keyword arguments for the fields. +You can also assign values to the field attributes after instantiation. +Then call ``doc.save()``. :: + + bttf = Movie(title="Back To The Future", year=1985) + bttf.actors = [ + "Michael J. Fox", + "Christopher Lloyd" + ] + bttf.imdb = Imdb(imdb_id="tt0088763", rating=8.5) + bttf.save() + + +Queries +------- + +Use the class ``objects`` attribute to make queries. A keyword argument +looks for an equal value on the field. :: + + bttf = Movies.objects(title="Back To The Future").get_or_404() + +Query operators may be used by concatenating them with the field name +using a double-underscore. ``objects``, and queries returned by +calling it, are iterable. :: + + some_theron_movie = Movie.objects(actors__in=["Charlize Theron"]).first() + + for recents in Movie.objects(year__gte=2017): + print(recents.title) + + +Documentation +------------- + +There are many more ways to define and query documents with MongoEngine. +For more information, check out the `official documentation +`_. + +Flask-MongoEngine adds helpful utilities on top of MongoEngine. Check +out their `documentation `_ as well. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/packages.rst b/test/fixtures/whole_applications/flask/docs/patterns/packages.rst new file mode 100644 index 0000000..90fa8a8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/packages.rst @@ -0,0 +1,133 @@ +Large Applications as Packages +============================== + +Imagine a simple flask application structure that looks like this:: + + /yourapplication + yourapplication.py + /static + style.css + /templates + layout.html + index.html + login.html + ... + +While this is fine for small applications, for larger applications +it's a good idea to use a package instead of a module. +The :doc:`/tutorial/index` is structured to use the package pattern, +see the :gh:`example code `. + +Simple Packages +--------------- + +To convert that into a larger one, just create a new folder +:file:`yourapplication` inside the existing one and move everything below it. +Then rename :file:`yourapplication.py` to :file:`__init__.py`. (Make sure to delete +all ``.pyc`` files first, otherwise things would most likely break) + +You should then end up with something like that:: + + /yourapplication + /yourapplication + __init__.py + /static + style.css + /templates + layout.html + index.html + login.html + ... + +But how do you run your application now? The naive ``python +yourapplication/__init__.py`` will not work. Let's just say that Python +does not want modules in packages to be the startup file. But that is not +a big problem, just add a new file called :file:`pyproject.toml` next to the inner +:file:`yourapplication` folder with the following contents: + +.. code-block:: toml + + [project] + name = "yourapplication" + dependencies = [ + "flask", + ] + + [build-system] + requires = ["flit_core<4"] + build-backend = "flit_core.buildapi" + +Install your application so it is importable: + +.. code-block:: text + + $ pip install -e . + +To use the ``flask`` command and run your application you need to set +the ``--app`` option that tells Flask where to find the application +instance: + +.. code-block:: text + + $ flask --app yourapplication run + +What did we gain from this? Now we can restructure the application a bit +into multiple modules. The only thing you have to remember is the +following quick checklist: + +1. the `Flask` application object creation has to be in the + :file:`__init__.py` file. That way each module can import it safely and the + `__name__` variable will resolve to the correct package. +2. all the view functions (the ones with a :meth:`~flask.Flask.route` + decorator on top) have to be imported in the :file:`__init__.py` file. + Not the object itself, but the module it is in. Import the view module + **after the application object is created**. + +Here's an example :file:`__init__.py`:: + + from flask import Flask + app = Flask(__name__) + + import yourapplication.views + +And this is what :file:`views.py` would look like:: + + from yourapplication import app + + @app.route('/') + def index(): + return 'Hello World!' + +You should then end up with something like that:: + + /yourapplication + pyproject.toml + /yourapplication + __init__.py + views.py + /static + style.css + /templates + layout.html + index.html + login.html + ... + +.. admonition:: Circular Imports + + Every Python programmer hates them, and yet we just added some: + circular imports (That's when two modules depend on each other. In this + case :file:`views.py` depends on :file:`__init__.py`). Be advised that this is a + bad idea in general but here it is actually fine. The reason for this is + that we are not actually using the views in :file:`__init__.py` and just + ensuring the module is imported and we are doing that at the bottom of + the file. + + +Working with Blueprints +----------------------- + +If you have larger applications it's recommended to divide them into +smaller groups where each group is implemented with the help of a +blueprint. For a gentle introduction into this topic refer to the +:doc:`/blueprints` chapter of the documentation. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/requestchecksum.rst b/test/fixtures/whole_applications/flask/docs/patterns/requestchecksum.rst new file mode 100644 index 0000000..25bc38b --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/requestchecksum.rst @@ -0,0 +1,55 @@ +Request Content Checksums +========================= + +Various pieces of code can consume the request data and preprocess it. +For instance JSON data ends up on the request object already read and +processed, form data ends up there as well but goes through a different +code path. This seems inconvenient when you want to calculate the +checksum of the incoming request data. This is necessary sometimes for +some APIs. + +Fortunately this is however very simple to change by wrapping the input +stream. + +The following example calculates the SHA1 checksum of the incoming data as +it gets read and stores it in the WSGI environment:: + + import hashlib + + class ChecksumCalcStream(object): + + def __init__(self, stream): + self._stream = stream + self._hash = hashlib.sha1() + + def read(self, bytes): + rv = self._stream.read(bytes) + self._hash.update(rv) + return rv + + def readline(self, size_hint): + rv = self._stream.readline(size_hint) + self._hash.update(rv) + return rv + + def generate_checksum(request): + env = request.environ + stream = ChecksumCalcStream(env['wsgi.input']) + env['wsgi.input'] = stream + return stream._hash + +To use this, all you need to do is to hook the calculating stream in +before the request starts consuming data. (Eg: be careful accessing +``request.form`` or anything of that nature. ``before_request_handlers`` +for instance should be careful not to access it). + +Example usage:: + + @app.route('/special-api', methods=['POST']) + def special_api(): + hash = generate_checksum(request) + # Accessing this parses the input stream + files = request.files + # At this point the hash is fully constructed. + checksum = hash.hexdigest() + return f"Hash was: {checksum}" diff --git a/test/fixtures/whole_applications/flask/docs/patterns/singlepageapplications.rst b/test/fixtures/whole_applications/flask/docs/patterns/singlepageapplications.rst new file mode 100644 index 0000000..1cb779b --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/singlepageapplications.rst @@ -0,0 +1,24 @@ +Single-Page Applications +======================== + +Flask can be used to serve Single-Page Applications (SPA) by placing static +files produced by your frontend framework in a subfolder inside of your +project. You will also need to create a catch-all endpoint that routes all +requests to your SPA. + +The following example demonstrates how to serve an SPA along with an API:: + + from flask import Flask, jsonify + + app = Flask(__name__, static_folder='app', static_url_path="/app") + + + @app.route("/heartbeat") + def heartbeat(): + return jsonify({"status": "healthy"}) + + + @app.route('/', defaults={'path': ''}) + @app.route('/') + def catch_all(path): + return app.send_static_file("index.html") diff --git a/test/fixtures/whole_applications/flask/docs/patterns/sqlalchemy.rst b/test/fixtures/whole_applications/flask/docs/patterns/sqlalchemy.rst new file mode 100644 index 0000000..7e4108d --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/sqlalchemy.rst @@ -0,0 +1,214 @@ +SQLAlchemy in Flask +=================== + +Many people prefer `SQLAlchemy`_ for database access. In this case it's +encouraged to use a package instead of a module for your flask application +and drop the models into a separate module (:doc:`packages`). While that +is not necessary, it makes a lot of sense. + +There are four very common ways to use SQLAlchemy. I will outline each +of them here: + +Flask-SQLAlchemy Extension +-------------------------- + +Because SQLAlchemy is a common database abstraction layer and object +relational mapper that requires a little bit of configuration effort, +there is a Flask extension that handles that for you. This is recommended +if you want to get started quickly. + +You can download `Flask-SQLAlchemy`_ from `PyPI +`_. + +.. _Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/ + + +Declarative +----------- + +The declarative extension in SQLAlchemy is the most recent method of using +SQLAlchemy. It allows you to define tables and models in one go, similar +to how Django works. In addition to the following text I recommend the +official documentation on the `declarative`_ extension. + +Here's the example :file:`database.py` module for your application:: + + from sqlalchemy import create_engine + from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base + + engine = create_engine('sqlite:////tmp/test.db') + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + Base = declarative_base() + Base.query = db_session.query_property() + + def init_db(): + # import all modules here that might define models so that + # they will be registered properly on the metadata. Otherwise + # you will have to import them first before calling init_db() + import yourapplication.models + Base.metadata.create_all(bind=engine) + +To define your models, just subclass the `Base` class that was created by +the code above. If you are wondering why we don't have to care about +threads here (like we did in the SQLite3 example above with the +:data:`~flask.g` object): that's because SQLAlchemy does that for us +already with the :class:`~sqlalchemy.orm.scoped_session`. + +To use SQLAlchemy in a declarative way with your application, you just +have to put the following code into your application module. Flask will +automatically remove database sessions at the end of the request or +when the application shuts down:: + + from yourapplication.database import db_session + + @app.teardown_appcontext + def shutdown_session(exception=None): + db_session.remove() + +Here is an example model (put this into :file:`models.py`, e.g.):: + + from sqlalchemy import Column, Integer, String + from yourapplication.database import Base + + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True) + email = Column(String(120), unique=True) + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return f'' + +To create the database you can use the `init_db` function: + +>>> from yourapplication.database import init_db +>>> init_db() + +You can insert entries into the database like this: + +>>> from yourapplication.database import db_session +>>> from yourapplication.models import User +>>> u = User('admin', 'admin@localhost') +>>> db_session.add(u) +>>> db_session.commit() + +Querying is simple as well: + +>>> User.query.all() +[] +>>> User.query.filter(User.name == 'admin').first() + + +.. _SQLAlchemy: https://www.sqlalchemy.org/ +.. _declarative: https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ + +Manual Object Relational Mapping +-------------------------------- + +Manual object relational mapping has a few upsides and a few downsides +versus the declarative approach from above. The main difference is that +you define tables and classes separately and map them together. It's more +flexible but a little more to type. In general it works like the +declarative approach, so make sure to also split up your application into +multiple modules in a package. + +Here is an example :file:`database.py` module for your application:: + + from sqlalchemy import create_engine, MetaData + from sqlalchemy.orm import scoped_session, sessionmaker + + engine = create_engine('sqlite:////tmp/test.db') + metadata = MetaData() + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + def init_db(): + metadata.create_all(bind=engine) + +As in the declarative approach, you need to close the session after +each request or application context shutdown. Put this into your +application module:: + + from yourapplication.database import db_session + + @app.teardown_appcontext + def shutdown_session(exception=None): + db_session.remove() + +Here is an example table and model (put this into :file:`models.py`):: + + from sqlalchemy import Table, Column, Integer, String + from sqlalchemy.orm import mapper + from yourapplication.database import metadata, db_session + + class User(object): + query = db_session.query_property() + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return f'' + + users = Table('users', metadata, + Column('id', Integer, primary_key=True), + Column('name', String(50), unique=True), + Column('email', String(120), unique=True) + ) + mapper(User, users) + +Querying and inserting works exactly the same as in the example above. + + +SQL Abstraction Layer +--------------------- + +If you just want to use the database system (and SQL) abstraction layer +you basically only need the engine:: + + from sqlalchemy import create_engine, MetaData, Table + + engine = create_engine('sqlite:////tmp/test.db') + metadata = MetaData(bind=engine) + +Then you can either declare the tables in your code like in the examples +above, or automatically load them:: + + from sqlalchemy import Table + + users = Table('users', metadata, autoload=True) + +To insert data you can use the `insert` method. We have to get a +connection first so that we can use a transaction: + +>>> con = engine.connect() +>>> con.execute(users.insert(), name='admin', email='admin@localhost') + +SQLAlchemy will automatically commit for us. + +To query your database, you use the engine directly or use a connection: + +>>> users.select(users.c.id == 1).execute().first() +(1, 'admin', 'admin@localhost') + +These results are also dict-like tuples: + +>>> r = users.select(users.c.id == 1).execute().first() +>>> r['name'] +'admin' + +You can also pass strings of SQL statements to the +:meth:`~sqlalchemy.engine.base.Connection.execute` method: + +>>> engine.execute('select * from users where id = :1', [1]).first() +(1, 'admin', 'admin@localhost') + +For more information about SQLAlchemy, head over to the +`website `_. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/sqlite3.rst b/test/fixtures/whole_applications/flask/docs/patterns/sqlite3.rst new file mode 100644 index 0000000..5932589 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/sqlite3.rst @@ -0,0 +1,147 @@ +Using SQLite 3 with Flask +========================= + +In Flask you can easily implement the opening of database connections on +demand and closing them when the context dies (usually at the end of the +request). + +Here is a simple example of how you can use SQLite 3 with Flask:: + + import sqlite3 + from flask import g + + DATABASE = '/path/to/database.db' + + def get_db(): + db = getattr(g, '_database', None) + if db is None: + db = g._database = sqlite3.connect(DATABASE) + return db + + @app.teardown_appcontext + def close_connection(exception): + db = getattr(g, '_database', None) + if db is not None: + db.close() + +Now, to use the database, the application must either have an active +application context (which is always true if there is a request in flight) +or create an application context itself. At that point the ``get_db`` +function can be used to get the current database connection. Whenever the +context is destroyed the database connection will be terminated. + +Example:: + + @app.route('/') + def index(): + cur = get_db().cursor() + ... + + +.. note:: + + Please keep in mind that the teardown request and appcontext functions + are always executed, even if a before-request handler failed or was + never executed. Because of this we have to make sure here that the + database is there before we close it. + +Connect on Demand +----------------- + +The upside of this approach (connecting on first use) is that this will +only open the connection if truly necessary. If you want to use this +code outside a request context you can use it in a Python shell by opening +the application context by hand:: + + with app.app_context(): + # now you can use get_db() + + +Easy Querying +------------- + +Now in each request handling function you can access `get_db()` to get the +current open database connection. To simplify working with SQLite, a +row factory function is useful. It is executed for every result returned +from the database to convert the result. For instance, in order to get +dictionaries instead of tuples, this could be inserted into the ``get_db`` +function we created above:: + + def make_dicts(cursor, row): + return dict((cursor.description[idx][0], value) + for idx, value in enumerate(row)) + + db.row_factory = make_dicts + +This will make the sqlite3 module return dicts for this database connection, which are much nicer to deal with. Even more simply, we could place this in ``get_db`` instead:: + + db.row_factory = sqlite3.Row + +This would use Row objects rather than dicts to return the results of queries. These are ``namedtuple`` s, so we can access them either by index or by key. For example, assuming we have a ``sqlite3.Row`` called ``r`` for the rows ``id``, ``FirstName``, ``LastName``, and ``MiddleInitial``:: + + >>> # You can get values based on the row's name + >>> r['FirstName'] + John + >>> # Or, you can get them based on index + >>> r[1] + John + # Row objects are also iterable: + >>> for value in r: + ... print(value) + 1 + John + Doe + M + +Additionally, it is a good idea to provide a query function that combines +getting the cursor, executing and fetching the results:: + + def query_db(query, args=(), one=False): + cur = get_db().execute(query, args) + rv = cur.fetchall() + cur.close() + return (rv[0] if rv else None) if one else rv + +This handy little function, in combination with a row factory, makes +working with the database much more pleasant than it is by just using the +raw cursor and connection objects. + +Here is how you can use it:: + + for user in query_db('select * from users'): + print(user['username'], 'has the id', user['user_id']) + +Or if you just want a single result:: + + user = query_db('select * from users where username = ?', + [the_username], one=True) + if user is None: + print('No such user') + else: + print(the_username, 'has the id', user['user_id']) + +To pass variable parts to the SQL statement, use a question mark in the +statement and pass in the arguments as a list. Never directly add them to +the SQL statement with string formatting because this makes it possible +to attack the application using `SQL Injections +`_. + +Initial Schemas +--------------- + +Relational databases need schemas, so applications often ship a +`schema.sql` file that creates the database. It's a good idea to provide +a function that creates the database based on that schema. This function +can do that for you:: + + def init_db(): + with app.app_context(): + db = get_db() + with app.open_resource('schema.sql', mode='r') as f: + db.cursor().executescript(f.read()) + db.commit() + +You can then create such a database from the Python shell: + +>>> from yourapplication import init_db +>>> init_db() diff --git a/test/fixtures/whole_applications/flask/docs/patterns/streaming.rst b/test/fixtures/whole_applications/flask/docs/patterns/streaming.rst new file mode 100644 index 0000000..c9e6ef2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/streaming.rst @@ -0,0 +1,85 @@ +Streaming Contents +================== + +Sometimes you want to send an enormous amount of data to the client, much +more than you want to keep in memory. When you are generating the data on +the fly though, how do you send that back to the client without the +roundtrip to the filesystem? + +The answer is by using generators and direct responses. + +Basic Usage +----------- + +This is a basic view function that generates a lot of CSV data on the fly. +The trick is to have an inner function that uses a generator to generate +data and to then invoke that function and pass it to a response object:: + + @app.route('/large.csv') + def generate_large_csv(): + def generate(): + for row in iter_all_rows(): + yield f"{','.join(row)}\n" + return generate(), {"Content-Type": "text/csv"} + +Each ``yield`` expression is directly sent to the browser. Note though +that some WSGI middlewares might break streaming, so be careful there in +debug environments with profilers and other things you might have enabled. + +Streaming from Templates +------------------------ + +The Jinja2 template engine supports rendering a template piece by +piece, returning an iterator of strings. Flask provides the +:func:`~flask.stream_template` and :func:`~flask.stream_template_string` +functions to make this easier to use. + +.. code-block:: python + + from flask import stream_template + + @app.get("/timeline") + def timeline(): + return stream_template("timeline.html") + +The parts yielded by the render stream tend to match statement blocks in +the template. + + +Streaming with Context +---------------------- + +The :data:`~flask.request` will not be active while the generator is +running, because the view has already returned at that point. If you try +to access ``request``, you'll get a ``RuntimeError``. + +If your generator function relies on data in ``request``, use the +:func:`~flask.stream_with_context` wrapper. This will keep the request +context active during the generator. + +.. code-block:: python + + from flask import stream_with_context, request + from markupsafe import escape + + @app.route('/stream') + def streamed_response(): + def generate(): + yield '

Hello ' + yield escape(request.args['name']) + yield '!

' + return stream_with_context(generate()) + +It can also be used as a decorator. + +.. code-block:: python + + @stream_with_context + def generate(): + ... + + return generate() + +The :func:`~flask.stream_template` and +:func:`~flask.stream_template_string` functions automatically +use :func:`~flask.stream_with_context` if a request is active. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/subclassing.rst b/test/fixtures/whole_applications/flask/docs/patterns/subclassing.rst new file mode 100644 index 0000000..d8de233 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/subclassing.rst @@ -0,0 +1,17 @@ +Subclassing Flask +================= + +The :class:`~flask.Flask` class is designed for subclassing. + +For example, you may want to override how request parameters are handled to preserve their order:: + + from flask import Flask, Request + from werkzeug.datastructures import ImmutableOrderedMultiDict + class MyRequest(Request): + """Request subclass to override request parameter storage""" + parameter_storage_class = ImmutableOrderedMultiDict + class MyFlask(Flask): + """Flask subclass using the custom request class""" + request_class = MyRequest + +This is the recommended approach for overriding or augmenting Flask's internal functionality. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/templateinheritance.rst b/test/fixtures/whole_applications/flask/docs/patterns/templateinheritance.rst new file mode 100644 index 0000000..bb5cba2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/templateinheritance.rst @@ -0,0 +1,68 @@ +Template Inheritance +==================== + +The most powerful part of Jinja is template inheritance. Template inheritance +allows you to build a base "skeleton" template that contains all the common +elements of your site and defines **blocks** that child templates can override. + +Sounds complicated but is very basic. It's easiest to understand it by starting +with an example. + + +Base Template +------------- + +This template, which we'll call :file:`layout.html`, defines a simple HTML skeleton +document that you might use for a simple two-column page. It's the job of +"child" templates to fill the empty blocks with content: + +.. sourcecode:: html+jinja + + + + + {% block head %} + + {% block title %}{% endblock %} - My Webpage + {% endblock %} + + +
{% block content %}{% endblock %}
+ + + + +In this example, the ``{% block %}`` tags define four blocks that child templates +can fill in. All the `block` tag does is tell the template engine that a +child template may override those portions of the template. + +Child Template +-------------- + +A child template might look like this: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block title %}Index{% endblock %} + {% block head %} + {{ super() }} + + {% endblock %} + {% block content %} +

Index

+

+ Welcome on my awesome homepage. + {% endblock %} + +The ``{% extends %}`` tag is the key here. It tells the template engine that +this template "extends" another template. When the template system evaluates +this template, first it locates the parent. The extends tag must be the +first tag in the template. To render the contents of a block defined in +the parent template, use ``{{ super() }}``. diff --git a/test/fixtures/whole_applications/flask/docs/patterns/urlprocessors.rst b/test/fixtures/whole_applications/flask/docs/patterns/urlprocessors.rst new file mode 100644 index 0000000..0d74320 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/urlprocessors.rst @@ -0,0 +1,126 @@ +Using URL Processors +==================== + +.. versionadded:: 0.7 + +Flask 0.7 introduces the concept of URL processors. The idea is that you +might have a bunch of resources with common parts in the URL that you +don't always explicitly want to provide. For instance you might have a +bunch of URLs that have the language code in it but you don't want to have +to handle it in every single function yourself. + +URL processors are especially helpful when combined with blueprints. We +will handle both application specific URL processors here as well as +blueprint specifics. + +Internationalized Application URLs +---------------------------------- + +Consider an application like this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.route('//') + def index(lang_code): + g.lang_code = lang_code + ... + + @app.route('//about') + def about(lang_code): + g.lang_code = lang_code + ... + +This is an awful lot of repetition as you have to handle the language code +setting on the :data:`~flask.g` object yourself in every single function. +Sure, a decorator could be used to simplify this, but if you want to +generate URLs from one function to another you would have to still provide +the language code explicitly which can be annoying. + +For the latter, this is where :func:`~flask.Flask.url_defaults` functions +come in. They can automatically inject values into a call to +:func:`~flask.url_for`. The code below checks if the +language code is not yet in the dictionary of URL values and if the +endpoint wants a value named ``'lang_code'``:: + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + +The method :meth:`~werkzeug.routing.Map.is_endpoint_expecting` of the URL +map can be used to figure out if it would make sense to provide a language +code for the given endpoint. + +The reverse of that function are +:meth:`~flask.Flask.url_value_preprocessor`\s. They are executed right +after the request was matched and can execute code based on the URL +values. The idea is that they pull information out of the values +dictionary and put it somewhere else:: + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code', None) + +That way you no longer have to do the `lang_code` assignment to +:data:`~flask.g` in every function. You can further improve that by +writing your own decorator that prefixes URLs with the language code, but +the more beautiful solution is using a blueprint. Once the +``'lang_code'`` is popped from the values dictionary and it will no longer +be forwarded to the view function reducing the code to this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + ... + + @app.route('//about') + def about(): + ... + +Internationalized Blueprint URLs +-------------------------------- + +Because blueprints can automatically prefix all URLs with a common string +it's easy to automatically do that for every function. Furthermore +blueprints can have per-blueprint URL processors which removes a whole lot +of logic from the :meth:`~flask.Flask.url_defaults` function because it no +longer has to check if the URL is really interested in a ``'lang_code'`` +parameter:: + + from flask import Blueprint, g + + bp = Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + ... + + @bp.route('/about') + def about(): + ... diff --git a/test/fixtures/whole_applications/flask/docs/patterns/viewdecorators.rst b/test/fixtures/whole_applications/flask/docs/patterns/viewdecorators.rst new file mode 100644 index 0000000..0b0479e --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/viewdecorators.rst @@ -0,0 +1,171 @@ +View Decorators +=============== + +Python has a really interesting feature called function decorators. This +allows some really neat things for web applications. Because each view in +Flask is a function, decorators can be used to inject additional +functionality to one or more functions. The :meth:`~flask.Flask.route` +decorator is the one you probably used already. But there are use cases +for implementing your own decorator. For instance, imagine you have a +view that should only be used by people that are logged in. If a user +goes to the site and is not logged in, they should be redirected to the +login page. This is a good example of a use case where a decorator is an +excellent solution. + +Login Required Decorator +------------------------ + +So let's implement such a decorator. A decorator is a function that +wraps and replaces another function. Since the original function is +replaced, you need to remember to copy the original function's information +to the new function. Use :func:`functools.wraps` to handle this for you. + +This example assumes that the login page is called ``'login'`` and that +the current user is stored in ``g.user`` and is ``None`` if there is no-one +logged in. :: + + from functools import wraps + from flask import g, request, redirect, url_for + + def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user is None: + return redirect(url_for('login', next=request.url)) + return f(*args, **kwargs) + return decorated_function + +To use the decorator, apply it as innermost decorator to a view function. +When applying further decorators, always remember +that the :meth:`~flask.Flask.route` decorator is the outermost. :: + + @app.route('/secret_page') + @login_required + def secret_page(): + pass + +.. note:: + The ``next`` value will exist in ``request.args`` after a ``GET`` request for + the login page. You'll have to pass it along when sending the ``POST`` request + from the login form. You can do this with a hidden input tag, then retrieve it + from ``request.form`` when logging the user in. :: + + + + +Caching Decorator +----------------- + +Imagine you have a view function that does an expensive calculation and +because of that you would like to cache the generated results for a +certain amount of time. A decorator would be nice for that. We're +assuming you have set up a cache like mentioned in :doc:`caching`. + +Here is an example cache function. It generates the cache key from a +specific prefix (actually a format string) and the current path of the +request. Notice that we are using a function that first creates the +decorator that then decorates the function. Sounds awful? Unfortunately +it is a little bit more complex, but the code should still be +straightforward to read. + +The decorated function will then work as follows + +1. get the unique cache key for the current request based on the current + path. +2. get the value for that key from the cache. If the cache returned + something we will return that value. +3. otherwise the original function is called and the return value is + stored in the cache for the timeout provided (by default 5 minutes). + +Here the code:: + + from functools import wraps + from flask import request + + def cached(timeout=5 * 60, key='view/{}'): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + cache_key = key.format(request.path) + rv = cache.get(cache_key) + if rv is not None: + return rv + rv = f(*args, **kwargs) + cache.set(cache_key, rv, timeout=timeout) + return rv + return decorated_function + return decorator + +Notice that this assumes an instantiated ``cache`` object is available, see +:doc:`caching`. + + +Templating Decorator +-------------------- + +A common pattern invented by the TurboGears guys a while back is a +templating decorator. The idea of that decorator is that you return a +dictionary with the values passed to the template from the view function +and the template is automatically rendered. With that, the following +three examples do exactly the same:: + + @app.route('/') + def index(): + return render_template('index.html', value=42) + + @app.route('/') + @templated('index.html') + def index(): + return dict(value=42) + + @app.route('/') + @templated() + def index(): + return dict(value=42) + +As you can see, if no template name is provided it will use the endpoint +of the URL map with dots converted to slashes + ``'.html'``. Otherwise +the provided template name is used. When the decorated function returns, +the dictionary returned is passed to the template rendering function. If +``None`` is returned, an empty dictionary is assumed, if something else than +a dictionary is returned we return it from the function unchanged. That +way you can still use the redirect function or return simple strings. + +Here is the code for that decorator:: + + from functools import wraps + from flask import request, render_template + + def templated(template=None): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + template_name = template + if template_name is None: + template_name = f"{request.endpoint.replace('.', '/')}.html" + ctx = f(*args, **kwargs) + if ctx is None: + ctx = {} + elif not isinstance(ctx, dict): + return ctx + return render_template(template_name, **ctx) + return decorated_function + return decorator + + +Endpoint Decorator +------------------ + +When you want to use the werkzeug routing system for more flexibility you +need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` +to a view function. This is possible with this decorator. For example:: + + from flask import Flask + from werkzeug.routing import Rule + + app = Flask(__name__) + app.url_map.add(Rule('/', endpoint='index')) + + @app.endpoint('index') + def my_index(): + return "Hello world" diff --git a/test/fixtures/whole_applications/flask/docs/patterns/wtforms.rst b/test/fixtures/whole_applications/flask/docs/patterns/wtforms.rst new file mode 100644 index 0000000..3d626f5 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/patterns/wtforms.rst @@ -0,0 +1,126 @@ +Form Validation with WTForms +============================ + +When you have to work with form data submitted by a browser view, code +quickly becomes very hard to read. There are libraries out there designed +to make this process easier to manage. One of them is `WTForms`_ which we +will handle here. If you find yourself in the situation of having many +forms, you might want to give it a try. + +When you are working with WTForms you have to define your forms as classes +first. I recommend breaking up the application into multiple modules +(:doc:`packages`) for that and adding a separate module for the +forms. + +.. admonition:: Getting the most out of WTForms with an Extension + + The `Flask-WTF`_ extension expands on this pattern and adds a + few little helpers that make working with forms and Flask more + fun. You can get it from `PyPI + `_. + +.. _Flask-WTF: https://flask-wtf.readthedocs.io/ + +The Forms +--------- + +This is an example form for a typical registration page:: + + from wtforms import Form, BooleanField, StringField, PasswordField, validators + + class RegistrationForm(Form): + username = StringField('Username', [validators.Length(min=4, max=25)]) + email = StringField('Email Address', [validators.Length(min=6, max=35)]) + password = PasswordField('New Password', [ + validators.DataRequired(), + validators.EqualTo('confirm', message='Passwords must match') + ]) + confirm = PasswordField('Repeat Password') + accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) + +In the View +----------- + +In the view function, the usage of this form looks like this:: + + @app.route('/register', methods=['GET', 'POST']) + def register(): + form = RegistrationForm(request.form) + if request.method == 'POST' and form.validate(): + user = User(form.username.data, form.email.data, + form.password.data) + db_session.add(user) + flash('Thanks for registering') + return redirect(url_for('login')) + return render_template('register.html', form=form) + +Notice we're implying that the view is using SQLAlchemy here +(:doc:`sqlalchemy`), but that's not a requirement, of course. Adapt +the code as necessary. + +Things to remember: + +1. create the form from the request :attr:`~flask.request.form` value if + the data is submitted via the HTTP ``POST`` method and + :attr:`~flask.request.args` if the data is submitted as ``GET``. +2. to validate the data, call the :func:`~wtforms.form.Form.validate` + method, which will return ``True`` if the data validates, ``False`` + otherwise. +3. to access individual values from the form, access `form..data`. + +Forms in Templates +------------------ + +Now to the template side. When you pass the form to the templates, you can +easily render them there. Look at the following example template to see +how easy this is. WTForms does half the form generation for us already. +To make it even nicer, we can write a macro that renders a field with +label and a list of errors if there are any. + +Here's an example :file:`_formhelpers.html` template with such a macro: + +.. sourcecode:: html+jinja + + {% macro render_field(field) %} +

{{ field.label }} +
{{ field(**kwargs)|safe }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+ {% endmacro %} + +This macro accepts a couple of keyword arguments that are forwarded to +WTForm's field function, which renders the field for us. The keyword +arguments will be inserted as HTML attributes. So, for example, you can +call ``render_field(form.username, class='username')`` to add a class to +the input element. Note that WTForms returns standard Python strings, +so we have to tell Jinja2 that this data is already HTML-escaped with +the ``|safe`` filter. + +Here is the :file:`register.html` template for the function we used above, which +takes advantage of the :file:`_formhelpers.html` template: + +.. sourcecode:: html+jinja + + {% from "_formhelpers.html" import render_field %} + +
+ {{ render_field(form.username) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ render_field(form.confirm) }} + {{ render_field(form.accept_tos) }} +
+

+

+ +For more information about WTForms, head over to the `WTForms +website`_. + +.. _WTForms: https://wtforms.readthedocs.io/ +.. _WTForms website: https://wtforms.readthedocs.io/ diff --git a/test/fixtures/whole_applications/flask/docs/quickstart.rst b/test/fixtures/whole_applications/flask/docs/quickstart.rst new file mode 100644 index 0000000..0d7ad3f --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/quickstart.rst @@ -0,0 +1,907 @@ +Quickstart +========== + +Eager to get started? This page gives a good introduction to Flask. +Follow :doc:`installation` to set up a project and install Flask first. + + +A Minimal Application +--------------------- + +A minimal Flask application looks something like this: + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello_world(): + return "

Hello, World!

" + +So what did that code do? + +1. First we imported the :class:`~flask.Flask` class. An instance of + this class will be our WSGI application. +2. Next we create an instance of this class. The first argument is the + name of the application's module or package. ``__name__`` is a + convenient shortcut for this that is appropriate for most cases. + This is needed so that Flask knows where to look for resources such + as templates and static files. +3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask + what URL should trigger our function. +4. The function returns the message we want to display in the user's + browser. The default content type is HTML, so HTML in the string + will be rendered by the browser. + +Save it as :file:`hello.py` or something similar. Make sure to not call +your application :file:`flask.py` because this would conflict with Flask +itself. + +To run the application, use the ``flask`` command or +``python -m flask``. You need to tell the Flask where your application +is with the ``--app`` option. + +.. code-block:: text + + $ flask --app hello run + * Serving Flask app 'hello' + * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) + +.. admonition:: Application Discovery Behavior + + As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you + don't have to use ``--app``. See :doc:`/cli` for more details. + +This launches a very simple builtin server, which is good enough for +testing but probably not what you want to use in production. For +deployment options see :doc:`deploying/index`. + +Now head over to http://127.0.0.1:5000/, and you should see your hello +world greeting. + +If another program is already using port 5000, you'll see +``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the +server tries to start. See :ref:`address-already-in-use` for how to +handle that. + +.. _public-server: + +.. admonition:: Externally Visible Server + + If you run the server you will notice that the server is only accessible + from your own computer, not from any other in the network. This is the + default because in debugging mode a user of the application can execute + arbitrary Python code on your computer. + + If you have the debugger disabled or trust the users on your network, + you can make the server publicly available simply by adding + ``--host=0.0.0.0`` to the command line:: + + $ flask run --host=0.0.0.0 + + This tells your operating system to listen on all public IPs. + + +Debug Mode +---------- + +The ``flask run`` command can do more than just start the development +server. By enabling debug mode, the server will automatically reload if +code changes, and will show an interactive debugger in the browser if an +error occurs during a request. + +.. image:: _static/debugger.png + :align: center + :class: screenshot + :alt: The interactive debugger in action. + +.. warning:: + + The debugger allows executing arbitrary Python code from the + browser. It is protected by a pin, but still represents a major + security risk. Do not run the development server or debugger in a + production environment. + +To enable debug mode, use the ``--debug`` option. + +.. code-block:: text + + $ flask --app hello run --debug + * Serving Flask app 'hello' + * Debug mode: on + * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: nnn-nnn-nnn + +See also: + +- :doc:`/server` and :doc:`/cli` for information about running in debug mode. +- :doc:`/debugging` for information about using the built-in debugger + and other debuggers. +- :doc:`/logging` and :doc:`/errorhandling` to log errors and display + nice error pages. + + +HTML Escaping +------------- + +When returning HTML (the default response type in Flask), any +user-provided values rendered in the output must be escaped to protect +from injection attacks. HTML templates rendered with Jinja, introduced +later, will do this automatically. + +:func:`~markupsafe.escape`, shown here, can be used manually. It is +omitted in most examples for brevity, but you should always be aware of +how you're using untrusted data. + +.. code-block:: python + + from markupsafe import escape + + @app.route("/") + def hello(name): + return f"Hello, {escape(name)}!" + +If a user managed to submit the name ````, +escaping causes it to be rendered as text, rather than running the +script in the user's browser. + +```` in the route captures a value from the URL and passes it to +the view function. These variable rules are explained below. + + +Routing +------- + +Modern web applications use meaningful URLs to help users. Users are more +likely to like a page and come back if the page uses a meaningful URL they can +remember and use to directly visit a page. + +Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: + + @app.route('/') + def index(): + return 'Index Page' + + @app.route('/hello') + def hello(): + return 'Hello, World' + +You can do more! You can make parts of the URL dynamic and attach multiple +rules to a function. + +Variable Rules +`````````````` + +You can add variable sections to a URL by marking sections with +````. Your function then receives the ```` +as a keyword argument. Optionally, you can use a converter to specify the type +of the argument like ````. :: + + from markupsafe import escape + + @app.route('/user/') + def show_user_profile(username): + # show the user profile for that user + return f'User {escape(username)}' + + @app.route('/post/') + def show_post(post_id): + # show the post with the given id, the id is an integer + return f'Post {post_id}' + + @app.route('/path/') + def show_subpath(subpath): + # show the subpath after /path/ + return f'Subpath {escape(subpath)}' + +Converter types: + +========== ========================================== +``string`` (default) accepts any text without a slash +``int`` accepts positive integers +``float`` accepts positive floating point values +``path`` like ``string`` but also accepts slashes +``uuid`` accepts UUID strings +========== ========================================== + + +Unique URLs / Redirection Behavior +`````````````````````````````````` + +The following two rules differ in their use of a trailing slash. :: + + @app.route('/projects/') + def projects(): + return 'The project page' + + @app.route('/about') + def about(): + return 'The about page' + +The canonical URL for the ``projects`` endpoint has a trailing slash. +It's similar to a folder in a file system. If you access the URL without +a trailing slash (``/projects``), Flask redirects you to the canonical URL +with the trailing slash (``/projects/``). + +The canonical URL for the ``about`` endpoint does not have a trailing +slash. It's similar to the pathname of a file. Accessing the URL with a +trailing slash (``/about/``) produces a 404 "Not Found" error. This helps +keep URLs unique for these resources, which helps search engines avoid +indexing the same page twice. + + +.. _url-building: + +URL Building +```````````` + +To build a URL to a specific function, use the :func:`~flask.url_for` function. +It accepts the name of the function as its first argument and any number of +keyword arguments, each corresponding to a variable part of the URL rule. +Unknown variable parts are appended to the URL as query parameters. + +Why would you want to build URLs using the URL reversing function +:func:`~flask.url_for` instead of hard-coding them into your templates? + +1. Reversing is often more descriptive than hard-coding the URLs. +2. You can change your URLs in one go instead of needing to remember to + manually change hard-coded URLs. +3. URL building handles escaping of special characters transparently. +4. The generated paths are always absolute, avoiding unexpected behavior + of relative paths in browsers. +5. If your application is placed outside the URL root, for example, in + ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly + handles that for you. + +For example, here we use the :meth:`~flask.Flask.test_request_context` method +to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` +tells Flask to behave as though it's handling a request even while we use a +Python shell. See :ref:`context-locals`. + +.. code-block:: python + + from flask import url_for + + @app.route('/') + def index(): + return 'index' + + @app.route('/login') + def login(): + return 'login' + + @app.route('/user/') + def profile(username): + return f'{username}\'s profile' + + with app.test_request_context(): + print(url_for('index')) + print(url_for('login')) + print(url_for('login', next='/')) + print(url_for('profile', username='John Doe')) + +.. code-block:: text + + / + /login + /login?next=/ + /user/John%20Doe + + +HTTP Methods +```````````` + +Web applications use different HTTP methods when accessing URLs. You should +familiarize yourself with the HTTP methods as you work with Flask. By default, +a route only answers to ``GET`` requests. You can use the ``methods`` argument +of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. +:: + + from flask import request + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + return do_the_login() + else: + return show_the_login_form() + +The example above keeps all methods for the route within one function, +which can be useful if each part uses some common data. + +You can also separate views for different methods into different +functions. Flask provides a shortcut for decorating such routes with +:meth:`~flask.Flask.get`, :meth:`~flask.Flask.post`, etc. for each +common HTTP method. + +.. code-block:: python + + @app.get('/login') + def login_get(): + return show_the_login_form() + + @app.post('/login') + def login_post(): + return do_the_login() + +If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method +and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise, +``OPTIONS`` is automatically implemented for you. + +.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt + +Static Files +------------ + +Dynamic web applications also need static files. That's usually where +the CSS and JavaScript files are coming from. Ideally your web server is +configured to serve them for you, but during development Flask can do that +as well. Just create a folder called :file:`static` in your package or next to +your module and it will be available at ``/static`` on the application. + +To generate URLs for static files, use the special ``'static'`` endpoint name:: + + url_for('static', filename='style.css') + +The file has to be stored on the filesystem as :file:`static/style.css`. + +Rendering Templates +------------------- + +Generating HTML from within Python is not fun, and actually pretty +cumbersome because you have to do the HTML escaping on your own to keep +the application secure. Because of that Flask configures the `Jinja2 +`_ template engine for you automatically. + +Templates can be used to generate any type of text file. For web applications, you'll +primarily be generating HTML pages, but you can also generate markdown, plain text for +emails, and anything else. + +For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. + +.. _MDN Web Docs: https://developer.mozilla.org/ + +To render a template you can use the :func:`~flask.render_template` +method. All you have to do is provide the name of the template and the +variables you want to pass to the template engine as keyword arguments. +Here's a simple example of how to render a template:: + + from flask import render_template + + @app.route('/hello/') + @app.route('/hello/') + def hello(name=None): + return render_template('hello.html', name=name) + +Flask will look for templates in the :file:`templates` folder. So if your +application is a module, this folder is next to that module, if it's a +package it's actually inside your package: + +**Case 1**: a module:: + + /application.py + /templates + /hello.html + +**Case 2**: a package:: + + /application + /__init__.py + /templates + /hello.html + +For templates you can use the full power of Jinja2 templates. Head over +to the official `Jinja2 Template Documentation +`_ for more information. + +Here is an example template: + +.. sourcecode:: html+jinja + + + Hello from Flask + {% if name %} +

Hello {{ name }}!

+ {% else %} +

Hello, World!

+ {% endif %} + +Inside templates you also have access to the :data:`~flask.Flask.config`, +:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects +as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions. + +Templates are especially useful if inheritance is used. If you want to +know how that works, see :doc:`patterns/templateinheritance`. Basically +template inheritance makes it possible to keep certain elements on each +page (like header, navigation and footer). + +Automatic escaping is enabled, so if ``name`` contains HTML it will be escaped +automatically. If you can trust a variable and you know that it will be +safe HTML (for example because it came from a module that converts wiki +markup to HTML) you can mark it as safe by using the +:class:`~markupsafe.Markup` class or by using the ``|safe`` filter in the +template. Head over to the Jinja 2 documentation for more examples. + +Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:: + + >>> from markupsafe import Markup + >>> Markup('Hello %s!') % 'hacker' + Markup('Hello <blink>hacker</blink>!') + >>> Markup.escape('hacker') + Markup('<blink>hacker</blink>') + >>> Markup('Marked up » HTML').striptags() + 'Marked up » HTML' + +.. versionchanged:: 0.5 + + Autoescaping is no longer enabled for all templates. The following + extensions for templates trigger autoescaping: ``.html``, ``.htm``, + ``.xml``, ``.xhtml``. Templates loaded from a string will have + autoescaping disabled. + +.. [#] Unsure what that :class:`~flask.g` object is? It's something in which + you can store information for your own needs. See the documentation + for :class:`flask.g` and :doc:`patterns/sqlite3`. + + +Accessing Request Data +---------------------- + +For web applications it's crucial to react to the data a client sends to +the server. In Flask this information is provided by the global +:class:`~flask.request` object. If you have some experience with Python +you might be wondering how that object can be global and how Flask +manages to still be threadsafe. The answer is context locals: + + +.. _context-locals: + +Context Locals +`````````````` + +.. admonition:: Insider Information + + If you want to understand how that works and how you can implement + tests with context locals, read this section, otherwise just skip it. + +Certain objects in Flask are global objects, but not of the usual kind. +These objects are actually proxies to objects that are local to a specific +context. What a mouthful. But that is actually quite easy to understand. + +Imagine the context being the handling thread. A request comes in and the +web server decides to spawn a new thread (or something else, the +underlying object is capable of dealing with concurrency systems other +than threads). When Flask starts its internal request handling it +figures out that the current thread is the active context and binds the +current application and the WSGI environments to that context (thread). +It does that in an intelligent way so that one application can invoke another +application without breaking. + +So what does this mean to you? Basically you can completely ignore that +this is the case unless you are doing something like unit testing. You +will notice that code which depends on a request object will suddenly break +because there is no request object. The solution is creating a request +object yourself and binding it to the context. The easiest solution for +unit testing is to use the :meth:`~flask.Flask.test_request_context` +context manager. In combination with the ``with`` statement it will bind a +test request so that you can interact with it. Here is an example:: + + from flask import request + + with app.test_request_context('/hello', method='POST'): + # now you can do something with the request until the + # end of the with block, such as basic assertions: + assert request.path == '/hello' + assert request.method == 'POST' + +The other possibility is passing a whole WSGI environment to the +:meth:`~flask.Flask.request_context` method:: + + with app.request_context(environ): + assert request.method == 'POST' + +The Request Object +`````````````````` + +The request object is documented in the API section and we will not cover +it here in detail (see :class:`~flask.Request`). Here is a broad overview of +some of the most common operations. First of all you have to import it from +the ``flask`` module:: + + from flask import request + +The current request method is available by using the +:attr:`~flask.Request.method` attribute. To access form data (data +transmitted in a ``POST`` or ``PUT`` request) you can use the +:attr:`~flask.Request.form` attribute. Here is a full example of the two +attributes mentioned above:: + + @app.route('/login', methods=['POST', 'GET']) + def login(): + error = None + if request.method == 'POST': + if valid_login(request.form['username'], + request.form['password']): + return log_the_user_in(request.form['username']) + else: + error = 'Invalid username/password' + # the code below is executed if the request method + # was GET or the credentials were invalid + return render_template('login.html', error=error) + +What happens if the key does not exist in the ``form`` attribute? In that +case a special :exc:`KeyError` is raised. You can catch it like a +standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request +error page is shown instead. So for many situations you don't have to +deal with that problem. + +To access parameters submitted in the URL (``?key=value``) you can use the +:attr:`~flask.Request.args` attribute:: + + searchword = request.args.get('key', '') + +We recommend accessing URL parameters with `get` or by catching the +:exc:`KeyError` because users might change the URL and presenting them a 400 +bad request page in that case is not user friendly. + +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.Request` documentation. + + +File Uploads +```````````` + +You can handle uploaded files with Flask easily. Just make sure not to +forget to set the ``enctype="multipart/form-data"`` attribute on your HTML +form, otherwise the browser will not transmit your files at all. + +Uploaded files are stored in memory or at a temporary location on the +filesystem. You can access those files by looking at the +:attr:`~flask.request.files` attribute on the request object. Each +uploaded file is stored in that dictionary. It behaves just like a +standard Python :class:`file` object, but it also has a +:meth:`~werkzeug.datastructures.FileStorage.save` method that +allows you to store that file on the filesystem of the server. +Here is a simple example showing how that works:: + + from flask import request + + @app.route('/upload', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + f = request.files['the_file'] + f.save('/var/www/uploads/uploaded_file.txt') + ... + +If you want to know how the file was named on the client before it was +uploaded to your application, you can access the +:attr:`~werkzeug.datastructures.FileStorage.filename` attribute. +However please keep in mind that this value can be forged +so never ever trust that value. If you want to use the filename +of the client to store the file on the server, pass it through the +:func:`~werkzeug.utils.secure_filename` function that +Werkzeug provides for you:: + + from werkzeug.utils import secure_filename + + @app.route('/upload', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + file = request.files['the_file'] + file.save(f"/var/www/uploads/{secure_filename(file.filename)}") + ... + +For some better examples, see :doc:`patterns/fileuploads`. + +Cookies +``````` + +To access cookies you can use the :attr:`~flask.Request.cookies` +attribute. To set cookies you can use the +:attr:`~flask.Response.set_cookie` method of response objects. The +:attr:`~flask.Request.cookies` attribute of request objects is a +dictionary with all the cookies the client transmits. If you want to use +sessions, do not use the cookies directly but instead use the +:ref:`sessions` in Flask that add some security on top of cookies for you. + +Reading cookies:: + + from flask import request + + @app.route('/') + def index(): + username = request.cookies.get('username') + # use cookies.get(key) instead of cookies[key] to not get a + # KeyError if the cookie is missing. + +Storing cookies:: + + from flask import make_response + + @app.route('/') + def index(): + resp = make_response(render_template(...)) + resp.set_cookie('username', 'the username') + return resp + +Note that cookies are set on response objects. Since you normally +just return strings from the view functions Flask will convert them into +response objects for you. If you explicitly want to do that you can use +the :meth:`~flask.make_response` function and then modify it. + +Sometimes you might want to set a cookie at a point where the response +object does not exist yet. This is possible by utilizing the +:doc:`patterns/deferredcallbacks` pattern. + +For this also see :ref:`about-responses`. + +Redirects and Errors +-------------------- + +To redirect a user to another endpoint, use the :func:`~flask.redirect` +function; to abort a request early with an error code, use the +:func:`~flask.abort` function:: + + from flask import abort, redirect, url_for + + @app.route('/') + def index(): + return redirect(url_for('login')) + + @app.route('/login') + def login(): + abort(401) + this_is_never_executed() + +This is a rather pointless example because a user will be redirected from +the index to a page they cannot access (401 means access denied) but it +shows how that works. + +By default a black and white error page is shown for each error code. If +you want to customize the error page, you can use the +:meth:`~flask.Flask.errorhandler` decorator:: + + from flask import render_template + + @app.errorhandler(404) + def page_not_found(error): + return render_template('page_not_found.html'), 404 + +Note the ``404`` after the :func:`~flask.render_template` call. This +tells Flask that the status code of that page should be 404 which means +not found. By default 200 is assumed which translates to: all went well. + +See :doc:`errorhandling` for more details. + +.. _about-responses: + +About Responses +--------------- + +The return value from a view function is automatically converted into +a response object for you. If the return value is a string it's +converted into a response object with the string as response body, a +``200 OK`` status code and a :mimetype:`text/html` mimetype. If the +return value is a dict or list, :func:`jsonify` is called to produce a +response. The logic that Flask applies to converting return values into +response objects is as follows: + +1. If a response object of the correct type is returned it's directly + returned from the view. +2. If it's a string, a response object is created with that data and + the default parameters. +3. If it's an iterator or generator returning strings or bytes, it is + treated as a streaming response. +4. If it's a dict or list, a response object is created using + :func:`~flask.json.jsonify`. +5. If a tuple is returned the items in the tuple can provide extra + information. Such tuples have to be in the form + ``(response, status)``, ``(response, headers)``, or + ``(response, status, headers)``. The ``status`` value will override + the status code and ``headers`` can be a list or dictionary of + additional header values. +6. If none of that works, Flask will assume the return value is a + valid WSGI application and convert that into a response object. + +If you want to get hold of the resulting response object inside the view +you can use the :func:`~flask.make_response` function. + +Imagine you have a view like this:: + + from flask import render_template + + @app.errorhandler(404) + def not_found(error): + return render_template('error.html'), 404 + +You just need to wrap the return expression with +:func:`~flask.make_response` and get the response object to modify it, then +return it:: + + from flask import make_response + + @app.errorhandler(404) + def not_found(error): + resp = make_response(render_template('error.html'), 404) + resp.headers['X-Something'] = 'A value' + return resp + + +APIs with JSON +`````````````` + +A common response format when writing an API is JSON. It's easy to get +started writing such an API with Flask. If you return a ``dict`` or +``list`` from a view, it will be converted to a JSON response. + +.. code-block:: python + + @app.route("/me") + def me_api(): + user = get_current_user() + return { + "username": user.username, + "theme": user.theme, + "image": url_for("user_image", filename=user.image), + } + + @app.route("/users") + def users_api(): + users = get_all_users() + return [user.to_json() for user in users] + +This is a shortcut to passing the data to the +:func:`~flask.json.jsonify` function, which will serialize any supported +JSON data type. That means that all the data in the dict or list must be +JSON serializable. + +For complex types such as database models, you'll want to use a +serialization library to convert the data to valid JSON types first. +There are many serialization libraries and Flask API extensions +maintained by the community that support more complex applications. + + +.. _sessions: + +Sessions +-------- + +In addition to the request object there is also a second object called +:class:`~flask.session` which allows you to store information specific to a +user from one request to the next. This is implemented on top of cookies +for you and signs the cookies cryptographically. What this means is that +the user could look at the contents of your cookie but not modify it, +unless they know the secret key used for signing. + +In order to use sessions you have to set a secret key. Here is how +sessions work:: + + from flask import session + + # Set the secret key to some random bytes. Keep this really secret! + app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + + @app.route('/') + def index(): + if 'username' in session: + return f'Logged in as {session["username"]}' + return 'You are not logged in' + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + session['username'] = request.form['username'] + return redirect(url_for('index')) + return ''' +
+

+

+

+ ''' + + @app.route('/logout') + def logout(): + # remove the username from the session if it's there + session.pop('username', None) + return redirect(url_for('index')) + +.. admonition:: How to generate good secret keys + + A secret key should be as random as possible. Your operating system has + ways to generate pretty random data based on a cryptographic random + generator. Use the following command to quickly generate a value for + :attr:`Flask.secret_key` (or :data:`SECRET_KEY`):: + + $ python -c 'import secrets; print(secrets.token_hex())' + '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +A note on cookie-based sessions: Flask will take the values you put into the +session object and serialize them into a cookie. If you are finding some +values do not persist across requests, cookies are indeed enabled, and you are +not getting a clear error message, check the size of the cookie in your page +responses compared to the size supported by web browsers. + +Besides the default client-side based sessions, if you want to handle +sessions on the server-side instead, there are several +Flask extensions that support this. + +Message Flashing +---------------- + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback they will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it on the next +(and only the next) request. This is usually combined with a layout +template to expose the message. + +To flash a message use the :func:`~flask.flash` method, to get hold of the +messages you can use :func:`~flask.get_flashed_messages` which is also +available in the templates. See :doc:`patterns/flashing` for a full +example. + +Logging +------- + +.. versionadded:: 0.3 + +Sometimes you might be in a situation where you deal with data that +should be correct, but actually is not. For example you may have +some client-side code that sends an HTTP request to the server +but it's obviously malformed. This might be caused by a user tampering +with the data, or the client code failing. Most of the time it's okay +to reply with ``400 Bad Request`` in that situation, but sometimes +that won't do and the code has to continue working. + +You may still want to log that something fishy happened. This is where +loggers come in handy. As of Flask 0.3 a logger is preconfigured for you +to use. + +Here are some example log calls:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning occurred (%d apples)', 42) + app.logger.error('An error occurred') + +The attached :attr:`~flask.Flask.logger` is a standard logging +:class:`~logging.Logger`, so head over to the official :mod:`logging` +docs for more information. + +See :doc:`errorhandling`. + + +Hooking in WSGI Middleware +-------------------------- + +To add WSGI middleware to your Flask application, wrap the application's +``wsgi_app`` attribute. For example, to apply Werkzeug's +:class:`~werkzeug.middleware.proxy_fix.ProxyFix` middleware for running +behind Nginx: + +.. code-block:: python + + from werkzeug.middleware.proxy_fix import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app) + +Wrapping ``app.wsgi_app`` instead of ``app`` means that ``app`` still +points at your Flask application, not at the middleware, so you can +continue to use and configure ``app`` directly. + +Using Flask Extensions +---------------------- + +Extensions are packages that help you accomplish common tasks. For +example, Flask-SQLAlchemy provides SQLAlchemy support that makes it simple +and easy to use with Flask. + +For more on Flask extensions, see :doc:`extensions`. + +Deploying to a Web Server +------------------------- + +Ready to deploy your new Flask app? See :doc:`deploying/index`. diff --git a/test/fixtures/whole_applications/flask/docs/reqcontext.rst b/test/fixtures/whole_applications/flask/docs/reqcontext.rst new file mode 100644 index 0000000..4f1846a --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/reqcontext.rst @@ -0,0 +1,243 @@ +.. currentmodule:: flask + +The Request Context +=================== + +The request context keeps track of the request-level data during a +request. Rather than passing the request object to each function that +runs during a request, the :data:`request` and :data:`session` proxies +are accessed instead. + +This is similar to :doc:`/appcontext`, which keeps track of the +application-level data independent of a request. A corresponding +application context is pushed when a request context is pushed. + + +Purpose of the Context +---------------------- + +When the :class:`Flask` application handles a request, it creates a +:class:`Request` object based on the environment it received from the +WSGI server. Because a *worker* (thread, process, or coroutine depending +on the server) handles only one request at a time, the request data can +be considered global to that worker during that request. Flask uses the +term *context local* for this. + +Flask automatically *pushes* a request context when handling a request. +View functions, error handlers, and other functions that run during a +request will have access to the :data:`request` proxy, which points to +the request object for the current request. + + +Lifetime of the Context +----------------------- + +When a Flask application begins handling a request, it pushes a request +context, which also pushes an :doc:`app context `. When the +request ends it pops the request context then the application context. + +The context is unique to each thread (or other worker type). +:data:`request` cannot be passed to another thread, the other thread has +a different context space and will not know about the request the parent +thread was pointing to. + +Context locals are implemented using Python's :mod:`contextvars` and +Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the +lifetime of context vars automatically, and local proxy wraps that +low-level interface to make the data easier to work with. + + +Manually Push a Context +----------------------- + +If you try to access :data:`request`, or anything that uses it, outside +a request context, you'll get this error message: + +.. code-block:: pytb + + RuntimeError: Working outside of request context. + + This typically means that you attempted to use functionality that + needed an active HTTP request. Consult the documentation on testing + for information about how to avoid this problem. + +This should typically only happen when testing code that expects an +active request. One option is to use the +:meth:`test client ` to simulate a full request. Or +you can use :meth:`~Flask.test_request_context` in a ``with`` block, and +everything that runs in the block will have access to :data:`request`, +populated with your test data. :: + + def generate_report(year): + format = request.args.get("format") + ... + + with app.test_request_context( + "/make_report/2017", query_string={"format": "short"} + ): + generate_report() + +If you see that error somewhere else in your code not related to +testing, it most likely indicates that you should move that code into a +view function. + +For information on how to use the request context from the interactive +Python shell, see :doc:`/shell`. + + +How the Context Works +--------------------- + +The :meth:`Flask.wsgi_app` method is called to handle each request. It +manages the contexts during the request. Internally, the request and +application contexts work like stacks. When contexts are pushed, the +proxies that depend on them are available and point at information from +the top item. + +When the request starts, a :class:`~ctx.RequestContext` is created and +pushed, which creates and pushes an :class:`~ctx.AppContext` first if +a context for that application is not already the top context. While +these contexts are pushed, the :data:`current_app`, :data:`g`, +:data:`request`, and :data:`session` proxies are available to the +original thread handling the request. + +Other contexts may be pushed to change the proxies during a request. +While this is not a common pattern, it can be used in advanced +applications to, for example, do internal redirects or chain different +applications together. + +After the request is dispatched and a response is generated and sent, +the request context is popped, which then pops the application context. +Immediately before they are popped, the :meth:`~Flask.teardown_request` +and :meth:`~Flask.teardown_appcontext` functions are executed. These +execute even if an unhandled exception occurred during dispatch. + + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +Flask dispatches a request in multiple stages which can affect the +request, response, and how errors are handled. The contexts are active +during all of these stages. + +A :class:`Blueprint` can add handlers for these events that are specific +to the blueprint. The handlers for a blueprint will run if the blueprint +owns the route that matches the request. + +#. Before each request, :meth:`~Flask.before_request` functions are + called. If one of these functions return a value, the other + functions are skipped. The return value is treated as the response + and the view function is not called. + +#. If the :meth:`~Flask.before_request` functions did not return a + response, the view function for the matched route is called and + returns a response. + +#. The return value of the view is converted into an actual response + object and passed to the :meth:`~Flask.after_request` + functions. Each function returns a modified or new response object. + +#. After the response is returned, the contexts are popped, which calls + the :meth:`~Flask.teardown_request` and + :meth:`~Flask.teardown_appcontext` functions. These functions are + called even if an unhandled exception was raised at any point above. + +If an exception is raised before the teardown functions, Flask tries to +match it with an :meth:`~Flask.errorhandler` function to handle the +exception and return a response. If no error handler is found, or the +handler itself raises an exception, Flask returns a generic +``500 Internal Server Error`` response. The teardown functions are still +called, and are passed the exception object. + +If debug mode is enabled, unhandled exceptions are not converted to a +``500`` response and instead are propagated to the WSGI server. This +allows the development server to present the interactive debugger with +the traceback. + + +Teardown Callbacks +~~~~~~~~~~~~~~~~~~ + +The teardown callbacks are independent of the request dispatch, and are +instead called by the contexts when they are popped. The functions are +called even if there is an unhandled exception during dispatch, and for +manually pushed contexts. This means there is no guarantee that any +other parts of the request dispatch have run first. Be sure to write +these functions in a way that does not depend on other callbacks and +will not fail. + +During testing, it can be useful to defer popping the contexts after the +request ends, so that their data can be accessed in the test function. +Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the +contexts until the ``with`` block exits. + +.. code-block:: python + + from flask import Flask, request + + app = Flask(__name__) + + @app.route('/') + def hello(): + print('during view') + return 'Hello, World!' + + @app.teardown_request + def show_teardown(exception): + print('after with block') + + with app.test_request_context(): + print('during with block') + + # teardown functions are called after the context with block exits + + with app.test_client() as client: + client.get('/') + # the contexts are not popped even though the request ended + print(request.path) + + # the contexts are popped and teardown functions are called after + # the client with block exits + +Signals +~~~~~~~ + +The following signals are sent: + +#. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions + are called. +#. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions + are called. +#. :data:`got_request_exception` is sent when an exception begins to be handled, but + before an :meth:`~Flask.errorhandler` is looked up or called. +#. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request` + functions are called. + + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +proxies are accessed in the same way for each worker thread, but +point to the unique object bound to each worker behind the scenes as +described on this page. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is actually a proxy: + +- The proxy objects cannot fake their type as the actual object types. + If you want to perform instance checks, you have to do that on the + object being proxied. +- The reference to the proxied object is needed in some situations, + such as sending :doc:`signals` or passing data to a background + thread. + +If you need to access the underlying object that is proxied, use the +:meth:`~werkzeug.local.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) diff --git a/test/fixtures/whole_applications/flask/docs/server.rst b/test/fixtures/whole_applications/flask/docs/server.rst new file mode 100644 index 0000000..11e976b --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/server.rst @@ -0,0 +1,115 @@ +.. currentmodule:: flask + +Development Server +================== + +Flask provides a ``run`` command to run the application with a development server. In +debug mode, this server provides an interactive debugger and will reload when code is +changed. + +.. warning:: + + Do not use the development server when deploying to production. It + is intended for use only during local development. It is not + designed to be particularly efficient, stable, or secure. + + See :doc:`/deploying/index` for deployment options. + +Command Line +------------ + +The ``flask run`` CLI command is the recommended way to run the development server. Use +the ``--app`` option to point to your application, and the ``--debug`` option to enable +debug mode. + +.. code-block:: text + + $ flask --app hello run --debug + +This enables debug mode, including the interactive debugger and reloader, and then +starts the server on http://localhost:5000/. Use ``flask run --help`` to see the +available options, and :doc:`/cli` for detailed instructions about configuring and using +the CLI. + + +.. _address-already-in-use: + +Address already in use +~~~~~~~~~~~~~~~~~~~~~~ + +If another program is already using port 5000, you'll see an ``OSError`` +when the server tries to start. It may have one of the following +messages: + +- ``OSError: [Errno 98] Address already in use`` +- ``OSError: [WinError 10013] An attempt was made to access a socket + in a way forbidden by its access permissions`` + +Either identify and stop the other program, or use +``flask run --port 5001`` to pick a different port. + +You can use ``netstat`` or ``lsof`` to identify what process id is using +a port, then use other operating system tools stop that process. The +following example shows that process id 6847 is using port 5000. + +.. tabs:: + + .. tab:: ``netstat`` (Linux) + + .. code-block:: text + + $ netstat -nlp | grep 5000 + tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python + + .. tab:: ``lsof`` (macOS / Linux) + + .. code-block:: text + + $ lsof -P -i :5000 + Python 6847 IPv4 TCP localhost:5000 (LISTEN) + + .. tab:: ``netstat`` (Windows) + + .. code-block:: text + + > netstat -ano | findstr 5000 + TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847 + +macOS Monterey and later automatically starts a service that uses port +5000. You can choose to disable this service instead of using a different port by +searching for "AirPlay Receiver" in System Preferences and toggling it off. + + +Deferred Errors on Reload +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the ``flask run`` command with the reloader, the server will +continue to run even if you introduce syntax errors or other +initialization errors into the code. Accessing the site will show the +interactive debugger for the error, rather than crashing the server. + +If a syntax error is already present when calling ``flask run``, it will +fail immediately and show the traceback rather than waiting until the +site is accessed. This is intended to make errors more visible initially +while still allowing the server to handle errors on reload. + + +In Code +------- + +The development server can also be started from Python with the :meth:`Flask.run` +method. This method takes arguments similar to the CLI options to control the server. +The main difference from the CLI command is that the server will crash if there are +errors when reloading. ``debug=True`` can be passed to enable debug mode. + +Place the call in a main block, otherwise it will interfere when trying to import and +run the application with a production server later. + +.. code-block:: python + + if __name__ == "__main__": + app.run(debug=True) + +.. code-block:: text + + $ python hello.py diff --git a/test/fixtures/whole_applications/flask/docs/shell.rst b/test/fixtures/whole_applications/flask/docs/shell.rst new file mode 100644 index 0000000..7e42e28 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/shell.rst @@ -0,0 +1,100 @@ +Working with the Shell +====================== + +.. versionadded:: 0.3 + +One of the reasons everybody loves Python is the interactive shell. It +basically allows you to execute Python commands in real time and +immediately get results back. Flask itself does not come with an +interactive shell, because it does not require any specific setup upfront, +just import your application and start playing around. + +There are however some handy helpers to make playing around in the shell a +more pleasant experience. The main issue with interactive console +sessions is that you're not triggering a request like a browser does which +means that :data:`~flask.g`, :data:`~flask.request` and others are not +available. But the code you want to test might depend on them, so what +can you do? + +This is where some helper functions come in handy. Keep in mind however +that these functions are not only there for interactive shell usage, but +also for unit testing and other situations that require a faked request +context. + +Generally it's recommended that you read :doc:`reqcontext` first. + +Command Line Interface +---------------------- + +Starting with Flask 0.11 the recommended way to work with the shell is the +``flask shell`` command which does a lot of this automatically for you. +For instance the shell is automatically initialized with a loaded +application context. + +For more information see :doc:`/cli`. + +Creating a Request Context +-------------------------- + +The easiest way to create a proper request context from the shell is by +using the :attr:`~flask.Flask.test_request_context` method which creates +us a :class:`~flask.ctx.RequestContext`: + +>>> ctx = app.test_request_context() + +Normally you would use the ``with`` statement to make this request object +active, but in the shell it's easier to use the +:meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods by hand: + +>>> ctx.push() + +From that point onwards you can work with the request object until you +call `pop`: + +>>> ctx.pop() + +Firing Before/After Request +--------------------------- + +By just creating a request context, you still don't have run the code that +is normally run before a request. This might result in your database +being unavailable if you are connecting to the database in a +before-request callback or the current user not being stored on the +:data:`~flask.g` object etc. + +This however can easily be done yourself. Just call +:meth:`~flask.Flask.preprocess_request`: + +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> app.preprocess_request() + +Keep in mind that the :meth:`~flask.Flask.preprocess_request` function +might return a response object, in that case just ignore it. + +To shutdown a request, you need to trick a bit before the after request +functions (triggered by :meth:`~flask.Flask.process_response`) operate on +a response object: + +>>> app.process_response(app.response_class()) + +>>> ctx.pop() + +The functions registered as :meth:`~flask.Flask.teardown_request` are +automatically called when the context is popped. So this is the perfect +place to automatically tear down resources that were needed by the request +context (such as database connections). + + +Further Improving the Shell Experience +-------------------------------------- + +If you like the idea of experimenting in a shell, create yourself a module +with stuff you want to star import into your interactive session. There +you could also define some more helper methods for common things such as +initializing the database, dropping tables etc. + +Just put them into a module (like `shelltools`) and import from there: + +>>> from shelltools import * diff --git a/test/fixtures/whole_applications/flask/docs/signals.rst b/test/fixtures/whole_applications/flask/docs/signals.rst new file mode 100644 index 0000000..739bb0b --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/signals.rst @@ -0,0 +1,167 @@ +Signals +======= + +Signals are a lightweight way to notify subscribers of certain events during the +lifecycle of the application and each request. When an event occurs, it emits the +signal, which calls each subscriber. + +Signals are implemented by the `Blinker`_ library. See its documentation for detailed +information. Flask provides some built-in signals. Extensions may provide their own. + +Many signals mirror Flask's decorator-based callbacks with similar names. For example, +the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` +decorator. The advantage of signals over handlers is that they can be subscribed to +temporarily, and can't directly affect the application. This is useful for testing, +metrics, auditing, and more. For example, if you want to know what templates were +rendered at what parts of what requests, there is a signal that will notify you of that +information. + + +Core Signals +------------ + +See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` +page also describes the order that signals and decorators execute. + + +Subscribing to Signals +---------------------- + +To subscribe to a signal, you can use the +:meth:`~blinker.base.Signal.connect` method of a signal. The first +argument is the function that should be called when the signal is emitted, +the optional second argument specifies a sender. To unsubscribe from a +signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. + +For all core Flask signals, the sender is the application that issued the +signal. When you subscribe to a signal, be sure to also provide a sender +unless you really want to listen for signals from all applications. This is +especially true if you are developing an extension. + +For example, here is a helper context manager that can be used in a unit test +to determine which templates were rendered and what variables were passed +to the template:: + + from flask import template_rendered + from contextlib import contextmanager + + @contextmanager + def captured_templates(app): + recorded = [] + def record(sender, template, context, **extra): + recorded.append((template, context)) + template_rendered.connect(record, app) + try: + yield recorded + finally: + template_rendered.disconnect(record, app) + +This can now easily be paired with a test client:: + + with captured_templates(app) as templates: + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert len(templates) == 1 + template, context = templates[0] + assert template.name == 'index.html' + assert len(context['items']) == 10 + +Make sure to subscribe with an extra ``**extra`` argument so that your +calls don't fail if Flask introduces new arguments to the signals. + +All the template rendering in the code issued by the application `app` +in the body of the ``with`` block will now be recorded in the `templates` +variable. Whenever a template is rendered, the template object as well as +context are appended to it. + +Additionally there is a convenient helper method +(:meth:`~blinker.base.Signal.connected_to`) that allows you to +temporarily subscribe a function to a signal with a context manager on +its own. Because the return value of the context manager cannot be +specified that way, you have to pass the list in as an argument:: + + from flask import template_rendered + + def captured_templates(app, recorded, **extra): + def record(sender, template, context): + recorded.append((template, context)) + return template_rendered.connected_to(record, app) + +The example above would then look like this:: + + templates = [] + with captured_templates(app, templates, **extra): + ... + template, context = templates[0] + +Creating Signals +---------------- + +If you want to use signals in your own application, you can use the +blinker library directly. The most common use case are named signals in a +custom :class:`~blinker.base.Namespace`. This is what is recommended +most of the time:: + + from blinker import Namespace + my_signals = Namespace() + +Now you can create new signals like this:: + + model_saved = my_signals.signal('model-saved') + +The name for the signal here makes it unique and also simplifies +debugging. You can access the name of the signal with the +:attr:`~blinker.base.NamedSignal.name` attribute. + +.. _signals-sending: + +Sending Signals +--------------- + +If you want to emit a signal, you can do so by calling the +:meth:`~blinker.base.Signal.send` method. It accepts a sender as first +argument and optionally some keyword arguments that are forwarded to the +signal subscribers:: + + class Model(object): + ... + + def save(self): + model_saved.send(self) + +Try to always pick a good sender. If you have a class that is emitting a +signal, pass ``self`` as sender. If you are emitting a signal from a random +function, you can pass ``current_app._get_current_object()`` as sender. + +.. admonition:: Passing Proxies as Senders + + Never pass :data:`~flask.current_app` as sender to a signal. Use + ``current_app._get_current_object()`` instead. The reason for this is + that :data:`~flask.current_app` is a proxy and not the real application + object. + + +Signals and Flask's Request Context +----------------------------------- + +Signals fully support :doc:`reqcontext` when receiving signals. +Context-local variables are consistently available between +:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can +rely on :class:`flask.g` and others as needed. Note the limitations described +in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. + + +Decorator Based Signal Subscriptions +------------------------------------ + +You can also easily subscribe to signals by using the +:meth:`~blinker.base.NamedSignal.connect_via` decorator:: + + from flask import template_rendered + + @template_rendered.connect_via(app) + def when_template_rendered(sender, template, context, **extra): + print(f'Template {template.name} is rendered with {context}') + + +.. _blinker: https://pypi.org/project/blinker/ diff --git a/test/fixtures/whole_applications/flask/docs/templating.rst b/test/fixtures/whole_applications/flask/docs/templating.rst new file mode 100644 index 0000000..23cfee4 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/templating.rst @@ -0,0 +1,229 @@ +Templates +========= + +Flask leverages Jinja2 as its template engine. You are obviously free to use +a different template engine, but you still have to install Jinja2 to run +Flask itself. This requirement is necessary to enable rich extensions. +An extension can depend on Jinja2 being present. + +This section only gives a very quick introduction into how Jinja2 +is integrated into Flask. If you want information on the template +engine's syntax itself, head over to the official `Jinja2 Template +Documentation `_ for +more information. + +Jinja Setup +----------- + +Unless customized, Jinja2 is configured by Flask as follows: + +- autoescaping is enabled for all templates ending in ``.html``, + ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using + :func:`~flask.templating.render_template`. +- autoescaping is enabled for all strings when using + :func:`~flask.templating.render_template_string`. +- a template has the ability to opt in/out autoescaping with the + ``{% autoescape %}`` tag. +- Flask inserts a couple of global functions and helpers into the + Jinja2 context, additionally to the values that are present by + default. + +Standard Context +---------------- + +The following global variables are available within Jinja2 templates +by default: + +.. data:: config + :noindex: + + The current configuration object (:data:`flask.Flask.config`) + + .. versionadded:: 0.6 + + .. versionchanged:: 0.10 + This is now always available, even in imported templates. + +.. data:: request + :noindex: + + The current request object (:class:`flask.request`). This variable is + unavailable if the template was rendered without an active request + context. + +.. data:: session + :noindex: + + The current session object (:class:`flask.session`). This variable + is unavailable if the template was rendered without an active request + context. + +.. data:: g + :noindex: + + The request-bound object for global variables (:data:`flask.g`). This + variable is unavailable if the template was rendered without an active + request context. + +.. function:: url_for + :noindex: + + The :func:`flask.url_for` function. + +.. function:: get_flashed_messages + :noindex: + + The :func:`flask.get_flashed_messages` function. + +.. admonition:: The Jinja Context Behavior + + These variables are added to the context of variables, they are not + global variables. The difference is that by default these will not + show up in the context of imported templates. This is partially caused + by performance considerations, partially to keep things explicit. + + What does this mean for you? If you have a macro you want to import, + that needs to access the request object you have two possibilities: + + 1. you explicitly pass the request to the macro as parameter, or + the attribute of the request object you are interested in. + 2. you import the macro "with context". + + Importing with context looks like this: + + .. sourcecode:: jinja + + {% from '_helpers.html' import my_macro with context %} + + +Controlling Autoescaping +------------------------ + +Autoescaping is the concept of automatically escaping special characters +for you. Special characters in the sense of HTML (or XML, and thus XHTML) +are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters +carry specific meanings in documents on their own you have to replace them +by so called "entities" if you want to use them for text. Not doing so +would not only cause user frustration by the inability to use these +characters in text, but can also lead to security problems. (see +:ref:`security-xss`) + +Sometimes however you will need to disable autoescaping in templates. +This can be the case if you want to explicitly inject HTML into pages, for +example if they come from a system that generates secure HTML like a +markdown to HTML converter. + +There are three ways to accomplish that: + +- In the Python code, wrap the HTML string in a :class:`~markupsafe.Markup` + object before passing it to the template. This is in general the + recommended way. +- Inside the template, use the ``|safe`` filter to explicitly mark a + string as safe HTML (``{{ myvariable|safe }}``) +- Temporarily disable the autoescape system altogether. + +To disable the autoescape system in templates, you can use the ``{% +autoescape %}`` block: + +.. sourcecode:: html+jinja + + {% autoescape false %} +

autoescaping is disabled here +

{{ will_not_be_escaped }} + {% endautoescape %} + +Whenever you do this, please be very cautious about the variables you are +using in this block. + +.. _registering-filters: + +Registering Filters +------------------- + +If you want to register your own filters in Jinja2 you have two ways to do +that. You can either put them by hand into the +:attr:`~flask.Flask.jinja_env` of the application or use the +:meth:`~flask.Flask.template_filter` decorator. + +The two following examples work the same and both reverse an object:: + + @app.template_filter('reverse') + def reverse_filter(s): + return s[::-1] + + def reverse_filter(s): + return s[::-1] + app.jinja_env.filters['reverse'] = reverse_filter + +In case of the decorator the argument is optional if you want to use the +function name as name of the filter. Once registered, you can use the filter +in your templates in the same way as Jinja2's builtin filters, for example if +you have a Python list in context called `mylist`:: + + {% for x in mylist | reverse %} + {% endfor %} + + +Context Processors +------------------ + +To inject new variables automatically into the context of a template, +context processors exist in Flask. Context processors run before the +template is rendered and have the ability to inject new values into the +template context. A context processor is a function that returns a +dictionary. The keys and values of this dictionary are then merged with +the template context, for all templates in the app:: + + @app.context_processor + def inject_user(): + return dict(user=g.user) + +The context processor above makes a variable called `user` available in +the template with the value of `g.user`. This example is not very +interesting because `g` is available in templates anyways, but it gives an +idea how this works. + +Variables are not limited to values; a context processor can also make +functions available to templates (since Python allows passing around +functions):: + + @app.context_processor + def utility_processor(): + def format_price(amount, currency="€"): + return f"{amount:.2f}{currency}" + return dict(format_price=format_price) + +The context processor above makes the `format_price` function available to all +templates:: + + {{ format_price(0.33) }} + +You could also build `format_price` as a template filter (see +:ref:`registering-filters`), but this demonstrates how to pass functions in a +context processor. + +Streaming +--------- + +It can be useful to not render the whole template as one complete +string, instead render it as a stream, yielding smaller incremental +strings. This can be used for streaming HTML in chunks to speed up +initial page load, or to save memory when rendering a very large +template. + +The Jinja2 template engine supports rendering a template piece +by piece, returning an iterator of strings. Flask provides the +:func:`~flask.stream_template` and :func:`~flask.stream_template_string` +functions to make this easier to use. + +.. code-block:: python + + from flask import stream_template + + @app.get("/timeline") + def timeline(): + return stream_template("timeline.html") + +These functions automatically apply the +:func:`~flask.stream_with_context` wrapper if a request is active, so +that it remains available in the template. diff --git a/test/fixtures/whole_applications/flask/docs/testing.rst b/test/fixtures/whole_applications/flask/docs/testing.rst new file mode 100644 index 0000000..8545bd3 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/testing.rst @@ -0,0 +1,319 @@ +Testing Flask Applications +========================== + +Flask provides utilities for testing an application. This documentation +goes over techniques for working with different parts of the application +in tests. + +We will use the `pytest`_ framework to set up and run our tests. + +.. code-block:: text + + $ pip install pytest + +.. _pytest: https://docs.pytest.org/ + +The :doc:`tutorial ` goes over how to write tests for +100% coverage of the sample Flaskr blog application. See +:doc:`the tutorial on tests ` for a detailed +explanation of specific tests for an application. + + +Identifying Tests +----------------- + +Tests are typically located in the ``tests`` folder. Tests are functions +that start with ``test_``, in Python modules that start with ``test_``. +Tests can also be further grouped in classes that start with ``Test``. + +It can be difficult to know what to test. Generally, try to test the +code that you write, not the code of libraries that you use, since they +are already tested. Try to extract complex behaviors as separate +functions to test individually. + + +Fixtures +-------- + +Pytest *fixtures* allow writing pieces of code that are reusable across +tests. A simple fixture returns a value, but a fixture can also do +setup, yield a value, then do teardown. Fixtures for the application, +test client, and CLI runner are shown below, they can be placed in +``tests/conftest.py``. + +If you're using an +:doc:`application factory `, define an ``app`` +fixture to create and configure an app instance. You can add code before +and after the ``yield`` to set up and tear down other resources, such as +creating and clearing a database. + +If you're not using a factory, you already have an app object you can +import and configure directly. You can still use an ``app`` fixture to +set up and tear down resources. + +.. code-block:: python + + import pytest + from my_project import create_app + + @pytest.fixture() + def app(): + app = create_app() + app.config.update({ + "TESTING": True, + }) + + # other setup can go here + + yield app + + # clean up / reset resources here + + + @pytest.fixture() + def client(app): + return app.test_client() + + + @pytest.fixture() + def runner(app): + return app.test_cli_runner() + + +Sending Requests with the Test Client +------------------------------------- + +The test client makes requests to the application without running a live +server. Flask's client extends +:doc:`Werkzeug's client `, see those docs for additional +information. + +The ``client`` has methods that match the common HTTP request methods, +such as ``client.get()`` and ``client.post()``. They take many arguments +for building the request; you can find the full documentation in +:class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, +``query_string``, ``headers``, and ``data`` or ``json``. + +To make a request, call the method the request should use with the path +to the route to test. A :class:`~werkzeug.test.TestResponse` is returned +to examine the response data. It has all the usual properties of a +response object. You'll usually look at ``response.data``, which is the +bytes returned by the view. If you want to use text, Werkzeug 2.1 +provides ``response.text``, or use ``response.get_data(as_text=True)``. + +.. code-block:: python + + def test_request_example(client): + response = client.get("/posts") + assert b"

Hello, World!

" in response.data + + +Pass a dict ``query_string={"key": "value", ...}`` to set arguments in +the query string (after the ``?`` in the URL). Pass a dict +``headers={}`` to set request headers. + +To send a request body in a POST or PUT request, pass a value to +``data``. If raw bytes are passed, that exact body is used. Usually, +you'll pass a dict to set form data. + + +Form Data +~~~~~~~~~ + +To send form data, pass a dict to ``data``. The ``Content-Type`` header +will be set to ``multipart/form-data`` or +``application/x-www-form-urlencoded`` automatically. + +If a value is a file object opened for reading bytes (``"rb"`` mode), it +will be treated as an uploaded file. To change the detected filename and +content type, pass a ``(file, filename, content_type)`` tuple. File +objects will be closed after making the request, so they do not need to +use the usual ``with open() as f:`` pattern. + +It can be useful to store files in a ``tests/resources`` folder, then +use ``pathlib.Path`` to get files relative to the current test file. + +.. code-block:: python + + from pathlib import Path + + # get the resources folder in the tests folder + resources = Path(__file__).parent / "resources" + + def test_edit_user(client): + response = client.post("/user/2/edit", data={ + "name": "Flask", + "theme": "dark", + "picture": (resources / "picture.png").open("rb"), + }) + assert response.status_code == 200 + + +JSON Data +~~~~~~~~~ + +To send JSON data, pass an object to ``json``. The ``Content-Type`` +header will be set to ``application/json`` automatically. + +Similarly, if the response contains JSON data, the ``response.json`` +attribute will contain the deserialized object. + +.. code-block:: python + + def test_json_data(client): + response = client.post("/graphql", json={ + "query": """ + query User($id: String!) { + user(id: $id) { + name + theme + picture_url + } + } + """, + variables={"id": 2}, + }) + assert response.json["data"]["user"]["name"] == "Flask" + + +Following Redirects +------------------- + +By default, the client does not make additional requests if the response +is a redirect. By passing ``follow_redirects=True`` to a request method, +the client will continue to make requests until a non-redirect response +is returned. + +:attr:`TestResponse.history ` is +a tuple of the responses that led up to the final response. Each +response has a :attr:`~werkzeug.test.TestResponse.request` attribute +which records the request that produced that response. + +.. code-block:: python + + def test_logout_redirect(client): + response = client.get("/logout") + # Check that there was one redirect response. + assert len(response.history) == 1 + # Check that the second request was to the index page. + assert response.request.path == "/index" + + +Accessing and Modifying the Session +----------------------------------- + +To access Flask's context variables, mainly +:data:`~flask.session`, use the client in a ``with`` statement. +The app and request context will remain active *after* making a request, +until the ``with`` block ends. + +.. code-block:: python + + from flask import session + + def test_access_session(client): + with client: + client.post("/auth/login", data={"username": "flask"}) + # session is still accessible + assert session["user_id"] == 1 + + # session is no longer accessible + +If you want to access or set a value in the session *before* making a +request, use the client's +:meth:`~flask.testing.FlaskClient.session_transaction` method in a +``with`` statement. It returns a session object, and will save the +session once the block ends. + +.. code-block:: python + + from flask import session + + def test_modify_session(client): + with client.session_transaction() as session: + # set a user id without going through the login route + session["user_id"] = 1 + + # session is saved now + + response = client.get("/users/me") + assert response.json["username"] == "flask" + + +.. _testing-cli: + +Running Commands with the CLI Runner +------------------------------------ + +Flask provides :meth:`~flask.Flask.test_cli_runner` to create a +:class:`~flask.testing.FlaskCliRunner`, which runs CLI commands in +isolation and captures the output in a :class:`~click.testing.Result` +object. Flask's runner extends :doc:`Click's runner `, +see those docs for additional information. + +Use the runner's :meth:`~flask.testing.FlaskCliRunner.invoke` method to +call commands in the same way they would be called with the ``flask`` +command from the command line. + +.. code-block:: python + + import click + + @app.cli.command("hello") + @click.option("--name", default="World") + def hello_command(name): + click.echo(f"Hello, {name}!") + + def test_hello_command(runner): + result = runner.invoke(args="hello") + assert "World" in result.output + + result = runner.invoke(args=["hello", "--name", "Flask"]) + assert "Flask" in result.output + + +Tests that depend on an Active Context +-------------------------------------- + +You may have functions that are called from views or commands, that +expect an active :doc:`application context ` or +:doc:`request context ` because they access ``request``, +``session``, or ``current_app``. Rather than testing them by making a +request or invoking the command, you can create and activate a context +directly. + +Use ``with app.app_context()`` to push an application context. For +example, database extensions usually require an active app context to +make queries. + +.. code-block:: python + + def test_db_post_model(app): + with app.app_context(): + post = db.session.query(Post).get(1) + +Use ``with app.test_request_context()`` to push a request context. It +takes the same arguments as the test client's request methods. + +.. code-block:: python + + def test_validate_user_edit(app): + with app.test_request_context( + "/user/2/edit", method="POST", data={"name": ""} + ): + # call a function that accesses `request` + messages = validate_edit_user() + + assert messages["name"][0] == "Name cannot be empty." + +Creating a test request context doesn't run any of the Flask dispatching +code, so ``before_request`` functions are not called. If you need to +call these, usually it's better to make a full request instead. However, +it's possible to call them manually. + +.. code-block:: python + + def test_auth_token(app): + with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}): + app.preprocess_request() + assert g.user.name == "Flask" diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/blog.rst b/test/fixtures/whole_applications/flask/docs/tutorial/blog.rst new file mode 100644 index 0000000..b06329e --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/blog.rst @@ -0,0 +1,336 @@ +.. currentmodule:: flask + +Blog Blueprint +============== + +You'll use the same techniques you learned about when writing the +authentication blueprint to write the blog blueprint. The blog should +list all posts, allow logged in users to create posts, and allow the +author of a post to edit or delete it. + +As you implement each view, keep the development server running. As you +save your changes, try going to the URL in your browser and testing them +out. + +The Blueprint +------------- + +Define the blueprint and register it in the application factory. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for + ) + from werkzeug.exceptions import abort + + from flaskr.auth import login_required + from flaskr.db import get_db + + bp = Blueprint('blog', __name__) + +Import and register the blueprint from the factory using +:meth:`app.register_blueprint() `. Place the +new code at the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import blog + app.register_blueprint(blog.bp) + app.add_url_rule('/', endpoint='index') + + return app + + +Unlike the auth blueprint, the blog blueprint does not have a +``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` +view at ``/create``, and so on. The blog is the main feature of Flaskr, +so it makes sense that the blog index will be the main index. + +However, the endpoint for the ``index`` view defined below will be +``blog.index``. Some of the authentication views referred to a plain +``index`` endpoint. :meth:`app.add_url_rule() ` +associates the endpoint name ``'index'`` with the ``/`` url so that +``url_for('index')`` or ``url_for('blog.index')`` will both work, +generating the same ``/`` URL either way. + +In another application you might give the blog blueprint a +``url_prefix`` and define a separate ``index`` view in the application +factory, similar to the ``hello`` view. Then the ``index`` and +``blog.index`` endpoints and URLs would be different. + + +Index +----- + +The index will show all of the posts, most recent first. A ``JOIN`` is +used so that the author information from the ``user`` table is +available in the result. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('/') + def index(): + db = get_db() + posts = db.execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' ORDER BY created DESC' + ).fetchall() + return render_template('blog/index.html', posts=posts) + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/index.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Posts{% endblock %}

+ {% if g.user %} + New + {% endif %} + {% endblock %} + + {% block content %} + {% for post in posts %} +
+
+
+

{{ post['title'] }}

+
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
+
+ {% if g.user['id'] == post['author_id'] %} + Edit + {% endif %} +
+

{{ post['body'] }}

+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} + {% endblock %} + +When a user is logged in, the ``header`` block adds a link to the +``create`` view. When the user is the author of a post, they'll see an +"Edit" link to the ``update`` view for that post. ``loop.last`` is a +special variable available inside `Jinja for loops`_. It's used to +display a line after each post except the last one, to visually separate +them. + +.. _Jinja for loops: https://jinja.palletsprojects.com/templates/#for + + +Create +------ + +The ``create`` view works the same as the auth ``register`` view. Either +the form is displayed, or the posted data is validated and the post is +added to the database or an error is shown. + +The ``login_required`` decorator you wrote earlier is used on the blog +views. A user must be logged in to visit these views, otherwise they +will be redirected to the login page. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('/create', methods=('GET', 'POST')) + @login_required + def create(): + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'INSERT INTO post (title, body, author_id)' + ' VALUES (?, ?, ?)', + (title, body, g.user['id']) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/create.html') + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/create.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}New Post{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% endblock %} + + +Update +------ + +Both the ``update`` and ``delete`` views will need to fetch a ``post`` +by ``id`` and check if the author matches the logged in user. To avoid +duplicating code, you can write a function to get the ``post`` and call +it from each view. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + def get_post(id, check_author=True): + post = get_db().execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' WHERE p.id = ?', + (id,) + ).fetchone() + + if post is None: + abort(404, f"Post id {id} doesn't exist.") + + if check_author and post['author_id'] != g.user['id']: + abort(403) + + return post + +:func:`abort` will raise a special exception that returns an HTTP status +code. It takes an optional message to show with the error, otherwise a +default message is used. ``404`` means "Not Found", and ``403`` means +"Forbidden". (``401`` means "Unauthorized", but you redirect to the +login page instead of returning that status.) + +The ``check_author`` argument is defined so that the function can be +used to get a ``post`` without checking the author. This would be useful +if you wrote a view to show an individual post on a page, where the user +doesn't matter because they're not modifying the post. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('//update', methods=('GET', 'POST')) + @login_required + def update(id): + post = get_post(id) + + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'UPDATE post SET title = ?, body = ?' + ' WHERE id = ?', + (title, body, id) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/update.html', post=post) + +Unlike the views you've written so far, the ``update`` function takes +an argument, ``id``. That corresponds to the ```` in the route. +A real URL will look like ``/1/update``. Flask will capture the ``1``, +ensure it's an :class:`int`, and pass it as the ``id`` argument. If you +don't specify ``int:`` and instead do ````, it will be a string. +To generate a URL to the update page, :func:`url_for` needs to be passed +the ``id`` so it knows what to fill in: +``url_for('blog.update', id=post['id'])``. This is also in the +``index.html`` file above. + +The ``create`` and ``update`` views look very similar. The main +difference is that the ``update`` view uses a ``post`` object and an +``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, +you could use one view and template for both actions, but for the +tutorial it's clearer to keep them separate. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/update.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+
+
+ +
+ {% endblock %} + +This template has two forms. The first posts the edited data to the +current page (``//update``). The other form contains only a button +and specifies an ``action`` attribute that posts to the delete view +instead. The button uses some JavaScript to show a confirmation dialog +before submitting. + +The pattern ``{{ request.form['title'] or post['title'] }}`` is used to +choose what data appears in the form. When the form hasn't been +submitted, the original ``post`` data appears, but if invalid form data +was posted you want to display that so the user can fix the error, so +``request.form`` is used instead. :data:`request` is another variable +that's automatically available in templates. + + +Delete +------ + +The delete view doesn't have its own template, the delete button is part +of ``update.html`` and posts to the ``//delete`` URL. Since there +is no template, it will only handle the ``POST`` method and then redirect +to the ``index`` view. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('//delete', methods=('POST',)) + @login_required + def delete(id): + get_post(id) + db = get_db() + db.execute('DELETE FROM post WHERE id = ?', (id,)) + db.commit() + return redirect(url_for('blog.index')) + +Congratulations, you've now finished writing your application! Take some +time to try out everything in the browser. However, there's still more +to do before the project is complete. + +Continue to :doc:`install`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/database.rst b/test/fixtures/whole_applications/flask/docs/tutorial/database.rst new file mode 100644 index 0000000..934f600 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/database.rst @@ -0,0 +1,209 @@ +.. currentmodule:: flask + +Define and Access the Database +============================== + +The application will use a `SQLite`_ database to store users and posts. +Python comes with built-in support for SQLite in the :mod:`sqlite3` +module. + +SQLite is convenient because it doesn't require setting up a separate +database server and is built-in to Python. However, if concurrent +requests try to write to the database at the same time, they will slow +down as each write happens sequentially. Small applications won't notice +this. Once you become big, you may want to switch to a different +database. + +The tutorial doesn't go into detail about SQL. If you are not familiar +with it, the SQLite docs describe the `language`_. + +.. _SQLite: https://sqlite.org/about.html +.. _language: https://sqlite.org/lang.html + + +Connect to the Database +----------------------- + +The first thing to do when working with a SQLite database (and most +other Python database libraries) is to create a connection to it. Any +queries and operations are performed using the connection, which is +closed after the work is finished. + +In web applications this connection is typically tied to the request. It +is created at some point when handling a request, and closed before the +response is sent. + +.. code-block:: python + :caption: ``flaskr/db.py`` + + import sqlite3 + + import click + from flask import current_app, g + + + def get_db(): + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + + def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +:data:`g` is a special object that is unique for each request. It is +used to store data that might be accessed by multiple functions during +the request. The connection is stored and reused instead of creating a +new connection if ``get_db`` is called a second time in the same +request. + +:data:`current_app` is another special object that points to the Flask +application handling the request. Since you used an application factory, +there is no application object when writing the rest of your code. +``get_db`` will be called when the application has been created and is +handling a request, so :data:`current_app` can be used. + +:func:`sqlite3.connect` establishes a connection to the file pointed at +by the ``DATABASE`` configuration key. This file doesn't have to exist +yet, and won't until you initialize the database later. + +:class:`sqlite3.Row` tells the connection to return rows that behave +like dicts. This allows accessing the columns by name. + +``close_db`` checks if a connection was created by checking if ``g.db`` +was set. If the connection exists, it is closed. Further down you will +tell your application about the ``close_db`` function in the application +factory so that it is called after each request. + + +Create the Tables +----------------- + +In SQLite, data is stored in *tables* and *columns*. These need to be +created before you can store and retrieve data. Flaskr will store users +in the ``user`` table, and posts in the ``post`` table. Create a file +with the SQL commands needed to create empty tables: + +.. code-block:: sql + :caption: ``flaskr/schema.sql`` + + DROP TABLE IF EXISTS user; + DROP TABLE IF EXISTS post; + + CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL + ); + + CREATE TABLE post ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author_id) REFERENCES user (id) + ); + +Add the Python functions that will run these SQL commands to the +``db.py`` file: + +.. code-block:: python + :caption: ``flaskr/db.py`` + + def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + + @click.command('init-db') + def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +:meth:`open_resource() ` opens a file relative to +the ``flaskr`` package, which is useful since you won't necessarily know +where that location is when deploying the application later. ``get_db`` +returns a database connection, which is used to execute the commands +read from the file. + +:func:`click.command` defines a command line command called ``init-db`` +that calls the ``init_db`` function and shows a success message to the +user. You can read :doc:`/cli` to learn more about writing commands. + + +Register with the Application +----------------------------- + +The ``close_db`` and ``init_db_command`` functions need to be registered +with the application instance; otherwise, they won't be used by the +application. However, since you're using a factory function, that +instance isn't available when writing the functions. Instead, write a +function that takes an application and does the registration. + +.. code-block:: python + :caption: ``flaskr/db.py`` + + def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) + +:meth:`app.teardown_appcontext() ` tells +Flask to call that function when cleaning up after returning the +response. + +:meth:`app.cli.add_command() ` adds a new +command that can be called with the ``flask`` command. + +Import and call this function from the factory. Place the new code at +the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import db + db.init_app(app) + + return app + + +Initialize the Database File +---------------------------- + +Now that ``init-db`` has been registered with the app, it can be called +using the ``flask`` command, similar to the ``run`` command from the +previous page. + +.. note:: + + If you're still running the server from the previous page, you can + either stop the server, or run this command in a new terminal. If + you use a new terminal, remember to change to your project directory + and activate the env as described in :doc:`/installation`. + +Run the ``init-db`` command: + +.. code-block:: none + + $ flask --app flaskr init-db + Initialized the database. + +There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in +your project. + +Continue to :doc:`views`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/deploy.rst b/test/fixtures/whole_applications/flask/docs/tutorial/deploy.rst new file mode 100644 index 0000000..eb3a53a --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/deploy.rst @@ -0,0 +1,111 @@ +Deploy to Production +==================== + +This part of the tutorial assumes you have a server that you want to +deploy your application to. It gives an overview of how to create the +distribution file and install it, but won't go into specifics about +what server or software to use. You can set up a new environment on your +development computer to try out the instructions below, but probably +shouldn't use it for hosting a real public application. See +:doc:`/deploying/index` for a list of many different ways to host your +application. + + +Build and Install +----------------- + +When you want to deploy your application elsewhere, you build a *wheel* +(``.whl``) file. Install and use the ``build`` tool to do this. + +.. code-block:: none + + $ pip install build + $ python -m build --wheel + +You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The +file name is in the format of {project name}-{version}-{python tag} +-{abi tag}-{platform tag}. + +Copy this file to another machine, +:ref:`set up a new virtualenv `, then install the +file with ``pip``. + +.. code-block:: none + + $ pip install flaskr-1.0.0-py3-none-any.whl + +Pip will install your project along with its dependencies. + +Since this is a different machine, you need to run ``init-db`` again to +create the database in the instance folder. + + .. code-block:: text + + $ flask --app flaskr init-db + +When Flask detects that it's installed (not in editable mode), it uses +a different directory for the instance folder. You can find it at +``.venv/var/flaskr-instance`` instead. + + +Configure the Secret Key +------------------------ + +In the beginning of the tutorial that you gave a default value for +:data:`SECRET_KEY`. This should be changed to some random bytes in +production. Otherwise, attackers could use the public ``'dev'`` key to +modify the session cookie, or anything else that uses the secret key. + +You can use the following command to output a random secret key: + +.. code-block:: none + + $ python -c 'import secrets; print(secrets.token_hex())' + + '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +Create the ``config.py`` file in the instance folder, which the factory +will read from if it exists. Copy the generated value into it. + +.. code-block:: python + :caption: ``.venv/var/flaskr-instance/config.py`` + + SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +You can also set any other necessary configuration here, although +``SECRET_KEY`` is the only one needed for Flaskr. + + +Run with a Production Server +---------------------------- + +When running publicly rather than in development, you should not use the +built-in development server (``flask run``). The development server is +provided by Werkzeug for convenience, but is not designed to be +particularly efficient, stable, or secure. + +Instead, use a production WSGI server. For example, to use `Waitress`_, +first install it in the virtual environment: + +.. code-block:: none + + $ pip install waitress + +You need to tell Waitress about your application, but it doesn't use +``--app`` like ``flask run`` does. You need to tell it to import and +call the application factory to get an application object. + +.. code-block:: none + + $ waitress-serve --call 'flaskr:create_app' + + Serving on http://0.0.0.0:8080 + +See :doc:`/deploying/index` for a list of many different ways to host +your application. Waitress is just an example, chosen for the tutorial +because it supports both Windows and Linux. There are many more WSGI +servers and deployment options that you may choose for your project. + +.. _Waitress: https://docs.pylonsproject.org/projects/waitress/en/stable/ + +Continue to :doc:`next`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/factory.rst b/test/fixtures/whole_applications/flask/docs/tutorial/factory.rst new file mode 100644 index 0000000..39febd1 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/factory.rst @@ -0,0 +1,162 @@ +.. currentmodule:: flask + +Application Setup +================= + +A Flask application is an instance of the :class:`Flask` class. +Everything about the application, such as configuration and URLs, will +be registered with this class. + +The most straightforward way to create a Flask application is to create +a global :class:`Flask` instance directly at the top of your code, like +how the "Hello, World!" example did on the previous page. While this is +simple and useful in some cases, it can cause some tricky issues as the +project grows. + +Instead of creating a :class:`Flask` instance globally, you will create +it inside a function. This function is known as the *application +factory*. Any configuration, registration, and other setup the +application needs will happen inside the function, then the application +will be returned. + + +The Application Factory +----------------------- + +It's time to start coding! Create the ``flaskr`` directory and add the +``__init__.py`` file. The ``__init__.py`` serves double duty: it will +contain the application factory, and it tells Python that the ``flaskr`` +directory should be treated as a package. + +.. code-block:: none + + $ mkdir flaskr + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + import os + + from flask import Flask + + + def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # a simple page that says hello + @app.route('/hello') + def hello(): + return 'Hello, World!' + + return app + +``create_app`` is the application factory function. You'll add to it +later in the tutorial, but it already does a lot. + +#. ``app = Flask(__name__, instance_relative_config=True)`` creates the + :class:`Flask` instance. + + * ``__name__`` is the name of the current Python module. The app + needs to know where it's located to set up some paths, and + ``__name__`` is a convenient way to tell it that. + + * ``instance_relative_config=True`` tells the app that + configuration files are relative to the + :ref:`instance folder `. The instance folder + is located outside the ``flaskr`` package and can hold local + data that shouldn't be committed to version control, such as + configuration secrets and the database file. + +#. :meth:`app.config.from_mapping() ` sets + some default configuration that the app will use: + + * :data:`SECRET_KEY` is used by Flask and extensions to keep data + safe. It's set to ``'dev'`` to provide a convenient value + during development, but it should be overridden with a random + value when deploying. + + * ``DATABASE`` is the path where the SQLite database file will be + saved. It's under + :attr:`app.instance_path `, which is the + path that Flask has chosen for the instance folder. You'll learn + more about the database in the next section. + +#. :meth:`app.config.from_pyfile() ` overrides + the default configuration with values taken from the ``config.py`` + file in the instance folder if it exists. For example, when + deploying, this can be used to set a real ``SECRET_KEY``. + + * ``test_config`` can also be passed to the factory, and will be + used instead of the instance configuration. This is so the tests + you'll write later in the tutorial can be configured + independently of any development values you have configured. + +#. :func:`os.makedirs` ensures that + :attr:`app.instance_path ` exists. Flask + doesn't create the instance folder automatically, but it needs to be + created because your project will create the SQLite database file + there. + +#. :meth:`@app.route() ` creates a simple route so you can + see the application working before getting into the rest of the + tutorial. It creates a connection between the URL ``/hello`` and a + function that returns a response, the string ``'Hello, World!'`` in + this case. + + +Run The Application +------------------- + +Now you can run your application using the ``flask`` command. From the +terminal, tell Flask where to find your application, then run it in +debug mode. Remember, you should still be in the top-level +``flask-tutorial`` directory, not the ``flaskr`` package. + +Debug mode shows an interactive debugger whenever a page raises an +exception, and restarts the server whenever you make changes to the +code. You can leave it running and just reload the browser page as you +follow the tutorial. + +.. code-block:: text + + $ flask --app flaskr run --debug + +You'll see output similar to this: + +.. code-block:: text + + * Serving Flask app "flaskr" + * Debug mode: on + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: nnn-nnn-nnn + +Visit http://127.0.0.1:5000/hello in a browser and you should see the +"Hello, World!" message. Congratulations, you're now running your Flask +web application! + +If another program is already using port 5000, you'll see +``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the +server tries to start. See :ref:`address-already-in-use` for how to +handle that. + +Continue to :doc:`database`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_edit.png b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_edit.png new file mode 100644 index 0000000..6cd6e39 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_edit.png differ diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_index.png b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_index.png new file mode 100644 index 0000000..aa2b50f Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_index.png differ diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_login.png b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_login.png new file mode 100644 index 0000000..d482c64 Binary files /dev/null and b/test/fixtures/whole_applications/flask/docs/tutorial/flaskr_login.png differ diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/index.rst b/test/fixtures/whole_applications/flask/docs/tutorial/index.rst new file mode 100644 index 0000000..d5dc5b3 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/index.rst @@ -0,0 +1,64 @@ +Tutorial +======== + +.. toctree:: + :caption: Contents: + :maxdepth: 1 + + layout + factory + database + views + templates + static + blog + install + tests + deploy + next + +This tutorial will walk you through creating a basic blog application +called Flaskr. Users will be able to register, log in, create posts, +and edit or delete their own posts. You will be able to package and +install the application on other computers. + +.. image:: flaskr_index.png + :align: center + :class: screenshot + :alt: screenshot of index page + +It's assumed that you're already familiar with Python. The `official +tutorial`_ in the Python docs is a great way to learn or review first. + +.. _official tutorial: https://docs.python.org/3/tutorial/ + +While it's designed to give a good starting point, the tutorial doesn't +cover all of Flask's features. Check out the :doc:`/quickstart` for an +overview of what Flask can do, then dive into the docs to find out more. +The tutorial only uses what's provided by Flask and Python. In another +project, you might decide to use :doc:`/extensions` or other libraries +to make some tasks simpler. + +.. image:: flaskr_login.png + :align: center + :class: screenshot + :alt: screenshot of login page + +Flask is flexible. It doesn't require you to use any particular project +or code layout. However, when first starting, it's helpful to use a more +structured approach. This means that the tutorial will require a bit of +boilerplate up front, but it's done to avoid many common pitfalls that +new developers encounter, and it creates a project that's easy to expand +on. Once you become more comfortable with Flask, you can step out of +this structure and take full advantage of Flask's flexibility. + +.. image:: flaskr_edit.png + :align: center + :class: screenshot + :alt: screenshot of edit page + +:gh:`The tutorial project is available as an example in the Flask +repository `, if you want to compare your project +with the final product as you follow the tutorial. + +Continue to :doc:`layout`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/install.rst b/test/fixtures/whole_applications/flask/docs/tutorial/install.rst new file mode 100644 index 0000000..db83e10 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/install.rst @@ -0,0 +1,89 @@ +Make the Project Installable +============================ + +Making your project installable means that you can build a *wheel* file and install that +in another environment, just like you installed Flask in your project's environment. +This makes deploying your project the same as installing any other library, so you're +using all the standard Python tools to manage everything. + +Installing also comes with other benefits that might not be obvious from +the tutorial or as a new Python user, including: + +* Currently, Python and Flask understand how to use the ``flaskr`` + package only because you're running from your project's directory. + Installing means you can import it no matter where you run from. + +* You can manage your project's dependencies just like other packages + do, so ``pip install yourproject.whl`` installs them. + +* Test tools can isolate your test environment from your development + environment. + +.. note:: + This is being introduced late in the tutorial, but in your future + projects you should always start with this. + + +Describe the Project +-------------------- + +The ``pyproject.toml`` file describes your project and how to build it. + +.. code-block:: toml + :caption: ``pyproject.toml`` + + [project] + name = "flaskr" + version = "1.0.0" + description = "The basic blog app built in the Flask tutorial." + dependencies = [ + "flask", + ] + + [build-system] + requires = ["flit_core<4"] + build-backend = "flit_core.buildapi" + +See the official `Packaging tutorial `_ for more +explanation of the files and options used. + +.. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ + + +Install the Project +------------------- + +Use ``pip`` to install your project in the virtual environment. + +.. code-block:: none + + $ pip install -e . + +This tells pip to find ``pyproject.toml`` in the current directory and install the +project in *editable* or *development* mode. Editable mode means that as you make +changes to your local code, you'll only need to re-install if you change the metadata +about the project, such as its dependencies. + +You can observe that the project is now installed with ``pip list``. + +.. code-block:: none + + $ pip list + + Package Version Location + -------------- --------- ---------------------------------- + click 6.7 + Flask 1.0 + flaskr 1.0.0 /home/user/Projects/flask-tutorial + itsdangerous 0.24 + Jinja2 2.10 + MarkupSafe 1.0 + pip 9.0.3 + Werkzeug 0.14.1 + +Nothing changes from how you've been running your project so far. +``--app`` is still set to ``flaskr`` and ``flask run`` still runs +the application, but you can call it from anywhere, not just the +``flask-tutorial`` directory. + +Continue to :doc:`tests`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/layout.rst b/test/fixtures/whole_applications/flask/docs/tutorial/layout.rst new file mode 100644 index 0000000..6f8e59f --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/layout.rst @@ -0,0 +1,110 @@ +Project Layout +============== + +Create a project directory and enter it: + +.. code-block:: none + + $ mkdir flask-tutorial + $ cd flask-tutorial + +Then follow the :doc:`installation instructions ` to set +up a Python virtual environment and install Flask for your project. + +The tutorial will assume you're working from the ``flask-tutorial`` +directory from now on. The file names at the top of each code block are +relative to this directory. + +---- + +A Flask application can be as simple as a single file. + +.. code-block:: python + :caption: ``hello.py`` + + from flask import Flask + + app = Flask(__name__) + + + @app.route('/') + def hello(): + return 'Hello, World!' + +However, as a project gets bigger, it becomes overwhelming to keep all +the code in one file. Python projects use *packages* to organize code +into multiple modules that can be imported where needed, and the +tutorial will do this as well. + +The project directory will contain: + +* ``flaskr/``, a Python package containing your application code and + files. +* ``tests/``, a directory containing test modules. +* ``.venv/``, a Python virtual environment where Flask and other + dependencies are installed. +* Installation files telling Python how to install your project. +* Version control config, such as `git`_. You should make a habit of + using some type of version control for all your projects, no matter + the size. +* Any other project files you might add in the future. + +.. _git: https://git-scm.com/ + +By the end, your project layout will look like this: + +.. code-block:: none + + /home/user/Projects/flask-tutorial + ├── flaskr/ + │ ├── __init__.py + │ ├── db.py + │ ├── schema.sql + │ ├── auth.py + │ ├── blog.py + │ ├── templates/ + │ │ ├── base.html + │ │ ├── auth/ + │ │ │ ├── login.html + │ │ │ └── register.html + │ │ └── blog/ + │ │ ├── create.html + │ │ ├── index.html + │ │ └── update.html + │ └── static/ + │ └── style.css + ├── tests/ + │ ├── conftest.py + │ ├── data.sql + │ ├── test_factory.py + │ ├── test_db.py + │ ├── test_auth.py + │ └── test_blog.py + ├── .venv/ + ├── pyproject.toml + └── MANIFEST.in + +If you're using version control, the following files that are generated +while running your project should be ignored. There may be other files +based on the editor you use. In general, ignore files that you didn't +write. For example, with git: + +.. code-block:: none + :caption: ``.gitignore`` + + .venv/ + + *.pyc + __pycache__/ + + instance/ + + .pytest_cache/ + .coverage + htmlcov/ + + dist/ + build/ + *.egg-info/ + +Continue to :doc:`factory`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/next.rst b/test/fixtures/whole_applications/flask/docs/tutorial/next.rst new file mode 100644 index 0000000..d41e8ef --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/next.rst @@ -0,0 +1,38 @@ +Keep Developing! +================ + +You've learned about quite a few Flask and Python concepts throughout +the tutorial. Go back and review the tutorial and compare your code with +the steps you took to get there. Compare your project to the +:gh:`example project `, which might look a bit +different due to the step-by-step nature of the tutorial. + +There's a lot more to Flask than what you've seen so far. Even so, +you're now equipped to start developing your own web applications. Check +out the :doc:`/quickstart` for an overview of what Flask can do, then +dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, +`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have +their own documentation too. You'll also be interested in +:doc:`/extensions` which make tasks like working with the database or +validating form data easier and more powerful. + +If you want to keep developing your Flaskr project, here are some ideas +for what to try next: + +* A detail view to show a single post. Click a post's title to go to + its page. +* Like / unlike a post. +* Comments. +* Tags. Clicking a tag shows all the posts with that tag. +* A search box that filters the index page by name. +* Paged display. Only show 5 posts per page. +* Upload an image to go along with a post. +* Format posts using Markdown. +* An RSS feed of new posts. + +Have fun and make awesome applications! + +.. _Jinja: https://palletsprojects.com/p/jinja/ +.. _Click: https://palletsprojects.com/p/click/ +.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ +.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/static.rst b/test/fixtures/whole_applications/flask/docs/tutorial/static.rst new file mode 100644 index 0000000..8e76c40 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/static.rst @@ -0,0 +1,72 @@ +Static Files +============ + +The authentication views and templates work, but they look very plain +right now. Some `CSS`_ can be added to add style to the HTML layout you +constructed. The style won't change, so it's a *static* file rather than +a template. + +Flask automatically adds a ``static`` view that takes a path relative +to the ``flaskr/static`` directory and serves it. The ``base.html`` +template already has a link to the ``style.css`` file: + +.. code-block:: html+jinja + + {{ url_for('static', filename='style.css') }} + +Besides CSS, other types of static files might be files with JavaScript +functions, or a logo image. They are all placed under the +``flaskr/static`` directory and referenced with +``url_for('static', filename='...')``. + +This tutorial isn't focused on how to write CSS, so you can just copy +the following into the ``flaskr/static/style.css`` file: + +.. code-block:: css + :caption: ``flaskr/static/style.css`` + + html { font-family: sans-serif; background: #eee; padding: 1rem; } + body { max-width: 960px; margin: 0 auto; background: white; } + h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } + a { color: #377ba8; } + hr { border: none; border-top: 1px solid lightgray; } + nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } + nav h1 { flex: auto; margin: 0; } + nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } + nav ul { display: flex; list-style: none; margin: 0; padding: 0; } + nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } + .content { padding: 0 1rem 1rem; } + .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } + .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } + .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } + .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } + .post > header > div:first-of-type { flex: auto; } + .post > header h1 { font-size: 1.5em; margin-bottom: 0; } + .post .about { color: slategray; font-style: italic; } + .post .body { white-space: pre-line; } + .content:last-child { margin-bottom: 0; } + .content form { margin: 1em 0; display: flex; flex-direction: column; } + .content label { font-weight: bold; margin-bottom: 0.5em; } + .content input, .content textarea { margin-bottom: 1em; } + .content textarea { min-height: 12em; resize: vertical; } + input.danger { color: #cc2f2e; } + input[type=submit] { align-self: start; min-width: 10em; } + +You can find a less compact version of ``style.css`` in the +:gh:`example code `. + +Go to http://127.0.0.1:5000/auth/login and the page should look like the +screenshot below. + +.. image:: flaskr_login.png + :align: center + :class: screenshot + :alt: screenshot of login page + +You can read more about CSS from `Mozilla's documentation `_. If +you change a static file, refresh the browser page. If the change +doesn't show up, try clearing your browser's cache. + +.. _CSS: https://developer.mozilla.org/docs/Web/CSS + +Continue to :doc:`blog`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/templates.rst b/test/fixtures/whole_applications/flask/docs/tutorial/templates.rst new file mode 100644 index 0000000..1a5535c --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/templates.rst @@ -0,0 +1,187 @@ +.. currentmodule:: flask + +Templates +========= + +You've written the authentication views for your application, but if +you're running the server and try to go to any of the URLs, you'll see a +``TemplateNotFound`` error. That's because the views are calling +:func:`render_template`, but you haven't written the templates yet. +The template files will be stored in the ``templates`` directory inside +the ``flaskr`` package. + +Templates are files that contain static data as well as placeholders +for dynamic data. A template is rendered with specific data to produce a +final document. Flask uses the `Jinja`_ template library to render +templates. + +In your application, you will use templates to render `HTML`_ which +will display in the user's browser. In Flask, Jinja is configured to +*autoescape* any data that is rendered in HTML templates. This means +that it's safe to render user input; any characters they've entered that +could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with +*safe* values that look the same in the browser but don't cause unwanted +effects. + +Jinja looks and behaves mostly like Python. Special delimiters are used +to distinguish Jinja syntax from the static data in the template. +Anything between ``{{`` and ``}}`` is an expression that will be output +to the final document. ``{%`` and ``%}`` denotes a control flow +statement like ``if`` and ``for``. Unlike Python, blocks are denoted +by start and end tags rather than indentation since static text within +a block could change indentation. + +.. _Jinja: https://jinja.palletsprojects.com/templates/ +.. _HTML: https://developer.mozilla.org/docs/Web/HTML + + +The Base Layout +--------------- + +Each page in the application will have the same basic layout around a +different body. Instead of writing the entire HTML structure in each +template, each template will *extend* a base template and override +specific sections. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/base.html`` + + + {% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
+ +:data:`g` is automatically available in templates. Based on if +``g.user`` is set (from ``load_logged_in_user``), either the username +and a log out link are displayed, or links to register and log in +are displayed. :func:`url_for` is also automatically available, and is +used to generate URLs to views instead of writing them out manually. + +After the page title, and before the content, the template loops over +each message returned by :func:`get_flashed_messages`. You used +:func:`flash` in the views to show error messages, and this is the code +that will display them. + +There are three blocks defined here that will be overridden in the other +templates: + +#. ``{% block title %}`` will change the title displayed in the + browser's tab and window title. + +#. ``{% block header %}`` is similar to ``title`` but will change the + title displayed on the page. + +#. ``{% block content %}`` is where the content of each page goes, such + as the login form or a blog post. + +The base template is directly in the ``templates`` directory. To keep +the others organized, the templates for a blueprint will be placed in a +directory with the same name as the blueprint. + + +Register +-------- + +.. code-block:: html+jinja + :caption: ``flaskr/templates/auth/register.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Register{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% endblock %} + +``{% extends 'base.html' %}`` tells Jinja that this template should +replace the blocks from the base template. All the rendered content must +appear inside ``{% block %}`` tags that override blocks from the base +template. + +A useful pattern used here is to place ``{% block title %}`` inside +``{% block header %}``. This will set the title block and then output +the value of it into the header block, so that both the window and page +share the same title without writing it twice. + +The ``input`` tags are using the ``required`` attribute here. This tells +the browser not to submit the form until those fields are filled in. If +the user is using an older browser that doesn't support that attribute, +or if they are using something besides a browser to make requests, you +still want to validate the data in the Flask view. It's important to +always fully validate the data on the server, even if the client does +some validation as well. + + +Log In +------ + +This is identical to the register template except for the title and +submit button. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/auth/login.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Log In{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% endblock %} + + +Register A User +--------------- + +Now that the authentication templates are written, you can register a +user. Make sure the server is still running (``flask run`` if it's not), +then go to http://127.0.0.1:5000/auth/register. + +Try clicking the "Register" button without filling out the form and see +that the browser shows an error message. Try removing the ``required`` +attributes from the ``register.html`` template and click "Register" +again. Instead of the browser showing an error, the page will reload and +the error from :func:`flash` in the view will be shown. + +Fill out a username and password and you'll be redirected to the login +page. Try entering an incorrect username, or the correct username and +incorrect password. If you log in you'll get an error because there's +no ``index`` view to redirect to yet. + +Continue to :doc:`static`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/tests.rst b/test/fixtures/whole_applications/flask/docs/tutorial/tests.rst new file mode 100644 index 0000000..f4744cd --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/tests.rst @@ -0,0 +1,559 @@ +.. currentmodule:: flask + +Test Coverage +============= + +Writing unit tests for your application lets you check that the code +you wrote works the way you expect. Flask provides a test client that +simulates requests to the application and returns the response data. + +You should test as much of your code as possible. Code in functions only +runs when the function is called, and code in branches, such as ``if`` +blocks, only runs when the condition is met. You want to make sure that +each function is tested with data that covers each branch. + +The closer you get to 100% coverage, the more comfortable you can be +that making a change won't unexpectedly change other behavior. However, +100% coverage doesn't guarantee that your application doesn't have bugs. +In particular, it doesn't test how the user interacts with the +application in the browser. Despite this, test coverage is an important +tool to use during development. + +.. note:: + This is being introduced late in the tutorial, but in your future + projects you should test as you develop. + +You'll use `pytest`_ and `coverage`_ to test and measure your code. +Install them both: + +.. code-block:: none + + $ pip install pytest coverage + +.. _pytest: https://pytest.readthedocs.io/ +.. _coverage: https://coverage.readthedocs.io/ + + +Setup and Fixtures +------------------ + +The test code is located in the ``tests`` directory. This directory is +*next to* the ``flaskr`` package, not inside it. The +``tests/conftest.py`` file contains setup functions called *fixtures* +that each test will use. Tests are in Python modules that start with +``test_``, and each test function in those modules also starts with +``test_``. + +Each test will create a new temporary database file and populate some +data that will be used in the tests. Write a SQL file to insert that +data. + +.. code-block:: sql + :caption: ``tests/data.sql`` + + INSERT INTO user (username, password) + VALUES + ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), + ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); + + INSERT INTO post (title, body, author_id, created) + VALUES + ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); + +The ``app`` fixture will call the factory and pass ``test_config`` to +configure the application and database for testing instead of using your +local development configuration. + +.. code-block:: python + :caption: ``tests/conftest.py`` + + import os + import tempfile + + import pytest + from flaskr import create_app + from flaskr.db import get_db, init_db + + with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: + _data_sql = f.read().decode('utf8') + + + @pytest.fixture + def app(): + db_fd, db_path = tempfile.mkstemp() + + app = create_app({ + 'TESTING': True, + 'DATABASE': db_path, + }) + + with app.app_context(): + init_db() + get_db().executescript(_data_sql) + + yield app + + os.close(db_fd) + os.unlink(db_path) + + + @pytest.fixture + def client(app): + return app.test_client() + + + @pytest.fixture + def runner(app): + return app.test_cli_runner() + +:func:`tempfile.mkstemp` creates and opens a temporary file, returning +the file descriptor and the path to it. The ``DATABASE`` path is +overridden so it points to this temporary path instead of the instance +folder. After setting the path, the database tables are created and the +test data is inserted. After the test is over, the temporary file is +closed and removed. + +:data:`TESTING` tells Flask that the app is in test mode. Flask changes +some internal behavior so it's easier to test, and other extensions can +also use the flag to make testing them easier. + +The ``client`` fixture calls +:meth:`app.test_client() ` with the application +object created by the ``app`` fixture. Tests will use the client to make +requests to the application without running the server. + +The ``runner`` fixture is similar to ``client``. +:meth:`app.test_cli_runner() ` creates a runner +that can call the Click commands registered with the application. + +Pytest uses fixtures by matching their function names with the names +of arguments in the test functions. For example, the ``test_hello`` +function you'll write next takes a ``client`` argument. Pytest matches +that with the ``client`` fixture function, calls it, and passes the +returned value to the test function. + + +Factory +------- + +There's not much to test about the factory itself. Most of the code will +be executed for each test already, so if something fails the other tests +will notice. + +The only behavior that can change is passing test config. If config is +not passed, there should be some default configuration, otherwise the +configuration should be overridden. + +.. code-block:: python + :caption: ``tests/test_factory.py`` + + from flaskr import create_app + + + def test_config(): + assert not create_app().testing + assert create_app({'TESTING': True}).testing + + + def test_hello(client): + response = client.get('/hello') + assert response.data == b'Hello, World!' + +You added the ``hello`` route as an example when writing the factory at +the beginning of the tutorial. It returns "Hello, World!", so the test +checks that the response data matches. + + +Database +-------- + +Within an application context, ``get_db`` should return the same +connection each time it's called. After the context, the connection +should be closed. + +.. code-block:: python + :caption: ``tests/test_db.py`` + + import sqlite3 + + import pytest + from flaskr.db import get_db + + + def test_get_close_db(app): + with app.app_context(): + db = get_db() + assert db is get_db() + + with pytest.raises(sqlite3.ProgrammingError) as e: + db.execute('SELECT 1') + + assert 'closed' in str(e.value) + +The ``init-db`` command should call the ``init_db`` function and output +a message. + +.. code-block:: python + :caption: ``tests/test_db.py`` + + def test_init_db_command(runner, monkeypatch): + class Recorder(object): + called = False + + def fake_init_db(): + Recorder.called = True + + monkeypatch.setattr('flaskr.db.init_db', fake_init_db) + result = runner.invoke(args=['init-db']) + assert 'Initialized' in result.output + assert Recorder.called + +This test uses Pytest's ``monkeypatch`` fixture to replace the +``init_db`` function with one that records that it's been called. The +``runner`` fixture you wrote above is used to call the ``init-db`` +command by name. + + +Authentication +-------------- + +For most of the views, a user needs to be logged in. The easiest way to +do this in tests is to make a ``POST`` request to the ``login`` view +with the client. Rather than writing that out every time, you can write +a class with methods to do that, and use a fixture to pass it the client +for each test. + +.. code-block:: python + :caption: ``tests/conftest.py`` + + class AuthActions(object): + def __init__(self, client): + self._client = client + + def login(self, username='test', password='test'): + return self._client.post( + '/auth/login', + data={'username': username, 'password': password} + ) + + def logout(self): + return self._client.get('/auth/logout') + + + @pytest.fixture + def auth(client): + return AuthActions(client) + +With the ``auth`` fixture, you can call ``auth.login()`` in a test to +log in as the ``test`` user, which was inserted as part of the test +data in the ``app`` fixture. + +The ``register`` view should render successfully on ``GET``. On ``POST`` +with valid form data, it should redirect to the login URL and the user's +data should be in the database. Invalid data should display error +messages. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + import pytest + from flask import g, session + from flaskr.db import get_db + + + def test_register(client, app): + assert client.get('/auth/register').status_code == 200 + response = client.post( + '/auth/register', data={'username': 'a', 'password': 'a'} + ) + assert response.headers["Location"] == "/auth/login" + + with app.app_context(): + assert get_db().execute( + "SELECT * FROM user WHERE username = 'a'", + ).fetchone() is not None + + + @pytest.mark.parametrize(('username', 'password', 'message'), ( + ('', '', b'Username is required.'), + ('a', '', b'Password is required.'), + ('test', 'test', b'already registered'), + )) + def test_register_validate_input(client, username, password, message): + response = client.post( + '/auth/register', + data={'username': username, 'password': password} + ) + assert message in response.data + +:meth:`client.get() ` makes a ``GET`` request +and returns the :class:`Response` object returned by Flask. Similarly, +:meth:`client.post() ` makes a ``POST`` +request, converting the ``data`` dict into form data. + +To test that the page renders successfully, a simple request is made and +checked for a ``200 OK`` :attr:`~Response.status_code`. If +rendering failed, Flask would return a ``500 Internal Server Error`` +code. + +:attr:`~Response.headers` will have a ``Location`` header with the login +URL when the register view redirects to the login view. + +:attr:`~Response.data` contains the body of the response as bytes. If +you expect a certain value to render on the page, check that it's in +``data``. Bytes must be compared to bytes. If you want to compare text, +use :meth:`get_data(as_text=True) ` +instead. + +``pytest.mark.parametrize`` tells Pytest to run the same test function +with different arguments. You use it here to test different invalid +input and error messages without writing the same code three times. + +The tests for the ``login`` view are very similar to those for +``register``. Rather than testing the data in the database, +:data:`session` should have ``user_id`` set after logging in. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + def test_login(client, auth): + assert client.get('/auth/login').status_code == 200 + response = auth.login() + assert response.headers["Location"] == "/" + + with client: + client.get('/') + assert session['user_id'] == 1 + assert g.user['username'] == 'test' + + + @pytest.mark.parametrize(('username', 'password', 'message'), ( + ('a', 'test', b'Incorrect username.'), + ('test', 'a', b'Incorrect password.'), + )) + def test_login_validate_input(auth, username, password, message): + response = auth.login(username, password) + assert message in response.data + +Using ``client`` in a ``with`` block allows accessing context variables +such as :data:`session` after the response is returned. Normally, +accessing ``session`` outside of a request would raise an error. + +Testing ``logout`` is the opposite of ``login``. :data:`session` should +not contain ``user_id`` after logging out. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + def test_logout(client, auth): + auth.login() + + with client: + auth.logout() + assert 'user_id' not in session + + +Blog +---- + +All the blog views use the ``auth`` fixture you wrote earlier. Call +``auth.login()`` and subsequent requests from the client will be logged +in as the ``test`` user. + +The ``index`` view should display information about the post that was +added with the test data. When logged in as the author, there should be +a link to edit the post. + +You can also test some more authentication behavior while testing the +``index`` view. When not logged in, each page shows links to log in or +register. When logged in, there's a link to log out. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + import pytest + from flaskr.db import get_db + + + def test_index(client, auth): + response = client.get('/') + assert b"Log In" in response.data + assert b"Register" in response.data + + auth.login() + response = client.get('/') + assert b'Log Out' in response.data + assert b'test title' in response.data + assert b'by test on 2018-01-01' in response.data + assert b'test\nbody' in response.data + assert b'href="/1/update"' in response.data + +A user must be logged in to access the ``create``, ``update``, and +``delete`` views. The logged in user must be the author of the post to +access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status +is returned. If a ``post`` with the given ``id`` doesn't exist, +``update`` and ``delete`` should return ``404 Not Found``. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + @pytest.mark.parametrize('path', ( + '/create', + '/1/update', + '/1/delete', + )) + def test_login_required(client, path): + response = client.post(path) + assert response.headers["Location"] == "/auth/login" + + + def test_author_required(app, client, auth): + # change the post author to another user + with app.app_context(): + db = get_db() + db.execute('UPDATE post SET author_id = 2 WHERE id = 1') + db.commit() + + auth.login() + # current user can't modify other user's post + assert client.post('/1/update').status_code == 403 + assert client.post('/1/delete').status_code == 403 + # current user doesn't see edit link + assert b'href="/1/update"' not in client.get('/').data + + + @pytest.mark.parametrize('path', ( + '/2/update', + '/2/delete', + )) + def test_exists_required(client, auth, path): + auth.login() + assert client.post(path).status_code == 404 + +The ``create`` and ``update`` views should render and return a +``200 OK`` status for a ``GET`` request. When valid data is sent in a +``POST`` request, ``create`` should insert the new post data into the +database, and ``update`` should modify the existing data. Both pages +should show an error message on invalid data. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + def test_create(client, auth, app): + auth.login() + assert client.get('/create').status_code == 200 + client.post('/create', data={'title': 'created', 'body': ''}) + + with app.app_context(): + db = get_db() + count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] + assert count == 2 + + + def test_update(client, auth, app): + auth.login() + assert client.get('/1/update').status_code == 200 + client.post('/1/update', data={'title': 'updated', 'body': ''}) + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post['title'] == 'updated' + + + @pytest.mark.parametrize('path', ( + '/create', + '/1/update', + )) + def test_create_update_validate(client, auth, path): + auth.login() + response = client.post(path, data={'title': '', 'body': ''}) + assert b'Title is required.' in response.data + +The ``delete`` view should redirect to the index URL and the post should +no longer exist in the database. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + def test_delete(client, auth, app): + auth.login() + response = client.post('/1/delete') + assert response.headers["Location"] == "/" + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post is None + + +Running the Tests +----------------- + +Some extra configuration, which is not required but makes running tests with coverage +less verbose, can be added to the project's ``pyproject.toml`` file. + +.. code-block:: toml + :caption: ``pyproject.toml`` + + [tool.pytest.ini_options] + testpaths = ["tests"] + + [tool.coverage.run] + branch = true + source = ["flaskr"] + +To run the tests, use the ``pytest`` command. It will find and run all +the test functions you've written. + +.. code-block:: none + + $ pytest + + ========================= test session starts ========================== + platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 + rootdir: /home/user/Projects/flask-tutorial + collected 23 items + + tests/test_auth.py ........ [ 34%] + tests/test_blog.py ............ [ 86%] + tests/test_db.py .. [ 95%] + tests/test_factory.py .. [100%] + + ====================== 24 passed in 0.64 seconds ======================= + +If any tests fail, pytest will show the error that was raised. You can +run ``pytest -v`` to get a list of each test function rather than dots. + +To measure the code coverage of your tests, use the ``coverage`` command +to run pytest instead of running it directly. + +.. code-block:: none + + $ coverage run -m pytest + +You can either view a simple coverage report in the terminal: + +.. code-block:: none + + $ coverage report + + Name Stmts Miss Branch BrPart Cover + ------------------------------------------------------ + flaskr/__init__.py 21 0 2 0 100% + flaskr/auth.py 54 0 22 0 100% + flaskr/blog.py 54 0 16 0 100% + flaskr/db.py 24 0 4 0 100% + ------------------------------------------------------ + TOTAL 153 0 44 0 100% + +An HTML report allows you to see which lines were covered in each file: + +.. code-block:: none + + $ coverage html + +This generates files in the ``htmlcov`` directory. Open +``htmlcov/index.html`` in your browser to see the report. + +Continue to :doc:`deploy`. diff --git a/test/fixtures/whole_applications/flask/docs/tutorial/views.rst b/test/fixtures/whole_applications/flask/docs/tutorial/views.rst new file mode 100644 index 0000000..7092dbc --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/tutorial/views.rst @@ -0,0 +1,305 @@ +.. currentmodule:: flask + +Blueprints and Views +==================== + +A view function is the code you write to respond to requests to your +application. Flask uses patterns to match the incoming request URL to +the view that should handle it. The view returns data that Flask turns +into an outgoing response. Flask can also go the other direction and +generate a URL to a view based on its name and arguments. + + +Create a Blueprint +------------------ + +A :class:`Blueprint` is a way to organize a group of related views and +other code. Rather than registering views and other code directly with +an application, they are registered with a blueprint. Then the blueprint +is registered with the application when it is available in the factory +function. + +Flaskr will have two blueprints, one for authentication functions and +one for the blog posts functions. The code for each blueprint will go +in a separate module. Since the blog needs to know about authentication, +you'll write the authentication one first. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + import functools + + from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for + ) + from werkzeug.security import check_password_hash, generate_password_hash + + from flaskr.db import get_db + + bp = Blueprint('auth', __name__, url_prefix='/auth') + +This creates a :class:`Blueprint` named ``'auth'``. Like the application +object, the blueprint needs to know where it's defined, so ``__name__`` +is passed as the second argument. The ``url_prefix`` will be prepended +to all the URLs associated with the blueprint. + +Import and register the blueprint from the factory using +:meth:`app.register_blueprint() `. Place the +new code at the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import auth + app.register_blueprint(auth.bp) + + return app + +The authentication blueprint will have views to register new users and +to log in and log out. + + +The First View: Register +------------------------ + +When the user visits the ``/auth/register`` URL, the ``register`` view +will return `HTML`_ with a form for them to fill out. When they submit +the form, it will validate their input and either show the form again +with an error message or create the new user and go to the login page. + +.. _HTML: https://developer.mozilla.org/docs/Web/HTML + +For now you will just write the view code. On the next page, you'll +write templates to generate the HTML form. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/register', methods=('GET', 'POST')) + def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + + if error is None: + try: + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)), + ) + db.commit() + except db.IntegrityError: + error = f"User {username} is already registered." + else: + return redirect(url_for("auth.login")) + + flash(error) + + return render_template('auth/register.html') + +Here's what the ``register`` view function is doing: + +#. :meth:`@bp.route ` associates the URL ``/register`` + with the ``register`` view function. When Flask receives a request + to ``/auth/register``, it will call the ``register`` view and use + the return value as the response. + +#. If the user submitted the form, + :attr:`request.method ` will be ``'POST'``. In this + case, start validating the input. + +#. :attr:`request.form ` is a special type of + :class:`dict` mapping submitted form keys and values. The user will + input their ``username`` and ``password``. + +#. Validate that ``username`` and ``password`` are not empty. + +#. If validation succeeds, insert the new user data into the database. + + - :meth:`db.execute ` takes a SQL + query with ``?`` placeholders for any user input, and a tuple of + values to replace the placeholders with. The database library + will take care of escaping the values so you are not vulnerable + to a *SQL injection attack*. + + - For security, passwords should never be stored in the database + directly. Instead, + :func:`~werkzeug.security.generate_password_hash` is used to + securely hash the password, and that hash is stored. Since this + query modifies data, + :meth:`db.commit() ` needs to be + called afterwards to save the changes. + + - An :exc:`sqlite3.IntegrityError` will occur if the username + already exists, which should be shown to the user as another + validation error. + +#. After storing the user, they are redirected to the login page. + :func:`url_for` generates the URL for the login view based on its + name. This is preferable to writing the URL directly as it allows + you to change the URL later without changing all code that links to + it. :func:`redirect` generates a redirect response to the generated + URL. + +#. If validation fails, the error is shown to the user. :func:`flash` + stores messages that can be retrieved when rendering the template. + +#. When the user initially navigates to ``auth/register``, or + there was a validation error, an HTML page with the registration + form should be shown. :func:`render_template` will render a template + containing the HTML, which you'll write in the next step of the + tutorial. + + +Login +----- + +This view follows the same pattern as the ``register`` view above. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/login', methods=('GET', 'POST')) + def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + +There are a few differences from the ``register`` view: + +#. The user is queried first and stored in a variable for later use. + + :meth:`~sqlite3.Cursor.fetchone` returns one row from the query. + If the query returned no results, it returns ``None``. Later, + :meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list + of all results. + +#. :func:`~werkzeug.security.check_password_hash` hashes the submitted + password in the same way as the stored hash and securely compares + them. If they match, the password is valid. + +#. :data:`session` is a :class:`dict` that stores data across requests. + When validation succeeds, the user's ``id`` is stored in a new + session. The data is stored in a *cookie* that is sent to the + browser, and the browser then sends it back with subsequent requests. + Flask securely *signs* the data so that it can't be tampered with. + +Now that the user's ``id`` is stored in the :data:`session`, it will be +available on subsequent requests. At the beginning of each request, if +a user is logged in their information should be loaded and made +available to other views. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.before_app_request + def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + +:meth:`bp.before_app_request() ` registers +a function that runs before the view function, no matter what URL is +requested. ``load_logged_in_user`` checks if a user id is stored in the +:data:`session` and gets that user's data from the database, storing it +on :data:`g.user `, which lasts for the length of the request. If +there is no user id, or if the id doesn't exist, ``g.user`` will be +``None``. + + +Logout +------ + +To log out, you need to remove the user id from the :data:`session`. +Then ``load_logged_in_user`` won't load a user on subsequent requests. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/logout') + def logout(): + session.clear() + return redirect(url_for('index')) + + +Require Authentication in Other Views +------------------------------------- + +Creating, editing, and deleting blog posts will require a user to be +logged in. A *decorator* can be used to check this for each view it's +applied to. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view + +This decorator returns a new view function that wraps the original view +it's applied to. The new function checks if a user is loaded and +redirects to the login page otherwise. If a user is loaded the original +view is called and continues normally. You'll use this decorator when +writing the blog views. + +Endpoints and URLs +------------------ + +The :func:`url_for` function generates the URL to a view based on a name +and arguments. The name associated with a view is also called the +*endpoint*, and by default it's the same as the name of the view +function. + +For example, the ``hello()`` view that was added to the app +factory earlier in the tutorial has the name ``'hello'`` and can be +linked to with ``url_for('hello')``. If it took an argument, which +you'll see later, it would be linked to using +``url_for('hello', who='World')``. + +When using a blueprint, the name of the blueprint is prepended to the +name of the function, so the endpoint for the ``login`` function you +wrote above is ``'auth.login'`` because you added it to the ``'auth'`` +blueprint. + +Continue to :doc:`templates`. diff --git a/test/fixtures/whole_applications/flask/docs/views.rst b/test/fixtures/whole_applications/flask/docs/views.rst new file mode 100644 index 0000000..f221027 --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/views.rst @@ -0,0 +1,324 @@ +Class-based Views +================= + +.. currentmodule:: flask.views + +This page introduces using the :class:`View` and :class:`MethodView` +classes to write class-based views. + +A class-based view is a class that acts as a view function. Because it +is a class, different instances of the class can be created with +different arguments, to change the behavior of the view. This is also +known as generic, reusable, or pluggable views. + +An example of where this is useful is defining a class that creates an +API based on the database model it is initialized with. + +For more complex API behavior and customization, look into the various +API extensions for Flask. + + +Basic Reusable View +------------------- + +Let's walk through an example converting a view function to a view +class. We start with a view function that queries a list of users then +renders a template to show the list. + +.. code-block:: python + + @app.route("/users/") + def user_list(): + users = User.query.all() + return render_template("users.html", users=users) + +This works for the user model, but let's say you also had more models +that needed list pages. You'd need to write another view function for +each model, even though the only thing that would change is the model +and template name. + +Instead, you can write a :class:`View` subclass that will query a model +and render a template. As the first step, we'll convert the view to a +class without any customization. + +.. code-block:: python + + from flask.views import View + + class UserList(View): + def dispatch_request(self): + users = User.query.all() + return render_template("users.html", objects=users) + + app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) + +The :meth:`View.dispatch_request` method is the equivalent of the view +function. Calling :meth:`View.as_view` method will create a view +function that can be registered on the app with its +:meth:`~flask.Flask.add_url_rule` method. The first argument to +``as_view`` is the name to use to refer to the view with +:func:`~flask.url_for`. + +.. note:: + + You can't decorate the class with ``@app.route()`` the way you'd + do with a basic view function. + +Next, we need to be able to register the same view class for different +models and templates, to make it more useful than the original function. +The class will take two arguments, the model and template, and store +them on ``self``. Then ``dispatch_request`` can reference these instead +of hard-coded values. + +.. code-block:: python + + class ListView(View): + def __init__(self, model, template): + self.model = model + self.template = template + + def dispatch_request(self): + items = self.model.query.all() + return render_template(self.template, items=items) + +Remember, we create the view function with ``View.as_view()`` instead of +creating the class directly. Any extra arguments passed to ``as_view`` +are then passed when creating the class. Now we can register the same +view to handle multiple models. + +.. code-block:: python + + app.add_url_rule( + "/users/", + view_func=ListView.as_view("user_list", User, "users.html"), + ) + app.add_url_rule( + "/stories/", + view_func=ListView.as_view("story_list", Story, "stories.html"), + ) + + +URL Variables +------------- + +Any variables captured by the URL are passed as keyword arguments to the +``dispatch_request`` method, as they would be for a regular view +function. + +.. code-block:: python + + class DetailView(View): + def __init__(self, model): + self.model = model + self.template = f"{model.__name__.lower()}/detail.html" + + def dispatch_request(self, id) + item = self.model.query.get_or_404(id) + return render_template(self.template, item=item) + + app.add_url_rule( + "/users/", + view_func=DetailView.as_view("user_detail", User) + ) + + +View Lifetime and ``self`` +-------------------------- + +By default, a new instance of the view class is created every time a +request is handled. This means that it is safe to write other data to +``self`` during the request, since the next request will not see it, +unlike other forms of global state. + +However, if your view class needs to do a lot of complex initialization, +doing it for every request is unnecessary and can be inefficient. To +avoid this, set :attr:`View.init_every_request` to ``False``, which will +only create one instance of the class and use it for every request. In +this case, writing to ``self`` is not safe. If you need to store data +during the request, use :data:`~flask.g` instead. + +In the ``ListView`` example, nothing writes to ``self`` during the +request, so it is more efficient to create a single instance. + +.. code-block:: python + + class ListView(View): + init_every_request = False + + def __init__(self, model, template): + self.model = model + self.template = template + + def dispatch_request(self): + items = self.model.query.all() + return render_template(self.template, items=items) + +Different instances will still be created each for each ``as_view`` +call, but not for each request to those views. + + +View Decorators +--------------- + +The view class itself is not the view function. View decorators need to +be applied to the view function returned by ``as_view``, not the class +itself. Set :attr:`View.decorators` to a list of decorators to apply. + +.. code-block:: python + + class UserList(View): + decorators = [cache(minutes=2), login_required] + + app.add_url_rule('/users/', view_func=UserList.as_view()) + +If you didn't set ``decorators``, you could apply them manually instead. +This is equivalent to: + +.. code-block:: python + + view = UserList.as_view("users_list") + view = cache(minutes=2)(view) + view = login_required(view) + app.add_url_rule('/users/', view_func=view) + +Keep in mind that order matters. If you're used to ``@decorator`` style, +this is equivalent to: + +.. code-block:: python + + @app.route("/users/") + @login_required + @cache(minutes=2) + def user_list(): + ... + + +Method Hints +------------ + +A common pattern is to register a view with ``methods=["GET", "POST"]``, +then check ``request.method == "POST"`` to decide what to do. Setting +:attr:`View.methods` is equivalent to passing the list of methods to +``add_url_rule`` or ``route``. + +.. code-block:: python + + class MyView(View): + methods = ["GET", "POST"] + + def dispatch_request(self): + if request.method == "POST": + ... + ... + + app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) + +This is equivalent to the following, except further subclasses can +inherit or change the methods. + +.. code-block:: python + + app.add_url_rule( + "/my-view", + view_func=MyView.as_view("my-view"), + methods=["GET", "POST"], + ) + + +Method Dispatching and APIs +--------------------------- + +For APIs it can be helpful to use a different function for each HTTP +method. :class:`MethodView` extends the basic :class:`View` to dispatch +to different methods of the class based on the request method. Each HTTP +method maps to a method of the class with the same (lowercase) name. + +:class:`MethodView` automatically sets :attr:`View.methods` based on the +methods defined by the class. It even knows how to handle subclasses +that override or define other methods. + +We can make a generic ``ItemAPI`` class that provides get (detail), +patch (edit), and delete methods for a given model. A ``GroupAPI`` can +provide get (list) and post (create) methods. + +.. code-block:: python + + from flask.views import MethodView + + class ItemAPI(MethodView): + init_every_request = False + + def __init__(self, model): + self.model = model + self.validator = generate_validator(model) + + def _get_item(self, id): + return self.model.query.get_or_404(id) + + def get(self, id): + item = self._get_item(id) + return jsonify(item.to_json()) + + def patch(self, id): + item = self._get_item(id) + errors = self.validator.validate(item, request.json) + + if errors: + return jsonify(errors), 400 + + item.update_from_json(request.json) + db.session.commit() + return jsonify(item.to_json()) + + def delete(self, id): + item = self._get_item(id) + db.session.delete(item) + db.session.commit() + return "", 204 + + class GroupAPI(MethodView): + init_every_request = False + + def __init__(self, model): + self.model = model + self.validator = generate_validator(model, create=True) + + def get(self): + items = self.model.query.all() + return jsonify([item.to_json() for item in items]) + + def post(self): + errors = self.validator.validate(request.json) + + if errors: + return jsonify(errors), 400 + + db.session.add(self.model.from_json(request.json)) + db.session.commit() + return jsonify(item.to_json()) + + def register_api(app, model, name): + item = ItemAPI.as_view(f"{name}-item", model) + group = GroupAPI.as_view(f"{name}-group", model) + app.add_url_rule(f"/{name}/", view_func=item) + app.add_url_rule(f"/{name}/", view_func=group) + + register_api(app, User, "users") + register_api(app, Story, "stories") + +This produces the following views, a standard REST API! + +================= ========== =================== +URL Method Description +----------------- ---------- ------------------- +``/users/`` ``GET`` List all users +``/users/`` ``POST`` Create a new user +``/users/`` ``GET`` Show a single user +``/users/`` ``PATCH`` Update a user +``/users/`` ``DELETE`` Delete a user +``/stories/`` ``GET`` List all stories +``/stories/`` ``POST`` Create a new story +``/stories/`` ``GET`` Show a single story +``/stories/`` ``PATCH`` Update a story +``/stories/`` ``DELETE`` Delete a story +================= ========== =================== diff --git a/test/fixtures/whole_applications/flask/docs/web-security.rst b/test/fixtures/whole_applications/flask/docs/web-security.rst new file mode 100644 index 0000000..3992e8d --- /dev/null +++ b/test/fixtures/whole_applications/flask/docs/web-security.rst @@ -0,0 +1,274 @@ +Security Considerations +======================= + +Web applications usually face all kinds of security problems and it's very +hard to get everything right. Flask tries to solve a few of these things +for you, but there are a couple more you have to take care of yourself. + +.. _security-xss: + +Cross-Site Scripting (XSS) +-------------------------- + +Cross site scripting is the concept of injecting arbitrary HTML (and with +it JavaScript) into the context of a website. To remedy this, developers +have to properly escape text so that it cannot include arbitrary HTML +tags. For more information on that have a look at the Wikipedia article +on `Cross-Site Scripting +`_. + +Flask configures Jinja2 to automatically escape all values unless +explicitly told otherwise. This should rule out all XSS problems caused +in templates, but there are still other places where you have to be +careful: + +- generating HTML without the help of Jinja2 +- calling :class:`~markupsafe.Markup` on data submitted by users +- sending out HTML from uploaded files, never do that, use the + ``Content-Disposition: attachment`` header to prevent that problem. +- sending out textfiles from uploaded files. Some browsers are using + content-type guessing based on the first few bytes so users could + trick a browser to execute HTML. + +Another thing that is very important are unquoted attributes. While +Jinja2 can protect you from XSS issues by escaping HTML, there is one +thing it cannot protect you from: XSS by attribute injection. To counter +this possible attack vector, be sure to always quote your attributes with +either double or single quotes when using Jinja expressions in them: + +.. sourcecode:: html+jinja + + + +Why is this necessary? Because if you would not be doing that, an +attacker could easily inject custom JavaScript handlers. For example an +attacker could inject this piece of HTML+JavaScript: + +.. sourcecode:: html + + onmouseover=alert(document.cookie) + +When the user would then move with the mouse over the input, the cookie +would be presented to the user in an alert window. But instead of showing +the cookie to the user, a good attacker might also execute any other +JavaScript code. In combination with CSS injections the attacker might +even make the element fill out the entire page so that the user would +just have to have the mouse anywhere on the page to trigger the attack. + +There is one class of XSS issues that Jinja's escaping does not protect +against. The ``a`` tag's ``href`` attribute can contain a `javascript:` URI, +which the browser will execute when clicked if not secured properly. + +.. sourcecode:: html + + click here + click here + +To prevent this, you'll need to set the :ref:`security-csp` response header. + +Cross-Site Request Forgery (CSRF) +--------------------------------- + +Another big problem is CSRF. This is a very complex topic and I won't +outline it here in detail just mention what it is and how to theoretically +prevent it. + +If your authentication information is stored in cookies, you have implicit +state management. The state of "being logged in" is controlled by a +cookie, and that cookie is sent with each request to a page. +Unfortunately that includes requests triggered by 3rd party sites. If you +don't keep that in mind, some people might be able to trick your +application's users with social engineering to do stupid things without +them knowing. + +Say you have a specific URL that, when you sent ``POST`` requests to will +delete a user's profile (say ``http://example.com/user/delete``). If an +attacker now creates a page that sends a post request to that page with +some JavaScript they just have to trick some users to load that page and +their profiles will end up being deleted. + +Imagine you were to run Facebook with millions of concurrent users and +someone would send out links to images of little kittens. When users +would go to that page, their profiles would get deleted while they are +looking at images of fluffy cats. + +How can you prevent that? Basically for each request that modifies +content on the server you would have to either use a one-time token and +store that in the cookie **and** also transmit it with the form data. +After receiving the data on the server again, you would then have to +compare the two tokens and ensure they are equal. + +Why does Flask not do that for you? The ideal place for this to happen is +the form validation framework, which does not exist in Flask. + +.. _security-json: + +JSON Security +------------- + +In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level +arrays to JSON. This was because of a security vulnerability in ECMAScript 4. + +ECMAScript 5 closed this vulnerability, so only extremely old browsers are +still vulnerable. All of these browsers have `other more serious +vulnerabilities +`_, so +this behavior was changed and :func:`~flask.jsonify` now supports serializing +arrays. + +Security Headers +---------------- + +Browsers recognize various response headers in order to control security. We +recommend reviewing each of the headers below for use in your application. +The `Flask-Talisman`_ extension can be used to manage HTTPS and the security +headers for you. + +.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman + +HTTP Strict Transport Security (HSTS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tells the browser to convert all HTTP requests to HTTPS, preventing +man-in-the-middle (MITM) attacks. :: + + response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + +.. _security-csp: + +Content Security Policy (CSP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tell the browser where it can load various types of resource from. This header +should be used whenever possible, but requires some work to define the correct +policy for your site. A very strict policy would be:: + + response.headers['Content-Security-Policy'] = "default-src 'self'" + +- https://csp.withgoogle.com/docs/index.html +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +X-Content-Type-Options +~~~~~~~~~~~~~~~~~~~~~~ + +Forces the browser to honor the response content type instead of trying to +detect it, which can be abused to generate a cross-site scripting (XSS) +attack. :: + + response.headers['X-Content-Type-Options'] = 'nosniff' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + +X-Frame-Options +~~~~~~~~~~~~~~~ + +Prevents external sites from embedding your site in an ``iframe``. This +prevents a class of attacks where clicks in the outer frame can be translated +invisibly to clicks on your page's elements. This is also known as +"clickjacking". :: + + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + +.. _security-cookie: + +Set-Cookie options +~~~~~~~~~~~~~~~~~~ + +These options can be added to a ``Set-Cookie`` header to improve their +security. Flask has configuration options to set these on the session cookie. +They can be set on other cookies too. + +- ``Secure`` limits cookies to HTTPS traffic only. +- ``HttpOnly`` protects the contents of cookies from being read with + JavaScript. +- ``SameSite`` restricts how cookies are sent with requests from + external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``. + ``Lax`` prevents sending cookies with CSRF-prone requests from + external sites, such as submitting a form. ``Strict`` prevents sending + cookies with all external requests, including following regular links. + +:: + + app.config.update( + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE='Lax', + ) + + response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax') + +Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after +the given time, or the current time plus the age, respectively. If neither +option is set, the cookie will be removed when the browser is closed. :: + + # cookie expires after 10 minutes + response.set_cookie('snakes', '3', max_age=600) + +For the session cookie, if :attr:`session.permanent ` +is set, then :data:`PERMANENT_SESSION_LIFETIME` is used to set the expiration. +Flask's default cookie implementation validates that the cryptographic +signature is not older than this value. Lowering this value may help mitigate +replay attacks, where intercepted cookies can be sent at a later time. :: + + app.config.update( + PERMANENT_SESSION_LIFETIME=600 + ) + + @app.route('/login', methods=['POST']) + def login(): + ... + session.clear() + session['user_id'] = user.id + session.permanent = True + ... + +Use :class:`itsdangerous.TimedSerializer` to sign and validate other cookie +values (or any values that need secure signatures). + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + +.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute + + +HTTP Public Key Pinning (HPKP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tells the browser to authenticate with the server using only the specific +certificate key to prevent MITM attacks. + +.. warning:: + Be careful when enabling this, as it is very difficult to undo if you set up + or upgrade your key incorrectly. + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning + + +Copy/Paste to Terminal +---------------------- + +Hidden characters such as the backspace character (``\b``, ``^H``) can +cause text to render differently in HTML than how it is interpreted if +`pasted into a terminal `__. + +For example, ``import y\bose\bm\bi\bt\be\b`` renders as +``import yosemite`` in HTML, but the backspaces are applied when pasted +into a terminal, and it becomes ``import os``. + +If you expect users to copy and paste untrusted code from your site, +such as from comments posted by users on a technical blog, consider +applying extra filtering, such as replacing all ``\b`` characters. + +.. code-block:: python + + body = body.replace("\b", "") + +Most modern terminals will warn about and remove hidden characters when +pasting, so this isn't strictly necessary. It's also possible to craft +dangerous commands in other ways that aren't possible to filter. +Depending on your site's use case, it may be good to show a warning +about copying code in general. diff --git a/test/fixtures/whole_applications/flask/examples/celery/README.md b/test/fixtures/whole_applications/flask/examples/celery/README.md new file mode 100644 index 0000000..038eb51 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/README.md @@ -0,0 +1,27 @@ +Background Tasks with Celery +============================ + +This example shows how to configure Celery with Flask, how to set up an API for +submitting tasks and polling results, and how to use that API with JavaScript. See +[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). + +From this directory, create a virtualenv and install the application into it. Then run a +Celery worker. + +```shell +$ python3 -m venv .venv +$ . ./.venv/bin/activate +$ pip install -r requirements.txt && pip install -e . +$ celery -A make_celery worker --loglevel INFO +``` + +In a separate terminal, activate the virtualenv and run the Flask development server. + +```shell +$ . ./.venv/bin/activate +$ flask -A task_app run --debug +``` + +Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling +requests in the browser dev tools and the Flask logs. You can see the tasks submitting +and completing in the Celery logs. diff --git a/test/fixtures/whole_applications/flask/examples/celery/make_celery.py b/test/fixtures/whole_applications/flask/examples/celery/make_celery.py new file mode 100644 index 0000000..f7d138e --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/make_celery.py @@ -0,0 +1,4 @@ +from task_app import create_app + +flask_app = create_app() +celery_app = flask_app.extensions["celery"] diff --git a/test/fixtures/whole_applications/flask/examples/celery/pyproject.toml b/test/fixtures/whole_applications/flask/examples/celery/pyproject.toml new file mode 100644 index 0000000..25887ca --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "flask-example-celery" +version = "1.0.0" +description = "Example Flask application with Celery background tasks." +readme = "README.md" +requires-python = ">=3.8" +dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "task_app" + +[tool.ruff] +src = ["src"] diff --git a/test/fixtures/whole_applications/flask/examples/celery/requirements.txt b/test/fixtures/whole_applications/flask/examples/celery/requirements.txt new file mode 100644 index 0000000..29075ab --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/requirements.txt @@ -0,0 +1,58 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --resolver=backtracking pyproject.toml +# +amqp==5.1.1 + # via kombu +async-timeout==4.0.2 + # via redis +billiard==3.6.4.0 + # via celery +blinker==1.6.2 + # via flask +celery[redis]==5.2.7 + # via flask-example-celery (pyproject.toml) +click==8.1.3 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # flask +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.2.0 + # via celery +flask==2.3.2 + # via flask-example-celery (pyproject.toml) +itsdangerous==2.1.2 + # via flask +jinja2==3.1.2 + # via flask +kombu==5.2.4 + # via celery +markupsafe==2.1.2 + # via + # jinja2 + # werkzeug +prompt-toolkit==3.0.38 + # via click-repl +pytz==2023.3 + # via celery +redis==4.5.4 + # via celery +six==1.16.0 + # via click-repl +vine==5.0.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.6 + # via prompt-toolkit +werkzeug==2.3.3 + # via flask diff --git a/test/fixtures/whole_applications/flask/examples/celery/src/task_app/__init__.py b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/__init__.py new file mode 100644 index 0000000..dafff8a --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/__init__.py @@ -0,0 +1,39 @@ +from celery import Celery +from celery import Task +from flask import Flask +from flask import render_template + + +def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + + @app.route("/") + def index() -> str: + return render_template("index.html") + + from . import views + + app.register_blueprint(views.bp) + return app + + +def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: + with app.app_context(): + return self.run(*args, **kwargs) + + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app diff --git a/test/fixtures/whole_applications/flask/examples/celery/src/task_app/tasks.py b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/tasks.py new file mode 100644 index 0000000..b6b3595 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/tasks.py @@ -0,0 +1,23 @@ +import time + +from celery import shared_task +from celery import Task + + +@shared_task(ignore_result=False) +def add(a: int, b: int) -> int: + return a + b + + +@shared_task() +def block() -> None: + time.sleep(5) + + +@shared_task(bind=True, ignore_result=False) +def process(self: Task, total: int) -> object: + for i in range(total): + self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) + time.sleep(1) + + return {"current": total, "total": total} diff --git a/test/fixtures/whole_applications/flask/examples/celery/src/task_app/templates/index.html b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/templates/index.html new file mode 100644 index 0000000..4e1145c --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/templates/index.html @@ -0,0 +1,108 @@ + + + + + Celery Example + + +

Celery Example

+Execute background tasks with Celery. Submits tasks and shows results using JavaScript. + +
+

Add

+

Start a task to add two numbers, then poll for the result. +

+
+
+ +
+

Result:

+ +
+

Block

+

Start a task that takes 5 seconds. However, the response will return immediately. +

+ +
+

+ +
+

Process

+

Start a task that counts, waiting one second each time, showing progress. +

+
+ +
+

+ + + + diff --git a/test/fixtures/whole_applications/flask/examples/celery/src/task_app/views.py b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/views.py new file mode 100644 index 0000000..99cf92d --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/celery/src/task_app/views.py @@ -0,0 +1,38 @@ +from celery.result import AsyncResult +from flask import Blueprint +from flask import request + +from . import tasks + +bp = Blueprint("tasks", __name__, url_prefix="/tasks") + + +@bp.get("/result/") +def result(id: str) -> dict[str, object]: + result = AsyncResult(id) + ready = result.ready() + return { + "ready": ready, + "successful": result.successful() if ready else None, + "value": result.get() if ready else result.result, + } + + +@bp.post("/add") +def add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = tasks.add.delay(a, b) + return {"result_id": result.id} + + +@bp.post("/block") +def block() -> dict[str, object]: + result = tasks.block.delay() + return {"result_id": result.id} + + +@bp.post("/process") +def process() -> dict[str, object]: + result = tasks.process.delay(total=request.form.get("total", type=int)) + return {"result_id": result.id} diff --git a/test/fixtures/whole_applications/flask/examples/javascript/.gitignore b/test/fixtures/whole_applications/flask/examples/javascript/.gitignore new file mode 100644 index 0000000..a306afb --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/.gitignore @@ -0,0 +1,14 @@ +.venv/ +*.pyc +__pycache__/ +instance/ +.cache/ +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.idea/ +*.swp +*~ diff --git a/test/fixtures/whole_applications/flask/examples/javascript/LICENSE.rst b/test/fixtures/whole_applications/flask/examples/javascript/LICENSE.rst new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/whole_applications/flask/examples/javascript/README.rst b/test/fixtures/whole_applications/flask/examples/javascript/README.rst new file mode 100644 index 0000000..697bb21 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/README.rst @@ -0,0 +1,48 @@ +JavaScript Ajax Example +======================= + +Demonstrates how to post form data and process a JSON response using +JavaScript. This allows making requests without navigating away from the +page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and +|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax. + +.. |fetch| replace:: ``fetch`` +.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch + +.. |XMLHttpRequest| replace:: ``XMLHttpRequest`` +.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest + +.. |jQuery.ajax| replace:: ``jQuery.ajax`` +.. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ + +.. _Flask docs: https://flask.palletsprojects.com/patterns/jquery/ + + +Install +------- + +.. code-block:: text + + $ python3 -m venv .venv + $ . .venv/bin/activate + $ pip install -e . + + +Run +--- + +.. code-block:: text + + $ flask --app js_example run + +Open http://127.0.0.1:5000 in a browser. + + +Test +---- + +.. code-block:: text + + $ pip install -e '.[test]' + $ coverage run -m pytest + $ coverage report diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/__init__.py b/test/fixtures/whole_applications/flask/examples/javascript/js_example/__init__.py new file mode 100644 index 0000000..0ec3ca2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/__init__.py @@ -0,0 +1,5 @@ +from flask import Flask + +app = Flask(__name__) + +from js_example import views # noqa: E402, F401 diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/base.html b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/base.html new file mode 100644 index 0000000..a4d35bd --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/base.html @@ -0,0 +1,33 @@ + +JavaScript Example + + + + +
+

{% block intro %}{% endblock %}

+
+
+ + + + + +
+= +{% block script %}{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/fetch.html b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/fetch.html new file mode 100644 index 0000000..e2944b8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/fetch.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block intro %} + fetch + is the modern plain JavaScript way to make requests. It's + supported in all modern browsers. +{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/jquery.html b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/jquery.html new file mode 100644 index 0000000..48f0c11 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/jquery.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block intro %} + jQuery is a popular library that + adds cross browser APIs for common tasks. However, it requires loading + an extra library. +{% endblock %} + +{% block script %} + + +{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/xhr.html b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/xhr.html new file mode 100644 index 0000000..1672d4d --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/templates/xhr.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block intro %} + XMLHttpRequest + is the original JavaScript way to make requests. It's natively supported + by all browsers, but has been superseded by + fetch. +{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/javascript/js_example/views.py b/test/fixtures/whole_applications/flask/examples/javascript/js_example/views.py new file mode 100644 index 0000000..9f0d26c --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/js_example/views.py @@ -0,0 +1,18 @@ +from flask import jsonify +from flask import render_template +from flask import request + +from . import app + + +@app.route("/", defaults={"js": "fetch"}) +@app.route("/") +def index(js): + return render_template(f"{js}.html", js=js) + + +@app.route("/add", methods=["POST"]) +def add(): + a = request.form.get("a", 0, type=float) + b = request.form.get("b", 0, type=float) + return jsonify(result=a + b) diff --git a/test/fixtures/whole_applications/flask/examples/javascript/pyproject.toml b/test/fixtures/whole_applications/flask/examples/javascript/pyproject.toml new file mode 100644 index 0000000..0ec631d --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "js_example" +version = "1.1.0" +description = "Demonstrates making AJAX requests to Flask." +readme = "README.rst" +license = {file = "LICENSE.rst"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = ["flask"] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/patterns/jquery/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "js_example" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["js_example", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/test/fixtures/whole_applications/flask/examples/javascript/tests/conftest.py b/test/fixtures/whole_applications/flask/examples/javascript/tests/conftest.py new file mode 100644 index 0000000..e0cabbf --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from js_example import app + + +@pytest.fixture(name="app") +def fixture_app(): + app.testing = True + yield app + app.testing = False + + +@pytest.fixture +def client(app): + return app.test_client() diff --git a/test/fixtures/whole_applications/flask/examples/javascript/tests/test_js_example.py b/test/fixtures/whole_applications/flask/examples/javascript/tests/test_js_example.py new file mode 100644 index 0000000..d155ad5 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/javascript/tests/test_js_example.py @@ -0,0 +1,27 @@ +import pytest +from flask import template_rendered + + +@pytest.mark.parametrize( + ("path", "template_name"), + ( + ("/", "xhr.html"), + ("/plain", "xhr.html"), + ("/fetch", "fetch.html"), + ("/jquery", "jquery.html"), + ), +) +def test_index(app, client, path, template_name): + def check(sender, template, context): + assert template.name == template_name + + with template_rendered.connected_to(check, app): + client.get(path) + + +@pytest.mark.parametrize( + ("a", "b", "result"), ((2, 3, 5), (2.5, 3, 5.5), (2, None, 2), (2, "b", 2)) +) +def test_add(client, a, b, result): + response = client.post("/add", data={"a": a, "b": b}) + assert response.get_json()["result"] == result diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/.gitignore b/test/fixtures/whole_applications/flask/examples/tutorial/.gitignore new file mode 100644 index 0000000..a306afb --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/.gitignore @@ -0,0 +1,14 @@ +.venv/ +*.pyc +__pycache__/ +instance/ +.cache/ +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.idea/ +*.swp +*~ diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/LICENSE.rst b/test/fixtures/whole_applications/flask/examples/tutorial/LICENSE.rst new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/README.rst b/test/fixtures/whole_applications/flask/examples/tutorial/README.rst new file mode 100644 index 0000000..653c216 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/README.rst @@ -0,0 +1,68 @@ +Flaskr +====== + +The basic blog app built in the Flask `tutorial`_. + +.. _tutorial: https://flask.palletsprojects.com/tutorial/ + + +Install +------- + +**Be sure to use the same version of the code as the version of the docs +you're reading.** You probably want the latest tagged version, but the +default Git version is the main branch. :: + + # clone the repository + $ git clone https://github.com/pallets/flask + $ cd flask + # checkout the correct version + $ git tag # shows the tagged versions + $ git checkout latest-tag-found-above + $ cd examples/tutorial + +Create a virtualenv and activate it:: + + $ python3 -m venv .venv + $ . .venv/bin/activate + +Or on Windows cmd:: + + $ py -3 -m venv .venv + $ .venv\Scripts\activate.bat + +Install Flaskr:: + + $ pip install -e . + +Or if you are using the main branch, install Flask from source before +installing Flaskr:: + + $ pip install -e ../.. + $ pip install -e . + + +Run +--- + +.. code-block:: text + + $ flask --app flaskr init-db + $ flask --app flaskr run --debug + +Open http://127.0.0.1:5000 in a browser. + + +Test +---- + +:: + + $ pip install '.[test]' + $ pytest + +Run with coverage report:: + + $ coverage run -m pytest + $ coverage report + $ coverage html # open htmlcov/index.html in a browser diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/__init__.py b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/__init__.py new file mode 100644 index 0000000..e35934d --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/__init__.py @@ -0,0 +1,51 @@ +import os + +from flask import Flask + + +def create_app(test_config=None): + """Create and configure an instance of the Flask application.""" + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + # a default secret that should be overridden by instance config + SECRET_KEY="dev", + # store the database in the instance folder + DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile("config.py", silent=True) + else: + # load the test config if passed in + app.config.update(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + @app.route("/hello") + def hello(): + return "Hello, World!" + + # register the database commands + from . import db + + db.init_app(app) + + # apply the blueprints to the app + from . import auth + from . import blog + + app.register_blueprint(auth.bp) + app.register_blueprint(blog.bp) + + # make url_for('index') == url_for('blog.index') + # in another app, you might define a separate main index here with + # app.route, while giving the blog blueprint a url_prefix, but for + # the tutorial the blog will be the main index + app.add_url_rule("/", endpoint="index") + + return app diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/auth.py b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/auth.py new file mode 100644 index 0000000..34c03a2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/auth.py @@ -0,0 +1,116 @@ +import functools + +from flask import Blueprint +from flask import flash +from flask import g +from flask import redirect +from flask import render_template +from flask import request +from flask import session +from flask import url_for +from werkzeug.security import check_password_hash +from werkzeug.security import generate_password_hash + +from .db import get_db + +bp = Blueprint("auth", __name__, url_prefix="/auth") + + +def login_required(view): + """View decorator that redirects anonymous users to the login page.""" + + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for("auth.login")) + + return view(**kwargs) + + return wrapped_view + + +@bp.before_app_request +def load_logged_in_user(): + """If a user id is stored in the session, load the user object from + the database into ``g.user``.""" + user_id = session.get("user_id") + + if user_id is None: + g.user = None + else: + g.user = ( + get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() + ) + + +@bp.route("/register", methods=("GET", "POST")) +def register(): + """Register a new user. + + Validates that the username is not already taken. Hashes the + password for security. + """ + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + error = None + + if not username: + error = "Username is required." + elif not password: + error = "Password is required." + + if error is None: + try: + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)), + ) + db.commit() + except db.IntegrityError: + # The username was already taken, which caused the + # commit to fail. Show a validation error. + error = f"User {username} is already registered." + else: + # Success, go to the login page. + return redirect(url_for("auth.login")) + + flash(error) + + return render_template("auth/register.html") + + +@bp.route("/login", methods=("GET", "POST")) +def login(): + """Log in a registered user by adding the user id to the session.""" + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + error = None + user = db.execute( + "SELECT * FROM user WHERE username = ?", (username,) + ).fetchone() + + if user is None: + error = "Incorrect username." + elif not check_password_hash(user["password"], password): + error = "Incorrect password." + + if error is None: + # store the user id in a new session and return to the index + session.clear() + session["user_id"] = user["id"] + return redirect(url_for("index")) + + flash(error) + + return render_template("auth/login.html") + + +@bp.route("/logout") +def logout(): + """Clear the current session, including the stored user id.""" + session.clear() + return redirect(url_for("index")) diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/blog.py b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/blog.py new file mode 100644 index 0000000..be0d92c --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/blog.py @@ -0,0 +1,125 @@ +from flask import Blueprint +from flask import flash +from flask import g +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for +from werkzeug.exceptions import abort + +from .auth import login_required +from .db import get_db + +bp = Blueprint("blog", __name__) + + +@bp.route("/") +def index(): + """Show all the posts, most recent first.""" + db = get_db() + posts = db.execute( + "SELECT p.id, title, body, created, author_id, username" + " FROM post p JOIN user u ON p.author_id = u.id" + " ORDER BY created DESC" + ).fetchall() + return render_template("blog/index.html", posts=posts) + + +def get_post(id, check_author=True): + """Get a post and its author by id. + + Checks that the id exists and optionally that the current user is + the author. + + :param id: id of post to get + :param check_author: require the current user to be the author + :return: the post with author information + :raise 404: if a post with the given id doesn't exist + :raise 403: if the current user isn't the author + """ + post = ( + get_db() + .execute( + "SELECT p.id, title, body, created, author_id, username" + " FROM post p JOIN user u ON p.author_id = u.id" + " WHERE p.id = ?", + (id,), + ) + .fetchone() + ) + + if post is None: + abort(404, f"Post id {id} doesn't exist.") + + if check_author and post["author_id"] != g.user["id"]: + abort(403) + + return post + + +@bp.route("/create", methods=("GET", "POST")) +@login_required +def create(): + """Create a new post for the current user.""" + if request.method == "POST": + title = request.form["title"] + body = request.form["body"] + error = None + + if not title: + error = "Title is required." + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", + (title, body, g.user["id"]), + ) + db.commit() + return redirect(url_for("blog.index")) + + return render_template("blog/create.html") + + +@bp.route("//update", methods=("GET", "POST")) +@login_required +def update(id): + """Update a post if the current user is the author.""" + post = get_post(id) + + if request.method == "POST": + title = request.form["title"] + body = request.form["body"] + error = None + + if not title: + error = "Title is required." + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) + ) + db.commit() + return redirect(url_for("blog.index")) + + return render_template("blog/update.html", post=post) + + +@bp.route("//delete", methods=("POST",)) +@login_required +def delete(id): + """Delete a post. + + Ensures that the post exists and that the logged in user is the + author of the post. + """ + get_post(id) + db = get_db() + db.execute("DELETE FROM post WHERE id = ?", (id,)) + db.commit() + return redirect(url_for("blog.index")) diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/db.py b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/db.py new file mode 100644 index 0000000..acaa4ae --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/db.py @@ -0,0 +1,52 @@ +import sqlite3 + +import click +from flask import current_app +from flask import g + + +def get_db(): + """Connect to the application's configured database. The connection + is unique for each request and will be reused if this is called + again. + """ + if "db" not in g: + g.db = sqlite3.connect( + current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + """If this request connected to the database, close the + connection. + """ + db = g.pop("db", None) + + if db is not None: + db.close() + + +def init_db(): + """Clear existing data and create new tables.""" + db = get_db() + + with current_app.open_resource("schema.sql") as f: + db.executescript(f.read().decode("utf8")) + + +@click.command("init-db") +def init_db_command(): + """Clear existing data and create new tables.""" + init_db() + click.echo("Initialized the database.") + + +def init_app(app): + """Register database functions with the Flask app. This is called by + the application factory. + """ + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/schema.sql b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/schema.sql new file mode 100644 index 0000000..dd4c866 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/schema.sql @@ -0,0 +1,20 @@ +-- Initialize the database. +-- Drop any existing data and create empty tables. + +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS post; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE post ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author_id) REFERENCES user (id) +); diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/static/style.css b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/static/style.css new file mode 100644 index 0000000..2f1f4d0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/static/style.css @@ -0,0 +1,134 @@ +html { + font-family: sans-serif; + background: #eee; + padding: 1rem; +} + +body { + max-width: 960px; + margin: 0 auto; + background: white; +} + +h1, h2, h3, h4, h5, h6 { + font-family: serif; + color: #377ba8; + margin: 1rem 0; +} + +a { + color: #377ba8; +} + +hr { + border: none; + border-top: 1px solid lightgray; +} + +nav { + background: lightgray; + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +nav h1 { + flex: auto; + margin: 0; +} + +nav h1 a { + text-decoration: none; + padding: 0.25rem 0.5rem; +} + +nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +nav ul li a, nav ul li span, header .action { + display: block; + padding: 0.5rem; +} + +.content { + padding: 0 1rem 1rem; +} + +.content > header { + border-bottom: 1px solid lightgray; + display: flex; + align-items: flex-end; +} + +.content > header h1 { + flex: auto; + margin: 1rem 0 0.25rem 0; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #cae6f6; + border: 1px solid #377ba8; +} + +.post > header { + display: flex; + align-items: flex-end; + font-size: 0.85em; +} + +.post > header > div:first-of-type { + flex: auto; +} + +.post > header h1 { + font-size: 1.5em; + margin-bottom: 0; +} + +.post .about { + color: slategray; + font-style: italic; +} + +.post .body { + white-space: pre-line; +} + +.content:last-child { + margin-bottom: 0; +} + +.content form { + margin: 1em 0; + display: flex; + flex-direction: column; +} + +.content label { + font-weight: bold; + margin-bottom: 0.5em; +} + +.content input, .content textarea { + margin-bottom: 1em; +} + +.content textarea { + min-height: 12em; + resize: vertical; +} + +input.danger { + color: #cc2f2e; +} + +input[type=submit] { + align-self: start; + min-width: 10em; +} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/login.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/login.html new file mode 100644 index 0000000..b326b5a --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Log In{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/register.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/register.html new file mode 100644 index 0000000..4320e17 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/auth/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/base.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/base.html new file mode 100644 index 0000000..f09e926 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/base.html @@ -0,0 +1,24 @@ + +{% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/create.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/create.html new file mode 100644 index 0000000..88e31e4 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/create.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}New Post{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/index.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/index.html new file mode 100644 index 0000000..3481b8e --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/index.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Posts{% endblock %}

+ {% if g.user %} + New + {% endif %} +{% endblock %} + +{% block content %} + {% for post in posts %} +
+
+
+

{{ post['title'] }}

+
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
+
+ {% if g.user['id'] == post['author_id'] %} + Edit + {% endif %} +
+

{{ post['body'] }}

+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} +{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/update.html b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/update.html new file mode 100644 index 0000000..2c405e6 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/flaskr/templates/blog/update.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+
+
+ +
+{% endblock %} diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/pyproject.toml b/test/fixtures/whole_applications/flask/examples/tutorial/pyproject.toml new file mode 100644 index 0000000..73a674c --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = [ + "flask", +] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/tutorial/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flaskr" + +[tool.flit.sdist] +include = [ + "tests/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["flaskr", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/conftest.py b/test/fixtures/whole_applications/flask/examples/tutorial/tests/conftest.py new file mode 100644 index 0000000..6bf62f0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/conftest.py @@ -0,0 +1,62 @@ +import os +import tempfile + +import pytest + +from flaskr import create_app +from flaskr.db import get_db +from flaskr.db import init_db + +# read in SQL for populating test data +with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: + _data_sql = f.read().decode("utf8") + + +@pytest.fixture +def app(): + """Create and configure a new app instance for each test.""" + # create a temporary file to isolate the database for each test + db_fd, db_path = tempfile.mkstemp() + # create the app with common test config + app = create_app({"TESTING": True, "DATABASE": db_path}) + + # create the database and load test data + with app.app_context(): + init_db() + get_db().executescript(_data_sql) + + yield app + + # close and remove the temporary database + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture +def client(app): + """A test client for the app.""" + return app.test_client() + + +@pytest.fixture +def runner(app): + """A test runner for the app's Click commands.""" + return app.test_cli_runner() + + +class AuthActions: + def __init__(self, client): + self._client = client + + def login(self, username="test", password="test"): + return self._client.post( + "/auth/login", data={"username": username, "password": password} + ) + + def logout(self): + return self._client.get("/auth/logout") + + +@pytest.fixture +def auth(client): + return AuthActions(client) diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/data.sql b/test/fixtures/whole_applications/flask/examples/tutorial/tests/data.sql new file mode 100644 index 0000000..9b68006 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/data.sql @@ -0,0 +1,8 @@ +INSERT INTO user (username, password) +VALUES + ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), + ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); + +INSERT INTO post (title, body, author_id, created) +VALUES + ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_auth.py b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_auth.py new file mode 100644 index 0000000..76db62f --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_auth.py @@ -0,0 +1,69 @@ +import pytest +from flask import g +from flask import session + +from flaskr.db import get_db + + +def test_register(client, app): + # test that viewing the page renders without template errors + assert client.get("/auth/register").status_code == 200 + + # test that successful registration redirects to the login page + response = client.post("/auth/register", data={"username": "a", "password": "a"}) + assert response.headers["Location"] == "/auth/login" + + # test that the user was inserted into the database + with app.app_context(): + assert ( + get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone() + is not None + ) + + +@pytest.mark.parametrize( + ("username", "password", "message"), + ( + ("", "", b"Username is required."), + ("a", "", b"Password is required."), + ("test", "test", b"already registered"), + ), +) +def test_register_validate_input(client, username, password, message): + response = client.post( + "/auth/register", data={"username": username, "password": password} + ) + assert message in response.data + + +def test_login(client, auth): + # test that viewing the page renders without template errors + assert client.get("/auth/login").status_code == 200 + + # test that successful login redirects to the index page + response = auth.login() + assert response.headers["Location"] == "/" + + # login request set the user_id in the session + # check that the user is loaded from the session + with client: + client.get("/") + assert session["user_id"] == 1 + assert g.user["username"] == "test" + + +@pytest.mark.parametrize( + ("username", "password", "message"), + (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), +) +def test_login_validate_input(auth, username, password, message): + response = auth.login(username, password) + assert message in response.data + + +def test_logout(client, auth): + auth.login() + + with client: + auth.logout() + assert "user_id" not in session diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_blog.py b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_blog.py new file mode 100644 index 0000000..55c769d --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_blog.py @@ -0,0 +1,83 @@ +import pytest + +from flaskr.db import get_db + + +def test_index(client, auth): + response = client.get("/") + assert b"Log In" in response.data + assert b"Register" in response.data + + auth.login() + response = client.get("/") + assert b"test title" in response.data + assert b"by test on 2018-01-01" in response.data + assert b"test\nbody" in response.data + assert b'href="/1/update"' in response.data + + +@pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) +def test_login_required(client, path): + response = client.post(path) + assert response.headers["Location"] == "/auth/login" + + +def test_author_required(app, client, auth): + # change the post author to another user + with app.app_context(): + db = get_db() + db.execute("UPDATE post SET author_id = 2 WHERE id = 1") + db.commit() + + auth.login() + # current user can't modify other user's post + assert client.post("/1/update").status_code == 403 + assert client.post("/1/delete").status_code == 403 + # current user doesn't see edit link + assert b'href="/1/update"' not in client.get("/").data + + +@pytest.mark.parametrize("path", ("/2/update", "/2/delete")) +def test_exists_required(client, auth, path): + auth.login() + assert client.post(path).status_code == 404 + + +def test_create(client, auth, app): + auth.login() + assert client.get("/create").status_code == 200 + client.post("/create", data={"title": "created", "body": ""}) + + with app.app_context(): + db = get_db() + count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] + assert count == 2 + + +def test_update(client, auth, app): + auth.login() + assert client.get("/1/update").status_code == 200 + client.post("/1/update", data={"title": "updated", "body": ""}) + + with app.app_context(): + db = get_db() + post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() + assert post["title"] == "updated" + + +@pytest.mark.parametrize("path", ("/create", "/1/update")) +def test_create_update_validate(client, auth, path): + auth.login() + response = client.post(path, data={"title": "", "body": ""}) + assert b"Title is required." in response.data + + +def test_delete(client, auth, app): + auth.login() + response = client.post("/1/delete") + assert response.headers["Location"] == "/" + + with app.app_context(): + db = get_db() + post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() + assert post is None diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_db.py b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_db.py new file mode 100644 index 0000000..2363bf8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_db.py @@ -0,0 +1,29 @@ +import sqlite3 + +import pytest + +from flaskr.db import get_db + + +def test_get_close_db(app): + with app.app_context(): + db = get_db() + assert db is get_db() + + with pytest.raises(sqlite3.ProgrammingError) as e: + db.execute("SELECT 1") + + assert "closed" in str(e.value) + + +def test_init_db_command(runner, monkeypatch): + class Recorder: + called = False + + def fake_init_db(): + Recorder.called = True + + monkeypatch.setattr("flaskr.db.init_db", fake_init_db) + result = runner.invoke(args=["init-db"]) + assert "Initialized" in result.output + assert Recorder.called diff --git a/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_factory.py b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_factory.py new file mode 100644 index 0000000..9b7ca57 --- /dev/null +++ b/test/fixtures/whole_applications/flask/examples/tutorial/tests/test_factory.py @@ -0,0 +1,12 @@ +from flaskr import create_app + + +def test_config(): + """Test create_app without passing test config.""" + assert not create_app().testing + assert create_app({"TESTING": True}).testing + + +def test_hello(client): + response = client.get("/hello") + assert response.data == b"Hello, World!" diff --git a/test/fixtures/whole_applications/flask/pyproject.toml b/test/fixtures/whole_applications/flask/pyproject.toml new file mode 100644 index 0000000..bf14d15 --- /dev/null +++ b/test/fixtures/whole_applications/flask/pyproject.toml @@ -0,0 +1,120 @@ +[project] +name = "Flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +readme = "README.md" +license = {file = "LICENSE.txt"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Typing :: Typed", +] +requires-python = ">=3.8" +dependencies = [ + "Werkzeug>=3.0.0", + "Jinja2>=3.1.2", + "itsdangerous>=2.1.2", + "click>=8.1.3", + "blinker>=1.6.2", + "importlib-metadata>=3.6.0; python_version < '3.10'", +] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://flask.palletsprojects.com/" +Changes = "https://flask.palletsprojects.com/changes/" +Source = "https://github.com/pallets/flask/" +Chat = "https://discord.gg/pallets" + +[project.optional-dependencies] +async = ["asgiref>=3.2"] +dotenv = ["python-dotenv"] + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flask" + +[tool.flit.sdist] +include = [ + "docs/", + "examples/", + "requirements/", + "tests/", + "CHANGES.rst", + "CONTRIBUTING.rst", + "tox.ini", +] +exclude = [ + "docs/_build/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +source = ["flask", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +python_version = "3.8" +files = ["src/flask", "tests/typing"] +show_error_codes = true +pretty = true +strict = true + +[[tool.mypy.overrides]] +module = [ + "asgiref.*", + "dotenv.*", + "cryptography.*", + "importlib_metadata", +] +ignore_missing_imports = true + +[tool.pyright] +pythonVersion = "3.8" +include = ["src/flask", "tests"] +typeCheckingMode = "basic" + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] +ignore-init-module-imports = true + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/test/fixtures/whole_applications/flask/requirements-skip/README.md b/test/fixtures/whole_applications/flask/requirements-skip/README.md new file mode 100644 index 0000000..675ca4a --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements-skip/README.md @@ -0,0 +1,2 @@ +Dependabot will only update files in the `requirements` directory. This directory is +separate because the pins in here should not be updated automatically. diff --git a/test/fixtures/whole_applications/flask/requirements-skip/tests-dev.txt b/test/fixtures/whole_applications/flask/requirements-skip/tests-dev.txt new file mode 100644 index 0000000..3e7f028 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements-skip/tests-dev.txt @@ -0,0 +1,6 @@ +https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz +https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz +https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz +https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz +https://github.com/pallets/click/archive/refs/heads/main.tar.gz +https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz diff --git a/test/fixtures/whole_applications/flask/requirements-skip/tests-min.in b/test/fixtures/whole_applications/flask/requirements-skip/tests-min.in new file mode 100644 index 0000000..c7ec996 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements-skip/tests-min.in @@ -0,0 +1,6 @@ +werkzeug==3.0.0 +jinja2==3.1.2 +markupsafe==2.1.1 +itsdangerous==2.1.2 +click==8.1.3 +blinker==1.6.2 diff --git a/test/fixtures/whole_applications/flask/requirements-skip/tests-min.txt b/test/fixtures/whole_applications/flask/requirements-skip/tests-min.txt new file mode 100644 index 0000000..8a6cbf0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements-skip/tests-min.txt @@ -0,0 +1,21 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile tests-min.in +# +blinker==1.6.2 + # via -r tests-min.in +click==8.1.3 + # via -r tests-min.in +itsdangerous==2.1.2 + # via -r tests-min.in +jinja2==3.1.2 + # via -r tests-min.in +markupsafe==2.1.1 + # via + # -r tests-min.in + # jinja2 + # werkzeug +werkzeug==3.0.0 + # via -r tests-min.in diff --git a/test/fixtures/whole_applications/flask/requirements/build.in b/test/fixtures/whole_applications/flask/requirements/build.in new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/test/fixtures/whole_applications/flask/requirements/build.txt b/test/fixtures/whole_applications/flask/requirements/build.txt new file mode 100644 index 0000000..9ecc489 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/build.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile build.in +# +build==1.2.1 + # via -r build.in +packaging==24.0 + # via build +pyproject-hooks==1.0.0 + # via build diff --git a/test/fixtures/whole_applications/flask/requirements/dev.in b/test/fixtures/whole_applications/flask/requirements/dev.in new file mode 100644 index 0000000..1efde82 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/dev.in @@ -0,0 +1,5 @@ +-r docs.txt +-r tests.txt +-r typing.txt +pre-commit +tox diff --git a/test/fixtures/whole_applications/flask/requirements/dev.txt b/test/fixtures/whole_applications/flask/requirements/dev.txt new file mode 100644 index 0000000..05b8758 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/dev.txt @@ -0,0 +1,195 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile dev.in +# +alabaster==0.7.16 + # via + # -r docs.txt + # sphinx +asgiref==3.8.1 + # via + # -r tests.txt + # -r typing.txt +babel==2.14.0 + # via + # -r docs.txt + # sphinx +cachetools==5.3.3 + # via tox +certifi==2024.2.2 + # via + # -r docs.txt + # requests +cffi==1.16.0 + # via + # -r typing.txt + # cryptography +cfgv==3.4.0 + # via pre-commit +chardet==5.2.0 + # via tox +charset-normalizer==3.3.2 + # via + # -r docs.txt + # requests +colorama==0.4.6 + # via tox +cryptography==42.0.5 + # via -r typing.txt +distlib==0.3.8 + # via virtualenv +docutils==0.20.1 + # via + # -r docs.txt + # sphinx + # sphinx-tabs +filelock==3.13.3 + # via + # tox + # virtualenv +identify==2.5.35 + # via pre-commit +idna==3.6 + # via + # -r docs.txt + # requests +imagesize==1.4.1 + # via + # -r docs.txt + # sphinx +iniconfig==2.0.0 + # via + # -r tests.txt + # -r typing.txt + # pytest +jinja2==3.1.3 + # via + # -r docs.txt + # sphinx +markupsafe==2.1.5 + # via + # -r docs.txt + # jinja2 +mypy==1.9.0 + # via -r typing.txt +mypy-extensions==1.0.0 + # via + # -r typing.txt + # mypy +nodeenv==1.8.0 + # via + # -r typing.txt + # pre-commit + # pyright +packaging==24.0 + # via + # -r docs.txt + # -r tests.txt + # -r typing.txt + # pallets-sphinx-themes + # pyproject-api + # pytest + # sphinx + # tox +pallets-sphinx-themes==2.1.1 + # via -r docs.txt +platformdirs==4.2.0 + # via + # tox + # virtualenv +pluggy==1.4.0 + # via + # -r tests.txt + # -r typing.txt + # pytest + # tox +pre-commit==3.7.0 + # via -r dev.in +pycparser==2.22 + # via + # -r typing.txt + # cffi +pygments==2.17.2 + # via + # -r docs.txt + # sphinx + # sphinx-tabs +pyproject-api==1.6.1 + # via tox +pyright==1.1.357 + # via -r typing.txt +pytest==8.1.1 + # via + # -r tests.txt + # -r typing.txt +python-dotenv==1.0.1 + # via + # -r tests.txt + # -r typing.txt +pyyaml==6.0.1 + # via pre-commit +requests==2.31.0 + # via + # -r docs.txt + # sphinx +snowballstemmer==2.2.0 + # via + # -r docs.txt + # sphinx +sphinx==7.2.6 + # via + # -r docs.txt + # pallets-sphinx-themes + # sphinx-tabs + # sphinxcontrib-log-cabinet +sphinx-tabs==3.4.5 + # via -r docs.txt +sphinxcontrib-applehelp==1.0.8 + # via + # -r docs.txt + # sphinx +sphinxcontrib-devhelp==1.0.6 + # via + # -r docs.txt + # sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via + # -r docs.txt + # sphinx +sphinxcontrib-jsmath==1.0.1 + # via + # -r docs.txt + # sphinx +sphinxcontrib-log-cabinet==1.0.1 + # via -r docs.txt +sphinxcontrib-qthelp==1.0.7 + # via + # -r docs.txt + # sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via + # -r docs.txt + # sphinx +tox==4.14.2 + # via -r dev.in +types-contextvars==2.4.7.3 + # via -r typing.txt +types-dataclasses==0.6.6 + # via -r typing.txt +typing-extensions==4.11.0 + # via + # -r typing.txt + # mypy +urllib3==2.2.1 + # via + # -r docs.txt + # requests +virtualenv==20.25.1 + # via + # pre-commit + # tox + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/test/fixtures/whole_applications/flask/requirements/docs.in b/test/fixtures/whole_applications/flask/requirements/docs.in new file mode 100644 index 0000000..fd5708f --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/docs.in @@ -0,0 +1,4 @@ +pallets-sphinx-themes +sphinx +sphinxcontrib-log-cabinet +sphinx-tabs diff --git a/test/fixtures/whole_applications/flask/requirements/docs.txt b/test/fixtures/whole_applications/flask/requirements/docs.txt new file mode 100644 index 0000000..0975297 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/docs.txt @@ -0,0 +1,64 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile docs.in +# +alabaster==0.7.16 + # via sphinx +babel==2.14.0 + # via sphinx +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +docutils==0.20.1 + # via + # sphinx + # sphinx-tabs +idna==3.6 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.3 + # via sphinx +markupsafe==2.1.5 + # via jinja2 +packaging==24.0 + # via + # pallets-sphinx-themes + # sphinx +pallets-sphinx-themes==2.1.1 + # via -r docs.in +pygments==2.17.2 + # via + # sphinx + # sphinx-tabs +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==7.2.6 + # via + # -r docs.in + # pallets-sphinx-themes + # sphinx-tabs + # sphinxcontrib-log-cabinet +sphinx-tabs==3.4.5 + # via -r docs.in +sphinxcontrib-applehelp==1.0.8 + # via sphinx +sphinxcontrib-devhelp==1.0.6 + # via sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-log-cabinet==1.0.1 + # via -r docs.in +sphinxcontrib-qthelp==1.0.7 + # via sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via sphinx +urllib3==2.2.1 + # via requests diff --git a/test/fixtures/whole_applications/flask/requirements/tests.in b/test/fixtures/whole_applications/flask/requirements/tests.in new file mode 100644 index 0000000..f4b3dad --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/tests.in @@ -0,0 +1,4 @@ +pytest +asgiref +greenlet ; python_version < "3.11" +python-dotenv diff --git a/test/fixtures/whole_applications/flask/requirements/tests.txt b/test/fixtures/whole_applications/flask/requirements/tests.txt new file mode 100644 index 0000000..2146b27 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/tests.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile tests.in +# +asgiref==3.8.1 + # via -r tests.in +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via pytest +pluggy==1.4.0 + # via pytest +pytest==8.1.1 + # via -r tests.in +python-dotenv==1.0.1 + # via -r tests.in diff --git a/test/fixtures/whole_applications/flask/requirements/typing.in b/test/fixtures/whole_applications/flask/requirements/typing.in new file mode 100644 index 0000000..59128f3 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/typing.in @@ -0,0 +1,8 @@ +mypy +pyright +pytest +types-contextvars +types-dataclasses +asgiref +cryptography +python-dotenv diff --git a/test/fixtures/whole_applications/flask/requirements/typing.txt b/test/fixtures/whole_applications/flask/requirements/typing.txt new file mode 100644 index 0000000..fa18143 --- /dev/null +++ b/test/fixtures/whole_applications/flask/requirements/typing.txt @@ -0,0 +1,41 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile typing.in +# +asgiref==3.8.1 + # via -r typing.in +cffi==1.16.0 + # via cryptography +cryptography==42.0.5 + # via -r typing.in +iniconfig==2.0.0 + # via pytest +mypy==1.9.0 + # via -r typing.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.8.0 + # via pyright +packaging==24.0 + # via pytest +pluggy==1.4.0 + # via pytest +pycparser==2.22 + # via cffi +pyright==1.1.357 + # via -r typing.in +pytest==8.1.1 + # via -r typing.in +python-dotenv==1.0.1 + # via -r typing.in +types-contextvars==2.4.7.3 + # via -r typing.in +types-dataclasses==0.6.6 + # via -r typing.in +typing-extensions==4.11.0 + # via mypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/test/fixtures/whole_applications/flask/src/flask/__init__.py b/test/fixtures/whole_applications/flask/src/flask/__init__.py new file mode 100644 index 0000000..e86eb43 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/__init__.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import typing as t + +from . import json as json +from .app import Flask as Flask +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import abort as abort +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import redirect as redirect +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string +from .wrappers import Request as Request +from .wrappers import Response as Response + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Flask 3.1. Use feature detection or" + " 'importlib.metadata.version(\"flask\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask") + + raise AttributeError(name) diff --git a/test/fixtures/whole_applications/flask/src/flask/__main__.py b/test/fixtures/whole_applications/flask/src/flask/__main__.py new file mode 100644 index 0000000..4e28416 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/test/fixtures/whole_applications/flask/src/flask/app.py b/test/fixtures/whole_applications/flask/src/flask/app.py new file mode 100644 index 0000000..7622b5e --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/app.py @@ -0,0 +1,1498 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import sys +import typing as t +import weakref +from datetime import timedelta +from inspect import iscoroutinefunction +from itertools import chain +from types import TracebackType +from urllib.parse import quote as _url_quote + +import click +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.routing import BuildError +from werkzeug.routing import MapAdapter +from werkzeug.routing import RequestRedirect +from werkzeug.routing import RoutingException +from werkzeug.routing import Rule +from werkzeug.serving import is_running_from_reloader +from werkzeug.wrappers import Response as BaseResponse + +from . import cli +from . import typing as ft +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +from .helpers import get_debug_flag +from .helpers import get_flashed_messages +from .helpers import get_load_dotenv +from .helpers import send_from_directory +from .sansio.app import App +from .sansio.scaffold import _sentinel +from .sessions import SecureCookieSessionInterface +from .sessions import SessionInterface +from .signals import appcontext_tearing_down +from .signals import got_request_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .templating import Environment +from .wrappers import Request +from .wrappers import Response + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + + from .testing import FlaskClient + from .testing import FlaskCliRunner + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class Flask(App): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + default_config = ImmutableDict( + { + "DEBUG": None, + "TESTING": False, + "PROPAGATE_EXCEPTIONS": None, + "SECRET_KEY": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "SERVER_NAME": None, + "APPLICATION_ROOT": "/", + "SESSION_COOKIE_NAME": "session", + "SESSION_COOKIE_DOMAIN": None, + "SESSION_COOKIE_PATH": None, + "SESSION_COOKIE_HTTPONLY": True, + "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "SEND_FILE_MAX_AGE_DEFAULT": None, + "TRAP_BAD_REQUEST_ERRORS": None, + "TRAP_HTTP_EXCEPTIONS": False, + "EXPLAIN_TEMPLATE_LOADING": False, + "PREFERRED_URL_SCHEME": "http", + "TEMPLATES_AUTO_RELOAD": None, + "MAX_COOKIE_SIZE": 4093, + } + ) + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class: type[Request] = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class: type[Response] = Response + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface: SessionInterface = SecureCookieSessionInterface() + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_url_path=static_url_path, + static_folder=static_folder, + static_host=static_host, + host_matching=host_matching, + subdomain_matching=subdomain_matching, + template_folder=template_folder, + instance_path=instance_path, + instance_relative_config=instance_relative_config, + root_path=root_path, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = cli.AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + # Add a static route using the provided static_url_path, static_host, + # and static_folder if there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere + if self.has_static_folder: + assert ( + bool(static_host) == host_matching + ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + host=static_host, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ) + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for + reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to + :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is + supported, valid values are "r" (or "rt") and "rb". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) + + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. + """ + return open(os.path.join(self.instance_path, resource), mode) + + def create_jinja_environment(self) -> Environment: + """Create the Jinja environment based on :attr:`jinja_options` + and the various Jinja-related methods of the app. Changing + :attr:`jinja_options` after this will have no effect. Also adds + Flask-related globals and filters to the environment. + + .. versionchanged:: 0.11 + ``Environment.auto_reload`` set in accordance with + ``TEMPLATES_AUTO_RELOAD`` configuration option. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + + if "autoescape" not in options: + options["autoescape"] = self.select_jinja_autoescape + + if "auto_reload" not in options: + auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] + + if auto_reload is None: + auto_reload = self.debug + + options["auto_reload"] = auto_reload + + rv = self.jinja_environment(self, **options) + rv.globals.update( + url_for=self.url_for, + get_flashed_messages=get_flashed_messages, + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g, + ) + rv.policies["json.dumps_function"] = self.json.dumps + return rv + + def create_url_adapter(self, request: Request | None) -> MapAdapter | None: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. + + .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + URL adapter is created for the application context. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + """ + if request is not None: + # If subdomain matching is disabled (the default), use the + # default subdomain in all cases. This should be the default + # in Werkzeug but it currently does not have that feature. + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + + return self.url_map.bind_to_environ( + request.environ, + server_name=self.config["SERVER_NAME"], + subdomain=subdomain, + ) + # We need at the very least the server name to be set for this + # to work. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def raise_routing_exception(self, request: Request) -> t.NoReturn: + """Intercept routing exceptions and possibly do something else. + + In debug mode, intercept a routing redirect and replace it with + an error if the body will be discarded. + + With modern Werkzeug this shouldn't occur, since it now uses a + 308 status which tells the browser to resend the method and + body. + + .. versionchanged:: 2.1 + Don't intercept 307 and 308 redirects. + + :meta private: + :internal: + """ + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.routing_exception.code in {307, 308} + or request.method in {"GET", "HEAD", "OPTIONS"} + ): + raise request.routing_exception # type: ignore[misc] + + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def update_template_context(self, context: dict[str, t.Any]) -> None: + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overridden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + names: t.Iterable[str | None] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. + orig_ctx = context.copy() + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(self.ensure_sync(func)()) + + context.update(orig_ctx) + + def make_shell_context(self) -> dict[str, t.Any]: + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 0.11 + """ + rv = {"app": self, "g": g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + + def run( + self, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the :command:`flask` command line script's ``run`` support. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to ``True`` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable + if present. + :param port: the port of the webserver. Defaults to ``5000`` or the + port defined in the ``SERVER_NAME`` config variable if present. + :param debug: if given, enable or disable debug mode. See + :attr:`debug`. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param options: the options to be forwarded to the underlying Werkzeug + server. See :func:`werkzeug.serving.run_simple` for more + information. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment + variables from :file:`.env` and :file:`.flaskenv` files. + + The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Ignore this call so that it doesn't start another server if + # the 'flask run' command is used. + if os.environ.get("FLASK_RUN_FROM_CLI") == "true": + if not is_running_from_reloader(): + click.secho( + " * Ignoring a call to 'app.run()' that would block" + " the current 'flask' CLI command.\n" + " Only call 'app.run()' in an 'if __name__ ==" + ' "__main__"\' guard.', + fg="red", + ) + + return + + if get_load_dotenv(load_dotenv): + cli.load_dotenv() + + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: + self.debug = get_debug_flag() + + # debug passed to method overrides all other sources + if debug is not None: + self.debug = bool(debug) + + server_name = self.config.get("SERVER_NAME") + sn_host = sn_port = None + + if server_name: + sn_host, _, sn_port = server_name.partition(":") + + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 + + options.setdefault("use_reloader", self.debug) + options.setdefault("use_debugger", self.debug) + options.setdefault("threaded", True) + + cli.show_server_banner(self.debug, self.name) + + from werkzeug.serving import run_simple + + try: + run_simple(t.cast(str, host), port, self, **options) + finally: + # reset the first request information if the development server + # reset normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: + """Creates a test client for this application. For information + about unit testing head over to :doc:`/testing`. + + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + + The test client can be used in a ``with`` block to defer the closing down + of the context until the end of the ``with`` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + Additionally, you may pass optional keyword arguments that will then + be passed to the application's :attr:`test_client_class` constructor. + For example:: + + from flask.testing import FlaskClient + + class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) + + app.test_client_class = CustomClient + client = app.test_client(authentication='Basic ....') + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for ``with`` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + + .. versionchanged:: 0.11 + Added `**kwargs` to support passing additional keyword arguments to + the constructor of :attr:`test_client_class`. + """ + cls = self.test_client_class + if cls is None: + from .testing import FlaskClient as cls + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) + + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: + """Create a CLI runner for testing CLI commands. + See :ref:`testing-cli`. + + Returns an instance of :attr:`test_cli_runner_class`, by default + :class:`~flask.testing.FlaskCliRunner`. The Flask app object is + passed as the first argument. + + .. versionadded:: 1.0 + """ + cls = self.test_cli_runner_class + + if cls is None: + from .testing import FlaskCliRunner as cls + + return cls(self, **kwargs) # type: ignore + + def handle_http_exception( + self, e: HTTPException + ) -> HTTPException | ft.ResponseReturnValue: + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionchanged:: 1.0.3 + ``RoutingException``, used internally for actions such as + slash redirects during routing, is not passed to error + handlers. + + .. versionchanged:: 1.0 + Exceptions are looked up by code *and* by MRO, so + ``HTTPException`` subclasses can be handled with a catch-all + handler for the base ``HTTPException``. + + .. versionadded:: 0.3 + """ + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e + + # RoutingExceptions are used internally to trigger routing + # actions, such as slash redirects raising RequestRedirect. They + # are not raised or handled in user code. + if isinstance(e, RoutingException): + return e + + handler = self._find_error_handler(e, request.blueprints) + if handler is None: + return e + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_user_exception( + self, e: Exception + ) -> HTTPException | ft.ResponseReturnValue: + """This method is called whenever an exception occurs that + should be handled. A special case is :class:`~werkzeug + .exceptions.HTTPException` which is forwarded to the + :meth:`handle_http_exception` method. This function will either + return a response value or reraise the exception with the same + traceback. + + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the + bad key in debug mode rather than a generic bad request + message. + + .. versionadded:: 0.7 + """ + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True + + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + handler = self._find_error_handler(e, request.blueprints) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_exception(self, e: Exception) -> Response: + """Handle an exception that did not have an error handler + associated with it, or that was raised from an error handler. + This always causes a 500 ``InternalServerError``. + + Always sends the :data:`got_request_exception` signal. + + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug + mode, the error will be re-raised so that the debugger can + display it. Otherwise, the original exception is logged, and + an :exc:`~werkzeug.exceptions.InternalServerError` is returned. + + If an error handler is registered for ``InternalServerError`` or + ``500``, it will be used. For consistency, the handler will + always receive the ``InternalServerError``. The original + unhandled exception is available as ``e.original_exception``. + + .. versionchanged:: 1.1.0 + Always passes the ``InternalServerError`` instance to the + handler, setting ``original_exception`` to the unhandled + error. + + .. versionchanged:: 1.1.0 + ``after_request`` functions and other finalization is done + even for the default 500 response when there is no handler. + + .. versionadded:: 0.3 + """ + exc_info = sys.exc_info() + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) + propagate = self.config["PROPAGATE_EXCEPTIONS"] + + if propagate is None: + propagate = self.testing or self.debug + + if propagate: + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise + + raise e + + self.log_exception(exc_info) + server_error: InternalServerError | ft.ResponseReturnValue + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error, request.blueprints) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), + ) -> None: + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error( + f"Exception on {request.path} [{request.method}]", exc_info=exc_info + ) + + def dispatch_request(self) -> ft.ResponseReturnValue: + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = request_ctx.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule: Rule = req.url_rule # type: ignore[assignment] + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if ( + getattr(rule, "provide_automatic_options", False) + and req.method == "OPTIONS" + ): + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + + def full_dispatch_request(self) -> Response: + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self._got_first_request = True + + try: + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + rv: ft.ResponseReturnValue | HTTPException, + from_error_handler: bool = False, + ) -> Response: + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ + response = self.make_response(rv) + try: + response = self.process_response(response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) + except Exception: + if not from_error_handler: + raise + self.logger.exception( + "Request finalizing failed with an error while handling an error" + ) + return response + + def make_default_options_response(self) -> Response: + """This method is called to create the default ``OPTIONS`` response. + This can be changed through subclassing to change the default + behavior of ``OPTIONS`` responses. + + .. versionadded:: 0.7 + """ + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] + rv = self.response_class() + rv.allow.update(methods) + return rv + + def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + return asgiref_async_to_sync(func) + + def url_for( + self, + /, + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, + ) -> str: + """Generate a URL to the given endpoint with the given values. + + This is called by :func:`flask.url_for`, and can be called + directly as well. + + An *endpoint* is the name of a URL rule, usually added with + :meth:`@app.route() `, and usually the same name as the + view function. A route defined in a :class:`~flask.Blueprint` + will prepend the blueprint's name separated by a ``.`` to the + endpoint. + + In some cases, such as email messages, you want URLs to include + the scheme and domain, like ``https://example.com/hello``. When + not in an active request, URLs will be external by default, but + this requires setting :data:`SERVER_NAME` so Flask knows what + domain to use. :data:`APPLICATION_ROOT` and + :data:`PREFERRED_URL_SCHEME` should also be configured as + needed. This config is only used when not in an active request. + + Functions can be decorated with :meth:`url_defaults` to modify + keyword arguments before the URL is built. + + If building fails for some reason, such as an unknown endpoint + or incorrect values, the app's :meth:`handle_url_build_error` + method is called. If that returns a string, that is returned, + otherwise a :exc:`~werkzeug.routing.BuildError` is raised. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it + is external. + :param _external: If given, prefer the URL to be internal + (False) or require it to be external (True). External URLs + include the scheme and domain. When not in an active + request, URLs are external by default. + :param values: Values to use for the variable parts of the URL + rule. Unknown keys are appended as query string arguments, + like ``?a=b&c=d``. + + .. versionadded:: 2.2 + Moved from ``flask.url_for``, which calls this method. + """ + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint + + # If the endpoint starts with "." and the request matches a + # blueprint, the endpoint is relative to the blueprint. + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + # When in a request, generate a URL without scheme and + # domain by default, unless a scheme is given. + if _external is None: + _external = _scheme is not None + else: + app_ctx = _cv_app.get(None) + + # If called by helpers.url_for, an app context is active, + # use its url_adapter. Otherwise, app.url_for was called + # directly, build an adapter. + if app_ctx is not None: + url_adapter = app_ctx.url_adapter + else: + url_adapter = self.create_url_adapter(None) + + if url_adapter is None: + raise RuntimeError( + "Unable to build URLs outside an active request" + " without 'SERVER_NAME' configured. Also configure" + " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" + " needed." + ) + + # When outside a request, generate a URL with scheme and + # domain by default. + if _external is None: + _external = True + + # It is an error to set _scheme when _external=False, in order + # to avoid accidental insecure URLs. + if _scheme is not None and not _external: + raise ValueError("When specifying '_scheme', '_external' must be True.") + + self.inject_url_defaults(endpoint, values) + + try: + rv = url_adapter.build( # type: ignore[union-attr] + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) + except BuildError as error: + values.update( + _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external + ) + return self.handle_url_build_error(error, endpoint, values) + + if _anchor is not None: + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" + + return rv + + def make_response(self, rv: ft.ResponseReturnValue) -> Response: + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` + A response object is created with the bytes as the body. + + ``dict`` + A dictionary that will be jsonify'd before being returned. + + ``list`` + A list that will be jsonify'd before being returned. + + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. + + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + A list will be converted to a JSON response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. + """ + + status = headers = None + + # unpack tuple returns + if isinstance(rv, tuple): + len_rv = len(rv) + + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv # type: ignore[misc] + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv + else: + rv, status = rv # type: ignore[assignment,misc] + # other sized tuples are not allowed + else: + raise TypeError( + "The view function did not return a valid response tuple." + " The tuple must have the form (body, status, headers)," + " (body, status), or (body, headers)." + ) + + # the body must not be None + if rv is None: + raise TypeError( + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." + ) + + # make sure the body is an instance of the response class + if not isinstance(rv, self.response_class): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class( + rv, + status=status, + headers=headers, # type: ignore[arg-type] + ) + status = headers = None + elif isinstance(rv, (dict, list)): + rv = self.json.response(rv) + elif isinstance(rv, BaseResponse) or callable(rv): + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type( + rv, # type: ignore[arg-type] + request.environ, + ) + except TypeError as e: + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None + else: + raise TypeError( + "The view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." + ) + + rv = t.cast(Response, rv) + # prefer the status if it was provided + if status is not None: + if isinstance(status, (str, bytes, bytearray)): + rv.status = status + else: + rv.status_code = status + + # extend existing headers with provided headers + if headers: + rv.headers.update(headers) # type: ignore[arg-type] + + return rv + + def preprocess_request(self) -> ft.ResponseReturnValue | None: + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) + + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv # type: ignore[no-any-return] + + return None + + def process_response(self, response: Response) -> Response: + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def do_teardown_appcontext( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: WSGIEnvironment) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with app.test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + Takes the same arguments as Werkzeug's + :class:`~werkzeug.test.EnvironBuilder`, with some defaults from + the application. See the linked Werkzeug docs for most of the + available arguments. Flask-specific behavior is listed here. + + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + from .testing import EnvironBuilder + + builder = EnvironBuilder(self, *args, **kwargs) + + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The actual WSGI application. This is not implemented in + :meth:`__call__` so that middlewares can be applied without + losing a reference to the app object. Instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + Teardown events for the request and app contexts are called + even if an unhandled error occurs. Other events may not be + called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. + + :param environ: A WSGI environment. + :param start_response: A callable accepting a status code, + a list of headers, and an optional exception context to + start the response. + """ + ctx = self.request_context(environ) + error: BaseException | None = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if error is not None and self.should_ignore_error(error): + error = None + + ctx.pop(error) + + def __call__( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The WSGI server calls the Flask application object as the + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ + return self.wsgi_app(environ, start_response) diff --git a/test/fixtures/whole_applications/flask/src/flask/blueprints.py b/test/fixtures/whole_applications/flask/src/flask/blueprints.py new file mode 100644 index 0000000..aa9eacf --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/blueprints.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import os +import typing as t +from datetime import timedelta + +from .cli import AppGroup +from .globals import current_app +from .helpers import send_from_directory +from .sansio.blueprints import Blueprint as SansioBlueprint +from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa +from .sansio.scaffold import _sentinel + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +class Blueprint(SansioBlueprint): + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore + ) -> None: + super().__init__( + name, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_group, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for + reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to + :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is + supported, valid values are "r" (or "rt") and "rb". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) diff --git a/test/fixtures/whole_applications/flask/src/flask/cli.py b/test/fixtures/whole_applications/flask/src/flask/cli.py new file mode 100644 index 0000000..ecb292a --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/cli.py @@ -0,0 +1,1109 @@ +from __future__ import annotations + +import ast +import collections.abc as cabc +import importlib.metadata +import inspect +import os +import platform +import re +import sys +import traceback +import typing as t +from functools import update_wrapper +from operator import itemgetter +from types import ModuleType + +import click +from click.core import ParameterSource +from werkzeug import run_simple +from werkzeug.serving import is_running_from_reloader +from werkzeug.utils import import_string + +from .globals import current_app +from .helpers import get_debug_flag +from .helpers import get_load_dotenv + +if t.TYPE_CHECKING: + import ssl + + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(module: ModuleType) -> Flask: + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in ("app", "application"): + app = getattr(module, attr_name, None) + + if isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise NoAppException( + "Detected multiple Flask applications in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify the correct one." + ) + + # Search for app factory functions. + for attr_name in ("create_app", "make_app"): + app_factory = getattr(module, attr_name, None) + + if inspect.isfunction(app_factory): + try: + app = app_factory() + + if isinstance(app, Flask): + return app + except TypeError as e: + if not _called_with_wrong_args(app_factory): + raise + + raise NoAppException( + f"Detected factory '{attr_name}' in module '{module.__name__}'," + " but could not call it without arguments. Use" + f" '{module.__name__}:{attr_name}(args)'" + " to specify arguments." + ) from e + + raise NoAppException( + "Failed to find Flask application or factory in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify one." + ) + + +def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def find_app_by_string(module: ModuleType, app_name: str) -> Flask: + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. + """ + from . import Flask + + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None + + if isinstance(expr, ast.Name): + name = expr.id + args = [] + kwargs = {} + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = { + kw.arg: ast.literal_eval(kw.value) + for kw in expr.keywords + if kw.arg is not None + } + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + + # If the attribute is a function, call it with any args and kwargs + # to get the real application. + if inspect.isfunction(attr): + try: + app = attr(*args, **kwargs) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." + ) + + +def prepare_import(path: str) -> str: + """Given a filename this will try to calculate the python path, add it + to the search path and return the actual module name that is expected. + """ + path = os.path.realpath(path) + + fname, ext = os.path.splitext(path) + if ext == ".py": + path = fname + + if os.path.basename(path) == "__init__": + path = os.path.dirname(path) + + module_name = [] + + # move up until outside package structure (no __init__.py) + while True: + path, name = os.path.split(path) + module_name.append(name) + + if not os.path.exists(os.path.join(path, "__init__.py")): + break + + if sys.path[0] != path: + sys.path.insert(0, path) + + return ".".join(module_name[::-1]) + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True +) -> Flask: ... + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... +) -> Flask | None: ... + + +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: bool = True +) -> Flask | None: + try: + __import__(module_name) + except ImportError: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[2].tb_next: # type: ignore[union-attr] + raise NoAppException( + f"While importing {module_name!r}, an ImportError was" + f" raised:\n\n{traceback.format_exc()}" + ) from None + elif raise_if_not_found: + raise NoAppException(f"Could not import {module_name!r}.") from None + else: + return None + + module = sys.modules[module_name] + + if app_name is None: + return find_best_app(module) + else: + return find_app_by_string(module, app_name) + + +def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: + if not value or ctx.resilient_parsing: + return + + flask_version = importlib.metadata.version("flask") + werkzeug_version = importlib.metadata.version("werkzeug") + + click.echo( + f"Python {platform.python_version()}\n" + f"Flask {flask_version}\n" + f"Werkzeug {werkzeug_version}", + color=ctx.color, + ) + ctx.exit() + + +version_option = click.Option( + ["--version"], + help="Show the Flask version.", + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) + + +class ScriptInfo: + """Helper object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + ) -> None: + #: Optionally the import path for the Flask application. + self.app_import_path = app_import_path + #: Optionally a function that is passed the script info to create + #: the instance of the application. + self.create_app = create_app + #: A dictionary with arbitrary data that can be associated with + #: this script info. + self.data: dict[t.Any, t.Any] = {} + self.set_debug_flag = set_debug_flag + self._loaded_app: Flask | None = None + + def load_app(self) -> Flask: + """Loads the Flask app (if not yet loaded) and returns it. Calling + this multiple times will just result in the already loaded app to + be returned. + """ + if self._loaded_app is not None: + return self._loaded_app + + if self.create_app is not None: + app: Flask | None = self.create_app() + else: + if self.app_import_path: + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] + )[:2] + import_name = prepare_import(path) + app = locate_app(import_name, name) + else: + for path in ("wsgi.py", "app.py"): + import_name = prepare_import(path) + app = locate_app(import_name, None, raise_if_not_found=False) + + if app is not None: + break + + if app is None: + raise NoAppException( + "Could not locate a Flask application. Use the" + " 'flask --app' option, 'FLASK_APP' environment" + " variable, or a 'wsgi.py' or 'app.py' file in the" + " current directory." + ) + + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() + + self._loaded_app = app + return app + + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def with_appcontext(f: F) -> F: + """Wraps a callback so that it's guaranteed to be executed with the + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. + """ + + @click.pass_context + def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + if not current_app: + app = ctx.ensure_object(ScriptInfo).load_app() + ctx.with_resource(app.app_context()) + + return ctx.invoke(f, *args, **kwargs) + + return update_wrapper(decorator, f) # type: ignore[return-value] + + +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def command( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop("with_appcontext", True) + + def decorator(f: t.Callable[..., t.Any]) -> click.Command: + if wrap_for_ctx: + f = with_appcontext(f) + return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] + + return decorator + + def group( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it defaults the group class to + :class:`AppGroup`. + """ + kwargs.setdefault("cls", AppGroup) + return super().group(*args, **kwargs) # type: ignore[no-any-return] + + +def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + info = ctx.ensure_object(ScriptInfo) + info.app_import_path = value + return value + + +# This option is eager so the app will be available if --help is given. +# --help is also eager, so --app must be before it in the param list. +# no_args_is_help bypasses eager processing, so this option must be +# processed manually in that case to ensure FLASK_APP gets picked up. +_app_option = click.Option( + ["-A", "--app"], + metavar="IMPORT", + help=( + "The Flask application or factory function to load, in the form 'module:name'." + " Module can be a dotted import or file path. Name is not required if it is" + " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" + " pass arguments." + ), + is_eager=True, + expose_value=False, + callback=_set_app, +) + + +def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: + # If the flag isn't provided, it will default to False. Don't use + # that, let debug be set by env in that case. + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] + + if source is not None and source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_DEBUG"] = "1" if value else "0" + return value + + +_debug_option = click.Option( + ["--debug/--no-debug"], + help="Set debug mode.", + expose_value=False, + callback=_set_debug, +) + + +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: + if value is None: + return None + + import importlib + + try: + importlib.import_module("dotenv") + except ImportError: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) + return value + + +# This option is eager so env vars are loaded as early as possible to be +# used by other options. +_env_file_option = click.Option( + ["-e", "--env-file"], + type=click.Path(exists=True, dir_okay=False), + help="Load environment variables from this file. python-dotenv must be installed.", + is_eager=True, + expose_value=False, + callback=_env_file_callback, +) + + +class FlaskGroup(AppGroup): + """Special subclass of the :class:`AppGroup` group that supports + loading more commands from the configured Flask app. Normally a + developer does not have to interface with this class but there are + some very advanced use cases for which it makes sense to create an + instance of this. see :ref:`custom-scripts`. + + :param add_default_commands: if this is True then the default run and + shell commands will be added. + :param add_version_option: adds the ``--version`` option. + :param create_app: an optional callback that is passed the script info and + returns the loaded app. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag. + + .. versionchanged:: 2.2 + Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. + + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment variables + from :file:`.env` and :file:`.flaskenv` files. + """ + + def __init__( + self, + add_default_commands: bool = True, + create_app: t.Callable[..., Flask] | None = None, + add_version_option: bool = True, + load_dotenv: bool = True, + set_debug_flag: bool = True, + **extra: t.Any, + ) -> None: + params = list(extra.pop("params", None) or ()) + # Processing is done with option callbacks instead of a group + # callback. This allows users to make a custom group callback + # without losing the behavior. --env-file must come first so + # that it is eagerly evaluated before --app. + params.extend((_env_file_option, _app_option, _debug_option)) + + if add_version_option: + params.append(version_option) + + if "context_settings" not in extra: + extra["context_settings"] = {} + + extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") + + super().__init__(params=params, **extra) + + self.create_app = create_app + self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag + + if add_default_commands: + self.add_command(run_command) + self.add_command(shell_command) + self.add_command(routes_command) + + self._loaded_plugin_commands = False + + def _load_plugin_commands(self) -> None: + if self._loaded_plugin_commands: + return + + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + + for ep in metadata.entry_points(group="flask.commands"): + self.add_command(ep.load(), ep.name) + + self._loaded_plugin_commands = True + + def get_command(self, ctx: click.Context, name: str) -> click.Command | None: + self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) + + if rv is not None: + return rv + + info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + app = info.load_app() + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) + + def list_commands(self, ctx: click.Context) -> list[str]: + self._load_plugin_commands() + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) + info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + except Exception: + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + + return sorted(rv) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: click.Context | None = None, + **extra: t.Any, + ) -> click.Context: + # Set a flag to tell app.run to become a no-op. If app.run was + # not in a __name__ == __main__ guard, it would start the server + # when importing, blocking whatever command is being called. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + + # Attempt to load .env and .flask env files. The --env-file + # option can cause another file to be loaded. + if get_load_dotenv(self.load_dotenv): + load_dotenv() + + if "obj" not in extra and "obj" not in self.context_settings: + extra["obj"] = ScriptInfo( + create_app=self.create_app, set_debug_flag=self.set_debug_flag + ) + + return super().make_context(info_name, args, parent=parent, **extra) + + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help: + # Attempt to load --env-file and --app early in case they + # were given as env vars. Otherwise no_args_is_help will not + # see commands from app.cli. + _env_file_option.handle_parse_result(ctx, {}, []) + _app_option.handle_parse_result(ctx, {}, []) + + return super().parse_args(ctx, args) + + +def _path_is_ancestor(path: str, other: str) -> bool: + """Take ``other`` and remove the length of ``path`` from it. Then join it + to ``path``. If it is the original value, ``path`` is an ancestor of + ``other``.""" + return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other + + +def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool: + """Load "dotenv" files in order of precedence to set environment variables. + + If an env var is already set it is not overwritten, so earlier files in the + list are preferred over later files. + + This is a no-op if `python-dotenv`_ is not installed. + + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + :param path: Load the file at this location instead of searching. + :return: ``True`` if a file was loaded. + + .. versionchanged:: 2.0 + The current directory is not changed to the location of the + loaded file. + + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + + .. versionadded:: 1.0 + """ + try: + import dotenv + except ImportError: + if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): + click.secho( + " * Tip: There are .env or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', + fg="yellow", + err=True, + ) + + return False + + # Always return after attempting to load a given path, don't load + # the default files. + if path is not None: + if os.path.isfile(path): + return dotenv.load_dotenv(path, encoding="utf-8") + + return False + + loaded = False + + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + dotenv.load_dotenv(path, encoding="utf-8") + loaded = True + + return loaded # True if at least one file was located and loaded. + + +def show_server_banner(debug: bool, app_import_path: str | None) -> None: + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if is_running_from_reloader(): + return + + if app_import_path is not None: + click.echo(f" * Serving Flask app '{app_import_path}'") + + if debug is not None: + click.echo(f" * Debug mode: {'on' if debug else 'off'}") + + +class CertParamType(click.ParamType): + """Click option type for the ``--cert`` option. Allows either an + existing file, the string ``'adhoc'``, or an import for a + :class:`~ssl.SSLContext` object. + """ + + name = "path" + + def __init__(self) -> None: + self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + try: + import ssl + except ImportError: + raise click.BadParameter( + 'Using "--cert" requires Python to be compiled with SSL support.', + ctx, + param, + ) from None + + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == "adhoc": + try: + import cryptography # noqa: F401 + except ImportError: + raise click.BadParameter( + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None + + return value + + obj = import_string(value, silent=True) + + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: + """The ``--key`` option must be specified when ``--cert`` is a file. + Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. + """ + cert = ctx.params.get("cert") + is_adhoc = cert == "adhoc" + + try: + import ssl + except ImportError: + is_context = False + else: + is_context = isinstance(cert, ssl.SSLContext) + + if value is not None: + if is_adhoc: + raise click.BadParameter( + 'When "--cert" is "adhoc", "--key" is not used.', ctx, param + ) + + if is_context: + raise click.BadParameter( + 'When "--cert" is an SSLContext object, "--key" is not used.', + ctx, + param, + ) + + if not cert: + raise click.BadParameter('"--cert" must also be specified.', ctx, param) + + ctx.params["cert"] = cert, value + + else: + if cert and not (is_adhoc or is_context): + raise click.BadParameter('Required when using "--cert".', ctx, param) + + return value + + +class SeparatedPathType(click.Path): + """Click option type that accepts a list of values separated by the + OS's path separator (``:``, ``;`` on Windows). Each value is + validated as a :class:`click.Path` type. + """ + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + items = self.split_envvar_value(value) + # can't call no-arg super() inside list comprehension until Python 3.12 + super_convert = super().convert + return [super_convert(item, param, ctx) for item in items] + + +@click.command("run", short_help="Run a development server.") +@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") +@click.option("--port", "-p", default=5000, help="The port to bind to.") +@click.option( + "--cert", + type=CertParamType(), + help="Specify a certificate file to use HTTPS.", + is_eager=True, +) +@click.option( + "--key", + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + callback=_validate_key, + expose_value=False, + help="The key file to use when specifying a certificate.", +) +@click.option( + "--reload/--no-reload", + default=None, + help="Enable or disable the reloader. By default the reloader " + "is active if debug is enabled.", +) +@click.option( + "--debugger/--no-debugger", + default=None, + help="Enable or disable the debugger. By default the debugger " + "is active if debug is enabled.", +) +@click.option( + "--with-threads/--without-threads", + default=True, + help="Enable or disable multithreading.", +) +@click.option( + "--extra-files", + default=None, + type=SeparatedPathType(), + help=( + "Extra files that trigger a reload on change. Multiple paths" + f" are separated by {os.path.pathsep!r}." + ), +) +@click.option( + "--exclude-patterns", + default=None, + type=SeparatedPathType(), + help=( + "Files matching these fnmatch patterns will not trigger a reload" + " on change. Multiple patterns are separated by" + f" {os.path.pathsep!r}." + ), +) +@pass_script_info +def run_command( + info: ScriptInfo, + host: str, + port: int, + reload: bool, + debugger: bool, + with_threads: bool, + cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, + extra_files: list[str] | None, + exclude_patterns: list[str] | None, +) -> None: + """Run a local development server. + + This server is for development purposes only. It does not provide + the stability, security, or performance of production WSGI servers. + + The reloader and debugger are enabled by default with the '--debug' + option. + """ + try: + app: WSGIApplication = info.load_app() + except Exception as e: + if is_running_from_reloader(): + # When reloading, print out the error immediately, but raise + # it later so the debugger or server can handle it. + traceback.print_exc() + err = e + + def app( + environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + raise err from None + + else: + # When not reloading, raise the error immediately so the + # command fails. + raise e from None + + debug = get_debug_flag() + + if reload is None: + reload = debug + + if debugger is None: + debugger = debug + + show_server_banner(debug, info.app_import_path) + + run_simple( + host, + port, + app, + use_reloader=reload, + use_debugger=debugger, + threaded=with_threads, + ssl_context=cert, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + ) + + +run_command.params.insert(0, _debug_option) + + +@click.command("shell", short_help="Run a shell in the app context.") +@with_appcontext +def shell_command() -> None: + """Run an interactive Python shell in the context of a given + Flask application. The application will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of management code + without having to manually configure the application. + """ + import code + + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {current_app.import_name}\n" + f"Instance: {current_app.instance_path}" + ) + ctx: dict[str, t.Any] = {} + + # Support the regular Python interpreter startup script if someone + # is using it. + startup = os.environ.get("PYTHONSTARTUP") + if startup and os.path.isfile(startup): + with open(startup) as f: + eval(compile(f.read(), startup, "exec"), ctx) + + ctx.update(current_app.make_shell_context()) + + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + + code.interact(banner=banner, local=ctx) + + +@click.command("routes", short_help="Show the routes for the app.") +@click.option( + "--sort", + "-s", + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), + default="endpoint", + help=( + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." + ), +) +@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") +@with_appcontext +def routes_command(sort: str, all_methods: bool) -> None: + """Show all registered routes with endpoints and methods.""" + rules = list(current_app.url_map.iter_rules()) + + if not rules: + click.echo("No routes were registered.") + return + + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] + + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] + + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") + + row.append(rule.rule) + rows.append(row) + + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] + + if has_domain: + headers.append("Host" if host_matching else "Subdomain") + sorts.append("domain") + + headers.append("Rule") + sorts.append("rule") + + try: + rows.sort(key=itemgetter(sorts.index(sort))) + except ValueError: + pass + + rows.insert(0, headers) + widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] + rows.insert(1, ["-" * w for w in widths]) + template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) + + for row in rows: + click.echo(template.format(*row)) + + +cli = FlaskGroup( + name="flask", + help="""\ +A general utility script for Flask applications. + +An application to load must be given with the '--app' option, +'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file +in the current directory. +""", +) + + +def main() -> None: + cli.main() + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/whole_applications/flask/src/flask/config.py b/test/fixtures/whole_applications/flask/src/flask/config.py new file mode 100644 index 0000000..7e3ba17 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/config.py @@ -0,0 +1,370 @@ +from __future__ import annotations + +import errno +import json +import os +import types +import typing as t + +from werkzeug.utils import import_string + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .sansio.app import App + + +T = t.TypeVar("T") + + +class ConfigAttribute(t.Generic[T]): + """Makes an attribute forward to the config""" + + def __init__( + self, name: str, get_converter: t.Callable[[t.Any], T] | None = None + ) -> None: + self.__name__ = name + self.get_converter = get_converter + + @t.overload + def __get__(self, obj: None, owner: None) -> te.Self: ... + + @t.overload + def __get__(self, obj: App, owner: type[App]) -> T: ... + + def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: + if obj is None: + return self + + rv = obj.config[self.__name__] + + if self.get_converter is not None: + rv = self.get_converter(rv) + + return rv # type: ignore[no-any-return] + + def __set__(self, obj: App, value: t.Any) -> None: + obj.config[self.__name__] = value + + +class Config(dict): # type: ignore[type-arg] + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__( + self, + root_path: str | os.PathLike[str], + defaults: dict[str, t.Any] | None = None, + ) -> None: + super().__init__(defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError( + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" + ) + return self.from_pyfile(rv, silent=silent) + + def from_prefixed_env( + self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads + ) -> bool: + """Load any environment variables that start with ``FLASK_``, + dropping the prefix from the env key for the config key. Values + are passed through a loading function to attempt to convert them + to more specific types than strings. + + Keys are loaded in :func:`sorted` order. + + The default loading function attempts to parse values as any + valid JSON type, including dicts and lists. + + Specific items in nested dicts can be set by separating the + keys with double underscores (``__``). If an intermediate key + doesn't exist, it will be initialized to an empty dict. + + :param prefix: Load env vars that start with this prefix, + separated with an underscore (``_``). + :param loads: Pass each string value to this function and use + the returned value as the config value. If any error is + raised it is ignored and the value remains a string. The + default is :func:`json.loads`. + + .. versionadded:: 2.1 + """ + prefix = f"{prefix}_" + len_prefix = len(prefix) + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + # Change to key.removeprefix(prefix) on Python >= 3.9. + key = key[len_prefix:] + + if "__" not in key: + # A non-nested key, set directly. + self[key] = value + continue + + # Traverse nested dictionaries with keys separated by "__". + current = self + *parts, tail = key.split("__") + + for part in parts: + # If an intermediate dict does not exist, create it. + if part not in current: + current[part] = {} + + current = current[part] + + current[tail] = value + + return True + + def from_pyfile( + self, filename: str | os.PathLike[str], silent: bool = False + ) -> bool: + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = types.ModuleType("config") + d.__file__ = filename + try: + with open(filename, mode="rb") as config_file: + exec(compile(config_file.read(), filename, "exec"), d.__dict__) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): + return False + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + self.from_object(d) + return True + + def from_object(self, obj: object | str) -> None: + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + Nothing is done to the object before loading. If the object is a + class and has ``@property`` attributes, it needs to be + instantiated before being passed to this method. + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_file( + self, + filename: str | os.PathLike[str], + load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], + silent: bool = False, + text: bool = True, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. + + .. code-block:: python + + import json + app.config.from_file("config.json", load=json.load) + + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. + :return: ``True`` if the file was loaded successfully. + + .. versionchanged:: 2.3 + The ``text`` parameter was added. + + .. versionadded:: 2.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename, "r" if text else "rb") as f: + obj = load(f) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + + return self.from_mapping(obj) + + def from_mapping( + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value + return True + + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> dict[str, t.Any]: + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace) :] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/test/fixtures/whole_applications/flask/src/flask/ctx.py b/test/fixtures/whole_applications/flask/src/flask/ctx.py new file mode 100644 index 0000000..9b164d3 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/ctx.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import contextvars +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from . import typing as ft +from .globals import _cv_app +from .globals import _cv_request +from .signals import appcontext_popped +from .signals import appcontext_pushed + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request + + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +class _AppCtxGlobals: + """A plain object. Used as a namespace for storing data during an + application context. + + Creating an app context automatically creates this object, which is + made available as the :data:`g` proxy. + + .. describe:: 'key' in g + + Check whether an attribute is present. + + .. versionadded:: 0.10 + + .. describe:: iter(g) + + Return an iterator over the attribute names. + + .. versionadded:: 0.10 + """ + + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Any | None = None) -> t.Any: + """Get an attribute by name, or a default value. Like + :meth:`dict.get`. + + :param name: Name of attribute to get. + :param default: Value to return if the attribute is not present. + + .. versionadded:: 0.10 + """ + return self.__dict__.get(name, default) + + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: + """Get and remove an attribute by name. Like :meth:`dict.pop`. + + :param name: Name of attribute to pop. + :param default: Value to return if the attribute is not present, + instead of raising a ``KeyError``. + + .. versionadded:: 0.11 + """ + if default is _sentinel: + return self.__dict__.pop(name) + else: + return self.__dict__.pop(name, default) + + def setdefault(self, name: str, default: t.Any = None) -> t.Any: + """Get the value of an attribute if it is present, otherwise + set and return a default value. Like :meth:`dict.setdefault`. + + :param name: Name of attribute to get. + :param default: Value to set and return if the attribute is not + present. + + .. versionadded:: 0.11 + """ + return self.__dict__.setdefault(name, default) + + def __contains__(self, item: str) -> bool: + return item in self.__dict__ + + def __iter__(self) -> t.Iterator[str]: + return iter(self.__dict__) + + def __repr__(self) -> str: + ctx = _cv_app.get(None) + if ctx is not None: + return f"" + return object.__repr__(self) + + +def after_this_request( + f: ft.AfterRequestCallable[t.Any], +) -> ft.AfterRequestCallable[t.Any]: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." + ) + + ctx._after_request_functions.append(f) + return f + + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def copy_current_request_context(f: F) -> F: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request or + # flask.session like you would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." + ) + + ctx = ctx.copy() + + def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: + with ctx: # type: ignore[union-attr] + return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] + + return update_wrapper(wrapper, f) # type: ignore[return-value] + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _cv_request.get(None) is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _cv_app.get(None) is not None + + +class AppContext: + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g: _AppCtxGlobals = app.app_ctx_globals_class() + self._cv_tokens: list[contextvars.Token[AppContext]] = [] + + def push(self) -> None: + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + if len(self._cv_tokens) == 1: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + def __enter__(self) -> AppContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. It will create the URL adapter and + request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the + request. When using the interactive debugger, the context will be + restored so ``request`` is still accessible. Similarly, the test + client can preserve the context after the request ends. However, + teardown functions may already have closed some resources such as + database connections. + """ + + def __init__( + self, + app: Flask, + environ: WSGIEnvironment, + request: Request | None = None, + session: SessionMixin | None = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] + + self._cv_tokens: list[ + tuple[contextvars.Token[RequestContext], AppContext | None] + ] = [] + + def copy(self) -> RequestContext: + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> RequestContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/test/fixtures/whole_applications/flask/src/flask/debughelpers.py b/test/fixtures/whole_applications/flask/src/flask/debughelpers.py new file mode 100644 index 0000000..2c8c4c4 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/debughelpers.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import typing as t + +from jinja2.loaders import BaseLoader +from werkzeug.routing import RequestRedirect + +from .blueprints import Blueprint +from .globals import request_ctx +from .sansio.app import App + +if t.TYPE_CHECKING: + from .sansio.scaffold import Scaffold + from .wrappers import Request + + +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request: Request, key: str) -> None: + form_matches = request.form.getlist(key) + buf = [ + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' + ] + if form_matches: + names = ", ".join(repr(x) for x in form_matches) + buf.append( + "\n\nThe browser instead transmitted some file names. " + f"This was submitted: {names}" + ) + self.msg = "".join(buf) + + def __str__(self) -> str: + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised in debug mode if a routing redirect + would cause the browser to drop the method or body. This happens + when method is not GET, HEAD or OPTIONS and the status code is not + 307 or 308. + """ + + def __init__(self, request: Request) -> None: + exc = request.routing_exception + assert isinstance(exc, RequestRedirect) + buf = [ + f"A request was sent to '{request.url}', but routing issued" + f" a redirect to the canonical URL '{exc.new_url}'." + ] + + if f"{request.base_url}/" == exc.new_url.partition("?")[0]: + buf.append( + " The URL was defined with a trailing slash. Flask" + " will redirect to the URL with a trailing slash if it" + " was accessed without one." + ) + + buf.append( + " Send requests to the canonical URL, or use 307 or 308 for" + " routing redirects. Otherwise, browsers will drop form" + " data.\n\n" + "This exception is only raised in debug mode." + ) + super().__init__("".join(buf)) + + +def attach_enctype_error_multidict(request: Request) -> None: + """Patch ``request.files.__getitem__`` to raise a descriptive error + about ``enctype=multipart/form-data``. + + :param request: The request to patch. + :meta private: + """ + oldcls = request.files.__class__ + + class newcls(oldcls): # type: ignore[valid-type, misc] + def __getitem__(self, key: str) -> t.Any: + try: + return super().__getitem__(key) + except KeyError as e: + if key not in request.form: + raise + + raise DebugFilesKeyError(request, key).with_traceback( + e.__traceback__ + ) from None + + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls + + +def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): + if key.startswith("_"): + continue + if isinstance(value, (tuple, list)): + if not all(isinstance(x, str) for x in value): + continue + yield f"{key}:" + for item in value: + yield f" - {item}" + continue + elif not isinstance(value, (str, int, float, bool)): + continue + yield f"{key}: {value!r}" + + +def explain_template_loading_attempts( + app: App, + template: str, + attempts: list[ + tuple[ + BaseLoader, + Scaffold, + tuple[str, str | None, t.Callable[[], bool] | None] | None, + ] + ], +) -> None: + """This should help developers understand what failed""" + info = [f"Locating template {template!r}:"] + total_found = 0 + blueprint = None + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint + + for idx, (loader, srcobj, triple) in enumerate(attempts): + if isinstance(srcobj, App): + src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: + src_info = repr(srcobj) + + info.append(f"{idx + 1:5}: trying loader of {src_info}") + + for line in _dump_loader_info(loader): + info.append(f" {line}") + + if triple is None: + detail = "no match" + else: + detail = f"found ({triple[1] or ''!r})" + total_found += 1 + info.append(f" -> {detail}") + + seems_fishy = False + if total_found == 0: + info.append("Error: the template could not be found.") + seems_fishy = True + elif total_found > 1: + info.append("Warning: multiple loaders returned a match for the template.") + seems_fishy = True + + if blueprint is not None and seems_fishy: + info.append( + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." + ) + info.append(" Maybe you did not place a template in the right folder?") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") + + app.logger.info("\n".join(info)) diff --git a/test/fixtures/whole_applications/flask/src/flask/globals.py b/test/fixtures/whole_applications/flask/src/flask/globals.py new file mode 100644 index 0000000..e2c410c --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/globals.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import typing as t +from contextvars import ContextVar + +from werkzeug.local import LocalProxy + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext + from .sessions import SessionMixin + from .wrappers import Request + + +_no_app_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ +""" +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg +) +current_app: Flask = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg +) +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg +) + +_no_req_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg +) +request: Request = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: SessionMixin = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg +) diff --git a/test/fixtures/whole_applications/flask/src/flask/helpers.py b/test/fixtures/whole_applications/flask/src/flask/helpers.py new file mode 100644 index 0000000..359a842 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/helpers.py @@ -0,0 +1,621 @@ +from __future__ import annotations + +import importlib.util +import os +import sys +import typing as t +from datetime import datetime +from functools import lru_cache +from functools import update_wrapper + +import werkzeug.utils +from werkzeug.exceptions import abort as _wz_abort +from werkzeug.utils import redirect as _wz_redirect +from werkzeug.wrappers import Response as BaseResponse + +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +from .globals import session +from .signals import message_flashed + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +def get_debug_flag() -> bool: + """Get whether debug mode should be enabled for the app, indicated by the + :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. + """ + val = os.environ.get("FLASK_DEBUG") + return bool(val and val.lower() not in {"0", "false", "no"}) + + +def get_load_dotenv(default: bool = True) -> bool: + """Get whether the user has disabled loading default dotenv files by + setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load + the files. + + :param default: What to return if the env var isn't set. + """ + val = os.environ.get("FLASK_SKIP_DOTENV") + + if not val: + return default + + return val.lower() in ("0", "false", "no") + + +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Iterator[t.AnyStr]: + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) # type: ignore[arg-type] + except TypeError: + + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore[operator] + return stream_with_context(gen) + + return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] + + def generator() -> t.Iterator[t.AnyStr | None]: + ctx = _cv_request.get(None) + if ctx is None: + raise RuntimeError( + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." + ) + with ctx: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + yield from gen + finally: + if hasattr(gen, "close"): + gen.close() + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g # type: ignore[return-value] + + +def make_response(*args: t.Any) -> Response: + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for( + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, +) -> str: + """Generate a URL to the given endpoint with the given values. + + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it is + external. + :param _external: If given, prefer the URL to be internal (False) or + require it to be external (True). External URLs include the + scheme and domain. When not in an active request, URLs are + external by default. + :param values: Values to use for the variable parts of the URL rule. + Unknown keys are appended as query string arguments, like + ``?a=b&c=d``. + + .. versionchanged:: 2.2 + Calls ``current_app.url_for``, allowing an app to override the + behavior. + + .. versionchanged:: 0.10 + The ``_scheme`` parameter was added. + + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. + + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. + """ + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) + + +def redirect( + location: str, code: int = 302, Response: type[BaseResponse] | None = None +) -> BaseResponse: + """Create a redirect response object. + + If :data:`~flask.current_app` is available, it will use its + :meth:`~flask.Flask.redirect` method, otherwise it will use + :func:`werkzeug.utils.redirect`. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + :param Response: The response class to use. Not used when + ``current_app`` is active, which uses ``app.response_class``. + + .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. + """ + if current_app: + return current_app.redirect(location, code=code) + + return _wz_redirect(location, code=code, Response=Response) + + +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + +def get_template_attribute(template_name: str, attribute: str) -> t.Any: + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named :file:`_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to access + """ + return getattr(current_app.jinja_env.get_template(template_name).module, attribute) + + +def flash(message: str, category: str = "message") -> None: + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged:: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # always in sync with the session object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get("_flashes", []) + flashes.append((category, message)) + session["_flashes"] = flashes + app = current_app._get_current_object() # type: ignore + message_flashed.send( + app, + _async_wrapper=app.ensure_sync, + message=message, + category=category, + ) + + +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> list[str] | list[tuple[str, str]]: + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to ``True``, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (``True`` gives a tuple, where ``False`` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + + See :doc:`/patterns/flashing` for examples. + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + .. versionchanged:: 0.9 + `category_filter` parameter added. + + :param with_categories: set to ``True`` to also receive categories. + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. + """ + flashes = request_ctx.flashes + if flashes is None: + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes + if category_filter: + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, + mimetype: str | None = None, + as_attachment: bool = False, + download_name: str | None = None, + conditional: bool = True, + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. + + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. + + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. + + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. + + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. + + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + removed because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 + """ + return werkzeug.utils.send_file( # type: ignore[return-value] + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + conditional=conditional, + etag=etag, + last_modified=last_modified, + max_age=max_age, + ) + ) + + +def send_from_directory( + directory: os.PathLike[str] | str, + path: os.PathLike[str] | str, + **kwargs: t.Any, +) -> Response: + """Send a file from within a directory using :func:`send_file`. + + .. code-block:: python + + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under, + relative to the current application's root path. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionadded:: 0.5 + """ + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) + + +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. + + Not to be confused with the value returned by :func:`find_package`. + + :meta private: + """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + + if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. + try: + spec = importlib.util.find_spec(import_name) + + if spec is None: + raise ValueError + except (ImportError, ValueError): + loader = None + else: + loader = spec.loader + + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. + if loader is None: + return os.getcwd() + + if hasattr(loader, "get_filename"): + filepath = loader.get_filename(import_name) + else: + # Fall back to imports. + __import__(import_name) + mod = sys.modules[import_name] + filepath = getattr(mod, "__file__", None) + + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. + if filepath is None: + raise RuntimeError( + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." + ) + + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/test/fixtures/whole_applications/flask/src/flask/json/__init__.py b/test/fixtures/whole_applications/flask/src/flask/json/__init__.py new file mode 100644 index 0000000..c0941d0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/json/__init__.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json as _json +import typing as t + +from ..globals import current_app +from .provider import _default + +if t.TYPE_CHECKING: # pragma: no cover + from ..wrappers import Response + + +def dumps(obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dumps() ` + method, otherwise it will use :func:`json.dumps`. + + :param obj: The data to serialize. + :param kwargs: Arguments passed to the ``dumps`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dumps``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.dumps(obj, **kwargs) + + kwargs.setdefault("default", _default) + return _json.dumps(obj, **kwargs) + + +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dump() ` + method, otherwise it will use :func:`json.dump`. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: Arguments passed to the ``dump`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dump``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, will be + removed in Flask 2.1. + """ + if current_app: + current_app.json.dump(obj, fp, **kwargs) + else: + kwargs.setdefault("default", _default) + _json.dump(obj, fp, **kwargs) + + +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.loads() ` + method, otherwise it will use :func:`json.loads`. + + :param s: Text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``loads`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.loads``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The data must be a + string or UTF-8 bytes. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.loads(s, **kwargs) + + return _json.loads(s, **kwargs) + + +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.load() ` + method, otherwise it will use :func:`json.load`. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``load`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.load``, allowing an app to override + the behavior. + + .. versionchanged:: 2.2 + The ``app`` parameter will be removed in Flask 2.3. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The file must be text + mode, or binary mode with UTF-8 bytes. + """ + if current_app: + return current_app.json.load(fp, **kwargs) + + return _json.load(fp, **kwargs) + + +def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. A dict or list returned from a view will be converted to a + JSON response automatically without needing to call this. + + This requires an active request or application context, and calls + :meth:`app.json.response() `. + + In debug mode, the output is formatted with indentation to make it + easier to read. This may also be controlled by the provider. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + + .. versionchanged:: 2.2 + Calls ``current_app.json.response``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 0.11 + Added support for serializing top-level arrays. This was a + security risk in ancient browsers. See :ref:`security-json`. + + .. versionadded:: 0.2 + """ + return current_app.json.response(*args, **kwargs) # type: ignore[return-value] diff --git a/test/fixtures/whole_applications/flask/src/flask/json/provider.py b/test/fixtures/whole_applications/flask/src/flask/json/provider.py new file mode 100644 index 0000000..f9b2e8f --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/json/provider.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import dataclasses +import decimal +import json +import typing as t +import uuid +import weakref +from datetime import date + +from werkzeug.http import http_date + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.sansio.response import Response + + from ..sansio.app import App + + +class JSONProvider: + """A standard set of JSON operations for an application. Subclasses + of this can be used to customize JSON behavior or use different + JSON libraries. + + To implement a provider for a specific library, subclass this base + class and implement at least :meth:`dumps` and :meth:`loads`. All + other methods have default implementations. + + To use a different provider, either subclass ``Flask`` and set + :attr:`~flask.Flask.json_provider_class` to a provider class, or set + :attr:`app.json ` to an instance of the class. + + :param app: An application instance. This will be stored as a + :class:`weakref.proxy` on the :attr:`_app` attribute. + + .. versionadded:: 2.2 + """ + + def __init__(self, app: App) -> None: + self._app: App = weakref.proxy(app) + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + :param obj: The data to serialize. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: May be passed to the underlying JSON library. + """ + fp.write(self.dumps(obj, **kwargs)) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + :param s: Text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + return self.loads(fp.read(), **kwargs) + + def _prepare_response_obj( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> t.Any: + if args and kwargs: + raise TypeError("app.json.response() takes either args or kwargs, not both") + + if not args and not kwargs: + return None + + if len(args) == 1: + return args[0] + + return args or kwargs + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. + + The :func:`~flask.json.jsonify` function calls this method for + the current application. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + return self._app.response_class(self.dumps(obj), mimetype="application/json") + + +def _default(o: t.Any) -> t.Any: + if isinstance(o, date): + return http_date(o) + + if isinstance(o, (decimal.Decimal, uuid.UUID)): + return str(o) + + if dataclasses and dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + + if hasattr(o, "__html__"): + return str(o.__html__()) + + raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") + + +class DefaultJSONProvider(JSONProvider): + """Provide JSON operations using Python's built-in :mod:`json` + library. Serializes the following additional data types: + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + """ + + default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] + """Apply this function to any object that :meth:`json.dumps` does + not know how to serialize. It should return a valid JSON type or + raise a ``TypeError``. + """ + + ensure_ascii = True + """Replace non-ASCII characters with escape sequences. This may be + more compatible with some clients, but can be disabled for better + performance and size. + """ + + sort_keys = True + """Sort the keys in any serialized dicts. This may be useful for + some caching situations, but can be disabled for better performance. + When enabled, keys must all be strings, they are not converted + before sorting. + """ + + compact: bool | None = None + """If ``True``, or ``None`` out of debug mode, the :meth:`response` + output will not add indentation, newlines, or spaces. If ``False``, + or ``None`` in debug mode, it will use a non-compact representation. + """ + + mimetype = "application/json" + """The mimetype set in :meth:`response`.""" + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON to a string. + + Keyword arguments are passed to :func:`json.dumps`. Sets some + parameter defaults from the :attr:`default`, + :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. + + :param obj: The data to serialize. + :param kwargs: Passed to :func:`json.dumps`. + """ + kwargs.setdefault("default", self.default) + kwargs.setdefault("ensure_ascii", self.ensure_ascii) + kwargs.setdefault("sort_keys", self.sort_keys) + return json.dumps(obj, **kwargs) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON from a string or bytes. + + :param s: Text or UTF-8 bytes. + :param kwargs: Passed to :func:`json.loads`. + """ + return json.loads(s, **kwargs) + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with it. The response mimetype + will be "application/json" and can be changed with + :attr:`mimetype`. + + If :attr:`compact` is ``False`` or debug mode is enabled, the + output will be formatted to be easier to read. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + dump_args: dict[str, t.Any] = {} + + if (self.compact is None and self._app.debug) or self.compact is False: + dump_args.setdefault("indent", 2) + else: + dump_args.setdefault("separators", (",", ":")) + + return self._app.response_class( + f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype + ) diff --git a/test/fixtures/whole_applications/flask/src/flask/json/tag.py b/test/fixtures/whole_applications/flask/src/flask/json/tag.py new file mode 100644 index 0000000..8dc3629 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/json/tag.py @@ -0,0 +1,327 @@ +""" +Tagged JSON +~~~~~~~~~~~ + +A compact representation for lossless serialization of non-standard JSON +types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this +to serialize the session data, but it may be useful in other places. It +can be extended to support other types. + +.. autoclass:: TaggedJSONSerializer + :members: + +.. autoclass:: JSONTag + :members: + +Let's see an example that adds support for +:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so +to handle this we will dump the items as a list of ``[key, value]`` +pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to +identify the type. The session serializer processes dicts first, so +insert the new tag at the front of the order since ``OrderedDict`` must +be processed before ``dict``. + +.. code-block:: python + + from flask.json.tag import JSONTag + + class TagOrderedDict(JSONTag): + __slots__ = ('serializer',) + key = ' od' + + def check(self, value): + return isinstance(value, OrderedDict) + + def to_json(self, value): + return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] + + def to_python(self, value): + return OrderedDict(value) + + app.session_interface.serializer.register(TagOrderedDict, index=0) +""" + +from __future__ import annotations + +import typing as t +from base64 import b64decode +from base64 import b64encode +from datetime import datetime +from uuid import UUID + +from markupsafe import Markup +from werkzeug.http import http_date +from werkzeug.http import parse_date + +from ..json import dumps +from ..json import loads + + +class JSONTag: + """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" + + __slots__ = ("serializer",) + + #: The tag to mark the serialized object with. If empty, this tag is + #: only used as an intermediate step during tagging. + key: str = "" + + def __init__(self, serializer: TaggedJSONSerializer) -> None: + """Create a tagger for the given serializer.""" + self.serializer = serializer + + def check(self, value: t.Any) -> bool: + """Check if the given value should be tagged by this tag.""" + raise NotImplementedError + + def to_json(self, value: t.Any) -> t.Any: + """Convert the Python object to an object that is a valid JSON type. + The tag will be added later.""" + raise NotImplementedError + + def to_python(self, value: t.Any) -> t.Any: + """Convert the JSON representation back to the correct type. The tag + will already be removed.""" + raise NotImplementedError + + def tag(self, value: t.Any) -> dict[str, t.Any]: + """Convert the value to a valid JSON type and add the tag structure + around it.""" + return {self.key: self.to_json(value)} + + +class TagDict(JSONTag): + """Tag for 1-item dicts whose only key matches a registered tag. + + Internally, the dict key is suffixed with `__`, and the suffix is removed + when deserializing. + """ + + __slots__ = () + key = " di" + + def check(self, value: t.Any) -> bool: + return ( + isinstance(value, dict) + and len(value) == 1 + and next(iter(value)) in self.serializer.tags + ) + + def to_json(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {f"{key}__": self.serializer.tag(value[key])} + + def to_python(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {key[:-2]: value[key]} + + +class PassDict(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, dict) + + def to_json(self, value: t.Any) -> t.Any: + # JSON objects may only have string keys, so don't bother tagging the + # key here. + return {k: self.serializer.tag(v) for k, v in value.items()} + + tag = to_json + + +class TagTuple(JSONTag): + __slots__ = () + key = " t" + + def check(self, value: t.Any) -> bool: + return isinstance(value, tuple) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + def to_python(self, value: t.Any) -> t.Any: + return tuple(value) + + +class PassList(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, list) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + tag = to_json + + +class TagBytes(JSONTag): + __slots__ = () + key = " b" + + def check(self, value: t.Any) -> bool: + return isinstance(value, bytes) + + def to_json(self, value: t.Any) -> t.Any: + return b64encode(value).decode("ascii") + + def to_python(self, value: t.Any) -> t.Any: + return b64decode(value) + + +class TagMarkup(JSONTag): + """Serialize anything matching the :class:`~markupsafe.Markup` API by + having a ``__html__`` method to the result of that method. Always + deserializes to an instance of :class:`~markupsafe.Markup`.""" + + __slots__ = () + key = " m" + + def check(self, value: t.Any) -> bool: + return callable(getattr(value, "__html__", None)) + + def to_json(self, value: t.Any) -> t.Any: + return str(value.__html__()) + + def to_python(self, value: t.Any) -> t.Any: + return Markup(value) + + +class TagUUID(JSONTag): + __slots__ = () + key = " u" + + def check(self, value: t.Any) -> bool: + return isinstance(value, UUID) + + def to_json(self, value: t.Any) -> t.Any: + return value.hex + + def to_python(self, value: t.Any) -> t.Any: + return UUID(value) + + +class TagDateTime(JSONTag): + __slots__ = () + key = " d" + + def check(self, value: t.Any) -> bool: + return isinstance(value, datetime) + + def to_json(self, value: t.Any) -> t.Any: + return http_date(value) + + def to_python(self, value: t.Any) -> t.Any: + return parse_date(value) + + +class TaggedJSONSerializer: + """Serializer that uses a tag system to compactly represent objects that + are not JSON types. Passed as the intermediate serializer to + :class:`itsdangerous.Serializer`. + + The following extra types are supported: + + * :class:`dict` + * :class:`tuple` + * :class:`bytes` + * :class:`~markupsafe.Markup` + * :class:`~uuid.UUID` + * :class:`~datetime.datetime` + """ + + __slots__ = ("tags", "order") + + #: Tag classes to bind when creating the serializer. Other tags can be + #: added later using :meth:`~register`. + default_tags = [ + TagDict, + PassDict, + TagTuple, + PassList, + TagBytes, + TagMarkup, + TagUUID, + TagDateTime, + ] + + def __init__(self) -> None: + self.tags: dict[str, JSONTag] = {} + self.order: list[JSONTag] = [] + + for cls in self.default_tags: + self.register(cls) + + def register( + self, + tag_class: type[JSONTag], + force: bool = False, + index: int | None = None, + ) -> None: + """Register a new tag with this serializer. + + :param tag_class: tag class to register. Will be instantiated with this + serializer instance. + :param force: overwrite an existing tag. If false (default), a + :exc:`KeyError` is raised. + :param index: index to insert the new tag in the tag order. Useful when + the new tag is a special case of an existing tag. If ``None`` + (default), the tag is appended to the end of the order. + + :raise KeyError: if the tag key is already registered and ``force`` is + not true. + """ + tag = tag_class(self) + key = tag.key + + if key: + if not force and key in self.tags: + raise KeyError(f"Tag '{key}' is already registered.") + + self.tags[key] = tag + + if index is None: + self.order.append(tag) + else: + self.order.insert(index, tag) + + def tag(self, value: t.Any) -> t.Any: + """Convert a value to a tagged representation if necessary.""" + for tag in self.order: + if tag.check(value): + return tag.tag(value) + + return value + + def untag(self, value: dict[str, t.Any]) -> t.Any: + """Convert a tagged representation back to the original type.""" + if len(value) != 1: + return value + + key = next(iter(value)) + + if key not in self.tags: + return value + + return self.tags[key].to_python(value[key]) + + def _untag_scan(self, value: t.Any) -> t.Any: + if isinstance(value, dict): + # untag each item recursively + value = {k: self._untag_scan(v) for k, v in value.items()} + # untag the dict itself + value = self.untag(value) + elif isinstance(value, list): + # untag each item recursively + value = [self._untag_scan(item) for item in value] + + return value + + def dumps(self, value: t.Any) -> str: + """Tag the value and dump it to a compact JSON string.""" + return dumps(self.tag(value), separators=(",", ":")) + + def loads(self, value: str) -> t.Any: + """Load data from a JSON string and deserialized any tagged objects.""" + return self._untag_scan(loads(value)) diff --git a/test/fixtures/whole_applications/flask/src/flask/logging.py b/test/fixtures/whole_applications/flask/src/flask/logging.py new file mode 100644 index 0000000..0cb8f43 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/logging.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import logging +import sys +import typing as t + +from werkzeug.local import LocalProxy + +from .globals import request + +if t.TYPE_CHECKING: # pragma: no cover + from .sansio.app import App + + +@LocalProxy +def wsgi_errors_stream() -> t.TextIO: + """Find the most appropriate error stream for the application. If a request + is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. + + If you configure your own :class:`logging.StreamHandler`, you may want to + use this for the stream. If you are using file or dict configuration and + can't import this directly, you can refer to it as + ``ext://flask.logging.wsgi_errors_stream``. + """ + if request: + return request.environ["wsgi.errors"] # type: ignore[no-any-return] + + return sys.stderr + + +def has_level_handler(logger: logging.Logger) -> bool: + """Check if there is a handler in the logging chain that will handle the + given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. + """ + level = logger.getEffectiveLevel() + current = logger + + while current: + if any(handler.level <= level for handler in current.handlers): + return True + + if not current.propagate: + break + + current = current.parent # type: ignore + + return False + + +#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format +#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. +default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore +default_handler.setFormatter( + logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") +) + + +def create_logger(app: App) -> logging.Logger: + """Get the Flask app's logger and configure it if needed. + + The logger name will be the same as + :attr:`app.import_name `. + + When :attr:`~flask.Flask.debug` is enabled, set the logger level to + :data:`logging.DEBUG` if it is not set. + + If there is no handler for the logger's effective level, add a + :class:`~logging.StreamHandler` for + :func:`~flask.logging.wsgi_errors_stream` with a basic format. + """ + logger = logging.getLogger(app.name) + + if app.debug and not logger.level: + logger.setLevel(logging.DEBUG) + + if not has_level_handler(logger): + logger.addHandler(default_handler) + + return logger diff --git a/test/fixtures/whole_applications/flask/src/flask/py.typed b/test/fixtures/whole_applications/flask/src/flask/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/whole_applications/flask/src/flask/sansio/README.md b/test/fixtures/whole_applications/flask/src/flask/sansio/README.md new file mode 100644 index 0000000..623ac19 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/sansio/README.md @@ -0,0 +1,6 @@ +# Sansio + +This folder contains code that can be used by alternative Flask +implementations, for example Quart. The code therefore cannot do any +IO, nor be part of a likely IO path. Finally this code cannot use the +Flask globals. diff --git a/test/fixtures/whole_applications/flask/src/flask/sansio/app.py b/test/fixtures/whole_applications/flask/src/flask/sansio/app.py new file mode 100644 index 0000000..01fd5db --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/sansio/app.py @@ -0,0 +1,964 @@ +from __future__ import annotations + +import logging +import os +import sys +import typing as t +from datetime import timedelta +from itertools import chain + +from werkzeug.exceptions import Aborter +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.routing import BuildError +from werkzeug.routing import Map +from werkzeug.routing import Rule +from werkzeug.sansio.response import Response +from werkzeug.utils import cached_property +from werkzeug.utils import redirect as _wz_redirect + +from .. import typing as ft +from ..config import Config +from ..config import ConfigAttribute +from ..ctx import _AppCtxGlobals +from ..helpers import _split_blueprint_path +from ..helpers import get_debug_flag +from ..json.provider import DefaultJSONProvider +from ..json.provider import JSONProvider +from ..logging import create_logger +from ..templating import DispatchingJinjaLoader +from ..templating import Environment +from .scaffold import _endpoint_from_view_func +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.wrappers import Response as BaseResponse + + from ..testing import FlaskClient + from ..testing import FlaskCliRunner + from .blueprints import Blueprint + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class App(Scaffold): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + + #: The class that is used for the Jinja environment. + #: + #: .. versionadded:: 0.11 + jinja_environment = Environment + + #: The class that is used for the :data:`~flask.g` instance. + #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on unexpected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 0.11 + config_class = Config + + #: The testing flag. Set this to ``True`` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate test helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: ``TESTING`` configuration key. Defaults to ``False``. + testing = ConfigAttribute[bool]("TESTING") + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. + secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute[timedelta]( + "PERMANENT_SESSION_LIFETIME", + get_converter=_make_timedelta, # type: ignore[arg-type] + ) + + json_provider_class: type[JSONProvider] = DefaultJSONProvider + """A subclass of :class:`~flask.json.provider.JSONProvider`. An + instance is created and assigned to :attr:`app.json` when creating + the app. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses + Python's built-in :mod:`json` library. A different provider can use + a different JSON library. + + .. versionadded:: 2.2 + """ + + #: Options that are passed to the Jinja environment in + #: :meth:`create_jinja_environment`. Changing these options after + #: the environment is created (accessing :attr:`jinja_env`) will + #: have no effect. + #: + #: .. versionchanged:: 1.1.0 + #: This is a ``dict`` instead of an ``ImmutableDict`` to allow + #: easier configuration. + #: + jinja_options: dict[str, t.Any] = {} + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: The map object to use for storing the URL rules and routing + #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. + #: + #: .. versionadded:: 1.1.0 + url_map_class = Map + + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. + #: + #: .. versionadded:: 0.7 + test_client_class: type[FlaskClient] | None = None + + #: The :class:`~click.testing.CliRunner` subclass, by default + #: :class:`~flask.testing.FlaskCliRunner` that is used by + #: :meth:`test_cli_runner`. Its ``__init__`` method should take a + #: Flask app object as the first argument. + #: + #: .. versionadded:: 1.0 + test_cli_runner_class: type[FlaskCliRunner] | None = None + + default_config: dict[str, t.Any] + response_class: type[Response] + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError( + "If an instance path is provided it must be absolute." + " A relative path was given instead." + ) + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. versionadded:: 2.2 + #: Moved from ``flask.abort``, which calls this object. + self.aborter = self.make_aborter() + + self.json: JSONProvider = self.json_provider_class(self) + """Provides access to JSON methods. Functions in ``flask.json`` + will call methods on this provider when the application context + is active. Used for handling JSON requests and responses. + + An instance of :attr:`json_provider_class`. Can be customized by + changing that attribute on a subclass, or by assigning to this + attribute afterwards. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, + uses Python's built-in :mod:`json` library. A different provider + can use a different JSON library. + + .. versionadded:: 2.2 + """ + + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function is called + #: with ``error``, ``endpoint`` and ``values``. If a function + #: returns ``None`` or raises a ``BuildError``, it is skipped. + #: Otherwise, its return value is returned by ``url_for``. + #: + #: .. versionadded:: 0.9 + self.url_build_error_handlers: list[ + t.Callable[[Exception, str, dict[str, t.Any]], str] + ] = [] + + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] + + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 0.11 + self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] + + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. + #: + #: .. versionadded:: 0.7 + self.blueprints: dict[str, Blueprint] = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. + #: + #: The key must match the name of the extension module. For example in + #: case of a "Flask-Foo" extension in `flask_foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions: dict[str, t.Any] = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(super(ListConverter, self).to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = self.url_map_class(host_matching=host_matching) + + self.subdomain_matching = subdomain_matching + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) + + @cached_property + def name(self) -> str: # type: ignore + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overridden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == "__main__": + fn: str | None = getattr(sys.modules["__main__"], "__file__", None) + if fn is None: + return "__main__" + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. + + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. + + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. + + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. + + .. versionchanged:: 1.0.0 + Behavior was simplified. The logger is always named + ``"flask.app"``. The level is only set during configuration, + it doesn't check ``app.debug`` each time. Only one format is + used, not different ones depending on ``app.debug``. No + handlers are removed, and a handler is only added if no + handlers are already configured. + + .. versionadded:: 0.3 + """ + return create_logger(self) + + @cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. + + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. + """ + return self.create_jinja_environment() + + def create_jinja_environment(self) -> Environment: + raise NotImplementedError() + + def make_config(self, instance_relative: bool = False) -> Config: + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + defaults = dict(self.default_config) + defaults["DEBUG"] = get_debug_flag() + return self.config_class(root_path, defaults) + + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + + def auto_find_instance_path(self) -> str: + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, "instance") + return os.path.join(prefix, "var", f"{self.name}-instance") + + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) + + @property + def debug(self) -> bool: + """Whether debug mode is enabled. When using ``flask run`` to start the + development server, an interactive debugger will be shown for unhandled + exceptions, and the server will be reloaded when code changes. This maps to the + :data:`DEBUG` config key. It may not behave as expected if set late. + + **Do not enable debug mode when deploying in production.** + + Default: ``False`` + """ + return self.config["DEBUG"] # type: ignore[no-any-return] + + @debug.setter + def debug(self, value: bool) -> None: + self.config["DEBUG"] = value + + if self.config["TEMPLATES_AUTO_RELOAD"] is None: + self.jinja_env.auto_reload = value + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on the application. Keyword + arguments passed to this method will override the defaults set on the + blueprint. + + Calls the blueprint's :meth:`~flask.Blueprint.register` method after + recording the blueprint in the application's :attr:`blueprints`. + + :param blueprint: The blueprint to register. + :param url_prefix: Blueprint routes will be prefixed with this. + :param subdomain: Blueprint routes will match on this subdomain. + :param url_defaults: Blueprint routes will use these default values for + view arguments. + :param options: Additional keyword arguments are passed to + :class:`~flask.blueprints.BlueprintSetupState`. They can be + accessed in :meth:`~flask.Blueprint.record` callbacks. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 0.7 + """ + blueprint.register(self, options) + + def iter_blueprints(self) -> t.ValuesView[Blueprint]: + """Iterates over all blueprints by the order they were registered. + + .. versionadded:: 0.11 + """ + return self.blueprints.values() + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + options["endpoint"] = endpoint + methods = options.pop("methods", None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only ``GET`` as default. + if methods is None: + methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): + raise TypeError( + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' + ) + methods = {item.upper() for item in methods} + + # Methods that should always be added + required_methods = set(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # Add the required methods now. + methods |= required_methods + + rule_obj = self.url_rule_class(rule, methods=methods, **options) + rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] + + self.url_map.add(rule_obj) + if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func != view_func: + raise AssertionError( + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" + ) + self.view_functions[endpoint] = view_func + + @setupmethod + def template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + + @setupmethod + def teardown_appcontext(self, f: T_teardown) -> T_teardown: + """Registers a function to be called when the application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. + + .. code-block:: python + + with app.app_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the app context is + made inactive. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + + @setupmethod + def shell_context_processor( + self, f: T_shell_context_processor + ) -> T_shell_context_processor: + """Registers a shell context processor function. + + .. versionadded:: 0.11 + """ + self.shell_context_processors.append(f) + return f + + def _find_error_handler( + self, e: Exception, blueprints: list[str] + ) -> ft.ErrorHandlerCallable | None: + """Return a registered error handler for an exception in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint handler for an exception class, app handler for an exception + class, or ``None`` if a suitable handler is not found. + """ + exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*blueprints, None) + + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] + + if not handler_map: + continue + + for cls in exc_class.__mro__: + handler = handler_map.get(cls) + + if handler is not None: + return handler + return None + + def trap_http_exception(self, e: Exception) -> bool: + """Checks if an HTTP exception should be trapped or not. By default + this will return ``False`` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It + also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. + + This is called for all HTTP exceptions raised by a view function. + If it returns ``True`` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + + .. versionadded:: 0.8 + """ + if self.config["TRAP_HTTP_EXCEPTIONS"]: + return True + + trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] + + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None + and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: + return isinstance(e, BadRequest) + + return False + + def should_ignore_error(self, error: BaseException | None) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def redirect(self, location: str, code: int = 302) -> BaseResponse: + """Create a redirect response object. + + This is called by :func:`flask.redirect`, and can be called + directly as well. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + + .. versionadded:: 2.2 + Moved from ``flask.redirect``, which calls this method. + """ + return _wz_redirect( + location, + code=code, + Response=self.response_class, # type: ignore[arg-type] + ) + + def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + names: t.Iterable[str | None] = (None,) + + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: BuildError, endpoint: str, values: dict[str, t.Any] + ) -> str: + """Called by :meth:`.url_for` if a + :exc:`~werkzeug.routing.BuildError` was raised. If this returns + a value, it will be returned by ``url_for``, otherwise the error + will be re-raised. + + Each function in :attr:`url_build_error_handlers` is called with + ``error``, ``endpoint`` and ``values``. If a function returns + ``None`` or raises a ``BuildError``, it is skipped. Otherwise, + its return value is returned by ``url_for``. + + :param error: The active ``BuildError`` being handled. + :param endpoint: The endpoint being built. + :param values: The keyword arguments passed to ``url_for``. + """ + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: + if rv is not None: + return rv + + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + + raise error diff --git a/test/fixtures/whole_applications/flask/src/flask/sansio/blueprints.py b/test/fixtures/whole_applications/flask/src/flask/sansio/blueprints.py new file mode 100644 index 0000000..4f912cc --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/sansio/blueprints.py @@ -0,0 +1,632 @@ +from __future__ import annotations + +import os +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from .. import typing as ft +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from .app import App + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) + + +class BlueprintSetupState: + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__( + self, + blueprint: Blueprint, + app: App, + options: t.Any, + first_registration: bool, + ) -> None: + #: a reference to the current application + self.app = app + + #: a reference to the blueprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get("subdomain") + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, ``None`` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get("url_prefix") + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get("url_defaults", ())) + + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + **options: t.Any, + ) -> None: + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix is not None: + if rule: + rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: + rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + + self.app.add_url_rule( + rule, + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), + view_func, + defaults=defaults, + **options, + ) + + +class Blueprint(Scaffold): + """Represents a blueprint, a collection of routes and other + app-related functions that can be registered on a real application + later. + + A blueprint is an object that allows defining application functions + without requiring an application object ahead of time. It uses the + same decorators as :class:`~flask.Flask`, but defers the need for an + application by recording them for later registration. + + Decorating a function with a blueprint creates a deferred function + that is called with :class:`~flask.blueprints.BlueprintSetupState` + when the blueprint is registered on an application. + + See :doc:`/blueprints` for more information. + + :param name: The name of the blueprint. Will be prepended to each + endpoint name. + :param import_name: The name of the blueprint package, usually + ``__name__``. This helps locate the ``root_path`` for the + blueprint. + :param static_folder: A folder with static files that should be + served by the blueprint's static route. The path is relative to + the blueprint's root path. Blueprint static files are disabled + by default. + :param static_url_path: The url to serve static files from. + Defaults to ``static_folder``. If the blueprint does not have + a ``url_prefix``, the app's static route will take precedence, + and the blueprint's static files won't be accessible. + :param template_folder: A folder with templates that should be added + to the app's template search path. The path is relative to the + blueprint's root path. Blueprint templates are disabled by + default. Blueprint templates have a lower precedence than those + in the app's templates folder. + :param url_prefix: A path to prepend to all of the blueprint's URLs, + to make them distinct from the rest of the app's routes. + :param subdomain: A subdomain that blueprint routes will match on by + default. + :param url_defaults: A dict of default values that blueprint routes + will receive by default. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 + """ + + _got_registered_once = False + + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore[assignment] + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if not name: + raise ValueError("'name' may not be empty.") + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: list[DeferredSetupFunction] = [] + + if url_defaults is None: + url_defaults = {} + + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called on the blueprint" + f" '{self.name}'. It has already been registered at least once, any" + " changes will not be applied consistently.\n" + "Make sure all imports, decorators, functions, etc. needed to set up" + " the blueprint are done before registering it." + ) + + @setupmethod + def record(self, func: DeferredSetupFunction) -> None: + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + self.deferred_functions.append(func) + + @setupmethod + def record_once(self, func: DeferredSetupFunction) -> None: + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) + + self.record(update_wrapper(wrapper, func)) + + def make_setup_state( + self, app: App, options: dict[str, t.Any], first_registration: bool = False + ) -> BlueprintSetupState: + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: App, options: dict[str, t.Any]) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. + :param options: Keyword arguments forwarded from + :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + + .. versionchanged:: 2.1 + Registering the same blueprint with the same name multiple + times is an error. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + bp_desc = "this" if app.blueprints[name] is self else "a different" + existing_at = f" '{name}'" if self_name != name else "" + + raise ValueError( + f"The name '{self_name}' is already registered for" + f" {bp_desc} blueprint{existing_at}. Use 'name=' to" + f" provide a unique name." + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self + self._got_registered_once = True + state = self.make_setup_state(app, options, first_bp_registration) + + if self.has_static_folder: + state.add_url_rule( + f"{self.static_url_path}/", + view_func=self.send_static_file, # type: ignore[attr-defined] + endpoint="static", + ) + + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + self._merge_blueprint_funcs(app, name) + + for deferred in self.deferred_functions: + deferred(state) + + cli_resolved_group = options.get("cli_group", self.cli_group) + + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) + + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain + + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix + + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix + + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) + + def _merge_blueprint_funcs(self, app: App, name: str) -> None: + def extend( + bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + ) -> None: + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: {exc_class: func for exc_class, func in code_values.items()} + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. + """ + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) + + @setupmethod + def app_template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_app_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_app_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_app_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def before_app_request(self, f: T_before_request) -> T_before_request: + """Like :meth:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. + """ + self.record_once( + lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def after_app_request(self, f: T_after_request) -> T_after_request: + """Like :meth:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. + """ + self.record_once( + lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def teardown_app_request(self, f: T_teardown) -> T_teardown: + """Like :meth:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. + """ + self.record_once( + lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_context_processor( + self, f: T_template_context_processor + ) -> T_template_context_processor: + """Like :meth:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. + """ + self.record_once( + lambda s: s.app.template_context_processors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_errorhandler( + self, code: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Like :meth:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. + """ + + def decorator(f: T_error_handler) -> T_error_handler: + def from_blueprint(state: BlueprintSetupState) -> None: + state.app.errorhandler(code)(f) + + self.record_once(from_blueprint) + return f + + return decorator + + @setupmethod + def app_url_value_preprocessor( + self, f: T_url_value_preprocessor + ) -> T_url_value_preprocessor: + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ + self.record_once( + lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/test/fixtures/whole_applications/flask/src/flask/sansio/scaffold.py b/test/fixtures/whole_applications/flask/src/flask/sansio/scaffold.py new file mode 100644 index 0000000..69e33a0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/sansio/scaffold.py @@ -0,0 +1,801 @@ +from __future__ import annotations + +import importlib.util +import os +import pathlib +import sys +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from jinja2 import BaseLoader +from jinja2 import FileSystemLoader +from werkzeug.exceptions import default_exceptions +from werkzeug.exceptions import HTTPException +from werkzeug.utils import cached_property + +from .. import typing as ft +from ..helpers import get_root_path +from ..templating import _default_template_ctx_processor + +if t.TYPE_CHECKING: # pragma: no cover + from click import Group + +# a singleton sentinel value for parameter defaults +_sentinel = object() + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) +T_route = t.TypeVar("T_route", bound=ft.RouteCallable) + + +def setupmethod(f: F) -> F: + f_name = f.__name__ + + def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: + self._check_setup_finished(f_name) + return f(self, *args, **kwargs) + + return t.cast(F, update_wrapper(wrapper_func, f)) + + +class Scaffold: + """Common behavior shared between :class:`~flask.Flask` and + :class:`~flask.blueprints.Blueprint`. + + :param import_name: The import name of the module where this object + is defined. Usually :attr:`__name__` should be used. + :param static_folder: Path to a folder of static files to serve. + If this is set, a static route will be added. + :param static_url_path: URL prefix for the static route. + :param template_folder: Path to a folder containing template files. + for rendering. If this is set, a Jinja loader will be added. + :param root_path: The path that static, template, and resource files + are relative to. Typically not set, it is discovered based on + the ``import_name``. + + .. versionadded:: 2.0 + """ + + cli: Group + name: str + _static_folder: str | None = None + _static_url_path: str | None = None + + def __init__( + self, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + root_path: str | None = None, + ): + #: The name of the package or module that this object belongs + #: to. Do not change this once it is set by the constructor. + self.import_name = import_name + + self.static_folder = static_folder # type: ignore + self.static_url_path = static_url_path + + #: The path to the templates folder, relative to + #: :attr:`root_path`, to add to the template loader. ``None`` if + #: templates should not be added. + self.template_folder = template_folder + + if root_path is None: + root_path = get_root_path(self.import_name) + + #: Absolute path to the package on the filesystem. Used to look + #: up resources contained in the package. + self.root_path = root_path + + #: A dictionary mapping endpoint names to view functions. + #: + #: To register a view function, use the :meth:`route` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.view_functions: dict[str, ft.RouteCallable] = {} + + #: A data structure of registered error handlers, in the format + #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is + #: the name of a blueprint the handlers are active for, or + #: ``None`` for all requests. The ``code`` key is the HTTP + #: status code for ``HTTPException``, or ``None`` for + #: other exceptions. The innermost dictionary maps exception + #: classes to handler functions. + #: + #: To register an error handler, use the :meth:`errorhandler` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.error_handler_spec: dict[ + ft.AppOrBlueprintKey, + dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], + ] = defaultdict(lambda: defaultdict(dict)) + + #: A data structure of functions to call at the beginning of + #: each request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`before_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.before_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`after_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.after_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request even if an exception is raised, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`teardown_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.teardown_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.TeardownCallable] + ] = defaultdict(list) + + #: A data structure of functions to call to pass extra context + #: values when rendering templates, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`context_processor` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.template_context_processors: dict[ + ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] + ] = defaultdict(list, {None: [_default_template_ctx_processor]}) + + #: A data structure of functions to call to modify the keyword + #: arguments passed to the view function, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the + #: :meth:`url_value_preprocessor` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_value_preprocessors: dict[ + ft.AppOrBlueprintKey, + list[ft.URLValuePreprocessorCallable], + ] = defaultdict(list) + + #: A data structure of functions to call to modify the keyword + #: arguments when generating URLs, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`url_defaults` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_default_functions: dict[ + ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] + ] = defaultdict(list) + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.name!r}>" + + def _check_setup_finished(self, f_name: str) -> None: + raise NotImplementedError + + @property + def static_folder(self) -> str | None: + """The absolute path to the configured static folder. ``None`` + if no static folder is set. + """ + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + else: + return None + + @static_folder.setter + def static_folder(self, value: str | os.PathLike[str] | None) -> None: + if value is not None: + value = os.fspath(value).rstrip(r"\/") + + self._static_folder = value + + @property + def has_static_folder(self) -> bool: + """``True`` if :attr:`static_folder` is set. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @property + def static_url_path(self) -> str | None: + """The URL prefix that the static route will be accessible from. + + If it was not configured during init, it is derived from + :attr:`static_folder`. + """ + if self._static_url_path is not None: + return self._static_url_path + + if self.static_folder is not None: + basename = os.path.basename(self.static_folder) + return f"/{basename}".rstrip("/") + + return None + + @static_url_path.setter + def static_url_path(self, value: str | None) -> None: + if value is not None: + value = value.rstrip("/") + + self._static_url_path = value + + @cached_property + def jinja_loader(self) -> BaseLoader | None: + """The Jinja loader for this object's templates. By default this + is a class :class:`jinja2.loaders.FileSystemLoader` to + :attr:`template_folder` if it is set. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + else: + return None + + def _method_route( + self, + method: str, + rule: str, + options: dict[str, t.Any], + ) -> t.Callable[[T_route], T_route]: + if "methods" in options: + raise TypeError("Use the 'route' decorator to use the 'methods' argument.") + + return self.route(rule, methods=[method], **options) + + @setupmethod + def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["GET"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("GET", rule, options) + + @setupmethod + def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["POST"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("POST", rule, options) + + @setupmethod + def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PUT"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PUT", rule, options) + + @setupmethod + def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["DELETE"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("DELETE", rule, options) + + @setupmethod + def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PATCH"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PATCH", rule, options) + + @setupmethod + def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Decorate a view function to register it with the given URL + rule and options. Calls :meth:`add_url_rule`, which has more + details about the implementation. + + .. code-block:: python + + @app.route("/") + def index(): + return "Hello, World!" + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and + ``OPTIONS`` are added automatically. + + :param rule: The URL rule string. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + + def decorator(f: T_route) -> T_route: + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) + return f + + return decorator + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a rule for routing incoming requests and building + URLs. The :meth:`route` decorator is a shortcut to call this + with the ``view_func`` argument. These are equivalent: + + .. code-block:: python + + @app.route("/") + def index(): + ... + + .. code-block:: python + + def index(): + ... + + app.add_url_rule("/", view_func=index) + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. An error + will be raised if a function has already been registered for the + endpoint. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is + always added automatically, and ``OPTIONS`` is added + automatically by default. + + ``view_func`` does not necessarily need to be passed, but if the + rule should participate in routing an endpoint name must be + associated with a view function at some point with the + :meth:`endpoint` decorator. + + .. code-block:: python + + app.add_url_rule("/", endpoint="index") + + @app.endpoint("index") + def index(): + ... + + If ``view_func`` has a ``required_methods`` attribute, those + methods are added to the passed and automatic methods. If it + has a ``provide_automatic_methods`` attribute, it is used as the + default if the parameter is not passed. + + :param rule: The URL rule string. + :param endpoint: The endpoint name to associate with the rule + and view function. Used when routing and building URLs. + Defaults to ``view_func.__name__``. + :param view_func: The view function to associate with the + endpoint name. + :param provide_automatic_options: Add the ``OPTIONS`` method and + respond to ``OPTIONS`` requests automatically. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + raise NotImplementedError + + @setupmethod + def endpoint(self, endpoint: str) -> t.Callable[[F], F]: + """Decorate a view function to register it for the given + endpoint. Used if a rule is added without a ``view_func`` with + :meth:`add_url_rule`. + + .. code-block:: python + + app.add_url_rule("/ex", endpoint="example") + + @app.endpoint("example") + def example(): + ... + + :param endpoint: The endpoint name to associate with the view + function. + """ + + def decorator(f: F) -> F: + self.view_functions[endpoint] = f + return f + + return decorator + + @setupmethod + def before_request(self, f: T_before_request) -> T_before_request: + """Register a function to run before each request. + + For example, this can be used to open a database connection, or + to load the logged in user from the session. + + .. code-block:: python + + @app.before_request + def load_user(): + if "user_id" in session: + g.user = db.session.get(session["user_id"]) + + The function will be called without any arguments. If it returns + a non-``None`` value, the value is handled as if it was the + return value from the view, and further request handling is + stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. + """ + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def after_request(self, f: T_after_request) -> T_after_request: + """Register a function to run after each request to this object. + + The function is called with the response object, and must return + a response object. This allows the functions to modify or + replace the response before it is sent. + + If a function raises an exception, any remaining + ``after_request`` functions will not be called. Therefore, this + should not be used for actions that must execute, such as to + close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f: T_teardown) -> T_teardown: + """Register a function to be called when the request context is + popped. Typically this happens at the end of each request, but + contexts may be pushed manually as well during testing. + + .. code-block:: python + + with app.test_request_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the request context is + made inactive. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor( + self, + f: T_template_context_processor, + ) -> T_template_context_processor: + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor( + self, + f: T_url_value_preprocessor, + ) -> T_url_value_preprocessor: + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. + """ + self.url_value_preprocessors[None].append(f) + return f + + @setupmethod + def url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. + """ + self.url_default_functions[None].append(f) + return f + + @setupmethod + def errorhandler( + self, code_or_exception: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception + """ + + def decorator(f: T_error_handler) -> T_error_handler: + self.register_error_handler(code_or_exception, f) + return f + + return decorator + + @setupmethod + def register_error_handler( + self, + code_or_exception: type[Exception] | int, + f: ft.ErrorHandlerCallable, + ) -> None: + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + exc_class, code = self._get_exc_class_and_code(code_or_exception) + self.error_handler_spec[None][code][exc_class] = f + + @staticmethod + def _get_exc_class_and_code( + exc_class_or_code: type[Exception] | int, + ) -> tuple[type[Exception], int | None]: + """Get the exception class being handled. For HTTP status codes + or ``HTTPException`` subclasses, return both the exception and + status code. + + :param exc_class_or_code: Any exception class, or an HTTP status + code as an integer. + """ + exc_class: type[Exception] + + if isinstance(exc_class_or_code, int): + try: + exc_class = default_exceptions[exc_class_or_code] + except KeyError: + raise ValueError( + f"'{exc_class_or_code}' is not a recognized HTTP" + " error code. Use a subclass of HTTPException with" + " that code instead." + ) from None + else: + exc_class = exc_class_or_code + + if isinstance(exc_class, Exception): + raise TypeError( + f"{exc_class!r} is an instance, not a class. Handlers" + " can only be registered for Exception classes or HTTP" + " error codes." + ) + + if not issubclass(exc_class, Exception): + raise ValueError( + f"'{exc_class.__name__}' is not a subclass of Exception." + " Handlers can only be registered for Exception classes" + " or HTTP error codes." + ) + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + + +def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: + # Path.is_relative_to doesn't exist until Python 3.9 + try: + path.relative_to(base) + return True + except ValueError: + return False + + +def _find_package_path(import_name: str) -> str: + """Find the path that contains the package or module.""" + root_mod_name, _, _ = import_name.partition(".") + + try: + root_spec = importlib.util.find_spec(root_mod_name) + + if root_spec is None: + raise ValueError("not found") + except (ImportError, ValueError): + # ImportError: the machinery told us it does not exist + # ValueError: + # - the module name was invalid + # - the module name is __main__ + # - we raised `ValueError` due to `root_spec` being `None` + return os.getcwd() + + if root_spec.submodule_search_locations: + if root_spec.origin is None or root_spec.origin == "namespace": + # namespace package + package_spec = importlib.util.find_spec(import_name) + + if package_spec is not None and package_spec.submodule_search_locations: + # Pick the path in the namespace that contains the submodule. + package_path = pathlib.Path( + os.path.commonpath(package_spec.submodule_search_locations) + ) + search_location = next( + location + for location in root_spec.submodule_search_locations + if _path_is_relative_to(package_path, location) + ) + else: + # Pick the first path. + search_location = root_spec.submodule_search_locations[0] + + return os.path.dirname(search_location) + else: + # package with __init__.py + return os.path.dirname(os.path.dirname(root_spec.origin)) + else: + # module + return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] + + +def find_package(import_name: str) -> tuple[str | None, str]: + """Find the prefix that a package is installed under, and the path + that it would be imported from. + + The prefix is the directory containing the standard directory + hierarchy (lib, bin, etc.). If the package is not installed to the + system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), + ``None`` is returned. + + The path is the entry in :attr:`sys.path` that contains the package + for import. If the package is not installed, it's assumed that the + package was imported from the current working directory. + """ + package_path = _find_package_path(import_name) + py_prefix = os.path.abspath(sys.prefix) + + # installed to the system + if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix): + return py_prefix, package_path + + site_parent, site_folder = os.path.split(package_path) + + # installed to a virtualenv + if site_folder.lower() == "site-packages": + parent, folder = os.path.split(site_parent) + + # Windows (prefix/lib/site-packages) + if folder.lower() == "lib": + return parent, package_path + + # Unix (prefix/lib/pythonX.Y/site-packages) + if os.path.basename(parent).lower() == "lib": + return os.path.dirname(parent), package_path + + # something else (prefix/site-packages) + return site_parent, package_path + + # not installed + return None, package_path diff --git a/test/fixtures/whole_applications/flask/src/flask/sessions.py b/test/fixtures/whole_applications/flask/src/flask/sessions.py new file mode 100644 index 0000000..ee19ad6 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/sessions.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import hashlib +import typing as t +from collections.abc import MutableMapping +from datetime import datetime +from datetime import timezone + +from itsdangerous import BadSignature +from itsdangerous import URLSafeTimedSerializer +from werkzeug.datastructures import CallbackDict + +from .json.tag import TaggedJSONSerializer + +if t.TYPE_CHECKING: # pragma: no cover + import typing_extensions as te + + from .app import Flask + from .wrappers import Request + from .wrappers import Response + + +# TODO generic when Python > 3.8 +class SessionMixin(MutableMapping): # type: ignore[type-arg] + """Expands a basic dictionary with session attributes.""" + + @property + def permanent(self) -> bool: + """This reflects the ``'_permanent'`` key in the dict.""" + return self.get("_permanent", False) + + @permanent.setter + def permanent(self, value: bool) -> None: + self["_permanent"] = bool(value) + + #: Some implementations can detect whether a session is newly + #: created, but that is not guaranteed. Use with caution. The mixin + # default is hard-coded ``False``. + new = False + + #: Some implementations can detect changes to the session and set + #: this when that happens. The mixin default is hard coded to + #: ``True``. + modified = True + + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. + accessed = True + + +# TODO generic when Python > 3.8 +class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg] + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`modified` and + :attr:`accessed` attributes. It cannot reliably track whether a + session is new (vs. empty), so :attr:`new` remains hard coded to + ``False``. + """ + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: will only be written to the response if this is ``True``. + modified = False + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False + + def __init__(self, initial: t.Any = None) -> None: + def on_update(self: te.Self) -> None: + self.modified = True + self.accessed = True + + super().__init__(initial, on_update) + + def __getitem__(self, key: str) -> t.Any: + self.accessed = True + return super().__getitem__(key) + + def get(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().get(key, default) + + def setdefault(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().setdefault(key, default) + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + raise RuntimeError( + "The session is unavailable because no secret " + "key was set. Set the secret_key on the " + "application to something unique and secret." + ) + + __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 + del _fail + + +class SessionInterface: + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns ``None`` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + Multiple requests with the same session may be sent and handled + concurrently. When implementing a new session interface, consider + whether reads or writes to the backing store must be synchronized. + There is no guarantee on the order in which the session for each + request is opened or saved, it will occur in the order that requests + begin and end processing. + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + #: A flag that indicates if the session interface is pickle based. + #: This can be used by Flask extensions to make a decision in regards + #: to how to deal with the session object. + #: + #: .. versionadded:: 0.10 + pickle_based = False + + def make_null_session(self, app: Flask) -> NullSession: + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj: object) -> bool: + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_name(self, app: Flask) -> str: + """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" + return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] + + def get_cookie_domain(self, app: Flask) -> str | None: + """The value of the ``Domain`` parameter on the session cookie. If not set, + browsers will only send the cookie to the exact domain it was set from. + Otherwise, they will send it to any subdomain of the given value as well. + + Uses the :data:`SESSION_COOKIE_DOMAIN` config. + + .. versionchanged:: 2.3 + Not set by default, does not fall back to ``SERVER_NAME``. + """ + return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] + + def get_cookie_path(self, app: Flask) -> str: + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the ``SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's ``None``. + """ + return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] + + def get_cookie_httponly(self, app: Flask) -> bool: + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] + + def get_cookie_secure(self, app: Flask) -> bool: + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] + + def get_cookie_samesite(self, app: Flask) -> str | None: + """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the + ``SameSite`` attribute. This currently just returns the value of + the :data:`SESSION_COOKIE_SAMESITE` setting. + """ + return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] + + def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: + """A helper method that returns an expiration date for the session + or ``None`` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.now(timezone.utc) + app.permanent_session_lifetime + return None + + def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: + """Used by session backends to determine if a ``Set-Cookie`` header + should be set for this session cookie for this response. If the session + has been modified, the cookie is set. If the session is permanent and + the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is + always set. + + This check is usually skipped if the session was deleted. + + .. versionadded:: 0.11 + """ + + return session.modified or ( + session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] + ) + + def open_session(self, app: Flask, request: Request) -> SessionMixin | None: + """This is called at the beginning of each request, after + pushing the request context, before matching the URL. + + This must return an object which implements a dictionary-like + interface as well as the :class:`SessionMixin` interface. + + This will return ``None`` to indicate that loading failed in + some way that is not immediately an error. The request + context will fall back to using :meth:`make_null_session` + in this case. + """ + raise NotImplementedError() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + """This is called at the end of each request, after generating + a response, before removing the request context. It is skipped + if :meth:`is_null_session` returns ``True``. + """ + raise NotImplementedError() + + +session_json_serializer = TaggedJSONSerializer() + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. + salt = "cookie-session" + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(_lazy_sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = "hmac" + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. + serializer = session_json_serializer + session_class = SecureCookieSession + + def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: + if not app.secret_key: + return None + signer_kwargs = dict( + key_derivation=self.key_derivation, digest_method=self.digest_method + ) + return URLSafeTimedSerializer( + app.secret_key, + salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs, + ) + + def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: + s = self.get_signing_serializer(app) + if s is None: + return None + val = request.cookies.get(self.get_cookie_name(app)) + if not val: + return self.session_class() + max_age = int(app.permanent_session_lifetime.total_seconds()) + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + name = self.get_cookie_name(app) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + secure = self.get_cookie_secure(app) + samesite = self.get_cookie_samesite(app) + httponly = self.get_cookie_httponly(app) + + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: + response.vary.add("Cookie") + + # If the session is modified to be empty, remove the cookie. + # If the session is empty, return without setting the cookie. + if not session: + if session.modified: + response.delete_cookie( + name, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + httponly=httponly, + ) + response.vary.add("Cookie") + + return + + if not self.should_set_cookie(app, session): + return + + expires = self.get_expiration_time(app, session) + val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore + response.set_cookie( + name, + val, # type: ignore + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/test/fixtures/whole_applications/flask/src/flask/signals.py b/test/fixtures/whole_applications/flask/src/flask/signals.py new file mode 100644 index 0000000..444fda9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/signals.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from blinker import Namespace + +# This namespace is only for signals provided by Flask itself. +_signals = Namespace() + +template_rendered = _signals.signal("template-rendered") +before_render_template = _signals.signal("before-render-template") +request_started = _signals.signal("request-started") +request_finished = _signals.signal("request-finished") +request_tearing_down = _signals.signal("request-tearing-down") +got_request_exception = _signals.signal("got-request-exception") +appcontext_tearing_down = _signals.signal("appcontext-tearing-down") +appcontext_pushed = _signals.signal("appcontext-pushed") +appcontext_popped = _signals.signal("appcontext-popped") +message_flashed = _signals.signal("message-flashed") diff --git a/test/fixtures/whole_applications/flask/src/flask/templating.py b/test/fixtures/whole_applications/flask/src/flask/templating.py new file mode 100644 index 0000000..618a3b3 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/templating.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import typing as t + +from jinja2 import BaseLoader +from jinja2 import Environment as BaseEnvironment +from jinja2 import Template +from jinja2 import TemplateNotFound + +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .helpers import stream_with_context +from .signals import before_render_template +from .signals import template_rendered + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .sansio.app import App + from .sansio.scaffold import Scaffold + + +def _default_template_ctx_processor() -> dict[str, t.Any]: + """Default template context processor. Injects `request`, + `session` and `g`. + """ + appctx = _cv_app.get(None) + reqctx = _cv_request.get(None) + rv: dict[str, t.Any] = {} + if appctx is not None: + rv["g"] = appctx.g + if reqctx is not None: + rv["request"] = reqctx.request + rv["session"] = reqctx.session + return rv + + +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app: App, **options: t.Any) -> None: + if "loader" not in options: + options["loader"] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app: App) -> None: + self.app = app + + def get_source( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: + return self._get_source_explained(environment, template) + return self._get_source_fast(environment, template) + + def _get_source_explained( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + attempts = [] + rv: tuple[str, str | None, t.Callable[[], bool] | None] | None + trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None + + for srcobj, loader in self._iter_loaders(template): + try: + rv = loader.get_source(environment, template) + if trv is None: + trv = rv + except TemplateNotFound: + rv = None + attempts.append((loader, srcobj, rv)) + + from .debughelpers import explain_template_loading_attempts + + explain_template_loading_attempts(self.app, template, attempts) + + if trv is not None: + return trv + raise TemplateNotFound(template) + + def _get_source_fast( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + for _srcobj, loader in self._iter_loaders(template): + try: + return loader.get_source(environment, template) + except TemplateNotFound: + continue + raise TemplateNotFound(template) + + def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: + loader = self.app.jinja_loader + if loader is not None: + yield self.app, loader + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + yield blueprint, loader + + def list_templates(self) -> list[str]: + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + result.add(template) + + return list(result) + + +def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + rv = template.render(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + return rv + + +def render_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> str: + """Render a template by name with the given context. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) + + +def render_template_string(source: str, **context: t.Any) -> str: + """Render a template from the given source string with the given + context. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) + + +def _stream( + app: Flask, template: Template, context: dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + def generate() -> t.Iterator[str]: + yield from template.generate(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +def stream_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> t.Iterator[str]: + """Render a template by name with the given context as a stream. + This returns an iterator of strings, which can be used as a + streaming response from a view. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, template, context) + + +def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: + """Render a template from the given source string with the given + context as a stream. This returns an iterator of strings, which can + be used as a streaming response from a view. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/test/fixtures/whole_applications/flask/src/flask/testing.py b/test/fixtures/whole_applications/flask/src/flask/testing.py new file mode 100644 index 0000000..a27b7c8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/testing.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import importlib.metadata +import typing as t +from contextlib import contextmanager +from contextlib import ExitStack +from copy import copy +from types import TracebackType +from urllib.parse import urlsplit + +import werkzeug.test +from click.testing import CliRunner +from werkzeug.test import Client +from werkzeug.wrappers import Request as BaseRequest + +from .cli import ScriptInfo +from .sessions import SessionMixin + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + from werkzeug.test import TestResponse + + from .app import Flask + + +class EnvironBuilder(werkzeug.test.EnvironBuilder): + """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the + application. + + :param app: The Flask application to configure the environment from. + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + + def __init__( + self, + app: Flask, + path: str = "/", + base_url: str | None = None, + subdomain: str | None = None, + url_scheme: str | None = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: + assert not (base_url or subdomain or url_scheme) or ( + base_url is not None + ) != bool( + subdomain or url_scheme + ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + + if base_url is None: + http_host = app.config.get("SERVER_NAME") or "localhost" + app_root = app.config["APPLICATION_ROOT"] + + if subdomain: + http_host = f"{subdomain}.{http_host}" + + if url_scheme is None: + url_scheme = app.config["PREFERRED_URL_SCHEME"] + + url = urlsplit(path) + base_url = ( + f"{url.scheme or url_scheme}://{url.netloc or http_host}" + f"/{app_root.lstrip('/')}" + ) + path = url.path + + if url.query: + sep = b"?" if isinstance(url.query, bytes) else "?" + path += sep + url.query + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore + """Serialize ``obj`` to a JSON-formatted string. + + The serialization will be configured according to the config associated + with this EnvironBuilder's ``app``. + """ + return self.app.json.dumps(obj, **kwargs) + + +_werkzeug_version = "" + + +def _get_werkzeug_version() -> str: + global _werkzeug_version + + if not _werkzeug_version: + _werkzeug_version = importlib.metadata.version("werkzeug") + + return _werkzeug_version + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has knowledge about + Flask's contexts to defer the cleanup of the request context until + the end of a ``with`` block. For general information about how to + use this class refer to :class:`werkzeug.test.Client`. + + .. versionchanged:: 0.12 + `app.test_client()` includes preset default environment, which can be + set after instantiation of the `app.test_client()` object in + `client.environ_base`. + + Basic usage is outlined in the :doc:`/testing` chapter. + """ + + application: Flask + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + self.preserve_context = False + self._new_contexts: list[t.ContextManager[t.Any]] = [] + self._context_stack = ExitStack() + self.environ_base = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", + } + + @contextmanager + def session_transaction( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Iterator[SessionMixin]: + """When used in combination with a ``with`` statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the ``with`` block is left the session is + stored back. + + :: + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + app = self.application + ctx = app.test_request_context(*args, **kwargs) + self._add_cookies_to_wsgi(ctx.request.environ) + + with ctx: + sess = app.session_interface.open_session(app, ctx.request) + + if sess is None: + raise RuntimeError("Session backend did not open a session.") + + yield sess + resp = app.response_class() + + if app.session_interface.is_null_session(sess): + return + + with ctx: + app.session_interface.save_session(app, sess, resp) + + self._update_cookies_from_response( + ctx.request.host.partition(":")[0], + ctx.request.path, + resp.headers.getlist("Set-Cookie"), + ) + + def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: + out = {**self.environ_base, **other} + + if self.preserve_context: + out["werkzeug.debug.preserve_context"] = self._new_contexts.append + + return out + + def _request_from_builder_args( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> BaseRequest: + kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) + builder = EnvironBuilder(self.application, *args, **kwargs) + + try: + return builder.get_request() + finally: + builder.close() + + def open( + self, + *args: t.Any, + buffered: bool = False, + follow_redirects: bool = False, + **kwargs: t.Any, + ) -> TestResponse: + if args and isinstance( + args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) + ): + if isinstance(args[0], werkzeug.test.EnvironBuilder): + builder = copy(args[0]) + builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] + request = builder.get_request() + elif isinstance(args[0], dict): + request = EnvironBuilder.from_environ( + args[0], app=self.application, environ_base=self._copy_environ({}) + ).get_request() + else: + # isinstance(args[0], BaseRequest) + request = copy(args[0]) + request.environ = self._copy_environ(request.environ) + else: + # request is None + request = self._request_from_builder_args(args, kwargs) + + # Pop any previously preserved contexts. This prevents contexts + # from being preserved across redirects or multiple requests + # within a single block. + self._context_stack.close() + + response = super().open( + request, + buffered=buffered, + follow_redirects=follow_redirects, + ) + response.json_module = self.application.json # type: ignore[assignment] + + # Re-push contexts that were preserved during the request. + while self._new_contexts: + cm = self._new_contexts.pop() + self._context_stack.enter_context(cm) + + return response + + def __enter__(self) -> FlaskClient: + if self.preserve_context: + raise RuntimeError("Cannot nest client invocations") + self.preserve_context = True + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.preserve_context = False + self._context_stack.close() + + +class FlaskCliRunner(CliRunner): + """A :class:`~click.testing.CliRunner` for testing a Flask app's + CLI commands. Typically created using + :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. + """ + + def __init__(self, app: Flask, **kwargs: t.Any) -> None: + self.app = app + super().__init__(**kwargs) + + def invoke( # type: ignore + self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any + ) -> t.Any: + """Invokes a CLI command in an isolated environment. See + :meth:`CliRunner.invoke ` for + full method documentation. See :ref:`testing-cli` for examples. + + If the ``obj`` argument is not given, passes an instance of + :class:`~flask.cli.ScriptInfo` that knows how to load the Flask + app being tested. + + :param cli: Command object to invoke. Default is the app's + :attr:`~flask.app.Flask.cli` group. + :param args: List of strings to invoke the command with. + + :return: a :class:`~click.testing.Result` object. + """ + if cli is None: + cli = self.app.cli + + if "obj" not in kwargs: + kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) + + return super().invoke(cli, args, **kwargs) diff --git a/test/fixtures/whole_applications/flask/src/flask/typing.py b/test/fixtures/whole_applications/flask/src/flask/typing.py new file mode 100644 index 0000000..cf6d4ae --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/typing.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import typing as t + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIApplication # noqa: F401 + from werkzeug.datastructures import Headers # noqa: F401 + from werkzeug.sansio.response import Response # noqa: F401 + +# The possible types that are directly convertible or are a Response object. +ResponseValue = t.Union[ + "Response", + str, + bytes, + t.List[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], +] + +# the possible types for an individual HTTP header +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. +HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + t.Tuple[ResponseValue, HeadersValue], + t.Tuple[ResponseValue, int], + t.Tuple[ResponseValue, int, HeadersValue], + "WSGIApplication", +] + +# Allow any subclass of werkzeug.Response, such as the one from Flask, +# as a callback argument. Using werkzeug.Response directly makes a +# callback annotated with flask.Response fail type checking. +ResponseClass = t.TypeVar("ResponseClass", bound="Response") + +AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] +TemplateContextProcessorCallable = t.Union[ + t.Callable[[], t.Dict[str, t.Any]], + t.Callable[[], t.Awaitable[t.Dict[str, t.Any]]], +] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, t.Dict[str, t.Any]], None] +URLValuePreprocessorCallable = t.Callable[ + [t.Optional[str], t.Optional[t.Dict[str, t.Any]]], None +] + +# This should take Exception, but that either breaks typing the argument +# with a specific exception, or decorating multiple times with different +# exceptions (and using a union type on the argument). +# https://github.com/pallets/flask/issues/4095 +# https://github.com/pallets/flask/issues/4295 +# https://github.com/pallets/flask/issues/4297 +ErrorHandlerCallable = t.Union[ + t.Callable[[t.Any], ResponseReturnValue], + t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], +] + +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/test/fixtures/whole_applications/flask/src/flask/views.py b/test/fixtures/whole_applications/flask/src/flask/views.py new file mode 100644 index 0000000..794fdc0 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/views.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import typing as t + +from . import typing as ft +from .globals import current_app +from .globals import request + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +http_method_funcs = frozenset( + ["get", "post", "head", "options", "delete", "put", "trace", "patch"] +) + + +class View: + """Subclass this class and override :meth:`dispatch_request` to + create a generic class-based view. Call :meth:`as_view` to create a + view function that creates an instance of the class with the given + arguments and calls its ``dispatch_request`` method with any URL + variables. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class Hello(View): + init_every_request = False + + def dispatch_request(self, name): + return f"Hello, {name}!" + + app.add_url_rule( + "/hello/", view_func=Hello.as_view("hello") + ) + + Set :attr:`methods` on the class to change what methods the view + accepts. + + Set :attr:`decorators` on the class to apply a list of decorators to + the generated view function. Decorators applied to the class itself + will not be applied to the generated view function! + + Set :attr:`init_every_request` to ``False`` for efficiency, unless + you need to store request-global data on ``self``. + """ + + #: The methods this view is registered for. Uses the same default + #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and + #: ``add_url_rule`` by default. + methods: t.ClassVar[t.Collection[str] | None] = None + + #: Control whether the ``OPTIONS`` method is handled automatically. + #: Uses the same default (``True``) as ``route`` and + #: ``add_url_rule`` by default. + provide_automatic_options: t.ClassVar[bool | None] = None + + #: A list of decorators to apply, in order, to the generated view + #: function. Remember that ``@decorator`` syntax is applied bottom + #: to top, so the first decorator in the list would be the bottom + #: decorator. + #: + #: .. versionadded:: 0.8 + decorators: t.ClassVar[list[t.Callable[[F], F]]] = [] + + #: Create a new instance of this view class for every request by + #: default. If a view subclass sets this to ``False``, the same + #: instance is used for every request. + #: + #: A single instance is more efficient, especially if complex setup + #: is done during init. However, storing data on ``self`` is no + #: longer safe across requests, and :data:`~flask.g` should be used + #: instead. + #: + #: .. versionadded:: 2.2 + init_every_request: t.ClassVar[bool] = True + + def dispatch_request(self) -> ft.ResponseReturnValue: + """The actual view function behavior. Subclasses must override + this and return a valid response. Any variables from the URL + rule are passed as keyword arguments. + """ + raise NotImplementedError() + + @classmethod + def as_view( + cls, name: str, *class_args: t.Any, **class_kwargs: t.Any + ) -> ft.RouteCallable: + """Convert the class into a view function that can be registered + for a route. + + By default, the generated view will create a new instance of the + view class for every request and call its + :meth:`dispatch_request` method. If the view class sets + :attr:`init_every_request` to ``False``, the same instance will + be used for every request. + + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. + + .. versionchanged:: 2.2 + Added the ``init_every_request`` class attribute. + """ + if cls.init_every_request: + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + self = view.view_class( # type: ignore[attr-defined] + *class_args, **class_kwargs + ) + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + else: + self = cls(*class_args, **class_kwargs) + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # We attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class-based + # view this thing came from, secondly it's also used for instantiating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls # type: ignore + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods # type: ignore + view.provide_automatic_options = cls.provide_automatic_options # type: ignore + return view + + +class MethodView(View): + """Dispatches request methods to the corresponding instance methods. + For example, if you implement a ``get`` method, it will be used to + handle ``GET`` requests. + + This can be useful for defining a REST API. + + :attr:`methods` is automatically set based on the methods defined on + the class. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class CounterAPI(MethodView): + def get(self): + return str(session.get("counter", 0)) + + def post(self): + session["counter"] = session.get("counter", 0) + 1 + return redirect(url_for("counter")) + + app.add_url_rule( + "/counter", view_func=CounterAPI.as_view("counter") + ) + """ + + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + if "methods" not in cls.__dict__: + methods = set() + + for base in cls.__bases__: + if getattr(base, "methods", None): + methods.update(base.methods) # type: ignore[attr-defined] + + for key in http_method_funcs: + if hasattr(cls, key): + methods.add(key.upper()) + + if methods: + cls.methods = methods + + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: + meth = getattr(self, request.method.lower(), None) + + # If the request method is HEAD and we don't have a handler for it + # retry with GET. + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + assert meth is not None, f"Unimplemented method {request.method!r}" + return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] diff --git a/test/fixtures/whole_applications/flask/src/flask/wrappers.py b/test/fixtures/whole_applications/flask/src/flask/wrappers.py new file mode 100644 index 0000000..c1eca80 --- /dev/null +++ b/test/fixtures/whole_applications/flask/src/flask/wrappers.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import typing as t + +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import HTTPException +from werkzeug.wrappers import Request as RequestBase +from werkzeug.wrappers import Response as ResponseBase + +from . import json +from .globals import current_app +from .helpers import _split_blueprint_path + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.routing import Rule + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + json_module: t.Any = json + + #: The internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: Though if the request's method was invalid for the URL rule, + #: the valid list is available in ``routing_exception.valid_methods`` + #: instead (an attribute of the Werkzeug exception + #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) + #: because the request was never internally bound. + #: + #: .. versionadded:: 0.6 + url_rule: Rule | None = None + + #: A dict of view arguments that matched the request. If an exception + #: happened when matching, this will be ``None``. + view_args: dict[str, t.Any] | None = None + + #: If matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception: HTTPException | None = None + + @property + def max_content_length(self) -> int | None: # type: ignore[override] + """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" + if current_app: + return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] + else: + return None + + @property + def endpoint(self) -> str | None: + """The endpoint that matched the request URL. + + This will be ``None`` if matching failed or has not been + performed yet. + + This in combination with :attr:`view_args` can be used to + reconstruct the same URL or a modified URL. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + + return None + + @property + def blueprint(self) -> str | None: + """The registered name of the current blueprint. + + This will be ``None`` if the endpoint is not part of a + blueprint, or if URL matching failed or has not been performed + yet. + + This does not necessarily match the name the blueprint was + created with. It may have been nested, or registered with a + different name. + """ + endpoint = self.endpoint + + if endpoint is not None and "." in endpoint: + return endpoint.rpartition(".")[0] + + return None + + @property + def blueprints(self) -> list[str]: + """The registered names of the current blueprint upwards through + parent blueprints. + + This will be an empty list if there is no current blueprint, or + if URL matching failed. + + .. versionadded:: 2.0.1 + """ + name = self.blueprint + + if name is None: + return [] + + return _split_blueprint_path(name) + + def _load_form_data(self) -> None: + super()._load_form_data() + + # In debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + if ( + current_app + and current_app.debug + and self.mimetype != "multipart/form-data" + and not self.files + ): + from .debughelpers import attach_enctype_error_multidict + + attach_enctype_error_multidict(self) + + def on_json_loading_failed(self, e: ValueError | None) -> t.Any: + try: + return super().on_json_loading_failed(e) + except BadRequest as e: + if current_app and current_app.debug: + raise + + raise BadRequest() from e + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + + .. versionchanged:: 1.0 + JSON support is added to the response, like the request. This is useful + when testing to get the test client response data as JSON. + + .. versionchanged:: 1.0 + + Added :attr:`max_cookie_size`. + """ + + default_mimetype: str | None = "text/html" + + json_module = json + + autocorrect_location_header = False + + @property + def max_cookie_size(self) -> int: # type: ignore + """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. + + See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in + Werkzeug's docs. + """ + if current_app: + return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] + + # return Werkzeug's default when not in an app context + return super().max_cookie_size diff --git a/test/fixtures/whole_applications/flask/tests/conftest.py b/test/fixtures/whole_applications/flask/tests/conftest.py new file mode 100644 index 0000000..58cf85d --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/conftest.py @@ -0,0 +1,160 @@ +import os +import pkgutil +import sys + +import pytest +from _pytest import monkeypatch + +from flask import Flask +from flask.globals import request_ctx + + +@pytest.fixture(scope="session", autouse=True) +def _standard_os_environ(): + """Set up ``os.environ`` at the start of the test session to have + standard values. Returns a list of operations that is used by + :func:`._reset_os_environ` after each test. + """ + mp = monkeypatch.MonkeyPatch() + out = ( + (os.environ, "FLASK_ENV_FILE", monkeypatch.notset), + (os.environ, "FLASK_APP", monkeypatch.notset), + (os.environ, "FLASK_DEBUG", monkeypatch.notset), + (os.environ, "FLASK_RUN_FROM_CLI", monkeypatch.notset), + (os.environ, "WERKZEUG_RUN_MAIN", monkeypatch.notset), + ) + + for _, key, value in out: + if value is monkeypatch.notset: + mp.delenv(key, False) + else: + mp.setenv(key, value) + + yield out + mp.undo() + + +@pytest.fixture(autouse=True) +def _reset_os_environ(monkeypatch, _standard_os_environ): + """Reset ``os.environ`` to the standard environ after each test, + in case a test changed something without cleaning up. + """ + monkeypatch._setitem.extend(_standard_os_environ) + + +@pytest.fixture +def app(): + app = Flask("flask_test", root_path=os.path.dirname(__file__)) + app.config.update( + TESTING=True, + SECRET_KEY="test key", + ) + return app + + +@pytest.fixture +def app_ctx(app): + with app.app_context() as ctx: + yield ctx + + +@pytest.fixture +def req_ctx(app): + with app.test_request_context() as ctx: + yield ctx + + +@pytest.fixture +def client(app): + return app.test_client() + + +@pytest.fixture +def test_apps(monkeypatch): + monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps")) + original_modules = set(sys.modules.keys()) + + yield + + # Remove any imports cached during the test. Otherwise "import app" + # will work in the next test even though it's no longer on the path. + for key in sys.modules.keys() - original_modules: + sys.modules.pop(key) + + +@pytest.fixture(autouse=True) +def leak_detector(): + yield + + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + leaks = [] + while request_ctx: + leaks.append(request_ctx._get_current_object()) + request_ctx.pop() + + assert leaks == [] + + +@pytest.fixture(params=(True, False)) +def limit_loader(request, monkeypatch): + """Patch pkgutil.get_loader to give loader without get_filename or archive. + + This provides for tests where a system has custom loaders, e.g. Google App + Engine's HardenedModulesHook, which have neither the `get_filename` method + nor the `archive` attribute. + + This fixture will run the testcase twice, once with and once without the + limitation/mock. + """ + if not request.param: + return + + class LimitedLoader: + def __init__(self, loader): + self.loader = loader + + def __getattr__(self, name): + if name in {"archive", "get_filename"}: + raise AttributeError(f"Mocking a loader which does not have {name!r}.") + return getattr(self.loader, name) + + old_get_loader = pkgutil.get_loader + + def get_loader(*args, **kwargs): + return LimitedLoader(old_get_loader(*args, **kwargs)) + + monkeypatch.setattr(pkgutil, "get_loader", get_loader) + + +@pytest.fixture +def modules_tmp_path(tmp_path, monkeypatch): + """A temporary directory added to sys.path.""" + rv = tmp_path / "modules_tmp" + rv.mkdir() + monkeypatch.syspath_prepend(os.fspath(rv)) + return rv + + +@pytest.fixture +def modules_tmp_path_prefix(modules_tmp_path, monkeypatch): + monkeypatch.setattr(sys, "prefix", os.fspath(modules_tmp_path)) + return modules_tmp_path + + +@pytest.fixture +def site_packages(modules_tmp_path, monkeypatch): + """Create a fake site-packages.""" + py_dir = f"python{sys.version_info.major}.{sys.version_info.minor}" + rv = modules_tmp_path / "lib" / py_dir / "site-packages" + rv.mkdir(parents=True) + monkeypatch.syspath_prepend(os.fspath(rv)) + return rv + + +@pytest.fixture +def purge_module(request): + def inner(name): + request.addfinalizer(lambda: sys.modules.pop(name, None)) + + return inner diff --git a/test/fixtures/whole_applications/flask/tests/static/config.json b/test/fixtures/whole_applications/flask/tests/static/config.json new file mode 100644 index 0000000..4eedab1 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/static/config.json @@ -0,0 +1,4 @@ +{ + "TEST_KEY": "foo", + "SECRET_KEY": "config" +} diff --git a/test/fixtures/whole_applications/flask/tests/static/config.toml b/test/fixtures/whole_applications/flask/tests/static/config.toml new file mode 100644 index 0000000..64acdbd --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/static/config.toml @@ -0,0 +1,2 @@ +TEST_KEY="foo" +SECRET_KEY="config" diff --git a/test/fixtures/whole_applications/flask/tests/static/index.html b/test/fixtures/whole_applications/flask/tests/static/index.html new file mode 100644 index 0000000..de8b69b --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/static/index.html @@ -0,0 +1 @@ +

Hello World!

diff --git a/test/fixtures/whole_applications/flask/tests/templates/_macro.html b/test/fixtures/whole_applications/flask/tests/templates/_macro.html new file mode 100644 index 0000000..3460ae2 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/_macro.html @@ -0,0 +1 @@ +{% macro hello(name) %}Hello {{ name }}!{% endmacro %} diff --git a/test/fixtures/whole_applications/flask/tests/templates/context_template.html b/test/fixtures/whole_applications/flask/tests/templates/context_template.html new file mode 100644 index 0000000..fadf3e5 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/context_template.html @@ -0,0 +1 @@ +

{{ value }}|{{ injected_value }} diff --git a/test/fixtures/whole_applications/flask/tests/templates/escaping_template.html b/test/fixtures/whole_applications/flask/tests/templates/escaping_template.html new file mode 100644 index 0000000..dc47644 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/escaping_template.html @@ -0,0 +1,6 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} diff --git a/test/fixtures/whole_applications/flask/tests/templates/mail.txt b/test/fixtures/whole_applications/flask/tests/templates/mail.txt new file mode 100644 index 0000000..d6cb92e --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/mail.txt @@ -0,0 +1 @@ +{{ foo}} Mail diff --git a/test/fixtures/whole_applications/flask/tests/templates/nested/nested.txt b/test/fixtures/whole_applications/flask/tests/templates/nested/nested.txt new file mode 100644 index 0000000..2c8634f --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/nested/nested.txt @@ -0,0 +1 @@ +I'm nested diff --git a/test/fixtures/whole_applications/flask/tests/templates/non_escaping_template.txt b/test/fixtures/whole_applications/flask/tests/templates/non_escaping_template.txt new file mode 100644 index 0000000..542864e --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/non_escaping_template.txt @@ -0,0 +1,8 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} +{{ text }} +{{ html }} diff --git a/test/fixtures/whole_applications/flask/tests/templates/simple_template.html b/test/fixtures/whole_applications/flask/tests/templates/simple_template.html new file mode 100644 index 0000000..c24612c --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/simple_template.html @@ -0,0 +1 @@ +

{{ whiskey }}

diff --git a/test/fixtures/whole_applications/flask/tests/templates/template_filter.html b/test/fixtures/whole_applications/flask/tests/templates/template_filter.html new file mode 100644 index 0000000..d51506a --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} diff --git a/test/fixtures/whole_applications/flask/tests/templates/template_test.html b/test/fixtures/whole_applications/flask/tests/templates/template_test.html new file mode 100644 index 0000000..92d5561 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/templates/template_test.html @@ -0,0 +1,3 @@ +{% if value is boolean %} + Success! +{% endif %} diff --git a/test/fixtures/whole_applications/flask/tests/test_appctx.py b/test/fixtures/whole_applications/flask/tests/test_appctx.py new file mode 100644 index 0000000..ca9e079 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_appctx.py @@ -0,0 +1,209 @@ +import pytest + +import flask +from flask.globals import app_ctx +from flask.globals import request_ctx + + +def test_basic_url_generation(app): + app.config["SERVER_NAME"] = "localhost" + app.config["PREFERRED_URL_SCHEME"] = "https" + + @app.route("/") + def index(): + pass + + with app.app_context(): + rv = flask.url_for("index") + assert rv == "https://localhost/" + + +def test_url_generation_requires_server_name(app): + with app.app_context(): + with pytest.raises(RuntimeError): + flask.url_for("index") + + +def test_url_generation_without_context_fails(): + with pytest.raises(RuntimeError): + flask.url_for("index") + + +def test_request_context_means_app_context(app): + with app.test_request_context(): + assert flask.current_app._get_current_object() is app + assert not flask.current_app + + +def test_app_context_provides_current_app(app): + with app.app_context(): + assert flask.current_app._get_current_object() is app + assert not flask.current_app + + +def test_app_tearing_down(app): + cleanup_stuff = [] + + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + with app.app_context(): + pass + + assert cleanup_stuff == [None] + + +def test_app_tearing_down_with_previous_exception(app): + cleanup_stuff = [] + + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + try: + raise Exception("dummy") + except Exception: + pass + + with app.app_context(): + pass + + assert cleanup_stuff == [None] + + +def test_app_tearing_down_with_handled_exception_by_except_block(app): + cleanup_stuff = [] + + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + with app.app_context(): + try: + raise Exception("dummy") + except Exception: + pass + + assert cleanup_stuff == [None] + + +def test_app_tearing_down_with_handled_exception_by_app_handler(app, client): + app.config["PROPAGATE_EXCEPTIONS"] = True + cleanup_stuff = [] + + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + @app.route("/") + def index(): + raise Exception("dummy") + + @app.errorhandler(Exception) + def handler(f): + return flask.jsonify(str(f)) + + with app.app_context(): + client.get("/") + + assert cleanup_stuff == [None] + + +def test_app_tearing_down_with_unhandled_exception(app, client): + app.config["PROPAGATE_EXCEPTIONS"] = True + cleanup_stuff = [] + + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + @app.route("/") + def index(): + raise ValueError("dummy") + + with pytest.raises(ValueError, match="dummy"): + with app.app_context(): + client.get("/") + + assert len(cleanup_stuff) == 1 + assert isinstance(cleanup_stuff[0], ValueError) + assert str(cleanup_stuff[0]) == "dummy" + + +def test_app_ctx_globals_methods(app, app_ctx): + # get + assert flask.g.get("foo") is None + assert flask.g.get("foo", "bar") == "bar" + # __contains__ + assert "foo" not in flask.g + flask.g.foo = "bar" + assert "foo" in flask.g + # setdefault + flask.g.setdefault("bar", "the cake is a lie") + flask.g.setdefault("bar", "hello world") + assert flask.g.bar == "the cake is a lie" + # pop + assert flask.g.pop("bar") == "the cake is a lie" + with pytest.raises(KeyError): + flask.g.pop("bar") + assert flask.g.pop("bar", "more cake") == "more cake" + # __iter__ + assert list(flask.g) == ["foo"] + # __repr__ + assert repr(flask.g) == "" + + +def test_custom_app_ctx_globals_class(app): + class CustomRequestGlobals: + def __init__(self): + self.spam = "eggs" + + app.app_ctx_globals_class = CustomRequestGlobals + with app.app_context(): + assert flask.render_template_string("{{ g.spam }}") == "eggs" + + +def test_context_refcounts(app, client): + called = [] + + @app.teardown_request + def teardown_req(error=None): + called.append("request") + + @app.teardown_appcontext + def teardown_app(error=None): + called.append("app") + + @app.route("/") + def index(): + with app_ctx: + with request_ctx: + pass + + assert flask.request.environ["werkzeug.request"] is not None + return "" + + res = client.get("/") + assert res.status_code == 200 + assert res.data == b"" + assert called == ["request", "app"] + + +def test_clean_pop(app): + app.testing = False + called = [] + + @app.teardown_request + def teardown_req(error=None): + raise ZeroDivisionError + + @app.teardown_appcontext + def teardown_app(error=None): + called.append("TEARDOWN") + + with app.app_context(): + called.append(flask.current_app.name) + + assert called == ["flask_test", "TEARDOWN"] + assert not flask.current_app diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/.flaskenv b/test/fixtures/whole_applications/flask/tests/test_apps/.flaskenv new file mode 100644 index 0000000..59f96af --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/.flaskenv @@ -0,0 +1,3 @@ +FOO=flaskenv +BAR=bar +EGGS=0 diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/__init__.py new file mode 100644 index 0000000..ad594cf --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/__init__.py @@ -0,0 +1,9 @@ +from flask import Flask + +app = Flask(__name__) +app.config["DEBUG"] = True +from blueprintapp.apps.admin import admin # noqa: E402 +from blueprintapp.apps.frontend import frontend # noqa: E402 + +app.register_blueprint(admin) +app.register_blueprint(frontend) diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/__init__.py new file mode 100644 index 0000000..b197fad --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/__init__.py @@ -0,0 +1,20 @@ +from flask import Blueprint +from flask import render_template + +admin = Blueprint( + "admin", + __name__, + url_prefix="/admin", + template_folder="templates", + static_folder="static", +) + + +@admin.route("/") +def index(): + return render_template("admin/index.html") + + +@admin.route("/index2") +def index2(): + return render_template("./admin/index.html") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/css/test.css b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/css/test.css new file mode 100644 index 0000000..b9f564d --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/test.txt b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/test.txt new file mode 100644 index 0000000..f220d22 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html new file mode 100644 index 0000000..eeec199 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/__init__.py new file mode 100644 index 0000000..7cc5cd8 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/__init__.py @@ -0,0 +1,14 @@ +from flask import Blueprint +from flask import render_template + +frontend = Blueprint("frontend", __name__, template_folder="templates") + + +@frontend.route("/") +def index(): + return render_template("frontend/index.html") + + +@frontend.route("/missing") +def missing_template(): + return render_template("missing_template.html") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html new file mode 100644 index 0000000..a062d71 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/app.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/app.py new file mode 100644 index 0000000..017ce28 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/app.py @@ -0,0 +1,3 @@ +from flask import Flask + +testapp = Flask("testapp") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/factory.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/factory.py new file mode 100644 index 0000000..1d27396 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/factory.py @@ -0,0 +1,13 @@ +from flask import Flask + + +def create_app(): + return Flask("app") + + +def create_app2(foo, bar): + return Flask("_".join(["app2", foo, bar])) + + +def no_app(): + pass diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/importerrorapp.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/importerrorapp.py new file mode 100644 index 0000000..2c96c9b --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/importerrorapp.py @@ -0,0 +1,5 @@ +from flask import Flask + +raise ImportError() + +testapp = Flask("testapp") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/__init__.py new file mode 100644 index 0000000..8330f6e --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/__init__.py @@ -0,0 +1,3 @@ +from flask import Flask + +application = Flask(__name__) diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/inner2/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/inner2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/inner2/flask.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/inner2/flask.py new file mode 100644 index 0000000..d7562aa --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/inner1/inner2/flask.py @@ -0,0 +1,3 @@ +from flask import Flask + +app = Flask(__name__) diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/message.txt b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/message.txt new file mode 100644 index 0000000..fc2b2cf --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/message.txt @@ -0,0 +1 @@ +So long, and thanks for all the fish. diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/multiapp.py b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/multiapp.py new file mode 100644 index 0000000..4ed0f32 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/cliapp/multiapp.py @@ -0,0 +1,4 @@ +from flask import Flask + +app1 = Flask("app1") +app2 = Flask("app2") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/hello.py b/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/hello.py new file mode 100644 index 0000000..71a2f90 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/hello.py @@ -0,0 +1,8 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello(): + return "Hello World!" diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/wsgi.py b/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/wsgi.py new file mode 100644 index 0000000..ab2d6e9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/helloworld/wsgi.py @@ -0,0 +1 @@ +from hello import app # noqa: F401 diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/__init__.py b/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/__init__.py new file mode 100644 index 0000000..b4ce4b1 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/__init__.py @@ -0,0 +1,3 @@ +from flask import Module + +mod = Module(__name__, "foo", subdomain="foo") diff --git a/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/static/hello.txt b/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/static/hello.txt new file mode 100644 index 0000000..12e23c1 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_apps/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain diff --git a/test/fixtures/whole_applications/flask/tests/test_async.py b/test/fixtures/whole_applications/flask/tests/test_async.py new file mode 100644 index 0000000..f52b049 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_async.py @@ -0,0 +1,145 @@ +import asyncio + +import pytest + +from flask import Blueprint +from flask import Flask +from flask import request +from flask.views import MethodView +from flask.views import View + +pytest.importorskip("asgiref") + + +class AppError(Exception): + pass + + +class BlueprintError(Exception): + pass + + +class AsyncView(View): + methods = ["GET", "POST"] + + async def dispatch_request(self): + await asyncio.sleep(0) + return request.method + + +class AsyncMethodView(MethodView): + async def get(self): + await asyncio.sleep(0) + return "GET" + + async def post(self): + await asyncio.sleep(0) + return "POST" + + +@pytest.fixture(name="async_app") +def _async_app(): + app = Flask(__name__) + + @app.route("/", methods=["GET", "POST"]) + @app.route("/home", methods=["GET", "POST"]) + async def index(): + await asyncio.sleep(0) + return request.method + + @app.errorhandler(AppError) + async def handle(_): + return "", 412 + + @app.route("/error") + async def error(): + raise AppError() + + blueprint = Blueprint("bp", __name__) + + @blueprint.route("/", methods=["GET", "POST"]) + async def bp_index(): + await asyncio.sleep(0) + return request.method + + @blueprint.errorhandler(BlueprintError) + async def bp_handle(_): + return "", 412 + + @blueprint.route("/error") + async def bp_error(): + raise BlueprintError() + + app.register_blueprint(blueprint, url_prefix="/bp") + + app.add_url_rule("/view", view_func=AsyncView.as_view("view")) + app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview")) + + return app + + +@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"]) +def test_async_route(path, async_app): + test_client = async_app.test_client() + response = test_client.get(path) + assert b"GET" in response.get_data() + response = test_client.post(path) + assert b"POST" in response.get_data() + + +@pytest.mark.parametrize("path", ["/error", "/bp/error"]) +def test_async_error_handler(path, async_app): + test_client = async_app.test_client() + response = test_client.get(path) + assert response.status_code == 412 + + +def test_async_before_after_request(): + app_before_called = False + app_after_called = False + bp_before_called = False + bp_after_called = False + + app = Flask(__name__) + + @app.route("/") + def index(): + return "" + + @app.before_request + async def before(): + nonlocal app_before_called + app_before_called = True + + @app.after_request + async def after(response): + nonlocal app_after_called + app_after_called = True + return response + + blueprint = Blueprint("bp", __name__) + + @blueprint.route("/") + def bp_index(): + return "" + + @blueprint.before_request + async def bp_before(): + nonlocal bp_before_called + bp_before_called = True + + @blueprint.after_request + async def bp_after(response): + nonlocal bp_after_called + bp_after_called = True + return response + + app.register_blueprint(blueprint, url_prefix="/bp") + + test_client = app.test_client() + test_client.get("/") + assert app_before_called + assert app_after_called + test_client.get("/bp/") + assert bp_before_called + assert bp_after_called diff --git a/test/fixtures/whole_applications/flask/tests/test_basic.py b/test/fixtures/whole_applications/flask/tests/test_basic.py new file mode 100644 index 0000000..214cfee --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_basic.py @@ -0,0 +1,1890 @@ +import gc +import re +import uuid +import warnings +import weakref +from datetime import datetime +from datetime import timezone +from platform import python_implementation + +import pytest +import werkzeug.serving +from markupsafe import Markup +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import Forbidden +from werkzeug.exceptions import NotFound +from werkzeug.http import parse_date +from werkzeug.routing import BuildError +from werkzeug.routing import RequestRedirect + +import flask + +require_cpython_gc = pytest.mark.skipif( + python_implementation() != "CPython", + reason="Requires CPython GC behavior", +) + + +def test_options_work(app, client): + @app.route("/", methods=["GET", "POST"]) + def index(): + return "Hello World" + + rv = client.open("/", method="OPTIONS") + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] + assert rv.data == b"" + + +def test_options_on_multiple_rules(app, client): + @app.route("/", methods=["GET", "POST"]) + def index(): + return "Hello World" + + @app.route("/", methods=["PUT"]) + def index_put(): + return "Aha!" + + rv = client.open("/", method="OPTIONS") + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] + + +@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) +def test_method_route(app, client, method): + method_route = getattr(app, method) + client_method = getattr(client, method) + + @method_route("/") + def hello(): + return "Hello" + + assert client_method("/").data == b"Hello" + + +def test_method_route_no_methods(app): + with pytest.raises(TypeError): + app.get("/", methods=["GET", "POST"]) + + +def test_provide_automatic_options_attr(): + app = flask.Flask(__name__) + + def index(): + return "Hello World!" + + index.provide_automatic_options = False + app.route("/")(index) + rv = app.test_client().open("/", method="OPTIONS") + assert rv.status_code == 405 + + app = flask.Flask(__name__) + + def index2(): + return "Hello World!" + + index2.provide_automatic_options = True + app.route("/", methods=["OPTIONS"])(index2) + rv = app.test_client().open("/", method="OPTIONS") + assert sorted(rv.allow) == ["OPTIONS"] + + +def test_provide_automatic_options_kwarg(app, client): + def index(): + return flask.request.method + + def more(): + return flask.request.method + + app.add_url_rule("/", view_func=index, provide_automatic_options=False) + app.add_url_rule( + "/more", + view_func=more, + methods=["GET", "POST"], + provide_automatic_options=False, + ) + assert client.get("/").data == b"GET" + + rv = client.post("/") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD"] + + rv = client.open("/", method="OPTIONS") + assert rv.status_code == 405 + + rv = client.head("/") + assert rv.status_code == 200 + assert not rv.data # head truncates + assert client.post("/more").data == b"POST" + assert client.get("/more").data == b"GET" + + rv = client.delete("/more") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD", "POST"] + + rv = client.open("/more", method="OPTIONS") + assert rv.status_code == 405 + + +def test_request_dispatching(app, client): + @app.route("/") + def index(): + return flask.request.method + + @app.route("/more", methods=["GET", "POST"]) + def more(): + return flask.request.method + + assert client.get("/").data == b"GET" + rv = client.post("/") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] + rv = client.head("/") + assert rv.status_code == 200 + assert not rv.data # head truncates + assert client.post("/more").data == b"POST" + assert client.get("/more").data == b"GET" + rv = client.delete("/more") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] + + +def test_disallow_string_for_allowed_methods(app): + with pytest.raises(TypeError): + app.add_url_rule("/", methods="GET POST", endpoint="test") + + +def test_url_mapping(app, client): + random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" + + def index(): + return flask.request.method + + def more(): + return flask.request.method + + def options(): + return random_uuid4 + + app.add_url_rule("/", "index", index) + app.add_url_rule("/more", "more", more, methods=["GET", "POST"]) + + # Issue 1288: Test that automatic options are not added + # when non-uppercase 'options' in methods + app.add_url_rule("/options", "options", options, methods=["options"]) + + assert client.get("/").data == b"GET" + rv = client.post("/") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] + rv = client.head("/") + assert rv.status_code == 200 + assert not rv.data # head truncates + assert client.post("/more").data == b"POST" + assert client.get("/more").data == b"GET" + rv = client.delete("/more") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] + rv = client.open("/options", method="OPTIONS") + assert rv.status_code == 200 + assert random_uuid4 in rv.data.decode("utf-8") + + +def test_werkzeug_routing(app, client): + from werkzeug.routing import Rule + from werkzeug.routing import Submount + + app.url_map.add( + Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) + ) + + def bar(): + return "bar" + + def index(): + return "index" + + app.view_functions["bar"] = bar + app.view_functions["index"] = index + + assert client.get("/foo/").data == b"index" + assert client.get("/foo/bar").data == b"bar" + + +def test_endpoint_decorator(app, client): + from werkzeug.routing import Rule + from werkzeug.routing import Submount + + app.url_map.add( + Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) + ) + + @app.endpoint("bar") + def bar(): + return "bar" + + @app.endpoint("index") + def index(): + return "index" + + assert client.get("/foo/").data == b"index" + assert client.get("/foo/bar").data == b"bar" + + +def test_session(app, client): + @app.route("/set", methods=["POST"]) + def set(): + assert not flask.session.accessed + assert not flask.session.modified + flask.session["value"] = flask.request.form["value"] + assert flask.session.accessed + assert flask.session.modified + return "value set" + + @app.route("/get") + def get(): + assert not flask.session.accessed + assert not flask.session.modified + v = flask.session.get("value", "None") + assert flask.session.accessed + assert not flask.session.modified + return v + + assert client.post("/set", data={"value": "42"}).data == b"value set" + assert client.get("/get").data == b"42" + + +def test_session_path(app, client): + app.config.update(APPLICATION_ROOT="/foo") + + @app.route("/") + def index(): + flask.session["testing"] = 42 + return "Hello World" + + rv = client.get("/", "http://example.com:8080/foo") + assert "path=/foo" in rv.headers["set-cookie"].lower() + + +def test_session_using_application_root(app, client): + class PrefixPathMiddleware: + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + environ["SCRIPT_NAME"] = self.prefix + return self.app(environ, start_response) + + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, "/bar") + app.config.update(APPLICATION_ROOT="/bar") + + @app.route("/") + def index(): + flask.session["testing"] = 42 + return "Hello World" + + rv = client.get("/", "http://example.com:8080/") + assert "path=/bar" in rv.headers["set-cookie"].lower() + + +def test_session_using_session_settings(app, client): + app.config.update( + SERVER_NAME="www.example.com:8080", + APPLICATION_ROOT="/test", + SESSION_COOKIE_DOMAIN=".example.com", + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_SAMESITE="Lax", + SESSION_COOKIE_PATH="/", + ) + + @app.route("/") + def index(): + flask.session["testing"] = 42 + return "Hello World" + + @app.route("/clear") + def clear(): + flask.session.pop("testing", None) + return "Goodbye World" + + rv = client.get("/", "http://www.example.com:8080/test/") + cookie = rv.headers["set-cookie"].lower() + # or condition for Werkzeug < 2.3 + assert "domain=example.com" in cookie or "domain=.example.com" in cookie + assert "path=/" in cookie + assert "secure" in cookie + assert "httponly" not in cookie + assert "samesite" in cookie + + rv = client.get("/clear", "http://www.example.com:8080/test/") + cookie = rv.headers["set-cookie"].lower() + assert "session=;" in cookie + # or condition for Werkzeug < 2.3 + assert "domain=example.com" in cookie or "domain=.example.com" in cookie + assert "path=/" in cookie + assert "secure" in cookie + assert "samesite" in cookie + + +def test_session_using_samesite_attribute(app, client): + @app.route("/") + def index(): + flask.session["testing"] = 42 + return "Hello World" + + app.config.update(SESSION_COOKIE_SAMESITE="invalid") + + with pytest.raises(ValueError): + client.get("/") + + app.config.update(SESSION_COOKIE_SAMESITE=None) + rv = client.get("/") + cookie = rv.headers["set-cookie"].lower() + assert "samesite" not in cookie + + app.config.update(SESSION_COOKIE_SAMESITE="Strict") + rv = client.get("/") + cookie = rv.headers["set-cookie"].lower() + assert "samesite=strict" in cookie + + app.config.update(SESSION_COOKIE_SAMESITE="Lax") + rv = client.get("/") + cookie = rv.headers["set-cookie"].lower() + assert "samesite=lax" in cookie + + +def test_missing_session(app): + app.secret_key = None + + def expect_exception(f, *args, **kwargs): + e = pytest.raises(RuntimeError, f, *args, **kwargs) + assert e.value.args and "session is unavailable" in e.value.args[0] + + with app.test_request_context(): + assert flask.session.get("missing_key") is None + expect_exception(flask.session.__setitem__, "foo", 42) + expect_exception(flask.session.pop, "foo") + + +def test_session_expiration(app, client): + permanent = True + + @app.route("/") + def index(): + flask.session["test"] = 42 + flask.session.permanent = permanent + return "" + + @app.route("/test") + def test(): + return str(flask.session.permanent) + + rv = client.get("/") + assert "set-cookie" in rv.headers + match = re.search(r"(?i)\bexpires=([^;]+)", rv.headers["set-cookie"]) + expires = parse_date(match.group()) + expected = datetime.now(timezone.utc) + app.permanent_session_lifetime + assert expires.year == expected.year + assert expires.month == expected.month + assert expires.day == expected.day + + rv = client.get("/test") + assert rv.data == b"True" + + permanent = False + rv = client.get("/") + assert "set-cookie" in rv.headers + match = re.search(r"\bexpires=([^;]+)", rv.headers["set-cookie"]) + assert match is None + + +def test_session_stored_last(app, client): + @app.after_request + def modify_session(response): + flask.session["foo"] = 42 + return response + + @app.route("/") + def dump_session_contents(): + return repr(flask.session.get("foo")) + + assert client.get("/").data == b"None" + assert client.get("/").data == b"42" + + +def test_session_special_types(app, client): + now = datetime.now(timezone.utc).replace(microsecond=0) + the_uuid = uuid.uuid4() + + @app.route("/") + def dump_session_contents(): + flask.session["t"] = (1, 2, 3) + flask.session["b"] = b"\xff" + flask.session["m"] = Markup("") + flask.session["u"] = the_uuid + flask.session["d"] = now + flask.session["t_tag"] = {" t": "not-a-tuple"} + flask.session["di_t_tag"] = {" t__": "not-a-tuple"} + flask.session["di_tag"] = {" di": "not-a-dict"} + return "", 204 + + with client: + client.get("/") + s = flask.session + assert s["t"] == (1, 2, 3) + assert type(s["b"]) is bytes # noqa: E721 + assert s["b"] == b"\xff" + assert type(s["m"]) is Markup # noqa: E721 + assert s["m"] == Markup("") + assert s["u"] == the_uuid + assert s["d"] == now + assert s["t_tag"] == {" t": "not-a-tuple"} + assert s["di_t_tag"] == {" t__": "not-a-tuple"} + assert s["di_tag"] == {" di": "not-a-dict"} + + +def test_session_cookie_setting(app): + is_permanent = True + + @app.route("/bump") + def bump(): + rv = flask.session["foo"] = flask.session.get("foo", 0) + 1 + flask.session.permanent = is_permanent + return str(rv) + + @app.route("/read") + def read(): + return str(flask.session.get("foo", 0)) + + def run_test(expect_header): + with app.test_client() as c: + assert c.get("/bump").data == b"1" + assert c.get("/bump").data == b"2" + assert c.get("/bump").data == b"3" + + rv = c.get("/read") + set_cookie = rv.headers.get("set-cookie") + assert (set_cookie is not None) == expect_header + assert rv.data == b"3" + + is_permanent = True + app.config["SESSION_REFRESH_EACH_REQUEST"] = True + run_test(expect_header=True) + + is_permanent = True + app.config["SESSION_REFRESH_EACH_REQUEST"] = False + run_test(expect_header=False) + + is_permanent = False + app.config["SESSION_REFRESH_EACH_REQUEST"] = True + run_test(expect_header=False) + + is_permanent = False + app.config["SESSION_REFRESH_EACH_REQUEST"] = False + run_test(expect_header=False) + + +def test_session_vary_cookie(app, client): + @app.route("/set") + def set_session(): + flask.session["test"] = "test" + return "" + + @app.route("/get") + def get(): + return flask.session.get("test") + + @app.route("/getitem") + def getitem(): + return flask.session["test"] + + @app.route("/setdefault") + def setdefault(): + return flask.session.setdefault("test", "default") + + @app.route("/clear") + def clear(): + flask.session.clear() + return "" + + @app.route("/vary-cookie-header-set") + def vary_cookie_header_set(): + response = flask.Response() + response.vary.add("Cookie") + flask.session["test"] = "test" + return response + + @app.route("/vary-header-set") + def vary_header_set(): + response = flask.Response() + response.vary.update(("Accept-Encoding", "Accept-Language")) + flask.session["test"] = "test" + return response + + @app.route("/no-vary-header") + def no_vary_header(): + return "" + + def expect(path, header_value="Cookie"): + rv = client.get(path) + + if header_value: + # The 'Vary' key should exist in the headers only once. + assert len(rv.headers.get_all("Vary")) == 1 + assert rv.headers["Vary"] == header_value + else: + assert "Vary" not in rv.headers + + expect("/set") + expect("/get") + expect("/getitem") + expect("/setdefault") + expect("/clear") + expect("/vary-cookie-header-set") + expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie") + expect("/no-vary-header", None) + + +def test_session_refresh_vary(app, client): + @app.get("/login") + def login(): + flask.session["user_id"] = 1 + flask.session.permanent = True + return "" + + @app.get("/ignored") + def ignored(): + return "" + + rv = client.get("/login") + assert rv.headers["Vary"] == "Cookie" + rv = client.get("/ignored") + assert rv.headers["Vary"] == "Cookie" + + +def test_flashes(app, req_ctx): + assert not flask.session.modified + flask.flash("Zap") + flask.session.modified = False + flask.flash("Zip") + assert flask.session.modified + assert list(flask.get_flashed_messages()) == ["Zap", "Zip"] + + +def test_extended_flashing(app): + # Be sure app.testing=True below, else tests can fail silently. + # + # Specifically, if app.testing is not set to True, the AssertionErrors + # in the view functions will cause a 500 response to the test client + # instead of propagating exceptions. + + @app.route("/") + def index(): + flask.flash("Hello World") + flask.flash("Hello World", "error") + flask.flash(Markup("Testing"), "warning") + return "" + + @app.route("/test/") + def test(): + messages = flask.get_flashed_messages() + assert list(messages) == [ + "Hello World", + "Hello World", + Markup("Testing"), + ] + return "" + + @app.route("/test_with_categories/") + def test_with_categories(): + messages = flask.get_flashed_messages(with_categories=True) + assert len(messages) == 3 + assert list(messages) == [ + ("message", "Hello World"), + ("error", "Hello World"), + ("warning", Markup("Testing")), + ] + return "" + + @app.route("/test_filter/") + def test_filter(): + messages = flask.get_flashed_messages( + category_filter=["message"], with_categories=True + ) + assert list(messages) == [("message", "Hello World")] + return "" + + @app.route("/test_filters/") + def test_filters(): + messages = flask.get_flashed_messages( + category_filter=["message", "warning"], with_categories=True + ) + assert list(messages) == [ + ("message", "Hello World"), + ("warning", Markup("Testing")), + ] + return "" + + @app.route("/test_filters_without_returning_categories/") + def test_filters2(): + messages = flask.get_flashed_messages(category_filter=["message", "warning"]) + assert len(messages) == 2 + assert messages[0] == "Hello World" + assert messages[1] == Markup("Testing") + return "" + + # Create new test client on each test to clean flashed messages. + + client = app.test_client() + client.get("/") + client.get("/test_with_categories/") + + client = app.test_client() + client.get("/") + client.get("/test_filter/") + + client = app.test_client() + client.get("/") + client.get("/test_filters/") + + client = app.test_client() + client.get("/") + client.get("/test_filters_without_returning_categories/") + + +def test_request_processing(app, client): + evts = [] + + @app.before_request + def before_request(): + evts.append("before") + + @app.after_request + def after_request(response): + response.data += b"|after" + evts.append("after") + return response + + @app.route("/") + def index(): + assert "before" in evts + assert "after" not in evts + return "request" + + assert "after" not in evts + rv = client.get("/").data + assert "after" in evts + assert rv == b"request|after" + + +def test_request_preprocessing_early_return(app, client): + evts = [] + + @app.before_request + def before_request1(): + evts.append(1) + + @app.before_request + def before_request2(): + evts.append(2) + return "hello" + + @app.before_request + def before_request3(): + evts.append(3) + return "bye" + + @app.route("/") + def index(): + evts.append("index") + return "damnit" + + rv = client.get("/").data.strip() + assert rv == b"hello" + assert evts == [1, 2] + + +def test_after_request_processing(app, client): + @app.route("/") + def index(): + @flask.after_this_request + def foo(response): + response.headers["X-Foo"] = "a header" + return response + + return "Test" + + resp = client.get("/") + assert resp.status_code == 200 + assert resp.headers["X-Foo"] == "a header" + + +def test_teardown_request_handler(app, client): + called = [] + + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + + @app.route("/") + def root(): + return "Response" + + rv = client.get("/") + assert rv.status_code == 200 + assert b"Response" in rv.data + assert len(called) == 1 + + +def test_teardown_request_handler_debug_mode(app, client): + called = [] + + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + + @app.route("/") + def root(): + return "Response" + + rv = client.get("/") + assert rv.status_code == 200 + assert b"Response" in rv.data + assert len(called) == 1 + + +def test_teardown_request_handler_error(app, client): + called = [] + app.testing = False + + @app.teardown_request + def teardown_request1(exc): + assert type(exc) is ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError() + except Exception: + pass + + @app.teardown_request + def teardown_request2(exc): + assert type(exc) is ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError() + except Exception: + pass + + @app.route("/") + def fails(): + raise ZeroDivisionError + + rv = client.get("/") + assert rv.status_code == 500 + assert b"Internal Server Error" in rv.data + assert len(called) == 2 + + +def test_before_after_request_order(app, client): + called = [] + + @app.before_request + def before1(): + called.append(1) + + @app.before_request + def before2(): + called.append(2) + + @app.after_request + def after1(response): + called.append(4) + return response + + @app.after_request + def after2(response): + called.append(3) + return response + + @app.teardown_request + def finish1(exc): + called.append(6) + + @app.teardown_request + def finish2(exc): + called.append(5) + + @app.route("/") + def index(): + return "42" + + rv = client.get("/") + assert rv.data == b"42" + assert called == [1, 2, 3, 4, 5, 6] + + +def test_error_handling(app, client): + app.testing = False + + @app.errorhandler(404) + def not_found(e): + return "not found", 404 + + @app.errorhandler(500) + def internal_server_error(e): + return "internal server error", 500 + + @app.errorhandler(Forbidden) + def forbidden(e): + return "forbidden", 403 + + @app.route("/") + def index(): + flask.abort(404) + + @app.route("/error") + def error(): + raise ZeroDivisionError + + @app.route("/forbidden") + def error2(): + flask.abort(403) + + rv = client.get("/") + assert rv.status_code == 404 + assert rv.data == b"not found" + rv = client.get("/error") + assert rv.status_code == 500 + assert b"internal server error" == rv.data + rv = client.get("/forbidden") + assert rv.status_code == 403 + assert b"forbidden" == rv.data + + +def test_error_handling_processing(app, client): + app.testing = False + + @app.errorhandler(500) + def internal_server_error(e): + return "internal server error", 500 + + @app.route("/") + def broken_func(): + raise ZeroDivisionError + + @app.after_request + def after_request(resp): + resp.mimetype = "text/x-special" + return resp + + resp = client.get("/") + assert resp.mimetype == "text/x-special" + assert resp.data == b"internal server error" + + +def test_baseexception_error_handling(app, client): + app.testing = False + + @app.route("/") + def broken_func(): + raise KeyboardInterrupt() + + with pytest.raises(KeyboardInterrupt): + client.get("/") + + +def test_before_request_and_routing_errors(app, client): + @app.before_request + def attach_something(): + flask.g.something = "value" + + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + + rv = client.get("/") + assert rv.status_code == 404 + assert rv.data == b"value" + + +def test_user_error_handling(app, client): + class MyException(Exception): + pass + + @app.errorhandler(MyException) + def handle_my_exception(e): + assert isinstance(e, MyException) + return "42" + + @app.route("/") + def index(): + raise MyException() + + assert client.get("/").data == b"42" + + +def test_http_error_subclass_handling(app, client): + class ForbiddenSubclass(Forbidden): + pass + + @app.errorhandler(ForbiddenSubclass) + def handle_forbidden_subclass(e): + assert isinstance(e, ForbiddenSubclass) + return "banana" + + @app.errorhandler(403) + def handle_403(e): + assert not isinstance(e, ForbiddenSubclass) + assert isinstance(e, Forbidden) + return "apple" + + @app.route("/1") + def index1(): + raise ForbiddenSubclass() + + @app.route("/2") + def index2(): + flask.abort(403) + + @app.route("/3") + def index3(): + raise Forbidden() + + assert client.get("/1").data == b"banana" + assert client.get("/2").data == b"apple" + assert client.get("/3").data == b"apple" + + +def test_errorhandler_precedence(app, client): + class E1(Exception): + pass + + class E2(Exception): + pass + + class E3(E1, E2): + pass + + @app.errorhandler(E2) + def handle_e2(e): + return "E2" + + @app.errorhandler(Exception) + def handle_exception(e): + return "Exception" + + @app.route("/E1") + def raise_e1(): + raise E1 + + @app.route("/E3") + def raise_e3(): + raise E3 + + rv = client.get("/E1") + assert rv.data == b"Exception" + + rv = client.get("/E3") + assert rv.data == b"E2" + + +@pytest.mark.parametrize( + ("debug", "trap", "expect_key", "expect_abort"), + [(False, None, True, True), (True, None, False, True), (False, True, False, False)], +) +def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort): + app.config["DEBUG"] = debug + app.config["TRAP_BAD_REQUEST_ERRORS"] = trap + + @app.route("/key") + def fail(): + flask.request.form["missing_key"] + + @app.route("/abort") + def allow_abort(): + flask.abort(400) + + if expect_key: + rv = client.get("/key") + assert rv.status_code == 400 + assert b"missing_key" not in rv.data + else: + with pytest.raises(KeyError) as exc_info: + client.get("/key") + + assert exc_info.errisinstance(BadRequest) + assert "missing_key" in exc_info.value.get_description() + + if expect_abort: + rv = client.get("/abort") + assert rv.status_code == 400 + else: + with pytest.raises(BadRequest): + client.get("/abort") + + +def test_trapping_of_all_http_exceptions(app, client): + app.config["TRAP_HTTP_EXCEPTIONS"] = True + + @app.route("/fail") + def fail(): + flask.abort(404) + + with pytest.raises(NotFound): + client.get("/fail") + + +def test_error_handler_after_processor_error(app, client): + app.testing = False + + @app.before_request + def before_request(): + if _trigger == "before": + raise ZeroDivisionError + + @app.after_request + def after_request(response): + if _trigger == "after": + raise ZeroDivisionError + + return response + + @app.route("/") + def index(): + return "Foo" + + @app.errorhandler(500) + def internal_server_error(e): + return "Hello Server Error", 500 + + for _trigger in "before", "after": + rv = client.get("/") + assert rv.status_code == 500 + assert rv.data == b"Hello Server Error" + + +def test_enctype_debug_helper(app, client): + from flask.debughelpers import DebugFilesKeyError + + app.debug = True + + @app.route("/fail", methods=["POST"]) + def index(): + return flask.request.files["foo"].filename + + with pytest.raises(DebugFilesKeyError) as e: + client.post("/fail", data={"foo": "index.txt"}) + assert "no file contents were transmitted" in str(e.value) + assert "This was submitted: 'index.txt'" in str(e.value) + + +def test_response_types(app, client): + @app.route("/text") + def from_text(): + return "Hällo Wörld" + + @app.route("/bytes") + def from_bytes(): + return "Hällo Wörld".encode() + + @app.route("/full_tuple") + def from_full_tuple(): + return ( + "Meh", + 400, + {"X-Foo": "Testing", "Content-Type": "text/plain; charset=utf-8"}, + ) + + @app.route("/text_headers") + def from_text_headers(): + return "Hello", {"X-Foo": "Test", "Content-Type": "text/plain; charset=utf-8"} + + @app.route("/text_status") + def from_text_status(): + return "Hi, status!", 400 + + @app.route("/response_headers") + def from_response_headers(): + return ( + flask.Response( + "Hello world", 404, {"Content-Type": "text/html", "X-Foo": "Baz"} + ), + {"Content-Type": "text/plain", "X-Foo": "Bar", "X-Bar": "Foo"}, + ) + + @app.route("/response_status") + def from_response_status(): + return app.response_class("Hello world", 400), 500 + + @app.route("/wsgi") + def from_wsgi(): + return NotFound() + + @app.route("/dict") + def from_dict(): + return {"foo": "bar"}, 201 + + @app.route("/list") + def from_list(): + return ["foo", "bar"], 201 + + assert client.get("/text").data == "Hällo Wörld".encode() + assert client.get("/bytes").data == "Hällo Wörld".encode() + + rv = client.get("/full_tuple") + assert rv.data == b"Meh" + assert rv.headers["X-Foo"] == "Testing" + assert rv.status_code == 400 + assert rv.mimetype == "text/plain" + + rv = client.get("/text_headers") + assert rv.data == b"Hello" + assert rv.headers["X-Foo"] == "Test" + assert rv.status_code == 200 + assert rv.mimetype == "text/plain" + + rv = client.get("/text_status") + assert rv.data == b"Hi, status!" + assert rv.status_code == 400 + assert rv.mimetype == "text/html" + + rv = client.get("/response_headers") + assert rv.data == b"Hello world" + assert rv.content_type == "text/plain" + assert rv.headers.getlist("X-Foo") == ["Bar"] + assert rv.headers["X-Bar"] == "Foo" + assert rv.status_code == 404 + + rv = client.get("/response_status") + assert rv.data == b"Hello world" + assert rv.status_code == 500 + + rv = client.get("/wsgi") + assert b"Not Found" in rv.data + assert rv.status_code == 404 + + rv = client.get("/dict") + assert rv.json == {"foo": "bar"} + assert rv.status_code == 201 + + rv = client.get("/list") + assert rv.json == ["foo", "bar"] + assert rv.status_code == 201 + + +def test_response_type_errors(): + app = flask.Flask(__name__) + app.testing = True + + @app.route("/none") + def from_none(): + pass + + @app.route("/small_tuple") + def from_small_tuple(): + return ("Hello",) + + @app.route("/large_tuple") + def from_large_tuple(): + return "Hello", 234, {"X-Foo": "Bar"}, "???" + + @app.route("/bad_type") + def from_bad_type(): + return True + + @app.route("/bad_wsgi") + def from_bad_wsgi(): + return lambda: None + + c = app.test_client() + + with pytest.raises(TypeError) as e: + c.get("/none") + + assert "returned None" in str(e.value) + assert "from_none" in str(e.value) + + with pytest.raises(TypeError) as e: + c.get("/small_tuple") + + assert "tuple must have the form" in str(e.value) + + with pytest.raises(TypeError): + c.get("/large_tuple") + + with pytest.raises(TypeError) as e: + c.get("/bad_type") + + assert "it was a bool" in str(e.value) + + with pytest.raises(TypeError): + c.get("/bad_wsgi") + + +def test_make_response(app, req_ctx): + rv = flask.make_response() + assert rv.status_code == 200 + assert rv.data == b"" + assert rv.mimetype == "text/html" + + rv = flask.make_response("Awesome") + assert rv.status_code == 200 + assert rv.data == b"Awesome" + assert rv.mimetype == "text/html" + + rv = flask.make_response("W00t", 404) + assert rv.status_code == 404 + assert rv.data == b"W00t" + assert rv.mimetype == "text/html" + + rv = flask.make_response(c for c in "Hello") + assert rv.status_code == 200 + assert rv.data == b"Hello" + assert rv.mimetype == "text/html" + + +def test_make_response_with_response_instance(app, req_ctx): + rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) + assert rv.status_code == 400 + assert rv.data == b'{"msg":"W00t"}\n' + assert rv.mimetype == "application/json" + + rv = flask.make_response(flask.Response(""), 400) + assert rv.status_code == 400 + assert rv.data == b"" + assert rv.mimetype == "text/html" + + rv = flask.make_response( + flask.Response("", headers={"Content-Type": "text/html"}), + 400, + [("X-Foo", "bar")], + ) + assert rv.status_code == 400 + assert rv.headers["Content-Type"] == "text/html" + assert rv.headers["X-Foo"] == "bar" + + +@pytest.mark.parametrize("compact", [True, False]) +def test_jsonify_no_prettyprint(app, compact): + app.json.compact = compact + rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"}) + data = rv.data.strip() + assert (b" " not in data) is compact + assert (b"\n" not in data) is compact + + +def test_jsonify_mimetype(app, req_ctx): + app.json.mimetype = "application/vnd.api+json" + msg = {"msg": {"submsg": "W00t"}} + rv = flask.make_response(flask.jsonify(msg), 200) + assert rv.mimetype == "application/vnd.api+json" + + +def test_json_dump_dataclass(app, req_ctx): + from dataclasses import make_dataclass + + Data = make_dataclass("Data", [("name", str)]) + value = app.json.dumps(Data("Flask")) + value = app.json.loads(value) + assert value == {"name": "Flask"} + + +def test_jsonify_args_and_kwargs_check(app, req_ctx): + with pytest.raises(TypeError) as e: + flask.jsonify("fake args", kwargs="fake") + assert "args or kwargs" in str(e.value) + + +def test_url_generation(app, req_ctx): + @app.route("/hello/", methods=["POST"]) + def hello(): + pass + + assert flask.url_for("hello", name="test x") == "/hello/test%20x" + assert ( + flask.url_for("hello", name="test x", _external=True) + == "http://localhost/hello/test%20x" + ) + + +def test_build_error_handler(app): + # Test base case, a URL which results in a BuildError. + with app.test_request_context(): + pytest.raises(BuildError, flask.url_for, "spam") + + # Verify the error is re-raised if not the current exception. + try: + with app.test_request_context(): + flask.url_for("spam") + except BuildError as err: + error = err + try: + raise RuntimeError("Test case where BuildError is not current.") + except RuntimeError: + pytest.raises(BuildError, app.handle_url_build_error, error, "spam", {}) + + # Test a custom handler. + def handler(error, endpoint, values): + # Just a test. + return "/test_handler/" + + app.url_build_error_handlers.append(handler) + with app.test_request_context(): + assert flask.url_for("spam") == "/test_handler/" + + +def test_build_error_handler_reraise(app): + # Test a custom handler which reraises the BuildError + def handler_raises_build_error(error, endpoint, values): + raise error + + app.url_build_error_handlers.append(handler_raises_build_error) + + with app.test_request_context(): + pytest.raises(BuildError, flask.url_for, "not.existing") + + +def test_url_for_passes_special_values_to_build_error_handler(app): + @app.url_build_error_handlers.append + def handler(error, endpoint, values): + assert values == { + "_external": False, + "_anchor": None, + "_method": None, + "_scheme": None, + } + return "handled" + + with app.test_request_context(): + flask.url_for("/") + + +def test_static_files(app, client): + rv = client.get("/static/index.html") + assert rv.status_code == 200 + assert rv.data.strip() == b"

Hello World!

" + with app.test_request_context(): + assert flask.url_for("static", filename="index.html") == "/static/index.html" + rv.close() + + +def test_static_url_path(): + app = flask.Flask(__name__, static_url_path="/foo") + app.testing = True + rv = app.test_client().get("/foo/index.html") + assert rv.status_code == 200 + rv.close() + + with app.test_request_context(): + assert flask.url_for("static", filename="index.html") == "/foo/index.html" + + +def test_static_url_path_with_ending_slash(): + app = flask.Flask(__name__, static_url_path="/foo/") + app.testing = True + rv = app.test_client().get("/foo/index.html") + assert rv.status_code == 200 + rv.close() + + with app.test_request_context(): + assert flask.url_for("static", filename="index.html") == "/foo/index.html" + + +def test_static_url_empty_path(app): + app = flask.Flask(__name__, static_folder="", static_url_path="") + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +def test_static_url_empty_path_default(app): + app = flask.Flask(__name__, static_folder="") + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +def test_static_folder_with_pathlib_path(app): + from pathlib import Path + + app = flask.Flask(__name__, static_folder=Path("static")) + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +def test_static_folder_with_ending_slash(): + app = flask.Flask(__name__, static_folder="static/") + + @app.route("/") + def catch_all(path): + return path + + rv = app.test_client().get("/catch/all") + assert rv.data == b"catch/all" + + +def test_static_route_with_host_matching(): + app = flask.Flask(__name__, host_matching=True, static_host="example.com") + c = app.test_client() + rv = c.get("http://example.com/static/index.html") + assert rv.status_code == 200 + rv.close() + with app.test_request_context(): + rv = flask.url_for("static", filename="index.html", _external=True) + assert rv == "http://example.com/static/index.html" + # Providing static_host without host_matching=True should error. + with pytest.raises(AssertionError): + flask.Flask(__name__, static_host="example.com") + # Providing host_matching=True with static_folder + # but without static_host should error. + with pytest.raises(AssertionError): + flask.Flask(__name__, host_matching=True) + # Providing host_matching=True without static_host + # but with static_folder=None should not error. + flask.Flask(__name__, host_matching=True, static_folder=None) + + +def test_request_locals(): + assert repr(flask.g) == "" + assert not flask.g + + +def test_server_name_subdomain(): + app = flask.Flask(__name__, subdomain_matching=True) + client = app.test_client() + + @app.route("/") + def index(): + return "default" + + @app.route("/", subdomain="foo") + def subdomain(): + return "subdomain" + + app.config["SERVER_NAME"] = "dev.local:5000" + rv = client.get("/") + assert rv.data == b"default" + + rv = client.get("/", "http://dev.local:5000") + assert rv.data == b"default" + + rv = client.get("/", "https://dev.local:5000") + assert rv.data == b"default" + + app.config["SERVER_NAME"] = "dev.local:443" + rv = client.get("/", "https://dev.local") + + # Werkzeug 1.0 fixes matching https scheme with 443 port + if rv.status_code != 404: + assert rv.data == b"default" + + app.config["SERVER_NAME"] = "dev.local" + rv = client.get("/", "https://dev.local") + assert rv.data == b"default" + + # suppress Werkzeug 0.15 warning about name mismatch + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Current server name", UserWarning, "flask.app" + ) + rv = client.get("/", "http://foo.localhost") + assert rv.status_code == 404 + + rv = client.get("/", "http://foo.dev.local") + assert rv.data == b"subdomain" + + +@pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) +def test_exception_propagation(app, client, key): + app.testing = False + + @app.route("/") + def index(): + raise ZeroDivisionError + + if key is not None: + app.config[key] = True + + with pytest.raises(ZeroDivisionError): + client.get("/") + else: + assert client.get("/").status_code == 500 + + +@pytest.mark.parametrize("debug", [True, False]) +@pytest.mark.parametrize("use_debugger", [True, False]) +@pytest.mark.parametrize("use_reloader", [True, False]) +@pytest.mark.parametrize("propagate_exceptions", [None, True, False]) +def test_werkzeug_passthrough_errors( + monkeypatch, debug, use_debugger, use_reloader, propagate_exceptions, app +): + rv = {} + + # Mocks werkzeug.serving.run_simple method + def run_simple_mock(*args, **kwargs): + rv["passthrough_errors"] = kwargs.get("passthrough_errors") + + monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) + app.config["PROPAGATE_EXCEPTIONS"] = propagate_exceptions + app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader) + + +def test_max_content_length(app, client): + app.config["MAX_CONTENT_LENGTH"] = 64 + + @app.before_request + def always_first(): + flask.request.form["myfile"] + AssertionError() + + @app.route("/accept", methods=["POST"]) + def accept_file(): + flask.request.form["myfile"] + AssertionError() + + @app.errorhandler(413) + def catcher(error): + return "42" + + rv = client.post("/accept", data={"myfile": "foo" * 100}) + assert rv.data == b"42" + + +def test_url_processors(app, client): + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and app.url_map.is_endpoint_expecting( + endpoint, "lang_code" + ): + values.setdefault("lang_code", flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop("lang_code", None) + + @app.route("//") + def index(): + return flask.url_for("about") + + @app.route("//about") + def about(): + return flask.url_for("something_else") + + @app.route("/foo") + def something_else(): + return flask.url_for("about", lang_code="en") + + assert client.get("/de/").data == b"/de/about" + assert client.get("/de/about").data == b"/foo" + assert client.get("/foo").data == b"/en/about" + + +def test_inject_blueprint_url_defaults(app): + bp = flask.Blueprint("foo", __name__, template_folder="template") + + @bp.url_defaults + def bp_defaults(endpoint, values): + values["page"] = "login" + + @bp.route("/") + def view(page): + pass + + app.register_blueprint(bp) + + values = dict() + app.inject_url_defaults("foo.view", values) + expected = dict(page="login") + assert values == expected + + with app.test_request_context("/somepage"): + url = flask.url_for("foo.view") + expected = "/login" + assert url == expected + + +def test_nonascii_pathinfo(app, client): + @app.route("/киртест") + def index(): + return "Hello World!" + + rv = client.get("/киртест") + assert rv.data == b"Hello World!" + + +def test_no_setup_after_first_request(app, client): + app.debug = True + + @app.route("/") + def index(): + return "Awesome" + + assert client.get("/").data == b"Awesome" + + with pytest.raises(AssertionError) as exc_info: + app.add_url_rule("/foo", endpoint="late") + + assert "setup method 'add_url_rule'" in str(exc_info.value) + + +def test_routing_redirect_debugging(monkeypatch, app, client): + app.config["DEBUG"] = True + + @app.route("/user/", methods=["GET", "POST"]) + def user(): + return flask.request.form["status"] + + # default redirect code preserves form data + rv = client.post("/user", data={"status": "success"}, follow_redirects=True) + assert rv.data == b"success" + + # 301 and 302 raise error + monkeypatch.setattr(RequestRedirect, "code", 301) + + with client, pytest.raises(AssertionError) as exc_info: + client.post("/user", data={"status": "error"}, follow_redirects=True) + + assert "canonical URL 'http://localhost/user/'" in str(exc_info.value) + + +def test_route_decorator_custom_endpoint(app, client): + app.debug = True + + @app.route("/foo/") + def foo(): + return flask.request.endpoint + + @app.route("/bar/", endpoint="bar") + def for_bar(): + return flask.request.endpoint + + @app.route("/bar/123", endpoint="123") + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for("foo") == "/foo/" + assert flask.url_for("bar") == "/bar/" + assert flask.url_for("123") == "/bar/123" + + assert client.get("/foo/").data == b"foo" + assert client.get("/bar/").data == b"bar" + assert client.get("/bar/123").data == b"123" + + +def test_get_method_on_g(app_ctx): + assert flask.g.get("x") is None + assert flask.g.get("x", 11) == 11 + flask.g.x = 42 + assert flask.g.get("x") == 42 + assert flask.g.x == 42 + + +def test_g_iteration_protocol(app_ctx): + flask.g.foo = 23 + flask.g.bar = 42 + assert "foo" in flask.g + assert "foos" not in flask.g + assert sorted(flask.g) == ["bar", "foo"] + + +def test_subdomain_basic_support(): + app = flask.Flask(__name__, subdomain_matching=True) + app.config["SERVER_NAME"] = "localhost.localdomain" + client = app.test_client() + + @app.route("/") + def normal_index(): + return "normal index" + + @app.route("/", subdomain="test") + def test_index(): + return "test index" + + rv = client.get("/", "http://localhost.localdomain/") + assert rv.data == b"normal index" + + rv = client.get("/", "http://test.localhost.localdomain/") + assert rv.data == b"test index" + + +def test_subdomain_matching(): + app = flask.Flask(__name__, subdomain_matching=True) + client = app.test_client() + app.config["SERVER_NAME"] = "localhost.localdomain" + + @app.route("/", subdomain="") + def index(user): + return f"index for {user}" + + rv = client.get("/", "http://mitsuhiko.localhost.localdomain/") + assert rv.data == b"index for mitsuhiko" + + +def test_subdomain_matching_with_ports(): + app = flask.Flask(__name__, subdomain_matching=True) + app.config["SERVER_NAME"] = "localhost.localdomain:3000" + client = app.test_client() + + @app.route("/", subdomain="") + def index(user): + return f"index for {user}" + + rv = client.get("/", "http://mitsuhiko.localhost.localdomain:3000/") + assert rv.data == b"index for mitsuhiko" + + +@pytest.mark.parametrize("matching", (False, True)) +def test_subdomain_matching_other_name(matching): + app = flask.Flask(__name__, subdomain_matching=matching) + app.config["SERVER_NAME"] = "localhost.localdomain:3000" + client = app.test_client() + + @app.route("/") + def index(): + return "", 204 + + # suppress Werkzeug 0.15 warning about name mismatch + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Current server name", UserWarning, "flask.app" + ) + # ip address can't match name + rv = client.get("/", "http://127.0.0.1:3000/") + assert rv.status_code == 404 if matching else 204 + + # allow all subdomains if matching is disabled + rv = client.get("/", "http://www.localhost.localdomain:3000/") + assert rv.status_code == 404 if matching else 204 + + +def test_multi_route_rules(app, client): + @app.route("/") + @app.route("//") + def index(test="a"): + return test + + rv = client.open("/") + assert rv.data == b"a" + rv = client.open("/b/") + assert rv.data == b"b" + + +def test_multi_route_class_views(app, client): + class View: + def __init__(self, app): + app.add_url_rule("/", "index", self.index) + app.add_url_rule("//", "index", self.index) + + def index(self, test="a"): + return test + + _ = View(app) + rv = client.open("/") + assert rv.data == b"a" + rv = client.open("/b/") + assert rv.data == b"b" + + +def test_run_defaults(monkeypatch, app): + rv = {} + + # Mocks werkzeug.serving.run_simple method + def run_simple_mock(*args, **kwargs): + rv["result"] = "running..." + + monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) + app.run() + assert rv["result"] == "running..." + + +def test_run_server_port(monkeypatch, app): + rv = {} + + # Mocks werkzeug.serving.run_simple method + def run_simple_mock(hostname, port, application, *args, **kwargs): + rv["result"] = f"running on {hostname}:{port} ..." + + monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) + hostname, port = "localhost", 8000 + app.run(hostname, port, debug=True) + assert rv["result"] == f"running on {hostname}:{port} ..." + + +@pytest.mark.parametrize( + "host,port,server_name,expect_host,expect_port", + ( + (None, None, "pocoo.org:8080", "pocoo.org", 8080), + ("localhost", None, "pocoo.org:8080", "localhost", 8080), + (None, 80, "pocoo.org:8080", "pocoo.org", 80), + ("localhost", 80, "pocoo.org:8080", "localhost", 80), + ("localhost", 0, "localhost:8080", "localhost", 0), + (None, None, "localhost:8080", "localhost", 8080), + (None, None, "localhost:0", "localhost", 0), + ), +) +def test_run_from_config( + monkeypatch, host, port, server_name, expect_host, expect_port, app +): + def run_simple_mock(hostname, port, *args, **kwargs): + assert hostname == expect_host + assert port == expect_port + + monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) + app.config["SERVER_NAME"] = server_name + app.run(host, port) + + +def test_max_cookie_size(app, client, recwarn): + app.config["MAX_COOKIE_SIZE"] = 100 + + # outside app context, default to Werkzeug static value, + # which is also the default config + response = flask.Response() + default = flask.Flask.default_config["MAX_COOKIE_SIZE"] + assert response.max_cookie_size == default + + # inside app context, use app config + with app.app_context(): + assert flask.Response().max_cookie_size == 100 + + @app.route("/") + def index(): + r = flask.Response("", status=204) + r.set_cookie("foo", "bar" * 100) + return r + + client.get("/") + assert len(recwarn) == 1 + w = recwarn.pop() + assert "cookie is too large" in str(w.message) + + app.config["MAX_COOKIE_SIZE"] = 0 + + client.get("/") + assert len(recwarn) == 0 + + +@require_cpython_gc +def test_app_freed_on_zero_refcount(): + # A Flask instance should not create a reference cycle that prevents CPython + # from freeing it when all external references to it are released (see #3761). + gc.disable() + try: + app = flask.Flask(__name__) + assert app.view_functions["static"] + weak = weakref.ref(app) + assert weak() is not None + del app + assert weak() is None + finally: + gc.enable() diff --git a/test/fixtures/whole_applications/flask/tests/test_blueprints.py b/test/fixtures/whole_applications/flask/tests/test_blueprints.py new file mode 100644 index 0000000..69bc71a --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_blueprints.py @@ -0,0 +1,1054 @@ +import pytest +from jinja2 import TemplateNotFound +from werkzeug.http import parse_cache_control_header + +import flask + + +def test_blueprint_specific_error_handling(app, client): + frontend = flask.Blueprint("frontend", __name__) + backend = flask.Blueprint("backend", __name__) + sideend = flask.Blueprint("sideend", __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return "frontend says no", 403 + + @frontend.route("/frontend-no") + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return "backend says no", 403 + + @backend.route("/backend-no") + def backend_no(): + flask.abort(403) + + @sideend.route("/what-is-a-sideend") + def sideend_no(): + flask.abort(403) + + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return "application itself says no", 403 + + assert client.get("/frontend-no").data == b"frontend says no" + assert client.get("/backend-no").data == b"backend says no" + assert client.get("/what-is-a-sideend").data == b"application itself says no" + + +def test_blueprint_specific_user_error_handling(app, client): + class MyDecoratorException(Exception): + pass + + class MyFunctionException(Exception): + pass + + blue = flask.Blueprint("blue", __name__) + + @blue.errorhandler(MyDecoratorException) + def my_decorator_exception_handler(e): + assert isinstance(e, MyDecoratorException) + return "boom" + + def my_function_exception_handler(e): + assert isinstance(e, MyFunctionException) + return "bam" + + blue.register_error_handler(MyFunctionException, my_function_exception_handler) + + @blue.route("/decorator") + def blue_deco_test(): + raise MyDecoratorException() + + @blue.route("/function") + def blue_func_test(): + raise MyFunctionException() + + app.register_blueprint(blue) + + assert client.get("/decorator").data == b"boom" + assert client.get("/function").data == b"bam" + + +def test_blueprint_app_error_handling(app, client): + errors = flask.Blueprint("errors", __name__) + + @errors.app_errorhandler(403) + def forbidden_handler(e): + return "you shall not pass", 403 + + @app.route("/forbidden") + def app_forbidden(): + flask.abort(403) + + forbidden_bp = flask.Blueprint("forbidden_bp", __name__) + + @forbidden_bp.route("/nope") + def bp_forbidden(): + flask.abort(403) + + app.register_blueprint(errors) + app.register_blueprint(forbidden_bp) + + assert client.get("/forbidden").data == b"you shall not pass" + assert client.get("/nope").data == b"you shall not pass" + + +@pytest.mark.parametrize( + ("prefix", "rule", "url"), + ( + ("", "/", "/"), + ("/", "", "/"), + ("/", "/", "/"), + ("/foo", "", "/foo"), + ("/foo/", "", "/foo/"), + ("", "/bar", "/bar"), + ("/foo/", "/bar", "/foo/bar"), + ("/foo/", "bar", "/foo/bar"), + ("/foo", "/bar", "/foo/bar"), + ("/foo/", "//bar", "/foo/bar"), + ("/foo//", "/bar", "/foo/bar"), + ), +) +def test_blueprint_prefix_slash(app, client, prefix, rule, url): + bp = flask.Blueprint("test", __name__, url_prefix=prefix) + + @bp.route(rule) + def index(): + return "", 204 + + app.register_blueprint(bp) + assert client.get(url).status_code == 204 + + +def test_blueprint_url_defaults(app, client): + bp = flask.Blueprint("test", __name__) + + @bp.route("/foo", defaults={"baz": 42}) + def foo(bar, baz): + return f"{bar}/{baz:d}" + + @bp.route("/bar") + def bar(bar): + return str(bar) + + app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23}) + app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19}) + + assert client.get("/1/foo").data == b"23/42" + assert client.get("/2/foo").data == b"19/42" + assert client.get("/1/bar").data == b"23" + assert client.get("/2/bar").data == b"19" + + +def test_blueprint_url_processors(app, client): + bp = flask.Blueprint("frontend", __name__, url_prefix="/") + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault("lang_code", flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop("lang_code") + + @bp.route("/") + def index(): + return flask.url_for(".about") + + @bp.route("/about") + def about(): + return flask.url_for(".index") + + app.register_blueprint(bp) + + assert client.get("/de/").data == b"/de/about" + assert client.get("/de/about").data == b"/de/" + + +def test_templates_and_static(test_apps): + from blueprintapp import app + + client = app.test_client() + + rv = client.get("/") + assert rv.data == b"Hello from the Frontend" + rv = client.get("/admin/") + assert rv.data == b"Hello from the Admin" + rv = client.get("/admin/index2") + assert rv.data == b"Hello from the Admin" + rv = client.get("/admin/static/test.txt") + assert rv.data.strip() == b"Admin File" + rv.close() + rv = client.get("/admin/static/css/test.css") + assert rv.data.strip() == b"/* nested file */" + rv.close() + + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] + try: + expected_max_age = 3600 + if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age: + expected_max_age = 7200 + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age + rv = client.get("/admin/static/css/test.css") + cc = parse_cache_control_header(rv.headers["Cache-Control"]) + assert cc.max_age == expected_max_age + rv.close() + finally: + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default + + with app.test_request_context(): + assert ( + flask.url_for("admin.static", filename="test.txt") + == "/admin/static/test.txt" + ) + + with app.test_request_context(): + with pytest.raises(TemplateNotFound) as e: + flask.render_template("missing.html") + assert e.value.name == "missing.html" + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template("nested/nested.txt") == "I'm nested" + + +def test_default_static_max_age(app): + class MyBlueprint(flask.Blueprint): + def get_send_file_max_age(self, filename): + return 100 + + blueprint = MyBlueprint("blueprint", __name__, static_folder="static") + app.register_blueprint(blueprint) + + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] + try: + with app.test_request_context(): + unexpected_max_age = 3600 + if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age: + unexpected_max_age = 7200 + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age + rv = blueprint.send_static_file("index.html") + cc = parse_cache_control_header(rv.headers["Cache-Control"]) + assert cc.max_age == 100 + rv.close() + finally: + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default + + +def test_templates_list(test_apps): + from blueprintapp import app + + templates = sorted(app.jinja_env.list_templates()) + assert templates == ["admin/index.html", "frontend/index.html"] + + +def test_dotted_name_not_allowed(app, client): + with pytest.raises(ValueError): + flask.Blueprint("app.ui", __name__) + + +def test_empty_name_not_allowed(app, client): + with pytest.raises(ValueError): + flask.Blueprint("", __name__) + + +def test_dotted_names_from_app(app, client): + test = flask.Blueprint("test", __name__) + + @app.route("/") + def app_index(): + return flask.url_for("test.index") + + @test.route("/test/") + def index(): + return flask.url_for("app_index") + + app.register_blueprint(test) + + rv = client.get("/") + assert rv.data == b"/test/" + + +def test_empty_url_defaults(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.route("/", defaults={"page": 1}) + @bp.route("/page/") + def something(page): + return str(page) + + app.register_blueprint(bp) + + assert client.get("/").data == b"1" + assert client.get("/page/2").data == b"2" + + +def test_route_decorator_custom_endpoint(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.route("/foo") + def foo(): + return flask.request.endpoint + + @bp.route("/bar", endpoint="bar") + def foo_bar(): + return flask.request.endpoint + + @bp.route("/bar/123", endpoint="123") + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route("/bar/foo") + def bar_foo(): + return flask.request.endpoint + + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.request.endpoint + + assert client.get("/").data == b"index" + assert client.get("/py/foo").data == b"bp.foo" + assert client.get("/py/bar").data == b"bp.bar" + assert client.get("/py/bar/123").data == b"bp.123" + assert client.get("/py/bar/foo").data == b"bp.bar_foo" + + +def test_route_decorator_custom_endpoint_with_dots(app, client): + bp = flask.Blueprint("bp", __name__) + + with pytest.raises(ValueError): + bp.route("/", endpoint="a.b")(lambda: "") + + with pytest.raises(ValueError): + bp.add_url_rule("/", endpoint="a.b") + + def view(): + return "" + + view.__name__ = "a.b" + + with pytest.raises(ValueError): + bp.add_url_rule("/", view_func=view) + + +def test_endpoint_decorator(app, client): + from werkzeug.routing import Rule + + app.url_map.add(Rule("/foo", endpoint="bar")) + + bp = flask.Blueprint("bp", __name__) + + @bp.endpoint("bar") + def foobar(): + return flask.request.endpoint + + app.register_blueprint(bp, url_prefix="/bp_prefix") + + assert client.get("/foo").data == b"bar" + assert client.get("/bp_prefix/bar").status_code == 404 + + +def test_template_filter(app): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_filter() + def my_reverse(s): + return s[::-1] + + app.register_blueprint(bp, url_prefix="/py") + assert "my_reverse" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse"] == my_reverse + assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + + +def test_add_template_filter(app): + bp = flask.Blueprint("bp", __name__) + + def my_reverse(s): + return s[::-1] + + bp.add_app_template_filter(my_reverse) + app.register_blueprint(bp, url_prefix="/py") + assert "my_reverse" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse"] == my_reverse + assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + + +def test_template_filter_with_name(app): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_filter("strrev") + def my_reverse(s): + return s[::-1] + + app.register_blueprint(bp, url_prefix="/py") + assert "strrev" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["strrev"] == my_reverse + assert app.jinja_env.filters["strrev"]("abcd") == "dcba" + + +def test_add_template_filter_with_name(app): + bp = flask.Blueprint("bp", __name__) + + def my_reverse(s): + return s[::-1] + + bp.add_app_template_filter(my_reverse, "strrev") + app.register_blueprint(bp, url_prefix="/py") + assert "strrev" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["strrev"] == my_reverse + assert app.jinja_env.filters["strrev"]("abcd") == "dcba" + + +def test_template_filter_with_template(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_filter() + def super_reverse(s): + return s[::-1] + + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_template_filter_after_route_with_template(app, client): + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_filter() + def super_reverse(s): + return s[::-1] + + app.register_blueprint(bp, url_prefix="/py") + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_add_template_filter_with_template(app, client): + bp = flask.Blueprint("bp", __name__) + + def super_reverse(s): + return s[::-1] + + bp.add_app_template_filter(super_reverse) + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_template_filter_with_name_and_template(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_filter("super_reverse") + def my_reverse(s): + return s[::-1] + + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_add_template_filter_with_name_and_template(app, client): + bp = flask.Blueprint("bp", __name__) + + def my_reverse(s): + return s[::-1] + + bp.add_app_template_filter(my_reverse, "super_reverse") + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_template_test(app): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_test() + def is_boolean(value): + return isinstance(value, bool) + + app.register_blueprint(bp, url_prefix="/py") + assert "is_boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["is_boolean"] == is_boolean + assert app.jinja_env.tests["is_boolean"](False) + + +def test_add_template_test(app): + bp = flask.Blueprint("bp", __name__) + + def is_boolean(value): + return isinstance(value, bool) + + bp.add_app_template_test(is_boolean) + app.register_blueprint(bp, url_prefix="/py") + assert "is_boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["is_boolean"] == is_boolean + assert app.jinja_env.tests["is_boolean"](False) + + +def test_template_test_with_name(app): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_test("boolean") + def is_boolean(value): + return isinstance(value, bool) + + app.register_blueprint(bp, url_prefix="/py") + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == is_boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_add_template_test_with_name(app): + bp = flask.Blueprint("bp", __name__) + + def is_boolean(value): + return isinstance(value, bool) + + bp.add_app_template_test(is_boolean, "boolean") + app.register_blueprint(bp, url_prefix="/py") + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == is_boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_template_test_with_template(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_template_test_after_route_with_template(app, client): + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + + app.register_blueprint(bp, url_prefix="/py") + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_add_template_test_with_template(app, client): + bp = flask.Blueprint("bp", __name__) + + def boolean(value): + return isinstance(value, bool) + + bp.add_app_template_test(boolean) + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_template_test_with_name_and_template(app, client): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_test("boolean") + def is_boolean(value): + return isinstance(value, bool) + + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_add_template_test_with_name_and_template(app, client): + bp = flask.Blueprint("bp", __name__) + + def is_boolean(value): + return isinstance(value, bool) + + bp.add_app_template_test(is_boolean, "boolean") + app.register_blueprint(bp, url_prefix="/py") + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_context_processing(app, client): + answer_bp = flask.Blueprint("answer_bp", __name__) + + def template_string(): + return flask.render_template_string( + "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" + "{% if answer %}{{ answer }} is the answer.{% endif %}" + ) + + # App global context processor + @answer_bp.app_context_processor + def not_answer_context_processor(): + return {"notanswer": 43} + + # Blueprint local context processor + @answer_bp.context_processor + def answer_context_processor(): + return {"answer": 42} + + # Setup endpoints for testing + @answer_bp.route("/bp") + def bp_page(): + return template_string() + + @app.route("/") + def app_page(): + return template_string() + + # Register the blueprint + app.register_blueprint(answer_bp) + + app_page_bytes = client.get("/").data + answer_page_bytes = client.get("/bp").data + + assert b"43" in app_page_bytes + assert b"42" not in app_page_bytes + + assert b"42" in answer_page_bytes + assert b"43" in answer_page_bytes + + +def test_template_global(app): + bp = flask.Blueprint("bp", __name__) + + @bp.app_template_global() + def get_answer(): + return 42 + + # Make sure the function is not in the jinja_env already + assert "get_answer" not in app.jinja_env.globals.keys() + app.register_blueprint(bp) + + # Tests + assert "get_answer" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["get_answer"] is get_answer + assert app.jinja_env.globals["get_answer"]() == 42 + + with app.app_context(): + rv = flask.render_template_string("{{ get_answer() }}") + assert rv == "42" + + +def test_request_processing(app, client): + bp = flask.Blueprint("bp", __name__) + evts = [] + + @bp.before_request + def before_bp(): + evts.append("before") + + @bp.after_request + def after_bp(response): + response.data += b"|after" + evts.append("after") + return response + + @bp.teardown_request + def teardown_bp(exc): + evts.append("teardown") + + # Setup routes for testing + @bp.route("/bp") + def bp_endpoint(): + return "request" + + app.register_blueprint(bp) + + assert evts == [] + rv = client.get("/bp") + assert rv.data == b"request|after" + assert evts == ["before", "after", "teardown"] + + +def test_app_request_processing(app, client): + bp = flask.Blueprint("bp", __name__) + evts = [] + + @bp.before_app_request + def before_app(): + evts.append("before") + + @bp.after_app_request + def after_app(response): + response.data += b"|after" + evts.append("after") + return response + + @bp.teardown_app_request + def teardown_app(exc): + evts.append("teardown") + + app.register_blueprint(bp) + + # Setup routes for testing + @app.route("/") + def bp_endpoint(): + return "request" + + # before first request + assert evts == [] + + # first request + resp = client.get("/").data + assert resp == b"request|after" + assert evts == ["before", "after", "teardown"] + + # second request + resp = client.get("/").data + assert resp == b"request|after" + assert evts == ["before", "after", "teardown"] * 2 + + +def test_app_url_processors(app, client): + bp = flask.Blueprint("bp", __name__) + + # Register app-wide url defaults and preprocessor on blueprint + @bp.app_url_defaults + def add_language_code(endpoint, values): + values.setdefault("lang_code", flask.g.lang_code) + + @bp.app_url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop("lang_code") + + # Register route rules at the app level + @app.route("//") + def index(): + return flask.url_for("about") + + @app.route("//about") + def about(): + return flask.url_for("index") + + app.register_blueprint(bp) + + assert client.get("/de/").data == b"/de/about" + assert client.get("/de/about").data == b"/de/" + + +def test_nested_blueprint(app, client): + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__) + grandchild = flask.Blueprint("grandchild", __name__) + + @parent.errorhandler(403) + def forbidden(e): + return "Parent no", 403 + + @parent.route("/") + def parent_index(): + return "Parent yes" + + @parent.route("/no") + def parent_no(): + flask.abort(403) + + @child.route("/") + def child_index(): + return "Child yes" + + @child.route("/no") + def child_no(): + flask.abort(403) + + @grandchild.errorhandler(403) + def grandchild_forbidden(e): + return "Grandchild no", 403 + + @grandchild.route("/") + def grandchild_index(): + return "Grandchild yes" + + @grandchild.route("/no") + def grandchild_no(): + flask.abort(403) + + child.register_blueprint(grandchild, url_prefix="/grandchild") + parent.register_blueprint(child, url_prefix="/child") + app.register_blueprint(parent, url_prefix="/parent") + + assert client.get("/parent/").data == b"Parent yes" + assert client.get("/parent/child/").data == b"Child yes" + assert client.get("/parent/child/grandchild/").data == b"Grandchild yes" + assert client.get("/parent/no").data == b"Parent no" + assert client.get("/parent/child/no").data == b"Parent no" + assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" + + +def test_nested_callback_order(app, client): + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__) + + @app.before_request + def app_before1(): + flask.g.setdefault("seen", []).append("app_1") + + @app.teardown_request + def app_teardown1(e=None): + assert flask.g.seen.pop() == "app_1" + + @app.before_request + def app_before2(): + flask.g.setdefault("seen", []).append("app_2") + + @app.teardown_request + def app_teardown2(e=None): + assert flask.g.seen.pop() == "app_2" + + @app.context_processor + def app_ctx(): + return dict(key="app") + + @parent.before_request + def parent_before1(): + flask.g.setdefault("seen", []).append("parent_1") + + @parent.teardown_request + def parent_teardown1(e=None): + assert flask.g.seen.pop() == "parent_1" + + @parent.before_request + def parent_before2(): + flask.g.setdefault("seen", []).append("parent_2") + + @parent.teardown_request + def parent_teardown2(e=None): + assert flask.g.seen.pop() == "parent_2" + + @parent.context_processor + def parent_ctx(): + return dict(key="parent") + + @child.before_request + def child_before1(): + flask.g.setdefault("seen", []).append("child_1") + + @child.teardown_request + def child_teardown1(e=None): + assert flask.g.seen.pop() == "child_1" + + @child.before_request + def child_before2(): + flask.g.setdefault("seen", []).append("child_2") + + @child.teardown_request + def child_teardown2(e=None): + assert flask.g.seen.pop() == "child_2" + + @child.context_processor + def child_ctx(): + return dict(key="child") + + @child.route("/a") + def a(): + return ", ".join(flask.g.seen) + + @child.route("/b") + def b(): + return flask.render_template_string("{{ key }}") + + parent.register_blueprint(child) + app.register_blueprint(parent) + assert ( + client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2" + ) + assert client.get("/b").data == b"child" + + +@pytest.mark.parametrize( + "parent_init, child_init, parent_registration, child_registration", + [ + ("/parent", "/child", None, None), + ("/parent", None, None, "/child"), + (None, None, "/parent", "/child"), + ("/other", "/something", "/parent", "/child"), + ], +) +def test_nesting_url_prefixes( + parent_init, + child_init, + parent_registration, + child_registration, + app, + client, +) -> None: + parent = flask.Blueprint("parent", __name__, url_prefix=parent_init) + child = flask.Blueprint("child", __name__, url_prefix=child_init) + + @child.route("/") + def index(): + return "index" + + parent.register_blueprint(child, url_prefix=child_registration) + app.register_blueprint(parent, url_prefix=parent_registration) + + response = client.get("/parent/child/") + assert response.status_code == 200 + + +def test_nesting_subdomains(app, client) -> None: + subdomain = "api" + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__) + + @child.route("/child/") + def index(): + return "child" + + parent.register_blueprint(child) + app.register_blueprint(parent, subdomain=subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get("/child/", base_url="http://api." + domain_name) + + assert response.status_code == 200 + + +def test_child_and_parent_subdomain(app, client) -> None: + child_subdomain = "api" + parent_subdomain = "parent" + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__, subdomain=child_subdomain) + + @child.route("/") + def index(): + return "child" + + parent.register_blueprint(child) + app.register_blueprint(parent, subdomain=parent_subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get( + "/", base_url=f"http://{child_subdomain}.{parent_subdomain}.{domain_name}" + ) + + assert response.status_code == 200 + + response = client.get("/", base_url=f"http://{parent_subdomain}.{domain_name}") + + assert response.status_code == 404 + + +def test_unique_blueprint_names(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + bp2 = flask.Blueprint("bp", __name__) + + app.register_blueprint(bp) + + with pytest.raises(ValueError): + app.register_blueprint(bp) # same bp, same name, error + + app.register_blueprint(bp, name="again") # same bp, different name, ok + + with pytest.raises(ValueError): + app.register_blueprint(bp2) # different bp, same name, error + + app.register_blueprint(bp2, name="alt") # different bp, different name, ok + + +def test_self_registration(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + with pytest.raises(ValueError): + bp.register_blueprint(bp) + + +def test_blueprint_renaming(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + bp2 = flask.Blueprint("bp2", __name__) + + @bp.get("/") + def index(): + return flask.request.endpoint + + @bp.get("/error") + def error(): + flask.abort(403) + + @bp.errorhandler(403) + def forbidden(_: Exception): + return "Error", 403 + + @bp2.get("/") + def index2(): + return flask.request.endpoint + + bp.register_blueprint(bp2, url_prefix="/a", name="sub") + app.register_blueprint(bp, url_prefix="/a") + app.register_blueprint(bp, url_prefix="/b", name="alt") + + assert client.get("/a/").data == b"bp.index" + assert client.get("/b/").data == b"alt.index" + assert client.get("/a/a/").data == b"bp.sub.index2" + assert client.get("/b/a/").data == b"alt.sub.index2" + assert client.get("/a/error").data == b"Error" + assert client.get("/b/error").data == b"Error" diff --git a/test/fixtures/whole_applications/flask/tests/test_cli.py b/test/fixtures/whole_applications/flask/tests/test_cli.py new file mode 100644 index 0000000..0999548 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_cli.py @@ -0,0 +1,686 @@ +# This file was part of Flask-CLI and was modified under the terms of +# its Revised BSD License. Copyright © 2015 CERN. +import importlib.metadata +import os +import platform +import ssl +import sys +import types +from functools import partial +from pathlib import Path + +import click +import pytest +from _pytest.monkeypatch import notset +from click.testing import CliRunner + +from flask import Blueprint +from flask import current_app +from flask import Flask +from flask.cli import AppGroup +from flask.cli import find_best_app +from flask.cli import FlaskGroup +from flask.cli import get_version +from flask.cli import load_dotenv +from flask.cli import locate_app +from flask.cli import NoAppException +from flask.cli import prepare_import +from flask.cli import run_command +from flask.cli import ScriptInfo +from flask.cli import with_appcontext + +cwd = Path.cwd() +test_path = (Path(__file__) / ".." / "test_apps").resolve() + + +@pytest.fixture +def runner(): + return CliRunner() + + +def test_cli_name(test_apps): + """Make sure the CLI object's name is the app's name and not the app itself""" + from cliapp.app import testapp + + assert testapp.cli.name == testapp.name + + +def test_find_best_app(test_apps): + class Module: + app = Flask("appname") + + assert find_best_app(Module) == Module.app + + class Module: + application = Flask("appname") + + assert find_best_app(Module) == Module.application + + class Module: + myapp = Flask("appname") + + assert find_best_app(Module) == Module.myapp + + class Module: + @staticmethod + def create_app(): + return Flask("appname") + + app = find_best_app(Module) + assert isinstance(app, Flask) + assert app.name == "appname" + + class Module: + @staticmethod + def create_app(**kwargs): + return Flask("appname") + + app = find_best_app(Module) + assert isinstance(app, Flask) + assert app.name == "appname" + + class Module: + @staticmethod + def make_app(): + return Flask("appname") + + app = find_best_app(Module) + assert isinstance(app, Flask) + assert app.name == "appname" + + class Module: + myapp = Flask("appname1") + + @staticmethod + def create_app(): + return Flask("appname2") + + assert find_best_app(Module) == Module.myapp + + class Module: + myapp = Flask("appname1") + + @staticmethod + def create_app(): + return Flask("appname2") + + assert find_best_app(Module) == Module.myapp + + class Module: + pass + + pytest.raises(NoAppException, find_best_app, Module) + + class Module: + myapp1 = Flask("appname1") + myapp2 = Flask("appname2") + + pytest.raises(NoAppException, find_best_app, Module) + + class Module: + @staticmethod + def create_app(foo, bar): + return Flask("appname2") + + pytest.raises(NoAppException, find_best_app, Module) + + class Module: + @staticmethod + def create_app(): + raise TypeError("bad bad factory!") + + pytest.raises(TypeError, find_best_app, Module) + + +@pytest.mark.parametrize( + "value,path,result", + ( + ("test", cwd, "test"), + ("test.py", cwd, "test"), + ("a/test", cwd / "a", "test"), + ("test/__init__.py", cwd, "test"), + ("test/__init__", cwd, "test"), + # nested package + ( + test_path / "cliapp" / "inner1" / "__init__", + test_path, + "cliapp.inner1", + ), + ( + test_path / "cliapp" / "inner1" / "inner2", + test_path, + "cliapp.inner1.inner2", + ), + # dotted name + ("test.a.b", cwd, "test.a.b"), + (test_path / "cliapp.app", test_path, "cliapp.app"), + # not a Python file, will be caught during import + (test_path / "cliapp" / "message.txt", test_path, "cliapp.message.txt"), + ), +) +def test_prepare_import(request, value, path, result): + """Expect the correct path to be set and the correct import and app names + to be returned. + + :func:`prepare_exec_for_file` has a side effect where the parent directory + of the given import is added to :data:`sys.path`. This is reset after the + test runs. + """ + original_path = sys.path[:] + + def reset_path(): + sys.path[:] = original_path + + request.addfinalizer(reset_path) + + assert prepare_import(value) == result + assert sys.path[0] == str(path) + + +@pytest.mark.parametrize( + "iname,aname,result", + ( + ("cliapp.app", None, "testapp"), + ("cliapp.app", "testapp", "testapp"), + ("cliapp.factory", None, "app"), + ("cliapp.factory", "create_app", "app"), + ("cliapp.factory", "create_app()", "app"), + ("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"), + # trailing comma space + ("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"), + # strip whitespace + ("cliapp.factory", " create_app () ", "app"), + ), +) +def test_locate_app(test_apps, iname, aname, result): + assert locate_app(iname, aname).name == result + + +@pytest.mark.parametrize( + "iname,aname", + ( + ("notanapp.py", None), + ("cliapp/app", None), + ("cliapp.app", "notanapp"), + # not enough arguments + ("cliapp.factory", 'create_app2("foo")'), + # invalid identifier + ("cliapp.factory", "create_app("), + # no app returned + ("cliapp.factory", "no_app"), + # nested import error + ("cliapp.importerrorapp", None), + # not a Python file + ("cliapp.message.txt", None), + ), +) +def test_locate_app_raises(test_apps, iname, aname): + with pytest.raises(NoAppException): + locate_app(iname, aname) + + +def test_locate_app_suppress_raise(test_apps): + app = locate_app("notanapp.py", None, raise_if_not_found=False) + assert app is None + + # only direct import error is suppressed + with pytest.raises(NoAppException): + locate_app("cliapp.importerrorapp", None, raise_if_not_found=False) + + +def test_get_version(test_apps, capsys): + class MockCtx: + resilient_parsing = False + color = None + + def exit(self): + return + + ctx = MockCtx() + get_version(ctx, None, "test") + out, err = capsys.readouterr() + assert f"Python {platform.python_version()}" in out + assert f"Flask {importlib.metadata.version('flask')}" in out + assert f"Werkzeug {importlib.metadata.version('werkzeug')}" in out + + +def test_scriptinfo(test_apps, monkeypatch): + obj = ScriptInfo(app_import_path="cliapp.app:testapp") + app = obj.load_app() + assert app.name == "testapp" + assert obj.load_app() is app + + # import app with module's absolute path + cli_app_path = str(test_path / "cliapp" / "app.py") + + obj = ScriptInfo(app_import_path=cli_app_path) + app = obj.load_app() + assert app.name == "testapp" + assert obj.load_app() is app + obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp") + app = obj.load_app() + assert app.name == "testapp" + assert obj.load_app() is app + + def create_app(): + return Flask("createapp") + + obj = ScriptInfo(create_app=create_app) + app = obj.load_app() + assert app.name == "createapp" + assert obj.load_app() is app + + obj = ScriptInfo() + pytest.raises(NoAppException, obj.load_app) + + # import app from wsgi.py in current directory + monkeypatch.chdir(test_path / "helloworld") + obj = ScriptInfo() + app = obj.load_app() + assert app.name == "hello" + + # import app from app.py in current directory + monkeypatch.chdir(test_path / "cliapp") + obj = ScriptInfo() + app = obj.load_app() + assert app.name == "testapp" + + +def test_app_cli_has_app_context(app, runner): + def _param_cb(ctx, param, value): + # current_app should be available in parameter callbacks + return bool(current_app) + + @app.cli.command() + @click.argument("value", callback=_param_cb) + def check(value): + app = click.get_current_context().obj.load_app() + # the loaded app should be the same as current_app + same_app = current_app._get_current_object() is app + return same_app, value + + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["check", "x"], standalone_mode=False) + assert result.return_value == (True, True) + + +def test_with_appcontext(runner): + @click.command() + @with_appcontext + def testcmd(): + click.echo(current_app.name) + + obj = ScriptInfo(create_app=lambda: Flask("testapp")) + + result = runner.invoke(testcmd, obj=obj) + assert result.exit_code == 0 + assert result.output == "testapp\n" + + +def test_appgroup_app_context(runner): + @click.group(cls=AppGroup) + def cli(): + pass + + @cli.command() + def test(): + click.echo(current_app.name) + + @cli.group() + def subgroup(): + pass + + @subgroup.command() + def test2(): + click.echo(current_app.name) + + obj = ScriptInfo(create_app=lambda: Flask("testappgroup")) + + result = runner.invoke(cli, ["test"], obj=obj) + assert result.exit_code == 0 + assert result.output == "testappgroup\n" + + result = runner.invoke(cli, ["subgroup", "test2"], obj=obj) + assert result.exit_code == 0 + assert result.output == "testappgroup\n" + + +def test_flaskgroup_app_context(runner): + def create_app(): + return Flask("flaskgroup") + + @click.group(cls=FlaskGroup, create_app=create_app) + def cli(**params): + pass + + @cli.command() + def test(): + click.echo(current_app.name) + + result = runner.invoke(cli, ["test"]) + assert result.exit_code == 0 + assert result.output == "flaskgroup\n" + + +@pytest.mark.parametrize("set_debug_flag", (True, False)) +def test_flaskgroup_debug(runner, set_debug_flag): + def create_app(): + app = Flask("flaskgroup") + app.debug = True + return app + + @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag) + def cli(**params): + pass + + @cli.command() + def test(): + click.echo(str(current_app.debug)) + + result = runner.invoke(cli, ["test"]) + assert result.exit_code == 0 + assert result.output == f"{not set_debug_flag}\n" + + +def test_flaskgroup_nested(app, runner): + cli = click.Group("cli") + flask_group = FlaskGroup(name="flask", create_app=lambda: app) + cli.add_command(flask_group) + + @flask_group.command() + def show(): + click.echo(current_app.name) + + result = runner.invoke(cli, ["flask", "show"]) + assert result.output == "flask_test\n" + + +def test_no_command_echo_loading_error(): + from flask.cli import cli + + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["missing"]) + assert result.exit_code == 2 + assert "FLASK_APP" in result.stderr + assert "Usage:" in result.stderr + + +def test_help_echo_loading_error(): + from flask.cli import cli + + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + assert "FLASK_APP" in result.stderr + assert "Usage:" in result.stdout + + +def test_help_echo_exception(): + def create_app(): + raise Exception("oh no") + + cli = FlaskGroup(create_app=create_app) + runner = CliRunner(mix_stderr=False) + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + assert "Exception: oh no" in result.stderr + assert "Usage:" in result.stdout + + +class TestRoutes: + @pytest.fixture + def app(self): + app = Flask(__name__) + app.add_url_rule( + "/get_post//", + methods=["GET", "POST"], + endpoint="yyy_get_post", + ) + app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post") + return app + + @pytest.fixture + def invoke(self, app, runner): + cli = FlaskGroup(create_app=lambda: app) + return partial(runner.invoke, cli) + + def expect_order(self, order, output): + # skip the header and match the start of each row + for expect, line in zip(order, output.splitlines()[2:]): + # do this instead of startswith for nicer pytest output + assert line[: len(expect)] == expect + + def test_simple(self, invoke): + result = invoke(["routes"]) + assert result.exit_code == 0 + self.expect_order(["aaa_post", "static", "yyy_get_post"], result.output) + + def test_sort(self, app, invoke): + default_output = invoke(["routes"]).output + endpoint_output = invoke(["routes", "-s", "endpoint"]).output + assert default_output == endpoint_output + self.expect_order( + ["static", "yyy_get_post", "aaa_post"], + invoke(["routes", "-s", "methods"]).output, + ) + self.expect_order( + ["yyy_get_post", "static", "aaa_post"], + invoke(["routes", "-s", "rule"]).output, + ) + match_order = [r.endpoint for r in app.url_map.iter_rules()] + self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) + + def test_all_methods(self, invoke): + output = invoke(["routes"]).output + assert "GET, HEAD, OPTIONS, POST" not in output + output = invoke(["routes", "--all-methods"]).output + assert "GET, HEAD, OPTIONS, POST" in output + + def test_no_routes(self, runner): + app = Flask(__name__, static_folder=None) + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["routes"]) + assert result.exit_code == 0 + assert "No routes were registered." in result.output + + def test_subdomain(self, runner): + app = Flask(__name__, static_folder=None) + app.add_url_rule("/a", subdomain="a", endpoint="a") + app.add_url_rule("/b", subdomain="b", endpoint="b") + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["routes"]) + assert result.exit_code == 0 + assert "Subdomain" in result.output + + def test_host(self, runner): + app = Flask(__name__, static_folder=None, host_matching=True) + app.add_url_rule("/a", host="a", endpoint="a") + app.add_url_rule("/b", host="b", endpoint="b") + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["routes"]) + assert result.exit_code == 0 + assert "Host" in result.output + + +def dotenv_not_available(): + try: + import dotenv # noqa: F401 + except ImportError: + return True + + return False + + +need_dotenv = pytest.mark.skipif( + dotenv_not_available(), reason="dotenv is not installed" +) + + +@need_dotenv +def test_load_dotenv(monkeypatch): + # can't use monkeypatch.delitem since the keys don't exist yet + for item in ("FOO", "BAR", "SPAM", "HAM"): + monkeypatch._setitem.append((os.environ, item, notset)) + + monkeypatch.setenv("EGGS", "3") + monkeypatch.chdir(test_path) + assert load_dotenv() + assert Path.cwd() == test_path + # .flaskenv doesn't overwrite .env + assert os.environ["FOO"] == "env" + # set only in .flaskenv + assert os.environ["BAR"] == "bar" + # set only in .env + assert os.environ["SPAM"] == "1" + # set manually, files don't overwrite + assert os.environ["EGGS"] == "3" + # test env file encoding + assert os.environ["HAM"] == "火腿" + # Non existent file should not load + assert not load_dotenv("non-existent-file") + + +@need_dotenv +def test_dotenv_path(monkeypatch): + for item in ("FOO", "BAR", "EGGS"): + monkeypatch._setitem.append((os.environ, item, notset)) + + load_dotenv(test_path / ".flaskenv") + assert Path.cwd() == cwd + assert "FOO" in os.environ + + +def test_dotenv_optional(monkeypatch): + monkeypatch.setitem(sys.modules, "dotenv", None) + monkeypatch.chdir(test_path) + load_dotenv() + assert "FOO" not in os.environ + + +@need_dotenv +def test_disable_dotenv_from_env(monkeypatch, runner): + monkeypatch.chdir(test_path) + monkeypatch.setitem(os.environ, "FLASK_SKIP_DOTENV", "1") + runner.invoke(FlaskGroup()) + assert "FOO" not in os.environ + + +def test_run_cert_path(): + # no key + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", __file__]) + + # no cert + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--key", __file__]) + + # cert specified first + ctx = run_command.make_context("run", ["--cert", __file__, "--key", __file__]) + assert ctx.params["cert"] == (__file__, __file__) + + # key specified first + ctx = run_command.make_context("run", ["--key", __file__, "--cert", __file__]) + assert ctx.params["cert"] == (__file__, __file__) + + +def test_run_cert_adhoc(monkeypatch): + monkeypatch.setitem(sys.modules, "cryptography", None) + + # cryptography not installed + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "adhoc"]) + + # cryptography installed + monkeypatch.setitem(sys.modules, "cryptography", types.ModuleType("cryptography")) + ctx = run_command.make_context("run", ["--cert", "adhoc"]) + assert ctx.params["cert"] == "adhoc" + + # no key with adhoc + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "adhoc", "--key", __file__]) + + +def test_run_cert_import(monkeypatch): + monkeypatch.setitem(sys.modules, "not_here", None) + + # ImportError + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "not_here"]) + + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "flask"]) + + # SSLContext + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + + monkeypatch.setitem(sys.modules, "ssl_context", ssl_context) + ctx = run_command.make_context("run", ["--cert", "ssl_context"]) + assert ctx.params["cert"] is ssl_context + + # no --key with SSLContext + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__]) + + +def test_run_cert_no_ssl(monkeypatch): + monkeypatch.setitem(sys.modules, "ssl", None) + + with pytest.raises(click.BadParameter): + run_command.make_context("run", ["--cert", "not_here"]) + + +def test_cli_blueprints(app): + """Test blueprint commands register correctly to the application""" + custom = Blueprint("custom", __name__, cli_group="customized") + nested = Blueprint("nested", __name__) + merged = Blueprint("merged", __name__, cli_group=None) + late = Blueprint("late", __name__) + + @custom.cli.command("custom") + def custom_command(): + click.echo("custom_result") + + @nested.cli.command("nested") + def nested_command(): + click.echo("nested_result") + + @merged.cli.command("merged") + def merged_command(): + click.echo("merged_result") + + @late.cli.command("late") + def late_command(): + click.echo("late_result") + + app.register_blueprint(custom) + app.register_blueprint(nested) + app.register_blueprint(merged) + app.register_blueprint(late, cli_group="late_registration") + + app_runner = app.test_cli_runner() + + result = app_runner.invoke(args=["customized", "custom"]) + assert "custom_result" in result.output + + result = app_runner.invoke(args=["nested", "nested"]) + assert "nested_result" in result.output + + result = app_runner.invoke(args=["merged"]) + assert "merged_result" in result.output + + result = app_runner.invoke(args=["late_registration", "late"]) + assert "late_result" in result.output + + +def test_cli_empty(app): + """If a Blueprint's CLI group is empty, do not register it.""" + bp = Blueprint("blue", __name__, cli_group="blue") + app.register_blueprint(bp) + + result = app.test_cli_runner().invoke(args=["blue", "--help"]) + assert result.exit_code == 2, f"Unexpected success:\n\n{result.output}" + + +def test_run_exclude_patterns(): + ctx = run_command.make_context("run", ["--exclude-patterns", __file__]) + assert ctx.params["exclude_patterns"] == [__file__] diff --git a/test/fixtures/whole_applications/flask/tests/test_config.py b/test/fixtures/whole_applications/flask/tests/test_config.py new file mode 100644 index 0000000..e5b1906 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_config.py @@ -0,0 +1,250 @@ +import json +import os + +import pytest + +import flask + +# config keys used for the TestConfig +TEST_KEY = "foo" +SECRET_KEY = "config" + + +def common_object_test(app): + assert app.secret_key == "config" + assert app.config["TEST_KEY"] == "foo" + assert "TestConfig" not in app.config + + +def test_config_from_pyfile(): + app = flask.Flask(__name__) + app.config.from_pyfile(f"{__file__.rsplit('.', 1)[0]}.py") + common_object_test(app) + + +def test_config_from_object(): + app = flask.Flask(__name__) + app.config.from_object(__name__) + common_object_test(app) + + +def test_config_from_file_json(): + app = flask.Flask(__name__) + current_dir = os.path.dirname(os.path.abspath(__file__)) + app.config.from_file(os.path.join(current_dir, "static", "config.json"), json.load) + common_object_test(app) + + +def test_config_from_file_toml(): + tomllib = pytest.importorskip("tomllib", reason="tomllib added in 3.11") + app = flask.Flask(__name__) + current_dir = os.path.dirname(os.path.abspath(__file__)) + app.config.from_file( + os.path.join(current_dir, "static", "config.toml"), tomllib.load, text=False + ) + common_object_test(app) + + +def test_from_prefixed_env(monkeypatch): + monkeypatch.setenv("FLASK_STRING", "value") + monkeypatch.setenv("FLASK_BOOL", "true") + monkeypatch.setenv("FLASK_INT", "1") + monkeypatch.setenv("FLASK_FLOAT", "1.2") + monkeypatch.setenv("FLASK_LIST", "[1, 2]") + monkeypatch.setenv("FLASK_DICT", '{"k": "v"}') + monkeypatch.setenv("NOT_FLASK_OTHER", "other") + + app = flask.Flask(__name__) + app.config.from_prefixed_env() + + assert app.config["STRING"] == "value" + assert app.config["BOOL"] is True + assert app.config["INT"] == 1 + assert app.config["FLOAT"] == 1.2 + assert app.config["LIST"] == [1, 2] + assert app.config["DICT"] == {"k": "v"} + assert "OTHER" not in app.config + + +def test_from_prefixed_env_custom_prefix(monkeypatch): + monkeypatch.setenv("FLASK_A", "a") + monkeypatch.setenv("NOT_FLASK_A", "b") + + app = flask.Flask(__name__) + app.config.from_prefixed_env("NOT_FLASK") + + assert app.config["A"] == "b" + + +def test_from_prefixed_env_nested(monkeypatch): + monkeypatch.setenv("FLASK_EXIST__ok", "other") + monkeypatch.setenv("FLASK_EXIST__inner__ik", "2") + monkeypatch.setenv("FLASK_EXIST__new__more", '{"k": false}') + monkeypatch.setenv("FLASK_NEW__K", "v") + + app = flask.Flask(__name__) + app.config["EXIST"] = {"ok": "value", "flag": True, "inner": {"ik": 1}} + app.config.from_prefixed_env() + + if os.name != "nt": + assert app.config["EXIST"] == { + "ok": "other", + "flag": True, + "inner": {"ik": 2}, + "new": {"more": {"k": False}}, + } + else: + # Windows env var keys are always uppercase. + assert app.config["EXIST"] == { + "ok": "value", + "OK": "other", + "flag": True, + "inner": {"ik": 1}, + "INNER": {"IK": 2}, + "NEW": {"MORE": {"k": False}}, + } + + assert app.config["NEW"] == {"K": "v"} + + +def test_config_from_mapping(): + app = flask.Flask(__name__) + app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"}) + common_object_test(app) + + app = flask.Flask(__name__) + app.config.from_mapping([("SECRET_KEY", "config"), ("TEST_KEY", "foo")]) + common_object_test(app) + + app = flask.Flask(__name__) + app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo") + common_object_test(app) + + app = flask.Flask(__name__) + app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip") + common_object_test(app) + + app = flask.Flask(__name__) + with pytest.raises(TypeError): + app.config.from_mapping({}, {}) + + +def test_config_from_class(): + class Base: + TEST_KEY = "foo" + + class Test(Base): + SECRET_KEY = "config" + + app = flask.Flask(__name__) + app.config.from_object(Test) + common_object_test(app) + + +def test_config_from_envvar(monkeypatch): + monkeypatch.setattr("os.environ", {}) + app = flask.Flask(__name__) + + with pytest.raises(RuntimeError) as e: + app.config.from_envvar("FOO_SETTINGS") + + assert "'FOO_SETTINGS' is not set" in str(e.value) + assert not app.config.from_envvar("FOO_SETTINGS", silent=True) + + monkeypatch.setattr( + "os.environ", {"FOO_SETTINGS": f"{__file__.rsplit('.', 1)[0]}.py"} + ) + assert app.config.from_envvar("FOO_SETTINGS") + common_object_test(app) + + +def test_config_from_envvar_missing(monkeypatch): + monkeypatch.setattr("os.environ", {"FOO_SETTINGS": "missing.cfg"}) + app = flask.Flask(__name__) + with pytest.raises(IOError) as e: + app.config.from_envvar("FOO_SETTINGS") + msg = str(e.value) + assert msg.startswith( + "[Errno 2] Unable to load configuration file (No such file or directory):" + ) + assert msg.endswith("missing.cfg'") + assert not app.config.from_envvar("FOO_SETTINGS", silent=True) + + +def test_config_missing(): + app = flask.Flask(__name__) + with pytest.raises(IOError) as e: + app.config.from_pyfile("missing.cfg") + msg = str(e.value) + assert msg.startswith( + "[Errno 2] Unable to load configuration file (No such file or directory):" + ) + assert msg.endswith("missing.cfg'") + assert not app.config.from_pyfile("missing.cfg", silent=True) + + +def test_config_missing_file(): + app = flask.Flask(__name__) + with pytest.raises(IOError) as e: + app.config.from_file("missing.json", load=json.load) + msg = str(e.value) + assert msg.startswith( + "[Errno 2] Unable to load configuration file (No such file or directory):" + ) + assert msg.endswith("missing.json'") + assert not app.config.from_file("missing.json", load=json.load, silent=True) + + +def test_custom_config_class(): + class Config(flask.Config): + pass + + class Flask(flask.Flask): + config_class = Config + + app = Flask(__name__) + assert isinstance(app.config, Config) + app.config.from_object(__name__) + common_object_test(app) + + +def test_session_lifetime(): + app = flask.Flask(__name__) + app.config["PERMANENT_SESSION_LIFETIME"] = 42 + assert app.permanent_session_lifetime.seconds == 42 + + +def test_get_namespace(): + app = flask.Flask(__name__) + app.config["FOO_OPTION_1"] = "foo option 1" + app.config["FOO_OPTION_2"] = "foo option 2" + app.config["BAR_STUFF_1"] = "bar stuff 1" + app.config["BAR_STUFF_2"] = "bar stuff 2" + foo_options = app.config.get_namespace("FOO_") + assert 2 == len(foo_options) + assert "foo option 1" == foo_options["option_1"] + assert "foo option 2" == foo_options["option_2"] + bar_options = app.config.get_namespace("BAR_", lowercase=False) + assert 2 == len(bar_options) + assert "bar stuff 1" == bar_options["STUFF_1"] + assert "bar stuff 2" == bar_options["STUFF_2"] + foo_options = app.config.get_namespace("FOO_", trim_namespace=False) + assert 2 == len(foo_options) + assert "foo option 1" == foo_options["foo_option_1"] + assert "foo option 2" == foo_options["foo_option_2"] + bar_options = app.config.get_namespace( + "BAR_", lowercase=False, trim_namespace=False + ) + assert 2 == len(bar_options) + assert "bar stuff 1" == bar_options["BAR_STUFF_1"] + assert "bar stuff 2" == bar_options["BAR_STUFF_2"] + + +@pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-15", "latin-1"]) +def test_from_pyfile_weird_encoding(tmp_path, encoding): + f = tmp_path / "my_config.py" + f.write_text(f'# -*- coding: {encoding} -*-\nTEST_VALUE = "föö"\n', encoding) + app = flask.Flask(__name__) + app.config.from_pyfile(os.fspath(f)) + value = app.config["TEST_VALUE"] + assert value == "föö" diff --git a/test/fixtures/whole_applications/flask/tests/test_converters.py b/test/fixtures/whole_applications/flask/tests/test_converters.py new file mode 100644 index 0000000..d94a765 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_converters.py @@ -0,0 +1,42 @@ +from werkzeug.routing import BaseConverter + +from flask import request +from flask import session +from flask import url_for + + +def test_custom_converters(app, client): + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(",") + + def to_url(self, value): + base_to_url = super().to_url + return ",".join(base_to_url(x) for x in value) + + app.url_map.converters["list"] = ListConverter + + @app.route("/") + def index(args): + return "|".join(args) + + assert client.get("/1,2,3").data == b"1|2|3" + + with app.test_request_context(): + assert url_for("index", args=[4, 5, 6]) == "/4,5,6" + + +def test_context_available(app, client): + class ContextConverter(BaseConverter): + def to_python(self, value): + assert request is not None + assert session is not None + return value + + app.url_map.converters["ctx"] = ContextConverter + + @app.get("/") + def index(name): + return name + + assert client.get("/admin").data == b"admin" diff --git a/test/fixtures/whole_applications/flask/tests/test_helpers.py b/test/fixtures/whole_applications/flask/tests/test_helpers.py new file mode 100644 index 0000000..3566385 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_helpers.py @@ -0,0 +1,349 @@ +import io +import os + +import pytest +import werkzeug.exceptions + +import flask +from flask.helpers import get_debug_flag + + +class FakePath: + """Fake object to represent a ``PathLike object``. + + This represents a ``pathlib.Path`` object in python 3. + See: https://www.python.org/dev/peps/pep-0519/ + """ + + def __init__(self, path): + self.path = path + + def __fspath__(self): + return self.path + + +class PyBytesIO: + def __init__(self, *args, **kwargs): + self._io = io.BytesIO(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self._io, name) + + +class TestSendfile: + def test_send_file(self, app, req_ctx): + rv = flask.send_file("static/index.html") + assert rv.direct_passthrough + assert rv.mimetype == "text/html" + + with app.open_resource("static/index.html") as f: + rv.direct_passthrough = False + assert rv.data == f.read() + + rv.close() + + def test_static_file(self, app, req_ctx): + # Default max_age is None. + + # Test with static file handler. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age is None + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age is None + rv.close() + + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 + + # Test with static file handler. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age == 3600 + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age == 3600 + rv.close() + + # Test with pathlib.Path. + rv = app.send_static_file(FakePath("index.html")) + assert rv.cache_control.max_age == 3600 + rv.close() + + class StaticFileApp(flask.Flask): + def get_send_file_max_age(self, filename): + return 10 + + app = StaticFileApp(__name__) + + with app.test_request_context(): + # Test with static file handler. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age == 10 + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age == 10 + rv.close() + + def test_send_from_directory(self, app, req_ctx): + app.root_path = os.path.join( + os.path.dirname(__file__), "test_apps", "subdomaintestmodule" + ) + rv = flask.send_from_directory("static", "hello.txt") + rv.direct_passthrough = False + assert rv.data.strip() == b"Hello Subdomain" + rv.close() + + +class TestUrlFor: + def test_url_for_with_anchor(self, app, req_ctx): + @app.route("/") + def index(): + return "42" + + assert flask.url_for("index", _anchor="x y") == "/#x%20y" + + def test_url_for_with_scheme(self, app, req_ctx): + @app.route("/") + def index(): + return "42" + + assert ( + flask.url_for("index", _external=True, _scheme="https") + == "https://localhost/" + ) + + def test_url_for_with_scheme_not_external(self, app, req_ctx): + app.add_url_rule("/", endpoint="index") + + # Implicit external with scheme. + url = flask.url_for("index", _scheme="https") + assert url == "https://localhost/" + + # Error when external=False with scheme + with pytest.raises(ValueError): + flask.url_for("index", _scheme="https", _external=False) + + def test_url_for_with_alternating_schemes(self, app, req_ctx): + @app.route("/") + def index(): + return "42" + + assert flask.url_for("index", _external=True) == "http://localhost/" + assert ( + flask.url_for("index", _external=True, _scheme="https") + == "https://localhost/" + ) + assert flask.url_for("index", _external=True) == "http://localhost/" + + def test_url_with_method(self, app, req_ctx): + from flask.views import MethodView + + class MyView(MethodView): + def get(self, id=None): + if id is None: + return "List" + return f"Get {id:d}" + + def post(self): + return "Create" + + myview = MyView.as_view("myview") + app.add_url_rule("/myview/", methods=["GET"], view_func=myview) + app.add_url_rule("/myview/", methods=["GET"], view_func=myview) + app.add_url_rule("/myview/create", methods=["POST"], view_func=myview) + + assert flask.url_for("myview", _method="GET") == "/myview/" + assert flask.url_for("myview", id=42, _method="GET") == "/myview/42" + assert flask.url_for("myview", _method="POST") == "/myview/create" + + def test_url_for_with_self(self, app, req_ctx): + @app.route("/") + def index(self): + return "42" + + assert flask.url_for("index", self="2") == "/2" + + +def test_redirect_no_app(): + response = flask.redirect("https://localhost", 307) + assert response.location == "https://localhost" + assert response.status_code == 307 + + +def test_redirect_with_app(app): + def redirect(location, code=302): + raise ValueError + + app.redirect = redirect + + with app.app_context(), pytest.raises(ValueError): + flask.redirect("other") + + +def test_abort_no_app(): + with pytest.raises(werkzeug.exceptions.Unauthorized): + flask.abort(401) + + with pytest.raises(LookupError): + flask.abort(900) + + +def test_app_aborter_class(): + class MyAborter(werkzeug.exceptions.Aborter): + pass + + class MyFlask(flask.Flask): + aborter_class = MyAborter + + app = MyFlask(__name__) + assert isinstance(app.aborter, MyAborter) + + +def test_abort_with_app(app): + class My900Error(werkzeug.exceptions.HTTPException): + code = 900 + + app.aborter.mapping[900] = My900Error + + with app.app_context(), pytest.raises(My900Error): + flask.abort(900) + + +class TestNoImports: + """Test Flasks are created without import. + + Avoiding ``__import__`` helps create Flask instances where there are errors + at import time. Those runtime errors will be apparent to the user soon + enough, but tools which build Flask instances meta-programmatically benefit + from a Flask which does not ``__import__``. Instead of importing to + retrieve file paths or metadata on a module or package, use the pkgutil and + imp modules in the Python standard library. + """ + + def test_name_with_import_error(self, modules_tmp_path): + (modules_tmp_path / "importerror.py").write_text("raise NotImplementedError()") + try: + flask.Flask("importerror") + except NotImplementedError: + AssertionError("Flask(import_name) is importing import_name.") + + +class TestStreaming: + def test_streaming_with_context(self, app, client): + @app.route("/") + def index(): + def generate(): + yield "Hello " + yield flask.request.args["name"] + yield "!" + + return flask.Response(flask.stream_with_context(generate())) + + rv = client.get("/?name=World") + assert rv.data == b"Hello World!" + + def test_streaming_with_context_as_decorator(self, app, client): + @app.route("/") + def index(): + @flask.stream_with_context + def generate(hello): + yield hello + yield flask.request.args["name"] + yield "!" + + return flask.Response(generate("Hello ")) + + rv = client.get("/?name=World") + assert rv.data == b"Hello World!" + + def test_streaming_with_context_and_custom_close(self, app, client): + called = [] + + class Wrapper: + def __init__(self, gen): + self._gen = gen + + def __iter__(self): + return self + + def close(self): + called.append(42) + + def __next__(self): + return next(self._gen) + + next = __next__ + + @app.route("/") + def index(): + def generate(): + yield "Hello " + yield flask.request.args["name"] + yield "!" + + return flask.Response(flask.stream_with_context(Wrapper(generate()))) + + rv = client.get("/?name=World") + assert rv.data == b"Hello World!" + assert called == [42] + + def test_stream_keeps_session(self, app, client): + @app.route("/") + def index(): + flask.session["test"] = "flask" + + @flask.stream_with_context + def gen(): + yield flask.session["test"] + + return flask.Response(gen()) + + rv = client.get("/") + assert rv.data == b"flask" + + +class TestHelpers: + @pytest.mark.parametrize( + ("debug", "expect"), + [ + ("", False), + ("0", False), + ("False", False), + ("No", False), + ("True", True), + ], + ) + def test_get_debug_flag(self, monkeypatch, debug, expect): + monkeypatch.setenv("FLASK_DEBUG", debug) + assert get_debug_flag() == expect + + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.helpers.make_response() + assert rv.status_code == 200 + assert rv.mimetype == "text/html" + + rv = flask.helpers.make_response("Hello") + assert rv.status_code == 200 + assert rv.data == b"Hello" + assert rv.mimetype == "text/html" + + @pytest.mark.parametrize("mode", ("r", "rb", "rt")) + def test_open_resource(self, mode): + app = flask.Flask(__name__) + + with app.open_resource("static/index.html", mode) as f: + assert "

Hello World!

" in str(f.read()) + + @pytest.mark.parametrize("mode", ("w", "x", "a", "r+")) + def test_open_resource_exceptions(self, mode): + app = flask.Flask(__name__) + + with pytest.raises(ValueError): + app.open_resource("static/index.html", mode) diff --git a/test/fixtures/whole_applications/flask/tests/test_instance_config.py b/test/fixtures/whole_applications/flask/tests/test_instance_config.py new file mode 100644 index 0000000..1918bd9 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_instance_config.py @@ -0,0 +1,111 @@ +import os + +import pytest + +import flask + + +def test_explicit_instance_paths(modules_tmp_path): + with pytest.raises(ValueError, match=".*must be absolute"): + flask.Flask(__name__, instance_path="instance") + + app = flask.Flask(__name__, instance_path=os.fspath(modules_tmp_path)) + assert app.instance_path == os.fspath(modules_tmp_path) + + +def test_uninstalled_module_paths(modules_tmp_path, purge_module): + (modules_tmp_path / "config_module_app.py").write_text( + "import os\n" + "import flask\n" + "here = os.path.abspath(os.path.dirname(__file__))\n" + "app = flask.Flask(__name__)\n" + ) + purge_module("config_module_app") + + from config_module_app import app + + assert app.instance_path == os.fspath(modules_tmp_path / "instance") + + +def test_uninstalled_package_paths(modules_tmp_path, purge_module): + app = modules_tmp_path / "config_package_app" + app.mkdir() + (app / "__init__.py").write_text( + "import os\n" + "import flask\n" + "here = os.path.abspath(os.path.dirname(__file__))\n" + "app = flask.Flask(__name__)\n" + ) + purge_module("config_package_app") + + from config_package_app import app + + assert app.instance_path == os.fspath(modules_tmp_path / "instance") + + +def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module): + def create_namespace(package): + project = tmp_path / f"project-{package}" + monkeypatch.syspath_prepend(os.fspath(project)) + ns = project / "namespace" / package + ns.mkdir(parents=True) + (ns / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") + return project + + _ = create_namespace("package1") + project2 = create_namespace("package2") + purge_module("namespace.package2") + purge_module("namespace") + + from namespace.package2 import app + + assert app.instance_path == os.fspath(project2 / "instance") + + +def test_installed_module_paths( + modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages, limit_loader +): + (site_packages / "site_app.py").write_text( + "import flask\napp = flask.Flask(__name__)\n" + ) + purge_module("site_app") + + from site_app import app + + assert app.instance_path == os.fspath( + modules_tmp_path / "var" / "site_app-instance" + ) + + +def test_installed_package_paths( + limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch +): + installed_path = modules_tmp_path / "path" + installed_path.mkdir() + monkeypatch.syspath_prepend(installed_path) + + app = installed_path / "installed_package" + app.mkdir() + (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") + purge_module("installed_package") + + from installed_package import app + + assert app.instance_path == os.fspath( + modules_tmp_path / "var" / "installed_package-instance" + ) + + +def test_prefix_package_paths( + limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages +): + app = site_packages / "site_package" + app.mkdir() + (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") + purge_module("site_package") + + import site_package + + assert site_package.app.instance_path == os.fspath( + modules_tmp_path / "var" / "site_package-instance" + ) diff --git a/test/fixtures/whole_applications/flask/tests/test_json.py b/test/fixtures/whole_applications/flask/tests/test_json.py new file mode 100644 index 0000000..1e2b27d --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_json.py @@ -0,0 +1,346 @@ +import datetime +import decimal +import io +import uuid + +import pytest +from werkzeug.http import http_date + +import flask +from flask import json +from flask.json.provider import DefaultJSONProvider + + +@pytest.mark.parametrize("debug", (True, False)) +def test_bad_request_debug_message(app, client, debug): + app.config["DEBUG"] = debug + app.config["TRAP_BAD_REQUEST_ERRORS"] = False + + @app.route("/json", methods=["POST"]) + def post_json(): + flask.request.get_json() + return None + + rv = client.post("/json", data=None, content_type="application/json") + assert rv.status_code == 400 + contains = b"Failed to decode JSON object" in rv.data + assert contains == debug + + +def test_json_bad_requests(app, client): + @app.route("/json", methods=["POST"]) + def return_json(): + return flask.jsonify(foo=str(flask.request.get_json())) + + rv = client.post("/json", data="malformed", content_type="application/json") + assert rv.status_code == 400 + + +def test_json_custom_mimetypes(app, client): + @app.route("/json", methods=["POST"]) + def return_json(): + return flask.request.get_json() + + rv = client.post("/json", data='"foo"', content_type="application/x+json") + assert rv.data == b"foo" + + +@pytest.mark.parametrize( + "test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')] +) +def test_json_as_unicode(test_value, expected, app, app_ctx): + app.json.ensure_ascii = test_value + rv = app.json.dumps("\N{SNOWMAN}") + assert rv == expected + + +def test_json_dump_to_file(app, app_ctx): + test_data = {"name": "Flask"} + out = io.StringIO() + + flask.json.dump(test_data, out) + out.seek(0) + rv = flask.json.load(out) + assert rv == test_data + + +@pytest.mark.parametrize( + "test_value", [0, -1, 1, 23, 3.14, "s", "longer string", True, False, None] +) +def test_jsonify_basic_types(test_value, app, client): + url = "/jsonify_basic_types" + app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x)) + rv = client.get(url) + assert rv.mimetype == "application/json" + assert flask.json.loads(rv.data) == test_value + + +def test_jsonify_dicts(app, client): + d = { + "a": 0, + "b": 23, + "c": 3.14, + "d": "t", + "e": "Hi", + "f": True, + "g": False, + "h": ["test list", 10, False], + "i": {"test": "dict"}, + } + + @app.route("/kw") + def return_kwargs(): + return flask.jsonify(**d) + + @app.route("/dict") + def return_dict(): + return flask.jsonify(d) + + for url in "/kw", "/dict": + rv = client.get(url) + assert rv.mimetype == "application/json" + assert flask.json.loads(rv.data) == d + + +def test_jsonify_arrays(app, client): + """Test jsonify of lists and args unpacking.""" + a_list = [ + 0, + 42, + 3.14, + "t", + "hello", + True, + False, + ["test list", 2, False], + {"test": "dict"}, + ] + + @app.route("/args_unpack") + def return_args_unpack(): + return flask.jsonify(*a_list) + + @app.route("/array") + def return_array(): + return flask.jsonify(a_list) + + for url in "/args_unpack", "/array": + rv = client.get(url) + assert rv.mimetype == "application/json" + assert flask.json.loads(rv.data) == a_list + + +@pytest.mark.parametrize( + "value", [datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5)] +) +def test_jsonify_datetime(app, client, value): + @app.route("/") + def index(): + return flask.jsonify(value=value) + + r = client.get() + assert r.json["value"] == http_date(value) + + +class FixedOffset(datetime.tzinfo): + """Fixed offset in hours east from UTC. + + This is a slight adaptation of the ``FixedOffset`` example found in + https://docs.python.org/2.7/library/datetime.html. + """ + + def __init__(self, hours, name): + self.__offset = datetime.timedelta(hours=hours) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return datetime.timedelta() + + +@pytest.mark.parametrize("tz", (("UTC", 0), ("PST", -8), ("KST", 9))) +def test_jsonify_aware_datetimes(tz): + """Test if aware datetime.datetime objects are converted into GMT.""" + tzinfo = FixedOffset(hours=tz[1], name=tz[0]) + dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) + gmt = FixedOffset(hours=0, name="GMT") + expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') + assert flask.json.dumps(dt) == expected + + +def test_jsonify_uuid_types(app, client): + """Test jsonify with uuid.UUID types""" + + test_uuid = uuid.UUID(bytes=b"\xde\xad\xbe\xef" * 4) + url = "/uuid_test" + app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid)) + + rv = client.get(url) + + rv_x = flask.json.loads(rv.data)["x"] + assert rv_x == str(test_uuid) + rv_uuid = uuid.UUID(rv_x) + assert rv_uuid == test_uuid + + +def test_json_decimal(): + rv = flask.json.dumps(decimal.Decimal("0.003")) + assert rv == '"0.003"' + + +def test_json_attr(app, client): + @app.route("/add", methods=["POST"]) + def add(): + json = flask.request.get_json() + return str(json["a"] + json["b"]) + + rv = client.post( + "/add", + data=flask.json.dumps({"a": 1, "b": 2}), + content_type="application/json", + ) + assert rv.data == b"3" + + +def test_tojson_filter(app, req_ctx): + # The tojson filter is tested in Jinja, this confirms that it's + # using Flask's dumps. + rv = flask.render_template_string( + "const data = {{ data|tojson }};", + data={"name": "", "time": datetime.datetime(2021, 2, 1, 7, 15)}, + ) + assert rv == ( + 'const data = {"name": "\\u003c/script\\u003e",' + ' "time": "Mon, 01 Feb 2021 07:15:00 GMT"};' + ) + + +def test_json_customization(app, client): + class X: # noqa: B903, for Python2 compatibility + def __init__(self, val): + self.val = val + + def default(o): + if isinstance(o, X): + return f"<{o.val}>" + + return DefaultJSONProvider.default(o) + + class CustomProvider(DefaultJSONProvider): + def object_hook(self, obj): + if len(obj) == 1 and "_foo" in obj: + return X(obj["_foo"]) + + return obj + + def loads(self, s, **kwargs): + kwargs.setdefault("object_hook", self.object_hook) + return super().loads(s, **kwargs) + + app.json = CustomProvider(app) + app.json.default = default + + @app.route("/", methods=["POST"]) + def index(): + return flask.json.dumps(flask.request.get_json()["x"]) + + rv = client.post( + "/", + data=flask.json.dumps({"x": {"_foo": 42}}), + content_type="application/json", + ) + assert rv.data == b'"<42>"' + + +def _has_encoding(name): + try: + import codecs + + codecs.lookup(name) + return True + except LookupError: + return False + + +def test_json_key_sorting(app, client): + app.debug = True + assert app.json.sort_keys + d = dict.fromkeys(range(20), "foo") + + @app.route("/") + def index(): + return flask.jsonify(values=d) + + rv = client.get("/") + lines = [x.strip() for x in rv.data.strip().decode("utf-8").splitlines()] + sorted_by_str = [ + "{", + '"values": {', + '"0": "foo",', + '"1": "foo",', + '"10": "foo",', + '"11": "foo",', + '"12": "foo",', + '"13": "foo",', + '"14": "foo",', + '"15": "foo",', + '"16": "foo",', + '"17": "foo",', + '"18": "foo",', + '"19": "foo",', + '"2": "foo",', + '"3": "foo",', + '"4": "foo",', + '"5": "foo",', + '"6": "foo",', + '"7": "foo",', + '"8": "foo",', + '"9": "foo"', + "}", + "}", + ] + sorted_by_int = [ + "{", + '"values": {', + '"0": "foo",', + '"1": "foo",', + '"2": "foo",', + '"3": "foo",', + '"4": "foo",', + '"5": "foo",', + '"6": "foo",', + '"7": "foo",', + '"8": "foo",', + '"9": "foo",', + '"10": "foo",', + '"11": "foo",', + '"12": "foo",', + '"13": "foo",', + '"14": "foo",', + '"15": "foo",', + '"16": "foo",', + '"17": "foo",', + '"18": "foo",', + '"19": "foo"', + "}", + "}", + ] + + try: + assert lines == sorted_by_int + except AssertionError: + assert lines == sorted_by_str + + +def test_html_method(): + class ObjectWithHTML: + def __html__(self): + return "

test

" + + result = json.dumps(ObjectWithHTML()) + assert result == '"

test

"' diff --git a/test/fixtures/whole_applications/flask/tests/test_json_tag.py b/test/fixtures/whole_applications/flask/tests/test_json_tag.py new file mode 100644 index 0000000..677160a --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_json_tag.py @@ -0,0 +1,86 @@ +from datetime import datetime +from datetime import timezone +from uuid import uuid4 + +import pytest +from markupsafe import Markup + +from flask.json.tag import JSONTag +from flask.json.tag import TaggedJSONSerializer + + +@pytest.mark.parametrize( + "data", + ( + {" t": (1, 2, 3)}, + {" t__": b"a"}, + {" di": " di"}, + {"x": (1, 2, 3), "y": 4}, + (1, 2, 3), + [(1, 2, 3)], + b"\xff", + Markup(""), + uuid4(), + datetime.now(tz=timezone.utc).replace(microsecond=0), + ), +) +def test_dump_load_unchanged(data): + s = TaggedJSONSerializer() + assert s.loads(s.dumps(data)) == data + + +def test_duplicate_tag(): + class TagDict(JSONTag): + key = " d" + + s = TaggedJSONSerializer() + pytest.raises(KeyError, s.register, TagDict) + s.register(TagDict, force=True, index=0) + assert isinstance(s.tags[" d"], TagDict) + assert isinstance(s.order[0], TagDict) + + +def test_custom_tag(): + class Foo: # noqa: B903, for Python2 compatibility + def __init__(self, data): + self.data = data + + class TagFoo(JSONTag): + __slots__ = () + key = " f" + + def check(self, value): + return isinstance(value, Foo) + + def to_json(self, value): + return self.serializer.tag(value.data) + + def to_python(self, value): + return Foo(value) + + s = TaggedJSONSerializer() + s.register(TagFoo) + assert s.loads(s.dumps(Foo("bar"))).data == "bar" + + +def test_tag_interface(): + t = JSONTag(None) + pytest.raises(NotImplementedError, t.check, None) + pytest.raises(NotImplementedError, t.to_json, None) + pytest.raises(NotImplementedError, t.to_python, None) + + +def test_tag_order(): + class Tag1(JSONTag): + key = " 1" + + class Tag2(JSONTag): + key = " 2" + + s = TaggedJSONSerializer() + + s.register(Tag1, index=-1) + assert isinstance(s.order[-2], Tag1) + + s.register(Tag2, index=None) + assert isinstance(s.order[-1], Tag2) diff --git a/test/fixtures/whole_applications/flask/tests/test_logging.py b/test/fixtures/whole_applications/flask/tests/test_logging.py new file mode 100644 index 0000000..a5f0463 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_logging.py @@ -0,0 +1,98 @@ +import logging +import sys +from io import StringIO + +import pytest + +from flask.logging import default_handler +from flask.logging import has_level_handler +from flask.logging import wsgi_errors_stream + + +@pytest.fixture(autouse=True) +def reset_logging(pytestconfig): + root_handlers = logging.root.handlers[:] + logging.root.handlers = [] + root_level = logging.root.level + + logger = logging.getLogger("flask_test") + logger.handlers = [] + logger.setLevel(logging.NOTSET) + + logging_plugin = pytestconfig.pluginmanager.unregister(name="logging-plugin") + + yield + + logging.root.handlers[:] = root_handlers + logging.root.setLevel(root_level) + + logger.handlers = [] + logger.setLevel(logging.NOTSET) + + if logging_plugin: + pytestconfig.pluginmanager.register(logging_plugin, "logging-plugin") + + +def test_logger(app): + assert app.logger.name == "flask_test" + assert app.logger.level == logging.NOTSET + assert app.logger.handlers == [default_handler] + + +def test_logger_debug(app): + app.debug = True + assert app.logger.level == logging.DEBUG + assert app.logger.handlers == [default_handler] + + +def test_existing_handler(app): + logging.root.addHandler(logging.StreamHandler()) + assert app.logger.level == logging.NOTSET + assert not app.logger.handlers + + +def test_wsgi_errors_stream(app, client): + @app.route("/") + def index(): + app.logger.error("test") + return "" + + stream = StringIO() + client.get("/", errors_stream=stream) + assert "ERROR in test_logging: test" in stream.getvalue() + + assert wsgi_errors_stream._get_current_object() is sys.stderr + + with app.test_request_context(errors_stream=stream): + assert wsgi_errors_stream._get_current_object() is stream + + +def test_has_level_handler(): + logger = logging.getLogger("flask.app") + assert not has_level_handler(logger) + + handler = logging.StreamHandler() + logging.root.addHandler(handler) + assert has_level_handler(logger) + + logger.propagate = False + assert not has_level_handler(logger) + logger.propagate = True + + handler.setLevel(logging.ERROR) + assert not has_level_handler(logger) + + +def test_log_view_exception(app, client): + @app.route("/") + def index(): + raise Exception("test") + + app.testing = False + stream = StringIO() + rv = client.get("/", errors_stream=stream) + assert rv.status_code == 500 + assert rv.data + err = stream.getvalue() + assert "Exception on / [GET]" in err + assert "Exception: test" in err diff --git a/test/fixtures/whole_applications/flask/tests/test_regression.py b/test/fixtures/whole_applications/flask/tests/test_regression.py new file mode 100644 index 0000000..0ddcf97 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_regression.py @@ -0,0 +1,30 @@ +import flask + + +def test_aborting(app): + class Foo(Exception): + whatever = 42 + + @app.errorhandler(Foo) + def handle_foo(e): + return str(e.whatever) + + @app.route("/") + def index(): + raise flask.abort(flask.redirect(flask.url_for("test"))) + + @app.route("/test") + def test(): + raise Foo() + + with app.test_client() as c: + rv = c.get("/") + location_parts = rv.headers["Location"].rpartition("/") + + if location_parts[0]: + # For older Werkzeug that used absolute redirects. + assert location_parts[0] == "http://localhost" + + assert location_parts[2] == "test" + rv = c.get("/test") + assert rv.data == b"42" diff --git a/test/fixtures/whole_applications/flask/tests/test_reqctx.py b/test/fixtures/whole_applications/flask/tests/test_reqctx.py new file mode 100644 index 0000000..6c38b66 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_reqctx.py @@ -0,0 +1,325 @@ +import warnings + +import pytest + +import flask +from flask.globals import request_ctx +from flask.sessions import SecureCookieSessionInterface +from flask.sessions import SessionInterface + +try: + from greenlet import greenlet +except ImportError: + greenlet = None + + +def test_teardown_on_pop(app): + buffer = [] + + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + assert buffer == [] + ctx.pop() + assert buffer == [None] + + +def test_teardown_with_previous_exception(app): + buffer = [] + + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + try: + raise Exception("dummy") + except Exception: + pass + + with app.test_request_context(): + assert buffer == [] + assert buffer == [None] + + +def test_teardown_with_handled_exception(app): + buffer = [] + + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + with app.test_request_context(): + assert buffer == [] + try: + raise Exception("dummy") + except Exception: + pass + assert buffer == [None] + + +def test_proper_test_request_context(app): + app.config.update(SERVER_NAME="localhost.localdomain:5000") + + @app.route("/") + def index(): + return None + + @app.route("/", subdomain="foo") + def sub(): + return None + + with app.test_request_context("/"): + assert ( + flask.url_for("index", _external=True) + == "http://localhost.localdomain:5000/" + ) + + with app.test_request_context("/"): + assert ( + flask.url_for("sub", _external=True) + == "http://foo.localhost.localdomain:5000/" + ) + + # suppress Werkzeug 0.15 warning about name mismatch + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Current server name", UserWarning, "flask.app" + ) + with app.test_request_context( + "/", environ_overrides={"HTTP_HOST": "localhost"} + ): + pass + + app.config.update(SERVER_NAME="localhost") + with app.test_request_context("/", environ_overrides={"SERVER_NAME": "localhost"}): + pass + + app.config.update(SERVER_NAME="localhost:80") + with app.test_request_context( + "/", environ_overrides={"SERVER_NAME": "localhost:80"} + ): + pass + + +def test_context_binding(app): + @app.route("/") + def index(): + return f"Hello {flask.request.args['name']}!" + + @app.route("/meh") + def meh(): + return flask.request.url + + with app.test_request_context("/?name=World"): + assert index() == "Hello World!" + with app.test_request_context("/meh"): + assert meh() == "http://localhost/meh" + assert not flask.request + + +def test_context_test(app): + assert not flask.request + assert not flask.has_request_context() + ctx = app.test_request_context() + ctx.push() + try: + assert flask.request + assert flask.has_request_context() + finally: + ctx.pop() + + +def test_manual_context_binding(app): + @app.route("/") + def index(): + return f"Hello {flask.request.args['name']}!" + + ctx = app.test_request_context("/?name=World") + ctx.push() + assert index() == "Hello World!" + ctx.pop() + with pytest.raises(RuntimeError): + index() + + +@pytest.mark.skipif(greenlet is None, reason="greenlet not installed") +class TestGreenletContextCopying: + def test_greenlet_context_copying(self, app, client): + greenlets = [] + + @app.route("/") + def index(): + flask.session["fizz"] = "buzz" + reqctx = request_ctx.copy() + + def g(): + assert not flask.request + assert not flask.current_app + with reqctx: + assert flask.request + assert flask.current_app == app + assert flask.request.path == "/" + assert flask.request.args["foo"] == "bar" + assert flask.session.get("fizz") == "buzz" + assert not flask.request + return 42 + + greenlets.append(greenlet(g)) + return "Hello World!" + + rv = client.get("/?foo=bar") + assert rv.data == b"Hello World!" + + result = greenlets[0].run() + assert result == 42 + + def test_greenlet_context_copying_api(self, app, client): + greenlets = [] + + @app.route("/") + def index(): + flask.session["fizz"] = "buzz" + + @flask.copy_current_request_context + def g(): + assert flask.request + assert flask.current_app == app + assert flask.request.path == "/" + assert flask.request.args["foo"] == "bar" + assert flask.session.get("fizz") == "buzz" + return 42 + + greenlets.append(greenlet(g)) + return "Hello World!" + + rv = client.get("/?foo=bar") + assert rv.data == b"Hello World!" + + result = greenlets[0].run() + assert result == 42 + + +def test_session_error_pops_context(): + class SessionError(Exception): + pass + + class FailingSessionInterface(SessionInterface): + def open_session(self, app, request): + raise SessionError() + + class CustomFlask(flask.Flask): + session_interface = FailingSessionInterface() + + app = CustomFlask(__name__) + + @app.route("/") + def index(): + # shouldn't get here + AssertionError() + + response = app.test_client().get("/") + assert response.status_code == 500 + assert not flask.request + assert not flask.current_app + + +def test_session_dynamic_cookie_name(): + # This session interface will use a cookie with a different name if the + # requested url ends with the string "dynamic_cookie" + class PathAwareSessionInterface(SecureCookieSessionInterface): + def get_cookie_name(self, app): + if flask.request.url.endswith("dynamic_cookie"): + return "dynamic_cookie_name" + else: + return super().get_cookie_name(app) + + class CustomFlask(flask.Flask): + session_interface = PathAwareSessionInterface() + + app = CustomFlask(__name__) + app.secret_key = "secret_key" + + @app.route("/set", methods=["POST"]) + def set(): + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get") + def get(): + v = flask.session.get("value", "None") + return v + + @app.route("/set_dynamic_cookie", methods=["POST"]) + def set_dynamic_cookie(): + flask.session["value"] = flask.request.form["value"] + return "value set" + + @app.route("/get_dynamic_cookie") + def get_dynamic_cookie(): + v = flask.session.get("value", "None") + return v + + test_client = app.test_client() + + # first set the cookie in both /set urls but each with a different value + assert test_client.post("/set", data={"value": "42"}).data == b"value set" + assert ( + test_client.post("/set_dynamic_cookie", data={"value": "616"}).data + == b"value set" + ) + + # now check that the relevant values come back - meaning that different + # cookies are being used for the urls that end with "dynamic cookie" + assert test_client.get("/get").data == b"42" + assert test_client.get("/get_dynamic_cookie").data == b"616" + + +def test_bad_environ_raises_bad_request(): + app = flask.Flask(__name__) + + from flask.testing import EnvironBuilder + + builder = EnvironBuilder(app) + environ = builder.get_environ() + + # use a non-printable character in the Host - this is key to this test + environ["HTTP_HOST"] = "\x8a" + + with app.request_context(environ): + response = app.full_dispatch_request() + assert response.status_code == 400 + + +def test_environ_for_valid_idna_completes(): + app = flask.Flask(__name__) + + @app.route("/") + def index(): + return "Hello World!" + + from flask.testing import EnvironBuilder + + builder = EnvironBuilder(app) + environ = builder.get_environ() + + # these characters are all IDNA-compatible + environ["HTTP_HOST"] = "ąśźäüжŠßя.com" + + with app.request_context(environ): + response = app.full_dispatch_request() + + assert response.status_code == 200 + + +def test_normal_environ_completes(): + app = flask.Flask(__name__) + + @app.route("/") + def index(): + return "Hello World!" + + response = app.test_client().get("/", headers={"host": "xn--on-0ia.com"}) + assert response.status_code == 200 diff --git a/test/fixtures/whole_applications/flask/tests/test_session_interface.py b/test/fixtures/whole_applications/flask/tests/test_session_interface.py new file mode 100644 index 0000000..613da37 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_session_interface.py @@ -0,0 +1,28 @@ +import flask +from flask.globals import request_ctx +from flask.sessions import SessionInterface + + +def test_open_session_with_endpoint(): + """If request.endpoint (or other URL matching behavior) is needed + while loading the session, RequestContext.match_request() can be + called manually. + """ + + class MySessionInterface(SessionInterface): + def save_session(self, app, session, response): + pass + + def open_session(self, app, request): + request_ctx.match_request() + assert request.endpoint is not None + + app = flask.Flask(__name__) + app.session_interface = MySessionInterface() + + @app.get("/") + def index(): + return "Hello, World!" + + response = app.test_client().get("/") + assert response.status_code == 200 diff --git a/test/fixtures/whole_applications/flask/tests/test_signals.py b/test/fixtures/whole_applications/flask/tests/test_signals.py new file mode 100644 index 0000000..32ab333 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_signals.py @@ -0,0 +1,181 @@ +import flask + + +def test_template_rendered(app, client): + @app.route("/") + def index(): + return flask.render_template("simple_template.html", whiskey=42) + + recorded = [] + + def record(sender, template, context): + recorded.append((template, context)) + + flask.template_rendered.connect(record, app) + try: + client.get("/") + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == "simple_template.html" + assert context["whiskey"] == 42 + finally: + flask.template_rendered.disconnect(record, app) + + +def test_before_render_template(): + app = flask.Flask(__name__) + + @app.route("/") + def index(): + return flask.render_template("simple_template.html", whiskey=42) + + recorded = [] + + def record(sender, template, context): + context["whiskey"] = 43 + recorded.append((template, context)) + + flask.before_render_template.connect(record, app) + try: + rv = app.test_client().get("/") + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == "simple_template.html" + assert context["whiskey"] == 43 + assert rv.data == b"

43

" + finally: + flask.before_render_template.disconnect(record, app) + + +def test_request_signals(): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append("before-signal") + + def after_request_signal(sender, response): + assert response.data == b"stuff" + calls.append("after-signal") + + @app.before_request + def before_request_handler(): + calls.append("before-handler") + + @app.after_request + def after_request_handler(response): + calls.append("after-handler") + response.data = "stuff" + return response + + @app.route("/") + def index(): + calls.append("handler") + return "ignored anyway" + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get("/") + assert rv.data == b"stuff" + + assert calls == [ + "before-signal", + "before-handler", + "handler", + "after-handler", + "after-signal", + ] + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + +def test_request_exception_signal(): + app = flask.Flask(__name__) + recorded = [] + + @app.route("/") + def index(): + raise ZeroDivisionError + + def record(sender, exception): + recorded.append(exception) + + flask.got_request_exception.connect(record, app) + try: + assert app.test_client().get("/").status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + finally: + flask.got_request_exception.disconnect(record, app) + + +def test_appcontext_signals(app, client): + recorded = [] + + def record_push(sender, **kwargs): + recorded.append("push") + + def record_pop(sender, **kwargs): + recorded.append("pop") + + @app.route("/") + def index(): + return "Hello" + + flask.appcontext_pushed.connect(record_push, app) + flask.appcontext_popped.connect(record_pop, app) + try: + rv = client.get("/") + assert rv.data == b"Hello" + assert recorded == ["push", "pop"] + finally: + flask.appcontext_pushed.disconnect(record_push, app) + flask.appcontext_popped.disconnect(record_pop, app) + + +def test_flash_signal(app): + @app.route("/") + def index(): + flask.flash("This is a flash message", category="notice") + return flask.redirect("/other") + + recorded = [] + + def record(sender, message, category): + recorded.append((message, category)) + + flask.message_flashed.connect(record, app) + try: + client = app.test_client() + with client.session_transaction(): + client.get("/") + assert len(recorded) == 1 + message, category = recorded[0] + assert message == "This is a flash message" + assert category == "notice" + finally: + flask.message_flashed.disconnect(record, app) + + +def test_appcontext_tearing_down_signal(app, client): + app.testing = False + recorded = [] + + def record_teardown(sender, exc): + recorded.append(exc) + + @app.route("/") + def index(): + raise ZeroDivisionError + + flask.appcontext_tearing_down.connect(record_teardown, app) + try: + rv = client.get("/") + assert rv.status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + finally: + flask.appcontext_tearing_down.disconnect(record_teardown, app) diff --git a/test/fixtures/whole_applications/flask/tests/test_subclassing.py b/test/fixtures/whole_applications/flask/tests/test_subclassing.py new file mode 100644 index 0000000..087c50d --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_subclassing.py @@ -0,0 +1,21 @@ +from io import StringIO + +import flask + + +def test_suppressed_exception_logging(): + class SuppressedFlask(flask.Flask): + def log_exception(self, exc_info): + pass + + out = StringIO() + app = SuppressedFlask(__name__) + + @app.route("/") + def index(): + raise Exception("test") + + rv = app.test_client().get("/", errors_stream=out) + assert rv.status_code == 500 + assert b"Internal Server Error" in rv.data + assert not out.getvalue() diff --git a/test/fixtures/whole_applications/flask/tests/test_templating.py b/test/fixtures/whole_applications/flask/tests/test_templating.py new file mode 100644 index 0000000..c9fb375 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_templating.py @@ -0,0 +1,451 @@ +import logging + +import pytest +import werkzeug.serving +from jinja2 import TemplateNotFound +from markupsafe import Markup + +import flask + + +def test_context_processing(app, client): + @app.context_processor + def context_processor(): + return {"injected_value": 42} + + @app.route("/") + def index(): + return flask.render_template("context_template.html", value=23) + + rv = client.get("/") + assert rv.data == b"

23|42" + + +def test_original_win(app, client): + @app.route("/") + def index(): + return flask.render_template_string("{{ config }}", config=42) + + rv = client.get("/") + assert rv.data == b"42" + + +def test_simple_stream(app, client): + @app.route("/") + def index(): + return flask.stream_template_string("{{ config }}", config=42) + + rv = client.get("/") + assert rv.data == b"42" + + +def test_request_less_rendering(app, app_ctx): + app.config["WORLD_NAME"] = "Special World" + + @app.context_processor + def context_processor(): + return dict(foo=42) + + rv = flask.render_template_string("Hello {{ config.WORLD_NAME }} {{ foo }}") + assert rv == "Hello Special World 42" + + +def test_standard_context(app, client): + @app.route("/") + def index(): + flask.g.foo = 23 + flask.session["test"] = "aha" + return flask.render_template_string( + """ + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + """ + ) + + rv = client.get("/?foo=42") + assert rv.data.split() == [b"42", b"23", b"False", b"aha"] + + +def test_escaping(app, client): + text = "

Hello World!" + + @app.route("/") + def index(): + return flask.render_template( + "escaping_template.html", text=text, html=Markup(text) + ) + + lines = client.get("/").data.splitlines() + assert lines == [ + b"<p>Hello World!", + b"

Hello World!", + b"

Hello World!", + b"

Hello World!", + b"<p>Hello World!", + b"

Hello World!", + ] + + +def test_no_escaping(app, client): + text = "

Hello World!" + + @app.route("/") + def index(): + return flask.render_template( + "non_escaping_template.txt", text=text, html=Markup(text) + ) + + lines = client.get("/").data.splitlines() + assert lines == [ + b"

Hello World!", + b"

Hello World!", + b"

Hello World!", + b"

Hello World!", + b"<p>Hello World!", + b"

Hello World!", + b"

Hello World!", + b"

Hello World!", + ] + + +def test_escaping_without_template_filename(app, client, req_ctx): + assert flask.render_template_string("{{ foo }}", foo="") == "<test>" + assert flask.render_template("mail.txt", foo="") == " Mail" + + +def test_macros(app, req_ctx): + macro = flask.get_template_attribute("_macro.html", "hello") + assert macro("World") == "Hello World!" + + +def test_template_filter(app): + @app.template_filter() + def my_reverse(s): + return s[::-1] + + assert "my_reverse" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse"] == my_reverse + assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + + +def test_add_template_filter(app): + def my_reverse(s): + return s[::-1] + + app.add_template_filter(my_reverse) + assert "my_reverse" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["my_reverse"] == my_reverse + assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" + + +def test_template_filter_with_name(app): + @app.template_filter("strrev") + def my_reverse(s): + return s[::-1] + + assert "strrev" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["strrev"] == my_reverse + assert app.jinja_env.filters["strrev"]("abcd") == "dcba" + + +def test_add_template_filter_with_name(app): + def my_reverse(s): + return s[::-1] + + app.add_template_filter(my_reverse, "strrev") + assert "strrev" in app.jinja_env.filters.keys() + assert app.jinja_env.filters["strrev"] == my_reverse + assert app.jinja_env.filters["strrev"]("abcd") == "dcba" + + +def test_template_filter_with_template(app, client): + @app.template_filter() + def super_reverse(s): + return s[::-1] + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_add_template_filter_with_template(app, client): + def super_reverse(s): + return s[::-1] + + app.add_template_filter(super_reverse) + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_template_filter_with_name_and_template(app, client): + @app.template_filter("super_reverse") + def my_reverse(s): + return s[::-1] + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_add_template_filter_with_name_and_template(app, client): + def my_reverse(s): + return s[::-1] + + app.add_template_filter(my_reverse, "super_reverse") + + @app.route("/") + def index(): + return flask.render_template("template_filter.html", value="abcd") + + rv = client.get("/") + assert rv.data == b"dcba" + + +def test_template_test(app): + @app.template_test() + def boolean(value): + return isinstance(value, bool) + + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_add_template_test(app): + def boolean(value): + return isinstance(value, bool) + + app.add_template_test(boolean) + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_template_test_with_name(app): + @app.template_test("boolean") + def is_boolean(value): + return isinstance(value, bool) + + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == is_boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_add_template_test_with_name(app): + def is_boolean(value): + return isinstance(value, bool) + + app.add_template_test(is_boolean, "boolean") + assert "boolean" in app.jinja_env.tests.keys() + assert app.jinja_env.tests["boolean"] == is_boolean + assert app.jinja_env.tests["boolean"](False) + + +def test_template_test_with_template(app, client): + @app.template_test() + def boolean(value): + return isinstance(value, bool) + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_add_template_test_with_template(app, client): + def boolean(value): + return isinstance(value, bool) + + app.add_template_test(boolean) + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_template_test_with_name_and_template(app, client): + @app.template_test("boolean") + def is_boolean(value): + return isinstance(value, bool) + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_add_template_test_with_name_and_template(app, client): + def is_boolean(value): + return isinstance(value, bool) + + app.add_template_test(is_boolean, "boolean") + + @app.route("/") + def index(): + return flask.render_template("template_test.html", value=False) + + rv = client.get("/") + assert b"Success!" in rv.data + + +def test_add_template_global(app, app_ctx): + @app.template_global() + def get_stuff(): + return 42 + + assert "get_stuff" in app.jinja_env.globals.keys() + assert app.jinja_env.globals["get_stuff"] == get_stuff + assert app.jinja_env.globals["get_stuff"](), 42 + + rv = flask.render_template_string("{{ get_stuff() }}") + assert rv == "42" + + +def test_custom_template_loader(client): + class MyFlask(flask.Flask): + def create_global_jinja_loader(self): + from jinja2 import DictLoader + + return DictLoader({"index.html": "Hello Custom World!"}) + + app = MyFlask(__name__) + + @app.route("/") + def index(): + return flask.render_template("index.html") + + c = app.test_client() + rv = c.get("/") + assert rv.data == b"Hello Custom World!" + + +def test_iterable_loader(app, client): + @app.context_processor + def context_processor(): + return {"whiskey": "Jameson"} + + @app.route("/") + def index(): + return flask.render_template( + [ + "no_template.xml", # should skip this one + "simple_template.html", # should render this + "context_template.html", + ], + value=23, + ) + + rv = client.get("/") + assert rv.data == b"

Jameson

" + + +def test_templates_auto_reload(app): + # debug is False, config option is None + assert app.debug is False + assert app.config["TEMPLATES_AUTO_RELOAD"] is None + assert app.jinja_env.auto_reload is False + # debug is False, config option is False + app = flask.Flask(__name__) + app.config["TEMPLATES_AUTO_RELOAD"] = False + assert app.debug is False + assert app.jinja_env.auto_reload is False + # debug is False, config option is True + app = flask.Flask(__name__) + app.config["TEMPLATES_AUTO_RELOAD"] = True + assert app.debug is False + assert app.jinja_env.auto_reload is True + # debug is True, config option is None + app = flask.Flask(__name__) + app.config["DEBUG"] = True + assert app.config["TEMPLATES_AUTO_RELOAD"] is None + assert app.jinja_env.auto_reload is True + # debug is True, config option is False + app = flask.Flask(__name__) + app.config["DEBUG"] = True + app.config["TEMPLATES_AUTO_RELOAD"] = False + assert app.jinja_env.auto_reload is False + # debug is True, config option is True + app = flask.Flask(__name__) + app.config["DEBUG"] = True + app.config["TEMPLATES_AUTO_RELOAD"] = True + assert app.jinja_env.auto_reload is True + + +def test_templates_auto_reload_debug_run(app, monkeypatch): + def run_simple_mock(*args, **kwargs): + pass + + monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) + + app.run() + assert not app.jinja_env.auto_reload + + app.run(debug=True) + assert app.jinja_env.auto_reload + + +def test_template_loader_debugging(test_apps, monkeypatch): + from blueprintapp import app + + called = [] + + class _TestHandler(logging.Handler): + def handle(self, record): + called.append(True) + text = str(record.msg) + assert "1: trying loader of application 'blueprintapp'" in text + assert ( + "2: trying loader of blueprint 'admin' (blueprintapp.apps.admin)" + ) in text + assert ( + "trying loader of blueprint 'frontend' (blueprintapp.apps.frontend)" + ) in text + assert "Error: the template could not be found" in text + assert ( + "looked up from an endpoint that belongs to the blueprint 'frontend'" + ) in text + assert "See https://flask.palletsprojects.com/blueprints/#templates" in text + + with app.test_client() as c: + monkeypatch.setitem(app.config, "EXPLAIN_TEMPLATE_LOADING", True) + monkeypatch.setattr( + logging.getLogger("blueprintapp"), "handlers", [_TestHandler()] + ) + + with pytest.raises(TemplateNotFound) as excinfo: + c.get("/missing") + + assert "missing_template.html" in str(excinfo.value) + + assert len(called) == 1 + + +def test_custom_jinja_env(): + class CustomEnvironment(flask.templating.Environment): + pass + + class CustomFlask(flask.Flask): + jinja_environment = CustomEnvironment + + app = CustomFlask(__name__) + assert isinstance(app.jinja_env, CustomEnvironment) diff --git a/test/fixtures/whole_applications/flask/tests/test_testing.py b/test/fixtures/whole_applications/flask/tests/test_testing.py new file mode 100644 index 0000000..de05215 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_testing.py @@ -0,0 +1,396 @@ +import importlib.metadata + +import click +import pytest + +import flask +from flask import appcontext_popped +from flask.cli import ScriptInfo +from flask.globals import _cv_request +from flask.json import jsonify +from flask.testing import EnvironBuilder +from flask.testing import FlaskCliRunner + + +def test_environ_defaults_from_config(app, client): + app.config["SERVER_NAME"] = "example.com:1234" + app.config["APPLICATION_ROOT"] = "/foo" + + @app.route("/") + def index(): + return flask.request.url + + ctx = app.test_request_context() + assert ctx.request.url == "http://example.com:1234/foo/" + + rv = client.get("/") + assert rv.data == b"http://example.com:1234/foo/" + + +def test_environ_defaults(app, client, app_ctx, req_ctx): + @app.route("/") + def index(): + return flask.request.url + + ctx = app.test_request_context() + assert ctx.request.url == "http://localhost/" + with client: + rv = client.get("/") + assert rv.data == b"http://localhost/" + + +def test_environ_base_default(app, client): + @app.route("/") + def index(): + flask.g.remote_addr = flask.request.remote_addr + flask.g.user_agent = flask.request.user_agent.string + return "" + + with client: + client.get("/") + assert flask.g.remote_addr == "127.0.0.1" + assert flask.g.user_agent == ( + f"Werkzeug/{importlib.metadata.version('werkzeug')}" + ) + + +def test_environ_base_modified(app, client): + @app.route("/") + def index(): + flask.g.remote_addr = flask.request.remote_addr + flask.g.user_agent = flask.request.user_agent.string + return "" + + client.environ_base["REMOTE_ADDR"] = "192.168.0.22" + client.environ_base["HTTP_USER_AGENT"] = "Foo" + + with client: + client.get("/") + assert flask.g.remote_addr == "192.168.0.22" + assert flask.g.user_agent == "Foo" + + +def test_client_open_environ(app, client, request): + @app.route("/index") + def index(): + return flask.request.remote_addr + + builder = EnvironBuilder(app, path="/index", method="GET") + request.addfinalizer(builder.close) + + rv = client.open(builder) + assert rv.data == b"127.0.0.1" + + environ = builder.get_environ() + client.environ_base["REMOTE_ADDR"] = "127.0.0.2" + rv = client.open(environ) + assert rv.data == b"127.0.0.2" + + +def test_specify_url_scheme(app, client): + @app.route("/") + def index(): + return flask.request.url + + ctx = app.test_request_context(url_scheme="https") + assert ctx.request.url == "https://localhost/" + + rv = client.get("/", url_scheme="https") + assert rv.data == b"https://localhost/" + + +def test_path_is_url(app): + eb = EnvironBuilder(app, "https://example.com/") + assert eb.url_scheme == "https" + assert eb.host == "example.com" + assert eb.script_root == "" + assert eb.path == "/" + + +def test_environbuilder_json_dumps(app): + """EnvironBuilder.json_dumps() takes settings from the app.""" + app.json.ensure_ascii = False + eb = EnvironBuilder(app, json="\u20ac") + assert eb.input_stream.read().decode("utf8") == '"\u20ac"' + + +def test_blueprint_with_subdomain(): + app = flask.Flask(__name__, subdomain_matching=True) + app.config["SERVER_NAME"] = "example.com:1234" + app.config["APPLICATION_ROOT"] = "/foo" + client = app.test_client() + + bp = flask.Blueprint("company", __name__, subdomain="xxx") + + @bp.route("/") + def index(): + return flask.request.url + + app.register_blueprint(bp) + + ctx = app.test_request_context("/", subdomain="xxx") + assert ctx.request.url == "http://xxx.example.com:1234/foo/" + + with ctx: + assert ctx.request.blueprint == bp.name + + rv = client.get("/", subdomain="xxx") + assert rv.data == b"http://xxx.example.com:1234/foo/" + + +def test_redirect_keep_session(app, client, app_ctx): + @app.route("/", methods=["GET", "POST"]) + def index(): + if flask.request.method == "POST": + return flask.redirect("/getsession") + flask.session["data"] = "foo" + return "index" + + @app.route("/getsession") + def get_session(): + return flask.session.get("data", "") + + with client: + rv = client.get("/getsession") + assert rv.data == b"" + + rv = client.get("/") + assert rv.data == b"index" + assert flask.session.get("data") == "foo" + + rv = client.post("/", data={}, follow_redirects=True) + assert rv.data == b"foo" + assert flask.session.get("data") == "foo" + + rv = client.get("/getsession") + assert rv.data == b"foo" + + +def test_session_transactions(app, client): + @app.route("/") + def index(): + return str(flask.session["foo"]) + + with client: + with client.session_transaction() as sess: + assert len(sess) == 0 + sess["foo"] = [42] + assert len(sess) == 1 + rv = client.get("/") + assert rv.data == b"[42]" + with client.session_transaction() as sess: + assert len(sess) == 1 + assert sess["foo"] == [42] + + +def test_session_transactions_no_null_sessions(): + app = flask.Flask(__name__) + + with app.test_client() as c: + with pytest.raises(RuntimeError) as e: + with c.session_transaction(): + pass + assert "Session backend did not open a session" in str(e.value) + + +def test_session_transactions_keep_context(app, client, req_ctx): + client.get("/") + req = flask.request._get_current_object() + assert req is not None + with client.session_transaction(): + assert req is flask.request._get_current_object() + + +def test_session_transaction_needs_cookies(app): + c = app.test_client(use_cookies=False) + + with pytest.raises(TypeError, match="Cookies are disabled."): + with c.session_transaction(): + pass + + +def test_test_client_context_binding(app, client): + app.testing = False + + @app.route("/") + def index(): + flask.g.value = 42 + return "Hello World!" + + @app.route("/other") + def other(): + raise ZeroDivisionError + + with client: + resp = client.get("/") + assert flask.g.value == 42 + assert resp.data == b"Hello World!" + assert resp.status_code == 200 + + with client: + resp = client.get("/other") + assert not hasattr(flask.g, "value") + assert b"Internal Server Error" in resp.data + assert resp.status_code == 500 + flask.g.value = 23 + + with pytest.raises(RuntimeError): + flask.g.value # noqa: B018 + + +def test_reuse_client(client): + c = client + + with c: + assert client.get("/").status_code == 404 + + with c: + assert client.get("/").status_code == 404 + + +def test_full_url_request(app, client): + @app.route("/action", methods=["POST"]) + def action(): + return "x" + + with client: + rv = client.post("http://domain.com/action?vodka=42", data={"gin": 43}) + assert rv.status_code == 200 + assert "gin" in flask.request.form + assert "vodka" in flask.request.args + + +def test_json_request_and_response(app, client): + @app.route("/echo", methods=["POST"]) + def echo(): + return jsonify(flask.request.get_json()) + + with client: + json_data = {"drink": {"gin": 1, "tonic": True}, "price": 10} + rv = client.post("/echo", json=json_data) + + # Request should be in JSON + assert flask.request.is_json + assert flask.request.get_json() == json_data + + # Response should be in JSON + assert rv.status_code == 200 + assert rv.is_json + assert rv.get_json() == json_data + + +def test_client_json_no_app_context(app, client): + @app.route("/hello", methods=["POST"]) + def hello(): + return f"Hello, {flask.request.json['name']}!" + + class Namespace: + count = 0 + + def add(self, app): + self.count += 1 + + ns = Namespace() + + with appcontext_popped.connected_to(ns.add, app): + rv = client.post("/hello", json={"name": "Flask"}) + + assert rv.get_data(as_text=True) == "Hello, Flask!" + assert ns.count == 1 + + +def test_subdomain(): + app = flask.Flask(__name__, subdomain_matching=True) + app.config["SERVER_NAME"] = "example.com" + client = app.test_client() + + @app.route("/", subdomain="") + def view(company_id): + return company_id + + with app.test_request_context(): + url = flask.url_for("view", company_id="xxx") + + with client: + response = client.get(url) + + assert 200 == response.status_code + assert b"xxx" == response.data + + +def test_nosubdomain(app, client): + app.config["SERVER_NAME"] = "example.com" + + @app.route("/") + def view(company_id): + return company_id + + with app.test_request_context(): + url = flask.url_for("view", company_id="xxx") + + with client: + response = client.get(url) + + assert 200 == response.status_code + assert b"xxx" == response.data + + +def test_cli_runner_class(app): + runner = app.test_cli_runner() + assert isinstance(runner, FlaskCliRunner) + + class SubRunner(FlaskCliRunner): + pass + + app.test_cli_runner_class = SubRunner + runner = app.test_cli_runner() + assert isinstance(runner, SubRunner) + + +def test_cli_invoke(app): + @app.cli.command("hello") + def hello_command(): + click.echo("Hello, World!") + + runner = app.test_cli_runner() + # invoke with command name + result = runner.invoke(args=["hello"]) + assert "Hello" in result.output + # invoke with command object + result = runner.invoke(hello_command) + assert "Hello" in result.output + + +def test_cli_custom_obj(app): + class NS: + called = False + + def create_app(): + NS.called = True + return app + + @app.cli.command("hello") + def hello_command(): + click.echo("Hello, World!") + + script_info = ScriptInfo(create_app=create_app) + runner = app.test_cli_runner() + runner.invoke(hello_command, obj=script_info) + assert NS.called + + +def test_client_pop_all_preserved(app, req_ctx, client): + @app.route("/") + def index(): + # stream_with_context pushes a third context, preserved by response + return flask.stream_with_context("hello") + + # req_ctx fixture pushed an initial context + with client: + # request pushes a second request context, preserved by client + rv = client.get("/") + + # close the response, releasing the context held by stream_with_context + rv.close() + # only req_ctx fixture should still be pushed + assert _cv_request.get(None) is req_ctx diff --git a/test/fixtures/whole_applications/flask/tests/test_user_error_handler.py b/test/fixtures/whole_applications/flask/tests/test_user_error_handler.py new file mode 100644 index 0000000..79c5a73 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_user_error_handler.py @@ -0,0 +1,295 @@ +import pytest +from werkzeug.exceptions import Forbidden +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.exceptions import NotFound + +import flask + + +def test_error_handler_no_match(app, client): + class CustomException(Exception): + pass + + @app.errorhandler(CustomException) + def custom_exception_handler(e): + assert isinstance(e, CustomException) + return "custom" + + with pytest.raises(TypeError) as exc_info: + app.register_error_handler(CustomException(), None) + + assert "CustomException() is an instance, not a class." in str(exc_info.value) + + with pytest.raises(ValueError) as exc_info: + app.register_error_handler(list, None) + + assert "'list' is not a subclass of Exception." in str(exc_info.value) + + @app.errorhandler(500) + def handle_500(e): + assert isinstance(e, InternalServerError) + + if e.original_exception is not None: + return f"wrapped {type(e.original_exception).__name__}" + + return "direct" + + with pytest.raises(ValueError) as exc_info: + app.register_error_handler(999, None) + + assert "Use a subclass of HTTPException" in str(exc_info.value) + + @app.route("/custom") + def custom_test(): + raise CustomException() + + @app.route("/keyerror") + def key_error(): + raise KeyError() + + @app.route("/abort") + def do_abort(): + flask.abort(500) + + app.testing = False + assert client.get("/custom").data == b"custom" + assert client.get("/keyerror").data == b"wrapped KeyError" + assert client.get("/abort").data == b"direct" + + +def test_error_handler_subclass(app): + class ParentException(Exception): + pass + + class ChildExceptionUnregistered(ParentException): + pass + + class ChildExceptionRegistered(ParentException): + pass + + @app.errorhandler(ParentException) + def parent_exception_handler(e): + assert isinstance(e, ParentException) + return "parent" + + @app.errorhandler(ChildExceptionRegistered) + def child_exception_handler(e): + assert isinstance(e, ChildExceptionRegistered) + return "child-registered" + + @app.route("/parent") + def parent_test(): + raise ParentException() + + @app.route("/child-unregistered") + def unregistered_test(): + raise ChildExceptionUnregistered() + + @app.route("/child-registered") + def registered_test(): + raise ChildExceptionRegistered() + + c = app.test_client() + + assert c.get("/parent").data == b"parent" + assert c.get("/child-unregistered").data == b"parent" + assert c.get("/child-registered").data == b"child-registered" + + +def test_error_handler_http_subclass(app): + class ForbiddenSubclassRegistered(Forbidden): + pass + + class ForbiddenSubclassUnregistered(Forbidden): + pass + + @app.errorhandler(403) + def code_exception_handler(e): + assert isinstance(e, Forbidden) + return "forbidden" + + @app.errorhandler(ForbiddenSubclassRegistered) + def subclass_exception_handler(e): + assert isinstance(e, ForbiddenSubclassRegistered) + return "forbidden-registered" + + @app.route("/forbidden") + def forbidden_test(): + raise Forbidden() + + @app.route("/forbidden-registered") + def registered_test(): + raise ForbiddenSubclassRegistered() + + @app.route("/forbidden-unregistered") + def unregistered_test(): + raise ForbiddenSubclassUnregistered() + + c = app.test_client() + + assert c.get("/forbidden").data == b"forbidden" + assert c.get("/forbidden-unregistered").data == b"forbidden" + assert c.get("/forbidden-registered").data == b"forbidden-registered" + + +def test_error_handler_blueprint(app): + bp = flask.Blueprint("bp", __name__) + + @bp.errorhandler(500) + def bp_exception_handler(e): + return "bp-error" + + @bp.route("/error") + def bp_test(): + raise InternalServerError() + + @app.errorhandler(500) + def app_exception_handler(e): + return "app-error" + + @app.route("/error") + def app_test(): + raise InternalServerError() + + app.register_blueprint(bp, url_prefix="/bp") + + c = app.test_client() + + assert c.get("/error").data == b"app-error" + assert c.get("/bp/error").data == b"bp-error" + + +def test_default_error_handler(): + bp = flask.Blueprint("bp", __name__) + + @bp.errorhandler(HTTPException) + def bp_exception_handler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return "bp-default" + + @bp.errorhandler(Forbidden) + def bp_forbidden_handler(e): + assert isinstance(e, Forbidden) + return "bp-forbidden" + + @bp.route("/undefined") + def bp_registered_test(): + raise NotFound() + + @bp.route("/forbidden") + def bp_forbidden_test(): + raise Forbidden() + + app = flask.Flask(__name__) + + @app.errorhandler(HTTPException) + def catchall_exception_handler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return "default" + + @app.errorhandler(Forbidden) + def catchall_forbidden_handler(e): + assert isinstance(e, Forbidden) + return "forbidden" + + @app.route("/forbidden") + def forbidden(): + raise Forbidden() + + @app.route("/slash/") + def slash(): + return "slash" + + app.register_blueprint(bp, url_prefix="/bp") + + c = app.test_client() + assert c.get("/bp/undefined").data == b"bp-default" + assert c.get("/bp/forbidden").data == b"bp-forbidden" + assert c.get("/undefined").data == b"default" + assert c.get("/forbidden").data == b"forbidden" + # Don't handle RequestRedirect raised when adding slash. + assert c.get("/slash", follow_redirects=True).data == b"slash" + + +class TestGenericHandlers: + """Test how very generic handlers are dispatched to.""" + + class Custom(Exception): + pass + + @pytest.fixture() + def app(self, app): + @app.route("/custom") + def do_custom(): + raise self.Custom() + + @app.route("/error") + def do_error(): + raise KeyError() + + @app.route("/abort") + def do_abort(): + flask.abort(500) + + @app.route("/raise") + def do_raise(): + raise InternalServerError() + + app.config["PROPAGATE_EXCEPTIONS"] = False + return app + + def report_error(self, e): + original = getattr(e, "original_exception", None) + + if original is not None: + return f"wrapped {type(original).__name__}" + + return f"direct {type(e).__name__}" + + @pytest.mark.parametrize("to_handle", (InternalServerError, 500)) + def test_handle_class_or_code(self, app, client, to_handle): + """``InternalServerError`` and ``500`` are aliases, they should + have the same behavior. Both should only receive + ``InternalServerError``, which might wrap another error. + """ + + @app.errorhandler(to_handle) + def handle_500(e): + assert isinstance(e, InternalServerError) + return self.report_error(e) + + assert client.get("/custom").data == b"wrapped Custom" + assert client.get("/error").data == b"wrapped KeyError" + assert client.get("/abort").data == b"direct InternalServerError" + assert client.get("/raise").data == b"direct InternalServerError" + + def test_handle_generic_http(self, app, client): + """``HTTPException`` should only receive ``HTTPException`` + subclasses. It will receive ``404`` routing exceptions. + """ + + @app.errorhandler(HTTPException) + def handle_http(e): + assert isinstance(e, HTTPException) + return str(e.code) + + assert client.get("/error").data == b"500" + assert client.get("/abort").data == b"500" + assert client.get("/not-found").data == b"404" + + def test_handle_generic(self, app, client): + """Generic ``Exception`` will handle all exceptions directly, + including ``HTTPExceptions``. + """ + + @app.errorhandler(Exception) + def handle_exception(e): + return self.report_error(e) + + assert client.get("/custom").data == b"direct Custom" + assert client.get("/error").data == b"direct KeyError" + assert client.get("/abort").data == b"direct InternalServerError" + assert client.get("/not-found").data == b"direct NotFound" diff --git a/test/fixtures/whole_applications/flask/tests/test_views.py b/test/fixtures/whole_applications/flask/tests/test_views.py new file mode 100644 index 0000000..eab5eda --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/test_views.py @@ -0,0 +1,260 @@ +import pytest +from werkzeug.http import parse_set_header + +import flask.views + + +def common_test(app): + c = app.test_client() + + assert c.get("/").data == b"GET" + assert c.post("/").data == b"POST" + assert c.put("/").status_code == 405 + meths = parse_set_header(c.open("/", method="OPTIONS").headers["Allow"]) + assert sorted(meths) == ["GET", "HEAD", "OPTIONS", "POST"] + + +def test_basic_view(app): + class Index(flask.views.View): + methods = ["GET", "POST"] + + def dispatch_request(self): + return flask.request.method + + app.add_url_rule("/", view_func=Index.as_view("index")) + common_test(app) + + +def test_method_based_view(app): + class Index(flask.views.MethodView): + def get(self): + return "GET" + + def post(self): + return "POST" + + app.add_url_rule("/", view_func=Index.as_view("index")) + + common_test(app) + + +def test_view_patching(app): + class Index(flask.views.MethodView): + def get(self): + raise ZeroDivisionError + + def post(self): + raise ZeroDivisionError + + class Other(Index): + def get(self): + return "GET" + + def post(self): + return "POST" + + view = Index.as_view("index") + view.view_class = Other + app.add_url_rule("/", view_func=view) + common_test(app) + + +def test_view_inheritance(app, client): + class Index(flask.views.MethodView): + def get(self): + return "GET" + + def post(self): + return "POST" + + class BetterIndex(Index): + def delete(self): + return "DELETE" + + app.add_url_rule("/", view_func=BetterIndex.as_view("index")) + + meths = parse_set_header(client.open("/", method="OPTIONS").headers["Allow"]) + assert sorted(meths) == ["DELETE", "GET", "HEAD", "OPTIONS", "POST"] + + +def test_view_decorators(app, client): + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers["X-Parachute"] = "awesome" + return resp + + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + + def dispatch_request(self): + return "Awesome" + + app.add_url_rule("/", view_func=Index.as_view("index")) + rv = client.get("/") + assert rv.headers["X-Parachute"] == "awesome" + assert rv.data == b"Awesome" + + +def test_view_provide_automatic_options_attr(): + app = flask.Flask(__name__) + + class Index1(flask.views.View): + provide_automatic_options = False + + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index1.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert rv.status_code == 405 + + app = flask.Flask(__name__) + + class Index2(flask.views.View): + methods = ["OPTIONS"] + provide_automatic_options = True + + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index2.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert sorted(rv.allow) == ["OPTIONS"] + + app = flask.Flask(__name__) + + class Index3(flask.views.View): + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index3.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert "OPTIONS" in rv.allow + + +def test_implicit_head(app, client): + class Index(flask.views.MethodView): + def get(self): + return flask.Response("Blub", headers={"X-Method": flask.request.method}) + + app.add_url_rule("/", view_func=Index.as_view("index")) + rv = client.get("/") + assert rv.data == b"Blub" + assert rv.headers["X-Method"] == "GET" + rv = client.head("/") + assert rv.data == b"" + assert rv.headers["X-Method"] == "HEAD" + + +def test_explicit_head(app, client): + class Index(flask.views.MethodView): + def get(self): + return "GET" + + def head(self): + return flask.Response("", headers={"X-Method": "HEAD"}) + + app.add_url_rule("/", view_func=Index.as_view("index")) + rv = client.get("/") + assert rv.data == b"GET" + rv = client.head("/") + assert rv.data == b"" + assert rv.headers["X-Method"] == "HEAD" + + +def test_endpoint_override(app): + app.debug = True + + class Index(flask.views.View): + methods = ["GET", "POST"] + + def dispatch_request(self): + return flask.request.method + + app.add_url_rule("/", view_func=Index.as_view("index")) + + with pytest.raises(AssertionError): + app.add_url_rule("/", view_func=Index.as_view("index")) + + # But these tests should still pass. We just log a warning. + common_test(app) + + +def test_methods_var_inheritance(app, client): + class BaseView(flask.views.MethodView): + methods = ["GET", "PROPFIND"] + + class ChildView(BaseView): + def get(self): + return "GET" + + def propfind(self): + return "PROPFIND" + + app.add_url_rule("/", view_func=ChildView.as_view("index")) + + assert client.get("/").data == b"GET" + assert client.open("/", method="PROPFIND").data == b"PROPFIND" + assert ChildView.methods == {"PROPFIND", "GET"} + + +def test_multiple_inheritance(app, client): + class GetView(flask.views.MethodView): + def get(self): + return "GET" + + class DeleteView(flask.views.MethodView): + def delete(self): + return "DELETE" + + class GetDeleteView(GetView, DeleteView): + pass + + app.add_url_rule("/", view_func=GetDeleteView.as_view("index")) + + assert client.get("/").data == b"GET" + assert client.delete("/").data == b"DELETE" + assert sorted(GetDeleteView.methods) == ["DELETE", "GET"] + + +def test_remove_method_from_parent(app, client): + class GetView(flask.views.MethodView): + def get(self): + return "GET" + + class OtherView(flask.views.MethodView): + def post(self): + return "POST" + + class View(GetView, OtherView): + methods = ["GET"] + + app.add_url_rule("/", view_func=View.as_view("index")) + + assert client.get("/").data == b"GET" + assert client.post("/").status_code == 405 + assert sorted(View.methods) == ["GET"] + + +def test_init_once(app, client): + n = 0 + + class CountInit(flask.views.View): + init_every_request = False + + def __init__(self): + nonlocal n + n += 1 + + def dispatch_request(self): + return str(n) + + app.add_url_rule("/", view_func=CountInit.as_view("index")) + assert client.get("/").data == b"1" + assert client.get("/").data == b"1" diff --git a/test/fixtures/whole_applications/flask/tests/typing/typing_app_decorators.py b/test/fixtures/whole_applications/flask/tests/typing/typing_app_decorators.py new file mode 100644 index 0000000..0e25a30 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/typing/typing_app_decorators.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from flask import Flask +from flask import Response + +app = Flask(__name__) + + +@app.after_request +def after_sync(response: Response) -> Response: + return Response() + + +@app.after_request +async def after_async(response: Response) -> Response: + return Response() + + +@app.before_request +def before_sync() -> None: ... + + +@app.before_request +async def before_async() -> None: ... + + +@app.teardown_appcontext +def teardown_sync(exc: BaseException | None) -> None: ... + + +@app.teardown_appcontext +async def teardown_async(exc: BaseException | None) -> None: ... diff --git a/test/fixtures/whole_applications/flask/tests/typing/typing_error_handler.py b/test/fixtures/whole_applications/flask/tests/typing/typing_error_handler.py new file mode 100644 index 0000000..ec9c886 --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/typing/typing_error_handler.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from http import HTTPStatus + +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import NotFound + +from flask import Flask + +app = Flask(__name__) + + +@app.errorhandler(400) +@app.errorhandler(HTTPStatus.BAD_REQUEST) +@app.errorhandler(BadRequest) +def handle_400(e: BadRequest) -> str: + return "" + + +@app.errorhandler(ValueError) +def handle_custom(e: ValueError) -> str: + return "" + + +@app.errorhandler(ValueError) +def handle_accept_base(e: Exception) -> str: + return "" + + +@app.errorhandler(BadRequest) +@app.errorhandler(404) +def handle_multiple(e: BadRequest | NotFound) -> str: + return "" diff --git a/test/fixtures/whole_applications/flask/tests/typing/typing_route.py b/test/fixtures/whole_applications/flask/tests/typing/typing_route.py new file mode 100644 index 0000000..8bc271b --- /dev/null +++ b/test/fixtures/whole_applications/flask/tests/typing/typing_route.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import typing as t +from http import HTTPStatus + +from flask import Flask +from flask import jsonify +from flask import stream_template +from flask.templating import render_template +from flask.views import View +from flask.wrappers import Response + +app = Flask(__name__) + + +@app.route("/str") +def hello_str() -> str: + return "

Hello, World!

" + + +@app.route("/bytes") +def hello_bytes() -> bytes: + return b"

Hello, World!

" + + +@app.route("/json") +def hello_json() -> Response: + return jsonify("Hello, World!") + + +@app.route("/json/dict") +def hello_json_dict() -> dict[str, t.Any]: + return {"response": "Hello, World!"} + + +@app.route("/json/dict") +def hello_json_list() -> list[t.Any]: + return [{"message": "Hello"}, {"message": "World"}] + + +class StatusJSON(t.TypedDict): + status: str + + +@app.route("/typed-dict") +def typed_dict() -> StatusJSON: + return {"status": "ok"} + + +@app.route("/generator") +def hello_generator() -> t.Generator[str, None, None]: + def show() -> t.Generator[str, None, None]: + for x in range(100): + yield f"data:{x}\n\n" + + return show() + + +@app.route("/generator-expression") +def hello_generator_expression() -> t.Iterator[bytes]: + return (f"data:{x}\n\n".encode() for x in range(100)) + + +@app.route("/iterator") +def hello_iterator() -> t.Iterator[str]: + return iter([f"data:{x}\n\n" for x in range(100)]) + + +@app.route("/status") +@app.route("/status/") +def tuple_status(code: int = 200) -> tuple[str, int]: + return "hello", code + + +@app.route("/status-enum") +def tuple_status_enum() -> tuple[str, int]: + return "hello", HTTPStatus.OK + + +@app.route("/headers") +def tuple_headers() -> tuple[str, dict[str, str]]: + return "Hello, World!", {"Content-Type": "text/plain"} + + +@app.route("/template") +@app.route("/template/") +def return_template(name: str | None = None) -> str: + return render_template("index.html", name=name) + + +@app.route("/template") +def return_template_stream() -> t.Iterator[str]: + return stream_template("index.html", name="Hello") + + +@app.route("/async") +async def async_route() -> str: + return "Hello" + + +class RenderTemplateView(View): + def __init__(self: RenderTemplateView, template_name: str) -> None: + self.template_name = template_name + + def dispatch_request(self: RenderTemplateView) -> str: + return render_template(self.template_name) + + +app.add_url_rule( + "/about", + view_func=RenderTemplateView.as_view("about_page", template_name="about.html"), +) diff --git a/test/fixtures/whole_applications/flask/tox.ini b/test/fixtures/whole_applications/flask/tox.ini new file mode 100644 index 0000000..5a232cd --- /dev/null +++ b/test/fixtures/whole_applications/flask/tox.ini @@ -0,0 +1,49 @@ +[tox] +envlist = + py3{12,11,10,9,8} + pypy310 + py312-min + py38-dev + style + typing + docs +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + -r requirements/tests.txt + min: -r requirements-skip/tests-min.txt + dev: -r requirements-skip/tests-dev.txt +commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} + +[testenv:style] +deps = pre-commit +skip_install = true +commands = pre-commit run --all-files + +[testenv:typing] +deps = -r requirements/typing.txt +commands = mypy + +[testenv:docs] +deps = -r requirements/docs.txt +commands = sphinx-build -W -b dirhtml docs docs/_build/dirhtml + +[testenv:update-requirements] +deps = + pip-tools + pre-commit +skip_install = true +change_dir = requirements +commands = + pre-commit autoupdate -j4 + pip-compile -U build.in + pip-compile -U docs.in + pip-compile -U tests.in + pip-compile -U typing.in + pip-compile -U dev.in diff --git a/test/fixtures/whole_applications/requests/.coveragerc b/test/fixtures/whole_applications/requests/.coveragerc new file mode 100644 index 0000000..b5008b2 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = requests/packages/* diff --git a/test/fixtures/whole_applications/requests/.git-blame-ignore-revs b/test/fixtures/whole_applications/requests/.git-blame-ignore-revs new file mode 100644 index 0000000..db51a96 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# You can configure git to automatically use this file with the following config: +# git config --global blame.ignoreRevsFile .git-blame-ignore-revs + +# Add automatic code formatting to Requests +2a6f290bc09324406708a4d404a88a45d848ddf9 diff --git a/test/fixtures/whole_applications/requests/.github/CODE_OF_CONDUCT.md b/test/fixtures/whole_applications/requests/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ff7f106 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +# Treat each other well + +Everyone participating in the _requests_ project, and in particular in the issue tracker, +pull requests, and social media activity, is expected to treat other people with respect +and more generally to follow the guidelines articulated in the +[Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). diff --git a/test/fixtures/whole_applications/requests/.github/CONTRIBUTING.md b/test/fixtures/whole_applications/requests/.github/CONTRIBUTING.md new file mode 100644 index 0000000..3470dfe --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contribution Guidelines + +Before opening any issues or proposing any pull requests, please read +our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/). + +To get the greatest chance of helpful responses, please also observe the +following additional notes. + +## Questions + +The GitHub issue tracker is for *bug reports* and *feature requests*. Please do +not use it to ask questions about how to use Requests. These questions should +instead be directed to [Stack Overflow](https://stackoverflow.com/). Make sure +that your question is tagged with the `python-requests` tag when asking it on +Stack Overflow, to ensure that it is answered promptly and accurately. + +## Good Bug Reports + +Please be aware of the following things when filing bug reports: + +1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature + to check whether your bug report or feature request has been mentioned in + the past. Duplicate bug reports and feature requests are a huge maintenance + burden on the limited resources of the project. If it is clear from your + report that you would have struggled to find the original, that's ok, but + if searching for a selection of words in your issue title would have found + the duplicate then the issue will likely be closed extremely abruptly. +2. When filing bug reports about exceptions or tracebacks, please include the + *complete* traceback. Partial tracebacks, or just the exception text, are + not helpful. Issues that do not contain complete tracebacks may be closed + without warning. +3. Make sure you provide a suitable amount of information to work with. This + means you should provide: + + - Guidance on **how to reproduce the issue**. Ideally, this should be a + *small* code sample that can be run immediately by the maintainers. + Failing that, let us know what you're doing, how often it happens, what + environment you're using, etc. Be thorough: it prevents us needing to ask + further questions. + - Tell us **what you expected to happen**. When we run your example code, + what are we expecting to happen? What does "success" look like for your + code? + - Tell us **what actually happens**. It's not helpful for you to say "it + doesn't work" or "it fails". Tell us *how* it fails: do you get an + exception? A hang? A non-200 status code? How was the actual result + different from your expected result? + - Tell us **what version of Requests you're using**, and + **how you installed it**. Different versions of Requests behave + differently and have different bugs, and some distributors of Requests + ship patches on top of the code we supply. + + If you do not provide all of these things, it will take us much longer to + fix your problem. If we ask you to clarify these and you never respond, we + will close your issue without fixing it. diff --git a/test/fixtures/whole_applications/requests/.github/FUNDING.yml b/test/fixtures/whole_applications/requests/.github/FUNDING.yml new file mode 100644 index 0000000..603c7ff --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://www.python.org/psf/sponsorship/'] diff --git a/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE.md b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..060d926 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ +Summary. + +## Expected Result + +What you expected. + +## Actual Result + +What happened instead. + +## Reproduction Steps + +```python +import requests + +``` + +## System Information + + $ python -m requests.help + +``` + +``` + +This command is only available on Requests v2.16.4 and greater. Otherwise, +please provide some basic information about your system (Python version, +operating system, &c). diff --git a/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Bug_report.md b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..fbbfaae --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +## Expected Result + + + +## Actual Result + + + +## Reproduction Steps + +```python +import requests + +``` + +## System Information + + $ python -m requests.help + +```json +{ + "paste": "here" +} +``` + + diff --git a/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Custom.md b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Custom.md new file mode 100644 index 0000000..57dc697 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Custom.md @@ -0,0 +1,7 @@ +--- +name: Request for Help +about: Guidance on using Requests. + +--- + +Please refer to our [Stack Overflow tag](https://stackoverflow.com/questions/tagged/python-requests) for guidance. diff --git a/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Feature_request.md b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..723389e --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +Requests is not accepting feature requests at this time. diff --git a/test/fixtures/whole_applications/requests/.github/SECURITY.md b/test/fixtures/whole_applications/requests/.github/SECURITY.md new file mode 100644 index 0000000..9021d42 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/SECURITY.md @@ -0,0 +1,93 @@ +# Vulnerability Disclosure + +If you think you have found a potential security vulnerability in +requests, please email [Nate](mailto:nate.prewitt@gmail.com) +and [Seth](mailto:sethmichaellarson@gmail.com) directly. +**Do not file a public issue.** + +Our PGP Key fingerprints are: + +- 8722 7E29 AD9C FF5C FAC3 EA6A 44D3 FF97 B80D C864 ([@nateprewitt](https://keybase.io/nateprewitt)) + +- EDD5 6765 A9D8 4653 CBC8 A134 51B0 6736 1740 F5FC ([@sethmlarson](https://keybase.io/sethmlarson)) + +You can also contact us on [Keybase](https://keybase.io) with the +profiles above if desired. + +If English is not your first language, please try to describe the +problem and its impact to the best of your ability. For greater detail, +please use your native language and we will try our best to translate it +using online services. + +Please also include the code you used to find the problem and the +shortest amount of code necessary to reproduce it. + +Please do not disclose this to anyone else. We will retrieve a CVE +identifier if necessary and give you full credit under whatever name or +alias you provide. We will only request an identifier when we have a fix +and can publish it in a release. + +We will respect your privacy and will only publicize your involvement if +you grant us permission. + +## Process + +This following information discusses the process the requests project +follows in response to vulnerability disclosures. If you are disclosing +a vulnerability, this section of the documentation lets you know how we +will respond to your disclosure. + +### Timeline + +When you report an issue, one of the project members will respond to you +within two days *at the outside*. In most cases responses will be +faster, usually within 12 hours. This initial response will at the very +least confirm receipt of the report. + +If we were able to rapidly reproduce the issue, the initial response +will also contain confirmation of the issue. If we are not, we will +often ask for more information about the reproduction scenario. + +Our goal is to have a fix for any vulnerability released within two +weeks of the initial disclosure. This may potentially involve shipping +an interim release that simply disables function while a more mature fix +can be prepared, but will in the vast majority of cases mean shipping a +complete release as soon as possible. + +Throughout the fix process we will keep you up to speed with how the fix +is progressing. Once the fix is prepared, we will notify you that we +believe we have a fix. Often we will ask you to confirm the fix resolves +the problem in your environment, especially if we are not confident of +our reproduction scenario. + +At this point, we will prepare for the release. We will obtain a CVE +number if one is required, providing you with full credit for the +discovery. We will also decide on a planned release date, and let you +know when it is. This release date will *always* be on a weekday. + +At this point we will reach out to our major downstream packagers to +notify them of an impending security-related patch so they can make +arrangements. In addition, these packagers will be provided with the +intended patch ahead of time, to ensure that they are able to promptly +release their downstream packages. Currently the list of people we +actively contact *ahead of a public release* is: + +- Jeremy Cline, Red Hat (@jeremycline) +- Daniele Tricoli, Debian (@eriol) + +We will notify these individuals at least a week ahead of our planned +release date to ensure that they have sufficient time to prepare. If you +believe you should be on this list, please let one of the maintainers +know at one of the email addresses at the top of this article. + +On release day, we will push the patch to our public repository, along +with an updated changelog that describes the issue and credits you. We +will then issue a PyPI release containing the patch. + +At this point, we will publicise the release. This will involve mails to +mailing lists, Tweets, and all other communication mechanisms available +to the core team. + +We will also explicitly mention which commits contain the fix to make it +easier for other distributors and users to easily patch their own +versions of requests if upgrading is not an option. diff --git a/test/fixtures/whole_applications/requests/.github/workflows/codeql-analysis.yml b/test/fixtures/whole_applications/requests/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..1e7dba2 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/workflows/codeql-analysis.yml @@ -0,0 +1,73 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: '0 23 * * 0' + +permissions: + contents: read + +jobs: + analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: "python" + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/test/fixtures/whole_applications/requests/.github/workflows/lint.yml b/test/fixtures/whole_applications/requests/.github/workflows/lint.yml new file mode 100644 index 0000000..df275c5 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/workflows/lint.yml @@ -0,0 +1,20 @@ +name: Lint code + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-20.04 + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 diff --git a/test/fixtures/whole_applications/requests/.github/workflows/lock-issues.yml b/test/fixtures/whole_applications/requests/.github/workflows/lock-issues.yml new file mode 100644 index 0000000..f8429c3 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/workflows/lock-issues.yml @@ -0,0 +1,19 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + action: + if: github.repository_owner == 'psf' + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + issue-lock-inactive-days: 90 + pr-lock-inactive-days: 90 diff --git a/test/fixtures/whole_applications/requests/.github/workflows/run-tests.yml b/test/fixtures/whole_applications/requests/.github/workflows/run-tests.yml new file mode 100644 index 0000000..c415950 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.github/workflows/run-tests.yml @@ -0,0 +1,34 @@ +name: Tests + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy-3.8", "pypy-3.9"] + os: [ubuntu-22.04, macOS-latest, windows-latest] + include: + # pypy-3.7 on Windows and Mac OS currently fails trying to compile + # cryptography. Moving pypy-3.7 to only test linux. + - python-version: pypy-3.7 + os: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + make + - name: Run tests + run: | + make ci diff --git a/test/fixtures/whole_applications/requests/.gitignore b/test/fixtures/whole_applications/requests/.gitignore new file mode 100644 index 0000000..de61154 --- /dev/null +++ b/test/fixtures/whole_applications/requests/.gitignore @@ -0,0 +1,37 @@ +.coverage +MANIFEST +coverage.xml +nosetests.xml +junit-report.xml +pylint.txt +toy.py +.cache/ +cover/ +build/ +docs/_build +requests.egg-info/ +*.pyc +*.swp +*.egg +env/ +.venv/ +.eggs/ +.tox/ +.pytest_cache/ +.vscode/ +.eggs/ + +.workon + +# in case you work with IntelliJ/PyCharm +.idea +*.iml +.python-version + + +t.py + +t2.py +dist + +/.mypy_cache/ diff --git a/test/fixtures/whole_applications/requests/.pre-commit-config.yaml b/test/fixtures/whole_applications/requests/.pre-commit-config.yaml new file mode 100644 index 0000000..5b915dc --- /dev/null +++ b/test/fixtures/whole_applications/requests/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +exclude: 'docs/|ext/' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + exclude: tests/test_lowlevel.py +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 + hooks: + - id: pyupgrade + args: [--py37-plus] +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 diff --git a/test/fixtures/whole_applications/requests/AUTHORS.rst b/test/fixtures/whole_applications/requests/AUTHORS.rst new file mode 100644 index 0000000..c4e554d --- /dev/null +++ b/test/fixtures/whole_applications/requests/AUTHORS.rst @@ -0,0 +1,194 @@ +Requests was lovingly created by Kenneth Reitz. + +Keepers of the Crystals +``````````````````````` + +- Nate Prewitt `@nateprewitt `_. +- Seth M. Larson `@sethmlarson `_. + +Previous Keepers of Crystals +```````````````````````````` +- Kenneth Reitz `@ken-reitz `_, reluctant Keeper of the Master Crystal. +- Cory Benfield `@lukasa `_ +- Ian Cordasco `@sigmavirus24 `_. + + +Patches and Suggestions +``````````````````````` + +- Various Pocoo Members +- Chris Adams +- Flavio Percoco Premoli +- Dj Gilcrease +- Justin Murphy +- Rob Madole +- Aram Dulyan +- Johannes Gorset +- 村山めがね (Megane Murayama) +- James Rowe +- Daniel Schauenberg +- Zbigniew Siciarz +- Daniele Tricoli 'Eriol' +- Richard Boulton +- Miguel Olivares +- Alberto Paro +- Jérémy Bethmont +- 潘旭 (Xu Pan) +- Tamás Gulácsi +- Rubén Abad +- Peter Manser +- Jeremy Selier +- Jens Diemer +- Alex (`@alopatin `_) +- Tom Hogans +- Armin Ronacher +- Shrikant Sharat Kandula +- Mikko Ohtamaa +- Den Shabalin +- Daniel Miller +- Alejandro Giacometti +- Rick Mak +- Johan Bergström +- Josselin Jacquard +- Travis N. Vaught +- Fredrik Möllerstrand +- Daniel Hengeveld +- Dan Head +- Bruno Renié +- David Fischer +- Joseph McCullough +- Juergen Brendel +- Juan Riaza +- Ryan Kelly +- Rolando Espinoza La fuente +- Robert Gieseke +- Idan Gazit +- Ed Summers +- Chris Van Horne +- Christopher Davis +- Ori Livneh +- Jason Emerick +- Bryan Helmig +- Jonas Obrist +- Lucian Ursu +- Tom Moertel +- Frank Kumro Jr +- Chase Sterling +- Marty Alchin +- takluyver +- Ben Toews (`@mastahyeti `_) +- David Kemp +- Brendon Crawford +- Denis (`@Telofy `_) +- Matt Giuca +- Adam Tauber +- Honza Javorek +- Brendan Maguire +- Chris Dary +- Danver Braganza +- Max Countryman +- Nick Chadwick +- Jonathan Drosdeck +- Jiri Machalek +- Steve Pulec +- Michael Kelly +- Michael Newman +- Jonty Wareing +- Shivaram Lingamneni +- Miguel Turner +- Rohan Jain (`@crodjer `_) +- Justin Barber +- Roman Haritonov (`@reclosedev `_) +- Josh Imhoff +- Arup Malakar +- Danilo Bargen (`@dbrgn `_) +- Torsten Landschoff +- Michael Holler (`@apotheos `_) +- Timnit Gebru +- Sarah Gonzalez +- Victoria Mo +- Leila Muhtasib +- Matthias Rahlf +- Jakub Roztocil +- Rhys Elsmore +- André Graf (`@dergraf `_) +- Stephen Zhuang (`@everbird `_) +- Martijn Pieters +- Jonatan Heyman +- David Bonner (`@rascalking `_) +- Vinod Chandru +- Johnny Goodnow +- Denis Ryzhkov +- Wilfred Hughes +- Dmitry Medvinsky +- Bryce Boe (`@bboe `_) +- Colin Dunklau (`@cdunklau `_) +- Bob Carroll (`@rcarz `_) +- Hugo Osvaldo Barrera (`@hobarrera `_) +- Łukasz Langa +- Dave Shawley +- James Clarke (`@jam `_) +- Kevin Burke +- Flavio Curella +- David Pursehouse (`@dpursehouse `_) +- Jon Parise (`@jparise `_) +- Alexander Karpinsky (`@homm86 `_) +- Marc Schlaich (`@schlamar `_) +- Park Ilsu (`@daftshady `_) +- Matt Spitz (`@mattspitz `_) +- Vikram Oberoi (`@voberoi `_) +- Can Ibanoglu (`@canibanoglu `_) +- Thomas Weißschuh (`@t-8ch `_) +- Jayson Vantuyl +- Pengfei.X +- Kamil Madac +- Michael Becker (`@beckerfuffle `_) +- Erik Wickstrom (`@erikwickstrom `_) +- Константин Подшумок (`@podshumok `_) +- Ben Bass (`@codedstructure `_) +- Jonathan Wong (`@ContinuousFunction `_) +- Martin Jul (`@mjul `_) +- Joe Alcorn (`@buttscicles `_) +- Syed Suhail Ahmed (`@syedsuhail `_) +- Scott Sadler (`@ssadler `_) +- Arthur Darcet (`@arthurdarcet `_) +- Ulrich Petri (`@ulope `_) +- Muhammad Yasoob Ullah Khalid (`@yasoob `_) +- Paul van der Linden (`@pvanderlinden `_) +- Colin Dickson (`@colindickson `_) +- Smiley Barry (`@smiley `_) +- Shagun Sodhani (`@shagunsodhani `_) +- Robin Linderborg (`@vienno `_) +- Brian Samek (`@bsamek `_) +- Dmitry Dygalo (`@Stranger6667 `_) +- piotrjurkiewicz +- Jesse Shapiro (`@haikuginger `_) +- Nate Prewitt (`@nateprewitt `_) +- Maik Himstedt +- Michael Hunsinger +- Brian Bamsch (`@bbamsch `_) +- Om Prakash Kumar (`@iamprakashom `_) +- Philipp Konrad (`@gardiac2002 `_) +- Hussain Tamboli (`@hussaintamboli `_) +- Casey Davidson (`@davidsoncasey `_) +- Andrii Soldatenko (`@a_soldatenko `_) +- Moinuddin Quadri (`@moin18 `_) +- Matt Kohl (`@mattkohl `_) +- Jonathan Vanasco (`@jvanasco `_) +- David Fontenot (`@davidfontenot `_) +- Shmuel Amar (`@shmuelamar `_) +- Gary Wu (`@garywu `_) +- Ryan Pineo (`@ryanpineo `_) +- Ed Morley (`@edmorley `_) +- Matt Liu (`@mlcrazy `_) +- Taylor Hoff (`@PrimordialHelios `_) +- Arthur Vigil (`@ahvigil `_) +- Nehal J Wani (`@nehaljwani `_) +- Demetrios Bairaktaris (`@DemetriosBairaktaris `_) +- Darren Dormer (`@ddormer `_) +- Rajiv Mayani (`@mayani `_) +- Antti Kaihola (`@akaihola `_) +- "Dull Bananas" (`@dullbananas `_) +- Alessio Izzo (`@aless10 `_) +- Sylvain Marié (`@smarie `_) +- Hod Bin Noon (`@hodbn `_) diff --git a/test/fixtures/whole_applications/requests/HISTORY.md b/test/fixtures/whole_applications/requests/HISTORY.md new file mode 100644 index 0000000..bbe6dd4 --- /dev/null +++ b/test/fixtures/whole_applications/requests/HISTORY.md @@ -0,0 +1,1906 @@ +Release History +=============== + +dev +--- + +- \[Short description of non-trivial change.\] + +2.31.0 (2023-05-22) +------------------- + +**Security** +- Versions of Requests between v2.3.0 and v2.30.0 are vulnerable to potential + forwarding of `Proxy-Authorization` headers to destination servers when + following HTTPS redirects. + + When proxies are defined with user info (https://user:pass@proxy:8080), Requests + will construct a `Proxy-Authorization` header that is attached to the request to + authenticate with the proxy. + + In cases where Requests receives a redirect response, it previously reattached + the `Proxy-Authorization` header incorrectly, resulting in the value being + sent through the tunneled connection to the destination server. Users who rely on + defining their proxy credentials in the URL are *strongly* encouraged to upgrade + to Requests 2.31.0+ to prevent unintentional leakage and rotate their proxy + credentials once the change has been fully deployed. + + Users who do not use a proxy or do not supply their proxy credentials through + the user information portion of their proxy URL are not subject to this + vulnerability. + + Full details can be read in our [Github Security Advisory](https://github.com/psf/requests/security/advisories/GHSA-j8r2-6x86-q33q) + and [CVE-2023-32681](https://nvd.nist.gov/vuln/detail/CVE-2023-32681). + + +2.30.0 (2023-05-03) +------------------- + +**Dependencies** +- ⚠️ Added support for urllib3 2.0. ⚠️ + + This may contain minor breaking changes so we advise careful testing and + reviewing https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html + prior to upgrading. + + Users who wish to stay on urllib3 1.x can pin to `urllib3<2`. + +2.29.0 (2023-04-26) +------------------- + +**Improvements** + +- Requests now defers chunked requests to the urllib3 implementation to improve + standardization. (#6226) +- Requests relaxes header component requirements to support bytes/str subclasses. (#6356) + +2.28.2 (2023-01-12) +------------------- + +**Dependencies** + +- Requests now supports charset\_normalizer 3.x. (#6261) + +**Bugfixes** + +- Updated MissingSchema exception to suggest https scheme rather than http. (#6188) + +2.28.1 (2022-06-29) +------------------- + +**Improvements** + +- Speed optimization in `iter_content` with transition to `yield from`. (#6170) + +**Dependencies** + +- Added support for chardet 5.0.0 (#6179) +- Added support for charset-normalizer 2.1.0 (#6169) + +2.28.0 (2022-06-09) +------------------- + +**Deprecations** + +- ⚠️ Requests has officially dropped support for Python 2.7. ⚠️ (#6091) +- Requests has officially dropped support for Python 3.6 (including pypy3.6). (#6091) + +**Improvements** + +- Wrap JSON parsing issues in Request's JSONDecodeError for payloads without + an encoding to make `json()` API consistent. (#6097) +- Parse header components consistently, raising an InvalidHeader error in + all invalid cases. (#6154) +- Added provisional 3.11 support with current beta build. (#6155) +- Requests got a makeover and we decided to paint it black. (#6095) + +**Bugfixes** + +- Fixed bug where setting `CURL_CA_BUNDLE` to an empty string would disable + cert verification. All Requests 2.x versions before 2.28.0 are affected. (#6074) +- Fixed urllib3 exception leak, wrapping `urllib3.exceptions.SSLError` with + `requests.exceptions.SSLError` for `content` and `iter_content`. (#6057) +- Fixed issue where invalid Windows registry entries caused proxy resolution + to raise an exception rather than ignoring the entry. (#6149) +- Fixed issue where entire payload could be included in the error message for + JSONDecodeError. (#6036) + +2.27.1 (2022-01-05) +------------------- + +**Bugfixes** + +- Fixed parsing issue that resulted in the `auth` component being + dropped from proxy URLs. (#6028) + +2.27.0 (2022-01-03) +------------------- + +**Improvements** + +- Officially added support for Python 3.10. (#5928) + +- Added a `requests.exceptions.JSONDecodeError` to unify JSON exceptions between + Python 2 and 3. This gets raised in the `response.json()` method, and is + backwards compatible as it inherits from previously thrown exceptions. + Can be caught from `requests.exceptions.RequestException` as well. (#5856) + +- Improved error text for misnamed `InvalidSchema` and `MissingSchema` + exceptions. This is a temporary fix until exceptions can be renamed + (Schema->Scheme). (#6017) + +- Improved proxy parsing for proxy URLs missing a scheme. This will address + recent changes to `urlparse` in Python 3.9+. (#5917) + +**Bugfixes** + +- Fixed defect in `extract_zipped_paths` which could result in an infinite loop + for some paths. (#5851) + +- Fixed handling for `AttributeError` when calculating length of files obtained + by `Tarfile.extractfile()`. (#5239) + +- Fixed urllib3 exception leak, wrapping `urllib3.exceptions.InvalidHeader` with + `requests.exceptions.InvalidHeader`. (#5914) + +- Fixed bug where two Host headers were sent for chunked requests. (#5391) + +- Fixed regression in Requests 2.26.0 where `Proxy-Authorization` was + incorrectly stripped from all requests sent with `Session.send`. (#5924) + +- Fixed performance regression in 2.26.0 for hosts with a large number of + proxies available in the environment. (#5924) + +- Fixed idna exception leak, wrapping `UnicodeError` with + `requests.exceptions.InvalidURL` for URLs with a leading dot (.) in the + domain. (#5414) + +**Deprecations** + +- Requests support for Python 2.7 and 3.6 will be ending in 2022. While we + don't have exact dates, Requests 2.27.x is likely to be the last release + series providing support. + +2.26.0 (2021-07-13) +------------------- + +**Improvements** + +- Requests now supports Brotli compression, if either the `brotli` or + `brotlicffi` package is installed. (#5783) + +- `Session.send` now correctly resolves proxy configurations from both + the Session and Request. Behavior now matches `Session.request`. (#5681) + +**Bugfixes** + +- Fixed a race condition in zip extraction when using Requests in parallel + from zip archive. (#5707) + +**Dependencies** + +- Instead of `chardet`, use the MIT-licensed `charset_normalizer` for Python3 + to remove license ambiguity for projects bundling requests. If `chardet` + is already installed on your machine it will be used instead of `charset_normalizer` + to keep backwards compatibility. (#5797) + + You can also install `chardet` while installing requests by + specifying `[use_chardet_on_py3]` extra as follows: + + ```shell + pip install "requests[use_chardet_on_py3]" + ``` + + Python2 still depends upon the `chardet` module. + +- Requests now supports `idna` 3.x on Python 3. `idna` 2.x will continue to + be used on Python 2 installations. (#5711) + +**Deprecations** + +- The `requests[security]` extra has been converted to a no-op install. + PyOpenSSL is no longer the recommended secure option for Requests. (#5867) + +- Requests has officially dropped support for Python 3.5. (#5867) + +2.25.1 (2020-12-16) +------------------- + +**Bugfixes** + +- Requests now treats `application/json` as `utf8` by default. Resolving + inconsistencies between `r.text` and `r.json` output. (#5673) + +**Dependencies** + +- Requests now supports chardet v4.x. + +2.25.0 (2020-11-11) +------------------- + +**Improvements** + +- Added support for NETRC environment variable. (#5643) + +**Dependencies** + +- Requests now supports urllib3 v1.26. + +**Deprecations** + +- Requests v2.25.x will be the last release series with support for Python 3.5. +- The `requests[security]` extra is officially deprecated and will be removed + in Requests v2.26.0. + +2.24.0 (2020-06-17) +------------------- + +**Improvements** + +- pyOpenSSL TLS implementation is now only used if Python + either doesn't have an `ssl` module or doesn't support + SNI. Previously pyOpenSSL was unconditionally used if available. + This applies even if pyOpenSSL is installed via the + `requests[security]` extra (#5443) + +- Redirect resolution should now only occur when + `allow_redirects` is True. (#5492) + +- No longer perform unnecessary Content-Length calculation for + requests that won't use it. (#5496) + +2.23.0 (2020-02-19) +------------------- + +**Improvements** + +- Remove defunct reference to `prefetch` in Session `__attrs__` (#5110) + +**Bugfixes** + +- Requests no longer outputs password in basic auth usage warning. (#5099) + +**Dependencies** + +- Pinning for `chardet` and `idna` now uses major version instead of minor. + This hopefully reduces the need for releases every time a dependency is updated. + +2.22.0 (2019-05-15) +------------------- + +**Dependencies** + +- Requests now supports urllib3 v1.25.2. + (note: 1.25.0 and 1.25.1 are incompatible) + +**Deprecations** + +- Requests has officially stopped support for Python 3.4. + +2.21.0 (2018-12-10) +------------------- + +**Dependencies** + +- Requests now supports idna v2.8. + +2.20.1 (2018-11-08) +------------------- + +**Bugfixes** + +- Fixed bug with unintended Authorization header stripping for + redirects using default ports (http/80, https/443). + +2.20.0 (2018-10-18) +------------------- + +**Bugfixes** + +- Content-Type header parsing is now case-insensitive (e.g. + charset=utf8 v Charset=utf8). +- Fixed exception leak where certain redirect urls would raise + uncaught urllib3 exceptions. +- Requests removes Authorization header from requests redirected + from https to http on the same hostname. (CVE-2018-18074) +- `should_bypass_proxies` now handles URIs without hostnames (e.g. + files). + +**Dependencies** + +- Requests now supports urllib3 v1.24. + +**Deprecations** + +- Requests has officially stopped support for Python 2.6. + +2.19.1 (2018-06-14) +------------------- + +**Bugfixes** + +- Fixed issue where status\_codes.py's `init` function failed trying + to append to a `__doc__` value of `None`. + +2.19.0 (2018-06-12) +------------------- + +**Improvements** + +- Warn user about possible slowdown when using cryptography version + < 1.3.4 +- Check for invalid host in proxy URL, before forwarding request to + adapter. +- Fragments are now properly maintained across redirects. (RFC7231 + 7.1.2) +- Removed use of cgi module to expedite library load time. +- Added support for SHA-256 and SHA-512 digest auth algorithms. +- Minor performance improvement to `Request.content`. +- Migrate to using collections.abc for 3.7 compatibility. + +**Bugfixes** + +- Parsing empty `Link` headers with `parse_header_links()` no longer + return one bogus entry. +- Fixed issue where loading the default certificate bundle from a zip + archive would raise an `IOError`. +- Fixed issue with unexpected `ImportError` on windows system which do + not support `winreg` module. +- DNS resolution in proxy bypass no longer includes the username and + password in the request. This also fixes the issue of DNS queries + failing on macOS. +- Properly normalize adapter prefixes for url comparison. +- Passing `None` as a file pointer to the `files` param no longer + raises an exception. +- Calling `copy` on a `RequestsCookieJar` will now preserve the cookie + policy correctly. + +**Dependencies** + +- We now support idna v2.7. +- We now support urllib3 v1.23. + +2.18.4 (2017-08-15) +------------------- + +**Improvements** + +- Error messages for invalid headers now include the header name for + easier debugging + +**Dependencies** + +- We now support idna v2.6. + +2.18.3 (2017-08-02) +------------------- + +**Improvements** + +- Running `$ python -m requests.help` now includes the installed + version of idna. + +**Bugfixes** + +- Fixed issue where Requests would raise `ConnectionError` instead of + `SSLError` when encountering SSL problems when using urllib3 v1.22. + +2.18.2 (2017-07-25) +------------------- + +**Bugfixes** + +- `requests.help` no longer fails on Python 2.6 due to the absence of + `ssl.OPENSSL_VERSION_NUMBER`. + +**Dependencies** + +- We now support urllib3 v1.22. + +2.18.1 (2017-06-14) +------------------- + +**Bugfixes** + +- Fix an error in the packaging whereby the `*.whl` contained + incorrect data that regressed the fix in v2.17.3. + +2.18.0 (2017-06-14) +------------------- + +**Improvements** + +- `Response` is now a context manager, so can be used directly in a + `with` statement without first having to be wrapped by + `contextlib.closing()`. + +**Bugfixes** + +- Resolve installation failure if multiprocessing is not available +- Resolve tests crash if multiprocessing is not able to determine the + number of CPU cores +- Resolve error swallowing in utils set\_environ generator + +2.17.3 (2017-05-29) +------------------- + +**Improvements** + +- Improved `packages` namespace identity support, for monkeypatching + libraries. + +2.17.2 (2017-05-29) +------------------- + +**Improvements** + +- Improved `packages` namespace identity support, for monkeypatching + libraries. + +2.17.1 (2017-05-29) +------------------- + +**Improvements** + +- Improved `packages` namespace identity support, for monkeypatching + libraries. + +2.17.0 (2017-05-29) +------------------- + +**Improvements** + +- Removal of the 301 redirect cache. This improves thread-safety. + +2.16.5 (2017-05-28) +------------------- + +- Improvements to `$ python -m requests.help`. + +2.16.4 (2017-05-27) +------------------- + +- Introduction of the `$ python -m requests.help` command, for + debugging with maintainers! + +2.16.3 (2017-05-27) +------------------- + +- Further restored the `requests.packages` namespace for compatibility + reasons. + +2.16.2 (2017-05-27) +------------------- + +- Further restored the `requests.packages` namespace for compatibility + reasons. + +No code modification (noted below) should be necessary any longer. + +2.16.1 (2017-05-27) +------------------- + +- Restored the `requests.packages` namespace for compatibility + reasons. +- Bugfix for `urllib3` version parsing. + +**Note**: code that was written to import against the +`requests.packages` namespace previously will have to import code that +rests at this module-level now. + +For example: + + from requests.packages.urllib3.poolmanager import PoolManager + +Will need to be re-written to be: + + from requests.packages import urllib3 + urllib3.poolmanager.PoolManager + +Or, even better: + + from urllib3.poolmanager import PoolManager + +2.16.0 (2017-05-26) +------------------- + +- Unvendor ALL the things! + +2.15.1 (2017-05-26) +------------------- + +- Everyone makes mistakes. + +2.15.0 (2017-05-26) +------------------- + +**Improvements** + +- Introduction of the `Response.next` property, for getting the next + `PreparedResponse` from a redirect chain (when + `allow_redirects=False`). +- Internal refactoring of `__version__` module. + +**Bugfixes** + +- Restored once-optional parameter for + `requests.utils.get_environ_proxies()`. + +2.14.2 (2017-05-10) +------------------- + +**Bugfixes** + +- Changed a less-than to an equal-to and an or in the dependency + markers to widen compatibility with older setuptools releases. + +2.14.1 (2017-05-09) +------------------- + +**Bugfixes** + +- Changed the dependency markers to widen compatibility with older pip + releases. + +2.14.0 (2017-05-09) +------------------- + +**Improvements** + +- It is now possible to pass `no_proxy` as a key to the `proxies` + dictionary to provide handling similar to the `NO_PROXY` environment + variable. +- When users provide invalid paths to certificate bundle files or + directories Requests now raises `IOError`, rather than failing at + the time of the HTTPS request with a fairly inscrutable certificate + validation error. +- The behavior of `SessionRedirectMixin` was slightly altered. + `resolve_redirects` will now detect a redirect by calling + `get_redirect_target(response)` instead of directly querying + `Response.is_redirect` and `Response.headers['location']`. Advanced + users will be able to process malformed redirects more easily. +- Changed the internal calculation of elapsed request time to have + higher resolution on Windows. +- Added `win_inet_pton` as conditional dependency for the `[socks]` + extra on Windows with Python 2.7. +- Changed the proxy bypass implementation on Windows: the proxy bypass + check doesn't use forward and reverse DNS requests anymore +- URLs with schemes that begin with `http` but are not `http` or + `https` no longer have their host parts forced to lowercase. + +**Bugfixes** + +- Much improved handling of non-ASCII `Location` header values in + redirects. Fewer `UnicodeDecodeErrors` are encountered on Python 2, + and Python 3 now correctly understands that Latin-1 is unlikely to + be the correct encoding. +- If an attempt to `seek` file to find out its length fails, we now + appropriately handle that by aborting our content-length + calculations. +- Restricted `HTTPDigestAuth` to only respond to auth challenges made + on 4XX responses, rather than to all auth challenges. +- Fixed some code that was firing `DeprecationWarning` on Python 3.6. +- The dismayed person emoticon (`/o\\`) no longer has a big head. I'm + sure this is what you were all worrying about most. + +**Miscellaneous** + +- Updated bundled urllib3 to v1.21.1. +- Updated bundled chardet to v3.0.2. +- Updated bundled idna to v2.5. +- Updated bundled certifi to 2017.4.17. + +2.13.0 (2017-01-24) +------------------- + +**Features** + +- Only load the `idna` library when we've determined we need it. This + will save some memory for users. + +**Miscellaneous** + +- Updated bundled urllib3 to 1.20. +- Updated bundled idna to 2.2. + +2.12.5 (2017-01-18) +------------------- + +**Bugfixes** + +- Fixed an issue with JSON encoding detection, specifically detecting + big-endian UTF-32 with BOM. + +2.12.4 (2016-12-14) +------------------- + +**Bugfixes** + +- Fixed regression from 2.12.2 where non-string types were rejected in + the basic auth parameters. While support for this behaviour has been + re-added, the behaviour is deprecated and will be removed in the + future. + +2.12.3 (2016-12-01) +------------------- + +**Bugfixes** + +- Fixed regression from v2.12.1 for URLs with schemes that begin with + "http". These URLs have historically been processed as though they + were HTTP-schemed URLs, and so have had parameters added. This was + removed in v2.12.2 in an overzealous attempt to resolve problems + with IDNA-encoding those URLs. This change was reverted: the other + fixes for IDNA-encoding have been judged to be sufficient to return + to the behaviour Requests had before v2.12.0. + +2.12.2 (2016-11-30) +------------------- + +**Bugfixes** + +- Fixed several issues with IDNA-encoding URLs that are technically + invalid but which are widely accepted. Requests will now attempt to + IDNA-encode a URL if it can but, if it fails, and the host contains + only ASCII characters, it will be passed through optimistically. + This will allow users to opt-in to using IDNA2003 themselves if they + want to, and will also allow technically invalid but still common + hostnames. +- Fixed an issue where URLs with leading whitespace would raise + `InvalidSchema` errors. +- Fixed an issue where some URLs without the HTTP or HTTPS schemes + would still have HTTP URL preparation applied to them. +- Fixed an issue where Unicode strings could not be used in basic + auth. +- Fixed an issue encountered by some Requests plugins where + constructing a Response object would cause `Response.content` to + raise an `AttributeError`. + +2.12.1 (2016-11-16) +------------------- + +**Bugfixes** + +- Updated setuptools 'security' extra for the new PyOpenSSL backend in + urllib3. + +**Miscellaneous** + +- Updated bundled urllib3 to 1.19.1. + +2.12.0 (2016-11-15) +------------------- + +**Improvements** + +- Updated support for internationalized domain names from IDNA2003 to + IDNA2008. This updated support is required for several forms of IDNs + and is mandatory for .de domains. +- Much improved heuristics for guessing content lengths: Requests will + no longer read an entire `StringIO` into memory. +- Much improved logic for recalculating `Content-Length` headers for + `PreparedRequest` objects. +- Improved tolerance for file-like objects that have no `tell` method + but do have a `seek` method. +- Anything that is a subclass of `Mapping` is now treated like a + dictionary by the `data=` keyword argument. +- Requests now tolerates empty passwords in proxy credentials, rather + than stripping the credentials. +- If a request is made with a file-like object as the body and that + request is redirected with a 307 or 308 status code, Requests will + now attempt to rewind the body object so it can be replayed. + +**Bugfixes** + +- When calling `response.close`, the call to `close` will be + propagated through to non-urllib3 backends. +- Fixed issue where the `ALL_PROXY` environment variable would be + preferred over scheme-specific variables like `HTTP_PROXY`. +- Fixed issue where non-UTF8 reason phrases got severely mangled by + falling back to decoding using ISO 8859-1 instead. +- Fixed a bug where Requests would not correctly correlate cookies set + when using custom Host headers if those Host headers did not use the + native string type for the platform. + +**Miscellaneous** + +- Updated bundled urllib3 to 1.19. +- Updated bundled certifi certs to 2016.09.26. + +2.11.1 (2016-08-17) +------------------- + +**Bugfixes** + +- Fixed a bug when using `iter_content` with `decode_unicode=True` for + streamed bodies would raise `AttributeError`. This bug was + introduced in 2.11. +- Strip Content-Type and Transfer-Encoding headers from the header + block when following a redirect that transforms the verb from + POST/PUT to GET. + +2.11.0 (2016-08-08) +------------------- + +**Improvements** + +- Added support for the `ALL_PROXY` environment variable. +- Reject header values that contain leading whitespace or newline + characters to reduce risk of header smuggling. + +**Bugfixes** + +- Fixed occasional `TypeError` when attempting to decode a JSON + response that occurred in an error case. Now correctly returns a + `ValueError`. +- Requests would incorrectly ignore a non-CIDR IP address in the + `NO_PROXY` environment variables: Requests now treats it as a + specific IP. +- Fixed a bug when sending JSON data that could cause us to encounter + obscure OpenSSL errors in certain network conditions (yes, really). +- Added type checks to ensure that `iter_content` only accepts + integers and `None` for chunk sizes. +- Fixed issue where responses whose body had not been fully consumed + would have the underlying connection closed but not returned to the + connection pool, which could cause Requests to hang in situations + where the `HTTPAdapter` had been configured to use a blocking + connection pool. + +**Miscellaneous** + +- Updated bundled urllib3 to 1.16. +- Some previous releases accidentally accepted non-strings as + acceptable header values. This release does not. + +2.10.0 (2016-04-29) +------------------- + +**New Features** + +- SOCKS Proxy Support! (requires PySocks; + `$ pip install requests[socks]`) + +**Miscellaneous** + +- Updated bundled urllib3 to 1.15.1. + +2.9.2 (2016-04-29) +------------------ + +**Improvements** + +- Change built-in CaseInsensitiveDict (used for headers) to use + OrderedDict as its underlying datastore. + +**Bugfixes** + +- Don't use redirect\_cache if allow\_redirects=False +- When passed objects that throw exceptions from `tell()`, send them + via chunked transfer encoding instead of failing. +- Raise a ProxyError for proxy related connection issues. + +2.9.1 (2015-12-21) +------------------ + +**Bugfixes** + +- Resolve regression introduced in 2.9.0 that made it impossible to + send binary strings as bodies in Python 3. +- Fixed errors when calculating cookie expiration dates in certain + locales. + +**Miscellaneous** + +- Updated bundled urllib3 to 1.13.1. + +2.9.0 (2015-12-15) +------------------ + +**Minor Improvements** (Backwards compatible) + +- The `verify` keyword argument now supports being passed a path to a + directory of CA certificates, not just a single-file bundle. +- Warnings are now emitted when sending files opened in text mode. +- Added the 511 Network Authentication Required status code to the + status code registry. + +**Bugfixes** + +- For file-like objects that are not sought to the very beginning, we + now send the content length for the number of bytes we will actually + read, rather than the total size of the file, allowing partial file + uploads. +- When uploading file-like objects, if they are empty or have no + obvious content length we set `Transfer-Encoding: chunked` rather + than `Content-Length: 0`. +- We correctly receive the response in buffered mode when uploading + chunked bodies. +- We now handle being passed a query string as a bytestring on Python + 3, by decoding it as UTF-8. +- Sessions are now closed in all cases (exceptional and not) when + using the functional API rather than leaking and waiting for the + garbage collector to clean them up. +- Correctly handle digest auth headers with a malformed `qop` + directive that contains no token, by treating it the same as if no + `qop` directive was provided at all. +- Minor performance improvements when removing specific cookies by + name. + +**Miscellaneous** + +- Updated urllib3 to 1.13. + +2.8.1 (2015-10-13) +------------------ + +**Bugfixes** + +- Update certificate bundle to match `certifi` 2015.9.6.2's weak + certificate bundle. +- Fix a bug in 2.8.0 where requests would raise `ConnectTimeout` + instead of `ConnectionError` +- When using the PreparedRequest flow, requests will now correctly + respect the `json` parameter. Broken in 2.8.0. +- When using the PreparedRequest flow, requests will now correctly + handle a Unicode-string method name on Python 2. Broken in 2.8.0. + +2.8.0 (2015-10-05) +------------------ + +**Minor Improvements** (Backwards Compatible) + +- Requests now supports per-host proxies. This allows the `proxies` + dictionary to have entries of the form + `{'://': ''}`. Host-specific proxies will + be used in preference to the previously-supported scheme-specific + ones, but the previous syntax will continue to work. +- `Response.raise_for_status` now prints the URL that failed as part + of the exception message. +- `requests.utils.get_netrc_auth` now takes an `raise_errors` kwarg, + defaulting to `False`. When `True`, errors parsing `.netrc` files + cause exceptions to be thrown. +- Change to bundled projects import logic to make it easier to + unbundle requests downstream. +- Changed the default User-Agent string to avoid leaking data on + Linux: now contains only the requests version. + +**Bugfixes** + +- The `json` parameter to `post()` and friends will now only be used + if neither `data` nor `files` are present, consistent with the + documentation. +- We now ignore empty fields in the `NO_PROXY` environment variable. +- Fixed problem where `httplib.BadStatusLine` would get raised if + combining `stream=True` with `contextlib.closing`. +- Prevented bugs where we would attempt to return the same connection + back to the connection pool twice when sending a Chunked body. +- Miscellaneous minor internal changes. +- Digest Auth support is now thread safe. + +**Updates** + +- Updated urllib3 to 1.12. + +2.7.0 (2015-05-03) +------------------ + +This is the first release that follows our new release process. For +more, see [our +documentation](https://requests.readthedocs.io/en/latest/community/release-process/). + +**Bugfixes** + +- Updated urllib3 to 1.10.4, resolving several bugs involving chunked + transfer encoding and response framing. + +2.6.2 (2015-04-23) +------------------ + +**Bugfixes** + +- Fix regression where compressed data that was sent as chunked data + was not properly decompressed. (\#2561) + +2.6.1 (2015-04-22) +------------------ + +**Bugfixes** + +- Remove VendorAlias import machinery introduced in v2.5.2. +- Simplify the PreparedRequest.prepare API: We no longer require the + user to pass an empty list to the hooks keyword argument. (c.f. + \#2552) +- Resolve redirects now receives and forwards all of the original + arguments to the adapter. (\#2503) +- Handle UnicodeDecodeErrors when trying to deal with a unicode URL + that cannot be encoded in ASCII. (\#2540) +- Populate the parsed path of the URI field when performing Digest + Authentication. (\#2426) +- Copy a PreparedRequest's CookieJar more reliably when it is not an + instance of RequestsCookieJar. (\#2527) + +2.6.0 (2015-03-14) +------------------ + +**Bugfixes** + +- CVE-2015-2296: Fix handling of cookies on redirect. Previously a + cookie without a host value set would use the hostname for the + redirected URL exposing requests users to session fixation attacks + and potentially cookie stealing. This was disclosed privately by + Matthew Daley of [BugFuzz](https://bugfuzz.com). This affects all + versions of requests from v2.1.0 to v2.5.3 (inclusive on both ends). +- Fix error when requests is an `install_requires` dependency and + `python setup.py test` is run. (\#2462) +- Fix error when urllib3 is unbundled and requests continues to use + the vendored import location. +- Include fixes to `urllib3`'s header handling. +- Requests' handling of unvendored dependencies is now more + restrictive. + +**Features and Improvements** + +- Support bytearrays when passed as parameters in the `files` + argument. (\#2468) +- Avoid data duplication when creating a request with `str`, `bytes`, + or `bytearray` input to the `files` argument. + +2.5.3 (2015-02-24) +------------------ + +**Bugfixes** + +- Revert changes to our vendored certificate bundle. For more context + see (\#2455, \#2456, and ) + +2.5.2 (2015-02-23) +------------------ + +**Features and Improvements** + +- Add sha256 fingerprint support. + ([shazow/urllib3\#540](https://github.com/shazow/urllib3/pull/540)) +- Improve the performance of headers. + ([shazow/urllib3\#544](https://github.com/shazow/urllib3/pull/544)) + +**Bugfixes** + +- Copy pip's import machinery. When downstream redistributors remove + requests.packages.urllib3 the import machinery will continue to let + those same symbols work. Example usage in requests' documentation + and 3rd-party libraries relying on the vendored copies of urllib3 + will work without having to fallback to the system urllib3. +- Attempt to quote parts of the URL on redirect if unquoting and then + quoting fails. (\#2356) +- Fix filename type check for multipart form-data uploads. (\#2411) +- Properly handle the case where a server issuing digest + authentication challenges provides both auth and auth-int + qop-values. (\#2408) +- Fix a socket leak. + ([shazow/urllib3\#549](https://github.com/shazow/urllib3/pull/549)) +- Fix multiple `Set-Cookie` headers properly. + ([shazow/urllib3\#534](https://github.com/shazow/urllib3/pull/534)) +- Disable the built-in hostname verification. + ([shazow/urllib3\#526](https://github.com/shazow/urllib3/pull/526)) +- Fix the behaviour of decoding an exhausted stream. + ([shazow/urllib3\#535](https://github.com/shazow/urllib3/pull/535)) + +**Security** + +- Pulled in an updated `cacert.pem`. +- Drop RC4 from the default cipher list. + ([shazow/urllib3\#551](https://github.com/shazow/urllib3/pull/551)) + +2.5.1 (2014-12-23) +------------------ + +**Behavioural Changes** + +- Only catch HTTPErrors in raise\_for\_status (\#2382) + +**Bugfixes** + +- Handle LocationParseError from urllib3 (\#2344) +- Handle file-like object filenames that are not strings (\#2379) +- Unbreak HTTPDigestAuth handler. Allow new nonces to be negotiated + (\#2389) + +2.5.0 (2014-12-01) +------------------ + +**Improvements** + +- Allow usage of urllib3's Retry object with HTTPAdapters (\#2216) +- The `iter_lines` method on a response now accepts a delimiter with + which to split the content (\#2295) + +**Behavioural Changes** + +- Add deprecation warnings to functions in requests.utils that will be + removed in 3.0 (\#2309) +- Sessions used by the functional API are always closed (\#2326) +- Restrict requests to HTTP/1.1 and HTTP/1.0 (stop accepting HTTP/0.9) + (\#2323) + +**Bugfixes** + +- Only parse the URL once (\#2353) +- Allow Content-Length header to always be overridden (\#2332) +- Properly handle files in HTTPDigestAuth (\#2333) +- Cap redirect\_cache size to prevent memory abuse (\#2299) +- Fix HTTPDigestAuth handling of redirects after authenticating + successfully (\#2253) +- Fix crash with custom method parameter to Session.request (\#2317) +- Fix how Link headers are parsed using the regular expression library + (\#2271) + +**Documentation** + +- Add more references for interlinking (\#2348) +- Update CSS for theme (\#2290) +- Update width of buttons and sidebar (\#2289) +- Replace references of Gittip with Gratipay (\#2282) +- Add link to changelog in sidebar (\#2273) + +2.4.3 (2014-10-06) +------------------ + +**Bugfixes** + +- Unicode URL improvements for Python 2. +- Re-order JSON param for backwards compat. +- Automatically defrag authentication schemes from host/pass URIs. + ([\#2249](https://github.com/psf/requests/issues/2249)) + +2.4.2 (2014-10-05) +------------------ + +**Improvements** + +- FINALLY! Add json parameter for uploads! + ([\#2258](https://github.com/psf/requests/pull/2258)) +- Support for bytestring URLs on Python 3.x + ([\#2238](https://github.com/psf/requests/pull/2238)) + +**Bugfixes** + +- Avoid getting stuck in a loop + ([\#2244](https://github.com/psf/requests/pull/2244)) +- Multiple calls to iter\* fail with unhelpful error. + ([\#2240](https://github.com/psf/requests/issues/2240), + [\#2241](https://github.com/psf/requests/issues/2241)) + +**Documentation** + +- Correct redirection introduction + ([\#2245](https://github.com/psf/requests/pull/2245/)) +- Added example of how to send multiple files in one request. + ([\#2227](https://github.com/psf/requests/pull/2227/)) +- Clarify how to pass a custom set of CAs + ([\#2248](https://github.com/psf/requests/pull/2248/)) + +2.4.1 (2014-09-09) +------------------ + +- Now has a "security" package extras set, + `$ pip install requests[security]` +- Requests will now use Certifi if it is available. +- Capture and re-raise urllib3 ProtocolError +- Bugfix for responses that attempt to redirect to themselves forever + (wtf?). + +2.4.0 (2014-08-29) +------------------ + +**Behavioral Changes** + +- `Connection: keep-alive` header is now sent automatically. + +**Improvements** + +- Support for connect timeouts! Timeout now accepts a tuple (connect, + read) which is used to set individual connect and read timeouts. +- Allow copying of PreparedRequests without headers/cookies. +- Updated bundled urllib3 version. +- Refactored settings loading from environment -- new + Session.merge\_environment\_settings. +- Handle socket errors in iter\_content. + +2.3.0 (2014-05-16) +------------------ + +**API Changes** + +- New `Response` property `is_redirect`, which is true when the + library could have processed this response as a redirection (whether + or not it actually did). +- The `timeout` parameter now affects requests with both `stream=True` + and `stream=False` equally. +- The change in v2.0.0 to mandate explicit proxy schemes has been + reverted. Proxy schemes now default to `http://`. +- The `CaseInsensitiveDict` used for HTTP headers now behaves like a + normal dictionary when references as string or viewed in the + interpreter. + +**Bugfixes** + +- No longer expose Authorization or Proxy-Authorization headers on + redirect. Fix CVE-2014-1829 and CVE-2014-1830 respectively. +- Authorization is re-evaluated each redirect. +- On redirect, pass url as native strings. +- Fall-back to autodetected encoding for JSON when Unicode detection + fails. +- Headers set to `None` on the `Session` are now correctly not sent. +- Correctly honor `decode_unicode` even if it wasn't used earlier in + the same response. +- Stop advertising `compress` as a supported Content-Encoding. +- The `Response.history` parameter is now always a list. +- Many, many `urllib3` bugfixes. + +2.2.1 (2014-01-23) +------------------ + +**Bugfixes** + +- Fixes incorrect parsing of proxy credentials that contain a literal + or encoded '\#' character. +- Assorted urllib3 fixes. + +2.2.0 (2014-01-09) +------------------ + +**API Changes** + +- New exception: `ContentDecodingError`. Raised instead of `urllib3` + `DecodeError` exceptions. + +**Bugfixes** + +- Avoid many many exceptions from the buggy implementation of + `proxy_bypass` on OS X in Python 2.6. +- Avoid crashing when attempting to get authentication credentials + from \~/.netrc when running as a user without a home directory. +- Use the correct pool size for pools of connections to proxies. +- Fix iteration of `CookieJar` objects. +- Ensure that cookies are persisted over redirect. +- Switch back to using chardet, since it has merged with charade. + +2.1.0 (2013-12-05) +------------------ + +- Updated CA Bundle, of course. +- Cookies set on individual Requests through a `Session` (e.g. via + `Session.get()`) are no longer persisted to the `Session`. +- Clean up connections when we hit problems during chunked upload, + rather than leaking them. +- Return connections to the pool when a chunked upload is successful, + rather than leaking it. +- Match the HTTPbis recommendation for HTTP 301 redirects. +- Prevent hanging when using streaming uploads and Digest Auth when a + 401 is received. +- Values of headers set by Requests are now always the native string + type. +- Fix previously broken SNI support. +- Fix accessing HTTP proxies using proxy authentication. +- Unencode HTTP Basic usernames and passwords extracted from URLs. +- Support for IP address ranges for no\_proxy environment variable +- Parse headers correctly when users override the default `Host:` + header. +- Avoid munging the URL in case of case-sensitive servers. +- Looser URL handling for non-HTTP/HTTPS urls. +- Accept unicode methods in Python 2.6 and 2.7. +- More resilient cookie handling. +- Make `Response` objects pickleable. +- Actually added MD5-sess to Digest Auth instead of pretending to like + last time. +- Updated internal urllib3. +- Fixed @Lukasa's lack of taste. + +2.0.1 (2013-10-24) +------------------ + +- Updated included CA Bundle with new mistrusts and automated process + for the future +- Added MD5-sess to Digest Auth +- Accept per-file headers in multipart file POST messages. +- Fixed: Don't send the full URL on CONNECT messages. +- Fixed: Correctly lowercase a redirect scheme. +- Fixed: Cookies not persisted when set via functional API. +- Fixed: Translate urllib3 ProxyError into a requests ProxyError + derived from ConnectionError. +- Updated internal urllib3 and chardet. + +2.0.0 (2013-09-24) +------------------ + +**API Changes:** + +- Keys in the Headers dictionary are now native strings on all Python + versions, i.e. bytestrings on Python 2, unicode on Python 3. +- Proxy URLs now *must* have an explicit scheme. A `MissingSchema` + exception will be raised if they don't. +- Timeouts now apply to read time if `Stream=False`. +- `RequestException` is now a subclass of `IOError`, not + `RuntimeError`. +- Added new method to `PreparedRequest` objects: + `PreparedRequest.copy()`. +- Added new method to `Session` objects: `Session.update_request()`. + This method updates a `Request` object with the data (e.g. cookies) + stored on the `Session`. +- Added new method to `Session` objects: `Session.prepare_request()`. + This method updates and prepares a `Request` object, and returns the + corresponding `PreparedRequest` object. +- Added new method to `HTTPAdapter` objects: + `HTTPAdapter.proxy_headers()`. This should not be called directly, + but improves the subclass interface. +- `httplib.IncompleteRead` exceptions caused by incorrect chunked + encoding will now raise a Requests `ChunkedEncodingError` instead. +- Invalid percent-escape sequences now cause a Requests `InvalidURL` + exception to be raised. +- HTTP 208 no longer uses reason phrase `"im_used"`. Correctly uses + `"already_reported"`. +- HTTP 226 reason added (`"im_used"`). + +**Bugfixes:** + +- Vastly improved proxy support, including the CONNECT verb. Special + thanks to the many contributors who worked towards this improvement. +- Cookies are now properly managed when 401 authentication responses + are received. +- Chunked encoding fixes. +- Support for mixed case schemes. +- Better handling of streaming downloads. +- Retrieve environment proxies from more locations. +- Minor cookies fixes. +- Improved redirect behaviour. +- Improved streaming behaviour, particularly for compressed data. +- Miscellaneous small Python 3 text encoding bugs. +- `.netrc` no longer overrides explicit auth. +- Cookies set by hooks are now correctly persisted on Sessions. +- Fix problem with cookies that specify port numbers in their host + field. +- `BytesIO` can be used to perform streaming uploads. +- More generous parsing of the `no_proxy` environment variable. +- Non-string objects can be passed in data values alongside files. + +1.2.3 (2013-05-25) +------------------ + +- Simple packaging fix + +1.2.2 (2013-05-23) +------------------ + +- Simple packaging fix + +1.2.1 (2013-05-20) +------------------ + +- 301 and 302 redirects now change the verb to GET for all verbs, not + just POST, improving browser compatibility. +- Python 3.3.2 compatibility +- Always percent-encode location headers +- Fix connection adapter matching to be most-specific first +- new argument to the default connection adapter for passing a block + argument +- prevent a KeyError when there's no link headers + +1.2.0 (2013-03-31) +------------------ + +- Fixed cookies on sessions and on requests +- Significantly change how hooks are dispatched - hooks now receive + all the arguments specified by the user when making a request so + hooks can make a secondary request with the same parameters. This is + especially necessary for authentication handler authors +- certifi support was removed +- Fixed bug where using OAuth 1 with body `signature_type` sent no + data +- Major proxy work thanks to @Lukasa including parsing of proxy + authentication from the proxy url +- Fix DigestAuth handling too many 401s +- Update vendored urllib3 to include SSL bug fixes +- Allow keyword arguments to be passed to `json.loads()` via the + `Response.json()` method +- Don't send `Content-Length` header by default on `GET` or `HEAD` + requests +- Add `elapsed` attribute to `Response` objects to time how long a + request took. +- Fix `RequestsCookieJar` +- Sessions and Adapters are now picklable, i.e., can be used with the + multiprocessing library +- Update charade to version 1.0.3 + +The change in how hooks are dispatched will likely cause a great deal of +issues. + +1.1.0 (2013-01-10) +------------------ + +- CHUNKED REQUESTS +- Support for iterable response bodies +- Assume servers persist redirect params +- Allow explicit content types to be specified for file data +- Make merge\_kwargs case-insensitive when looking up keys + +1.0.3 (2012-12-18) +------------------ + +- Fix file upload encoding bug +- Fix cookie behavior + +1.0.2 (2012-12-17) +------------------ + +- Proxy fix for HTTPAdapter. + +1.0.1 (2012-12-17) +------------------ + +- Cert verification exception bug. +- Proxy fix for HTTPAdapter. + +1.0.0 (2012-12-17) +------------------ + +- Massive Refactor and Simplification +- Switch to Apache 2.0 license +- Swappable Connection Adapters +- Mountable Connection Adapters +- Mutable ProcessedRequest chain +- /s/prefetch/stream +- Removal of all configuration +- Standard library logging +- Make Response.json() callable, not property. +- Usage of new charade project, which provides python 2 and 3 + simultaneous chardet. +- Removal of all hooks except 'response' +- Removal of all authentication helpers (OAuth, Kerberos) + +This is not a backwards compatible change. + +0.14.2 (2012-10-27) +------------------- + +- Improved mime-compatible JSON handling +- Proxy fixes +- Path hack fixes +- Case-Insensitive Content-Encoding headers +- Support for CJK parameters in form posts + +0.14.1 (2012-10-01) +------------------- + +- Python 3.3 Compatibility +- Simply default accept-encoding +- Bugfixes + +0.14.0 (2012-09-02) +------------------- + +- No more iter\_content errors if already downloaded. + +0.13.9 (2012-08-25) +------------------- + +- Fix for OAuth + POSTs +- Remove exception eating from dispatch\_hook +- General bugfixes + +0.13.8 (2012-08-21) +------------------- + +- Incredible Link header support :) + +0.13.7 (2012-08-19) +------------------- + +- Support for (key, value) lists everywhere. +- Digest Authentication improvements. +- Ensure proxy exclusions work properly. +- Clearer UnicodeError exceptions. +- Automatic casting of URLs to strings (fURL and such) +- Bugfixes. + +0.13.6 (2012-08-06) +------------------- + +- Long awaited fix for hanging connections! + +0.13.5 (2012-07-27) +------------------- + +- Packaging fix + +0.13.4 (2012-07-27) +------------------- + +- GSSAPI/Kerberos authentication! +- App Engine 2.7 Fixes! +- Fix leaking connections (from urllib3 update) +- OAuthlib path hack fix +- OAuthlib URL parameters fix. + +0.13.3 (2012-07-12) +------------------- + +- Use simplejson if available. +- Do not hide SSLErrors behind Timeouts. +- Fixed param handling with urls containing fragments. +- Significantly improved information in User Agent. +- client certificates are ignored when verify=False + +0.13.2 (2012-06-28) +------------------- + +- Zero dependencies (once again)! +- New: Response.reason +- Sign querystring parameters in OAuth 1.0 +- Client certificates no longer ignored when verify=False +- Add openSUSE certificate support + +0.13.1 (2012-06-07) +------------------- + +- Allow passing a file or file-like object as data. +- Allow hooks to return responses that indicate errors. +- Fix Response.text and Response.json for body-less responses. + +0.13.0 (2012-05-29) +------------------- + +- Removal of Requests.async in favor of + [grequests](https://github.com/kennethreitz/grequests) +- Allow disabling of cookie persistence. +- New implementation of safe\_mode +- cookies.get now supports default argument +- Session cookies not saved when Session.request is called with + return\_response=False +- Env: no\_proxy support. +- RequestsCookieJar improvements. +- Various bug fixes. + +0.12.1 (2012-05-08) +------------------- + +- New `Response.json` property. +- Ability to add string file uploads. +- Fix out-of-range issue with iter\_lines. +- Fix iter\_content default size. +- Fix POST redirects containing files. + +0.12.0 (2012-05-02) +------------------- + +- EXPERIMENTAL OAUTH SUPPORT! +- Proper CookieJar-backed cookies interface with awesome dict-like + interface. +- Speed fix for non-iterated content chunks. +- Move `pre_request` to a more usable place. +- New `pre_send` hook. +- Lazily encode data, params, files. +- Load system Certificate Bundle if `certify` isn't available. +- Cleanups, fixes. + +0.11.2 (2012-04-22) +------------------- + +- Attempt to use the OS's certificate bundle if `certifi` isn't + available. +- Infinite digest auth redirect fix. +- Multi-part file upload improvements. +- Fix decoding of invalid %encodings in URLs. +- If there is no content in a response don't throw an error the second + time that content is attempted to be read. +- Upload data on redirects. + +0.11.1 (2012-03-30) +------------------- + +- POST redirects now break RFC to do what browsers do: Follow up with + a GET. +- New `strict_mode` configuration to disable new redirect behavior. + +0.11.0 (2012-03-14) +------------------- + +- Private SSL Certificate support +- Remove select.poll from Gevent monkeypatching +- Remove redundant generator for chunked transfer encoding +- Fix: Response.ok raises Timeout Exception in safe\_mode + +0.10.8 (2012-03-09) +------------------- + +- Generate chunked ValueError fix +- Proxy configuration by environment variables +- Simplification of iter\_lines. +- New trust\_env configuration for disabling system/environment hints. +- Suppress cookie errors. + +0.10.7 (2012-03-07) +------------------- + +- encode\_uri = False + +0.10.6 (2012-02-25) +------------------- + +- Allow '=' in cookies. + +0.10.5 (2012-02-25) +------------------- + +- Response body with 0 content-length fix. +- New async.imap. +- Don't fail on netrc. + +0.10.4 (2012-02-20) +------------------- + +- Honor netrc. + +0.10.3 (2012-02-20) +------------------- + +- HEAD requests don't follow redirects anymore. +- raise\_for\_status() doesn't raise for 3xx anymore. +- Make Session objects picklable. +- ValueError for invalid schema URLs. + +0.10.2 (2012-01-15) +------------------- + +- Vastly improved URL quoting. +- Additional allowed cookie key values. +- Attempted fix for "Too many open files" Error +- Replace unicode errors on first pass, no need for second pass. +- Append '/' to bare-domain urls before query insertion. +- Exceptions now inherit from RuntimeError. +- Binary uploads + auth fix. +- Bugfixes. + +0.10.1 (2012-01-23) +------------------- + +- PYTHON 3 SUPPORT! +- Dropped 2.5 Support. (*Backwards Incompatible*) + +0.10.0 (2012-01-21) +------------------- + +- `Response.content` is now bytes-only. (*Backwards Incompatible*) +- New `Response.text` is unicode-only. +- If no `Response.encoding` is specified and `chardet` is available, + `Response.text` will guess an encoding. +- Default to ISO-8859-1 (Western) encoding for "text" subtypes. +- Removal of decode\_unicode. (*Backwards Incompatible*) +- New multiple-hooks system. +- New `Response.register_hook` for registering hooks within the + pipeline. +- `Response.url` is now Unicode. + +0.9.3 (2012-01-18) +------------------ + +- SSL verify=False bugfix (apparent on windows machines). + +0.9.2 (2012-01-18) +------------------ + +- Asynchronous async.send method. +- Support for proper chunk streams with boundaries. +- session argument for Session classes. +- Print entire hook tracebacks, not just exception instance. +- Fix response.iter\_lines from pending next line. +- Fix but in HTTP-digest auth w/ URI having query strings. +- Fix in Event Hooks section. +- Urllib3 update. + +0.9.1 (2012-01-06) +------------------ + +- danger\_mode for automatic Response.raise\_for\_status() +- Response.iter\_lines refactor + +0.9.0 (2011-12-28) +------------------ + +- verify ssl is default. + +0.8.9 (2011-12-28) +------------------ + +- Packaging fix. + +0.8.8 (2011-12-28) +------------------ + +- SSL CERT VERIFICATION! +- Release of Cerifi: Mozilla's cert list. +- New 'verify' argument for SSL requests. +- Urllib3 update. + +0.8.7 (2011-12-24) +------------------ + +- iter\_lines last-line truncation fix +- Force safe\_mode for async requests +- Handle safe\_mode exceptions more consistently +- Fix iteration on null responses in safe\_mode + +0.8.6 (2011-12-18) +------------------ + +- Socket timeout fixes. +- Proxy Authorization support. + +0.8.5 (2011-12-14) +------------------ + +- Response.iter\_lines! + +0.8.4 (2011-12-11) +------------------ + +- Prefetch bugfix. +- Added license to installed version. + +0.8.3 (2011-11-27) +------------------ + +- Converted auth system to use simpler callable objects. +- New session parameter to API methods. +- Display full URL while logging. + +0.8.2 (2011-11-19) +------------------ + +- New Unicode decoding system, based on over-ridable + Response.encoding. +- Proper URL slash-quote handling. +- Cookies with `[`, `]`, and `_` allowed. + +0.8.1 (2011-11-15) +------------------ + +- URL Request path fix +- Proxy fix. +- Timeouts fix. + +0.8.0 (2011-11-13) +------------------ + +- Keep-alive support! +- Complete removal of Urllib2 +- Complete removal of Poster +- Complete removal of CookieJars +- New ConnectionError raising +- Safe\_mode for error catching +- prefetch parameter for request methods +- OPTION method +- Async pool size throttling +- File uploads send real names +- Vendored in urllib3 + +0.7.6 (2011-11-07) +------------------ + +- Digest authentication bugfix (attach query data to path) + +0.7.5 (2011-11-04) +------------------ + +- Response.content = None if there was an invalid response. +- Redirection auth handling. + +0.7.4 (2011-10-26) +------------------ + +- Session Hooks fix. + +0.7.3 (2011-10-23) +------------------ + +- Digest Auth fix. + +0.7.2 (2011-10-23) +------------------ + +- PATCH Fix. + +0.7.1 (2011-10-23) +------------------ + +- Move away from urllib2 authentication handling. +- Fully Remove AuthManager, AuthObject, &c. +- New tuple-based auth system with handler callbacks. + +0.7.0 (2011-10-22) +------------------ + +- Sessions are now the primary interface. +- Deprecated InvalidMethodException. +- PATCH fix. +- New config system (no more global settings). + +0.6.6 (2011-10-19) +------------------ + +- Session parameter bugfix (params merging). + +0.6.5 (2011-10-18) +------------------ + +- Offline (fast) test suite. +- Session dictionary argument merging. + +0.6.4 (2011-10-13) +------------------ + +- Automatic decoding of unicode, based on HTTP Headers. +- New `decode_unicode` setting. +- Removal of `r.read/close` methods. +- New `r.faw` interface for advanced response usage.\* +- Automatic expansion of parameterized headers. + +0.6.3 (2011-10-13) +------------------ + +- Beautiful `requests.async` module, for making async requests w/ + gevent. + +0.6.2 (2011-10-09) +------------------ + +- GET/HEAD obeys allow\_redirects=False. + +0.6.1 (2011-08-20) +------------------ + +- Enhanced status codes experience `\o/` +- Set a maximum number of redirects (`settings.max_redirects`) +- Full Unicode URL support +- Support for protocol-less redirects. +- Allow for arbitrary request types. +- Bugfixes + +0.6.0 (2011-08-17) +------------------ + +- New callback hook system +- New persistent sessions object and context manager +- Transparent Dict-cookie handling +- Status code reference object +- Removed Response.cached +- Added Response.request +- All args are kwargs +- Relative redirect support +- HTTPError handling improvements +- Improved https testing +- Bugfixes + +0.5.1 (2011-07-23) +------------------ + +- International Domain Name Support! +- Access headers without fetching entire body (`read()`) +- Use lists as dicts for parameters +- Add Forced Basic Authentication +- Forced Basic is default authentication type +- `python-requests.org` default User-Agent header +- CaseInsensitiveDict lower-case caching +- Response.history bugfix + +0.5.0 (2011-06-21) +------------------ + +- PATCH Support +- Support for Proxies +- HTTPBin Test Suite +- Redirect Fixes +- settings.verbose stream writing +- Querystrings for all methods +- URLErrors (Connection Refused, Timeout, Invalid URLs) are treated as + explicitly raised + `r.requests.get('hwe://blah'); r.raise_for_status()` + +0.4.1 (2011-05-22) +------------------ + +- Improved Redirection Handling +- New 'allow\_redirects' param for following non-GET/HEAD Redirects +- Settings module refactoring + +0.4.0 (2011-05-15) +------------------ + +- Response.history: list of redirected responses +- Case-Insensitive Header Dictionaries! +- Unicode URLs + +0.3.4 (2011-05-14) +------------------ + +- Urllib2 HTTPAuthentication Recursion fix (Basic/Digest) +- Internal Refactor +- Bytes data upload Bugfix + +0.3.3 (2011-05-12) +------------------ + +- Request timeouts +- Unicode url-encoded data +- Settings context manager and module + +0.3.2 (2011-04-15) +------------------ + +- Automatic Decompression of GZip Encoded Content +- AutoAuth Support for Tupled HTTP Auth + +0.3.1 (2011-04-01) +------------------ + +- Cookie Changes +- Response.read() +- Poster fix + +0.3.0 (2011-02-25) +------------------ + +- Automatic Authentication API Change +- Smarter Query URL Parameterization +- Allow file uploads and POST data together +- + + New Authentication Manager System + + : - Simpler Basic HTTP System + - Supports all built-in urllib2 Auths + - Allows for custom Auth Handlers + +0.2.4 (2011-02-19) +------------------ + +- Python 2.5 Support +- PyPy-c v1.4 Support +- Auto-Authentication tests +- Improved Request object constructor + +0.2.3 (2011-02-15) +------------------ + +- + + New HTTPHandling Methods + + : - Response.\_\_nonzero\_\_ (false if bad HTTP Status) + - Response.ok (True if expected HTTP Status) + - Response.error (Logged HTTPError if bad HTTP Status) + - Response.raise\_for\_status() (Raises stored HTTPError) + +0.2.2 (2011-02-14) +------------------ + +- Still handles request in the event of an HTTPError. (Issue \#2) +- Eventlet and Gevent Monkeypatch support. +- Cookie Support (Issue \#1) + +0.2.1 (2011-02-14) +------------------ + +- Added file attribute to POST and PUT requests for multipart-encode + file uploads. +- Added Request.url attribute for context and redirects + +0.2.0 (2011-02-14) +------------------ + +- Birth! + +0.0.1 (2011-02-13) +------------------ + +- Frustration +- Conception diff --git a/test/fixtures/whole_applications/requests/LICENSE b/test/fixtures/whole_applications/requests/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/test/fixtures/whole_applications/requests/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/test/fixtures/whole_applications/requests/MANIFEST.in b/test/fixtures/whole_applications/requests/MANIFEST.in new file mode 100644 index 0000000..633be36 --- /dev/null +++ b/test/fixtures/whole_applications/requests/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md LICENSE NOTICE HISTORY.md pytest.ini requirements-dev.txt +recursive-include tests *.py diff --git a/test/fixtures/whole_applications/requests/Makefile b/test/fixtures/whole_applications/requests/Makefile new file mode 100644 index 0000000..f74dc42 --- /dev/null +++ b/test/fixtures/whole_applications/requests/Makefile @@ -0,0 +1,27 @@ +.PHONY: docs +init: + pip install -r requirements-dev.txt +test: + # This runs all of the tests on all supported Python versions. + tox -p +ci: + pytest tests --junitxml=report.xml + +test-readme: + python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.rst and HISTORY.rst ok") || echo "Invalid markup in README.rst or HISTORY.rst!" + +flake8: + flake8 --ignore=E501,F401,E128,E402,E731,F821 requests + +coverage: + pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=requests tests + +publish: + pip install 'twine>=1.5.0' + python setup.py sdist bdist_wheel + twine upload dist/* + rm -fr build dist .egg requests.egg-info + +docs: + cd docs && make html + @echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m" diff --git a/test/fixtures/whole_applications/requests/NOTICE b/test/fixtures/whole_applications/requests/NOTICE new file mode 100644 index 0000000..1ff62db --- /dev/null +++ b/test/fixtures/whole_applications/requests/NOTICE @@ -0,0 +1,2 @@ +Requests +Copyright 2019 Kenneth Reitz diff --git a/test/fixtures/whole_applications/requests/README.md b/test/fixtures/whole_applications/requests/README.md new file mode 100644 index 0000000..c90ef08 --- /dev/null +++ b/test/fixtures/whole_applications/requests/README.md @@ -0,0 +1,78 @@ +# Requests + +**Requests** is a simple, yet elegant, HTTP library. + +```python +>>> import requests +>>> r = requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass')) +>>> r.status_code +200 +>>> r.headers['content-type'] +'application/json; charset=utf8' +>>> r.encoding +'utf-8' +>>> r.text +'{"authenticated": true, ...' +>>> r.json() +{'authenticated': True, ...} +``` + +Requests allows you to send HTTP/1.1 requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your `PUT` & `POST` data — but nowadays, just use the `json` method! + +Requests is one of the most downloaded Python packages today, pulling in around `30M downloads / week`— according to GitHub, Requests is currently [depended upon](https://github.com/psf/requests/network/dependents?package_id=UGFja2FnZS01NzA4OTExNg%3D%3D) by `1,000,000+` repositories. You may certainly put your trust in this code. + +[![Downloads](https://pepy.tech/badge/requests/month)](https://pepy.tech/project/requests) +[![Supported Versions](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests) +[![Contributors](https://img.shields.io/github/contributors/psf/requests.svg)](https://github.com/psf/requests/graphs/contributors) + +## Installing Requests and Supported Versions + +Requests is available on PyPI: + +```console +$ python -m pip install requests +``` + +Requests officially supports Python 3.7+. + +## Supported Features & Best–Practices + +Requests is ready for the demands of building robust and reliable HTTP–speaking applications, for the needs of today. + +- Keep-Alive & Connection Pooling +- International Domains and URLs +- Sessions with Cookie Persistence +- Browser-style TLS/SSL Verification +- Basic & Digest Authentication +- Familiar `dict`–like Cookies +- Automatic Content Decompression and Decoding +- Multi-part File Uploads +- SOCKS Proxy Support +- Connection Timeouts +- Streaming Downloads +- Automatic honoring of `.netrc` +- Chunked HTTP Requests + +## API Reference and User Guide available on [Read the Docs](https://requests.readthedocs.io) + +[![Read the Docs](https://raw.githubusercontent.com/psf/requests/main/ext/ss.png)](https://requests.readthedocs.io) + +## Cloning the repository + +When cloning the Requests repository, you may need to add the `-c +fetch.fsck.badTimezone=ignore` flag to avoid an error about a bad commit (see +[this issue](https://github.com/psf/requests/issues/2690) for more background): + +```shell +git clone -c fetch.fsck.badTimezone=ignore https://github.com/psf/requests.git +``` + +You can also apply this setting to your global Git config: + +```shell +git config --global fetch.fsck.badTimezone ignore +``` + +--- + +[![Kenneth Reitz](https://raw.githubusercontent.com/psf/requests/main/ext/kr.png)](https://kennethreitz.org) [![Python Software Foundation](https://raw.githubusercontent.com/psf/requests/main/ext/psf.png)](https://www.python.org/psf) diff --git a/test/fixtures/whole_applications/requests/docs/.nojekyll b/test/fixtures/whole_applications/requests/docs/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/.nojekyll @@ -0,0 +1 @@ + diff --git a/test/fixtures/whole_applications/requests/docs/Makefile b/test/fixtures/whole_applications/requests/docs/Makefile new file mode 100644 index 0000000..08a2acf --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/Makefile @@ -0,0 +1,216 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Requests.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Requests.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Requests" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Requests" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/test/fixtures/whole_applications/requests/docs/_static/custom.css b/test/fixtures/whole_applications/requests/docs/_static/custom.css new file mode 100644 index 0000000..465e8a9 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_static/custom.css @@ -0,0 +1,177 @@ +body > div.document > div.sphinxsidebar > div > form > table > tbody > tr:nth-child(2) > td > select { + width: 100%!important; +} + +#python27 > a { + color: white; +} + +/* Carbon by BuySellAds */ +#carbonads { + display: block; + overflow: hidden; + margin: 1.5em 0 2em; + padding: 1em; + border: solid 1px #cccccc; + border-radius: 2px; + background-color: #eeeeee; + text-align: center; + line-height: 1.5; +} + +#carbonads a { + border-bottom: 0; +} + +#carbonads span { + display: block; + overflow: hidden; +} + +.carbon-img { + display: block; + margin: 0 auto 1em; + text-align: center; +} + +.carbon-text { + display: block; + margin-bottom: 1em; +} + +.carbon-poweredby { + display: block; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 10px; + line-height: 1; +} + + +/* Native CPC by BuySellAds */ + +#native-ribbon #_custom_ { + position: fixed; + right: 0; + bottom: 0; + left: 0; + box-shadow: 0 -1px 4px 1px hsla(0, 0%, 0%, .15); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, + Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; + transition: all .25s ease-in-out; + transform: translateY(calc(100% - 35px)); + + flex-flow: column nowrap; +} + +#native-ribbon #_custom_:hover { + transform: translateY(0); +} + +.native-img { + margin-right: 20px; + max-height: 50px; + border-radius: 3px; +} + +.native-sponsor { + margin: 10px 20px; + text-align: center; + text-transform: uppercase; + letter-spacing: .5px; + font-size: 12px; + transition: all .3s ease-in-out; + transform-origin: left; +} + +#native-ribbon #_custom_:hover .native-sponsor { + margin: 0 20px; + opacity: 0; + transform: scaleY(0); +} + +.native-flex { + display: flex; + padding: 10px 20px 25px; + text-decoration: none; + + flex-flow: row nowrap; + justify-content: center; + align-items: center; +} + +.native-main { + display: flex; + + flex-flow: row nowrap; + align-items: center; +} + +.native-details { + display: flex; + margin-right: 30px; + + flex-flow: column nowrap; +} + +.native-company { + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 10px; +} + +.native-desc { + letter-spacing: 1px; + font-weight: 300; + font-size: 14px; + line-height: 1.4; +} + +.native-cta { + padding: 10px 14px; + border-radius: 3px; + box-shadow: 0 6px 13px 0 hsla(0, 0%, 0%, .15); + text-transform: uppercase; + white-space: nowrap; + letter-spacing: 1px; + font-weight: 400; + font-size: 12px; + transition: all .3s ease-in-out; + transform: translateY(-1px); +} + +.native-cta:hover { + box-shadow: none; + transform: translateY(1px); +} + +@media only screen and (min-width: 320px) and (max-width: 759px) { + .native-flex { + padding: 5px 5px 15px; + flex-direction: column; + + flex-wrap: wrap; + } + + .native-img { + margin: 0; + display: none; + } + + .native-details { + margin: 0; + } + + .native-main { + flex-direction: column; + text-align: left; + + flex-wrap: wrap; + align-content: center; + } + + .native-cta { + display: none; + } +} diff --git a/test/fixtures/whole_applications/requests/docs/_static/requests-sidebar.png b/test/fixtures/whole_applications/requests/docs/_static/requests-sidebar.png new file mode 100644 index 0000000..d2b8a69 Binary files /dev/null and b/test/fixtures/whole_applications/requests/docs/_static/requests-sidebar.png differ diff --git a/test/fixtures/whole_applications/requests/docs/_templates/hacks.html b/test/fixtures/whole_applications/requests/docs/_templates/hacks.html new file mode 100644 index 0000000..eca5dff --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_templates/hacks.html @@ -0,0 +1,80 @@ + + + + + + + + + +
+
+ + diff --git a/test/fixtures/whole_applications/requests/docs/_templates/sidebarintro.html b/test/fixtures/whole_applications/requests/docs/_templates/sidebarintro.html new file mode 100644 index 0000000..2b595b5 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_templates/sidebarintro.html @@ -0,0 +1,37 @@ + + +

+ +

+ +

+ Requests is an elegant and simple HTTP library for Python, built for + human beings. +

+ +

Useful Links

+ + +
+
diff --git a/test/fixtures/whole_applications/requests/docs/_templates/sidebarlogo.html b/test/fixtures/whole_applications/requests/docs/_templates/sidebarlogo.html new file mode 100644 index 0000000..a3454b7 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_templates/sidebarlogo.html @@ -0,0 +1,30 @@ +

+ +

+ +

+ Requests is an elegant and simple HTTP library for Python, built for + human beings. You are currently looking at the documentation of the + development release. +

+ +

Useful Links

+ + diff --git a/test/fixtures/whole_applications/requests/docs/_themes/.gitignore b/test/fixtures/whole_applications/requests/docs/_themes/.gitignore new file mode 100644 index 0000000..66b6e4c --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_themes/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.pyo +.DS_Store diff --git a/test/fixtures/whole_applications/requests/docs/_themes/LICENSE b/test/fixtures/whole_applications/requests/docs/_themes/LICENSE new file mode 100644 index 0000000..3d1e04a --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_themes/LICENSE @@ -0,0 +1,45 @@ +Modifications: + +Copyright (c) 2011 Kenneth Reitz. + + +Original Project: + +Copyright (c) 2010 by Armin Ronacher. + + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/whole_applications/requests/docs/_themes/flask_theme_support.py b/test/fixtures/whole_applications/requests/docs/_themes/flask_theme_support.py new file mode 100644 index 0000000..33f4744 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/_themes/flask_theme_support.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/test/fixtures/whole_applications/requests/docs/api.rst b/test/fixtures/whole_applications/requests/docs/api.rst new file mode 100644 index 0000000..34959dd --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/api.rst @@ -0,0 +1,261 @@ +.. _api: + +Developer Interface +=================== + +.. module:: requests + +This part of the documentation covers all the interfaces of Requests. For +parts where Requests depends on external libraries, we document the most +important right here and provide links to the canonical documentation. + + +Main Interface +-------------- + +All of Requests' functionality can be accessed by these 7 methods. +They all return an instance of the :class:`Response ` object. + +.. autofunction:: request + +.. autofunction:: head +.. autofunction:: get +.. autofunction:: post +.. autofunction:: put +.. autofunction:: patch +.. autofunction:: delete + +Exceptions +---------- + +.. autoexception:: requests.RequestException +.. autoexception:: requests.ConnectionError +.. autoexception:: requests.HTTPError +.. autoexception:: requests.URLRequired +.. autoexception:: requests.TooManyRedirects +.. autoexception:: requests.ConnectTimeout +.. autoexception:: requests.ReadTimeout +.. autoexception:: requests.Timeout +.. autoexception:: requests.JSONDecodeError + + +Request Sessions +---------------- + +.. _sessionapi: + +.. autoclass:: Session + :inherited-members: + + +Lower-Level Classes +------------------- + +.. autoclass:: requests.Request + :inherited-members: + +.. autoclass:: Response + :inherited-members: + + +Lower-Lower-Level Classes +------------------------- + +.. autoclass:: requests.PreparedRequest + :inherited-members: + +.. autoclass:: requests.adapters.BaseAdapter + :inherited-members: + +.. autoclass:: requests.adapters.HTTPAdapter + :inherited-members: + +Authentication +-------------- + +.. autoclass:: requests.auth.AuthBase +.. autoclass:: requests.auth.HTTPBasicAuth +.. autoclass:: requests.auth.HTTPProxyAuth +.. autoclass:: requests.auth.HTTPDigestAuth + + + +Encodings +--------- + +.. autofunction:: requests.utils.get_encodings_from_content +.. autofunction:: requests.utils.get_encoding_from_headers +.. autofunction:: requests.utils.get_unicode_from_response + + +.. _api-cookies: + +Cookies +------- + +.. autofunction:: requests.utils.dict_from_cookiejar +.. autofunction:: requests.utils.add_dict_to_cookiejar +.. autofunction:: requests.cookies.cookiejar_from_dict + +.. autoclass:: requests.cookies.RequestsCookieJar + :inherited-members: + +.. autoclass:: requests.cookies.CookieConflictError + :inherited-members: + + + +Status Code Lookup +------------------ + +.. autoclass:: requests.codes + +.. automodule:: requests.status_codes + + +Migrating to 1.x +---------------- + +This section details the main differences between 0.x and 1.x and is meant +to ease the pain of upgrading. + + +API Changes +~~~~~~~~~~~ + +* ``Response.json`` is now a callable and not a property of a response. + + :: + + import requests + r = requests.get('https://api.github.com/events') + r.json() # This *call* raises an exception if JSON decoding fails + +* The ``Session`` API has changed. Sessions objects no longer take parameters. + ``Session`` is also now capitalized, but it can still be + instantiated with a lowercase ``session`` for backwards compatibility. + + :: + + s = requests.Session() # formerly, session took parameters + s.auth = auth + s.headers.update(headers) + r = s.get('https://httpbin.org/headers') + +* All request hooks have been removed except 'response'. + +* Authentication helpers have been broken out into separate modules. See + requests-oauthlib_ and requests-kerberos_. + +.. _requests-oauthlib: https://github.com/requests/requests-oauthlib +.. _requests-kerberos: https://github.com/requests/requests-kerberos + +* The parameter for streaming requests was changed from ``prefetch`` to + ``stream`` and the logic was inverted. In addition, ``stream`` is now + required for raw response reading. + + :: + + # in 0.x, passing prefetch=False would accomplish the same thing + r = requests.get('https://api.github.com/events', stream=True) + for chunk in r.iter_content(8192): + ... + +* The ``config`` parameter to the requests method has been removed. Some of + these options are now configured on a ``Session`` such as keep-alive and + maximum number of redirects. The verbosity option should be handled by + configuring logging. + + :: + + import requests + import logging + + # Enabling debugging at http.client level (requests->urllib3->http.client) + # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA. + # the only thing missing will be the response.body which is not logged. + try: # for Python 3 + from http.client import HTTPConnection + except ImportError: + from httplib import HTTPConnection + HTTPConnection.debuglevel = 1 + + logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests + logging.getLogger().setLevel(logging.DEBUG) + requests_log = logging.getLogger("urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + + requests.get('https://httpbin.org/headers') + + + +Licensing +~~~~~~~~~ + +One key difference that has nothing to do with the API is a change in the +license from the ISC_ license to the `Apache 2.0`_ license. The Apache 2.0 +license ensures that contributions to Requests are also covered by the Apache +2.0 license. + +.. _ISC: https://opensource.org/licenses/ISC +.. _Apache 2.0: https://opensource.org/licenses/Apache-2.0 + + +Migrating to 2.x +---------------- + + +Compared with the 1.0 release, there were relatively few backwards +incompatible changes, but there are still a few issues to be aware of with +this major release. + +For more details on the changes in this release including new APIs, links +to the relevant GitHub issues and some of the bug fixes, read Cory's blog_ +on the subject. + +.. _blog: https://lukasa.co.uk/2013/09/Requests_20/ + + +API Changes +~~~~~~~~~~~ + +* There were a couple changes to how Requests handles exceptions. + ``RequestException`` is now a subclass of ``IOError`` rather than + ``RuntimeError`` as that more accurately categorizes the type of error. + In addition, an invalid URL escape sequence now raises a subclass of + ``RequestException`` rather than a ``ValueError``. + + :: + + requests.get('http://%zz/') # raises requests.exceptions.InvalidURL + + Lastly, ``httplib.IncompleteRead`` exceptions caused by incorrect chunked + encoding will now raise a Requests ``ChunkedEncodingError`` instead. + +* The proxy API has changed slightly. The scheme for a proxy URL is now + required. + + :: + + proxies = { + "http": "10.10.1.10:3128", # use http://10.10.1.10:3128 instead + } + + # In requests 1.x, this was legal, in requests 2.x, + # this raises requests.exceptions.MissingSchema + requests.get("http://example.org", proxies=proxies) + + +Behavioural Changes +~~~~~~~~~~~~~~~~~~~~~~~ + +* Keys in the ``headers`` dictionary are now native strings on all Python + versions, i.e. bytestrings on Python 2 and unicode on Python 3. If the + keys are not native strings (unicode on Python 2 or bytestrings on Python 3) + they will be converted to the native string type assuming UTF-8 encoding. + +* Values in the ``headers`` dictionary should always be strings. This has + been the project's position since before 1.0 but a recent change + (since version 2.11.0) enforces this more strictly. It's advised to avoid + passing header values as unicode when possible. diff --git a/test/fixtures/whole_applications/requests/docs/community/faq.rst b/test/fixtures/whole_applications/requests/docs/community/faq.rst new file mode 100644 index 0000000..9d900be --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/faq.rst @@ -0,0 +1,89 @@ +.. _faq: + +Frequently Asked Questions +========================== + +This part of the documentation answers common questions about Requests. + +Encoded Data? +------------- + +Requests automatically decompresses gzip-encoded responses, and does +its best to decode response content to unicode when possible. + +When either the `brotli `_ or `brotlicffi `_ +package is installed, requests also decodes Brotli-encoded responses. + +You can get direct access to the raw response (and even the socket), +if needed as well. + + +Custom User-Agents? +------------------- + +Requests allows you to easily override User-Agent strings, along with +any other HTTP Header. See `documentation about headers `_. + + + +Why not Httplib2? +----------------- + +Chris Adams gave an excellent summary on +`Hacker News `_: + + httplib2 is part of why you should use requests: it's far more respectable + as a client but not as well documented and it still takes way too much code + for basic operations. I appreciate what httplib2 is trying to do, that + there's a ton of hard low-level annoyances in building a modern HTTP + client, but really, just use requests instead. Kenneth Reitz is very + motivated and he gets the degree to which simple things should be simple + whereas httplib2 feels more like an academic exercise than something + people should use to build production systems[1]. + + Disclosure: I'm listed in the requests AUTHORS file but can claim credit + for, oh, about 0.0001% of the awesomeness. + + 1. http://code.google.com/p/httplib2/issues/detail?id=96 is a good example: + an annoying bug which affect many people, there was a fix available for + months, which worked great when I applied it in a fork and pounded a couple + TB of data through it, but it took over a year to make it into trunk and + even longer to make it onto PyPI where any other project which required " + httplib2" would get the working version. + + +Python 3 Support? +----------------- + +Yes! Requests officially supports Python 3.7+ and PyPy. + +Python 2 Support? +----------------- + +No! As of Requests 2.28.0, Requests no longer supports Python 2.7. Users who +have been unable to migrate should pin to `requests<2.28`. Full information +can be found in `psf/requests#6023 `_. + +It is *highly* recommended users migrate to Python 3.8+ now since Python +2.7 is no longer receiving bug fixes or security updates as of January 1, 2020. + +What are "hostname doesn't match" errors? +----------------------------------------- + +These errors occur when :ref:`SSL certificate verification ` +fails to match the certificate the server responds with to the hostname +Requests thinks it's contacting. If you're certain the server's SSL setup is +correct (for example, because you can visit the site with your browser) and +you're using Python 2.7, a possible explanation is that you need +Server-Name-Indication. + +`Server-Name-Indication`_, or SNI, is an official extension to SSL where the +client tells the server what hostname it is contacting. This is important +when servers are using `Virtual Hosting`_. When such servers are hosting +more than one SSL site they need to be able to return the appropriate +certificate based on the hostname the client is connecting to. + +Python 3 already includes native support for SNI in their SSL modules. + +.. _`Server-Name-Indication`: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _`virtual hosting`: https://en.wikipedia.org/wiki/Virtual_hosting diff --git a/test/fixtures/whole_applications/requests/docs/community/out-there.rst b/test/fixtures/whole_applications/requests/docs/community/out-there.rst new file mode 100644 index 0000000..c33ab3c --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/out-there.rst @@ -0,0 +1,22 @@ +Integrations +============ + +Python for iOS +-------------- + +Requests is built into the wonderful `Python for iOS `_ runtime! + +To give it a try, simply:: + + import requests + + +Articles & Talks +================ +- `Python for the Web `_ teaches how to use Python to interact with the web, using Requests. +- `Daniel Greenfeld's Review of Requests `_ +- `My 'Python for Humans' talk `_ ( `audio `_ ) +- `Issac Kelly's 'Consuming Web APIs' talk `_ +- `Blog post about Requests via Yum `_ +- `Russian blog post introducing Requests `_ +- `Sending JSON in Requests `_ diff --git a/test/fixtures/whole_applications/requests/docs/community/recommended.rst b/test/fixtures/whole_applications/requests/docs/community/recommended.rst new file mode 100644 index 0000000..517f4b1 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/recommended.rst @@ -0,0 +1,62 @@ +.. _recommended: + +Recommended Packages and Extensions +=================================== + +Requests has a great variety of powerful and useful third-party extensions. +This page provides an overview of some of the best of them. + +Certifi CA Bundle +----------------- + +`Certifi`_ is a carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the +identity of TLS hosts. It has been extracted from the Requests project. + +.. _Certifi: https://github.com/certifi/python-certifi + +CacheControl +------------ + +`CacheControl`_ is an extension that adds a full HTTP cache to Requests. This +makes your web requests substantially more efficient, and should be used +whenever you're making a lot of web requests. + +.. _CacheControl: https://cachecontrol.readthedocs.io/en/latest/ + +Requests-Toolbelt +----------------- + +`Requests-Toolbelt`_ is a collection of utilities that some users of Requests may desire, +but do not belong in Requests proper. This library is actively maintained +by members of the Requests core team, and reflects the functionality most +requested by users within the community. + +.. _Requests-Toolbelt: https://toolbelt.readthedocs.io/en/latest/index.html + + +Requests-Threads +---------------- + +`Requests-Threads`_ is a Requests session that returns the amazing Twisted's awaitable Deferreds instead of Response objects. This allows the use of ``async``/``await`` keyword usage on Python 3, or Twisted's style of programming, if desired. + +.. _Requests-Threads: https://github.com/requests/requests-threads + +Requests-OAuthlib +----------------- + +`requests-oauthlib`_ makes it possible to do the OAuth dance from Requests +automatically. This is useful for the large number of websites that use OAuth +to provide authentication. It also provides a lot of tweaks that handle ways +that specific OAuth providers differ from the standard specifications. + +.. _requests-oauthlib: https://requests-oauthlib.readthedocs.io/en/latest/ + + +Betamax +------- + +`Betamax`_ records your HTTP interactions so the NSA does not have to. +A VCR imitation designed only for Python-Requests. + +.. _betamax: https://github.com/betamaxpy/betamax diff --git a/test/fixtures/whole_applications/requests/docs/community/release-process.rst b/test/fixtures/whole_applications/requests/docs/community/release-process.rst new file mode 100644 index 0000000..4aa98f7 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/release-process.rst @@ -0,0 +1,53 @@ +Release Process and Rules +========================= + +.. versionadded:: v2.6.2 + +Starting with the version to be released after ``v2.6.2``, the following rules +will govern and describe how the Requests core team produces a new release. + +Major Releases +-------------- + +A major release will include breaking changes. When it is versioned, it will +be versioned as ``vX.0.0``. For example, if the previous release was +``v10.2.7`` the next version will be ``v11.0.0``. + +Breaking changes are changes that break backwards compatibility with prior +versions. If the project were to change the ``text`` attribute on a +``Response`` object to a method, that would only happen in a Major release. + +Major releases may also include miscellaneous bug fixes. The core developers of +Requests are committed to providing a good user experience. This means we're +also committed to preserving backwards compatibility as much as possible. Major +releases will be infrequent and will need strong justifications before they are +considered. + +Minor Releases +-------------- + +A minor release will not include breaking changes but may include miscellaneous +bug fixes. If the previous version of Requests released was ``v10.2.7`` a minor +release would be versioned as ``v10.3.0``. + +Minor releases will be backwards compatible with releases that have the same +major version number. In other words, all versions that would start with +``v10.`` should be compatible with each other. + +Hotfix Releases +--------------- + +A hotfix release will only include bug fixes that were missed when the project +released the previous version. If the previous version of Requests released +``v10.2.7`` the hotfix release would be versioned as ``v10.2.8``. + +Hotfixes will **not** include upgrades to vendored dependencies after +``v2.6.2`` + +Reasoning +--------- + +In the 2.5 and 2.6 release series, the Requests core team upgraded vendored +dependencies and caused a great deal of headaches for both users and the core +team. To reduce this pain, we're forming a concrete set of procedures so +expectations will be properly set. diff --git a/test/fixtures/whole_applications/requests/docs/community/support.rst b/test/fixtures/whole_applications/requests/docs/community/support.rst new file mode 100644 index 0000000..ee905f5 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/support.rst @@ -0,0 +1,31 @@ +.. _support: + +Support +======= + +If you have questions or issues about Requests, there are several options: + +Stack Overflow +-------------- + +If your question does not contain sensitive (possibly proprietary) +information or can be properly anonymized, please ask a question on +`Stack Overflow `_ +and use the tag ``python-requests``. + + +File an Issue +------------- + +If you notice some unexpected behaviour in Requests, or want to see support +for a new feature, +`file an issue on GitHub `_. + + +Send a Tweet +------------ + +If your question is less than 280 characters, feel free to send a tweet to +`@nateprewitt `_, +`@sethmlarson `_, or +`@sigmavirus24 `_. diff --git a/test/fixtures/whole_applications/requests/docs/community/updates.rst b/test/fixtures/whole_applications/requests/docs/community/updates.rst new file mode 100644 index 0000000..c787c45 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/updates.rst @@ -0,0 +1,18 @@ +.. _updates: + + +Community Updates +================= + +If you'd like to stay up to date on the community and development of Requests, +there are several options: + + +GitHub +------ + +The best way to track the development of Requests is through +`the GitHub repo `_. + + +.. include:: ../../HISTORY.md diff --git a/test/fixtures/whole_applications/requests/docs/community/vulnerabilities.rst b/test/fixtures/whole_applications/requests/docs/community/vulnerabilities.rst new file mode 100644 index 0000000..6a9c7d9 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/community/vulnerabilities.rst @@ -0,0 +1,110 @@ +Vulnerability Disclosure +======================== + +If you think you have found a potential security vulnerability in requests, +please email `Nate `_ and `Seth `_ directly. **Do not file a public issue.** + +Our PGP Key fingerprints are: + +- 8722 7E29 AD9C FF5C FAC3 EA6A 44D3 FF97 B80D C864 (`@nateprewitt `_) + +- EDD5 6765 A9D8 4653 CBC8 A134 51B0 6736 1740 F5FC (`@sethmlarson `_) + +You can also contact us on `Keybase `_ with the +profiles above if desired. + +If English is not your first language, please try to describe the problem and +its impact to the best of your ability. For greater detail, please use your +native language and we will try our best to translate it using online services. + +Please also include the code you used to find the problem and the shortest +amount of code necessary to reproduce it. + +Please do not disclose this to anyone else. We will retrieve a CVE identifier +if necessary and give you full credit under whatever name or alias you provide. +We will only request an identifier when we have a fix and can publish it in a +release. + +We will respect your privacy and will only publicize your involvement if you +grant us permission. + +Process +------- + +This following information discusses the process the requests project follows +in response to vulnerability disclosures. If you are disclosing a +vulnerability, this section of the documentation lets you know how we will +respond to your disclosure. + +Timeline +~~~~~~~~ + +When you report an issue, one of the project members will respond to you within +two days *at the outside*. In most cases responses will be faster, usually +within 12 hours. This initial response will at the very least confirm receipt +of the report. + +If we were able to rapidly reproduce the issue, the initial response will also +contain confirmation of the issue. If we are not, we will often ask for more +information about the reproduction scenario. + +Our goal is to have a fix for any vulnerability released within two weeks of +the initial disclosure. This may potentially involve shipping an interim +release that simply disables function while a more mature fix can be prepared, +but will in the vast majority of cases mean shipping a complete release as soon +as possible. + +Throughout the fix process we will keep you up to speed with how the fix is +progressing. Once the fix is prepared, we will notify you that we believe we +have a fix. Often we will ask you to confirm the fix resolves the problem in +your environment, especially if we are not confident of our reproduction +scenario. + +At this point, we will prepare for the release. We will obtain a CVE number +if one is required, providing you with full credit for the discovery. We will +also decide on a planned release date, and let you know when it is. This +release date will *always* be on a weekday. + +At this point we will reach out to our major downstream packagers to notify +them of an impending security-related patch so they can make arrangements. In +addition, these packagers will be provided with the intended patch ahead of +time, to ensure that they are able to promptly release their downstream +packages. Currently the list of people we actively contact *ahead of a public +release* is: + +- Python Maintenance Team, Red Hat (python-maint@redhat.com) +- Daniele Tricoli, Debian (@eriol) + +We will notify these individuals at least a week ahead of our planned release +date to ensure that they have sufficient time to prepare. If you believe you +should be on this list, please let one of the maintainers know at one of the +email addresses at the top of this article. + +On release day, we will push the patch to our public repository, along with an +updated changelog that describes the issue and credits you. We will then issue +a PyPI release containing the patch. + +At this point, we will publicise the release. This will involve mails to +mailing lists, Tweets, and all other communication mechanisms available to the +core team. + +We will also explicitly mention which commits contain the fix to make it easier +for other distributors and users to easily patch their own versions of requests +if upgrading is not an option. + +Previous CVEs +------------- + +- Fixed in 2.20.0 + - `CVE 2018-18074 `_ + +- Fixed in 2.6.0 + + - `CVE 2015-2296 `_, + reported by Matthew Daley of `BugFuzz `_. + +- Fixed in 2.3.0 + + - `CVE 2014-1829 `_ + + - `CVE 2014-1830 `_ diff --git a/test/fixtures/whole_applications/requests/docs/conf.py b/test/fixtures/whole_applications/requests/docs/conf.py new file mode 100644 index 0000000..edbd72b --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/conf.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +# +# Requests documentation build configuration file, created by +# sphinx-quickstart on Fri Feb 19 00:05:47 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# Insert Requests' path into the system. +sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("_themes")) + +import requests + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.viewcode", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = u"Requests" +copyright = u'MMXVIX. A Kenneth Reitz Project' +author = u"Kenneth Reitz" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = requests.__version__ +# The full version, including alpha/beta/rc tags. +release = requests.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = False + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "flask_theme_support.FlaskyStyle" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "alabaster" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "show_powered_by": False, + "github_user": "requests", + "github_repo": "requests", + "github_banner": True, + "show_related": False, + "note_bg": "#FFF59C", +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +html_use_smartypants = False + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"], + "**": [ + "sidebarlogo.html", + "localtoc.html", + "relations.html", + "sourcelink.html", + "searchbox.html", + "hacks.html", + ], +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "Requestsdoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "Requests.tex", u"Requests Documentation", u"Kenneth Reitz", "manual") +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "requests", u"Requests Documentation", [author], 1)] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "Requests", + u"Requests Documentation", + author, + "Requests", + "One line description of project.", + "Miscellaneous", + ) +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The basename for the epub file. It defaults to the project name. +# epub_basename = project + +# The HTML theme for the epub output. Since the default themes are not +# optimized for small screen space, using the same theme for HTML and epub +# output is usually not wise. This defaults to 'epub', a theme designed to save +# visual space. +# epub_theme = 'epub' + +# The language of the text. It defaults to the language option +# or 'en' if the language is not set. +# epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +# epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# epub_identifier = '' + +# A unique identification for the text. +# epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +# epub_cover = () + +# A sequence of (type, uri, title) tuples for the guide element of content.opf. +# epub_guide = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_pre_files = [] + +# HTML files that should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_post_files = [] + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + +# The depth of the table of contents in toc.ncx. +# epub_tocdepth = 3 + +# Allow duplicate toc entries. +# epub_tocdup = True + +# Choose between 'default' and 'includehidden'. +# epub_tocscope = 'default' + +# Fix unsupported image types using the Pillow. +# epub_fix_images = False + +# Scale large images. +# epub_max_image_width = 0 + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# epub_show_urls = 'inline' + +# If false, no index is generated. +# epub_use_index = True + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "urllib3": ("https://urllib3.readthedocs.io/en/latest", None), +} diff --git a/test/fixtures/whole_applications/requests/docs/dev/authors.rst b/test/fixtures/whole_applications/requests/docs/dev/authors.rst new file mode 100644 index 0000000..e9799a9 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/dev/authors.rst @@ -0,0 +1,4 @@ +Authors +======= + +.. include:: ../../AUTHORS.rst diff --git a/test/fixtures/whole_applications/requests/docs/dev/contributing.rst b/test/fixtures/whole_applications/requests/docs/dev/contributing.rst new file mode 100644 index 0000000..961f7c3 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/dev/contributing.rst @@ -0,0 +1,167 @@ +.. _contributing: + +Contributor's Guide +=================== + +If you're reading this, you're probably interested in contributing to Requests. +Thank you very much! Open source projects live-and-die based on the support +they receive from others, and the fact that you're even considering +contributing to the Requests project is *very* generous of you. + +This document lays out guidelines and advice for contributing to this project. +If you're thinking of contributing, please start by reading this document and +getting a feel for how contributing to this project works. If you have any +questions, feel free to reach out to either `Nate Prewitt`_, `Ian Cordasco`_, +or `Seth Michael Larson`_, the primary maintainers. + +.. _Ian Cordasco: http://www.coglib.com/~icordasc/ +.. _Nate Prewitt: https://www.nateprewitt.com/ +.. _Seth Michael Larson: https://sethmlarson.dev/ + +The guide is split into sections based on the type of contribution you're +thinking of making, with a section that covers general guidelines for all +contributors. + +Be Cordial +---------- + + **Be cordial or be on your way**. *—Kenneth Reitz* + +Requests has one very important rule governing all forms of contribution, +including reporting bugs or requesting features. This golden rule is +"`be cordial or be on your way`_". + +**All contributions are welcome**, as long as +everyone involved is treated with respect. + +.. _be cordial or be on your way: https://kenreitz.org/essays/2013/01/27/be-cordial-or-be-on-your-way + +.. _early-feedback: + +Get Early Feedback +------------------ + +If you are contributing, do not feel the need to sit on your contribution until +it is perfectly polished and complete. It helps everyone involved for you to +seek feedback as early as you possibly can. Submitting an early, unfinished +version of your contribution for feedback in no way prejudices your chances of +getting that contribution accepted, and can save you from putting a lot of work +into a contribution that is not suitable for the project. + +Contribution Suitability +------------------------ + +Our project maintainers have the last word on whether or not a contribution is +suitable for Requests. All contributions will be considered carefully, but from +time to time, contributions will be rejected because they do not suit the +current goals or needs of the project. + +If your contribution is rejected, don't despair! As long as you followed these +guidelines, you will have a much better chance of getting your next +contribution accepted. + + +Code Contributions +------------------ + +Steps for Submitting Code +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When contributing code, you'll want to follow this checklist: + +1. Fork the repository on GitHub. +2. Run the tests to confirm they all pass on your system. If they don't, you'll + need to investigate why they fail. If you're unable to diagnose this + yourself, raise it as a bug report by following the guidelines in this + document: :ref:`bug-reports`. +3. Write tests that demonstrate your bug or feature. Ensure that they fail. +4. Make your change. +5. Run the entire test suite again, confirming that all tests pass *including + the ones you just added*. +6. Send a GitHub Pull Request to the main repository's ``main`` branch. + GitHub Pull Requests are the expected method of code collaboration on this + project. + +The following sub-sections go into more detail on some of the points above. + +Code Review +~~~~~~~~~~~ + +Contributions will not be merged until they've been code reviewed. You should +implement any code review feedback unless you strongly object to it. In the +event that you object to the code review feedback, you should make your case +clearly and calmly. If, after doing so, the feedback is judged to still apply, +you must either apply the feedback or withdraw your contribution. + +Code Style +~~~~~~~~~~ + +Requests uses a collection of tools to ensure the code base has a consistent +style as it grows. We have these orchestrated using a tool called +`pre-commit`_. This can be installed locally and run over your changes prior +to opening a PR, and will also be run as part of the CI approval process +before a change is merged. + +You can find the full list of formatting requirements specified in the +`.pre-commit-config.yaml`_ at the top level directory of Requests. + +.. _pre-commit: https://pre-commit.com/ +.. _.pre-commit-config.yaml: https://github.com/psf/requests/blob/main/.pre-commit-config.yaml + +New Contributors +~~~~~~~~~~~~~~~~ + +If you are new or relatively new to Open Source, welcome! Requests aims to +be a gentle introduction to the world of Open Source. If you're concerned about +how best to contribute, please consider mailing a maintainer (listed above) and +asking for help. + +Please also check the :ref:`early-feedback` section. + + +Documentation Contributions +--------------------------- + +Documentation improvements are always welcome! The documentation files live in +the ``docs/`` directory of the codebase. They're written in +`reStructuredText`_, and use `Sphinx`_ to generate the full suite of +documentation. + +When contributing documentation, please do your best to follow the style of the +documentation files. This means a soft-limit of 79 characters wide in your text +files and a semi-formal, yet friendly and approachable, prose style. + +When presenting Python code, use single-quoted strings (``'hello'`` instead of +``"hello"``). + +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Sphinx: http://sphinx-doc.org/index.html + + +.. _bug-reports: + +Bug Reports +----------- + +Bug reports are hugely important! Before you raise one, though, please check +through the `GitHub issues`_, **both open and closed**, to confirm that the bug +hasn't been reported before. Duplicate bug reports are a huge drain on the time +of other contributors, and should be avoided as much as possible. + +.. _GitHub issues: https://github.com/psf/requests/issues + + +Feature Requests +---------------- + +Requests is in a perpetual feature freeze, only the BDFL can add or approve of +new features. The maintainers believe that Requests is a feature-complete +piece of software at this time. + +One of the most important skills to have while maintaining a largely-used +open source project is learning the ability to say "no" to suggested changes, +while keeping an open ear and mind. + +If you believe there is a feature missing, feel free to raise a feature +request, but please do be aware that the overwhelming likelihood is that your +feature request will not be accepted. diff --git a/test/fixtures/whole_applications/requests/docs/index.rst b/test/fixtures/whole_applications/requests/docs/index.rst new file mode 100644 index 0000000..306b60f --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/index.rst @@ -0,0 +1,140 @@ +.. Requests documentation master file, created by + sphinx-quickstart on Sun Feb 13 23:54:25 2011. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Requests: HTTP for Humans™ +========================== + +Release v\ |version|. (:ref:`Installation `) + + +.. image:: https://pepy.tech/badge/requests/month + :target: https://pepy.tech/project/requests + :alt: Requests Downloads Per Month Badge + +.. image:: https://img.shields.io/pypi/l/requests.svg + :target: https://pypi.org/project/requests/ + :alt: License Badge + +.. image:: https://img.shields.io/pypi/wheel/requests.svg + :target: https://pypi.org/project/requests/ + :alt: Wheel Support Badge + +.. image:: https://img.shields.io/pypi/pyversions/requests.svg + :target: https://pypi.org/project/requests/ + :alt: Python Version Support Badge + +**Requests** is an elegant and simple HTTP library for Python, built for human beings. + +------------------- + +**Behold, the power of Requests**:: + + >>> r = requests.get('https://api.github.com/user', auth=('user', 'pass')) + >>> r.status_code + 200 + >>> r.headers['content-type'] + 'application/json; charset=utf8' + >>> r.encoding + 'utf-8' + >>> r.text + '{"type":"User"...' + >>> r.json() + {'private_gists': 419, 'total_private_repos': 77, ...} + +See `similar code, sans Requests `_. + + +**Requests** allows you to send HTTP/1.1 requests extremely easily. +There's no need to manually add query strings to your +URLs, or to form-encode your POST data. Keep-alive and HTTP connection pooling +are 100% automatic, thanks to `urllib3 `_. + +Beloved Features +---------------- + +Requests is ready for today's web. + +- Keep-Alive & Connection Pooling +- International Domains and URLs +- Sessions with Cookie Persistence +- Browser-style SSL Verification +- Automatic Content Decoding +- Basic/Digest Authentication +- Elegant Key/Value Cookies +- Automatic Decompression +- Unicode Response Bodies +- HTTP(S) Proxy Support +- Multipart File Uploads +- Streaming Downloads +- Connection Timeouts +- Chunked Requests +- ``.netrc`` Support + +Requests officially supports Python 3.7+, and runs great on PyPy. + + +The User Guide +-------------- + +This part of the documentation, which is mostly prose, begins with some +background information about Requests, then focuses on step-by-step +instructions for getting the most out of Requests. + +.. toctree:: + :maxdepth: 2 + + user/install + user/quickstart + user/advanced + user/authentication + + +The Community Guide +------------------- + +This part of the documentation, which is mostly prose, details the +Requests ecosystem and community. + +.. toctree:: + :maxdepth: 2 + + community/recommended + community/faq + community/out-there + community/support + community/vulnerabilities + community/release-process + +.. toctree:: + :maxdepth: 1 + + community/updates + +The API Documentation / Guide +----------------------------- + +If you are looking for information on a specific function, class, or method, +this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + + +The Contributor Guide +--------------------- + +If you want to contribute to the project, this part of the documentation is for +you. + +.. toctree:: + :maxdepth: 3 + + dev/contributing + dev/authors + +There are no more guides. You are now guideless. +Good luck. diff --git a/test/fixtures/whole_applications/requests/docs/make.bat b/test/fixtures/whole_applications/requests/docs/make.bat new file mode 100644 index 0000000..090d760 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Requests.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Requests.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/test/fixtures/whole_applications/requests/docs/requirements.txt b/test/fixtures/whole_applications/requests/docs/requirements.txt new file mode 100644 index 0000000..8c24882 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/requirements.txt @@ -0,0 +1,3 @@ +# Pinning to avoid unexpected breakages. +# Used by RTD to generate docs. +Sphinx==4.2.0 diff --git a/test/fixtures/whole_applications/requests/docs/user/advanced.rst b/test/fixtures/whole_applications/requests/docs/user/advanced.rst new file mode 100644 index 0000000..c664a83 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/user/advanced.rst @@ -0,0 +1,1099 @@ +.. _advanced: + +Advanced Usage +============== + +This document covers some of Requests more advanced features. + +.. _session-objects: + +Session Objects +--------------- + +The Session object allows you to persist certain parameters across +requests. It also persists cookies across all requests made from the +Session instance, and will use ``urllib3``'s `connection pooling`_. So if +you're making several requests to the same host, the underlying TCP +connection will be reused, which can result in a significant performance +increase (see `HTTP persistent connection`_). + +A Session object has all the methods of the main Requests API. + +Let's persist some cookies across requests:: + + s = requests.Session() + + s.get('https://httpbin.org/cookies/set/sessioncookie/123456789') + r = s.get('https://httpbin.org/cookies') + + print(r.text) + # '{"cookies": {"sessioncookie": "123456789"}}' + + +Sessions can also be used to provide default data to the request methods. This +is done by providing data to the properties on a Session object:: + + s = requests.Session() + s.auth = ('user', 'pass') + s.headers.update({'x-test': 'true'}) + + # both 'x-test' and 'x-test2' are sent + s.get('https://httpbin.org/headers', headers={'x-test2': 'true'}) + + +Any dictionaries that you pass to a request method will be merged with the +session-level values that are set. The method-level parameters override session +parameters. + +Note, however, that method-level parameters will *not* be persisted across +requests, even if using a session. This example will only send the cookies +with the first request, but not the second:: + + s = requests.Session() + + r = s.get('https://httpbin.org/cookies', cookies={'from-my': 'browser'}) + print(r.text) + # '{"cookies": {"from-my": "browser"}}' + + r = s.get('https://httpbin.org/cookies') + print(r.text) + # '{"cookies": {}}' + + +If you want to manually add cookies to your session, use the +:ref:`Cookie utility functions ` to manipulate +:attr:`Session.cookies `. + +Sessions can also be used as context managers:: + + with requests.Session() as s: + s.get('https://httpbin.org/cookies/set/sessioncookie/123456789') + +This will make sure the session is closed as soon as the ``with`` block is +exited, even if unhandled exceptions occurred. + + +.. admonition:: Remove a Value From a Dict Parameter + + Sometimes you'll want to omit session-level keys from a dict parameter. To + do this, you simply set that key's value to ``None`` in the method-level + parameter. It will automatically be omitted. + +All values that are contained within a session are directly available to you. +See the :ref:`Session API Docs ` to learn more. + +.. _request-and-response-objects: + +Request and Response Objects +---------------------------- + +Whenever a call is made to ``requests.get()`` and friends, you are doing two +major things. First, you are constructing a ``Request`` object which will be +sent off to a server to request or query some resource. Second, a ``Response`` +object is generated once Requests gets a response back from the server. +The ``Response`` object contains all of the information returned by the server and +also contains the ``Request`` object you created originally. Here is a simple +request to get some very important information from Wikipedia's servers:: + + >>> r = requests.get('https://en.wikipedia.org/wiki/Monty_Python') + +If we want to access the headers the server sent back to us, we do this:: + + >>> r.headers + {'content-length': '56170', 'x-content-type-options': 'nosniff', 'x-cache': + 'HIT from cp1006.eqiad.wmnet, MISS from cp1010.eqiad.wmnet', 'content-encoding': + 'gzip', 'age': '3080', 'content-language': 'en', 'vary': 'Accept-Encoding,Cookie', + 'server': 'Apache', 'last-modified': 'Wed, 13 Jun 2012 01:33:50 GMT', + 'connection': 'close', 'cache-control': 'private, s-maxage=0, max-age=0, + must-revalidate', 'date': 'Thu, 14 Jun 2012 12:59:39 GMT', 'content-type': + 'text/html; charset=UTF-8', 'x-cache-lookup': 'HIT from cp1006.eqiad.wmnet:3128, + MISS from cp1010.eqiad.wmnet:80'} + +However, if we want to get the headers we sent the server, we simply access the +request, and then the request's headers:: + + >>> r.request.headers + {'Accept-Encoding': 'identity, deflate, compress, gzip', + 'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'} + +.. _prepared-requests: + +Prepared Requests +----------------- + +Whenever you receive a :class:`Response ` object +from an API call or a Session call, the ``request`` attribute is actually the +``PreparedRequest`` that was used. In some cases you may wish to do some extra +work to the body or headers (or anything else really) before sending a +request. The simple recipe for this is the following:: + + from requests import Request, Session + + s = Session() + + req = Request('POST', url, data=data, headers=headers) + prepped = req.prepare() + + # do something with prepped.body + prepped.body = 'No, I want exactly this as the body.' + + # do something with prepped.headers + del prepped.headers['Content-Type'] + + resp = s.send(prepped, + stream=stream, + verify=verify, + proxies=proxies, + cert=cert, + timeout=timeout + ) + + print(resp.status_code) + +Since you are not doing anything special with the ``Request`` object, you +prepare it immediately and modify the ``PreparedRequest`` object. You then +send that with the other parameters you would have sent to ``requests.*`` or +``Session.*``. + +However, the above code will lose some of the advantages of having a Requests +:class:`Session ` object. In particular, +:class:`Session `-level state such as cookies will +not get applied to your request. To get a +:class:`PreparedRequest ` with that state +applied, replace the call to :meth:`Request.prepare() +` with a call to +:meth:`Session.prepare_request() `, like this:: + + from requests import Request, Session + + s = Session() + req = Request('GET', url, data=data, headers=headers) + + prepped = s.prepare_request(req) + + # do something with prepped.body + prepped.body = 'Seriously, send exactly these bytes.' + + # do something with prepped.headers + prepped.headers['Keep-Dead'] = 'parrot' + + resp = s.send(prepped, + stream=stream, + verify=verify, + proxies=proxies, + cert=cert, + timeout=timeout + ) + + print(resp.status_code) + +When you are using the prepared request flow, keep in mind that it does not take into account the environment. +This can cause problems if you are using environment variables to change the behaviour of requests. +For example: Self-signed SSL certificates specified in ``REQUESTS_CA_BUNDLE`` will not be taken into account. +As a result an ``SSL: CERTIFICATE_VERIFY_FAILED`` is thrown. +You can get around this behaviour by explicitly merging the environment settings into your session:: + + from requests import Request, Session + + s = Session() + req = Request('GET', url) + + prepped = s.prepare_request(req) + + # Merge environment settings into session + settings = s.merge_environment_settings(prepped.url, {}, None, None, None) + resp = s.send(prepped, **settings) + + print(resp.status_code) + +.. _verification: + +SSL Cert Verification +--------------------- + +Requests verifies SSL certificates for HTTPS requests, just like a web browser. +By default, SSL verification is enabled, and Requests will throw a SSLError if +it's unable to verify the certificate:: + + >>> requests.get('https://requestb.in') + requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com' + +I don't have SSL setup on this domain, so it throws an exception. Excellent. GitHub does though:: + + >>> requests.get('https://github.com') + + +You can pass ``verify`` the path to a CA_BUNDLE file or directory with certificates of trusted CAs:: + + >>> requests.get('https://github.com', verify='/path/to/certfile') + +or persistent:: + + s = requests.Session() + s.verify = '/path/to/certfile' + +.. note:: If ``verify`` is set to a path to a directory, the directory must have been processed using + the ``c_rehash`` utility supplied with OpenSSL. + +This list of trusted CAs can also be specified through the ``REQUESTS_CA_BUNDLE`` environment variable. +If ``REQUESTS_CA_BUNDLE`` is not set, ``CURL_CA_BUNDLE`` will be used as fallback. + +Requests can also ignore verifying the SSL certificate if you set ``verify`` to False:: + + >>> requests.get('https://kennethreitz.org', verify=False) + + +Note that when ``verify`` is set to ``False``, requests will accept any TLS +certificate presented by the server, and will ignore hostname mismatches +and/or expired certificates, which will make your application vulnerable to +man-in-the-middle (MitM) attacks. Setting verify to ``False`` may be useful +during local development or testing. + +By default, ``verify`` is set to True. Option ``verify`` only applies to host certs. + +Client Side Certificates +------------------------ + +You can also specify a local cert to use as client side certificate, as a single +file (containing the private key and the certificate) or as a tuple of both +files' paths:: + + >>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key')) + + +or persistent:: + + s = requests.Session() + s.cert = '/path/client.cert' + +If you specify a wrong path or an invalid cert, you'll get a SSLError:: + + >>> requests.get('https://kennethreitz.org', cert='/wrong_path/client.pem') + SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib + +.. warning:: The private key to your local certificate *must* be unencrypted. + Currently, Requests does not support using encrypted keys. + +.. _ca-certificates: + +CA Certificates +--------------- + +Requests uses certificates from the package `certifi`_. This allows for users +to update their trusted certificates without changing the version of Requests. + +Before version 2.16, Requests bundled a set of root CAs that it trusted, +sourced from the `Mozilla trust store`_. The certificates were only updated +once for each Requests version. When ``certifi`` was not installed, this led to +extremely out-of-date certificate bundles when using significantly older +versions of Requests. + +For the sake of security we recommend upgrading certifi frequently! + +.. _HTTP persistent connection: https://en.wikipedia.org/wiki/HTTP_persistent_connection +.. _connection pooling: https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.connectionpool +.. _certifi: https://certifiio.readthedocs.io/ +.. _Mozilla trust store: https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt + +.. _body-content-workflow: + +Body Content Workflow +--------------------- + +By default, when you make a request, the body of the response is downloaded +immediately. You can override this behaviour and defer downloading the response +body until you access the :attr:`Response.content ` +attribute with the ``stream`` parameter:: + + tarball_url = 'https://github.com/psf/requests/tarball/main' + r = requests.get(tarball_url, stream=True) + +At this point only the response headers have been downloaded and the connection +remains open, hence allowing us to make content retrieval conditional:: + + if int(r.headers['content-length']) < TOO_LONG: + content = r.content + ... + +You can further control the workflow by use of the :meth:`Response.iter_content() ` +and :meth:`Response.iter_lines() ` methods. +Alternatively, you can read the undecoded body from the underlying +urllib3 :class:`urllib3.HTTPResponse ` at +:attr:`Response.raw `. + +If you set ``stream`` to ``True`` when making a request, Requests cannot +release the connection back to the pool unless you consume all the data or call +:meth:`Response.close `. This can lead to +inefficiency with connections. If you find yourself partially reading request +bodies (or not reading them at all) while using ``stream=True``, you should +make the request within a ``with`` statement to ensure it's always closed:: + + with requests.get('https://httpbin.org/get', stream=True) as r: + # Do things with the response here. + +.. _keep-alive: + +Keep-Alive +---------- + +Excellent news — thanks to urllib3, keep-alive is 100% automatic within a session! +Any requests that you make within a session will automatically reuse the appropriate +connection! + +Note that connections are only released back to the pool for reuse once all body +data has been read; be sure to either set ``stream`` to ``False`` or read the +``content`` property of the ``Response`` object. + +.. _streaming-uploads: + +Streaming Uploads +----------------- + +Requests supports streaming uploads, which allow you to send large streams or +files without reading them into memory. To stream and upload, simply provide a +file-like object for your body:: + + with open('massive-body', 'rb') as f: + requests.post('http://some.url/streamed', data=f) + +.. warning:: It is strongly recommended that you open files in :ref:`binary + mode `. This is because Requests may attempt to provide + the ``Content-Length`` header for you, and if it does this value + will be set to the number of *bytes* in the file. Errors may occur + if you open the file in *text mode*. + + +.. _chunk-encoding: + +Chunk-Encoded Requests +---------------------- + +Requests also supports Chunked transfer encoding for outgoing and incoming requests. +To send a chunk-encoded request, simply provide a generator (or any iterator without +a length) for your body:: + + def gen(): + yield 'hi' + yield 'there' + + requests.post('http://some.url/chunked', data=gen()) + +For chunked encoded responses, it's best to iterate over the data using +:meth:`Response.iter_content() `. In +an ideal situation you'll have set ``stream=True`` on the request, in which +case you can iterate chunk-by-chunk by calling ``iter_content`` with a ``chunk_size`` +parameter of ``None``. If you want to set a maximum size of the chunk, +you can set a ``chunk_size`` parameter to any integer. + + +.. _multipart: + +POST Multiple Multipart-Encoded Files +------------------------------------- + +You can send multiple files in one request. For example, suppose you want to +upload image files to an HTML form with a multiple file field 'images':: + + + +To do that, just set files to a list of tuples of ``(form_field_name, file_info)``:: + + >>> url = 'https://httpbin.org/post' + >>> multiple_files = [ + ... ('images', ('foo.png', open('foo.png', 'rb'), 'image/png')), + ... ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))] + >>> r = requests.post(url, files=multiple_files) + >>> r.text + { + ... + 'files': {'images': 'data:image/png;base64,iVBORw ....'} + 'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a', + ... + } + +.. warning:: It is strongly recommended that you open files in :ref:`binary + mode `. This is because Requests may attempt to provide + the ``Content-Length`` header for you, and if it does this value + will be set to the number of *bytes* in the file. Errors may occur + if you open the file in *text mode*. + + +.. _event-hooks: + +Event Hooks +----------- + +Requests has a hook system that you can use to manipulate portions of +the request process, or signal event handling. + +Available hooks: + +``response``: + The response generated from a Request. + + +You can assign a hook function on a per-request basis by passing a +``{hook_name: callback_function}`` dictionary to the ``hooks`` request +parameter:: + + hooks={'response': print_url} + +That ``callback_function`` will receive a chunk of data as its first +argument. + +:: + + def print_url(r, *args, **kwargs): + print(r.url) + +Your callback function must handle its own exceptions. Any unhandled exception won't be passed silently and thus should be handled by the code calling Requests. + +If the callback function returns a value, it is assumed that it is to +replace the data that was passed in. If the function doesn't return +anything, nothing else is affected. + +:: + + def record_hook(r, *args, **kwargs): + r.hook_called = True + return r + +Let's print some request method arguments at runtime:: + + >>> requests.get('https://httpbin.org/', hooks={'response': print_url}) + https://httpbin.org/ + + +You can add multiple hooks to a single request. Let's call two hooks at once:: + + >>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]}) + >>> r.hook_called + True + +You can also add hooks to a ``Session`` instance. Any hooks you add will then +be called on every request made to the session. For example:: + + >>> s = requests.Session() + >>> s.hooks['response'].append(print_url) + >>> s.get('https://httpbin.org/') + https://httpbin.org/ + + +A ``Session`` can have multiple hooks, which will be called in the order +they are added. + +.. _custom-auth: + +Custom Authentication +--------------------- + +Requests allows you to specify your own authentication mechanism. + +Any callable which is passed as the ``auth`` argument to a request method will +have the opportunity to modify the request before it is dispatched. + +Authentication implementations are subclasses of :class:`AuthBase `, +and are easy to define. Requests provides two common authentication scheme +implementations in ``requests.auth``: :class:`HTTPBasicAuth ` and +:class:`HTTPDigestAuth `. + +Let's pretend that we have a web service that will only respond if the +``X-Pizza`` header is set to a password value. Unlikely, but just go with it. + +:: + + from requests.auth import AuthBase + + class PizzaAuth(AuthBase): + """Attaches HTTP Pizza Authentication to the given Request object.""" + def __init__(self, username): + # setup any auth-related data here + self.username = username + + def __call__(self, r): + # modify and return the request + r.headers['X-Pizza'] = self.username + return r + +Then, we can make a request using our Pizza Auth:: + + >>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth')) + + +.. _streaming-requests: + +Streaming Requests +------------------ + +With :meth:`Response.iter_lines() ` you can easily +iterate over streaming APIs such as the `Twitter Streaming +API `_. Simply +set ``stream`` to ``True`` and iterate over the response with +:meth:`~requests.Response.iter_lines()`:: + + import json + import requests + + r = requests.get('https://httpbin.org/stream/20', stream=True) + + for line in r.iter_lines(): + + # filter out keep-alive new lines + if line: + decoded_line = line.decode('utf-8') + print(json.loads(decoded_line)) + +When using `decode_unicode=True` with +:meth:`Response.iter_lines() ` or +:meth:`Response.iter_content() `, you'll want +to provide a fallback encoding in the event the server doesn't provide one:: + + r = requests.get('https://httpbin.org/stream/20', stream=True) + + if r.encoding is None: + r.encoding = 'utf-8' + + for line in r.iter_lines(decode_unicode=True): + if line: + print(json.loads(line)) + +.. warning:: + + :meth:`~requests.Response.iter_lines()` is not reentrant safe. + Calling this method multiple times causes some of the received data + being lost. In case you need to call it from multiple places, use + the resulting iterator object instead:: + + lines = r.iter_lines() + # Save the first line for later or just skip it + + first_line = next(lines) + + for line in lines: + print(line) + +.. _proxies: + +Proxies +------- + +If you need to use a proxy, you can configure individual requests with the +``proxies`` argument to any request method:: + + import requests + + proxies = { + 'http': 'http://10.10.1.10:3128', + 'https': 'http://10.10.1.10:1080', + } + + requests.get('http://example.org', proxies=proxies) + +Alternatively you can configure it once for an entire +:class:`Session `:: + + import requests + + proxies = { + 'http': 'http://10.10.1.10:3128', + 'https': 'http://10.10.1.10:1080', + } + session = requests.Session() + session.proxies.update(proxies) + + session.get('http://example.org') + +.. warning:: Setting ``session.proxies`` may behave differently than expected. + Values provided will be overwritten by environmental proxies + (those returned by `urllib.request.getproxies `_). + To ensure the use of proxies in the presence of environmental proxies, + explicitly specify the ``proxies`` argument on all individual requests as + initially explained above. + + See `#2018 `_ for details. + +When the proxies configuration is not overridden per request as shown above, +Requests relies on the proxy configuration defined by standard +environment variables ``http_proxy``, ``https_proxy``, ``no_proxy``, +and ``all_proxy``. Uppercase variants of these variables are also supported. +You can therefore set them to configure Requests (only set the ones relevant +to your needs):: + + $ export HTTP_PROXY="http://10.10.1.10:3128" + $ export HTTPS_PROXY="http://10.10.1.10:1080" + $ export ALL_PROXY="socks5://10.10.1.10:3434" + + $ python + >>> import requests + >>> requests.get('http://example.org') + +To use HTTP Basic Auth with your proxy, use the `http://user:password@host/` +syntax in any of the above configuration entries:: + + $ export HTTPS_PROXY="http://user:pass@10.10.1.10:1080" + + $ python + >>> proxies = {'http': 'http://user:pass@10.10.1.10:3128/'} + +.. warning:: Storing sensitive username and password information in an + environment variable or a version-controlled file is a security risk and is + highly discouraged. + +To give a proxy for a specific scheme and host, use the +`scheme://hostname` form for the key. This will match for +any request to the given scheme and exact hostname. + +:: + + proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'} + +Note that proxy URLs must include the scheme. + +Finally, note that using a proxy for https connections typically requires your +local machine to trust the proxy's root certificate. By default the list of +certificates trusted by Requests can be found with:: + + from requests.utils import DEFAULT_CA_BUNDLE_PATH + print(DEFAULT_CA_BUNDLE_PATH) + +You override this default certificate bundle by setting the ``REQUESTS_CA_BUNDLE`` +(or ``CURL_CA_BUNDLE``) environment variable to another file path:: + + $ export REQUESTS_CA_BUNDLE="/usr/local/myproxy_info/cacert.pem" + $ export https_proxy="http://10.10.1.10:1080" + + $ python + >>> import requests + >>> requests.get('https://example.org') + +SOCKS +^^^^^ + +.. versionadded:: 2.10.0 + +In addition to basic HTTP proxies, Requests also supports proxies using the +SOCKS protocol. This is an optional feature that requires that additional +third-party libraries be installed before use. + +You can get the dependencies for this feature from ``pip``: + +.. code-block:: bash + + $ python -m pip install requests[socks] + +Once you've installed those dependencies, using a SOCKS proxy is just as easy +as using a HTTP one:: + + proxies = { + 'http': 'socks5://user:pass@host:port', + 'https': 'socks5://user:pass@host:port' + } + +Using the scheme ``socks5`` causes the DNS resolution to happen on the client, rather than on the proxy server. This is in line with curl, which uses the scheme to decide whether to do the DNS resolution on the client or proxy. If you want to resolve the domains on the proxy server, use ``socks5h`` as the scheme. + +.. _compliance: + +Compliance +---------- + +Requests is intended to be compliant with all relevant specifications and +RFCs where that compliance will not cause difficulties for users. This +attention to the specification can lead to some behaviour that may seem +unusual to those not familiar with the relevant specification. + +Encodings +^^^^^^^^^ + +When you receive a response, Requests makes a guess at the encoding to +use for decoding the response when you access the :attr:`Response.text +` attribute. Requests will first check for an +encoding in the HTTP header, and if none is present, will use +`charset_normalizer `_ +or `chardet `_ to attempt to +guess the encoding. + +If ``chardet`` is installed, ``requests`` uses it, however for python3 +``chardet`` is no longer a mandatory dependency. The ``chardet`` +library is an LGPL-licenced dependency and some users of requests +cannot depend on mandatory LGPL-licensed dependencies. + +When you install ``requests`` without specifying ``[use_chardet_on_py3]`` extra, +and ``chardet`` is not already installed, ``requests`` uses ``charset-normalizer`` +(MIT-licensed) to guess the encoding. + +The only time Requests will not guess the encoding is if no explicit charset +is present in the HTTP headers **and** the ``Content-Type`` +header contains ``text``. In this situation, `RFC 2616 +`_ specifies +that the default charset must be ``ISO-8859-1``. Requests follows the +specification in this case. If you require a different encoding, you can +manually set the :attr:`Response.encoding ` +property, or use the raw :attr:`Response.content `. + +.. _http-verbs: + +HTTP Verbs +---------- + +Requests provides access to almost the full range of HTTP verbs: GET, OPTIONS, +HEAD, POST, PUT, PATCH and DELETE. The following provides detailed examples of +using these various verbs in Requests, using the GitHub API. + +We will begin with the verb most commonly used: GET. HTTP GET is an idempotent +method that returns a resource from a given URL. As a result, it is the verb +you ought to use when attempting to retrieve data from a web location. An +example usage would be attempting to get information about a specific commit +from GitHub. Suppose we wanted commit ``a050faf`` on Requests. We would get it +like so:: + + >>> import requests + >>> r = requests.get('https://api.github.com/repos/psf/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad') + +We should confirm that GitHub responded correctly. If it has, we want to work +out what type of content it is. Do this like so:: + + >>> if r.status_code == requests.codes.ok: + ... print(r.headers['content-type']) + ... + application/json; charset=utf-8 + +So, GitHub returns JSON. That's great, we can use the :meth:`r.json +` method to parse it into Python objects. + +:: + + >>> commit_data = r.json() + + >>> print(commit_data.keys()) + ['committer', 'author', 'url', 'tree', 'sha', 'parents', 'message'] + + >>> print(commit_data['committer']) + {'date': '2012-05-10T11:10:50-07:00', 'email': 'me@kennethreitz.com', 'name': 'Kenneth Reitz'} + + >>> print(commit_data['message']) + makin' history + +So far, so simple. Well, let's investigate the GitHub API a little bit. Now, +we could look at the documentation, but we might have a little more fun if we +use Requests instead. We can take advantage of the Requests OPTIONS verb to +see what kinds of HTTP methods are supported on the url we just used. + +:: + + >>> verbs = requests.options(r.url) + >>> verbs.status_code + 500 + +Uh, what? That's unhelpful! Turns out GitHub, like many API providers, don't +actually implement the OPTIONS method. This is an annoying oversight, but it's +OK, we can just use the boring documentation. If GitHub had correctly +implemented OPTIONS, however, they should return the allowed methods in the +headers, e.g. + +:: + + >>> verbs = requests.options('http://a-good-website.com/api/cats') + >>> print(verbs.headers['allow']) + GET,HEAD,POST,OPTIONS + +Turning to the documentation, we see that the only other method allowed for +commits is POST, which creates a new commit. As we're using the Requests repo, +we should probably avoid making ham-handed POSTS to it. Instead, let's play +with the Issues feature of GitHub. + +This documentation was added in response to +`Issue #482 `_. Given that +this issue already exists, we will use it as an example. Let's start by getting it. + +:: + + >>> r = requests.get('https://api.github.com/repos/psf/requests/issues/482') + >>> r.status_code + 200 + + >>> issue = json.loads(r.text) + + >>> print(issue['title']) + Feature any http verb in docs + + >>> print(issue['comments']) + 3 + +Cool, we have three comments. Let's take a look at the last of them. + +:: + + >>> r = requests.get(r.url + '/comments') + >>> r.status_code + 200 + + >>> comments = r.json() + + >>> print(comments[0].keys()) + ['body', 'url', 'created_at', 'updated_at', 'user', 'id'] + + >>> print(comments[2]['body']) + Probably in the "advanced" section + +Well, that seems like a silly place. Let's post a comment telling the poster +that he's silly. Who is the poster, anyway? + +:: + + >>> print(comments[2]['user']['login']) + kennethreitz + +OK, so let's tell this Kenneth guy that we think this example should go in the +quickstart guide instead. According to the GitHub API doc, the way to do this +is to POST to the thread. Let's do it. + +:: + + >>> body = json.dumps({u"body": u"Sounds great! I'll get right on it!"}) + >>> url = u"https://api.github.com/repos/psf/requests/issues/482/comments" + + >>> r = requests.post(url=url, data=body) + >>> r.status_code + 404 + +Huh, that's weird. We probably need to authenticate. That'll be a pain, right? +Wrong. Requests makes it easy to use many forms of authentication, including +the very common Basic Auth. + +:: + + >>> from requests.auth import HTTPBasicAuth + >>> auth = HTTPBasicAuth('fake@example.com', 'not_a_real_password') + + >>> r = requests.post(url=url, data=body, auth=auth) + >>> r.status_code + 201 + + >>> content = r.json() + >>> print(content['body']) + Sounds great! I'll get right on it. + +Brilliant. Oh, wait, no! I meant to add that it would take me a while, because +I had to go feed my cat. If only I could edit this comment! Happily, GitHub +allows us to use another HTTP verb, PATCH, to edit this comment. Let's do +that. + +:: + + >>> print(content[u"id"]) + 5804413 + + >>> body = json.dumps({u"body": u"Sounds great! I'll get right on it once I feed my cat."}) + >>> url = u"https://api.github.com/repos/psf/requests/issues/comments/5804413" + + >>> r = requests.patch(url=url, data=body, auth=auth) + >>> r.status_code + 200 + +Excellent. Now, just to torture this Kenneth guy, I've decided to let him +sweat and not tell him that I'm working on this. That means I want to delete +this comment. GitHub lets us delete comments using the incredibly aptly named +DELETE method. Let's get rid of it. + +:: + + >>> r = requests.delete(url=url, auth=auth) + >>> r.status_code + 204 + >>> r.headers['status'] + '204 No Content' + +Excellent. All gone. The last thing I want to know is how much of my ratelimit +I've used. Let's find out. GitHub sends that information in the headers, so +rather than download the whole page I'll send a HEAD request to get the +headers. + +:: + + >>> r = requests.head(url=url, auth=auth) + >>> print(r.headers) + ... + 'x-ratelimit-remaining': '4995' + 'x-ratelimit-limit': '5000' + ... + +Excellent. Time to write a Python program that abuses the GitHub API in all +kinds of exciting ways, 4995 more times. + +.. _custom-verbs: + +Custom Verbs +------------ + +From time to time you may be working with a server that, for whatever reason, +allows use or even requires use of HTTP verbs not covered above. One example of +this would be the MKCOL method some WEBDAV servers use. Do not fret, these can +still be used with Requests. These make use of the built-in ``.request`` +method. For example:: + + >>> r = requests.request('MKCOL', url, data=data) + >>> r.status_code + 200 # Assuming your call was correct + +Utilising this, you can make use of any method verb that your server allows. + + +.. _link-headers: + +Link Headers +------------ + +Many HTTP APIs feature Link headers. They make APIs more self describing and +discoverable. + +GitHub uses these for `pagination `_ +in their API, for example:: + + >>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10' + >>> r = requests.head(url=url) + >>> r.headers['link'] + '; rel="next", ; rel="last"' + +Requests will automatically parse these link headers and make them easily consumable:: + + >>> r.links["next"] + {'url': 'https://api.github.com/users/kennethreitz/repos?page=2&per_page=10', 'rel': 'next'} + + >>> r.links["last"] + {'url': 'https://api.github.com/users/kennethreitz/repos?page=7&per_page=10', 'rel': 'last'} + +.. _transport-adapters: + +Transport Adapters +------------------ + +As of v1.0.0, Requests has moved to a modular internal design. Part of the +reason this was done was to implement Transport Adapters, originally +`described here`_. Transport Adapters provide a mechanism to define interaction +methods for an HTTP service. In particular, they allow you to apply per-service +configuration. + +Requests ships with a single Transport Adapter, the :class:`HTTPAdapter +`. This adapter provides the default Requests +interaction with HTTP and HTTPS using the powerful `urllib3`_ library. Whenever +a Requests :class:`Session ` is initialized, one of these is +attached to the :class:`Session ` object for HTTP, and one +for HTTPS. + +Requests enables users to create and use their own Transport Adapters that +provide specific functionality. Once created, a Transport Adapter can be +mounted to a Session object, along with an indication of which web services +it should apply to. + +:: + + >>> s = requests.Session() + >>> s.mount('https://github.com/', MyAdapter()) + +The mount call registers a specific instance of a Transport Adapter to a +prefix. Once mounted, any HTTP request made using that session whose URL starts +with the given prefix will use the given Transport Adapter. + +Many of the details of implementing a Transport Adapter are beyond the scope of +this documentation, but take a look at the next example for a simple SSL use- +case. For more than that, you might look at subclassing the +:class:`BaseAdapter `. + +Example: Specific SSL Version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Requests team has made a specific choice to use whatever SSL version is +default in the underlying library (`urllib3`_). Normally this is fine, but from +time to time, you might find yourself needing to connect to a service-endpoint +that uses a version that isn't compatible with the default. + +You can use Transport Adapters for this by taking most of the existing +implementation of HTTPAdapter, and adding a parameter *ssl_version* that gets +passed-through to `urllib3`. We'll make a Transport Adapter that instructs the +library to use SSLv3:: + + import ssl + from urllib3.poolmanager import PoolManager + + from requests.adapters import HTTPAdapter + + + class Ssl3HttpAdapter(HTTPAdapter): + """"Transport adapter" that allows us to use SSLv3.""" + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = PoolManager( + num_pools=connections, maxsize=maxsize, + block=block, ssl_version=ssl.PROTOCOL_SSLv3) + +.. _`described here`: https://kenreitz.org/essays/2012/06/14/the-future-of-python-http +.. _`urllib3`: https://github.com/urllib3/urllib3 + +.. _blocking-or-nonblocking: + +Blocking Or Non-Blocking? +------------------------- + +With the default Transport Adapter in place, Requests does not provide any kind +of non-blocking IO. The :attr:`Response.content ` +property will block until the entire response has been downloaded. If +you require more granularity, the streaming features of the library (see +:ref:`streaming-requests`) allow you to retrieve smaller quantities of the +response at a time. However, these calls will still block. + +If you are concerned about the use of blocking IO, there are lots of projects +out there that combine Requests with one of Python's asynchronicity frameworks. +Some excellent examples are `requests-threads`_, `grequests`_, `requests-futures`_, and `httpx`_. + +.. _`requests-threads`: https://github.com/requests/requests-threads +.. _`grequests`: https://github.com/spyoungtech/grequests +.. _`requests-futures`: https://github.com/ross/requests-futures +.. _`httpx`: https://github.com/encode/httpx + +Header Ordering +--------------- + +In unusual circumstances you may want to provide headers in an ordered manner. If you pass an ``OrderedDict`` to the ``headers`` keyword argument, that will provide the headers with an ordering. *However*, the ordering of the default headers used by Requests will be preferred, which means that if you override default headers in the ``headers`` keyword argument, they may appear out of order compared to other headers in that keyword argument. + +If this is problematic, users should consider setting the default headers on a :class:`Session ` object, by setting :attr:`Session ` to a custom ``OrderedDict``. That ordering will always be preferred. + +.. _timeouts: + +Timeouts +-------- + +Most requests to external servers should have a timeout attached, in case the +server is not responding in a timely manner. By default, requests do not time +out unless a timeout value is set explicitly. Without a timeout, your code may +hang for minutes or more. + +The **connect** timeout is the number of seconds Requests will wait for your +client to establish a connection to a remote machine (corresponding to the +`connect()`_) call on the socket. It's a good practice to set connect timeouts +to slightly larger than a multiple of 3, which is the default `TCP packet +retransmission window `_. + +Once your client has connected to the server and sent the HTTP request, the +**read** timeout is the number of seconds the client will wait for the server +to send a response. (Specifically, it's the number of seconds that the client +will wait *between* bytes sent from the server. In 99.9% of cases, this is the +time before the server sends the first byte). + +If you specify a single value for the timeout, like this:: + + r = requests.get('https://github.com', timeout=5) + +The timeout value will be applied to both the ``connect`` and the ``read`` +timeouts. Specify a tuple if you would like to set the values separately:: + + r = requests.get('https://github.com', timeout=(3.05, 27)) + +If the remote server is very slow, you can tell Requests to wait forever for +a response, by passing None as a timeout value and then retrieving a cup of +coffee. + +:: + + r = requests.get('https://github.com', timeout=None) + +.. _`connect()`: https://linux.die.net/man/2/connect diff --git a/test/fixtures/whole_applications/requests/docs/user/authentication.rst b/test/fixtures/whole_applications/requests/docs/user/authentication.rst new file mode 100644 index 0000000..0737bd3 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/user/authentication.rst @@ -0,0 +1,145 @@ +.. _authentication: + +Authentication +============== + +This document discusses using various kinds of authentication with Requests. + +Many web services require authentication, and there are many different types. +Below, we outline various forms of authentication available in Requests, from +the simple to the complex. + + +Basic Authentication +-------------------- + +Many web services that require authentication accept HTTP Basic Auth. This is +the simplest kind, and Requests supports it straight out of the box. + +Making requests with HTTP Basic Auth is very simple:: + + >>> from requests.auth import HTTPBasicAuth + >>> basic = HTTPBasicAuth('user', 'pass') + >>> requests.get('https://httpbin.org/basic-auth/user/pass', auth=basic) + + +In fact, HTTP Basic Auth is so common that Requests provides a handy shorthand +for using it:: + + >>> requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass')) + + +Providing the credentials in a tuple like this is exactly the same as the +``HTTPBasicAuth`` example above. + + +netrc Authentication +~~~~~~~~~~~~~~~~~~~~ + +If no authentication method is given with the ``auth`` argument, Requests will +attempt to get the authentication credentials for the URL's hostname from the +user's netrc file. The netrc file overrides raw HTTP authentication headers +set with `headers=`. + +If credentials for the hostname are found, the request is sent with HTTP Basic +Auth. + + +Digest Authentication +--------------------- + +Another very popular form of HTTP Authentication is Digest Authentication, +and Requests supports this out of the box as well:: + + >>> from requests.auth import HTTPDigestAuth + >>> url = 'https://httpbin.org/digest-auth/auth/user/pass' + >>> requests.get(url, auth=HTTPDigestAuth('user', 'pass')) + + + +OAuth 1 Authentication +---------------------- + +A common form of authentication for several web APIs is OAuth. The ``requests-oauthlib`` +library allows Requests users to easily make OAuth 1 authenticated requests:: + + >>> import requests + >>> from requests_oauthlib import OAuth1 + + >>> url = 'https://api.twitter.com/1.1/account/verify_credentials.json' + >>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', + ... 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET') + + >>> requests.get(url, auth=auth) + + +For more information on how to OAuth flow works, please see the official `OAuth`_ website. +For examples and documentation on requests-oauthlib, please see the `requests_oauthlib`_ +repository on GitHub + +OAuth 2 and OpenID Connect Authentication +----------------------------------------- + +The ``requests-oauthlib`` library also handles OAuth 2, the authentication mechanism +underpinning OpenID Connect. See the `requests-oauthlib OAuth2 documentation`_ for +details of the various OAuth 2 credential management flows: + +* `Web Application Flow`_ +* `Mobile Application Flow`_ +* `Legacy Application Flow`_ +* `Backend Application Flow`_ + +Other Authentication +-------------------- + +Requests is designed to allow other forms of authentication to be easily and +quickly plugged in. Members of the open-source community frequently write +authentication handlers for more complicated or less commonly-used forms of +authentication. Some of the best have been brought together under the +`Requests organization`_, including: + +- Kerberos_ +- NTLM_ + +If you want to use any of these forms of authentication, go straight to their +GitHub page and follow the instructions. + + +New Forms of Authentication +--------------------------- + +If you can't find a good implementation of the form of authentication you +want, you can implement it yourself. Requests makes it easy to add your own +forms of authentication. + +To do so, subclass :class:`AuthBase ` and implement the +``__call__()`` method:: + + >>> import requests + >>> class MyAuth(requests.auth.AuthBase): + ... def __call__(self, r): + ... # Implement my authentication + ... return r + ... + >>> url = 'https://httpbin.org/get' + >>> requests.get(url, auth=MyAuth()) + + +When an authentication handler is attached to a request, +it is called during request setup. The ``__call__`` method must therefore do +whatever is required to make the authentication work. Some forms of +authentication will additionally add hooks to provide further functionality. + +Further examples can be found under the `Requests organization`_ and in the +``auth.py`` file. + +.. _OAuth: https://oauth.net/ +.. _requests_oauthlib: https://github.com/requests/requests-oauthlib +.. _requests-oauthlib OAuth2 documentation: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html +.. _Web Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#web-application-flow +.. _Mobile Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#mobile-application-flow +.. _Legacy Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#legacy-application-flow +.. _Backend Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#backend-application-flow +.. _Kerberos: https://github.com/requests/requests-kerberos +.. _NTLM: https://github.com/requests/requests-ntlm +.. _Requests organization: https://github.com/requests diff --git a/test/fixtures/whole_applications/requests/docs/user/install.rst b/test/fixtures/whole_applications/requests/docs/user/install.rst new file mode 100644 index 0000000..7fa9a60 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/user/install.rst @@ -0,0 +1,36 @@ +.. _install: + +Installation of Requests +======================== + +This part of the documentation covers the installation of Requests. +The first step to using any software package is getting it properly installed. + + +$ python -m pip install requests +-------------------------------- + +To install Requests, simply run this simple command in your terminal of choice:: + + $ python -m pip install requests + +Get the Source Code +------------------- + +Requests is actively developed on GitHub, where the code is +`always available `_. + +You can either clone the public repository:: + + $ git clone https://github.com/psf/requests.git + +Or, download the `tarball `_:: + + $ curl -OL https://github.com/psf/requests/tarball/main + # optionally, zipball is also available (for Windows users). + +Once you have a copy of the source, you can embed it in your own Python +package, or install it into your site-packages easily:: + + $ cd requests + $ python -m pip install . diff --git a/test/fixtures/whole_applications/requests/docs/user/quickstart.rst b/test/fixtures/whole_applications/requests/docs/user/quickstart.rst new file mode 100644 index 0000000..464e4f5 --- /dev/null +++ b/test/fixtures/whole_applications/requests/docs/user/quickstart.rst @@ -0,0 +1,571 @@ +.. _quickstart: + +Quickstart +========== + +.. module:: requests.models + +Eager to get started? This page gives a good introduction in how to get started +with Requests. + +First, make sure that: + +* Requests is :ref:`installed ` +* Requests is :ref:`up-to-date ` + + +Let's get started with some simple examples. + + +Make a Request +-------------- + +Making a request with Requests is very simple. + +Begin by importing the Requests module:: + + >>> import requests + +Now, let's try to get a webpage. For this example, let's get GitHub's public +timeline:: + + >>> r = requests.get('https://api.github.com/events') + +Now, we have a :class:`Response ` object called ``r``. We can +get all the information we need from this object. + +Requests' simple API means that all forms of HTTP request are as obvious. For +example, this is how you make an HTTP POST request:: + + >>> r = requests.post('https://httpbin.org/post', data={'key': 'value'}) + +Nice, right? What about the other HTTP request types: PUT, DELETE, HEAD and +OPTIONS? These are all just as simple:: + + >>> r = requests.put('https://httpbin.org/put', data={'key': 'value'}) + >>> r = requests.delete('https://httpbin.org/delete') + >>> r = requests.head('https://httpbin.org/get') + >>> r = requests.options('https://httpbin.org/get') + +That's all well and good, but it's also only the start of what Requests can +do. + + +Passing Parameters In URLs +-------------------------- + +You often want to send some sort of data in the URL's query string. If +you were constructing the URL by hand, this data would be given as key/value +pairs in the URL after a question mark, e.g. ``httpbin.org/get?key=val``. +Requests allows you to provide these arguments as a dictionary of strings, +using the ``params`` keyword argument. As an example, if you wanted to pass +``key1=value1`` and ``key2=value2`` to ``httpbin.org/get``, you would use the +following code:: + + >>> payload = {'key1': 'value1', 'key2': 'value2'} + >>> r = requests.get('https://httpbin.org/get', params=payload) + +You can see that the URL has been correctly encoded by printing the URL:: + + >>> print(r.url) + https://httpbin.org/get?key2=value2&key1=value1 + +Note that any dictionary key whose value is ``None`` will not be added to the +URL's query string. + +You can also pass a list of items as a value:: + + >>> payload = {'key1': 'value1', 'key2': ['value2', 'value3']} + + >>> r = requests.get('https://httpbin.org/get', params=payload) + >>> print(r.url) + https://httpbin.org/get?key1=value1&key2=value2&key2=value3 + +Response Content +---------------- + +We can read the content of the server's response. Consider the GitHub timeline +again:: + + >>> import requests + + >>> r = requests.get('https://api.github.com/events') + >>> r.text + '[{"repository":{"open_issues":0,"url":"https://github.com/... + +Requests will automatically decode content from the server. Most unicode +charsets are seamlessly decoded. + +When you make a request, Requests makes educated guesses about the encoding of +the response based on the HTTP headers. The text encoding guessed by Requests +is used when you access ``r.text``. You can find out what encoding Requests is +using, and change it, using the ``r.encoding`` property:: + + >>> r.encoding + 'utf-8' + >>> r.encoding = 'ISO-8859-1' + +If you change the encoding, Requests will use the new value of ``r.encoding`` +whenever you call ``r.text``. You might want to do this in any situation where +you can apply special logic to work out what the encoding of the content will +be. For example, HTML and XML have the ability to specify their encoding in +their body. In situations like this, you should use ``r.content`` to find the +encoding, and then set ``r.encoding``. This will let you use ``r.text`` with +the correct encoding. + +Requests will also use custom encodings in the event that you need them. If +you have created your own encoding and registered it with the ``codecs`` +module, you can simply use the codec name as the value of ``r.encoding`` and +Requests will handle the decoding for you. + +Binary Response Content +----------------------- + +You can also access the response body as bytes, for non-text requests:: + + >>> r.content + b'[{"repository":{"open_issues":0,"url":"https://github.com/... + +The ``gzip`` and ``deflate`` transfer-encodings are automatically decoded for you. + +The ``br`` transfer-encoding is automatically decoded for you if a Brotli library +like `brotli `_ or `brotlicffi `_ is installed. + +For example, to create an image from binary data returned by a request, you can +use the following code:: + + >>> from PIL import Image + >>> from io import BytesIO + + >>> i = Image.open(BytesIO(r.content)) + + +JSON Response Content +--------------------- + +There's also a builtin JSON decoder, in case you're dealing with JSON data:: + + >>> import requests + + >>> r = requests.get('https://api.github.com/events') + >>> r.json() + [{'repository': {'open_issues': 0, 'url': 'https://github.com/... + +In case the JSON decoding fails, ``r.json()`` raises an exception. For example, if +the response gets a 204 (No Content), or if the response contains invalid JSON, +attempting ``r.json()`` raises ``requests.exceptions.JSONDecodeError``. This wrapper exception +provides interoperability for multiple exceptions that may be thrown by different +python versions and json serialization libraries. + +It should be noted that the success of the call to ``r.json()`` does **not** +indicate the success of the response. Some servers may return a JSON object in a +failed response (e.g. error details with HTTP 500). Such JSON will be decoded +and returned. To check that a request is successful, use +``r.raise_for_status()`` or check ``r.status_code`` is what you expect. + + +Raw Response Content +-------------------- + +In the rare case that you'd like to get the raw socket response from the +server, you can access ``r.raw``. If you want to do this, make sure you set +``stream=True`` in your initial request. Once you do, you can do this:: + + >>> r = requests.get('https://api.github.com/events', stream=True) + + >>> r.raw + + + >>> r.raw.read(10) + b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03' + +In general, however, you should use a pattern like this to save what is being +streamed to a file:: + + with open(filename, 'wb') as fd: + for chunk in r.iter_content(chunk_size=128): + fd.write(chunk) + +Using ``Response.iter_content`` will handle a lot of what you would otherwise +have to handle when using ``Response.raw`` directly. When streaming a +download, the above is the preferred and recommended way to retrieve the +content. Note that ``chunk_size`` can be freely adjusted to a number that +may better fit your use cases. + +.. note:: + + An important note about using ``Response.iter_content`` versus ``Response.raw``. + ``Response.iter_content`` will automatically decode the ``gzip`` and ``deflate`` + transfer-encodings. ``Response.raw`` is a raw stream of bytes -- it does not + transform the response content. If you really need access to the bytes as they + were returned, use ``Response.raw``. + + +Custom Headers +-------------- + +If you'd like to add HTTP headers to a request, simply pass in a ``dict`` to the +``headers`` parameter. + +For example, we didn't specify our user-agent in the previous example:: + + >>> url = 'https://api.github.com/some/endpoint' + >>> headers = {'user-agent': 'my-app/0.0.1'} + + >>> r = requests.get(url, headers=headers) + +Note: Custom headers are given less precedence than more specific sources of information. For instance: + +* Authorization headers set with `headers=` will be overridden if credentials + are specified in ``.netrc``, which in turn will be overridden by the ``auth=`` + parameter. Requests will search for the netrc file at `~/.netrc`, `~/_netrc`, + or at the path specified by the `NETRC` environment variable. +* Authorization headers will be removed if you get redirected off-host. +* Proxy-Authorization headers will be overridden by proxy credentials provided in the URL. +* Content-Length headers will be overridden when we can determine the length of the content. + +Furthermore, Requests does not change its behavior at all based on which custom headers are specified. The headers are simply passed on into the final request. + +Note: All header values must be a ``string``, bytestring, or unicode. While permitted, it's advised to avoid passing unicode header values. + +More complicated POST requests +------------------------------ + +Typically, you want to send some form-encoded data — much like an HTML form. +To do this, simply pass a dictionary to the ``data`` argument. Your +dictionary of data will automatically be form-encoded when the request is made:: + + >>> payload = {'key1': 'value1', 'key2': 'value2'} + + >>> r = requests.post('https://httpbin.org/post', data=payload) + >>> print(r.text) + { + ... + "form": { + "key2": "value2", + "key1": "value1" + }, + ... + } + +The ``data`` argument can also have multiple values for each key. This can be +done by making ``data`` either a list of tuples or a dictionary with lists +as values. This is particularly useful when the form has multiple elements that +use the same key:: + + >>> payload_tuples = [('key1', 'value1'), ('key1', 'value2')] + >>> r1 = requests.post('https://httpbin.org/post', data=payload_tuples) + >>> payload_dict = {'key1': ['value1', 'value2']} + >>> r2 = requests.post('https://httpbin.org/post', data=payload_dict) + >>> print(r1.text) + { + ... + "form": { + "key1": [ + "value1", + "value2" + ] + }, + ... + } + >>> r1.text == r2.text + True + +There are times that you may want to send data that is not form-encoded. If +you pass in a ``string`` instead of a ``dict``, that data will be posted directly. + +For example, the GitHub API v3 accepts JSON-Encoded POST/PATCH data:: + + >>> import json + + >>> url = 'https://api.github.com/some/endpoint' + >>> payload = {'some': 'data'} + + >>> r = requests.post(url, data=json.dumps(payload)) + +Please note that the above code will NOT add the ``Content-Type`` header +(so in particular it will NOT set it to ``application/json``). + +If you need that header set and you don't want to encode the ``dict`` yourself, +you can also pass it directly using the ``json`` parameter (added in version 2.4.2) +and it will be encoded automatically: + + >>> url = 'https://api.github.com/some/endpoint' + >>> payload = {'some': 'data'} + + >>> r = requests.post(url, json=payload) + +Note, the ``json`` parameter is ignored if either ``data`` or ``files`` is passed. + +POST a Multipart-Encoded File +----------------------------- + +Requests makes it simple to upload Multipart-encoded files:: + + >>> url = 'https://httpbin.org/post' + >>> files = {'file': open('report.xls', 'rb')} + + >>> r = requests.post(url, files=files) + >>> r.text + { + ... + "files": { + "file": "" + }, + ... + } + +You can set the filename, content_type and headers explicitly:: + + >>> url = 'https://httpbin.org/post' + >>> files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})} + + >>> r = requests.post(url, files=files) + >>> r.text + { + ... + "files": { + "file": "" + }, + ... + } + +If you want, you can send strings to be received as files:: + + >>> url = 'https://httpbin.org/post' + >>> files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')} + + >>> r = requests.post(url, files=files) + >>> r.text + { + ... + "files": { + "file": "some,data,to,send\\nanother,row,to,send\\n" + }, + ... + } + +In the event you are posting a very large file as a ``multipart/form-data`` +request, you may want to stream the request. By default, ``requests`` does not +support this, but there is a separate package which does - +``requests-toolbelt``. You should read `the toolbelt's documentation +`_ for more details about how to use it. + +For sending multiple files in one request refer to the :ref:`advanced ` +section. + +.. warning:: It is strongly recommended that you open files in :ref:`binary + mode `. This is because Requests may attempt to provide + the ``Content-Length`` header for you, and if it does this value + will be set to the number of *bytes* in the file. Errors may occur + if you open the file in *text mode*. + + +Response Status Codes +--------------------- + +We can check the response status code:: + + >>> r = requests.get('https://httpbin.org/get') + >>> r.status_code + 200 + +Requests also comes with a built-in status code lookup object for easy +reference:: + + >>> r.status_code == requests.codes.ok + True + +If we made a bad request (a 4XX client error or 5XX server error response), we +can raise it with +:meth:`Response.raise_for_status() `:: + + >>> bad_r = requests.get('https://httpbin.org/status/404') + >>> bad_r.status_code + 404 + + >>> bad_r.raise_for_status() + Traceback (most recent call last): + File "requests/models.py", line 832, in raise_for_status + raise http_error + requests.exceptions.HTTPError: 404 Client Error + +But, since our ``status_code`` for ``r`` was ``200``, when we call +``raise_for_status()`` we get:: + + >>> r.raise_for_status() + None + +All is well. + + +Response Headers +---------------- + +We can view the server's response headers using a Python dictionary:: + + >>> r.headers + { + 'content-encoding': 'gzip', + 'transfer-encoding': 'chunked', + 'connection': 'close', + 'server': 'nginx/1.0.4', + 'x-runtime': '148ms', + 'etag': '"e1ca502697e5c9317743dc078f67693f"', + 'content-type': 'application/json' + } + +The dictionary is special, though: it's made just for HTTP headers. According to +`RFC 7230 `_, HTTP Header names +are case-insensitive. + +So, we can access the headers using any capitalization we want:: + + >>> r.headers['Content-Type'] + 'application/json' + + >>> r.headers.get('content-type') + 'application/json' + +It is also special in that the server could have sent the same header multiple +times with different values, but requests combines them so they can be +represented in the dictionary within a single mapping, as per +`RFC 7230 `_: + + A recipient MAY combine multiple header fields with the same field name + into one "field-name: field-value" pair, without changing the semantics + of the message, by appending each subsequent field value to the combined + field value in order, separated by a comma. + +Cookies +------- + +If a response contains some Cookies, you can quickly access them:: + + >>> url = 'http://example.com/some/cookie/setting/url' + >>> r = requests.get(url) + + >>> r.cookies['example_cookie_name'] + 'example_cookie_value' + +To send your own cookies to the server, you can use the ``cookies`` +parameter:: + + >>> url = 'https://httpbin.org/cookies' + >>> cookies = dict(cookies_are='working') + + >>> r = requests.get(url, cookies=cookies) + >>> r.text + '{"cookies": {"cookies_are": "working"}}' + +Cookies are returned in a :class:`~requests.cookies.RequestsCookieJar`, +which acts like a ``dict`` but also offers a more complete interface, +suitable for use over multiple domains or paths. Cookie jars can +also be passed in to requests:: + + >>> jar = requests.cookies.RequestsCookieJar() + >>> jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies') + >>> jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere') + >>> url = 'https://httpbin.org/cookies' + >>> r = requests.get(url, cookies=jar) + >>> r.text + '{"cookies": {"tasty_cookie": "yum"}}' + + +Redirection and History +----------------------- + +By default Requests will perform location redirection for all verbs except +HEAD. + +We can use the ``history`` property of the Response object to track redirection. + +The :attr:`Response.history ` list contains the +:class:`Response ` objects that were created in order to +complete the request. The list is sorted from the oldest to the most recent +response. + +For example, GitHub redirects all HTTP requests to HTTPS:: + + >>> r = requests.get('http://github.com/') + + >>> r.url + 'https://github.com/' + + >>> r.status_code + 200 + + >>> r.history + [] + + +If you're using GET, OPTIONS, POST, PUT, PATCH or DELETE, you can disable +redirection handling with the ``allow_redirects`` parameter:: + + >>> r = requests.get('http://github.com/', allow_redirects=False) + + >>> r.status_code + 301 + + >>> r.history + [] + +If you're using HEAD, you can enable redirection as well:: + + >>> r = requests.head('http://github.com/', allow_redirects=True) + + >>> r.url + 'https://github.com/' + + >>> r.history + [] + + +Timeouts +-------- + +You can tell Requests to stop waiting for a response after a given number of +seconds with the ``timeout`` parameter. Nearly all production code should use +this parameter in nearly all requests. Failure to do so can cause your program +to hang indefinitely:: + + >>> requests.get('https://github.com/', timeout=0.001) + Traceback (most recent call last): + File "", line 1, in + requests.exceptions.Timeout: HTTPConnectionPool(host='github.com', port=80): Request timed out. (timeout=0.001) + + +.. admonition:: Note + + ``timeout`` is not a time limit on the entire response download; + rather, an exception is raised if the server has not issued a + response for ``timeout`` seconds (more precisely, if no bytes have been + received on the underlying socket for ``timeout`` seconds). If no timeout is specified explicitly, requests do + not time out. + + +Errors and Exceptions +--------------------- + +In the event of a network problem (e.g. DNS failure, refused connection, etc), +Requests will raise a :exc:`~requests.exceptions.ConnectionError` exception. + +:meth:`Response.raise_for_status() ` will +raise an :exc:`~requests.exceptions.HTTPError` if the HTTP request +returned an unsuccessful status code. + +If a request times out, a :exc:`~requests.exceptions.Timeout` exception is +raised. + +If a request exceeds the configured number of maximum redirections, a +:exc:`~requests.exceptions.TooManyRedirects` exception is raised. + +All exceptions that Requests explicitly raises inherit from +:exc:`requests.exceptions.RequestException`. + +----------------------- + +Ready for more? Check out the :ref:`advanced ` section. + + +If you're on the job market, consider taking `this programming quiz `_. A substantial donation will be made to this project, if you find a job through this platform. diff --git a/test/fixtures/whole_applications/requests/ext/LICENSE b/test/fixtures/whole_applications/requests/ext/LICENSE new file mode 100644 index 0000000..c38aa5c --- /dev/null +++ b/test/fixtures/whole_applications/requests/ext/LICENSE @@ -0,0 +1 @@ +Copyright 2019 Kenneth Reitz. All rights reserved. diff --git a/test/fixtures/whole_applications/requests/ext/flower-of-life.jpg b/test/fixtures/whole_applications/requests/ext/flower-of-life.jpg new file mode 100644 index 0000000..f92cc3b Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/flower-of-life.jpg differ diff --git a/test/fixtures/whole_applications/requests/ext/kr-compressed.png b/test/fixtures/whole_applications/requests/ext/kr-compressed.png new file mode 100644 index 0000000..5321064 Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/kr-compressed.png differ diff --git a/test/fixtures/whole_applications/requests/ext/kr.png b/test/fixtures/whole_applications/requests/ext/kr.png new file mode 100644 index 0000000..b18d76b Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/kr.png differ diff --git a/test/fixtures/whole_applications/requests/ext/psf-compressed.png b/test/fixtures/whole_applications/requests/ext/psf-compressed.png new file mode 100644 index 0000000..3bc0d5c Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/psf-compressed.png differ diff --git a/test/fixtures/whole_applications/requests/ext/psf.png b/test/fixtures/whole_applications/requests/ext/psf.png new file mode 100644 index 0000000..c5815e2 Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/psf.png differ diff --git a/test/fixtures/whole_applications/requests/ext/requests-logo-compressed.png b/test/fixtures/whole_applications/requests/ext/requests-logo-compressed.png new file mode 100644 index 0000000..cb4bc64 Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/requests-logo-compressed.png differ diff --git a/test/fixtures/whole_applications/requests/ext/requests-logo.ai b/test/fixtures/whole_applications/requests/ext/requests-logo.ai new file mode 100644 index 0000000..7da8cb7 --- /dev/null +++ b/test/fixtures/whole_applications/requests/ext/requests-logo.ai @@ -0,0 +1,8722 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + requests + + + Adobe Illustrator CC 2017 (Macintosh) + 2016-11-23T19:55:35-05:00 + 2016-11-23T19:55:35-05:00 + 2016-11-23T19:55:35-05:00 + + + + 200 + 256 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADIAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FULqOq6XplubnUryCytx1muJEiT/AIJyoxVA6d5z8n6ncLbabrun3tw5ISG3u4JXYgVICo7E7Yqn GKvGra91L82vNmvWKX1xZeQfL1wdP4WUrQtqN2n9/wCtLGySeitfgCNQ9Wr0xVLvO35D6domh/pX yQ8+la/ZUeG/s29B6VHP1vRC8k41rUN8ty2FWRfkH+aWqedNI1PTNfEf+JfLlx9U1GaEBY5xVlSZ VFACTGwYAU7ilaAK9K1HVdL0yD6xqV5BY29aetcyJElfDk5UYqlFl+Y35e30ohsvNGk3Up2EcN9b SNU9NlcnFWQRyJIiyRsHRwGR1NQQdwQRireKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KqN5eWllay3 d5PHbWsCl5p5WCRoo6szNQAYq8Z0v/nIbUr7VLiwg0SzuEQ/6JetqBsobpAQpe3+sQfvVDdeJ5f5 OxwsqTm6P5z6+JV1Gez8maFGpae5sm+tX7qR0jd1ZFA8eCsT0xYqVt+Snky7ne+n0P8ASt7JT1dX 8w3VxdXchA2JhqVUb9Kof8kYFeffnT+Wfka1/K867oXl+20vU4bmVbqa1DKyfVknDceR+EetCp6V pthV9HaPIkukWMiMHR7eJlcGoYFAQQR1rgV5N/zjlawaHpeueVLicfpnTdQuXvrZjR6tPIiyhT8X B40RgfAg98VTL83/ADrYWtsdGkna1tAVOpXrExRsWVjFapLsebFebFfsqviaYQFYV/zi3Z6jqvmH zr55ki9DTdYuRFZrQL6jBmlditeoVkqe5Y++JVlH/OR3luy17TvKlndMoR9bRGjbq8ZtpnkUfMR0 6daYhV91+S/5eSxaiYdJsrG+024ZlmUpHD6ckaSBmR0lSFQjcSEUAla9DgVjunflTNp2oR/oG8m8 t6mEjdL3TJpY7e4XYoJola+t2XvvHHUdqYVRGqfmD+c3kOHl5iuPLvmO0gjE0ssczWd9KC1OCQxh q9PtLDTuehxVP/yc/O/UvzH1W/tm8uLpNnYQpJJdG+W4YvIfgURGGFirLVuYqu3uMCvV8VdirsVd irsVdirsVdirsVS3XPM3l7Qbb6zrWpW2nQ0JVriVYy1OvEMasfYYqxSf80m1T1bfyTpNxrdwgUm+ uEey05Fb9ozyqGenhGp+YG+GksL1Xy//AIkuG1HzLdt5uvYXCRaNYTNbaNZuBVvVnDGNKdGLM8h2 A8MVtIfNd3pmt3K6XDpn+K9YtITb6TpemK0GkaczcQ0Ubo0EhcKAXd+gAARRUqqEg0n8tfN456ZZ eaX/AEkJkS8i0uSYabZO5CJFPeyyGNpOTBfSiid67e+KeJB+ZQ35c+cLC88ueYbrXL+zKT69I0rN HHGrCKVZV5yH0ZJJEUcunRSzfZVu3uiXHlzz/wCTNTsLH4TrCtIzR1lhZ2UfHFIQUHLhup4sGryU McDFMfyd1eTUPIGn21yU/SOjc9H1CKPlRJrBvRAPIA/FGqP8mxVhv/ORPlCeSHSPO+nEQT6FME1d oZXtJp7GeRFKm4io9I2HTsGJxV5z+a3l21svq/6W0nU5frpC2wvtSS5ihkmKwxTFrcvIIyxAblWv zwpt9KeTPL6eXvKej6IEhR9Ps4beb6uvGJpUjAldQQD8b1ap3Nd98CGHecuev/mXoOlWqerH5XST Wb7meEP1mVDHZRu1GIoBI7EDb4fHFXjP5/efLW8sR5d0O8l1FnuHutQurQSelcyIoE0h4GjxR7Rx gMyrwblX4ThVvT/ydh1HRIfMnkvWri70lwEki0W6ljlgYKBKRDKjS1JrWOpcVqobZcVYp5e8m+Xv L2tve+btNuPM+gXkhSPXbT1ZpI5F+3FPa8gRL3cPVl8K4q9ssfI2i6rYW195F1aDWbOxAjjtJp/q 17aFWLCOK9t0E8LrUjhMhbtyUVxVX0vzV+ZujXh02KKfV5Y1aZ9I1uGSK7WJNj6GpWaT20yljtzD sO7YqyL/AJXh5Y0+3jk83WV/5UkcCh1C3Z4HcAFlhuLf1kcCvU0+VdsCsp8p+ePKfm60mu/LmpRa jBAwSZowwKMwqAyuFYVHtiqeYq7FXYq7FUBrfmDQ9CsmvdZv7fTrRa/vrmRYlJALUXkRyag6DfFW Cw/m7d+Zjcxfl1osmtx20noza7euLHS43pUlXes83D9oRx/TuMKvPrTyPaR3V63lvWJNb873kpk1 J7K1hudIt3MnP03luixRU6U+sM5p9jtim1P81fMX5veTNEtbXWLnSLzRtRAtpzDp6GFWkYlkEck3 7xkVagekFaviMVCS2Gv6DfQW7+cfPEsVghUrYx8bT01TtHa6fG6x8T4n54U0mn+NvyuErWHlez1n WtGgEcK6Vptq0NnPO5NDczfu7icu3SJqKaH4TgRSbahp/wCaeuQW31yBPKOgiiQaXYfV31ARyAlv SjqkVuvE0Y7yrUijV44qhTf+RtA0mXR7WGDWrycr62k6cfrrzioKtfz1EsoVj8f1loox/vs4oY1b /lE2qtrE9hO9p5pnie7j0qxuDHFayM6lVuZFWOJBRmJBCV/3WnFasptkHkT80NW03zDfTXtibm+i iiXzrZ2PF5HkjTjHqUEQYAtx4rOqfaBUgVWhVem+e9d8sea/yd8zX2l3sWpaa+l3UglgKtxaOEyL yVqFWUqCVYBvauBDC/zfaxgtvIVzdAT3NzdWOnm9joYfUjvrOZlY8thSCWm3jirN/N35q6VYCbTd Ant9Q1oBleZ5ANPsiNi97cj4E4n/AHWDzY7Ad8VeHaq8nmyy1Xyl5auJbzT0nF75780LGst5dSmi n0bZWDtbxMeXEbj7IAoSSlD/AJXaB5S8v30nlbzbCNI12+dZ/LPmwlvqtzHTgLc8m9MFqnknIV5U BVgjFVmup+S9W8p6tJqum3beVtWuCA+qxMZtEviWqFuw4PoSGvwmYfaJrK5OBCM8weYLy39G887e XLzTrh1J/wAZ+Vy0iCMEEG4Xi4KenQ0cyr144pQ8/mL8n9a0+HU9ctL+/wBQIKReZbLTLrTbh42b jGv1i0KcqbL9rifDemFDCPMmped7Pzla6L5X82a7pGlXPEfWvNUv1e2qyl0SKdxzXkNlUxg++KvS NU/J630fW5dcudLm/MLTrgq0sGo3Bl1G1PHjJJbiQpb3Csu/AhWrShOBWP8AlvRPNXli6vH/ACr8 wm+0iGX1bnyZq8chltS1TJFNG4W8g3+w6KeR+1UCpVZpbf8AOQWm6cY4PPWg6h5WuG63JT6/YE1p Rbm1DmviGQUxVnXl3z95J8yAfoHXbHUZCOXowTxtKB/lRV9RfpGKp9irx7zT5V/5yGe5k+oearW9 0lmBa2toY9OuyvXikhSfjQ9/VFcKRTFYfJej2spu/PXkPzJrmrDZ7jlHrKyqGqp5xzErX+UAU74q rp+aX5d3pmtZNF8yalbpMIYdDlit7eygNSy231WCWCJlTj9mVXbxxpaTDWfOH5t6hpSL5d8tN5Q8 uJSJLiWFpr0qx4qsNnAvqJWv7MZp15jFCl5c/LTzFLqaeZtWc2k6BSNY8yOLy+ioeRaC39Q28Pxb KzFeI/3XXcqu8xeZ/wAuS02h6Zpt5521i5jaO41FZyLoNzBSM3p4/VoWYtXjwj7KrVIxW2MaD5B8 6Nos+gwahdT6h6rj9FaaY1sbTlRib68lWjOP99qPUNOijfFNsP1m+0W416Xy95kOupc2TJHeoZvr c83pgCRUjglNvapRBWiTN71xW3p+nav+Tsdlb6Zo3ljUxcao6+hZacl1EJpoNuJmZoAKdJHNOO/I gVxWkZd6J+Z2u6OLGDT7L8uvJ6HnNY20ynU7gJQlOcfowr6vGm7qd/iJGKGGa3pv5eTrHoMtqj6n Kwa28v6OEn1GSZvhLXN4VoZOLFiAVQUJZ5Dtilhev+XL3TvL91qGtwIbWGRreKR2ktLyaadZCIpp QiPcei5qZGj4Ow4q1K4q9C/5yH1O3ufJH5btaRC6s7+4ivEglqFlJjiISQAfDz9Yg+GKGOaf5Rvd Q1f6951uZdJ0JTJDJpOkeog06dCUCehRqGlSxUEsu68gdlL0y38o6VJ+iG8uhNO1WyiWLR9f0WIt YXpjXb1nh5rFIwDCSO4qrdORrXAhdeXejRXPoecrK68qNqM4Goho0udHvpf5oyfV+rTNTlyWgB6s 53wqlGq32m+XoLn/AAj+aelQWLB1k0DWJku4FBH2UDtPOgIO4oa1+jAm0PofnP8ANfzLYp+itWew 0W0ol9c6ToJVo4WBCS2i3Lyi4iqP91KHC/EFPQFCh5I0pPImpXtz5rgu/Nmgay8jReYrFmn09I5D Sf61pqH00oQzSOA5XeoGKs+1HyJaapo9fLdzHrvlyeMcNFuZ/wC7jJ5BbS5dZfg/lguEdP5SmBWJ aZN5w8myGDyxqMkiWx5f4A1l1SXgx40tZH9SQIpJKiCSVCf2u2FVTWPzo/LXVLqO1/MDyxqXlrXb VS0N7NDKHhANC1ve2nG5C17qgGBU8s/zw/IKbTrfTrrXzeMi8Od5BfTTOT1DTPDyc/F44VSa6038 k/NF19Y0LydqvmOQMOM9hZS2sQk7f6XeGzCU9pKd8Cpv5C/LL81LHXpL+581X2h+WlkRrPyw9yus TLEoAaOS6ukZIwSDtGGoDs1RXFXs2KvOvPf5a+d/NHrwQefLrSdOmdiLK2tIxSNukbSxyQyuKdeT b4VUtB/L/wDMny/pFtpGmea7D6larGsZOkpE1E6qQkxB5j7TH4j41xVIPON5+Z+mzLpp87wSavcr /oOj6RpcUt9Pt1KSyMIUB6ys6qB92KULZ/lZrF1b295+ZXnR4iij17G3uPq9K/ZSW6d/j2pWi/6r dy2m0Cutflb5bsX0ltYs/Qt3b0fL3lt3kllBHHhPfuzTykqPi4NH4NUUxQmpvvPXmLRVg023j8ke T7ZT9ZS1JW7+rryLn6z6YjgFBXjCryV6mhrihEaafKPk7QAqzJ5Z0GaNopdVu1+r6tqQVTX6tCQs sYq/Lmw5k7hRUPirz2LSr380teS68l6ePJ/kvRo/qU/mSUGG6uIlblKAw4s1eRJV2P8AM5BNMUtf mJ+V3lzR7FdZstYNjoYRbUHVJriae4R2LSTheUJRDxoihW505KN1xW1LSte88WulxXXlLyrBpf16 3ddJttKsD9dkgjKqLy5DyPIIXZiFdt3I+nFWKapPrvmG41CEaZPeSadJLBd61qpDyfW/SAEVrDAO Mb8kkkaOPlUbyEKrHFWafm79W1H8mvy31CIQxiOa1RIXqgJZAkn7wcuC1SpPE0/AqEFf+YvzR0fz ZJp1rosnmO7towp03VreK6l+o3ZHpBbu2k/0qHkp4s9aEbgYq61g16P81bQ6sLHyVNqkCNbX+mve pb3FC3MxfEsaSofgkimQDahUVqVWc+dtE/MLWPLn+GV803A1+SMNdabdx2iWmpwKB6psLuG2hkHw /GyHky9KUoxCrPI8/lTX0uItKt+PnTToBZavoGvyCdbkAFT/AKWIyWk9MFVmjFSv21YUOFVnl3Rv Mvkd3tvJxlntLNS975RvpQLmEOKerE1WjdS1PjqY23pJFuuBU3038xfJ+p6hO6aoPJnmtCGvtL1d fqtteAqP7+GRvRYt0WSKUvTerL8OKtf8q3tL6A635bux5J1AyGSOG3uIb3R7o/C/rW6RvGY1c0HO P0m8VPdVrWU/O620xrfVfKuiedraKk0YiuuG671EV5ExLCm1HJ8MVUNN/M3zl5pQ2U/kiC51KDe4 0z6zaC6gqf2oby4sp06fa4Yqmuo+XfMOraOLR/y501bjl6izXmoRwSRyLQJJFLbJdzLIB0ZZAR44 qmP5Y+VPze0e6d/NnmS2u9IBf6to8aveSxq1eCnUJxFM3D/LV6+OKvScVdirsVdirBdT/I78rtT1 KfUrvRf9NuZGlnmhuruDm7faYiGaNd6+GKbSrzT5A0Ty/pyv5U/L238z6hLVVhup4BDDxX4HkN9I 1RU7BB9I2xW0i/L/AMu/mDo9y+r3vki3bzVeRmKW/l1G2gsbaDn8FtawW6XLQxIlPhVSWIJLb4UK n5o+afNmgaYs/mLzjY+WnuKpZ6Po1t9avLqu1FuLuSLhQkfvPTRV7ntgViPk63/J6yjOt+ZtSk81 +bbtxcx2sEl1qTRMRVYI5FCxzSfzs9ATsAB1Kp75z/MHVIZbKOz8vX0FvPPFaaJo+pmDTLdpwQFK QMBLKE2b94hiSlaq1DilfYeVfLa3UfmP8yvNGk6lq0fP6nAbhX023kJ6qJmi5gUoefxGnxMRQKoY ov5gf4n8wL5T0PVYdM0i/dm8webr+4ht7zUQjelwt1Ur6EZ3SGNd+J+HjuxVegWMnlzT7bUtA/Lm yTWtQ0+0lgX1Cq6VYtQrKZZAvxSzOSXCBmkpSqqtVCsC8921/F+RH5bWmm2gu7m5uLK2ioocNLKp lVVjqORd49h0PTCqtqs1nqHlyXzFoWsNoM+iyE6fDfP6Mlnwm4yWUsjheUYehWGTi4p8IeiEKqul +b/Jfn+1a01WKFY7mSJ/MWmSF5Yo5+QX69ZzWwZopWP7aD05Oj8DuyqcXmm+cfKFtdaTr0E/mnyF bqlxpuqQpzurRVoyyc4W9eMwAV9VAx70NTxVRGmp5J862a3c/mCBdYh3tNZiKwytCpV1+ssh9BpF dQdwAdi0fIUCqvrXmH8yvK2v6dPr9jpvmTS2qNL1xHFl6VxICnou7h0jeUNxViyxt/rbYFTHzXqP lzzLpJsfOf5da2oZSGkjsY7xoWIoWhmspJpR9rqFHuMVeUWtz+RGkaxDoWutqFjblP8AQddthqel sDupivbMiJUmWtDLDHwetSFxV67o35KeSLm1i1PQvMOrPZ3aK0N1BfpcxugNVKvLHNWh71riqOb8 gfy9utRh1LXF1DzBqFsALe41S/uZilN/hVXjQCu9ONK4q9HAAAAFANgB0pirsVdirsVdirsVdirs VdiqlcWdpcU+sQRzUqB6iq1AevUHFVXFXkP5leX/AC15o82HTNM8r2nmPzYkMaX2pak8v1DTIPtR +sEb4pH51WGKjGvJiBuVUv0nTPyN/KiJrLVZrO+8w3j89QnW0SWQOxrxEMKyJaQrX4UqNv5jviqc 6v5p/KnzXZx2ljYX2uFJFmEOladdAtwNOMkrRRQhK9Q7gVp3AxVLfKfk3zNovmlW8r6Nd+VvI91F dS63pOpXNrcLJO8NIZLWGGW6eF+dOdZAOI6bAYqwzzDfzW+lfkTo8S/6PNqFhI42ZQ1vc2wSm/UK zivhXCqRedPKsl5541zXPPOl3dvf3NyY7QRWLXFn9UibjD6LxxTpI7xqObH4tzsMVbTQfLl2aaf5 Lv5Z1AIay0i4gkQMOQcSrHFwPcHlXwwKq6PdedtLvbjT7DW73lFEWk0LzAlzBcKrGiUlT6vchOtJ UdhXxxVM/wAn/wAs7rW7qfVI49Ih05boW2taI0dwt5bsKNI8F3G6yr6sTBk4yek1ela4Vetaj+S2 mS289tpfmHW9KtrlSk1ol2LyB0YAMjLfJdMVIFOPKmBUw/Ln8t5vJFvJZx+ZNT1jTigS3sdQeKSO Dj09ErGroKbBA3H2xVmUsUU0ZjlRZI2+0jAMDTfocVbRERFRFCooAVQKAAdABireKuxV2KuxV2Ku xV2KuxV2KuxV2KrZPU9NvTp6lDw5V48qbVp2xV4f+RVvez+TJL+4jSXzHq9zf3GoXMh/eXMhukLK 1ePHlHE0Y6AcTil35PeZB5Z8z+YPJ/m62Gl69qGpT31jfzLxW+ilYlQsp5BggHwfGRvx2I3KHpVr cCP8ydQtpBVrrSLSW3bsFtrm4WRf+CuFOBU31S/RIpbaFHuLlkKmKKlUDD7TsxVVG9dzU9gcVfO8 sUa+V/yXjS2a7u4brTpbW5UcQEWQuy8zt8aoBQ+2FL6M0/VrK+5LC5WeP++tpAUlQ/5SNv8AT0PY 4EJJ5U/e6/5tuUasL6lFEvSnKCwt0k/4YU+jFWDf85CXNhCfKgt1V/Mb6iy2aIFMn1NreQXJeu/o hvTL+4FMVedflrqNzo//ADkbZ6fYSNHZa9YzR6jbGvps0MMlyjqK05B0602q3icVfU2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV4t5t8ueZ/IHmG582eXvrGp+WbuSS61LSI0EkllcSOXa4gV aM0TepLzUA8eR23qqlnWla/5D/MXy8i8IdTsLqNZZLO5RWKcqqC61cI1QwBrWvQ4rSWah+VV5FKl x5Y80X+i3EUZgtxOsepRQwmlY4vrQMyKeI2EuKFK40f85rCxFppt1oOoxKrBmkiutPmJrUGqNdoW b9o7dfpxV5frHlf877bRPLuhx+U4p4PKrBrPUbG9t2Ex6LWOQxzKEXtShIB7YUobXvzY/NuSeM61 olp5YdTxtdQvbO/t+45BbpXKgHuOQHjXAq/TNS/NWPTTZJ5oh0+zmd53On2qNKzzSGWRxcTcnq7M fiNf1YoQWpS6N5e+ua9qt/LdarcJxk1C/la5upQD8MUYJX4eR2RAoxVk3/OP/wCVWuv5lm/MvzVC 9pdSo8eh6ZMGWWNJRRp5A1CpKMyqtO5NKccVfQmKuxV2KuxV2KuxV2KuxV2KuxV2KuxVIf0trGqS 00SOKLTwaNq10GdZKdfq0KlDIv8AxYzqv8vMYq3HoOt0Bl8y3xk7iOGwRPoVraRv+GxV11qOvaSf WvYk1HS1A9a4tUZLmEDq7wVcSr3YxkEdkPZVNjf2Ishfm4jFkYxKLrmvpGMiofnXjxI3riqXfpHU tRAGlxfV7VhX9IXKkVB/3zAeLN/rPxHhyxV5X+bem+R/KVrHcafHcweeNYZodPuNNuXsriaRnLNN c+h6cTIrPuWTfpsNwpDBpbn827G2VtM86XEsixj1lviWXmKksjn1OK7/ALSn54ra2y/5yA/NOG7W A3Gm6jI/BYYxEbgytUAhEtPTcs1dtqHt4YaSzi0/P7zRbWOk3eteVKw69O9rpZsLj1J2mSZoCjWs ioQS61X94ARtWuNIpnmg+e/JHncXWhurJqCKVv8AQNUhMF0qsDyDQyfaFOvAmnfAh5f+Z/5JX+ga bPq35e6jJpsLuBeaRKTJbormnqQtxkaKh4g1+FRvVVGKbZJ+UXlD8sdQ0mHW9Dtph5msHMN/d6oT calbXifDJHKJuap0/wB1gAjpQ4qXqIvprZf9ySrGo/4+0r6J/wBYHeP6ar/lVxQu1TV7LTbP61cM SrFUhjjHOSWR/sRxKN2Zuw/hiqVmHzhqMayNdRaFGzVFtFEt1cBd9nlc+iGO1VWNgP5mxVUfR/MM bI9r5gmd1ryjvLe2libbaogS1kG/g+Kr7PX5U1GPStWgFpfThjZyoS9vchASwjeg4yADkY23puC1 GoqnOKuxV2KuxV2KuxV2KpD5seSeKx0aN+B1i4+r3BDcXFqkbS3HEjerJH6dRuOVffFU7iijijSK JQkcahURRRVUCgAA6AYquxVKZtcedjDo0QvpQSr3HLjbREEg85AG5MCPsICfGnXFUm8u6FBZa3dW N7It3JAkd/Yx0KwwfWZJRKsMJZwoWRKg9QGoMVZfir5p8/TtqP5/amtzV00awhisvBDLFHIfpPrv T6cKejGfzZ1efT/K5jhdo3vJVhaRR0ShZv1fdXEKA9z/ACL8o6Vofk6K9toQbvVAs9xfPvNNVFJL Md6c+VPvwKXnMs1jqOrflWpHqRNruqy21xGSQwhvnddqdC8QPy8MKvUvzC8tafqLyTXC8LhLZ7zS 71RV7e/slLq69OXOM/Eh+FgmBUF+Q3m/zP5q8krd+YIP3kcnox3LAKZkMUcikpxUUKyijd/xJKl5 zrctv+XH/ORdjqUdzKmleaZDa3togJUF44lV35H7IllVhx3+E4ofSGBWEjTNTuPOV7qOmukNroyR 2dvYzcmtpZpU9e4kRQQIX4SonqIp/aqD0xVktnrUMswtbqNrK+P2beanx06mJx8Mg+W/iBiqY4ql +vaUuqaVPaV9Ocj1LScbNFcR/FDKp7FHAP8AZiq7QNSOp6LZ3zUEs0Y9dVqAsy/DKg5b/DIGXFUf irsVdirsVdirsVY/rcEn+KvLl1X90rXluR/ly2/qKfugbFU/xVhP5m2er39ra2en6hLYxqk11erC qkywRNFG6EsQKKJy5HQ0piqE06PVBDbxHzBNaK44W0ckcAik4mhWOZERARSnAxhhQ/DiqWajP5ms /OElrbRXupanHpyurwSxGNYpp2CiV3ihVKGFjSjMf2e+KpvHa3LJTWfM+q2rSCpiSEWcSMvXjM0L FgP+MmKvKPzW8uv5d8xWvnPSb5tW0e4VYdbuTL9auFZFIV34MoKmNQoNPhp8qqQlvmLSLDzV5f8A QjnBhmHqW9yg5hWKlQ3HauzEEdfpxQjfyx/MDV/KFvZ+VPOF1c29hbOkWk6zCY1sGjZi3p3MkiM6 MN6fdt9rFKjpes6XqNz5ATTJ+A0+81G8N1GwARXlvJeI5I9DwCs3JejLsa7FWZeaNV1Lzpe3flLS b2/FnbRn/EGrSeh6cIkRo2tIPq8YEk0ocr9qgHLYnFQ9O8l6FbeXPKtpp0aPFDbIzcJCGdFqSFPA CvFaAd9vHAh4BawR/mL+d02oaoy2eg+VZneeSWREZ7j1G+qxAGv7wBEEib/YINDQYSl7JTWLef1t M1TVNatQm9nJEkZqGG6XJihVvh/ZapP8wwISLyTPrmoTeYRYyzWywalK05vJ/TkXnGjASIYpviUb HcdKYqjfMaavc6JOIr+a9oPhaJUWB2XZUV3SRpHZuhjAAP7QxVk/kCLWIPLFvaazdNe6laNJBc3L nkzOjGvxGtadAcVZHiqQeQmkk8r2106lfr8t1fop7Je3UtygHtwlFMVZBirsVdirsVdirsVQGuaX +ktOe3WT0bhSstpcUqYp4mDxPTuAw3HcVHfFUPpOvxXU50+8T6nrMS8p7Fz9oDYyQMaerET0YdOj BW2xVddKreY7FWAKmyvAQdwQZbXFWM6rd6doMl5YS2p1KOZUkk0ZB6zy2rH0xOImqP3TDhKTtxAY mvVVvyVpGrWGjRa1Z8LibWFS8u7CWR24o61hjguHLsPSiKqA9VP+TirLLDVba8qnF7e6X+8tJxwl X349GXwZSVPY4qx6/h8valrTzXtnFdBFktrW3MKu9w67TOdt41/u1LUUHlXqMVeK+cdLbyNePLps az6DLSaXRkuElubD1JBGqwliDMru393+z2JXFUJbeaPLeozPpzzrHeH4JdOvEaCap6qYpgpb6K4q l13+Wvl5pjdaY1xo178RS4sJni4lhxNEqUAI2PEDFVXyn5n/ADK/KzT/AKpaRWfmDyzC5llBi9K6 RTQMx4HkxoPtEvQdqAUKXoFp5881fmwEg8vQy6F5KLNFql/6kS6ldceIkhtFLfu0+I1k60Bpv8OK s/8AI9n5c8vKPLuixxwadRpbKMDjIrKf30U3KkhkBPMF/iIJ/lwIZFcaoiySW9mn1y9jHx26Oo4V FR6jE0QH7/AHFWB+dtNfSPMOk+Y52+tR6lcrp2raRF8EFyphlaBvTPL1poilF5/a6Cm2Ksg8vSw6 veveNIkj2vFJoE3W3koGS2ANKGMfFJUfbp04UCqbaB/dXn/Mbc/8nDiqCv8AUV1uSfRNJlYpvFqm oxH4IE/biSQfanYfDRfsfaah4qyrIIYYoYUhiUJFGoSNF2CqooAPkMVXYq7FXYq7FXYq7FXYqhr/ AEvT9QjWO9t0nVDyjLirI3TkjfaVvcGuKpCfK2nxa3aqtxfFDa3PFWvrtiv7y3+y5k9QA06cqYqi bny3Y2ltLc6XaqdTVP75yXnnAWnpyzyFpH5DoXY0ND2xVLvKV6lh5a0W4BY6VcWsCymQkm1mCBWD cvsx8xxYfsN7fZVTjX7a2vFgs+Fb2fn9WuFqHgUAepMrqVZePw0oftFcVef+RvJdnLpIF/LdT3Ns 3p3UYlZmV4yU58JS4kjNGKkdPsgGlcVQXkjQLDzX+YGo67NNLcaL5TufqWh2jmscl1GDzupG25tG WYRClFrUb1JUor88fLHk7zBFDbakI4dTij9SW7PoJxtG5r+8kmBoqlWZSASKGlKk4Qh4l5XsPM9n H5QstFvjqk3mL62gs76Q+iDa8ifSm4GRBxXjQ1Fd/ksiGZ2eoGW4ubK6gksdUspDDe6fPxEsbqAT TiWV0PIFXU8WHTAxSfRZbHyl+Ylj68PqeWPMr+hfWQB4xXdKJNEQyCPmxVW8anr2UvTdd8oTXesa Z+i4ZdGvo5RKri7uZWjtyShSYeoYl9VpACI9wP2t8UPUtMFpHYgwwpaoOXrRLQBZFNJORoOVCPtd +uKsU87rdTX3l40Jmk1VBYWgqdo4JnMsgFKD4RyNPgWvc0xVkMflbRERituI7iVhJc3cBNvNLICT zkkhKMxqxO5piqXab5P0mRrj6zJd3SJcSBIprqdowGoX5IGVX5Hrz5fdirJLe3t7aBILeJIYIwFj ijUKiqOgVRQAYqqYq7FXYq7FXYq7FXYq7FXYqgZ/+O3af8w1z/ycgxVG4qxK7uR5Z1K5W6hM3lrV GecsimT6tcMC1wrRgHlFLvJtUhue1Dsqq+Vri3iht3SQzWGoKW0m4ZieNvUtDbknevBiy13/AGT9 ndViXmfVY7V7kJJ9XtEitzfzF+BkSbjDJEg6MqP6TOajqwru2Ksb/JzzXY+W/Nur+VdXuliGu389 7oN1IphiuBI9VRCwA5tyIHiV26ipSXf85P8A5eecPMdlaX3l6xfUkh9JL20hdVcxxNK3IoSpfeUU 41pvtiCgMK/LzW9M0zzP5HGpTCxi8uvrMetTXgMCWr3KSmFXMgj4VHw0I64syFXXdei88fmw/mjy 8rx+WbO2Sze7dTGL6eH1R6qKRyKgTBKnsv3BgxX8w9WubnzXpGi2ieobaRL2440qvBhx3JCr717Y WQfTPlueeb6zPeur3ay2379d0dZZ43cq1ADxX00I7ccDFFaxrWm6dfrqt9OYtGlKqsEal3uLpaCK YRoC7rQcBtuQp+yAcVRPl2G+1bVpvMmowm3iRWtdEs3oWjgJBlnft6k7KBtsEUUJqSVWT4qg9M/4 +/8AmJk/hiqNxV2KuxV2KuxV2KuxV2KuxV2KoGf/AI7dp/zDXP8AycgxVG4qxTzhPc6pcReWtOt1 uLiTjc388jFYbaJDyi9Qr8RMsiceA+0vKtBvirEo/wAvdM1OO5le9uk1K3LC+jgJsoIGVmY+nFb8 Qzs0dQzux4kMT9kYql2paFpXl/ULeTSxMusw2yyW81yWvIrj1ahkYTc/T4IHeX0ipp8XfFUoj8la Ja3Woad5ytJ7Y6JpR1LTNYtZzHNEtpNLJNLZyxlKtynGzKKAJyXFVDyj+e/nTQNCsp/O+nNrNlLG hbUtPWl3DX/lohIRH+GnxKV71rtimmQ33lr8kPzhkGoWVyLfzBLFz9e3ItrxlApWWCVSkwWlCxRq dOQxTuGEec/IPmr8sNPfULXUhr+ihT+6uD6U0BFAP3dWBjBKj4KUr0pivNEeRvKV9Hcaz5psBHqV /pss1NckqLa5YIivb2MPJgy8gyhydzx37YoZhpfkHStS0u+05bi9iSOeIm8SWeBJZLmQUaO2RooU WJmcEIgqwpXY1UJjYeR49Pgn03SAsGuXqGC/gv3a5U2qijzwTMWuERiRwNWAbYrUVCrN/KGrG905 rOeBrTUNLb6peWjgBl4D9244/CUkjoyldu3bFU9xVB6Z/wAff/MTJ/DFUbirsVdirsVdirsVdirs VdirsVQM/wDx3LT/AJhrn/k5BiqpqV/Bp9hPezn91boXYDqadFFe5OwxVjHl2e9XRLSSFA+ueYR+ kbqapeKITKCHdv5YouEca/tUH+UcVRWoWUWhs19Ef9DuozDqbsdzK391cN2qWJR/9YdlxVJ9It7L VIJNRuWaSW6jJ0qOHiZFhZQqXKhqqvqIiBfUFKbftEYqwv8AM/TvMGs/lhfC4T0vMHluL1Pq8ZJW SCLj60quCvqc4BVl2pWnHdTirznW9W1U+SLC70gejPeiABq/3aSrUHkKgb0FffFLG9Ku9RuPKWgy +teXmoW+p6mmnfUw5uYLuWGyNtySP94tZY5TtufiIxZW+g/P0uot+QmoS+bpWj1JLYxS3QT0pCZJ fQRzGporyRSfGvTc7U2xY9Us/LL/ABNqP5feU7BZlS1NtHPexFVQiG1kX044JAPtSKyO3IH/AFhW mKll2o6rp2hX1oWT6ppd1JGjq/wfVnR1l4OoBHFvTcowahYsBXbFDIrDTJbsDVrpfQ1SQs1qxFHh gO6QOBTkpA5SKf2jt0BCqX6rqDWWpabrSQGN2lOnavC1AwgYO6SVNOYikSqU6qzcdzTFWWAggEGo PQ4qg9M/4+/+YmT+GKo3FXYq7FXYq7FXYq7FXYqsuJ4LeCS4nkWKCFWkllchVVFFWZiegAGKsdjX XPMIW4+sSaRojkNBDEON5cx02eSQ726t1VVHOnVlJ4hVSk8s6XHr9nGrXZ52d3yla9u3l+GW2p+9 aUvtXxxVIPMU2v2l9NZaSz61Zww/V4tLupD6pvGjJLpccXdxHG6l/VNASN69FU6/Lp9Lg8k6XfBh 69zbQx3cp5NI88Cei0dGHP4GQqqAbdBiqdXdpc6tbT2lygt9OuEaKROs0sbgq1f2YwR82p/KcVYX 5a8kjSFudKt7i9BspK8UupFkeBl4wOqsxjZSq8P2SGVtyKAKpT580CKHTtQu7a/mae9jjtoIoTOl w1zI6Rek4MoLNMjryVl+yvTauKvIvMHmK18sa5PoN3YILAp69zpluxkfTVkb+6YlER4zyVowrVUG nhinm630HQr5INW8m3y2Gp2TB7e5gYkBg3IJMhPJd69fiGK8kw82+efMn5haVa+V9csreO40iY3e sejNQXUVvXk0UQ+I8U+3xYkVrxFDRS9e0fy3ZWkIm0+6kityDBpunaUruVWN+LuryvIlGkU/GwC0 pixRR/Lq01jUbf8ATsk8/wBVAmNv9ancoCTwWSUMnJmdeXwKoXj3qDirNA2pWZPrE3tsOjooE6j/ AClHwyf7EA+CnFWH/mHq9obvy2mnIL3Vrq/52ttGd5UtY3Z0Y9ECu6gs/wBipPiCqiPLul3l3Ky6 lqlwYo6mwsLSR7OKKJSFaJhGRMzwv8J5PSnHYVpiqM0Xyvpr/XLmKS6trsXcyi4huZwSEcheas7R yUH+/FbFUcmpanpN5Da6u63NhcusNpqiqEdZXNEiuUUBAXOySJRSfhKqaclU+xV2KuxV2KuxV2Ku xVIfMyxXt1pWiyN+6vbgz3UVaGSC0X1Svupl9IMO6kjviqe4qxXzhr8Oi6jYzsyC5ntrq3sY5GCq 88kttwBJIAAoWap6A4qlmkXF3bWnopcxWl1Kx9bUrmjluZq31WIkNJVtzKwCsfiAZaAKpZpks/lz zrqEthaX2r6VfWa3d4zRf6SlwJXWWSESejyR/gLRxr1NV6UxVmWma9f6zaG40yO0EdQObXAnKnuH SEEBh4c64qk3m6S90hI9cvdaS3lgHpNBbQxxvLC7r6ixLK07O4A5KPHpSuKsUOlSeYvMkV/fLdW9 no1rHe2iXExluHWeRkFxKpDxx0jjcpGK/wCVuaKqx78lBbxN+aWr6jbx3F/Lqc1vc6fKo4C2h5gK wox9P96Q+x2XFLxafWbyz07R/NGm20tvrWtanewSiWSedHt4xbPBCwkZ2ZfTnpyJLbVrXCqaa/az j8xvKT2ET293qlxGb6AMRT1GT1I2INPiXkGFN8VD3rywbzyNpOqaZdtKbHTbsx2l5auG+CaOOZEl in5qgHq8RIB0BrTbkEM58vW2r/o5bu21y11Vbusxu/QRkkYgAcXgeNSq8eI2OwxV2pea7zSJIIL+ yW4uZ9oYbGQyzSU+0VhZUYAV33IA74qxny9dxX+v6pr+t282m6pBL9TsnihkZIrdFBdJHRZIncsa OT1oKUpiqO1bWEtR+mLGe2uJbdllu47aQETLQLzKklonK1Qq1RQj4iwXFWQ+T9QtNR0uW+s5BLa3 NzPJDIO6tIT9/jiqaahY21/ZT2VyvOC4QxyAGhowpUEdCOoPY4qhvLl/LfaLbTTMXuUD292/HjW4 tnaCYhewMsbU9sVTLFXYq7FXYq7FXYqx7zDZj/EflvVCxVLa4uLaSv2Qt3bsFJ9zLEiD3anfFWQY qknmnyfonma2hh1KHm9sxktJhQtG57gGqmtNwRiqTJpWk6KPR1LTjBZA7alayTfV/nNGG5Q18d1/ ysVVtK0rSJ/NlzcWBD2lnaRxNJHLI4NxO/qEFuRqUjjjI3/axVN5/KHli4vPrs+mW8t7Sn1l0DS0 8OZ+L8cVbt/KXlq2uXurbTYILmQcZJ41COy1rQstCRirGNN8vWunfmBf2kqyiy1LT45LD9/KEYW8 sgniYchX0/rC8V6cD7Yq8w81+Y2/LD8yrSbQbKW8hvbI/wCKNLQFw0ME3CG7jpUiYoSzdjXcVNcW QROveQ/LXnIf4x/KbVLWS9tmae60UUSB3kUB2WJlDW08ipx+JOLe25KqTfkX5S0/zj5ifzhrbc9U tLieM6QvNI7WOKJIl9VSAxllZwR24p49FD2PyzolhqOv6zqX1ZBYW1+0VlIrScpJYYUt5mJrTjG6 uoH83LwxQyA+TfKpuZbo6VbG5nNZp/THNzQCrN1PTviq/TvKnlzTBKNO0+Kz9clpvQHp8ye7caV+ nFUs0ay0nT7/AFy3kJgWC4W4DSTOP3NxEjB6ltgZllX6MVa1DyzZa27RSWVbJkp9bupJZHJYmoig claU/afb/JIxVO9C0PTNC0qDS9MhEFnbg+nGPFmLMfpZicVRk00UMLzSsEijUvI7bBVUVJPyGKpR 5Lt7qHy3atdRmG4ummvZYGILRteTvcmNiNqp6vE4qneKuxV2KuxV2KuxVDanp1tqVhNY3Ib0Z1oW Q8XUg1V0YfZZGAZT2IriqR2/mOfSFWz80EQSKwig1YKRa3NSQrMwBWCQinJHIFfs1GKp9bXtndRG W2njniGxkjdXUEb9VJGKpPc+aYLpms/L4XVr4/CzxNW1gJ/auLgBkWnXgKuey03xVbpPlSTRdNWH S7rhds5nu3kSsM8zmsjGMEemD0HpkUFOtMVTFdXgjuhaXim1nIXg7ikMrECoik6MQTTiaN7UxVH4 qk3mrR7vUtND6dKLfWLJxc6ZcHosygjg3+RKjNG3sa02xV4X+bcl5f6npXmd7KXS50tXtdas5iGp AzI4lt5UBWVoj/eR/aAQ7d8Uh5JCNV8ialpt15fun/SN1eK9rrUZRbeeCQKv1dzy4ULkswf4SKGu 2FL13yHFr9x5+83ap5NlDaR5hdbSTUwOFut9yMk08CGhk9KMyFTSlTXfYMoL6A0nS7PStMttNsk4 WtrGI4gTUkD9pj3Zjux7nfAhF4qgX1CSYmPToxcODRpmPGBd6H4wDyI8FrvsSMVSvWPLl491ba3a MlzrdjULFN8EE0TbvCAA3pn9qN9yp6kgnFUbp/mXS7uRbZ3NnqBFW067pFcD5IT8Y/ykLKexxVF3 +qaZp6CS/u4bRGrxaeRYwadaciK4qksslx5nK28MUkHl0kNdXEqmN7xRuIYo2owhb9t2HxLstQ3L FWS4q7FXYq7FXYq7FXYq7FXEBgQRUHYg9CMVSdvJvlB5zcNoentOTUzG1gLk+PLjXFU2iijijWKJ BHGgCoigBQBsAAOmKrsVU7m1trqB7e5iSaCQUeKRQyke4O2KpSNO1XSwTpkpvLQUpp1y/wASDuIZ zU08FkqPBlG2KrL3zdp9taM3pyvqPJY4dIICXUkrkhEVGNCDQnmDwABatAcVYB5+0XU7LRL3X9eY 6ix9OWDR4A5sILhnVCjxhg06yB/iZqbgkBS2KQHg+swQaNeTWM0sdzFqEtyl/ZtGPqj3MdwYqWyI KQLGg59aU6fs4UvXfyZXzJ5e/LbRPMKJ9f0ERTG40pY1FzbwPO7yXMLqAZT3ZG3IGxrtgQXtB1e0 eONrSt48qJJFHBRiUkFUckkKqkdCxFe2KGlsLi6UHU2UqdzZRE+j06OxAaXv1AU/y98VR6qqKFUB VUUVRsAB2GKt4qoXun2F/Abe+torqA9Yp0WRD/sWBGKoPT/K/lnTZRNp2kWVlKNhJb28UTAUp1RV PfFUzxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVil9ZLqvn2OKda22kad6sUqMUdLm9mKVDKQ QRFbEfJj2OKqmvWN5JpN9purWra7ot1C0cohAW74kH4WQcVkPgycSD+z3xV8p6VpA8yeb9Csb/RN TRjPz1EQgTyiBGKF2jdI3Rv3vGVzX+ZfDCm31tp1jqD20VuIl0fS4lCQWUJBuOA2o8ikpH8kqf8A KBwIQvkeCCw/TWh28Xo2ulX7R2q8i/7q4hiuh8R3NGnYb9BtU4qyfFXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYqxp3ns/zCXkALPV9NCI3c3NlKzEH3MVxUf6p8MVZJirF9O8kRWf 5gar5u9fk2o2kNqltxpwMdPUbl35emn44qyjFWO+Tj9ZuNe1VDWDUNRf6s/Z47WGO05j2LwNQ9xQ 98VZHirsVdirsVdirsVdirsVdirsVdirsVYb+Y1t+ZTWa3fkvVrGwNrDNJc299bNN6zKvJOLhhw6 EH4T1xV8yeSv+ckfz183+adO8t6bdaXFe6lIYoZJ7aka0UuSxXm1AqnoMVegfmLrX/OVnkvSpfME t/o+qaTaLW7SwtuTRp3lkjlRH4rXco5p1IAriqK/5x7/AOclNS8864fK3me2gh1d4nl0+9tQyJP6 Q5SRvGxejhQWBU0IB2HdV9B4q7FXYq7FUu17RY9Wslh9Vra6gkW4sbyMAvBcICEkUHY7MVZTsykq djiqWQ+bDp4+r+aIf0ZOmx1ABjp81P20n3ENf5JipHbkByxVGy+bvKkMIml1mxSJvsubmKhJ6AfF v9GKpbcalqnmMGy0aOex0uT4bvWpkaF2j6NHZxuFkLMNvVZQq9V5HoqyOzs7WytIbO1jWG2t0WKG JdlVEFFUfIDFVbFXYq7FXYq7FXmX5kf85Dflz5DubjTb+5lvddtwvPSbSMtIpdQ685H4xICrA/ar Ttir03FXYq7FXYq7FXYqo3tubizntw3EzRvGG605KRX8cVfn3/zjh/5O3yt/zES/9Q0uKvt382fM Gk6D+W/mK/1VkFt9QuIFif8A3bLPG0ccIHcyMwXFXy3/AM4f/l5rGoeek85ywPFo2ixzJBcsCFmu p42g9OMnZuEcjlqdNvHFX2LrOtaToumz6nq13FY6fbLznup3CIo9ye56AdScVeM6h/zlAkttd6n5 X8lazr/l2xLLPriI0FueH2mT4JTxHU8qEdwMVZd+VH55+TPzJjmh0oy2eq2qiS40u64iXhWnqRlS yyIDsSNxtUCoxVOvPv5neTPIlilz5hvhDLN/vJYxD1bqc1pSKJdzvtyNF8TiryjVP+cto9Guom1v yHremaXcH9xd3SiGSRSK1SOVURjTsJfpxVmvkj8//JfnjzbB5d8spc3Zaxkv7u8kjMKQcGRPSZX+ Jmq+5Hw9KE12VSHzB/zlP+SuhvL9Slm1S9iLIY7K0eMhwSpUyXAgWle6k/TirObf80/LcX5c2Pn3 XXbR9GvIYZzzV7hoxcMFiBECuxJ5Dou2KsDX/nLP8vL7zBpWhaBaX2q3eqXlvZRy+mtvCpuJViDE ytzJHKtOH0jFXq3mrzd5b8qaRJq/mG/i0+wj29SU7s3UJGgqzuf5VBOKvHdS/wCcp5rfTv09aeQd an8qFgE1yYCCNlJpzACSJxPY8+u2KvQ/y8/ODyT578v3OtaVd/V49PUtqltecYpbVQpbnL8RXhxU kODx2PgcVYVH/wA5IXmv395b/l15J1Dzba2D8LjUfVSytydyODOkteQFQGCsfDFU0/L78+R5k813 flPXvLV55W1yztXvZ47uRHhWKIryYyMsLU+OobhQjvir5S/PLzHoOtfntq2s6bfR3ejvc2IF9DV4 yILaCKVkp9sK0bbr1ptir6+8vf8AOQf5V6/b6pc2OrMtro1t9c1C4uIJoUji5BB8TqOTMzAKo3J6 Yqxay/5yK8zeYopr7yN+XGp6/o0LtH+kZriOyDsuxEa+nPy69A1fEYqyj8p/zmt/P97q+ly6HeaD rGien9fs7sqeJlLAAGiPyBQ1DIMVej4q7FXYqtlljiieWQ8Y41LO3gFFScVfnv8A844Cv52eVh/x fN/1DS4qnn/ORnlrzp5P88wRanq9/rvl+V/rugPqs817GFVhzgcTs6s0ZPFv5lIJ64q+vPyh88aF 5z8habq+kQxWkYQW91p0ACJa3EYHqQhR0UV5L/kkHFXz3/zm55lvG1ny95ZSUrZx2z6lNCDs0kkj Qxsw/wAkRPx+ZxVmv5d/n/5B0fyFoWiW+ha7PFYWEFtM9vp3qRPKsQ9VgyvRubksT3rirxP8ltK8 06P+cVp5isdC1K18vWtxdvcytaTLFFZPFJ8MjFSoohHU9aYqmf8Azj/eXX5h/wDOQn+IfMj/AFy6 hjudSjjkJZEdCEhRAeiw+qCg7UGKvqr84rO0uvyp83pcwpOkejX80ayKGCyRW0jxuK9GRlDKexxV 8x/84TQofzE1uc/3iaQ6L4Ue5gJ/4gMVRv8Azm9Gg81eWnCgO1jMGYDcgTCgJ9q4q+ivyT/8lH5Q /wC2Va/8mxir5Fks4G/5yyWFR6ca+bVlAQADkl4JPxYb4qmP/OXfmSfUvzbXRriZv0doltbxCFKH i1yqzyuB/Oyug+gYq9n1j8/vy/1TyffaHa+W9eksbqxlsoIRpp9Hg0RiVQVfZR026Yq+cfy88mfm b+ifNsWn6LqMNrf6LNFNI9rMsUginhnaNWKqDIyROqgGu5HtirIPyM/5yP8A+VbaHc+X7/RTqOnz XLXaTwyiKaN3REZSrKyuP3YpuCP1Kvdfy/8AzN/J38yvzC/S9u11beZ5tHl0X9DanHCILi0aX6xK F4+qsjAginqboW+DrRV8+/nVpWmWf/OSd3p1pZwW+ni+0pRZxRokIV7a1Lj01AWjFiTtvir3r/nK LyLaw/lJdy+V9HtrRoLu3uNU+o20cbvaRhweXpBSVSRkc1rQCvviryv8nf8AnKu28leUbHytrGhP eWun+oLe9tJVWQpJI0pDxOoUkFzvzFfxxV7j+Tvnr8rPOPmzzL5i8sXV1H5h1qKzbVdKv1jjdI7F GhjkhVAwYEPRyJH/AGfs7VVet4q7FXYql/mG5gtdB1G4ncRwxW0zO7GgACHFXwB/zjzdW9t+dHlW WdxHH9aZOTGg5SQyIoqfFmAxV9p/nV+Wdr+YXkW80fiq6rCDc6PcNQFLlAeKlj0ST7Dexr2GKvkL 8hfzUvfyv89TWOsiSLRL2T6nrlqwPKCWNiqzcf5omqGH8te9MVekf85o+WLm8fy751sF+taW1ubG 4uoqOiVczW7Fl/Zk9R6Hpt74qzP/AJxy/Pfybe+RtN8ta9qcGla3osK2iC9kWGOeCL4YWikkIWqx 0UpWu1QKYq9Sn88eRddupPKmn69aXmq6lbTokFnItyyp6Z5O5i5qgA/nIr9OKvh/8q/Nd1+VH5tx XGtwSRpYSzadrduoq6xtVHKjblwdVcfzU98VfTv5pfn5+W2ofl1runaBqJ13U9X0y8t4LKxilaRE kt3Ek83JV9KOFGLvyoaDFXkv/OEv/Ke69/2yv+xiLFUf/wA5wW841/yvcGNhA9pcxrLT4S6yKWWv iAwxVnv5Rf8AOQn5WaZ+VWi2Wp6t9U1PSLOO0uNOMUrzvJEOK+kqKQ/qUBFDtXemKvn/AETWpbz/ AJyYstW1G1fS5LvzMkslnOOMkHrXQ4Ryjsy8gG98VZL/AM5i+UNQ078yI/MfosdN1u2iAuQPgFzb J6TxEj9r00RhXrXbpir6A/KX/nIDyN5r8rWR1LWLXTPMFvCkWpWd9MkDNKigNLE0pUSI5HLYkjoc VRvn387PLml+U/MV95V1Kx1rXNDtUumt42a4t0DzJCPVkgIXrJ9j1Ax+/FVx/Lj8nPzK0S18wXGi affnUoVlfULMehKzuo5cpIGRy6nYhySOhxV8u+UfJNvaf85PWeheTbh7vS9H1aOcXQYS8bW2Ky3K vItFoPiir47dcVUv+cibsab/AM5H6pqEyMYre40y5oNiyRWdsTxr/qkYq+jPO/8Azkl5JstDsr7y xqtjqry3tpFqMUiyt6NjO9J5WQemwZV2APfqD0xVOvMH5Ifkp5rsX1KbRbJIrmMzLqunMLUFSOXr B4CsbePJgR44q+bv+cYfLlwfz6ml0SZrnQtE+vCe9FCktsyyW9vVhRayMyuKdaHwxV9t4q7FXYqx Lzz+VHkDz1NaTeatL/SMlirpat69zBwWQguP3EkVa8R1xVj0/wDzjP8AkdMgR/K8YC7gpc3kZ+kp MpOKs28p+UfL3lLRIdD8vWn1LS4Gd4rf1JZaNIxdzzmaRzVj3OKsU8x/84/flD5k1u61vWfL63Gp 3rB7qdbq8hDsFC8uEM0aA0Xei7nc74qnvl/8tPI2geWbjyvpekxpoF2zvc6fM0lzHI0oCvy+sNKx qFG1cVeean/ziJ+Tl7dPPDb31grkn6vbXR9MV8PWWVh9+Ks48hflF+X/AJDDv5c0pLe7lUpNfys0 1yykglfUkJKrsPhWg26Yql/5hfkR+W/n27W/1qwaLUwArahZv6EzqAABIQGWSgFAWUkDYHFUj1L8 ovI/5e/lH55i8u2ZW4utD1L6zfXDercuotJKIZKCiCleKgCu/XFXif8AzhL/AMp7r3/bK/7GIsVf VXnfyV5T84aHJpfmeyS809T6oLEo8TqD+8jkUhkYAncHpsdsVeeeRfyY/JbyxaWvn6y0+aFI7UX1 vNqUjTGCNk5iQRAuvPj9mlT/AC74qjtZ/If8i9Y8yyzanon1jXdZM+oy1u9QVpKuplkKpMqRjnKK CgHYDbFWXR/lp5GTybH5MOkxzeWYQwi0+4aScLydpKrJKzygh3JU8qr2pirxy7/5xt/ItvNUOkwW uscppWiY284azilSI3BgeSQNLX0x+yTSoqQTir1PSvya/LDSfLN35XtNDgTStUVUv42ZzNc+mea+ pMW9VirDktG+HtTFWB6t/wA42/lToNq1xBPrdlaXk8Fq1hY3zBZXuZVhRSrirD95vVvs4q9J8ifl j5I8i2b23lrTEtDLT6xdMTJcS0/nlcliP8n7I7DFUD+YP5M/l75+lhufMWm+rfW6+nFfQSPDMI61 4MyEB18AwNO1MVQVp/zj9+U1t5Um8rroavplxMtzcM8spneeNSiSGcMJKqrtxAPEVO25xVjo/wCc VPyyht5LdbzWItMYlpbAX7LbEdTyXj7V64q9I8l+TvKHlXRY7HytYwWenOBIGgPMzVG0jyks0hI6 MzHFU+xV2KuxV2KuxV2KuxV2KuxV2KuxV5v+b1x+ZN9pep+VvK/lOPVrHWdKmtptbl1GC1WCW5WS B4/qzj1JCsZDhuQG9O2KvEfyZ/Kj88fyv8zXWvp5Th1lbmyeyayXU7W2PxyxShw7GRaj0qUI79cV fT3mePVbrynqUNhBy1O5s5YoIOaikssZUDmSq/CW61xVgV7+Xd1b2nmDTbLRRPYTNpCWg9SAm4hs ypuXUSv8M1HkWshWopQ4qh/NugJpwv8AV4/KNvBAukrbaYI1suNpeyTSkMVU8vU5yRcTGp3r8QxV lXkDyrJpFzeXn1A6TBPb2tpFYGRJJGNqH5TztG0ic39TiKMfhX6Aqw3ypZeZNS8s30MN/PqVyDb3 F9Zp9Vt6vd3Pq6jZtKI45UuR6Tq3KULxddhtRVMbv8tr+fRNVu4dOS2uP9Il8u+XlkVFtJJ4Y4WZ pUf0+bekX4KeALGpPZVkN95VkvtH8q6N9ReHSrKeKTUIZJIw6RWtvIIkf0pCGLy8OXBm74qxLT/K vnWw05Db6C311dEu7K0j+tW6xW11d3Us0gAEjCgUoI+I3HwsV7Kusvy51UXv6RudFJkhltmt4xLb +t6UN7aoq8hLx/d2ljyA5U+Og3rirbeVPOUnpXr6ATr31DUorvVDc2zSS3tzGEhNTJVYFDN6a9th xWm6qat5GuLBtbtLTTbltMurSxsONnLbLLeQxxyC45PcSAqx9XizN8XEUU+CrMfJtnqll5Y06z1R EjvLWIQlE40EcZKwg8Pg5ekF5cfh5Vptiqc4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FX/9k= + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:9b43c0f3-16f7-47d6-9898-94b64c380baa + uuid:6a743afe-74fb-424e-b7c5-f3c5bb7c4fe2 + + xmp.iid:ebb2c7d3-470b-4310-8407-fdf65cb41c3a + xmp.did:ebb2c7d3-470b-4310-8407-fdf65cb41c3a + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:ebb2c7d3-470b-4310-8407-fdf65cb41c3a + 2016-11-23T19:19:19-05:00 + Adobe Illustrator CC 2017 (Macintosh) + / + + + saved + xmp.iid:9b43c0f3-16f7-47d6-9898-94b64c380baa + 2016-11-23T19:55:30-05:00 + Adobe Illustrator CC 2017 (Macintosh) + / + + + + Web + Document + 1 + False + False + + 1024.000000 + 1280.000000 + Pixels + + + + + OperatorMonoSSm-BookItalic + Operator Mono SSm + Book Italic + Open Type + Version 1.200 + False + OperatorMonoSSm-BookItalic.otf + + + Palatino-Bold + Palatino + Bold + TrueType + 11.0d2e1 + False + Palatino.ttc + + + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 15.00 + 21.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 1024.0 1280.0]/Type/Page>> endobj 10 0 obj <>stream +H|)N.Ye`~_D?`WvWW%22ӟ>_~\?U6?__^>\_^n~{}x_zkzjwm^{כ?YMww,ֿn\~GO>Sf-qVs&Cf9]˵z[/7'/_-31J#egmT{u9񭘊mv1͓u"e*KsW2FZ9|?%Q̷{͞ ;ΓsY꼷hrm*Ndq8_^|<;Gg>+{'gI +Gpb!3qI٩N[EPa$^P"F +(|R 0WR=&VXVpFB> + +K{L-( +ڬ#1\#F\*ט\ +`rJnGG~wIl28cOn.j4xf oWԦ̽b|SZmPx'0x鍖\ԨxQP1Krw;-j펶-JZ:%ae}NwPjܺ+t*X˙b!Vί*9t+ŝcB`s0iDY ~;03}-թz+c|6l~!_|O/jy^H{~نLn9 rJp)ﻆM5̮MݡJ߂)_ +1%f8fpM0XǢDA(S6(TLqbٶ)c24nm36ûAFP1 h^Y6.{R.anl9%7`Į%*hIE/ M=F"PGS\hQFDRတ+.ͽ-t"vIvPԜK{cֲsߪ`7AACK2] ǿAQ:(*;؇d{0 x _$W8h?*zX.bRᦾ(EORÀ<z\P@vNiZ +zUe +5)3Ie&.hd Yy ԕv60 6Z?B +985A5GqȄ185qId2#Xia[ Q9^! ɝXDzs͈ +? 7^U*>ݿ UCēB>A{㴔$4cnoHj0a*/S;/Pt`;+U͒ҳxC6]hfѡ`ȘdO +HӜ'C|$1b%ZA<$,Ef/n%$A81?B<&‘3-'ȚNэŦ:D=Ηr:bYGHrkF9M]k.+2FSC%ۂ׍*zQg&Kaz,XS+AZ]ۣ'M:1|#T,'Xҳ|[#DvS<Z,5JlD Dt_d>0\^?HWeH)"'l] ojjW/ޤ6oxLQz¶ I(P҈\9UFgՎ !|M- lA1lޖOS,Vʞ})P:[B i<ץ򮊃ɢb9&VUL9FxGҩץJ2zB4jO,PKJܕːrʧfҰD@J$*8/4E0XxLr,G ) c;i-@{ntTe1x۠ߡykiWyg +urҍD_wk-{yLJ(ZjWQ/8M)oUϝˣ $nAe[I=z'n?:>Im%'}bU3ڞ_*SmUTbOhsopX2F>{#}Pb ާܓ$>:a-^s +6A] Eom?u\9f(B,Us/&M "|* +.nN7]A\b})tHIHeֻܺ ]~} 4`7V qjC\MLߏ0_*5vyjydR6,)ػgDspj#ڹ.ͩ{e<3*ͭk,^싘:_Ӊ warfUp>p1"A@;HC6tLP?Xu٘} u$|zGEGٮhoML'{ol%'RtC?ȎO/=[BU-VkzoU\{I>`䘕 mzW}9$V 2;G,l(BeY[)ab)r&p(ќ̕u2^/ŰJL|',50\6jNF2'1S-r[N$ٹ]h17=yp@;e"{Ѵ 'ԄxPP) OSӧ˺-~֢J-+ٚig*{AOo◢Z=+jn8]H(&#w]J}G"f+{z`-ouGo*O wsvVy +oS1w-) dž,M.k=BG}તSEywT]Ӳ&Blf%Ʀ`\k_B@T^TKHq1 SvXa=KO]]2T`|1{8?\;"z-`Bg1u"u },pxծ䢭S38@-e?On8!"aUEo*vu&]_{ś-^5(DPRW|חfXS A-cgK/aIPԈGfBK"0[ӓW9#.Hذ#ӖA4(tN \S;{uҺE< x٘jiۚ7өt >1oLe#h0M$إ+fA22r"(QBg|~Q :F=K[Hsen׮j2M]Qv %:^^oJ[.T2ejfYYhHژt>~Ҭ^;ۖVM}`|=lLMuMDbvKKBB{ebafv&د'y8Ð̠z >B*GiVjbIioDU/k}S}Fbip4rJsB-p-%5 5 HA,qǽrʂ4}Q2L+tZϬ!0߹#FX 5l vw~ўHӌr*O%˼EL[R,K8*=a煴5zP8F&5!ĭMb818w{lES%B?_@gM'Ƈ'0f$Z,݃#iZ,m?G΀]ɮ~X mww颻4玸$P'yۇ߷t+TgW`h8?Ǫʽh?Gbܟ Ve;m]鯣xo[|ˑKGUhlBJYtnf?.>"ܪ"ޟEgZNemMhz ԽO.{Zq7Q2rndL,q x] 8vsPJ%uobF a>I=}vAM0Ȼ'9OśZɹרń U~6j.oYA;ިG "YiF0`ս A"M_F׻ޡ> +5 {ҙo +ֺ@~'+ٗ 9kvW!^ƹ #IСyXeFa =9Ndzq7I9Oq~fR1c<u-קĴ.^wӖX:T^΢=%_=+yĻ~8}3cLw{FRjAerc SH5|RjitX@<y_(-Bb>ʩ)in^6g@SZJEwHN@ + ~F(Ӄ1gr쌾6-oJ7v#E>Yc~i kѫCXel]h )7R7MzK[X.γ&q]t7)zR= 1[%f37S nO6"TeАLXƍ3."^V.4"i4%:1MWWJ“U֨ҹ4^.SeXkD7ݣߓhw.*RILQuFоm AbJ76.p"EH\p]755J' 9E9Mڰ݅ƚ^z kJ %_z)`itk-[Zε|DG={)Oo)JdvDtL9z47"V׵ 6SQ-o Z-JRW!Nq}(M>O +Ù.AW]i#Ga-ۣl^uT}]q! K`eӚ^fy*wƵ27{΃~K(0pb}6i 5oqu11}>h7M_ފ/CC1ԇ)l4u~j*хjzV%ij ^YW0!UMvC#ypn_F"t+ME5YIH5-c +72a.+rSJy"Up*1Bc߫R&U;Kފs(LJ=]R0=m˃!LMif\k;ëfr+nzg^0yфXWT@-3^'GLԟɥTVxuJ&&|j%siTyK5. ɿwBzCa0RF */PS6K +5kt?M]P:9΃3 ^&-[ejqfu/+^$ +7?HxA\ihmxB$U;ځ +BмT4{8^iT ]Ydd.InZ+?xBļG@69Lf 1Jl2P% A/vl +Y~vWd J2$~&*0$õ{O3VN- 5Î L":5AiXUz %Jg?[hjs wK#txs B?jߺ*4Ϋ*sZ,UEGhdykĄZ)G)I1n ZvR/y RLQTu!X oM-&X EJ Ϛw\3|[髴Z&BR9We߮#rɒF~Nh7V_̣(6M*Dx OvVR<|8zscޗaEYGϨkG2lL4[@3lHGsHn14Ͳ8.fK#]'7\h9IèVBqSMYz|[v"90R! r'71fӢ{=>`Uq,?5Ju$ӀOk<,Eg|U#g^:J_4'˝n2|@i5 },%}X`q4[w&̧׋:_ Bڄ=B2c~ִX5h)~O1^t1pT5)RG0%H0v8tu'?Qi*TL*n: +"iyg+TXnkO|KM_9 6(L0aKme{(6O+Vu3翼o +^OPNَ*>9#lٿFtԳQJRWn@S\STyC?ʽV|xv8s +GaNWdX"|aPtLf'>1TOrcFf:?^_~n'%}^孵򦇩y*/pv[n-a{+5Fi9 Jdt;.3rS8 NBaM"Yq  MO5KG|8,v{t7/$Fɭ̫KkJ=z(Q",:9rjliB)bV<3o%ZV\3Y/֡|b6>fp]>=]m{̫\@oLEﯚ6.opKmJvK_׉ +/Sx|[vjfͣ-8mjg%:hp[)I/BDN` szG$Yax[fh/-<宠u'U)7[ Xᅩw:1R#&HIo X'{RwH7Bν=YP'n,PbKؙN[7Ma-,Ȭ DDʠJR d((kik +VnT(vuX5 CeAUrLX +;s>͔п+h}C6t'=2u?m9‰U-qm'(mU TNe2 nS?^زX~z4 iG37`zQl`Q3n8n"%Ooފ`Q;x2'6itzF"j|e +oL6GV5OL~)cKC#m^u`;E)ea"S{êzEꊳjjN̴g O^1bPv#c탰Ia`~IY~/0 +,u?'975W{r6ԃ3MI!fNS@"Պ +H8ЎV&)]>\ +7(2mOad@w\|^s˗rs] +}Sgs̅ ި_GD,5ķVy}Y=1cOdeF>]m./@-tV>_dKvJ CYEo}z2}?\PNlg⤺RE*zr5+ "7)+]`!lݙ! R\[߂B7 /G7ac躝^ +IzXڰc{7hضy4gK"m.GݩS|DOSfʷ\TE@<8yt%*䤘UR-ʧ)k6NFnR h-x3Ac .qIZp)@d{/4ݫNq"ǒ0=Y/3&ONeujL߿?:u_'PQùz-_U+8]% 8Za d{Q ?ed&ퟖFHJ;sNF +W|}5 r>cDYzCWha^E9j8Ox8!9ՍMЊ/T|)JT\Ib}E@r +A,X#Ӑh݄' RCzDaI_3 gd/TgkƻzbV 9[iP<:S˫B3}zپm.JN-ty~8t|i?u@)Ԓ& t+eROv8@(~vD*AD`:X6cDɥN0(* FO%[vڼa#-+iл#AW.ˊBCc&%ZBhѨooXLq}ޕZ^px"h_~mrx<#&.Я14<Ï Q`iS_;jsYA}nAߪ;~&>pvڟ|ZֻmY igt4E›Pw; e +&V7w5*\/D HG|K5OJ-i*82`JwVgsM6էdQLE q1sLvtamjc:<+bV2g"a#|@S8vhmNL>"O4Dc bS 7 k6eՖ- Ko; tjyLvD=bU:/)eA^@ܡ.1  Gwxӓl:("®(61|Ģ) 6,wDii $uj6٣fAOP;>e9aNFi`! xC3~8fe(aeێt̲caoh 5).XvmPI@طm, +eFUw}[~fܥ%P/oE(sYAr3"e:8$/q5.uݼ#Djq}CI%ӈ%ePհ]h+N,{v ?F[Ry)qL5,-oSuڮ5 ^ |޳A`} +db_u~a^\VD+_p2Pdc= ic_qR~TÛnurM@T&'A%zwӑzU"|۸v*!\M&amz|kM/W#rZVܖi,J>/ޝ6ydZQ=`hz«&.mvjTM=Swqu> ڕ՘9EφxPu #c]Џ9ѥ#t)t!KtGSVRsNGg: zw-c; ECkEl:&CYBG4Uͧt0p[ƼҔIs9L>m+"#o_˿Cݗ6O[gU5r5 f SIbL)q/VY$Xڐб*<>q8GJ $ɐ`YX3<*rL2xre| +zz-ͫȿd  MUUw%DA(kg/L+fj wj~et{*W2!Y:X 9Ml]JjkwL(ع[O|6a}+HuWU# /&*[.Bg>u}B FD]{> qڛd1؝pV?W&7#b`o +!nFe0;6=hAMQgHIIL"(u&8єGeV4#ӱWa\SɑWbڻ +1!ĸ@(W: 6whrL[pl^|`|LC6k6cZ#\Gz Y;]o\^eԦ2A~HįLzt,{C/t}]Ue0HIg.{zP+,h.lf Kv=L.碓Ktǥ j72`(Y #Zz}k}-R![sJMWY}P ;ukv5.F\yNaahc:}1*ڪj. lO!}Ūj^O#`?[hNXu{w$ +һf>ʸ"_‰i?AFl}If)"6ӑqВ"L06|eՍ+^k˿W'I[7w$JgOqLۧ'!Q$pFoa`a}6Cw% Զ/,3jiMGFk>9,]qS }sR]T$g5tEsẐPid׍z0y¾.4K7Tf=5] CC::x'G2}iLa|NѯBvjM^iȎCmTZ ׵"q(A*?UC4 [ls(%fE~pX?FL9"lOK M .^':Q,>M,XS/'rJT%sIH$K*6MsxnG^Oޮc$yԕϢUݺZ_slV< Yݸ֢Lc<~ޯg.czze/ <{R*r֙s|c<4u|GDӊr*OP%Zu֫Q|7&kwI/-R*,R~adдշZ''/Sۼp1((EšwyiR*v }XVCs4>fňӸF0^~WYy7D| |m0iOe*0Q̂d5"%L7,w_ 0o]R:s-c?X 2ިԪ#YB#%2Ա˻eoJE˥?*SBf_&6\]"z@15_Ò4]ńlti+F30/dyٚemǼeR:8zJtV Uz4"uZpZ %2F,8w*#[YaUxfWڷX7E˛ݘ-U#c,tڽ.+f +lU1~ WC>\そK~pfU +S V.l0iϤZ& +:,GC|3r?pҾԹZ{EGU[6lftS{&*J rY  X8E73,zȉOnʙ|X^t!hyIշkHrmN- BAlY8}}ǺvdǍ1tߧCt\*y#Ɍ,%f䎂#j쫬GJeKO/* d9fE;w@ӽ[Jke _~Ֆag\x=ۖP +ZR`1rug&\yKodŗ2X nVe,`t2l֎mG\x +ݼA+ +gyR3xaEN&2&VѼ20ľٶHfхU`ӱj|~Xhy~׏3uKkD%Qke n/V;8"W#,ޭYdI?͢0Mrn3{w@C'od-E" =0vd EQc$BsJ~2[}u4_q%O:)^}]F,@Ed%{ =4-naYnGMQbMlTd[K|uʨP9`ׁCh3;Q-jSzwxd&Zr[<7F It_Y곭üB͍jR[QCrOX) +c&GV΋OYm$?ZNóGu6;QlbJkݿ[uW5G8@./6M^a%oIYo̲>^d-&B^3$փ5zp5ZD=5jok )qD*n, r쉩39nR7*ϘI3+UI\ch|N"s;I`xWe4U{pųODZffR%$-yl3;:N֖-G):xjkPu5bX:V<(9zĥo4n3;I:4¯r; h+1%[Rɧ%kٚ1r_5LA&auچnrҁM{.#2ZI@!Zz vqQJ4|ٛ=0\&" ǜՖ_XKa4"CŞĝ3\y9.]T3e\NfCt;TREl>024Ŗ=]ZZԭSpy2 (U@I L5w]!H? +TJװ#O$%LEHW%V#U>r,c[Zl{Qsӫ/`zp\]p7uswR?W! <1qrz"dzlչMȪlꭕ4[Ho~8gh˳ӊAa/H$apnl%%kiIUQR*kꕤbX¼h}ڨ%a1҃ +1<ZLBtXzq`tQt) ^u-ANYH1%f?lMz9 Cs +_`IyfT]l:)(xXi Ҫy\Ok5$h9Pz[ +sx^ZZwɓ;hwm=jP]om +DU7a7V`n|]vVXҰhJekOfO#WfJGtpm0;n %'|᎑+H"P'0ȞIvp~Qwl7T؊i@Liu׵Vo)|mmTg@A 8@Xvh!]M;C; z"HHzlثn8ΘֹOl`)5Cg;_[cj9h+ aJY.vG( zI v9[=slN6E&? N3i7S [._N_DNG;' +@/.+y۰itQvBGƶy;mc}{bqA7қA|nG0g>`cPB5Z=ԕ|;VgdOݬΒu`b*]mP\$C0&qn_9Eh!qnX3Wr1/]o׼>. .iB;ĶFjgr)y3"? Ɇ=!.:cyߟ?nhlki[ev7Ƣ#uWg,a4:28b"ԀwP]ڧVj `͛3{qx;٪|+қKf"*}䓓bnLKfnS+5a KI>G0R.-:tm&Bk# D{8 +O&0D6(v?Gtq#?ih;D96x(Z|4.2 rݱYc, ̜n$8uƫebPe(R)_`;kp y'7v~x7"oIiPM_lOن CFUW`E6Ʊzbr˝^jhTK1X҂n89yRI"Z N6e#PJO+cX5IkOݠ/9]?{I +g_HN£-/ ]H>"sQpiy|^MS,"phzJlҘ4ICݹFZ8qml6==uz؁@]uq~s"alǤI$/'2e޳nU5.Re6#_³yl"ݚ=j-r#92=/?#B=jA%1>l[}IDG)z۹Z6nwt1AE:A3+uLoռVsdɜcxu2S A3lΤ2ny=g;9VEW=D\Wޛ-:ԫ ?3HH\:-R}%LĚRI8hG(=7,R3Wޟ \uTBH5'%2%$"39(76JaҠj7L;@Qm|-ۘ:FB'5tbbn,m*kS'Qm*( hjh_˧R1G}i W<+OY,xb؊RFδ$Y EuXdۮC5gr|Nxʝ}?dx}8Z8k1Pf&f!aފٰ~S787b +lfSC|WaF˺NTW̫hQ6atMRnG0H2Q/iȏ怪TQUÊG!`6Q 'wgOIW t-'v-n4aGE8ՙV(VGӽCڙIs0g_/ +o]s]{wrN9錖M& +Y^vMP( +VO䗰vYhзm끖) +6u~8~껄~pXM-l7y+Y% >5Wf 6->!G]A16ɨvoAK`{c-n@I- x~CSYљN2p*b9&p9xW0"Tmj#GA[鏍6[?F)gS6;!Jv?ܨ̹ DBE|oxOvaWJ5f1f)U Swm3B`%poʎA Cv{zpJ0jSb~ 3N 'tne.ؼXA&Nڭ݋Ҧ-˩f?5hF 7=8Ӽ^vm喅5a KGᚊVy#W^N<圆-/ +I_A=,#fo5e>@]Ȃucn3>Jk3oGQVAɴX_Fy>y#YY4XszX&'#Sby/?U WEZi6*z92*jN5es g0q&&,,B?na-Sf6G$}WSǒ?(ʞ,V8hqxh;˖azjvbab()v$訳¾GL\NQp9Nlni+ !ʃs5^W4+Wyr[MB[1"uAn?eɱнN,?kX&,Vf 7Ӆb0;iu7j~tI]0^9icrKTNS0˧ +TM˞Jxc\]uKy{vƯqQ K&YcYV78q¦+jOA_mhŃsZԝE0SB|h||WoL:iMnZp? /0>6< G9ƍ MwWwOX(,C86<> ^4ۂ!-"/cy\/NN@^=5$v:|2)!+4ʬ+"%{~ 5%˾B涂B7;R+3*@Wp2{fJq9C_&'`ӗX7D +="Ƞh2ɲKNR⢛P[]Yq/+`>p @")s{#vPQeiU8e\$:^na+W]lYU8v$aZ{M~rM聞~q! AW촯4{/ ' +;ϝS뻶ŹI^& NjZRokV Ҋb^"sdZK>W{{P"wz?pN@ M )|a4&T8e(gcժ>FScq][(?ג{O?+%^9gy4gxB"lg@e3jva)VSru%^>eCSȱY ҋo͕+]8IGѺ#TUazf,YwńX{q邑m\&לBa%=Z mbǝ)g>T>:Uw[Xhۖو!e0~^=V +? + :r.še]=y'{E&fJy:ċJv wM !.:*:|nֶ;L}q˲^|iȔ$VU̴𷾓luwΏ^#sND\_̚E#>(c Wc~l|a׳jn]eܴA_"rkm3U\F@Zzۯ))1 blC 瓱Լ%'s8y61nj|)Vܼ19w$1Mza^ ڢVO7ujNSck&@D7{qgÒ5Ў42"NiF%eZʯ:{_37<3Ƥ F?=SXw}IQǝlW6-)mQc0B1 +Ug!v#=͊t;Q ܳKtHQlL`*F*ZHMPm~HAd^\M9zNBClxF=H7EÞ-Q/L_edzm]}+ѽ;hv^eˬ)@;a}pF6(Ҳ#O.xAw+(1GY/8+ \aQQlPH5G=RCׄs?Lc0,+ Xs!J4NR. v ƒ)OaucSt+6iÓSq;SD OfwbԨrBdKaL-nJ !'LiȞd,X)͠9eOx^Uk8zsM!'qc]XXwQ4@*;ê-ݻ?6 l62b tΆaUԏu a5Axc* N;ESw;T7[7Gw",ΚP5U +rsjo/R}|9Le(>I +.cA iИz_ 1"5maĕwc1ض*AQZ]L\vvbt#L{'f 4T[Jz&Y}M5KAɊEk<%$;@uչ|!REFb5TvE1ަZl2U7}S&X}Ц=&cwpLxfbv"Աej?9z3Q)Wpgu`qzw9Ҩ_bG.e^uKL?e/5oB,jiF֮bH )ƀx(i# 9RD bCl{]p:&-K Xq"mcs\mo':̴t7% ǒ#2k4>6bmw 1?vW@zgwEr忻+4D=%Z{R9;;-VJ^ + $Oe9*wH+IQ&0RAfY]yI_!i-bm]W" [w8dpDQN> +0`ŰsO9quܻjVʕ2 SKSÑ^TyB1tvZ+Fj`HP"&}D2⻣o1_&7Wq?HKD"ddhCVe1m޻g8>84;+CvѠd)c =ڛtWF#gtCy$I@t? L[|h"c2infe97{|*D\H=~?(Ǹ焝[ EPi8ٰL +sBOqk)QmV۹`H[)UK6W!UwH/\yT_^#4Ac5~cb6F"~uǕ]p힞40Dq  +v:Ͱ&NO}rLӣφ#U,UQgFlX"$[~ knbחnb}%RV\bN| HP$1Sx&Pmly_qo >\?k&7ueVzJXtx3W'izo8 3~(z=~#>%w?tLY%9uyp*W9eV./8sB ;ݮ9u{2_2eCi!Aج]݋FhG<*fW.o紆7bQ : 2K lp]߻p<Ԫ =DC˹65d_Ok!0 >iӼmkvlOfҘK؁fx  ia_͐3ݝ)ɤRո~r`K +7E3mI 9b ©w +I ec  =a׭CfRG]xZr 8EǪd>^!τStdLyʇU&=}|S3QwG+ $QԋjEeZ=EXX'pl)j:5:J5Ht2^h3H*'Z8 YyKA*)j/$c %C,e7*0v=Vj|l&&E͚C섄4up>12aWK_XwayjH;p}};B!]A*a\ͪ\g$[6>g`c1GJ ZΆj?S6{N̘Cٶs{鞟#1ʱ#,>Eŗ =R'Kwu(JV2}9KOuRu8oj^9 q>4 cbvTw|f!Es@:Uǁ+%{Vriwxp,7Mek֢ 9\Tj+tja4gNmy4z-TAHoH /Biؘ V*HX +iܻgcyKjݰ4ڞ%uejU2s/'};:荼!'CpRO78G5adYP ϧd-IrٓSIk)ZA~u6u:ՒD6{umugG+5! -?X5>Eap3^f@pQpa0jd +kLUN/;jTQ>٦󄌛R3$#[Ⱥ T 8}&m +gBjL`jlm:}hRL—ӵiwm\NCݶ7E ?קxz)z~js(D5]CyLZJP ^ Iў¯r_\G_ʜrFy9[L5fC".u,*8Z!j`XzRUggVNCkE]q lo x@0ri.wTbRH]]&EiO۵w/x/Puc{L7Ե^suf<Nl[ ˁ1 />(.PE| +q2ҩU6FxUК&ʷ'eSӂ@ޯ C"Ogj"L~[B17 S܋VݚlXPى=޺"y bȮ mgۖ}d"V#j*S1ιI}6,Zv/~]TVΑ[nge]J&:5?;%~n'F[ʁ)G*‹wwҘɀ\6/a~þ40v BEVBYq0ni&7xtp"ݣl]pE,vZ<E!UO)HmA'Kל4aůn.'-?DøY7ؕW7˼!'{m?R8vDt5fgaT ҭrD70Ք8%C6S2mBm}//;‰8Bg3F'茪W$+d I! Qء&g"G,8&+&:!"X@e%W8.^c{))i}a׌J!'UԒ^^>2Sپgl֒ǒ {yYC8H"Nd)~|pU½*yinc0m^"ٝ\qLL64\Z9,G*{Imͥ=y\hE_Yq$e*Mq,Wa: 9vb,U[>NR: 9Z,)_Sjs)փ5^^6ui-SjǼܴЈ]ĠB}_#ч,HBfr&ʓ%CYF-G%lji]2⫚vE~" if^>˻UƦ_Gyb8GGkʖO.9FH"i_?`s8kg:wv\Tǧ4f/kqaLw6sMfwq=-j96 gGMi*}`.F@z:tZs);XiۨB*0޸cȤ3k r "wR [i{z)wu3˫`mS=Dq$=7HoL9}ԗEIH¤]r.鮤So*.rQU1K`aT Z/+g3eE[&'Y[Nͺ̡r.>v3ttdO;4:pSoWhhLVҜ5ַ;<պ&8Dg1dmy +~H'YPs@2t2\iهʊ Ơ|.O52F+LD`yGt g^3E $PrVebOx]s_XMl3oY I!0qѐ{@ x振P EPBdm`qVdʥ蔼6M9%ThȲ^ ad]G<z7#mjoKN/#t2jPo0/XyA~a1LlۼoBI%/T^ym4}欭eۤcWtʵ6e$ɺ?vu׃[_6;=Ege1~FOd#3?O5L3ch];>vε:H7iQʙ%Wݖ;5 jQwKET[ZL2VFwunۃ*Yoެ*U?{եJMy66 +veW5qj%RUdZ+~d@GR^ zt0#-c2 Xg\;I͆8#oi[YզGLL@-cJXQi3[&/޳UTk.4:ni WJywOWJhNLTiD7W*VaʒdN@D-!< [:BIAE b-.oc+QvEs6SRk1dM%ԖyE-zu!P`LJ㉥To]`=HO,m'0V9lJ*vP,3QILԽѴA7D@!tBLF (~᱇F}F1dH/`% oi|Au%Ku;yY@UmlCnȩn5ѓFOvd$Wvjm>2OTZEY{󭟕C4~wY؈mzye^#XnAt5>YmeMP_gͱLUjoyI,Mwd60hSK 6igMZ&%MC- g6+sǏ*zI6^⏙S^tOBO{W&.fi4VD9zxxF~x@QqXw$َŤ'To$p蜖 ,K$'1Ͳu- ' DŌ"} /I2>=7e}Pd6IFu<9pe0aM^.:VѤeqT0zWEEj_F8({9d?10{C{`J&@f1Ջy‡1=]cry*8dBZHke6KĶԪm5Ef9E"ɹ`Zym'KꈻNۧ/ayXY~λ+Jӈ,=ڂ|MjVe-;fcbЯ f\y2=?g?\g-Lކ5vbB꽓m&=Vm":9XrldL2d3 +w!I ,kBM/Wsb하Sz%%(%vm<VѪ&Iz]ev%c|-s}B%j>1;e0g;a7T1KԦތc63LXze7 +/Fѯӓ6KK{n1)J$;u?1 ){|]ZSMlHοVoN(S- <%f]S=>eg^wK;U%1Ct1Ǣk>j=κ5W +ὨDEfB~7>@",D"G3v{y[٭-)&\4-4}iu 7V/O_۞0 5,(l gJ?R"/OuF$jֈyygoנ1?ׯAӠ԰z^o wfw}zE+y:f؎r <zN10NOsgDi7?v9#z$ڑ9mHߪ:Jkj U,L;SHgh*݃?_oy-^Ҡ~}3& +hpW_j\.Џ1+>/SU0Av 61#r~>w<#A$Hvчᰳskm'tu7emT"2?cB%UMFV]Cy@6B? 7+ѩ@qtAj\F78ߖnE[Q7fyp +4H-\^4"&cpSz$9HЏiDr/Æ`ƺ-iXlz4ɵB"dxt`ӗ+4Yrl I o]PJ'+}hk{bH@J_䫙>˾Yϣ +fQ[w &t ݩLؓSz @hsA':1;tDk`@^%7OZ7θFR?~ZXXߧfZБvGӲg +nT<J[W^+a| ۆ$Ԝoۿ@S^&Y8/P8z_w|Ғ_FNiӢnўƹsEKvXMn9fgAIBO.lVḴRt"3EL--$QysoC4y2Uʆ4!t'o̬]K\y]gF=ȭuD(u{yCMV+b@]1 ++< \x2a/=rLi>Ynv2I2dt"$qMrGM6͊ +W7Y=f ݺof[YĆOKD=g ;2Άr9e]lziԝ/4u㕐dAelzhM<*=1?NqLG)+ m]pbjv|W+ɧ \[ sXǃ)J?=^,j6mcG& {#.i nFpTa!5mY}& }g~f T~hA7΂K5+DQG%8%;`f=]y/emxd;+(5n:6Nn#:;$c-3<~;<@wueqNSgfᶝtOA-{xOAKU6>,I#nSvr!OėHSM+K #^.+X j>ΒeF.8Pb_0c +nѢg;u&UY|SRC^y{jsra/ˏ ?) +}^3,yܙ9h!^96INKʌPwqBL +]|ںNSKԻ'uAV/#VKe=f3^FvkȜLC*A76qaST9 *˄߶s}2!g4@e|`fk]J6&wJS j^MFcGj8aݑTش+4p Og:4}TMȈ ̒r+9iWgwl[_C!JE6ZjX禚ЗZsK8$HU8t'{l VFtgtk]() 02;Pqt8jLk[9nZ_Jy l6Em@TiU=?澠3ӄڰg^}1Wh1gfV"K>Ty}j:w i7%C?^9m:|DN&X 4gb Pf]5Sc[ҔaAMx.`!|̕뗓@ y$|:6UXjmڬ5pwQ[ak݋m5g~*A=iҷ%c_XݦbxgCad߫B:&O?kW[Y0wKF2ڴ'U/`$"9/8o1bI[[ث Y!Z#1?k0Jz#lzJ\ݩ&23s'FZ?Dxq 5aR1<2ćÔ7Z2B4o%>ŊfB(ePh{ees` f8i"VN4j%!G^D`/m!Ņ_Dw,K67|Fq<8˹2eZ_{ Zg#%ńZEmdȀ%]j3-"{pyć{iS6@;ܴ)љȼ4wqzede\h*2٧1V I {\JY*G⥺vt.Q8j[[3SߔSw 7l|u?=be>:;Ҕ@ WGv1BMlkHt?0HQE]xʘ=.&q޸ +: +Up +ͦ.'S\=Iހs"yFJ}Vl1 /^ӃV{ ]-ZIs]ڢud1gN\/BThpthtW͔ ;a!FA[F!E)6mw)caTϩU)u1aEK7;I;}m)L#U >jL#UGf!eEVur!_fcTNM!2St ؎afK>tvlvy;R!I۷c;Lۅp&62pOv\E8VͫgBj@ٹ5cSCi5YqyHX04(N!\Tҭ2s~NuZ1.{ZpbsgK&za iJb(kiij*1m*\z4'yV4 ej1y; ?E 3S=0up7ѱx vԽmk*zP Flg){(Mk\M=u7l ݞ/_txz0@^ilIZuDžڷ?% >.Pf ;l{=fsjRԆR$p<,SGx}^)ɅNކ~a5F9<v:Iu$Apƒ[̲MV0]k'Kl?rbۚ_߷%Վs۾Zi3G+) +/RQ-b4 +@ uv GR[7cR"“PنzG.Aqrvj4z%Qe)hogU +iM@]zF~? FF[] 帄&;!OP[ɺQ|l*Xs.~eFww&ySuNv_ycJoDGSۺL"N=Ԍ+hcKz҅\Ϛnv ٯ]`Yis4Z[ 6ҁ+6&L[0tZO o_򚘚,uZz ,. + lT^):o1jڳz8j]32(U`ҏҷ `8F$qk-}ԁ1MHQRg~0L7Q|kwvf^ر&FhS U^vܐzXwpt~$~|//HTSFH'6sc68>o\y]u(EN|ٍ)RrbF!?2\hoqT\6.:#Zf=Ty_-pM= a@vfjIJJW!vl8M/z֋'TB ^|xWHv;[/ KQ@^[a}bOwlF uAufUTeH Bc^ =YKw-2]`j~ %VvGiƽ9U{HE׎}IO^OAػrS$ +-p\2nz3rHB5NZР$!!`nnx[h]>oyzB0&[HN|xrʹKD{8/rf/Ph}F94'0)қJC깕L`|]s~Es+ !EZ~췰u8A +\Ƅ +s*d/<ύ^uc%P6H옟7qQT,>VM iK@UD [$u/54e}{>ŢL4ZL']Te | Kh +֍M[hdW{$6Cܘ* ٞt=ϲs{6]cmEЁDY}Pf7h~TȫmEͤuo0iIok'y6ʸ˲Zze<-jG޻Ų}"(K}l_iЛMgcoy>^_igCo,\0dG|&@ή[POdV7ud5P@>QvBDT׸5}GP(7ug{C)YU\p-֞8=/jH{ 𧶺{Qo`͡M1Aqdk=DN<:ذ֢\7AVi,OGu/Ij 7h$Ey 6$368%y W3MVӼk6xSU|mPIi S[槿x8j/:k[%w*A(Ƞs{򝅢QvzLBI9mߺ6͊4x~H4BoOp8cJYk<޸U1Grc8۰WBub<w’ )Fy {7sD)btH\8e'Jn8Z=Xv`?`ifM gf{C녱 2L_+xζPmm%ؒz#+9O ӭm9xtڐUW. 9Ge|UwnۇD14n?J,%aydz0Wkh55:GcKym:„P-ۋeKL_91TдU]*guU.X%ٕrS4t]Qx^so?7lR  ⼙&m+پZ n^|hs:*tG}3!i^YH,@d~RiNd1|~c{Hǩ_{3HjX4l_ƾL&BZLpKs<AXWQx‘!\u ꥆNpx䠎R[iY/V'ٗk~e~cpO:a2ԡMs'v[?}۝xS`|IO}ytVw 럁1/6~:j7;cBJn)Q +žz>/nM\1 q]99>F\,}pj{w:kЃPEvT5nar.4 UsFE y͡IN~w;Tz2IcGB(LM/tuI[i4r"<|hUk%),E#Ñ͆A˻pc9uJOXv|8֨eoܲCIipXm)TY=^$d2C_Bs9~}qhv;浑 &}Q!z[v9+F`;#u">9IpNc5p43{{Sk {RK@=_bUzPag/W dx䬁^ZVYtQŕՍ4+L/ O(16IBztc#E[`u+w@z'ރ_{_3AjK֠36@ SC;ԟǣ'8 aVfz%-4ry'AWlQgaЇOi-,MCMS3E>Q.7ЫIۿXn +|1Mbghts˂^á, Gb}/Ye8rզpR:~V{O}G/Bf 0:^ʦS xxK1|Q꽜Y!{Kdx}GN[]PiI{v+9;\5 +tx}l$H'zB9h^<:{BW=< ElZ/fTidz`B҄Ѕw TK.UT}֧]܊Tω640V|`\B4++ +~8UUO܅Fuso0^t*1VbBTV8 =3&|!ux%0"ܗ:(QHSnA"ÄCڣ}1Z1C8] mUctO@WicڎU&(x ֜NE[D +M8S~Ig y& +wV새v"MR쨪xv]5 zM9T]~Q' Ԃ L Ǫ?1uea{m!3CsI4 y>89-.obh|+q5t ĥz{_7Uqu-WE4ysV|Rne,dMƖ!T%DO%-u9ҭАl)XQ0F]|\w%]v/:ՄP(mkʬd=UImrq~_>g@4煻\ ĥZ̸EmtC*JӣtrSorʯJ4Hl07KlF7ڧ;R4t{VX{Pt7<$/E.R]"XSM9]g|?! ՜Fe PS,J&Rs~]z4Y 3b!rtQ\_TӓA³Kڛf6}# ̦IC˯o61/~C9^V:)λRb=Im8X}׆e%3i ~!akLCJP icoH+u_!ÎozxoU%6F2̷elFgHlOqvUc?yNl @ob]!R= Jݚ2O;iimWmXƁyBGE;0I?)AbAf쟙99# Ǡi-vuX7&U[W\oBHTE{|p+Ď].IvFkAG`=:EV; iԛK$;iOW]q8wq|k)?1Owa Yq]d)q(cֹ ihٵs$ŻI:h/tYכ5Rt⮕Ϫ9-yuFLU9snt!$:x'&/0{>sF"RkNPjJix2ͲP{ْNˇo^3SC0pH2#~aNuHڣS_Pyå`׫1\ 6Ud#8ZԱI~ȉ!U_C@ÙDJ{M< xP/`ՙ}h[yLՌf޼7z1Ǐ] bm]cˣh7ű ieH- -_ -r_ Ez`RFWx4Ek:9lglx6崊cJZW $SGe|4sRtbs]&30A@4]n$|N: +TD]2a8aK&$H~5oLwlu[{5Ss||xGЁ`4,F+N:gFڗHG^\M<8DBfPc(gl &r8z`zݠzq[O[19WI.^-kN/~;*f'PWU:hfOd`t vqG\D:l=VwX!`٨SC2͊HxLŖIBQ0#Qriw;Jm)N9.V}LZ+KigmzFP?w5 }zOrƨأ>RNjQYb4st2R3V |*9lLcSVT>T $;PiVƞ ·]GgAgt덞4 :9푎CYǾB\܆%`3?Uɐ4DJ̽Φ;fŝ~ۙУ`Q8"ӜpSrT.#.~-t^7OVO%F;Th^26yz_Lf\pd`MD%?oQKZ&Ķ_IFlF3Lɵ_fI_G;6ͼ *~'jNWB{̂Mw{j_#y7xq'ݒ}1 2E,{np\ܸ\݂DBĔllHݲaKp7SC7ܿV'd?HBA e|4s +rFj9b䃹i;p50x'=}Iw)!ZMep8ΣT_zBΧ9'SG%bF_';9^rͭ5A!@!w{mZӐðHZy;1_s-x++6vF#͕>Sc-9Qv(Dٿ4fO==,QHFCI&+Q_ STZMʽg$䵽Y.H7EO/{-ڢfplIr[9D:/@ +3Σ7}Ηڤ ˂Te0[%ˆ EQ/kęYj +*/nD ^??1f7P1ɶx̍Z3RG@ΪU[̗ĸP-l:\a7HiÀd م cNaba)9X+ +brӡv}^mަ;~.+vѝ"K&&YZL#NaX72>N +Ox|\yfX3PIm>RZ=O468\ell +5Cr"]+]ws;.۴VM̲ b;jְU^fM Ń.o .9O$x@FnD⯞2g%9IBWGOgbh 6U$2V.o^ X1rya(ضcZ;Wma{ L`F m]cF$i4S:L yrmk*Q\=\/U_ >4ˮ +zج\J+"F^'8\K bT0[ws +4BN_L\|WJӧjIloh:wG(+Dv0 -7^ݰYR]'uqX5<5΁Ă7娷L)݇X1Ĕ8[сvsIJNn<.Ł8Fj:186G2{<:d\%h\jERpiGOU`D`(k-Dܚ!`:+,7 Jp(Y B9kjXh0 u[2XD@$/Y^Dk1K_jHyM}Ph\*ځT嵁;>e3*CGh. eHϑOU_{ɒ# aŰSGrIvQg7)îrlDش9h%=pC()ϓl:\D7-y'mz}FQbSz1xjn-9Uԛ΂ʔ NE@J30wZWTU-Q[$-{+$2( L= ZݖuK0Z}CtG:`fy7 *l$5}:푃r09g}eILe g۴2g 0Fp멻IKXnVțiW "/1I7#IՈhC Eٮ +Tt4H6@88fiƜeSO W[ǞwF{k*bp@}jKrvij5Wrܧv'ȂGS{vWAC_b4xU,ţ&~XH״0׬C*U' { ,~;03ٓ8&GVKQNA5-SxՕEtMm? +Ɇ&q$k<^0B{vFG&b֎f툜Dm^LzMUK\ҟ_sTfUv?"V] + +Nr"c%, ӗ]ԵtGɲ7lM@DBCnlD(ä^Ù }BӺpQz: 07LVKxF /vG%ߜܘ$Y3GxχPP=;OΒPLi!SäN&_ 6]=ܻ ld,,dS]Q$2|7C]i߮krKxР#Ju09j&/Ѱq\p:22HAR=t~9![1G%{z{=>mf|~;] +Gy٤Ǒ0t?:DQwˎdY'$dֳ{ԗM&C´j g]1'huY!0Y(4dң> ?}*֬tAEU~Xb-Ge@ Dž~yv$`P[d;X w?z.zp YrrV%酀KSŝcWwiԊ%=ק׳5/&?0~2 C]%hy˅K _ +=@iJ+]3 :ur* &-9"5 nԟr[v|[N)t47=mR常q-(IyGjz8^aԪ9D6gu'D13'S lU!E=u'̆ M5nTS-5J3]sCmpzgG9lw "Kٷnpʹ"M }.40LyLGʹ~˗џ +,0op'B>%7-unNپDZv IV}_,1fd"0z1j v˅5-VWFwn7dAG}-W줨W$ 5vuW[ahUa盘v}WU[V ]0AbFшaY 0[7u o0cj#4y_$MpKl2QzFuOup\0#F9S㩍YDSlI֐1#XZ>0G8apa`tL v׳ `@$nmZJ,TԋOFcaHvf/^ 1#po?xQKd/쎦{2>N/ƭIXD㏟s& ueHP!@0cU-sܮfFTS?"kE~oe`n0C 84Z C*ԅ*,([$2zqSfEԨe?ͼ^wSQU6ԕolA_04N#Lji=YavZex5?on][. ?YhV{h xܰteYqj !Y/ZK6Ҕu7"=բHy‘+Hɛkɿ;dJy~*PD5)X84=Fokut +n[{MIrHDSaԸ;~{ hȁї[.?m \ys#HD3o$E.i%2N?V. ʼnѯ>Eo+i_{:;}&#*sm>?W,h :Y7gFH)yƛ;.FCBqCtIÅh>R]{ɖtmoeZ$vmz< \oWkFe횎Fg\9H*6ݟ'0|T#}5_C."jK/(rpEި>m*̸*6zSm,-婁r/#I\fTy3idb?1Ҏ6ǜ]H tܟ5 B $s'nƓē \D`=l>PF{RYA7 *FU㯳[_GK qzlj[.N t+ +]GҤjb}%c}W|/V6{Gm؜;‰V]@:іTH y">k/?YO Quѓ.؅ +4jnMAsgfaUZrgHH6q=XR׾ޙϢsTȟ {HOwG~EX#pL2Yheo$&N d vsؐ6J&k:x)}!gH+Y~Q^.I:{v ;'A*uij=DTE +vz(¤55/58U;ٺZ,s 8W=ɗryy,0~%Vu)l+{A: 4_ahme6{ L@BaMUnJR,.7lߕZQgbu8dĦM뀓YL8Sߦ[Ci+:mQ胞OV- %k9`9(j@ U"M,|nc<βR[Jb]2VL;FG;?^~vUolߌa#j67{Ai%ٞ(⢈g)䇅@KBk +&p͖JᭉMt. PaS*H~~֍nԢ歏P nUto+g:.Ą:+I$Teıc.fctZXjD\4N˻l}%~Ny2N PC>GG\W }q3_ ٪fc&@ܢ:2)]aT8=WG"3y΄q밑C9Y?K#m%9WjʾTY=;z(fc\}=lx/=Lƶܣ@K c\ҽXaW231n#)lX[H?Z:}kRe+Q30G7~-1lJ{!j|/ V!L)s?&QA_}5__7lv3 Y̚nQ̤s,J˙]YPi(5Iaf""O Aj<k%J2~\I^ٱ–os#1Ty(eT͞XH"(5Gݶ=$DDgc'L8C՞'#98m,Pmc3A{ + g\wT1^^* DwrkDkT? Q'<8:FZx8Zu,TvYl+[{P :a賴6.>q#8$f4z͜?5~dOO$kwz}u!{ӛjU 8(9/Fs +P:w=ip467}szFvC_cY721]dԚ&0as~݂Rͽ5t? ?1)w_ȫʟ KF]ؙ\[;YvQsθ, ('o9P2U5u̫#}5FiI?4mv72OOkX{bVv5>fpy.{$Bx=w|54L6<)WЭH7]w`z遆wuَog5Fk[aR+fgџh-ة[lL83Yz"U2Nd[b@zT߯PL;edbG vY墮cs. +NۗmuU˦vӁRw2Kڕfb{((Isr1]~78F_hیRUG3`i-/._Ff?۱7w7bfomӑ-1Y;YUo=;,p +_z+Ѳ沑6,=_-$l\N4r݇˗؁H?r  ?9KE *qA[gG<WjܯcI׀O02dL[v>0\ Q2|ur% ^Ә@%!כ +:ހjtst2/ަ,\?WQ/Z.+ 7qfa3 !scm r~b~Thvn PB(e6;{:[Quڃ"8eati' t&tsXx^,~߂5[ߢ%𢷝7տ 0"U푷,% {@n`w1ՙzO|NQ)گ:mK5oq$9miڠj ~ӫ cR, ޫ(S5 ɟxsBFJʌTeA@I]Ήڪk֩WP:ֽȰb>/#Á6(`n~l#^ͧ^ŶaY(򂔭+91(6-Nn#QiZc;VxU__^x$ p3!JECnMH28(Q2rHm &8  &E83i9ޮ 31TEqbdggH~mH:9gϼ!j +cg֊mf$%hd3g9a3 0^essxbv!^N&p= +$ʻ1ԭnJфT0=o/5VG5E:nb痕^mĒ᣹` ~~I$'X+1)}E;e?LF֓՘.f@Euc!&wa\h!qc}fͪ82lU(bM;=UlĄ-_)%~ Zt0c۪XB%[1%_ZAu&k:y+7$f+8Bpvɥ4w+EId,:"[2$b;o۱DRc@nCcƗhXv?"6 ('9(kt<~'xSt%7F^cn 4uYz?R#vD#tk@}8J}6;T [|Ul82}PnUHո=#$YOp/z8=5.'Ҫ),ȏ c/ eSb'*ΨƽAF.<_zԵ(v PD 2sFЩ"F[-&ykי vѢ'RxL3Uɞ̌H_nkeXn'k-yÒLY,'FrG6hU [XZԣSXo~ + 6F#ٝuzZ:D]TTz\r=W ktLP m蹗ϪHX'O֌[,]{bЭ4nje>My)a|oK7.G["A>~uAGB{XMQK|+SӉd"oql'cp]}<6,%0T ZтFr,Z\f6+豱aS9L@xpQ3ܺpɰ +ȎfS .o +o7CZ66; u?kݰnS]w\)}7 /!D[>g<wY_kNo;Krl:=7R@Gs8˱l2({'TN,莘 5j֫*GjdlrSl@fP!NAYwT9C>sѐw<>m!_J>vmB"]!RgCYܗyfG)wP|Ǝ5 <'WӅ"X=󢇚S8ӜO!Muc4^Z2zJHߵ^C>cJ?5MF֜oC+w-8g:z?0ϟSl I +៿~鯟GRlRCB5Ï*<fB +B$I4 'f稦`dL ߊsP7mǡ7 V%/_.v焽DJ_:\P~@g|xTJlRʪ$ ~)B{ 3BsT3(bR}H[3XҴ%?^>C{yIk ?BD9Q+.zA3Rh_@{$YؼU*q`ٍ*~ƈrt"ʳ@KZ0ֺ +&hW@&yW/eSzF&WܱZ*F܂6ɣ9Ca,v"d2.濳v>Q٘ʌZJhzޞNQq!€x䮵t65]h록HO8gX")QXwoCV?)NhGtLLÎQ,PXӣ!Mۻ nF՟mԥW$EMc> !pBI15%DxIpiЯ0ͲKV + A-m{.6zx^~q\[<4 iZE-h_xzxc%k&;UѺ V[NXEQ}-' + \ | Sy $xZW+x bhn= jS 1"(< {n(ցgc] +GiB5Y9_a=mΚcq`)J}2,bV863-Oy=n%I+.hT[wTdBI>QiFX%SXoqdS DX +iQ ޠzPԦ +.PΊ)@{޸in\r4_F @qKv9mAnG3qe,PT1oo'cǛ{n>w29H +5׀Ue(Z2g)咗hLac2h\~ Z"΂EgG^09k?Xg(qPSMpsf#}KvѰxX1NXs9g͵Nw=Cm8<6Q͋st+$KjY[&?}[}̝ڪJ,,'l֪ 0pkA(ZFqxsvZgeͼ&?&6/8«N I ;xeLQ|CP? \å7)T.BA}R+_s,"m.  +}ڌP۰SoJ;V8zt ='ypw*Bڀ_jeYX:Sjv~C5Y#؈$|̗*5I${\eE(לE*O3v ZΡLkX)u.Io4 0s[t!m* o;2g.i3}=bI3`1\gV7>w[ +6[:@Y`!H_ftpexPgQp?U9-"6N0x +"ktp/PAtTi/LsxҺeQ'myo9SWmjQ1ȁ㌲tWFenn 7a!:. d WK~⎍c Z?гqBJ 7#dƼܣ +ƎMT}u"u(.lzB +cU/T݆Ǯw4pO؋*G!+(јi)T>yö+ G [:A2koz(7D3pNw$KVHfsJvnjG/Ц~$-M8Kun+"uREW(~aO[q_ӷOL)LLD?#ۧ&Hg G q20,K J}܃S 3 HJd/셠SfO^|WF7p,9US9isH[4㢝$X.Y:Čcy?ia'᷾Vϛ6v x>yG69 +Y#Iz%'d):ns)gr<#d-#؈YԚ6Ⱦ Z~s1'MSl{x߱/Lꍻտ}XmN +m-nKL㘼5T3sn:ߢZM +ܘ_KBդ';G>OFqbЈb &F-Ho,4cFF1HvleI/JmMn8HiitlӃ&Qs0~/u>6c[?eV} xB>`J̲BLGL>x&bR_E(d iFA9j$dXtͅfW^Q /uZ&]x=:Hc{i1jL߲ej0lL}Ѧto4ʴۣ KbL%uOi +( =9H휏Rd(F4ǹz*`D4w$%a&K3 xտd`<>>q+L!' +kH&J5EK9i!1-J/3&\ۆT0GwtvG ^ne0o=:lPcQSH"R*(DtRoY^f^5^(U_ B:E6 :SL}Z-,e2иG'Y?V6+$ <+u04Фh^h#JMH[g6S4OKܚrv0 +icRw+ \TceN2CߤX(>oJIRwubxWCNOa'fױD@kS**`ܴxb\vلlXN"&r/ne [ޱc[vhs׿*l.i`sr_Jw6xk|Rzp pDM:">hH]T9/:C9pT7wFFby&t2E7(`*-i ь?!a4gɢq?[FF=.@Y]w;i1Ubx3Qp 0ۨpN/Sq-.oJ fɥw?>~=*!n|6 +ͪk: #jcY^FPxNḓE搶[Z[@ǎgP gI3(YZQBOW\leuSQj^Ӟv2Zo=Hwޯ>`WSUm2DTd4_]~Y͓yQ(jJ$i"4vmIXbS:RA-N\]qzC2@tBQ?FTjսu[cG3EDp}PT9IS5ץP"0_6c(Hߑ0p?U޽ %y^.j]%e”:g(O5y}Rrd]IuL@ˏɿ9f .+JC3hau.y/kΙLt Y(E]\‰S\]J+9l]:xn6Vuɯ +s-U {9% }UL֝wsBpe_sE GuixةϲZat Q<-w2 ~e/*i?hKZuq.5l)p?-L[U:k7Fe;%[Q$$3P7{SA띂;z>OﶯrTS} +Ey:h:iZmu+g-0*3Sq~:V~aK ࣿ>;^^^?Xf40u:B-$퇗TPޑ0QLzO /9LUBp8lauFU~fsꎪM5A$yJ>~4d))^i]X QV-3wMcdsXU-4[>/t +k{8kKAEV\;NKkVበVL;㨴,8Tg=耛jG + +^%]I0p\鎙YgKWUi6S(BmDu2N0v`JHtO$G~@FAb[b;9Qu==AAM>WH`{Խ2C^[e,v_/a>u6h&)՛+Ȇ7;?=n ͑ +L;#/znT(>G +Bg7˙@mT:Xijš𶐄9ԣ٠2|"C AfF"i{4 ϣOw rlMx&p# +!^9nYEIbCy?nnۃT&vt^>k :^ +i_:LVpvL/6(a~)ߡAxc7uǰ%hu6Nu0S9GZJ9Ѽ|>Dޢ +`"Jv/ Z=bQV6}Ј*U2.f!` ơZyPRn籯;EBCK5#3~]"2>yJp; &^X5 [Ş}Ζr Ф}&w(a=3ЙDatzMoadWfa$PtAm| +QE ҈~C'л|L*Q]o{ð6XIV4.bG-e:zcB[crlRKmB,h[j*E%*Y!t/LGs5e+M^pVݱc|),CB1Jh:1PNv,nk4-4<>UQm1<4'ZK4uUKؕofD-p\PH1` բ9mתoC7V__BS̪(y~GC +Զaj=a-97#f/ vVj`npph;БbBД%mQrs:$+t`" 9+˗å.0hJ %ELꛌlH 5+]yl$JEX5k3UϗN0VEXm…pT%{[܄sC{W@N;Ko0,lz, tߤY&-Yyvh-]nXq5T0zIXUюIm ayzXS3Oj<FO&V0lix=K}Y@q4ਲD3Y٬_`9.#(7C +>X캐u+>[h;-MVGvOŐ~Z .v0340 +ݧB􄚡dX2_ U--)7t2[erqʦht[j WUrYĞf{ kޒZe7Gh/7{ZΛХk+ژ|fG\MQ]Hpn&^-4A\l,eU jQtX_UER-hPhu/ +A-d e:uJוR"-',] @O( YD +:6A2cmV3`HmFz+H\:UŶ+v@8_v~.PۗpWEQ|LK TGQ^2}1Ώ&)[$v2դ &͡cljJ3${bbI/.y8כwYU٦h|+ 2K\ X#Us׏x^!̶mc ^5b{U-'>_۱-jy%ӂ4BݵRzFyp4֕ζJ鿿8%Q琞U&qKA%ͶKE/- @u< .Nh:,2Dz-[&R*:U :Ɗ kQA +^M*gbjiayN&H\H`͆6 Oӕ(;خy[OkQ˚ JD 8L;DŽC2bGǹI5Oڞ qpBjx9B MZ- (]w:L@N -F3|kh씍ޏh@CqxӚQդB@V;MyU/止YI{0N 1޻Q3תL0%Quq[($i. +Zl׷&dp񀚳7]jO B(`Gwi_iέ`kfB2"GHu殪 T}kvDje~.7!?ur}\W;uylfU~,%Ut5\ >p̣c$"P;Yݾg:KI{p>5ЯCh+t9+@'pGa %?=&$3gd^ qM32_;sj^oM_g{4`K1s!v,LEt{=l##7XηyP5)8qV+kpUEMcH+,3UW5s17 J懇HGhnIȬG4 +a~3E2w4Xu 5#v VH=e7Kv:7Qs j?=,W!>kU둼 +_5/爚k@[BA;i]H%ʳy|Ƌ 6ddQΫ8lom\뚸a;$Q0oQ"w誅9Fo=ȈK+OԲa:yh@؅ 8ee U Z9*d5KE BSYMe"Xe6϶pYT'2٩r6o^}o)Mdذ$۸ZiCVMI{k41k}j*zY1UQ6p1+ӤbJP3ao2˴Kv;DZ6 ~@\;n \zoR]YL0u{E-oD澡}X>>8fd~}}oجJ bm>!ZRT%W6Lg1 SsZHg!O7!Dl+ǷJOߵ:(~//i~_L1Ez^8nZH{{4J|h\N.tys@Vt,R r\\dF7YrحJl">u&͌H?UR̴`-/o\,2R=իL0-}SQ+Ծ^"Hᴇ?P0eI}'n%^5ŤB;։c)mwsX*V?T [p0ԓ ? Tb'|PqIgfrzż!el;BÖa,sɺO,@ªe &qg=oq 7饥3,67f6^&,QYmZs/q˯ܞEuo9omEBݖU (3An1! Yo=pl%Z_ݬV/V`:)dѩݘ A2!Oi嵠õۥk +'\`6IM(8Њ)CmW }PtɺLP1*"oRe^ eAIl?H=߬jت9 +#0_@ZI2ZZIlZ\ +ڱF 㹚'Z,=x,̴ ;ʧ:saɿp)`t_,&Jz;pTɝY&9J#ǵ>$kP`B 18͏<\?<Q(3r5189<%ɩƑw4^qu;!A<y0'@Ȁj] xab8T0 >0Z +'5x4mzTEHqb8Q`'&]v4-͉ldjB"y-7 + xT,__m#lebn۶n>x_kc؏#NiK!PTHY [chČpfU6~!ӡKMUT#5jx/uӛ]T\y81a a-|=I /F>G) + A!ra+ee<+ƲV>whu` AMrjs'dHXŖ4`G3R)lL43U2<-7\u#q8).\lWg$LS9.Ղ68iHR<0gE]yeG]O QN{4$!m׵2L$ܞ/:O[ډCe +H-P"Tfd1LE6lW?RRbUKG u!\:,\G!y $MlXŜ~c=:3숼KSI$%ezkx:H)obQv 5P!Ҏă\?D!2r#s?">\f.ВJe)S/3E +Uh_BkXˡaP5CUV(b;檞*w>, ly&1;X;h>(Hr, w܉ 6yhM,A㛩I?/l?MUկYO掠Nm$;}YeyQy9_VNӷ\U^17F_=]湼jzv "vC_/]sIZ"Z4Qe^rz>Ta31||klγp"<66VO8JMNXQNOYCm”5 ]k%!@:iE`BZn/3߂eE"#ɱb]2ALrI' 2}2nQ(ҳUAGH$0fǔi}5v{zG!fa~M8ۭܜ0CL^{Ԝ꼥0j:trz"^ӊk|l$zg:jMаspЍl-}˨#yyW/ԃfKl7Ν24,YeiIiX9&0= v8 y쏐r1ma6&X;mH*h >RRA!geɃ0Z Hv-tNRS?jm fo.U5,]2jҥp'X8NQMH3 s:D:Dk< ˗iHk3;8'1`p3s]iTƦS$q]? ۥ^AL܅xfx$yj +ܥemmF Y0{20-*ӑ/Jy)䵿.5-"aٚyscPw W8p,g%1!2?r-zE0NʥjI` +Ft;eChasGUeKa{@Yb-˻qSOKi6hgX+oi:*.;_vg+\@,?EY4x{4`1&6&N 2-;z{I}=c7iRn`'s󴚔l`껫Q-'9֦L3\XmO?&f`ݡOSpzj:iGrÙ-ă.qpCې蓞ʲzzB]pR4l,fg3 7trvQ|rHV dhv\C7+2ZrN{}ym2Qbz)j^6th;*N_IVmKm<5fW]ŀI;j ]"e}>E8JhP0j۲6psos*S{-::oT8&H>lU HDrZ̆&)!HѬa]˵K-'5˙V'k46#(Cchza*ri]l0-GHMdKqHyty²LЏ&zCք7CH fj"E o zibvne7];_VTדfOZ'XCR :lhI#Zvu09uqSGUݲ4'tx."vq0gkl^6#Ȫpj&K]}CB=+ڽ's j‹ +p 5 {Mtd*LY:?^jB3-c#']^wbSk$:a͉c̵;x=a9_gҶ3x1WI;hEbB0aXcLܥ/ֱ#74t^czMA"R4LA3\gS/6J|9~6 .G,{ivG1F:y)miTՎ{~g}c6뻻+eAqgϕysT"!P%WN-?a|baAg6:ge bۡTta|X \&+$jvLս=o@Lj}]Z$Qk,j UNC/hw& };mvO68?T7@b+![·m,&K1RB3m{R>VNR6 {*6@v<}őjo\/k;g5 "F,UZmSߗ'j$P3G5)'I:@54Vb؆X"8Vң3Khr1⹜OXiP)\jH<HIQh[ضex(JFJ61y#dC" 1rX̋>nkHzXZzx>EU9ey$~H>i2BˤS wiQbS[lW,B[{2~զdbՂ;8P]9 C='΍7cpR< 2-ql#szN$a;9Ϟs9)?>OAPJ7^n5wp'꯵&6 ~0~'X^tu3W&՗)PJGU>gl!R^nq8݊7  |i=y%_vH* Ows\slY)5TazG} LMd,NXQ%[|svk]R;Պg ͮ&;:82`ڎJĴԑ1鵕m @TV?߇-#,nw4fH9| +>JIiibuْ! U@7^sVtqv6I,ӓwJ)NXVtxS5YePUǾx+_ 7Wlsm +J u? B˺-o4Bƞ껈]J|e5$xB E뿣@Rkհ9i*"MӱP+ZXQ<+k+".7'YriƗ :*;v"͓zq8nl+w.y4TRÎJE͊ch5m+YXZFjAQ {zГlu|nappb"ve6.zyXFj~~/WrNٳvlU6lgd$괏&ќTos;2,z|FYxE {~϶9:cOfThnTQWW۵X:*˩S}|OiuEisKB ._ Avcp:IlH3ZxGm$biqj~/[L`-PѺ>1@(N!^Ⱦ =![_46^l'm]qNS8c⬆)Zoӱ#4^qП 9śgK_讒LKj1Oh͵NIGIqF=zA@ԉ\.iS֧TwNz=$|Y֟r[kou5>1m,OT SM޴b ,Nsʼ^ߦ$5$l +HmuG4,.5ȏxRwwKIL!6Ц7c2FvZؿn#+G?iWxǃ r/l>5_kUZѷ-3gj_.y-&>V0GIp!FnUiSm^ݨWpRu`>$æKNp+rHӏ]fktT*`^g:Kru^p-aκM ΋]_BWa>o=+Ȟ{aS]A4`ˉc]^5tۧ|Dt٨E-TtTqbY,>:hH_<{=N'KTHO]#Wi''|*ߤZ.M*W9u8oje:.C >1 +&v9ϋ [XaL}^փ4EbOWJhHg@%I ?|5ҟ?x߄Z<1nܶG̖-9a(Epn<gE]vbk(yO?`si߶[ mEc[)JRVq6upblg>RX} +0Jܬԧ1Gsa2dY'ƑS0jyp2[x;uEO?WMFM"lࠗhVi"֡J:Ǝ">ؚ#9+_ ߐt=Q]#8xQ:IЖ%[M%*mV4 涣Z+ 7.-!QNS>,\N'mQD$/!i?g?UL5T3Gqw" ,hV@p"|U]1]WGg}32aRu $kuղ5^^ y͞w^;U8l*\+v/UOS3<˂:=8m2ZF}j:_jՏ.5n Xm"I/$9#@A2?͙"=U-f^$2+ws[Z(Ai/jp+\Z[!i[ +朝'gwRzAעغ;*&|`.-kv4%čA'ʎB9 ,U~ +/6ZݗFX <Д]vL' L!N6 AU&~JcDtH=rf}y1O -&`( f+Q`"I)ȷd5Kx裏tPCPoJ(AfQH~3th2㜙"Cd܊i\Z8~))STf-¹" .=3LPN4,=Kҫ3%'Sz8. j ?]O6 +4JihKX"Mu4:ꕵ0=#<3;%WAμ-,J\4o꠳mB-I`wc/q"a'v9Mb؊,Mg*.{X5?=$`ꥼ^o<jXo_ ƻOTNܦʝMK  Pr3W]^sdSG׈<;g<->vDiN3s'@&0RJh a_T4"E0+IkY3w:ơ'_ H D.d?,G}tl彸3g듩^0KhpL(SyPZwysco@tਟ?=5I̷S<[N1A7y٩qf/o!GVxQ{Khو c!|ȴ1w[Q!^4L_"ײP{\zn":9hO: [[M&Or8LaFeR^wcsl!6aD>Ȑw@y+!z1N+RB'Ο^t Zʙ. +-A>ܬzVlw0c3!WD_WO$: O.RK3vx./*sÓXig]<ĺ=_ָpl{rqF#rٖ蛟wB$̻ 1wdf\^omΟOS=Jxf;K^0royoA +_W vҶ':Jr؀}zi%7_׿H$Om۵@8ҮZ3EXy95 +3(< ER7ׅ4|= (iΓkg0]].CS#ƮbvB鲸#ņ10b 3(qar9lm2U}ts>AI/.r$ڥ". x+Tɶ$^Sԩ&noE;+nRVt8蔥YG7z'GHE(1n,JLCANnh9 @ךM _F{65ȉG>;w{68ac>znZU#.R{uuS~rG_\ͥt$C9Yn~L6Gñ4&zZKe(s%V$~#e>RC^ԸkͥV{?۽7w+j0}aS/\kX)P l_b#s¼V̿Tr-7Xkl5 W1!U]\Sz<pε]@}ǝ@hƳ<ǎS#vˋˑa<=c|xzs=p+YɭggQM=d49,X%$h\gG F>qhV3ƿ]c J>y]N--Jx83 JTǴ\}>Hor&k||mh\a )t yf'_d$'f#i3Fwuۣ= +j V - Og=,T59.sh%DӋ1Ш1?.m: d?[8Za8_~u`EC(N(Vph eW$)P1`Iievh:lHc>q,,O&.^ҙz +Qd]ag6TDWNVٖM/NZJsHB&__>'M'ꮿMvB{|)}#^H Z"fM#G T5ӧ.xdR!/}q _]e?8DZ4i⒐gЊ^[Ï1jj"O1ށ'f8gLYf^;qٙn~/j~.aPd݋=D֝,0Y۠P= #)o&5g*%{GiuLȭXJzuƭ)Yu-a}nrK[g9ӜUԞt|i%օ<ag +p&cgHbIi]:X!>FoВ0$gQ(-zQ +B}"'TS*Ґֽv]t%VۢaXɨXAķtS FʈԛL`_+MG? 뚒by=??v tF%1T(jLB2x*ts@t|'gVYE{׻a(aL jHb۴m +'TD~/OYiSY}}/"*`e<9֛,Â=jZ +\B!vp%Ѭpe1nms>3u~> x"۴<+#h:D(()47Yb%t[cxTCE8jx!6TD5WN,soH>*Um5n'ɻ^f:WZԹf4rPz_Np#PMِJSozCVv㿱\?ul/\/"ځ@iszܶjSBr!ؓ,Ơ'EO:Жw͔`5IE39V.)fbkfߖg+\%]-- jo_GMm\rnO=fx_|= fVqҿtgg7;zp+oNk3V8azRJFi+d۔ ņcS4$LZZ(9Y^=H}ncS+˛( ~|_rL hx{NWAjH,h-};,?j+-ןݟ{/t^'*e)Lkzf+`(-03QbwsS!=A `B<#iE'KoО$]s ,oR)~ZZkFS0y}&\%3ӈ漞$9RD\6bTy^Fm467ujG@ NuGx9DaL/3! ZKѰ)fdd$pRnͫA4o?wO[nØ֟~)ASJl +UV6@Mmƀ<BCih(`an슉!D)ckv#ՎV<riNo0|eY^K?WWhEO l~"54,z/=aW{<[tڭƿ:47MP?+pJQOi[Z@`-,̽SB0II*8KuЏ:瘴}~B4Z;TͩR @UiA6Wg , k$!m[A{H=2-y=٘SVEnKHq&_Xsk8"Y+mWfJؖVCL}Bt)1C-=Haw2jf)*M^IQ'R @^C \B^z2mz2j<&FXeه=m6n*?*>4.S`p&F*,7h{KҖj2cI 0pI6s/Qޮq!-m+~VMN5r +^2n|4U ZmIL)RIl,ڦVFPbPLm3] L4AL. ;oRH*$V;Sj?GOUP)_=AP%C(:QR)9DB#X$Y +:wnkz1 b 81{ g[ɧBL[߾l[ +¶O~f,ˣ|5nPNphGXKݐFbÁV0/}>J^yVN8jpJҧg`fa{s!9KԠ4`B"#:N&K+B_paȿ47O6D҅Ԃ +F4 +/0-'L_R^&qX *tovTDp';%y')u$/qPؐ(FNRX'<x +$diyV__\َڥx;+j=n٥ 2jDló<+S\{B-27.ǧ&Nf2~6R5##G +Z-ItY-0T_WLuxL H#kՉӱqq0.ੁZxH|K{A] D_} +U^5cu7lU.qh:ȹecu?!Pp=2i_G$yL*o|426q2Rݬ7=ݯ6F-j>r~F.dfطsy]n > +XnaxH¦YT?A ͚9'ItfN[5}¡<0c(>k}~Su io55䂷/޶"k$aĸ8n΄%:( 5me#] vfaQ|Hq)}=Iدz5VL",ymw߭4$JkxJ&fN-y TFXӛQi~dk4jk& RJ.f +kuY}aiӅ2pΚa?vA͜C1?̔&+%U'KO U!]^4* ;_eԺv"aT0@-$@6e lgw{?(~x6 cA5k8&2$թ ٜ/B^NTFOTI'DVx\ +[Fv\c}Ydj3FMAo\ &JW!<;lm[zjEK/@©,/"\P Kk:k&4SX- z1)A!mɛjj_+ +M:Ǩ6ƉT $KE2xWN8b+nվAAU;K*Oʨ8CjBw,3765V%aʞR.`&L!F7/ԡ.`\z} .҄2n'&_cxw-S?%,xwmKQ8ax4Dz}* їU]b`7uc5>yk$#< 1eE$PݯL)\d*_ҁZU@,g9* b#w9K5k[Q/ύ1|֙.JA3\d:^^}$F&@#fEFp6ZS8Bn4CԌlZܯ(`#UsMB] s}dP[18"r%V啊{EMCjJ=9$tg j^VH:sbgOיBHU6az+\jcgԹ'hhZbYq sf X2mr){G)GFTRM +ՔMK.\Önq-μ~$9n:5lʉ7%^D(-FJ~/VەA%HlҁwKr#s-S!$(ܟۨ b÷U==ߴP%aU;zL*?63hQxd?T;~&! ?Ұm|bM!s6H0Z v abWc|.x $cZ+Rc\)K8'3i/z~ZMp4xUϯ%̹Xhd^YyOV!U!2m% +rVnM._1ZHAb+/sG)KUItQJf!H7#"纀Ax +95^qCw|%(qg 8xRl濞)㵶DP槙>[(4W[ǀ/]+qv C6e +QWd&o}C3EH,P%C]8mX,KcpĠ^OQHJ}P=?Յ4Tu7H`g{XG9 Pc$ifkӛ+V ;.(f%'{|~*?ZW.qQۿ" sj;y:Wm5˥u È:TSݢR !^pVJPETĐDsA7X`9]G aD@sTӿlQ[NV&h=3g(#ODؽ c ˷\=Lg؛!*hƿ}Cdڸ*ĺQg3NӨvR x.T#,BdSfqsfhUAmFi*O_Y0+*Qw֛[/L)Jڬ{uDסE?l0\ꀎ}c{NZ*vJD1l`(nqhYS_];kn* $A]>ccUwKinTZDcrulc>9F7<6ƃ}2el4:6w5'jఄ 4BM|D} T+=uXR%\8DebpðM|il.LI9Dz$ wҺ Rd6Q>mU)zUGV<fJ;~bt:6QΪ^gHZSSV*؞{EK0 Ҭ˹!cl9PcsS4E忐EE:Iu ׈/nDϺgX!Bp{s[!Mqh~R!i`=fT2ogm~=!Dx‰)=í:m_\ `1KmrC j]fE~]:I+8$5͎AK[44eA|ThC:Yl]GaeE S7VҜE)v"ў4ԉ?V",,:d78u=fy^!a2}ffąk;F;8*]H7z`tz`˕FXt||&v/LtٔQ{FH/U K͞FU(rk z֫R:aC˒[ǞeWӰ֬8}#hRU@6a^skXAl(Ge"=KQ{-ɭ +wdx= O\;Z m!^Np2][ 0:9L ޳ƕ!cMUPe1M~mJefqvp7(*;.M˵/o5B? VͤɹNgYPu` n>`{kkkgtY{(Pg_;f*TβA1Woԍ'!Nۦ-S(/}\Kk@D>¿ rvz&0'(wH1nJ 듻F~s): ;icÆ5ge6l'tM/^ñIar @Ʃ#}l@$!'0ӖSҘh/45Z+8LamQ43$p}gnmQzT41b?V8j]Tt.Ũ6'0.Ž\AcmLB^ѧ4۷#Aa9l[`5 l͉XgNbLSYqJC8а&";*q4;c am)f—^լܹ_hPB "OS#!`q9NvKC(MatY~9Rӻד%7\40)(bn>%.fçg'Mnk7xS^&ٍ@)tØ>Ǐڤ\Ȣ%@"F3EEџԚM '[I^+ #?l`՗&ija0v]K@6S-a ݼ w[ lXlgrqQ,&tYQ~$`MtB%U8i=Gz49ʑ];5?6"-Cڀ&@QOq.vNGEjmmo7Xy[벩y& u WOltBWg*l5n {$ O/DÞ +d_)ɡqQ_>a?=܄+nrM4tRj÷1WSĔ;8,ps55rô9ua~Z o2(P8$&DʷNQ&8&A,S9)DjN+d#ٚ"lW IQlV3N̕Xk0joM?x2kR 10]1 7I=Gb}Xϰȏ +Cli?gNۄ.O%ឺ焈i:|ٖS܃b=U::0Ɩղ5~nǢ 8 +>uE=#ӂ*:ǂw45+e +>~F]$̔Y5@.N5Ќ~'m7ͱYI7X[^Q@JF,pۧ T$ӹ8呂O2z6 㪤%/)qO8ԝҮ»&L\utȦuĎXNp@(+.AS'Got¼Ӈy3=^LIRv)~lyoHX\RV`)Y˄oVR:3=}%f>MJ. ٤n62Ph) Wt<%׼Rp`$8# 73||Nń .(ܤQuA\ l8h^&N?<Y|N3v+h=L&+^u8$棁}cb*ajfɕXq"I:W A R3rSuWڢ]5ִͼ)ʼn,[̗͞ފ{F}CHHR3U92 ݺ*d e}Wig{Ӻ6AKcqwP3=+MLqdN h‡+  ]$x6w? ;Y&DOXl>v}pOMwN8w9b}oaֺyc?J&wO1iU92|Ɩ:R3?0쩒Z!<]~E*|H{{.4c]-يQsVŁ޻ZS,!ϵ(U#G{p|0+Gw-g]Uv/ciXY'k#z%ErAuzN#JzVhvmSt%.!dڟN76陆6&&zޡK N:(aT6-OM3ѧFQ9Ef nTv&j3 >oؘva%\"'׵-h\>XK앀"jKCxԁܫNL* +H;oVf˾1]cdϗ ]5F` qRD$մ,]B,bӛ +O5 [5n]T%L[".x>F!F+1th ORki/0 E GJ)酪/c~~]5Jau_Thf,rvߌm8rFskWrĄvUgbջ͚6hi )Y>׫Thvc(⊊ '֪5aKoZi@2jΝcFc`w" iݽ]kfG;mE0-,DKz{uMYBtCwʿr @ +P |C +3~Hz-붷U]bPdP XSDY2zu"^lgi2phTΎJDZDb"yCawe#n ڥ|lvEY2_PUhd 1+jUc,4mdlg8CI S(,$ͰQ=cr{A]4? +Axd hrjYFU|{osq:/b//e sp\ lbt3 -%<'1 +ƨ*]f {.ɚx=at w;0nMd0 z?MXdT7S"Ea!/תoGB }P!6bm[27\: X}& +Q, ݄C?v"S +W8Ff!v}F<@X(lX ͨlo[K?$?.2\4-Ap,@:f/{="`EhzÐ!~[Ǭ%cE8mIu^l\Q-.@vlO BhẄR(DSQ8}7jx8\ ;\TּqN̴CܴԘI!살TVRsW\-$wung'C1-5R@ZnsHC40HβjyrL7L{UP5G;tk~Q|:Tt;qZKy;TLNi)~Ή<5M t# W-ڪiq= w/DP.#nRʣ rxR,> +$y{ +y:NnZ[Hjׂ~Z|  I%\YZ]by gq<m|lJf[ۇ)k7%fD-`|`Tx{F#W*%R̉B04=D!i6V?䫬5_bm?&?;1mt x`G:a^\/ïՑ䘮:@P.7m S&׵DQU`$jv3uc6/V Ww0kﶱzۮ9Tkë45G/ZӋdT-=JV[pbZ&uW + .9ЀJ%Ď"ξ0Y+>IM=ug͚|9!٪;E]Z~ >RHK4[?--`f2L/aŜ*Te|-8ĄW-UFePkp?!rjK].S -sZ3롫1nSo7>W?;\,L=s;j4\?YeSbGzЙJxÖ%!#uK&"zY|Lb@,G#~1[jtﵜ h42Y.S)r5bUVD |\zIE\q6CȺz+D~]s<@Qv u Z +Q)Ŗ/-{bn.EvkȠ՗×W?{6]^5Z&=1=Q9,RP#rϵҼm Kh{&ϥ桡6L)aT8uzU +N] y}ͫ :d/YK"J<"ܖa8Hv.aiQ ~eK_ DEvtDrҠ1y B}Z=KE#vc&k%MqL,0=)&.&C*s2FخTcrS޹ku> AAh@v +MmJ,+-}ãq,Ic ,IM2|K=xBݚ~A=q VaF#=\LK[PmR?S ++{9CNT ( ۮiixp2n$GO`= 0bl "XW,BAt=Q)(绷ie*$Z]<f(oDC-2+LJ8ibp},r1:urr; +[N68v)y$\Uɟ6\Fav;B &j__2*=3rJ'Qߧn'/ykAvBy(S\vLpK5-ލ%1'"<}~ֳ֐qWqbQk–?P$Ӛ:O4K]| خ;æ!=$lhRGѽfOt"cu/;#2EE;G; +9lާ(!uȋ\,;4`oΈ66a"SDke =HV{-Ɣl6 gXxhu{WLm}Fr<zixE˜~w9r:+,ڮY|[q+][9Buyݙ!hbBG1_Ac@lE/x}SW_ 4X.XD!ǩϳ|&ncvf;mL<ʨ&maVC)/XG$'#rb.}<ҧ=zS=#1|{iء;D;ff< wlLqVfKI-6iEc{?JnnUT,o36YEQ)9;}g̝'-'ℸ3S'(Ŕ<+GM,xrN c>z3rĀ`^8sH% HPcW6>) +O (lVDЃLiKv"/Æ ޗk7͘=t3M>wm&n;Sn|ݠ[ynMN{_KUfW]Ί& /YH k{|1VM~ZI{ۆ3`A3!G?\Qz LW&\w?/7#;LyQNuH,mBA~n ZXI._j_<@ٹu+VTZ1~tGjLW[[D>a4b%Uژ.\Ke:j!;Ĥ:8_Ⱦ9&hoUw W& ຯE?UT9$L,7mF0xkA%WMWt0==kxe2\]g_U,7%s8BѢN9%g蓛\uq "cYX`rhV*0{` ǰ!0Η2z +~XISkf8B\ƃu,1͈iXW\KF n'wbnu +.6[gvcω㝇f^d“fmAdÒ\"LN eZSe%9QOQ~g1ֶ* bvrB*8Jsu֚~\(X"w7iˇ 9WCKi4lڔ7o4M< #oFݫYjLP.N&FlFepNfN>3;ϓH3*K2$y +l׌ +'^|ݢmµn}a)\KOO|Tpt)P#ZCWI`Oxf AIt.*֫+u;8kYG6vӶ 4{h#8J*L;Ob߫$> c̢~tp~@BJ,?>,K/ oMhpӬٛK+̬~/6a@|J$h־$nl̅鸄 ۷jl¤S-2b'+ ۟YPR%U +1,j zFfb\5^^Hm}3ǒ0.@'%jr`z\tjeu$R4(bGf +>4P +tcBQ&$W2FFEڽgԁLaGnƷ OΒ2N簱v~ V XV'fHvp˦*(K|h +/oXYr?}A7g !O)mUQ~;cq! Ɍ/NTV0Eڞʆ [t.,p4THٝT x[,)2aT+^;y-a:ebmyOTX7&-[\-QG,}0"V=1a:Hw!:h0@VQ=:Ō/_ulxP2>?&,Nܲ8{mJce2}r+D 1AldO4f l,l@49ʷJ=R$l}ݷ2 +4RK@xm?@{6yibܑDj{a㘪N454\h$*j- EVLeBS3g\Ť"l/Gmږ[[f Q"(MS7J<äna7&տUڠ 2%,vTοIoo{ӓ# p:v?'\ZxQ+v PfwjY'.1LE3a7A6). )oTQjm.LaZޣ٫w߷DzDGx >05K %j'@~Ov:*xxg۞1߿zra H)¿7^$"Ha218bK#G_G FB _(1ں*n#@9#Z55W{ѭHÅn]0beT5MDɓ +gB~neW +j츈vICJ +Wy ݫ K-j|طʶ[_X +^Y,[/TL 9Vƹ~~:|#(}ޏ`tv|Mx5ԋxq,ˬv\WL Cۣ2 L ]ⓛJP!ٺJ5O)Tck|lvBbæt6zؑ'sgaUl-Ooל 5k +jQvSr,|Pp ej,%x4)$ Y%̳VPx%h)`pr\UÑ=ȐFFa;)u@BrF9V1\P% /]ʲG=p" /6f]C IFռ؎ufaZ̊ +7 e5xG>I74ugf3k׵e6Q% syuK:ic@/eR(8˳ j]-s]yեqb:ϬZXo7Ҹ8YDP\<.^bxޗ7X԰ey ۍV CPr3eQ=Ĺ` ~+VחmɑFIH/.,B CBHOaȈ/ Ǧl,Lp]z5<6t{nAs o"Lq}d +EYK.s #n>0INTh` '"aK™&jE ZfJf^a _y"p>7K "T>~;xj7ܿ)h۲zQ _ +QyUh^<>=_l7;c&r>9dID7Rzt&T3c24a~ wӭ󄷥D8mrF3O~ir}ekcW;xXÜ-P+y ޫ  +Yڮ03΀ypܠ'n6/4cY=x{AUӅ]qZqG{܌]Z1":P6'Gs{<`En[s)4ћ>YP;Q18~ O9"՝IVXqWqz>=СubSdắm+ &\ }ǖaRg P{,tM1/J~Ws X,bx33lVK*n':}~HbJ@)9.沄.gdcziwH]} 5 ArL.߮ mE2`@)CUXXlJ,X b C6WfIiVw= )\'GZ,omTؘ'o^&̬^5n#cY枢f\E gW57JשVDrqQiGv}v饧 +R4Fb8=G2 ? U+Fm +׾7GM* 9Ц=tްᣲfbj[)M6jO^?;/l<Ҕ^zSaժv}E&B%5Ib: \GW啔UV7_SK7M5xj$ږMT!QDg +Lzѝzγp)b7c1ƃ+ 8x\SҋݜB=ˉhB!q܎kY([,6 ̫]i:meGUTKnet# &&"XeXܐT^zBuɦáN m2򥡃JI%3XYh>v \+|\qʲ,ˆRޝ љ:Te,&Ӧ ratCJxzI & Sh"?"ik&`<M`( 0)̖'<p_0C5WѲ?& MzL~Zeth[=I' L>OES(K![rȫz|ԺDJnTkVU%ì@n]҃%c(3qC졊G?xүD7⹞hP.h)TQUv-rem|P5e>1%,z[CkH4g?A}e[̃ UnW0PίS,_;n͸<{["R?hg)}kبt$90\nZVKexf>z%ATx/T{Wu+h3?yE3qm ,O}hݝQKct:)m^Wt&VzfȑulB\طqP9^7EZc$ݿ薎- uf؂!06TbǣPVk3Z.zL!KRfn2`pܽn"É;B4w+Ayju;%Ӝ[ʨGbdfţ.9j>ږ%/I] Ah)wZ#Y|4RlY=i/bV0nhrK%U5ZxAÓ_@k{\⒐NIj@**EpjBG*?a~wqe;r\ډ+Nk`(wZ$VEOI0?I&ZPQjFn7h+Va:>4bL-5{'ﺂAVB]lc?|^3+1B-qθQh(f|sݱ84͓} Czl!4ՙnlEZ2#"-myD:j60-1f{*P: './pl|1UTpU ysJT{c[ ~++tvp3#g^sSzVXmGȚ];^6pUcRuo5j\c]0FQ\ 76J{OX~y`$c0)KZjz]7z|wY(`KC r~ቴy ^ B@5gVr=^TYi=& ۹HLIm`X2ȝf!C6{FAɧN4DwD}5|s~gǹip}:x6Rk㩶:0 GGR ;2_=T|{*T[e]0/rDZƅHKmn7aH)+ޟ#q(XyWѽ~x<Ԭ-f:")9-+VXQ)xz&EۏS] =qa1OvybhYis66ObҘ #شjIµgq1d-5u^y4 h[V<a,X @D|k>y5L5G*jg;^<hрŰm1aCdRڜy<ݤlD$ݞ "";P < j wNte)>+C?Zb5cqz~-z܊˞5w*]sHnQj ++bCgrTpɠe"H}Ro҈j g@ nKvk|hH=uS( +4,~TB!M|H=^ $ i/ΉD4\jy%xm{65j _ :&ڸ zix,X%㋝K/lTtA3q*mVX8\ݼ~͞?bE* *+ '7BA-X!5M@qZghkf/ǫmЬ^iڒ-V ud֥_#YpqmAɻM.s8 Z*bWr)|Wh5D4C˾vJ'4Bfw*{e9ѦFQ"Fn[4ERjoUug?c\eKG}<%Se+SC*d#)sEg+IRc Ճh`:= Lw+ugm7!evDcU,kf[xhv\һ#'³d# v[.J,kxۗ[uw +njo ˼)〓SX%='nz-.~[٦ӡfSe5drޙQ0v eeh/{=iwq<@t-d"x5edVN/*3oCkzbް=k'[޵_Y=8: |y"Z@܉1hd_IjLgt,ys%+"!(D݅B3Wk`gl=z?(+-^Snë袭za0Rh#]H#u;ؠĞx^20>أntRݖR.Nzq @Ѣ3Wn{ђK=󦽖!tA[bR>ُz GZv9nYX`u; 8.}2:ʛqxA@Qq{jcb;G) Ofb:f"P#u;FG({{i{; ̷^sOr7AOhjL7U}f# OXq,GQz7)ۤi` ~1iGyA6$~ll'a Wض9nv0T T,w2{#}HJ|f;-j*QN cNEe@ci4Ixi]=,Nz}B®-bkl9\̫4`U&'}-37, {Mh".TreojRKJ_6NgwEfTD9y<"Mmbo6I3v<5ўsZWݭ7"[m[Ņ= 4pL4Mϖ&%}lN[|ˈ0Oǜ}X+zcYGhAڣߝX:}֑)/{ާkj>-W⿏=׻[4f3<+X}DcrȻb~U,hDz_BL8xl,=L딕FQY~s +x4[a>oNSI2WOh-s2cn;-d@h-[ěĨ:GǦdU^>3soԤd٭A;&fx@J=6+Pw?e4VMH kBC1!k#ჺX762[`:qGDaIyhjs9ƈN;зRtG1PھSޗ: oA۾چk$z5 Թ"#o_wGڊD8oEoSU$ +0|8]b_-lVBg؎ZfB#N4g/Xvr:Krd>k/9z>\ZK}wB">(EjM|D(=`5x +4ֶ+{ja,#.0)rz!u4'(/Zl% #Z:y]حգ{]lR==./<k^bޔOqJ7d76CMx }[:R!P: [ʭm/]Ku!yqWڹ+ATY::LI (9*(y穙 z"GǛq *DGc018Caz +߬~%fJ> +Qн*gIwXl^# x:,&<Ѩkf Z +w5ļn݅WZveۮ$J_l\}4 9}mk?*Lr,]8p4KiO)K&$ t6ذs9Biʎ`2jw P+z.E̓U,;[>nr`p'uhI +\m깵FИT" /*uK6B`\\怢zy(Ռe ?(wTL…R7+waOe,ͲXT-- ~ ( 9rLMҡs>enjͰK|WjfY%guVt{As/H#b4SM $iV)3m+]$hWm}Oogwyê[$`g?FftW\y;A6 ~X@KoAy* VOH?j96m|Hp|Gƌ#vw#-L&Ŝg*;GnIol `;J[۩i:4m.WLL<]ݣT[o 4Pi]ng=Q?dg:\ZLNWkRۃӶ3IϺO_Ha% lTa¬R4킵arsgsdM_ou#TKG2.+ wU,~| k%4{q $ ^iاLi=U(pTP}l9M%9Rz~Lq]!xg"W:-oG??ɒ@qw6Hxծ +NinZ8=Hn}2Qdug[JTJȆy )HFI-I_ W&Zf0jK6>$=#@$5 3Whj ؜PףDI)J 2-q+jx]0MiJ"NkU,Bp Ʊnv.LW%1n) u}>J"&dV %&ܮz𙭊\V@#VId"`Gޥ*2ʳ^LfRͼU&ձhrV ~f6J6ޙު'Hbز,n,…Ɏ _ocg?Bw Ҿn3[t&^;eXuD68`:A,AfDY@F\}Au}?,\|w +|-79X]~פsp*8Zѓ!l~ebO9ZD8m/s skY/~bdqE8/;SoV^s 4UhÊ9g4N #cgHn鲋̣ZGH9at*NUV(U0$l<17[;[nfn⨼60T@\?2HWwDR*mcQjK +pB0G+'[qMTE3#G{%?.'Yfn_䣁ҒFMAǟ 6q}i}Xt>u]F^( 5ݭU/i].Rdܐ>>(SImOr__Ykf)~OifaQ8$$9SV\]Gqܨ6&2mm}tj +S܅cd:NqSݎF + K|u[);m7T4ug[;Am/|m.w13E!.=i}N\R/Pv6$@Sj6V.uqv?'B~4SZQ ŁKVz-+X/OyvVuj4yQ8"U@TѸ5b cY=IqQ+pXrX f=h;ͼY V<حb2p@ʝZ\E+ MS&TR +Ǘhfm0kie* q?2| kgreJojh#صmwejNKQ|`;˘+%Pܺy%~),gr^ߊLr o3;ò-^ +)w'}%*P? CJ%Dc֠/~K}LrJ_vJԦ!s^ffT1'9d RETI(ddI7 ]gn7Vy9"pn;WSl|S-7Xlaqc̝VDͶxo8 +WjpΏJXУK"VYSoP2>wmbaUCw2FbGZ"Дi[f]٥9|AtvɃ=ĐTVH%+`;0NΦ[*c^JoAyCRvt f{;-h,:.mhD_,J\8d,Lr8vN8nt08;<*cZ?_Uݾ1VjB,J LjL;7P˜.ZzFwӁu#I񹍤,wϝ9Ϗ rtC&rE鰄|UXmzJ`V%tc- rP)LnE{lۊ#K8 l `ҵ|uvaͮO30kԣ1`'hG ;l*Z,et$NM /[NK=Ӝ7t)9Ǣ H*b{^7BmZfF{d/mimwͽ q:~ǹԞUM5UR +P:T󑹂˝I3ZNjt3]|j1R-SX)w&J?݈#fkU: J(ާ,7i-7QX~Kٴd0E],,Hݞ]A/h+1gtf N$e&3fipp4G3-dTBX]Aze[R\R/ 1eOqݬ{b˭d@Mo'wdOO$Ώl}*]AjώO*E]Rq{\Ǟ̅WOYyN/v#?`s<>jrr؆H@z>ݞTS9m G>uR#Œ~ c@q|Uux2=~{E]w&`Nʅ5kGf Q&ѼWcV߇k̼oi]HB6|\Z!gm;^H7$ZE2M$*]GlVtI x*[2tA]l7 3,Skgv9:j cA*?8vԜD|-{'"s߰W G35Agj1ǹezexP\3])xm2l5oqLE?650LCgJ 5jõf]$Li3d́TׅaVz$6G@x9i# ID=?EZ$(l7i}o2?ݹm vB:d7 si3WEˮy8Sc(ﷳ>?'kX2 (>ã*H=4H6&fW7#.|ZR@ +V,$ %SUGHzrv?FѽO Ё?P癍~e&n>Y/"sff>G;m I +elG7uˆyQ59T +5gA*Vw5t:\j[lZ*J]c4`9 k5+ۘt#Vcc U`U߯DU?1- ;*+G{Rb W҈Po`I[j<'ru͚i]%%ҼT ,V` u}iE)}اep:f̩U45OaaVj/H3K\F?t?lWKRctup,'JPsxf2Ls[TfCap b3KqrX&%ɞ=Hl~LmdFmw<3a-g_0gT>> +&dA ګxDE*́|Q}}T"6jB3_aU>{te7cr]C'$Jc2ڠaCq- #3P4^}ȩ~Wn" l/y'%!p~ ,YjIUYK5VX~tx5 +.0T@XWK&l +wF;tVNV2G$w~F$JUk6ƢZ0i4qe|AO*\ICp2R1LbJm5^?ƵW+22RZW`^Q'p7P)izI.&>%>֋F]?ql.fCݞMdxyCq x}Aq]/VuJ?7LhK~Slvr&ApJ74*&Sd)Fp- +n3^`.c ,k>t1b_PƒZ[!(@:R{9(63(3>RgW}>44(KnmRVkŦ&-Ry'vX ؚX6r/^ە h[gԬPaճ}%>p8_o;URb WCodYȣNOEO`mz&Hm ^cZbRpR(GbƓ(;HsE27?N)07lKfr1i BUNσ?sŁ۷@4^܋ bEKRX͛ѪekZ̺Sܰ=ƃlb#ژ1870UCϸmll6]Z=5=ơg7aHKJ3bq$R9liV;g2VX \Os\'A3gyO^1nivs8N[¨*jtxJJIaޞvy 4[{⺶E0VDvԶ6cQ/ffWݭ# ܸɐtۉY?GP댥:W!6GS!`n7ӎlaZj94*ZDoavAhHw(h_]!(AVZ WD#:.G-9՛ 5CA?"NUp !!b5N8hAbSpbPC%7Bk>0ʰՂv5v&۵XLjY~w#4/;1N6:5 ++TJմI{z@10|䜴_G/L3@~TʨX,*q +Sd9*?#n +[%r\(V'+](H09A14bmnuW5Ӈkv8 $] #>У8_Q]R C`r;n+"總+|S90dņY51BDx8nNY؜1Zy0o%{Qhdr"/c*=R$o{ +^nOpѕ6ؙKq}2 kmіi ;^Dž.)Ly8r8m @18Vst zrf52Bժ?R:Q9pE7I!w:q/wmwӳm}s|~6U]\ZΜn2ol6:?*ѱ뭪zጨN96%We-piGo1bs`bqٮdOC)JXoͲ +}H2ѿkx9r ɷnSr"F @40>~Xvt6@aOOe1KF]~ֻI ;Uv8pIo\|!y{ Mx?# !# +hl‰/ +X|} }C f4v{mcB s޿&BFaV@.n:aWTeD͟2ܺ 'agX 5J<8bG"\]~f:Uj:|/.Պ ] s2irIs7~N > >Oo}GR]cOmX"̌(c*R YDR`"6acIgÏX !?0N]-BjIGAo6v!0WoVdaoj Ou?ƶ%Ր)J7\缾M}dᦴR+1()N &fK{Go gr,_01& ^=g5vG}Og}vrLhwv7#~4cفŇ?Kϑ6[ֳJ\Y.F`&QA2鳚yjwW]w)+dg/<&b\+4F;QrQ%W b#N^@CHd5,ΠFddY;R1ן +[* ޻}` [* hT||G՛IqaqQ'{#B=nQ'I.grIU_(Foj&iaމatp "|Ei"IN @s:|n[vHPO{*xwbzh̨̹3r\RK#.Ktjle bDsv:u~fF.p]PCŜ5NaA鋴8=c0Qa>;rwy$w""~B0j'`fM%5R(1O/ +qKvBQF<{HEoUvQb2 Zn0eU;(<UN[z*U[mǶhL&vT.M뚊zcX$yGc-:<}'m1_&Vp-R󴼴"ǚ)`3@04ONC$V Vl4fA Ooqr(K0}h4 jEz=rIvOOJj"Cؘ|28PHCSsP8NP ׾cٶ; 3n OQ7gu_PEYgt`$6VYsӂfw #jjK8$i(!O^01S(x OײH$W|~amhΣKpݥAZ+Hcv1uKDuH5M@.־ |6M[jzsjB0tF@4pqI?*TR$`W +δL 4UlP-E]?x[ + QVe CUHw p~JJxv(1ߢ t-wqF+Cbo$i*.ޖhrO\M/8X+U_^ 㯻EH$ӬB=SU6IRS 6 AS.ahmֵoklAʙҀ 7kWi#|;?m=XU:Ua~3wzSFU_8s1;N>fdX^ZE+8Jo?&H2[(fSDݶ!Rj3w 2y+ +5g;Uo#G"9e8J~p5p~*6ە]yKvmAN;z9IpYG{gmԋ^Ǎ BʆxJ -Uw'wut{'&i\%0֞F&7>VGm#yD2x8J6bڱqMeόb5=\.+A<ͦͦe^K#5"oH3J3ISVM,M!@ަIY1 6V1M +Xw8僙_@͈:ӧgO˩cjٌ5,KxļZr:/nF./, +6 xiN Xvq-7`7̿\cv@(ki5cFݬFHS~Dr={ɽxlɝ1_25Bdef~,̲oD]$T; gb$׶/t6Hd3&^oyx3pD1݉h_?kVy w)*&Uz9cÅiAXjکm j;hlS@79Sx':o y$0i&cG]&ݪ&(mygޯ?:apݲPz(|`#Y6ä\s̀mOI.n2Ɏ$7OQ~pG[]o/b&K i$"|/Pu]i)x3l>ky?u|?j'Rk}E3(Z.B94P!4ML L%TNE 5 7cA{*2>J3 CXX"C¬ wLQ6 =4VBX7. $LĞ4XTė Uʰ-JEj\ e!u*`uz~OS1mtB#ħc@4RS6@8Bu1j$=RaQĞ8pNKNB*y%ܝ"mu̖%qO$U]WS[ҕ[~X|?8uQTU +(nYV"x4&ImJpV!UB!˜Srӆ Xِ U ykjQ +'<zkf.ĵM r1PR4^w̋V֑݇|f y:RJun+y6X"+U+*jp͸&MCM ձ(6 -';LK_(:ԴTuNkڳ`9aXL87֝vph3nr6hUR4q4͐ԠxzM +;CY4SbVOճȴ\~Ų Sz 4FsąIDDKW.sQ="y.FQ)}fp7zn%Xƫ /Sh@[5On?H[ϟ N +_Lq:hd׵ݒ:5ӈM\[ ~Z*?ė|ygg)mcVҵ/(@IQ,˦:pPܞAHX$J{bbξzQ~JQn/"?"Uh_]Ǩ;6d`nuI9 =)Ve3|y:I/h8R$!H^@kb:OX&eӰ}{K}XbO?jiEӎ 9P:{!~- 6F8զQj2x lCtG@j>i\]}:Tv!FOA^צy k̚E C +2F9 ]/y͓@Q wQ9w9Ͷ %iLAK!<2TfXm0haLaZ u[|u:`C9k-͊r_4QicuБ]ސ(hY|@k PHxÑBcWz$E+A"?%܂GbbZ(NO8ƍt05LDyT..%k hyM)#eNv,Q7բ`xfmf/V[9J g.'78hPMUkWNp0 +L׍\.ҳ V +{҄86M`o 8bFnX%~9FgZ!/@I߈>9A]pJR +oW5Аҧ^Ljvf@ɽF +`ثJzn\\BЈx7-M>"e jSRWx`UeqeUBĸF>m7=UaG0 5gJͶJpB*r*lXg^/db<|e죏)OY8L[a |(G{wKTwTصB<2|kiGt:%qT0"ѥzLG +Taܠ4k`|A>>a/ْnvY۞< 'SǰUmܷL| +r +rP\Hn/H2Dg M}C# +EzJ +kNNFr*g=zWBeU$]&B HŲp}Q'QzׇYiSYW*!T!4Ptyiq`{' H0qocay83=񃄴}"ZuYz/MpLc@d0X.S\ʇhة݇4 ;6!5~Òcﶆ5U;[ɰU`/Z`Ҹ@9[]/zD!L 49;ƝF.D?uXqDăG5kƿ@a{hއcOoN;ڤ'Ͽá^ _:r](=X% w"yTGvsrKTySґƥ>d0ĝKx!LS&b2s[r|L*F =FZîOTYll溟}$9HY~@2j]źiսnG/V|_Zס|_D4pBh01hvĨ+.ޑmOW{pLN|'/~0-ޥf*"TZp1s,oK;,ć=eUwcmU: ELby³U(1dAMWƇ"Yw1L4) :*\ۯ?h9J)3B]M>Zlh8 9`޵n^2WVKDPu>u%73"o%Ӫ&j^p5vJtogbQpg=V +<#"@YbPMwG.03& R`\E'Jcu)v<}J-dӀNsl(huM7T;b~z/KϭߡX G_L>zZ1d#[ BNHM߼w~X(!p܀W"hnxk~<s cgj4'fmo/?J<$$X]?22NY u&FY\3:t|3A#ylC06`5lcc[Cvg4fc#Xb&R{RCRW~A{HY{ +C&HTiaS jHeWo銤/d=L TUg `i/Psj{1TW +bf_ZFf܊pziݼWT4zZ o䶪]i=O5i>TzqywiSv'jb*(Nxgˢd63 *T-ClZՒpz@,^9nCnϏ +%Ra>s'n-˃4lETsPsiWRrn8 .yn[h:)I]am%Dْ3+ޘl RCK}Auь31#0QÚk:<Cov]~t/N:_kcjEۧ ١?=hMsgL$(Hv$pt}2Bh~u=<0s +)ɱ_ǔjOyym+=4ѫ#c4}ҨuB[> %OTaOR{: Ww%-1u8=]j傪ǻAZ)=ig慈`ݢ4+OFʲ +9o%ؙJWkD +ʊԣ EhKSTĕZ\UpWKѲ<:)JkTj`#ͤ^^sө q7=x&NεfzOŽw X]=@Mvz(]?%I>E]@2Fк?VF6jeT~wcxM^kV@rxK+"pjJ(1#fM_C\KqgWK$K81&Ԡ;i_.~Zg+u'wZCo$W"ŔyKlR/T.,ջAFCs\{#;~j-~0's7}MĊv^Zq9Jja+#!>r3Nz I@>uӛeHQK,kA҅Qwz #{ LL@g#ޖ2> r~`T>dU7{y섂O>|bYUDҟ1Ӄ N{ôo?q7Զ3I\o7"ݺ q ;P fɶǵGІMMe6E%]ꡤqxlU&ڟeLj|[*eygq!'ýJl= -rAs9^4h?rS5Tl(MXBjf]TdΎ-|i@68~ZuXᦚcV +EHt>R>*Du$]>9,:lͨQ䒽E<#` ۸'޲0kɁk>5L.m4k%WS t,~|(_}s#äm NvZj%kfn$X5.w}#{љ`tj&qҠ +]iN#8[QUN0wyaŽqkn$,Fo_:;R2/4]3ocFvDf[ + +Ӳ€ Wߤ,=s8 TfP,"Rh8 y؀zq0yu!G~}Z\mv<DI%9l9YK%܍gkO%hd7PēSP'?$/Ѫ?1]t{^AĝC:i 5$ٶ5^'mY.3tVQ +Z kY3^?BCXُOoÐc-A w"j8gaeX>a"L-@ihwwjrŸSzz)$TGZR'8ߎ5nΚnܕy!îL^?Gn|O?l>Yv2]x"%zV왒5N +>,ȔL^Uql%͐Xv]^gX"܍}G" ~TV{3)lC +Ԥڴ*᜴t↗/p5ߚƐL!w/CvpjK6Je0]CDvO0kfGǻ +u}责,D[VrH@8JUho މj|c,Nӓ>&nߕZįiXC,L]602xLOzb@v+[z7۴zj_/mA ,'.a?qKzo ͻ3.8TnuoY;XM|Ds诈xX VC`mpمSm?E}wUb {hnV܍t1vF6C`M1s)R_оdhtx\'z Dia3A(/1_ Fʆcs @c|4SxH)4ݩ辕a dqwVs=?׭o꽼681ɨλSj<3~$/v("jvIa~R?ޚ,ZiKu0#Ís8js-[1ʖE<9`vd8_ vYz?zS> ~p>ȾpBvSjX5沌Mc`ycgz2? )SgZsOuJ(uqֽ㎷;d!wܵ4^D³[d$՟V;.ɫ 닣J$i2Zcs*VV9~fB*:,wu8p3#%G;U~5/̤nVՓK &RYcq<+ {~Cى*͉Eپ8(f#YLWsጼ"!AEտ`1 |\-Zsj$u)(yNĩ-AvgmG5ŶFo`/rJ6cIPl +I=(HTBܪ1O.^Zs%p_H8;i?=.r @`՘39=-S_4xsE:n)F.AcKC?K"R}0.p7fl4)H 5JfF?AuCZ )kt|͟xN*B*>MP5%K$LOPNNܽrOSs˝>:]tS,%vTqex 5<+ud^yK*#q/(CSнr\վK #9;L;zWwX%9 bcͭCrd&IKV6d0JӖ +{h-ک8tO&㷗^^4i8aRRKp-{0%ivz>RHr&Ơ9-S&S3Z+4$ ='@9%!3m|yșxpZjzEaXL., .Z ?\jS Ri#}?ߐ-QpfE :R>Lbl㊌-\ةͶ,G[|39 mK D!AUEfju='1[sJ:SЈu29܂HkmQ$Iz +a Yd >Ϟ/56ڞiH[2LQ xE0Dk=,{ +g4X~74hN9-t`RW@z06 +i}&t0?IWX$L9ڵϋ_M {Xݳ3[fwZb3uĭQTׯ(h0yłm Ӣ8eZhٲpicq"yRr'ȣ;ӖD22 +çsY'_e1۬ٝa{GEaggY7“Q elܦ}i6xNE?^9`|tuk%FG'L#!$qQnC3;R5ELl`SfY{[;t#o/m-̖^ үa +lP:,e?jȻ5|<]ev fB8?M'QS +ȧ#[fh }Sv?W_}j2u(s/Ok+wɋ_ ;ڱ֢8kqI52M#q֖}r#n԰!V[{T+lA-mRZr:"e#yFjcwpwRu{:OuC`nFjvaLᄰ}_ХӾ^p_k]d0, B,}6d((߸Zg[OQc2\ceNԊMܲ3Icݟ>3FN `?Td*}^P&VH ]F\&T5xF7W\Ca[à蛛p3HL}˪ӆrYgKƶPg i#R27(hBG2{w@)0'Wסհ9*l(:)h6vlmQUPEչAׁt*,L4 C UĆ fuϩBz!],:\E@be3:F>=ĬVPvRpqq;ѽR(A3P`L< @%[8CZc p}@Ov_p}l'ZOÌ LmUٱ`Va m6۴בMɆFQ֭c/ڏ{OUz3oW(Эuz)0]!1Fz+WZOm=c8 3P@Ѽ"n^hS^&q0Hp>Ooz;~ eUV$@ +e-Ѧ^|Ag >@wOKma* Bs;g2r:,3S۽ +oliu %<sY_<Ŝc9պ_fd&*gW \o62 U[Qif4.+Gl6d#͆Us6jpVPFB?D[s14vS> wdƾ;w4I܆Vg֤$B?-sNX >J2VJXv6Z}q8_y)SoT؇e&@NiJ M. kPZ1-$jj3/4_ 6f]OpS$RfL]Zcө>3qԑm-&(H=c`4}p0,c˒lX.Dg=[^?mdUImSM犉B90D*55NMÞ>P(-Y`8ٞB|pq(N4a YKRkZw`fN1cSE^ 5Z+FE169E=_ҏU+o\#|6y.e|H75 8{,Dl!L]nԶ7`Ym*/hf >X'd`Q Wܷcdu].;#Ý 36umP;j?U6(E{%bMCR;mLǥ!KpYO0e$t#P"\.w`󄹙-Id2RnVt7Ǯޝ_@C@#{d4W*rz!@a(Ù]U|mӫ + +sWJqRb N|Ze#a,Ukꘆ꾞T d +3Fg} a#pYbabFf˫qgQ2aQ|`XfˆT gp7uv1}d`m,+Ǖ3oqL#L-%ZnꈈM&<ݴm鐦~rV:x+>w%Vjtӗ0F+$ฆG(6 j5ꅈ1tPuřO [8'BDeV}G\]4<=p(z_]5C: 'Ht7 7aNUQffϰ" ԽdHaz4W}MrAdqGFڹ/RT.6 L:.&;5ț6ѩINg70sHe+!6,Ig4Ai/ȷ)̸f0-ꐧcc%ߔwC, (kM,|ztQ;QgdBNmqٮ^8Z8Cɣ*^ /I*Qj%N^cBm~w;vMO=]c$}; +MݼqIᓣyz^zeUd4n:a*-cüLܣC4+ib*lcS>߶ ۷YL'P@.}uI_]cP7,:VЌ>wF!Mw]IA}Ԇ3D)26eh6z +15jj;O&MЄuU烸O;i^! ,G `@8g8-2ɺ +=e" ),yi'-0QIe:W1)8iXٱFOFͲmvo{*56'Wk +ntJ02Su% QZ**v3M8nNd·,qx:_)Ûkw1D5_yd6tusazױ˷ȶ7g+vX%qI ⊨d\7WxTg "~u}  =v&22AM[ _/5dt(O<ݸ,Z9c!4sQpgpO-fuX +BHRnG1ڬ<} %y522#} VAP& @$JmDJr +kwdyPL[(G7Υ#qR{f 'MTP;iUWu:uFs]J@bHL'_=b<1`beIMT s:En,ǥ3zV CRq!>oNAmÇ)?0hwUu}&dVہqlR=^OXij Xx3,,n#hi)b,jVW,Ѱ"Tm=ӗۀrG>NPJS(wz-2 + RC8sCbLF̠ndd„R-Ƥ}Ɬa9-ӒuBTYL))qv]ʰ aV}9ʹz#"Okvfb-k/e z=v!hE}=%9i -& dkk7:c_M]V,Tэ5:-y߁RjL%œa?[Mrx'(KKxYgCKWaD7YWvM+NәjVNF8.!lѽ$F}UƠ=>՟_& ڧtsMj+lpQӰxv:5.J9Uݒڽ'7]m~ 3<@׃/~~fgbΓ2=֬?<VLHg0cqq;I:4rsضtQ宧e LDH:ؑ:V7mc={L+|َ q,ԹY,6r<eX^@aħ i=VNn{s+6cן{:'`4iD47__'Xh+&g9fT9dPK\)r9Ga@ͺ w۫/K`q w?ˤq~~ϐCҕ h8P65u2]1^[{d2[a(#]ԘȹظUZ(Nop& Pmd U +wWB)2io' dor,>g>V2tWNxz(d(APc}uw1'QpcoQTj,MN/`X)4^h՚ R՜>9?u³ ur_; nNw&"P^E2=u#(`giv_KIK6+'0:frrKNֻ[[*ۢ.V!L#|QMZ-sQn,٧=zmQ՝l{&`w$3еWm>{\dN9rڻk:M[8^_"7-fl7M6=sp)U($ 4v{:t,ڹq ,',$YT9OH1 Cr>z+ ֆت;Ngo p!w ‡gՍ:E$#s$C {Y ֤/!m__~JO-v +SFAL=Oo^ycm$8PD2;;Tsaf\?6]w^F7:Tñ5V`r7VL33bԐ9m `Xƞm\_ [$}j9eP'ծ]STIʨ]BFsN-_V]Y}fMEJud&2}! } !`5e*:0}>Voapzf5 R)OA<PsBӻOy$ɭ@tO ,t,EYiêdÇI{궇&{֗4C$C1zϕBE5zB.)H?Y*ը9AqRtDlo:O/DQ3koѼuy}ѓ^<9tIOtE,GٞmNP'YSUctx|U ٳT̋yk^ HPds`*,ry:&9W8bKES|s«5G#%E6#P`q#E;Tkt}X<j_Q^f׻r xᴉoH +_Iӿ;8K[:18jiĄG5ܕ9wɓȡM!<k*29ൖTr"niWkEԿ9uZerTO+9sYIO)<6%x=sjϫ.-=/HvĔ/C!]in!,JlrM7N EqꚵffFjXTF"qvkC愋l׼Wg6)7bvn1'9Ytڅm}nxLAoc՞}yAPʲ?j5:3DZqLl|0T#>dU= ^;W+nRD|YiO0YOSŧ%L-RsN¤F~%jwN醛]~hvD^Ŵ.9c$#5fE؎"4.ܜnu-Uu0LgܗDHz`RǝEz'j]N5WrFb?hGY CBUQ'GYrU@֞hbf;1?"'ü|\( kptDCqdÁ̓02@u3rEMy*sf/Yl_ "* {zY԰RK?9i x3kqB4E(#mZT$3~)cO_C_9H-q'riFe96"˴&(GiS87g/|X.odz%@@i!%T8ĺVy2pLGEeϨKoIy41B*T0kٌ]iz& Kc$3^2il/磡R,U>k)'Tw ~EH)ꓴ$o".>kH4;t{q%PI{+[FyԷU9d33n2q%Cְf@(d-yc.ILMEsdsch(*Cɳ0Ɉ~lD; &EbLuHE&Mj$`jXqp4r,շbq#7gۭ$覎ϣjuեvŲ&jy^wJ Z%楏Cl:anSj$V!j@h!-PUÙ+uU굵CܠAK,>MΛ2hIV/N#fI̬g} Mt\HiOעPf^҄L,':B ehAe`^v @Y^EK<e,r"7~8>\f3gT#7k:05ғNKjM 6dM;eW7l\!.鹏, +!<%/%]Sd:D}oiA; KVm!r d >ȃYv y`[LWO_n'A}Dz_m +a3_qFl7){Jv|PD-a\#uڬU6nz>c"b^W.oaz6S~5??,9ZiDI/X0djwUs$x}qstm\@ʷJ>TƠe:E͸Bӏcꀪ٤nߩORwψ拀9sB1dF*y%c0jtIv$ D:.P8 })[%ٛN@0f{\#CAc& p%X21:!ϣuB!:AAoĎ:gViNWj"WtYZuh['NSH]*eu}2l&?.@ZZdȚFp{O +M-m߯ & dsDք& jk7qAz_|oG^p1'LS98Z?Z),\ʀ%GJYKN#y_p-^Yv7S YX"3.rv H !-pΆ&}ڲ..&_,>>81>Ώ8m񉘮3Ixg4K;KlQi T_~i`ׁaGB*tT2IwF:2 tL2{f-a'^!|5,-KlN/h( %H|(R+[y$p,6;^7*H@h8N5˓ MOWwD d.LF`8bZBPv,)BǴ8Xy.!^ۇZ|l[ +^H!,*؈[|yH<=ͥIZm2P~67KN'(q.|NȎr._خkv3 cq=قo5\7`.0eN4,˫|s`AH}ְ|3~f ahMˇ +KmiFcBt䔰wt8j hAfmYKlD=%3iya&u}kRbe.B! #x~Qtn*r)Q6`O:`{x}V4M*NOq@iypnrU0Ÿ [vEf`b'6FBˁT^|3,rF!~2< +`oO@ <)(":qLQ^0$e 8  ΍ ?>Ƴg=6YVmvq2Kb[)xؼ\} 'Rvz:f+:麈0&5@ĶbMUS49~@t+Mq œ>: kJWd`1d̝3׬caAg5@\ѸB rTA1»SAL"~s14mB`OѷS]@;GBv+Ymk-+OɏC7#и֏ҼAe$*Je"V3"YK=L' !r7$҅{9KF~(-]bY"8&_EMwҖm+gukzVzZ-;~}H(\L$P[dW:zkói2v[& 6^7/K*!Z*x\uzU\8dHh uyg{Жԯj1hp/ H @塵jhx x2B#arzO!MSM(5nejU7+xGjyoOs[_,bc&!2!v5h#{ 2ɜe/ ]Qw[JݬhzLE./]nQm-t:tXE1H]$MxCW&߿c1L}NC6˝h)"?D&}*FvYhbckbi1x\s +GvгX4wӗ8WvU $U^f`z'Rf  + -^nUVU֌i7BIND9%aK':Aa$>d4N2鏏YS 'fKk+U,kMml_P Ij ^ᙬ9jk9}6+,:*=W_0ۡp$!^mbc1=ΦM.q\ZI6ɌƞzU Lz.Y񑮶5%{ch7YʟxЍa"l ++~0:JTÍh3riKs,hp S?,fP}J{+1mx$\,=2 /p*YeD1͕ԅ -XX/}sH[)ͮm::Zxo5ӂcM |'.A\~[bT +vwHKFOv#I ~9~JWtaޅI, jt,R\xvGYj)Fv\pd<-%|XփYS@ռ>Fp io&;rHq@GrIȍf؏@)wNcMo8j-87МV^t=_íQ;o4J4˫]Ú']lZ~άx U?IT +=668sxlFy`]n^'^L^0Ukv} gV*}䴶ZœV?YA)샚@k!9GSL~-//a&B̘cf(T]Vau]rl)g J}Pxw90 5B `ZmcyT! :A43uZQ";]T(g9B4'/-`.f>}[Z WOoV\w{^4$Óhnl5/Cx?߻( hPe`jwv ĜPbo..sPߪ [QYlZHr{VJg YmY{tсg_aa4.r`Gs~)zֳ jh;u=dỒSl!Q^b#.m5~͝eTP4LJm>ޫQA$8hܐՌ8&C_wlNZ +lTT5B2 rwZ6Yw``*?ǥa0׵dp|DE&3ߍ'Pw e*KE&ɩN{唆csoT4R/do}tӐũÇpo0D^\ts_;Yh~ςZA9HmVě5 6V5 =gLKʽD8Eߚ]sj_?+;Rbh^ N-k?CJN\KK]Fǰtd@f>27մf峓ᴫ5HQ wĪ KC܍i±V%k{lH1X^4垇%zƕȩfx@7d&Z ҹBrt<$,Z] z`N⪶RPݻ=nx#1ᑯ tiE7nX.P- 4I^.MJ kpF$injL5UAU)Qa,E=N/N5Xfswu+^)w߲PD *]"*)5(LN z;omG.nhʤK82*he1YDcc5k|љƳ%#l?XoOi9Źsz+$ͮK䀸x I$i,2lD'g8c,0Q沜 Ɯ{xMZ/dilsӂ6d~q4j5S V_x =C˖Ǎf?KR,Ԃ7]g-Pxu1enkYyVL3T/'B¢U ZSN3h%$)3?Bgj FUPp!*|V|liw4[Y?,OsKUyO#NP3L?s+"͑WbѪNu _KTy`l״f`v+}:㶂ү+OK[O_x2lr0fdP&FvP3 [<7Y2" Pbsr m%os!$wǡIf#bǚׯhY FENjdlⵏ԰ka],zd(Ԣ/$o'ζřp,t#bL64*z2ohkܮd:JM.[ l)-C`ҌogOcԉ-hJh%G?FKsR3**Q5(iJC% W֘zMn!h^7s<},u2*F<ͷtϔ{/vqhMPYY7dW" 6 \GQ|j69da+CFdLᾒ?LZ 7siyhMu}q_(<7 յ>-ɘO ?|CSеI ,Hq&݈^Uy4T60"hwL)ex >tll/4S+0k` 1KC>شj[6;fA2d/-o*' TTQx-Ozk1$U" \.FOl2՘\-oI7v_nT73nN#gR'4b +*޶sF#smAA0u84U$/?HB-@0.`ChyULN$0D?"L$]= gφVu3*4 \6!+3R K:ٴKQv +H(ʚ ?t9vн՘! QI4ۀoQ ^⑀Ay8`O.eb^[dG%W)dG{Y.*U}$K\LҐIwX2%SM%/pT('BAGdWfD M0 fȇ*il=Ut7~zJ3!_3%~o2Tɣhw(%.h`hYrE k;qM%) 5;&>0{t IY2,Q\X3Eۤ)RT/{'%0{MЅ@=r=Д6(4ErjyhL5yɉրn瑲:Ҕރ72v'i$)ߩ b 2JC?LX5dpNq,f4ʠŨt +C j@9M*(צN^f 1C,A\ujDR6/Wr6YBS*baA-afߺ潄vʙYmE,_* 0:/xE-ԅlp[j_m }131T&h$Sf4 +M+ǐAXh*]$1IJ$Xq2Wt +LᳮBFWsXp&a}ir\Jƺ'0a@zWƝ30Y Ω``HO5.q/rEKdMQQcV(8RLjowYv0ƸE=s vYc~M` +j6-E^lQo;$Ah3]5d7FSƱL(ݹM2`*,\ZJ&Va|-YnUÇ*֡jly=+2F# 8 :6kpUsVh7rpa].usjN~k4Ԙ@}d>9`0l8rZ|5ҋ˭lAwʻd %e>+k!%Y|}nENc`2iI2*+<#> +1J TAŨER8>g6+t^7i4O E|iz}2oAk|!2~6Eǫ=b"%",>?]@Flwq(§!іT%ݺZ'R_[6~LOBLldD_8/ QaWDm!'H1R-U5/FrȵgQGf݅_&QVӱE9H`A™O)PƦHTe{M3s"VMByήǮ&uٷ]l构Y^tX;y$oVz(5 V?TȐ 'mrF " 9heRKGXIlY4>WY/=~mArGMo!RR.- .0̻VOtY|aq~4Lf7[r@MRz \溹$+R 'EeUeРӺLmj#r\iSDIb!W|b{b.Wāl 8$Z.Քa!WnVJR{DC^^*ҧit@*C\5kß46=eSd?3 +šBv=b'u*Cdx -s^${''p:g o6`1GT/!."! xXj]1䢼t:n;1No%V>!imjU 欪Ls1(tŵZ5Od(/#%G +*f<|%<@v6fs@K}Eэ %BP.!ͯEuVGu <܎昳вn3]n ޴B^h 9Ԅ3_ HˑmHoo6}>N lVm@I%Ƨ*iol,Vahn:vgrJb2%_%HNDw@t(s$ i?}W`ܘ ]ƾvS60Ine&C5s!KdpmW6i+j~i8ZCi@ +]5k%,Z^לEgD:@O u- +MxwzY xu=aֳ[[2 +yp( Kq'dL1@ީ@=u5LlLe4^/iߪ>(;k | ^%_X.x,>@CRUwtHMf\ܰ-EU +f:PVTWMy '|i}L@_)&.Z E0tR||s=C+y>%3=$н8ڨPp=10!1`~3bGA3(FOٴ /Z KqQ sB-ϣ-Lwa0)mGw|ϿK<&> =4I ['z[-عp%6Zm`Rҕ~P6imF78f;sPx%]^5TgomGZ㣂ї-U'Xj.ete=GoQJV^(eًna4qL/*ʋ4b}TXug>]I:A%#QO.diNggZ_'g{ +YgL?})*T%`S'W(΄F}c>Ѓ~1Kk8FHTSc:.vtwR(~V?`WԪ^bZѨ_(5-6T4$lP_{s=zf _xqօd p'8&3ٻ ][Jh$Xho<~`ͱF]0o$;>ة_`\`Cn-(ckxN 'Al1]J% )@ڡ] zel}~uL]&w1~:(>mvZƱځ+y{*@VE C_CqM%Ӹh1OQ̵4S{XfeaW{ 8LefTV6d6,ԇMT/*iQMӺP^rpl ?bxp%ct37G[s瓅cGQo1@ߏ9V -Gu.zKQ&$}ɖKKn{GSLF,n xYR̨bёbl~F^Hi6^@ Y"?p~Iqvj] -߇xԌ69NׯFi.BU7A#}*IXȤr S-Ve)U0潓R=|9AuC&)͑{Pa0Q Փ۶m_ Orj/FY;$eBQV^oǕ>X6vC. ua Di%6SrG\~4* SWUQԖRF% Vʼn90~3@KK`&%%cpdp97:P$|C/hZfpD;|s{Ziь5YW)$4MWH\)M..DH؜D";`ݣ䏇 H >LsnVZ,=L7qst>GSnwu +AW[Ҭ#ݜԊg!djEΈ].Ɏ6JmXOO{\''zIjz0yfrd6_cQ\`uMDKTw_F k!ʺnUtc&iʂ*¦&&iZ5!Zݼ5*_9ss'bW0w;RZ2jIelۗgEcZOw5RQCb@`&/&_Y2z'*Si|Q*P l5|mh }3(! Q] 0s22zV[^lG)WMO8}V>j!tD(|{rhӂox((k5o'i& +ݏۮWG"LhA>7D7+ִz E luDn>L8SL9M[k=Eh浟UCtO _FaF9:'IlN=5t?ZCkCe$/ Z/qR\ pd_r/E:r4CD-½NC-V[Bj6ĬC^[bJҕܯA;k R #W>ڞ:jV]=T[cFF^WԷ[c=\V멦)"_5 fkG^鎫u^0C9$UѧuAMesKT6FTu a^<=̺?2]-s Vhhuelߙ!Bq0^K؟A][:^;õoײhom\+d+dz-Sb|/_1tHod͐˼Mh]Sg#͞`aiYk_V%JͯFzE8-cf?dCxXMcrb/G5hewsSDu[W]8 TeͿ[}hǶz}i#"4:JDac]XR o@#ҍ9|ZdBĦ yA"msZưet}z^]3(z՝e0m9 2݂2+C@?娰^o0z Ҧ*\XedH.d´=z,,Ww˧Qrw4wx|y[ԅlnpF Lޮ|~EK&WLkIlRhUП͞O MWt@Z?rGQQZo%?AK ςVЀ_]Ju#Q*E"w$[M4?|_ +b(= +Hq]9*~9S`>٨Y*nmePqWnV; (\նn`!@P[̨j-r!O\H'&tXV܏94Ѣ:K+S&⢅e51PBn@~GYh4-O>D/g4cIsWԨ-Op0+=CG7pY?W ZۺI)upK@iv}4l̸ӵBsG>*P-kVi8R*bFJ⟿qQ;'O02<ƁGn?yJ % ۗLRQad{ARv1Du:W=uRdZ0[,r6icc[ۗˋ1_qs]-jj.=?*5kG̺ +7.U[z䉿 hŽGF=v0:#ur 9EM? <̯NME|3rP"[`L,;)jfeu2rŽ ˵6vmheXU:oxG.=|WwtzG?Zb J"1@2ovnS}ބ r7rcX!K;Ā1L0vg]vNgO6 knU ^MI8h֪7UYmvXǎĴƇlv8( #gNu:e1Kӟu-ͨ,Ue5 t6z]tzy+܏)AA @&qlWԁGQT~Q蘒c@r'AtFq<ޗz4mFdrQgemj%fX6U;o/ߤawzu,sz ;!^Y?ئNh*f19+0W\w8,-ޠL=jeo=ѵel>lkMJ +IQsa] +8xVJv1NӼ85 IK,hLξL {4_#W67(iW@_fa! fQnfPdM[Qywagm)e.0|Kydu#AtUh@Fԏs<*֩oSvǫUʼj~v@f(יU5w uuߏr:㽤u9ezjY{=UCӣLo A∟ڥue{c)3&a;l5<R;b4jN*q0UuN(Fp&m7tʭ_- 6J|:`ŕL5; 5 ;QW1+o&{k#pLqb]4`* rjsLAs5 EhM_68$Zݦ%_[v^!b̳]19!,i\ONJsPS +¢&%7Ng|h\p#M`Ltu Cěmt;p1!~ABŬVM_M֯C+T;(wURvuŪz'I6Az.s_鴚t3~a36m;sda-*vNК=MJ9x{ǵ6ߛ2YaeTG3{iE !{Wi%N˖V۰ +zRmr t ;/!!%L!Apx[}F +mh[I%\ 0=zϬ7?_6S\Rio^=*<4| CtgjR汻ІO󁄋a"CZ0um%L}]*6u#*'ۍR5j =*&a&/:k^!SDN*90T=J^M{"ǝ"nHeMlf&<&o:9v$'Fّ eЀ޵rmsT.u)]6} ,}:!WmH!9uv-Wwh*;Zb]nW +ӳ40|T] H`(79 rRq̐;ǝ9q;l6tB}ިKLٌImˇZ0-I[r>Ӫ3{5tؠ볛afa1n?G +{8~m?ʶ +EI:1IX\}q +S"Lu[KEMhg%WTt|ztu`!*3DZ6.p3)[jRNu1RK4K:BO%6`WB|۞s)\v:!Cu"XI_ƾ&7M:~ܔFX8ofJ%%{NK=kJˬvȒѫ~䔉阝޳ݤ:>ja0@~LԠl9W?ɕJTPO5I`lkֱ9=ƌ622M#sw_BB6AW0- c[=p7IdZTG8fkV\pe"C9*MKlv͒Gu!'ԷRwBqW5 ?B"a!Z@tΜ<bcLnIp̮4M֤N৹'dq4O/i5 vDk%OFp`0`uVGA3:!z ɱLINuw ZofEPT~_y':y~mSܐ 2\czf!)[m2I˦'Ql@TdSS{]וGujt+I>8-sXEN_)Kg*줼5z9Ab'k3z飖pM3-F/m4?*?iVzy|E߮'oB\=_*|hYvyk]2(aj0"2.pX:NjW4;tzR8>qKOcZSnOJ9; 9ɉp' YF}ۄVI0JkN3kƿD?|R J' nhCy>$;q,Y`h*hrp}UJulaթoFADXej#\4͡Ef r?ߺQuT5gMU֦J|< =,826=!V6#aQa 1DyN1ng}H'!*UK)a |FZyêj{X֝}6G+!)C#-pMd,!ULn]dX,k-R=~1QGx/auq("< )HUO@>e~Ȕg+F|ռ~\Dx_y.TK3k_Ty7K̶T% {|1ym\:Vrܔ)O<:Ԝyw_t묮j(ɲ6Uو߯S\a9UTpcs@V nQ6C agS֪[fIJSS^Yl] #t׊+qG46vplM"qx{2375c8li1Aְ> AR:YZc3O‰lekJLvY~F˦z[2+6{%>Tjwھ^=n.xm%WU'u޷ ?{@5.MMZ^pGȗ$8$G3=,~1t )WCN" ږE{ö? +rkov^ƨi'>nl}T5C_v҉`QOeX;ydݬՍmLi,ϟ[Ob<r^ahA+ZFV=(4+"ԗ=fiZ"yF,Q^.ɍ0)|9yד/QURDFVRI/!|t30R4;`GY.!=wArȖYY vuvtJ_k@ڱ ,'{ߊ'ծpac9GvrtjŘ6R9~p%Z) aJf1.93vg>?,oe i㶌2R=*JܦQ/lx<ӱp$612 R 9EYIUk +o#, z4}ֲ ==أzsxO+DY ]*Ҕig5VIǮm*`js|n$o."<$w%< oӺZ;R)d,n=,:b.x"(vSRۺ$SGʡh d_hZV11=Z[M5npָTͬkw<"~]#)~IBh6zs.kEoKMpyQ/dILe d\|j8e=W6eWWXuUÍ讌Śsx~[8Lˉl?忖sK4p2.Z U%Xb)%2In5R%* 7/C m_\WHu1}5q~yo5Jy=?/ɒ윎ύ'l>jNsZUC)8/TZPRG`pnu:8VPյx<Ҩjطe3. V9:M/3l&B~P188\X#C[G=F7pzG}0Fư=t_/VЎ BξF}o\$hMz3G%Y#)@9bz0e{))QD:;ECQ[f\{wYOozF}pӴf?j QZNM ɓ-_^%`p_u )^խyӷ?ۿ;o|EuXPa$4arG3*n<ڳ̵m'q(1Whs4HubVjdaG[r8xB,XV1R-_?ݤ(t LT5HIURf oY7FC9{k.\M ʻHyCU~jSd 9ɣ2 T3fzzB3, 0U趄 ++?ntyjÎH? +,WCd\ +Pf@1XtĊ{T_,"B$ m/THD Z9)p +3&jMQg7A 4%7Ip0C;4 vK5}d&w,k;$%Ui +(g،lZ-;Z`* r3ۖ e Cǚ㫳à9RG4[" +[]^AT]fiQE.hp kͭ<ZNrG>}ոh{96=)`q&L#0TT 1ӑTviĹW/N^)w߿gj$)sբٻ !'Q=,g+}DCFEr.J|*aHRI( ohJJq;BB^fa4u1/od4:Ye\bn#oM5x ̨s臚y_uϠ\1jٸAN? Bi7" ~>z28 +G2.isv}$DCzN k{cYK[;,areӊM= ªl92C5}Xv]/?tba+Pt8_ ˀ5! n&?@6Lj]QgQ_\LZ6fjȗt5{vG3a%9DY'i`̺dv|y|sn40}t.unE8x49vq}ǎBGhn}9*}<K?9DmˌϪ_|1h,֖wne[]J/y~N=$rZBQ~FiI]6,J?#@3wұ:qNOt1V.SH`7(RG+SFKm6Z8xtɝi7Ҩbsǀm r~Zv3 /L ksq5KӘa#U݋u'i|ŜlWž%O@siJY3I&awsJjeFFX}Hh)*rd*,>孝>o~^_{=}[CVخ*va?RN>twh3>uzV^3Dnd.QiN|qW-uk +g^J Yt#G> [a3 ;tɺ/K +G(onݡ繏 RyۚzMwZ2I8^(y\^nP4kʌYĸLr iE~|K[dkۘw7x*4z"0|%7s1VAIPj1Zr+#.2Iސ@}bX`H𲟻V̓@x- Hj,$B}&29g/z `},ۀˀ&)g_!ƨWh<X2U r3) 2ĀK)jjP[Eɲ*Yp.,.˵;N9w{[z +ҷtUՁQ^C+(:10GOZ!7W{w܋4PmjLȿ=1%bDiC=jt+".oP|=~)*@|Ѭxr{Rb[g;)G+_lsbhy]fVs<|cL4 +]F[v DOd&OJXE>:+Xnm?,Wp\w7'eTEmB8h T [-㊐ρ}72D$MDɮm4oQF +ɺ pEMTu>km3gL7jؿBlmI8tV//;i1yj:D✀\ڛ-@Gl6} +-/jI;Ƥ)eC$`%[4Ө?]M_k?^1E쉎R/~:7ˌމ=&{4|d#nډ -΢$vҲ80b-J\Tø[2c "r ,.*i^uZrKG/6LOM"OKul8#xێad<o1a#OkCnm#QlC_Pxg:v'eqcCke?&JlpYnd]g~aZ|w'pY/OSH~ 4k,wز<ׇ9=m5{'y +*;ػhLGA޵84a?Ol4:yc\:oz Kߺx`=Wk[ힸ-i 9بy>i4}-jƦUhPc?qǚ"xK'/3nezy35HYRۓ['3~ӝڝYH+d7#z_'mcDN.ToZ=i۵х;y@D*ulꉇqY͐]wZ9@ät͕Y^)MabBEdOM6B@uPgQgP e-1&o=1- Dղ ,!f&5ۯ,ONAck?࠱#8J]Sꪌ侪ϱnGw9: '6+rfbszW#>ugƽۣyjgpi*:kh ,,쾒kI\)0Em|I!' 1RkOY5ՆUYHLD< +e5e]ynI^]Hӧ W윷Vc:AKr&aeRы6\.i..2K3svrFp[؝^mƗX/0L ,OǘַׯCŦʕV/y==}GXӟSm?pyvV {Y={Na!a'6lxׄSp0HR`}1Cݬⵔf$a_cv7pnXiXeWbc {ۡTǓKj*5v#Eq B'g{ؽ ܾup"KmӼDJaJ;ӛa#&m׍9ECxVFOh5o;{')]\W(~|R.h5!~fMӈKkLjQX7ȚKeN@[7ynJU5r'V4"c.ѐds+b^zV[s0YSNe5;tP'5eJLDRdY s'J, 0 kUU{ZTݿby[ ۲dIєOHF +dZu߯j\}1\Mi.hW>:,E?t ~LT!a(T}_Lugym-a4Av FW8}cŪی9jB#^lXܚfH.q[@!?z#JѾaߜ7]Ub.re*G7tQc/Z<iq~J'{IN^qmLwb+D(^bط;8 +`kML?\j]FAxōvp5Oòbh +*2mR{#ʚq0%O{Öwb +d7$w6`9->LsWWMfoX.fTGGyd@tSTtmLI^ՆR$p`C.N@81SXޕ-a'^gmh*jӯܿ!]~#!_ 8홈g|Rn1~γovKaOgV,2н㯲 ,u d5Zpݖ,. Ig^teEU%$wSW-QɨrY1 +ۗ:B%)JSyMMԨ;_F:+?ɁV?44%T\SWz'ГVup<T%u1Vl|J+0aik0PܿfV݅`ͭ +*l: K# +"z41 1}*!Z`@e4,K(h|$SX5ȴdzH0_ C;4 ܪprCDV <>%tz<-mK2v",flǓa=i߱ D +kuǁ2]*0Ӵ=GVMXS}s5&u^ SpdiYkyxh:{L]Mi85ͺvtae_Qa[VBs@jEL?㽹ZwjE(<2t& ]o#UmW;mf yk2?v}9ueK2*J%PmsNGʝFxa +Qh`U=;9u?N>TK⦙zxX:iVB{#3m9͢`?~}"-{¼3~R,l7.BPj8Y5RlێhTcm̟[-j#Y_"v+wt<k^emFd9:E`ӫӎ~"ˑt_  m)VآuIk)4ckȀӄgrEL[&3I:QC8͞6lf"ïtmC4#iw>!R9ʶ>C50+CNŖU+xsG{ nݝws] -zIrFD:/@YNyE_@?@Mֽ)$ +Ș`I(ⲍ(e)z +ʀ2{B3<灆(26S}aB9agcV\~|h`ME#=W wzP=|w\NOcFӽe:eA[76g.ks#l6zA(@~(N7T 3nݶ;fYF) 5Rbx/^easzaj?V ©JF=D@i5v9݌7T8CP2߸bw~z0,O3$ۚW hxb~ +n='fQ\Cb=\RM J̸OD7WOLITUuSJgb%8ĵ Zeaut5!iLwnsX{n)iIBeX wfsU\=d*Ύ+β=V*jBPzjv.GPxAc߫<5}i [MIO4VKv)|e//˝~Ƃ}P[1sӻÔR1As1GwV^)PҚHt˾X{7m1&=۴ᖣHQJ;szRPQmIldL!w#5JmW!u<ѷݐnxJay 5]Yٔ܇ r_Aff|"H{4MܬSk'?ͫrl-/Rdʸ <" +}5K-1$MOʥ ؖdEvvjDDn*$1]0 1wLU'.* 1<]?n~4{Y.%ZdQF#\/sPYh;ۭBA,[DR{>.ihi[%'M dJDN=|$4?KbsV>T{ag, iS)+e қJ,N #GRsDS_fR 1pmwkxSǍFm{L Ig$_佺4qG[PYI_L_%^>9߬,}nUqգ\\1'HFU[<[ +AgEf4I7K/ʥo̩?^܂.H\=Q"ӳ*yKK>z,9#0`K;KQTK۪U!m2XgJu}YEJ˹kTNF<&jx~wJߓܰ:FiӾnjbcu|ᴻԇĖmCf[ .orNڇ~z3qstǝu`j~`r?0e-SYDe5 +Us׌=v]^< n|C͒rg$F+vڌMw&^cT*>nG:D<^ܞbHWИu\قHC[9mL6[l+-!^Z{fβ\=1r'U5y[ +5Yފ~ FN{Y/zdeU[.w +@xi2Xنu'L G3DG'փ˝N|om#1M\>V&{d~13vFl| ZZ)hz"?62?]Z2O|q.dS +Evʑp7%^sY{ŧZNlz9Ū0;qq(+bl.6=Jr95?Ys?r~ ˵)k@MA=zc@NHs͖n*a{Qf&Zcd3ښ5WGJ TJX@*f +%2ghˉV-&&)RbO׸L.bmּN^=e2zqvӫ7V1A"w2E^[I5-2^5%<,.ɜ%|ů- rװM1t Mt8cwLWjs ䷥l>F:bV7ܞ[}\:@ch C̕B̏h5V'\Cjbi'Tzthy(GD$7#ӂI;E%ʚz?nfI&.i;!YٞnCN(ӯgB DP RFz7pe+`jcφEfx> hactOwfkۇ< +MT}A2 "q&'N>tzYe')a.oנWGo 4ޞN}i<r{.Ҳjy .C=vsY`2MM;e>O,~m[^C+&ӊoD(՚az:V~k6KZu-I?/H3Xzd rO-96&/ CjXd/Ӡsƭݺc]M.WFsꗋF3mA,RL>;v'&M#¼c>g'@po=*D;")oGu5Q.~ıv(V(S/uH߮?$\nwW"2jN/LY zYT?߶l&7~9KI̎wr_j3+ХDN +;ޖ \ȋrB )CVIvbST(Ib~$xcD:B qY Oa#e:^}1!ꛣ;h캑7J.IKE P3Jav"_SO@O8I'.ju(vd3[餣tf"(Wsj!䟈kPGA&܏Q76Ҋ-L)'l+QS'.S#fn +P{"I|<-Nt'D>8PyuP1xIL?w^-W:VQ_PUҹGº_cZE_MGָٌ;R]7G3K`HuMЩ9RA#)絗~$]lpiQMcA8Y-@DOv3,ܫk՝R*V^rK^I׎a9#N&FU+=aAB ޠAyEݖT0] 8ס@(^@h_^6J<"[4J8u r SrW ˅ڙلAO`H؛S`pbDWu(%YxV/QC[`ȶW9r:B;}HJcK۫b]mrp/**cLݨ)k|!ɇq 5:5] +d׻pCًB/DB qN &0A_Oj{a3Ci}~`Ȩ+ocXWX ۰[&*u2Sh)}s +4Q:iCE"=͆L250w9_̳ǖ <\b,U6:V12L_x!MQ~ޝxU ++3S=#_q4UIyhǂ0Aľ+ǍE [=UY +irv!⃮*]wM}غ\ aŻb)bԱ:1|Bӳ2(Z5ӟscVi`{D-V+ . q6/G65lŶYrӖl6Φ>]q]Mw~YkQOa<^$ڰ6)mX3~HPĸ;8njȑMA*5Yaao: XUz e۩-,ZOytP w(/''*v|lfS :Ev]FY=Z8,h64ߖ +PU-MEi&| w:KJ p +Ӿ,뿰)+^]Ø??{NL]ߣ>{8O sWq rxVx$uHDEۜnJ7< +IZWTխkZ-j8??? +Flj  dV䥯6O`y*Mc%,̭@&GshjAWh Jj U>\;Wv.NZ\6&,3mx:ƙ-O} 跟/=fp3BքZ4[6[0Q7gRaL-6tԋnSmۢI^_z3^k  B|[VW<5'tDzɁh/18,SVg6QR7g|yz;yZ!ۇGħC9n۩;;htX2P˖AKSSbp7b/:f饵!`Q۔eB,c;:ەSzuý9|3,=q"FSoZ5GH1 .{[Gǝ5@a"N"thOd≠GxDZ~i c؈OᩱlZQܾ%=L|p.U=:4qw-wx2깗˷3s+?}8'i%F^vRS+Ewx´T6YF3mN'1mrE3d+s(~AhBX7ouջte0XK:W^#F6~IHy:]#~[Z߼cRٝh0?w|9y9|BӕGdM03G~}Tja%BetNvWNX}X Q(TG͇-c 8A|v%l4'1o\fYS;-'Y[$a'~KJ?mخai^ ߅N>aVC, _A뒛:jB{%-Uϝ\zLգs$#F IfFSk I-Eꜗp~X:-M,eԇb@Yw~2]zDܡu?!ԥ:itlyb}.r~øUH@un<FqT]"KZ][~l?;zˇީ?nE'KtgGXRyUr쓀kOZ\q+z+؜6&Lw|0 +eAh5'1+>5@1@rl$qTQo:f('yWphnE__ +w'A05~I:<9wv\YRņ6dT֒ ǧeIR#^?J/$&UaG%dag^A':݆Z9TX9ҐHl3s +%L[bY62=I//&rl/r ]2[kqF +FZA˶^"BWHCeM;j"(:X1GJ VNkeCKO^<>D «nQܪl)SGJ,qv 5W>:jsoA{7'@/z\]#¼Uw..MUH*i >Xbo.ءj<(M;pLWd{wz%NxY_ϭ1 .PvмmY!ox=ȶ 4?Ž,6*Y.=V@m'I)Dn[H(=wo.."BF5dXzesޣ-ⶮhaBq p;|^pӄr GY aLCHɈKէ1S.,Z^^{8il 5yط1آkOߘ`aN<T|, jks>}Eyvm3gr}:F%Spm+DGh5Qjv{FvZs^;m[WON,zoqҴ{~ZO+h./A1/vH L4VN-#mL]?>-Гa#^ai{<2>ORCtdAI;ӉZ,W6{%⶷KSYضVEe-5%ۚs\JUh洺 +u!ظ|s!;<6%|3hrt['SANM^,Wsʝ<6uڭOy"ty\?;0,Q~Z"1jpEYC:,H'+.iP5+mw8ylD+نfu{" g1yR`^Ly#-JCm@>Oq[҉;dq0\m #ڸs>^h qͶY,ɐD/'싳=ʥt@D"++O(eu=o^7st;@Y.pG{^gɄq}=^ 0|<|o콦{ CAwjǬmܵ2c<11zy]Rrǒ#ׁ?z$33d_Nwe}2I +""#{\<=!u>xٲ[q<(F#fbSB"oZ>yi`fO{ڿٌ<6C&AvHrczʖ_vscϵK>E-G5j]o$TAܕ@*釪؛dUW MZ7p0`LBTh]j.tQR4xݛ+Fǝ[nRRe.M#i=qd@U]pC|k)cR +)v?V)2Vo" |D+H'jT$`!j kᏻK ;VuO枀Zڷ,d H{MiVѰ GfΉp~ʇFmLCTS>lf,L(5MhS0G(e+U,3y@azJW8Nv"k+H4*LYHT߄ZYo^cKnIt7*HaT7s8L'|o]tm_Kj1@lsvM{D=A>6N dwWLLmdK"H<5たIŊu1Hz A])7!g5I-kcrW`22n؁kWp$x|⮶FUoFW1V +QBy^6HhxI$':q3\bÕeT=fˋ3p?VD>}U9[^\V`RBwd`Za 'LŐ~fQYaҲU#.9+yL|J9E؊%=`Nr3͚b:$i_"F\96áp׉0|jk)sGxM=7͑siAmXm$ + :`S +dߋVo4mnݡ>p褤/tITIȳ)I1dn}aݭ&сZTF>'_ƶu~O?줹g'움VpN7ԭ~ /X6 ZtSpxj+Pk8~?:T9ԙo鞣co JjVOjH #iy54C`ŚX HvNwMzu*`bp$@Kw.M&έg\hpޝͳ uw@5TbD +4-Eb1-OkRk8gE|\Rdm ~;<>\[1A+^ñF37v_ZSЏrYuL./~>mHeQOyrk!üZ6S*WN;JA";&dڹtfaJBH;n**T + bҵfdi)>]{SJ`o~YDalnh)v)24EBiY_v*<[e!a&AwokY(?)6;8`΀}tD'Dn1[Lp'x:.\zڑMoK4-ԇy +ʀrj>J4]bL_Gc~ٺO>OL8l-;/E+5NNlݷ\gs_1ާepKԴx)|\s Q~i_ӛ^5PqũVPpC; _EyrsӉC}RY˦N{s%hi͜wXm##9ƇH=ñ)C'ݪKɯĈ[{&EA"+7/x&B^M +} m랆vnvi3)4Vb.6*a{>Ɣgk0kQrZ٢ Bsl&!_%} '.@fݒceTL0?{O[H9`c).gX1mW\RkxYٴHu1ArTЂ3=eٌ!pѡbrQsgf$+5IfP7P#*G nYx +hz"$XaMly-.PP#ǺDD} ,YN|N7:ƛ8w,M$J+"/˫2#KhZ%Ʃ2}QE21"$Vcy9tO:)gtU'y&80}uOyu8<*w m82$u*U@kQh24w@mՂ [l{Y{5pZáCa=)k;Cn,n27+M)o&Waܟ۾Y?"LwN1_Ո6LC[CG5E1(ڢR;u}tJn A49KڳUL(k2:*h;4wN{vNEIn(q~l[Y=wxKN]b&V"m@0zjҲmF%sЙ+pM*[`v[e +ҴbO }8ٍf="p.69evize4Kf:"~>%tŅafF=+J9&iATG{W0Y^O\ڀ,*1ݎpI%ɝ^+"+ՠ[D!Mp}mIϓS5t/Yyc'ӆ -rӪ,OAu5!&yBj4ۏ2dL̟ ,k69r/\&gy%-ϱO͖߱pmV{F҂>f/U8Q߇-Xt֍ 誴|RO0Kf^jZuƝ1[2!R 2`w7L"mZ٪9!Vzӊe'N;OWG bboZ8 XQzaMuwU)X wki+fKAc~d.G+PTdtS\ll_)nv5+5"Yu jfi^9'C۸:*nfeGA)&fa*I*b#K, .]7W=xUs8q:}u/ CII祿f%ϊ]A؃9'R|@VX[Ī7kI|ſxl$6O'*#nkxd_6aG/0&t[1'U[gÔI? AG M:VKgl:^e0ﻤ|~ٺz7(Z<@ ONOxXd"̂@3(yŹbfv1$aں3=8ߖ{>oDg AMTf9#:}=I7<6Ꮅ$I3{Ux;inn}LI/ɒML"VO>f+C5{Q^{ _n >o<>)RX4]1jJ/Da5솢(M8M~&,M"Wdz;S=&-q7'ٶ9A#IY1[^%ЇUtDۖWSyL2=5uJG.ip 1BO*#;qh0Ub0  Qck)P^3uWne6e`l5\ޏ;~F=;uCq&Uaitq_f>6}wېDc$pӒ}!Z.~O[\_կОU^lc]a6ñ!6RR;leVN6ʯFa9e]]?$F35%E}y> LȄm>b(5t^ fvrij?ӽhŚbE"F'*_iNG[D|jśÄyMI|Lx8["@,Mfzh9 ?u~3 +%RVr1^.I8)YFFu9:%,zp* s I$(νǞmdtjmJyF$U34,VK٫¨U+~alwlwR'MXϾ_f1sn^775|ڦ LӒ>gT/D8M9kpJu_!-Y⣕_)s4ݍ]6 wv!^-K!QSؿ|h;'Igf81324S U +dG?M*Wt"} o]plQ\MlZuGR[ Nsm`y%M||kοmy:@ f 8c$' 25v2Ԕ[־fYIxÆd+- QP+}_? >.S.R2|@!tڟvLS8_uO<@<| cm&L|x%w:q3bΫ9xqSD T +pǓzSX"~cƠ B| +:i]pO.pӌVatp6U("_N>S_{hz2;Ld=WM,4ZyH6LUV/C}-=AS׵/,3/L}^K,Ub#[`D;֤n]O-;n1N6ep3zj/XHo7i62 +ux,+[/l+u2z";e \g*"F'յ +uӮ>*=T;ߦ~s8fiKdwv&12ѶE1BW8WB6c.%~/j Xҡ@h5$2-leG)f\VjƎw٧9ss:"͒#+NC$V{+f|Ony O^Kb:ut0ZɉVIVi$S>0؜cE9f1r @ߧJp%R2k\Qy~a>+QF֠p6urA JbycڿjKK33ʥ pѮ$cGP >$; A!5#vG[ j>K'&R}FYRJ&o9@%hObH\cXh<[ J;÷qr׆(9Pwlyun^R$N +Dk=W2|-nr8iEw1ItT$"~~kY<)!:[%%6E O[R9ţ5M3ZӳJY&ٻ +}P9cYc̞gҫFew'rP^!Un’>C2ׄY>YWhc#k`,V/HYM{ +Ču˰YhIL˝u]UF$cq-Hc߇&Q +XsτZkkڌN"lCfM5ZܠBy h{~b pQ9=^KhL{6R );c",N^ r[9EynK@YAOy (}Dx!oЦg /ݼ/ Xeh(kB&[\./8m|ί#Ԁ'+z,Zf +UzJ3ti7G7La\~ A{"*f+;HF{_{X 5'kd4SAc}8,w͹סN%Cp!hHtE^vZp 9C"bYy-C*#8aWI鳗gfIw|_ + N)U]ܧ5E|c1_(_c{ξRe,H֝MCi'azP5 Z m NZs2\izCOmr-CE{)Vo%K_)R! ^lWD\0O:V\*l4jWm0[L=&IOX-^6kN'˦)fe~ +GmkۦSQFe9Um.Tc[`]p[q0|A3 +[`eǚYYP&s4!#s?R`^Q-U8)QGUh`6 I1mA 8҆drz%Ddл\uwCh. ksYB/XIN%*ͤc:4NM"e241}C`c;&9ᘺ*qy UO¿28k,# Ǐw&6l:ȲCEҜV5>orOy&hBƹ# ϹA8bI7x~ 1V o#vv">vMύg  M=^ z*0R;txn +;ܻe^RA{CnłS85wh Z/'fv-B ɡ2:FpuDjײa(e?XD qT=i׀B(GiE#& Do٠6~<~Q v;oI+8(/s,ɍs@D#C?&kFzڋ v-%NlܼƹGP!bH2-WÕSE΢l>uhwg}nq~^k+إf9 5S@&Cq!vRe%tSjªW+jX3i6xҮoV#ӲqJLCqxS9Q5)3f7ېdN$w=<4,dO2cX.hb.& Vf>jsQiPPH[wOXRA9I[ޗ0sv^ +,6dqP|[Ran&@RWNlG~p㹩ͧzV1!:ăs3ѧ0L;˸ws_l"}75=iarcϳǟ}_;(H>'ݐ1s+;5).y!僉+O$:vlف߃kMvCϑmlݾd >3CjyLV4 N8 i ԲR}T{/l/mKB@ӣodff帏-q!cz+hQO 2lZ.8Sf@MI}Uڛ᷎E^Ns2ܘ +꓁o.>[Qnj##H7ȿ6N;#_'3u 80dI#r`LF[:^ݍ!MIf%>HBߕ0leJ\lhI$IuWÒk dFI0?idgI,oJ}zpCD0xt#v܇Fc V0\4-VDDΎitą@a4gNڀFh?`rt +4k9=հ(/IS1 :,X}N8a4ӏnt +˺U"Ac0PڤtzA}`+C(P#@lbsѧI̹{Ǫ.7qǂ:}9yU8 U޽L8zN3h* +ֲckpd&XLp^|3VlXEyQ@zU3-_߇8(U + #|*} XT-l*I#S1%3G%0CZK)q;WaN#Ģּiݚ׫ e]Ȩח ]{*AC B1`#pwg}R{ӽmNy{r:BLFe Q(rF+#fmǩm/H$QFvY}wp8z}_ _78#wl[]OBf>_8fׄ}$H*>/p n*=Dv̫n\/,YO@28^d 6Dy"PH(bt6*SRk#@:0\8߫ñaSXƐ׭g(sXʻšvX2I݄j5QA#A[q!mcoŷlmAPިf"^jIͶy}a4!$ a*-`+\J85idL; 2tt7NN|ߥ&J;MGp#]ZM^С ^2u=%/7t*hZ"Q- qMڔ {# +VǏm#lnn._իpLlou-"i+^31SЌ6U RZX^T壉 BUf*Qv2rǤcb֤s<+\5kv׿c(!aaKaA;7yƌ1k,A~&9˩~ TTӖ+vÞ\jP'k̇"cݗm{*ζldJPLUUou ƶ7qVP'On~͍Z2mП5ёoy̓Ѝ{U:7#8YQÇgmm۷; xohtǷ\'cDGgbSmQ>&ݫ?_h?V rS^nq8 >ODI[ Xb@"Vإ~jphHbtv,!.?X>8d:₤ӟa{)0k2 y@nEc29Fkn +*-e|P '>Ws yx~9CxPq4-'j[jJW ?N?,̖3pIx_HNu &ǻsǃk +wc|Sbk˱F++la/+W 3o׃#+ְ1аVup8;w<ģ>g"@gC5eoKl/ H <:pD;DD8H'm2nJzNtE{_~?O_(~}^8lHUVw@p~4:-prioDu) YV8^y ›'ydsmEi+%mOoU n0YOKj)4*F:Ve,"D(XQ{DZK24 i'Ѥ_cg~Iޮ*љuKڌCC+ Ө,IrMWs(u2L>"2AhEA,fZwHon]qd-<^VʍvlQ~:iщJfɗ$E(9L,6l0ڗpݿ"63x$I Qsrf{t{7NQ&z *LⵢFY6V5l>&1W-0D' +B3SLO15_UOfNH^FbL/y#к^kus2[`̎a(4PjrA-Gx:ē@ZA+Lcp1X{a@qu_-+<kkjx"n&X U +T^2Y>dXk[G!eܕvP+ zbULk}Q ĉc۰VgSu]0wd-۠u4xs(k*] `7 XκI09W+/p㿦v9ku"Zҝ_n TqkUL;EQs##BNZl-Q>uj/Њj.貏.$1 + 5V#;ԋy?.J{MYg=aRfZO;x@,XomgN2+bT?rը7$GǭzDv6"fDۏ?[o=<[;P}x2{p ]'[GgM $?o |(xm1ZYjtг9 x@#i+k V@d&/z5 |u'7"ΚS8I`PڌA/_y7&zm^H.S 3νӀ2Q&w(K@TI:?qwQqp>"[~&c2ٶLsZo@^]X ?fSGG4 r= ctYtJ*="ߍ"mӌzW;,1r2hWNMSmcS\޴3LӢ6\ڷk,ah 1=߾uG1:"gs h l)@muu^(Gb#&u@{T 5Y Ö fݴҦy&xqJ3_S/ˑ^Π$xm>ߚ p|B﷏RǤ5s֭̓pM 1R<h򞿫¿)k'lraoI)t;V1QI!i.?fi3/ C&v#ú04JY)jzAٓYCRJLsnv-B2I#I{g.}cjj4a2|1SB8^f*j]=+ĩPxI,O(wpfr1[.U4=Єv00h +l%IBՎ rD+`%c@0_ec5/&KDwRA:-#Y{.XQؗY#x)4FL34{sUdlΉܑqHӋ5gUˊmܠѓ{LspD`'j"N9i#^ٕm5'u Qf +Lzw$·|*]hS +&CECj80/#i-i`2b|8YKkSn؂3¤g:I%` ܲjcYlr]ֱ,xI3~~]orlӼh%`.ޘV\=ooI)U;;[di81iv:2^C'YL>9Na@+~f9zA+,j#[9#a&HWB`3\ήT.=o(I1uJ;<4fbz?Bz aXlx3 /T7s8]c]A^e[H]m`I/汼#ߗ/ƛt6[y?Fzu(k,c6ӳw|,K79+gwGU9^x߭f8pcZj;Q̾ ^A#K5KVvqԞ:sX~#_RT:xضqװ LXa婙J%>׮s4n)3ѫhyo_UN==P^-LLwcڨ9MՊ\mI<y" |"~tpSJmo\*3l;waʓ”tf݌@4l@j&]\;&U=6mG)"I<`$L@1?ɞ)Mfz/of3ܲͩmbUN1BGzSqZj_=5J~U|X>3׬4tk9v4mǤ(? +M pεHAJaxٰM|}E_Xh, + S>l16~nUOcl|d +]8@_OάN`֍kܰB\ᶘ￯J P3%6eԶ:Om0Kۖy0TG86ma>SpáS +mJN;ݬC MrEg8Q%6v~밆~~^%O>|{5$R`ag{RmG]ښa2"e/s敇\Ss/3W# Q4T]ԣ'V| +Lo+U5rXw'b.qOF6 wY^#UD}$68-Gt\7Z[*0C(||B#P2 KQ$甞]<];"fJBQ22rO@Z">  TR[حe^ vz䋂n4R7$]E,F9T{ZEQsDAJvkB&?z#z gĥA9FmRa9&"Ηi3v>rA=Re}VW_)fٷAkw6=_t@q +87DN?-{I~RcD`T4vU`ں&28we1j?-іX:Oˉ.誙E}ּ0*ޫ. o(}]jJ3sc,^W}w&uvy9*lLf4@+[cnthl^cCtݴbi,=ɪ8sxcZ6('B$&:N u{(nmN}q|;yw^C +)]j[!PmozZÛ.̓/I}υeRr;_ <dzCæCn](P9dy^D 9T|F/mn #@ P1G>j\}aٖm0촇"-{(D4]\XW Pa %;騄[؊@;eUY>hMaNGr -#qmZ?" HcbS QA@h+6]Ǹ>r>BiF9,+lguwR#Y +λɟJct(ֶ_SU $,lim pKxtaq9@c\vB. >8FO҉s~;4rE4N`4 L.,Rd"MA#0"bKZNMw8tc}ʆ 3qG{{~b_nķBO&^":BKI_r`}FRj.Dzu{SI?_j{ʡwX]Qz!]Hd)JO4KD]9&"[&;ߓk]jh$iӬK…=J1]Aռlʤ`xkˬDw9^9(.@9Vs\6vA,xDm`wV5IYyE9e0csVePƼdd٧7~q1 Z+hˋ@)'4y]11-Bk$H5 m^nbB)Y6=FA ȁ8FG,`#^5&l.W|VStьAb;îBXb{qS977Zό5~e E(䤹_C{]e)u z[d.c0ch bHsw WyH[؈^jw7zꛛ)4^/smk7qsBpw * ^gk]PF0_;|^ݏqvF_\(9_(w:h t(nZ(H:'o?tpa`lPFKK]CdHuT--֝uנSk"[A,mVddo$m }Om%ר ^˶|o;%%Ky`51'6AevOU#zNPOnBl΃7~mppsbS/uxs\ߕJ);dm 0nlN7 I鳊`ͮ unuǶSُkub/_NGP3:op BDH tǨ!EVvOwuQa(`pd e'9#J;oib +E׳8+ L^N 6>?BR==Srt!?FziR #:ꄷfa [b9aCs#,xv'jWǭfɏϺ9{T֌YWtDS\L;qi\jMY烈=]vq,`2Cs럟_q7Fpo:T̖^G+GVeUO8O]<. qC:ff%5̹ĵXY- B/{i1W,%C㼣,OWG3̧ㄟ_Oobդ0RQNSZkfwoR!)N՝IUJ?Hw.WA_hǽ?HHN.S2;|?CFq~DAA1O߿{&e֛"A^Pn`Y7 xi7NU=~I /͌]Q۫}pga߭5k/t@xc? i_#6,Ӷz}!X̍;hesh8}/Щ'FHO#]b9#RsuAe" b]8{HU=U^NX֬t? jͺN|d+ p&A.xR^Х9)|i)^]vt+4 ~'݅f-gd kZ|ƶ&n+h"5.Qp[v,PY2x&mJY@!Brm# +kzZ)sk)lNKWL0?q. +xT>AT"BA#+ +"ZE=8Q3Q|yCTg;gmTHViFR'V49QmtW~YҤ7}׆4p>'}DZı$- ^B>2MwDk!qBRd0_[pY&lPw=4>uiC5)3c!&XןkFMǥ\pžSx7W\L M?[Pϟ6c#4eB (`]{g|OqXWb)mrHևERUO\# ̠ZA*]`r(.gM̎+NIr*1|x-'R<$F +h[M#0/My$7#Atߧ: m< %?YVֵaJC4-p +ks!Ba%jmbŒЦ@!רW +k V|B-uYך>2kܨe+{O 19LsUˢ@S3:aGOXSHjfh'<[]Qn1wumNi3 +ePz"ls0ah ;b~[Ͳ!n#ϪJ-,_w8L8r{ +IQdk5~@>Jz-E:|5xR +VsaL'^C|{~T=3.&`k7:;zϠ{Wj_D&lX8 +)Լ' +CE-'/kM<|>UJxF/L3Gڧw%Pkk[fw7Bß2Pt 6:~w9bZgP>qA]p/JmsC` [M -usV㽬ZIC䦰r2vǴoj۩%/O4ufO%"Ow@CuNrܘי_2h{TgnGyj&EzT+E;Ȋaq3(8v~/2d!@C$ uJayAv.2zSdZlt.O9~^?{R+֣yaB)P.E.~siAwBkMD +W %:{w4(v9C"kWGҦfnN<ʩ}:(,=v;9|FLe‚{O(auKMb="4pMKX! v}'ʳ da} Zolrj˖VBʧ>ɎcN g*op& qtq-Ov>0Jh6 +˚k̎wt(5еZR}6)~2JXj_iQ%֪bXT{-C+k>YmO 3MBeYH;\uu?7d~u$a*.u i4Faͱk=pHhz?C;TWLhq+ኂ6'ZaҊ#~+ !!v¿;gNl}m75=y$XG櫴cͻ) MY=7FS[Sx:ݙc߆R"=yrsb⫊u6p%qo;GwY竃δqr9B-B+ @ى{W!}g̭Z/+\ulhJ7Bg /,ZўN|?9.l/x~N=>IjkC::Puð^1Qog.Sڬj%bbNlwWɽbDm{HFKWZ0CjgkKmM}7HB`rcvqjojө[3-ۧT}b9`rma,Z8FP5g$)@BnJt[/}?I5=p~ʃ9YS3 Jz}-)kïGؘ:DEҷ5B9IG}z5zcOkaYWF#t_fKQόO[M^[*javs"uB֍]oFpf{*r;+@,SQ͔uJG/Rg!x +-ӣҪ eKn&үXnuOgњ{44_B +GKtZ)?䯱ݔ$ M73ΟoeYKiTج̑N%C$XzH +ur8>R0o1aY[;g:[b]@L-){64. Ё )`mlL )j8QGp c*&Ii qXrF\Y L| ${wbт8wXTVŲ5 ~ZbT3J1t^mi8Y#/BFm-HBC}mڢ6٩mc6׳ `Ϩa#U- i*-E}$abm#掩R6|WyO w2JM hP$9 xw@r j[052 pf$^F6AN (*gbIRv )@>.Z[n;1 ]>r唲>~a IzL:Ay,@> "(v٫t/ۭ,qFY2d,'GBSk\*-A +Qr3'tJͩ]K@ +yf\8-3PS$998K3b떍ŷP`~" _:l~6saYܸzN(Kod>S.q[c ¡ZtmX + ?^_hvӋQg).m>KĐ!aY-0 +/%wQk2EHm +e8ܭirЁA +AB_*b`{ځJ?W Bs$^ŐttBu eDSbQ4@ι=c% J4WLɊ!hKJt>{#-dw9B'*3 eH2 )0|e¾5M`oT:s:W1¢{Al_zm⟿BE̊݃-ot +Pq5ßB(T GgRNQJ͝]6 'Uq+0 sF胴^L}PdA!&X\£F\ u/{7Sid95zYs ++:"zH͑cf6l7cs`׷_~6Mxƛ4lx\icqqueT`j4LQʖeb6#HU3Z2hl'=咘G5B\SKU)-<3HNJ<G%sSc4>XTSsqi4x|Q;2%(vHM1S]F{8Ӑ"VǪ]?TtW*m7|_q>HR_Xȥ AqrYˢ~׳fd)os[}Սs*-o6^og0-x lTH3ϰ$Շx#2~~~3 ﮰ!_X<#}U2]gs0!89-m97{˞^wK|H-;&+9EVGFҲ@gD ᪈TCk =06zx`ܹ@ +}8J/i^t9i`W-ٿZk5uڮcsZJG@ض˄8i2q섘WggRcEm#z,%V}-A ,9AobS f\y6^]mmFu/2;),LG ][7:s?<@=|K $qfypVIUBNؔ3hkKn +W^0iBE[/,{9gP _~Pݯc؞k˜N,\WV5Q=ر(3]Wp\x+ZΨǀg( I=1` +vR^U`^|P}1 ;2ޕ.HRB4/CaGܿFAGA>-TD&bJ12eتh~hn>C]I1 cq4w.Vي դk gzFMMe?{1nj,qKc 2Ӧz}|jz9Dٹ^)ʹ3hIsrxP iZ͔˙xTk97"62DwfZtA'ݽ$fZd2;qWέOL 3g('~NJ-/L6 ǭν=_(>Ņ Mot|!a,}j U:>(o"D4E5xCC=Vq{i_ Ziޙܷ1I*Un.|]cˆ bj pOO(Χ'&N9*$nryΛvz7?jr R5f6-xH`BȖ""O8t#Mk(ŞAEղD簗7j^YA"Ss7˨LT_8 +ZÍeDFς&ԾqbZ\ʲL,3 [tGjqT'`X#3f 7z)Zu&\$;a٨SW|`UB9< 2Rmv{b`(d&FtzFt>teU_&N+1]/z`C kW\zF_ptPciu ܻ(jN \ʎ5RZ2kCh'a_x, wo7qcʟʲ*Hz: W$kFGO!uA4 veu9T}vj>=:mD(H՘׾*GX2N\Χ"p"^FiAT29=CEo< 0jź H,3WTbdu7U}`+esw3 hS\s89Wf8$% lZa|gNm|i쵉Ι)np@2~VQqKz hßЬT95;C)vldXb1kUr;*s_b݈.ؤ:;~QIH-HRa~:He +זn8Rć2pRĥ0|Ցq>[Ơט|yއP[Ffg灳㻝xY9`4~Yv,^Rپ-*묿T.VoqFLܨ!Rz:9b"Jˊq +:=5W51f7F(]a]]ɼBO5mT]mChZ=@yN'͈C: +םt]I͕"ãiGWA3o^ A|ݪ#ظQ^nɍ:݊7sOw7OvdGLhTtSP*{J:M~#i"aѰbt~yTq\k!Vh..VQy Z37atcفmʆ3]|弯>1wAJDη}ߘjC/ tfBk0Z >$#,ϼĮ?収wG[=\+꺊I=?Ú耭r\i #%:~ +JdW)Z֐% 8u՘{!oRº [9'zi^L 5r0RL>MOC !/8`Y&#w;ۨt==Sۡ'뗴됹T-cHsCY2WmՔJUdcU(@n),wLUiPD:q;;9#ti/augrMu8T]YIHC'!xWFvLN{ي}4TtɾLa˴A pm]\tC9³/jE +3W}(gvk%<à$9Si>t~|}J^fHM#K$n1CYmB i|TmX-=ISUqhZd1L9{WXf֬x{=^l/f k3tPݦPKV^c#ɓ6Ą ׏l<9fH3{a q[BWqs+XO-SKH,qey=uNDO4~&w/֍]󩅹-ۗҮͨ lF%1Xo[ʬ'E٢yBoZ~Y618 k!IOsyuVcmHlOx,7{EAWٲ\,KB?[/K@!y٘¥{dްNmBU\hԧ C2KRq:LJs"}b?ɮ + (2ÙPMT~;WNdDm, M;|!vzyl9?3w"zEhP1%p) +4mKf=aՅOqoG.NlҏT髋/_msee +hD"-Ex1/E骚ӌDklYfSڷ^Է4YԒZ}5%EoA쳰^6%ul.^2n4y,d|ㆢՔpuk>&ź9 +c\9ɗX0̕sSa=?8yf[OZ7)L Y#_rpi1jv S\*X:fiX/H0a6+8ו}U 8FȘiK%GPؿ6([I_l.lŐ 8SztT66U;R?eG?t\/!=,ݬ ] ; +BQfj)r#Sz^Dhio U~ʏiMl_&g{*:R)M]SP7mCG|‚+5e@PCTxΰi|mh/0nO/*4`E-lS^HdFd}pon Jm+i3U`ll,ԥu`&] +ߺ71)0o%|zaЯC)4a^Ɗ0*o;iB_w_3 %"e$\U$sUoiCfX2˻\ks3RX5~(k__'0Z/Vh+(ăXbj*3prIPLjc +Ga8f=̨Se8~`{kqwCc#4&/TS\)vWؽgS'т ׾ǵdڥ$c/"F鷃&=y?g^؉ wN5hV1,؃ +=*Ga +VHsJ)E <"{Qe(g]^>RjΨP-lJbcVکY. sv<s0뻗Y`˦Jw3ox_+)=SgH<&jgN߯!X6)o|<4E;Dx9عWIetRޢrvm6M}śCv(/59̉sFOF%xs(ֺ!STTp2ivI*#e[Sg \e!P]]T-;ڤ_\x2ٍJ '1kst`Z3 r-;.-Ҫi]vG+v`KQӫk4 WZ|B ,ţ^[%FUؖUiI?! opqEɵl;Za0Xus + Zi0Uzas=&OI,.q",Q\J>yoD +?d<~OQ0 nnM"l4m@綶 h*w'I⁥Ihdv:IX3$eǚv 6l-CmЋ- }pLsPHkiG){ˋoq&Il`蛞WuN::&36<ڊe).1<}pnOy#tdMη/;^SOp4fpUMa +;j3ڌ듛J6=`#0ZL6/,4DCij)?, ŝy]xo:eYpџ1tSgZ;P?{/R!X W\ sU|N7%~*-WP38b7 fb<"F_H\8_RjKÑplHݚtc'6nADٱ!;4e.mN,#`P^#`VUOnbY|{{v¿>0uIR^7$ux1$jͲzZ!=p?%0Ѷ}a\>l-sxnLW/;g*ۺhpKH4.m>Y %#׸n>҆uN4"4q`#ua髜[|\Oߊb<06|n FMRTZh[S((\\ucJSUOY30c# b?XyKwTCzRY|NE@čyOЄ #nBz n yxnxQx@ha9F$^^:UM]4[S6]Y]KkQ5sds=Wkz%R37IgJx}[^=,$qR +A\ܴ619UV>u}Ks("x\b؍"xtd|45>7 #Sٶ (\ +&%mL84tRi'HBrT|N?R41bi3E4}⒧'M@w/Alْ#ApdQyqsX~1?{\wy5gZyAYIw i}~k&B@SKm +PXƾbْ׉m׵m,齓q Ʊ$L01Tia(d~y+璥t xKoh|;_RӒPJuقg]bRk ;v45R5W$1D"դ$rec)ģϮ<\ym?t 7l&4AQ Mz9RdSt @ts"R;_qq00w'y6er$ 'F2"9 ZNdP(7jH8|<6܇mNgu$̢`!+H8OW)K(BxJ:oD9Q2R};zx˼K2Tm"e)u ޑtz_/.9A@5 TG3BڢEƚ򈔰ypu `jۀC|Z? B2d=! ]7)^d%s;j:oL^?:AY/>eUs. KM2UN<@L|8_/G0lcmFAX~3 +gn=r6q=͓XFkw;o\~P,T9)8qϩlDi8SR}imrMdep4]?lA&S™mz ZjȽuI\$+D͒Kv|s$Tt۔,[}n tTy{`Sv+r$yu(!h(׌`ٹ$6E$ŠBXSpwB/:;Ą`ɿ_8Vχ ݪ: +A?-Px 9`;A}p2&LIc5 v5K4UG8m64krɾsi=d8t@fmᲙǒdk/$?j)G-#NRi@ j9DdPf[Żw7gb "+#lO!g,C6Zy"a! 70vp2|d^ffŮjf&(ݑ`YV3nQ86Lɦ$$ ;]D.rftn"Wc$VK#eҏz5rhm`ǶװMMӋ"jJK6 jomZ_`uo-ʈfu0,9A=' +]s_aW;Qi$GW:]H׈B̀# xJ/ًfW@1`loq ]e1RDFxuS(X}:\?m]z`ߟ\(h} D˫xL(c$FI['K!-uȇTB* !Y!*\2M]5o{:*<Y; #DM=݁$Dulw7󐰬fxfGG4/@ :fzZ/ vm&]߈C]ҷV,3aٓ:>\&<F`]iS&OlM~ -Am0rr\䅺X@`V[W`O2rqHfT:dY +3!6C+{8FpR*>fʥ_; w4c/Ӎ^q+.Sb gu\Y= AFt" U6dQށ-y'^nU7*O˻tb;vM>|Q];xvd +8G ~"\޾h'; +Nw6;l,r4gb=;Vե9:R;4 + Qvܫ™{|?!:\$3nU[VPfe qe*åG_к`l֞*8l)] O߉Tp*efuBq~7ʿy]w&K?yKOmsʽA*5JGB4-fnQz@KEX)d]:~kW9b0auUfp-NYN({Jr b<A2˶R*ːDϡYn&SL*cw}? b??ՁU&7>m0H- nogVCiɣ0臭BT97-y9Nۅ!ؓNAGԌ +>XىD D= sD-4LBZiQ'`PM| ;VlwZ#}F5h∄2#Ԥ7kF:O^y"ϰRG^d;i+c+C>*\H SS{5\5"VF-Tk/fbXT8Ub|v"P%MoV6j!lݙrmclѣz7f߀A8amGQՋ7:%~7B7 -m\G"=m!4n^sMi4SzFSأ4m8K:"Z9[c"I{q91fSŲ"LnsT֡>c'Ḏ4N2`ab%CY)c^̕vz +8>InH@."5чTDsY_6MS$qK:"8EA=tMgۣ?6bXjpۏV +𐤤3,&D}L#pA޾a+цQ00A{r?z5F'm 3t>G<7A5(i+i4o +EoQ5`^HryKCDp?4)h-f,iFԑ`PJ\:) #e{m+7` I0.'( +9Cu:ػ ~)Ᾰ #Oa# P~\c:;UCc5%u7{7WCjxXƶ)Wz}RH?ޛƿʍ1TjB x wI違Գfq>WMKʆC[h)R@ ҾjHxPZlp+*'f.*E솺mBUIV =}{ qJ Ս o?'?%U8= +3ˤ9THoӻZOHcs9l#*NOV=eU-T]sfU5+l4vяY0 5lx驣zQ"`bwZMOs^y^!'J5:Ҝk)h$7LCl,_sSvDF?r۽ ޤrKCysnfbѷ +`s +݌\Pwh +\Y2œ2j.\=N6o '* TBZBdfsaUN͢“*bLa9CTZe5֯CDUgع8N% AT5:V@Œ!H,oE~մU`g{9.H `Or}Dz!6v܍Y C{9 (rTGS ]#tB?|/$:fiGmRL/8a?HMM)H +_r$ss^v: '[W/p4 ~W;. + Ei`nuT1ݜժ]vr9!^ju0Lk[(5KHӾDx8sO/NEbma[cowv_?X%C2 Dzf1H䰒 dACi;|EP9;&\j_UOú.ˊ(Z`d1B>ItnfAr0#Z>yAZXcR4Ðf1/H*<ӧTxYeV$Š$ kmB[g +OJ6= Z$4\[p9q/L8[ɳGjaW-0YORvM7UuB@Vƭȶ<Fɂ;IVfӖ3 R@GI>~?l]bs0!,m HGAF ܂%*wx"ׂmVaL2Ńӣ;q[:^?7epiݍːX.LRՋض}X"-zeoο%=bx. "2ꬉ ޣ T-kyjbﴒK(o>mY|{zW_3PW*-ڌunst=t򕳐B{]}'{?["馆竟̀ rQ7tz)ݡ5 ټ-؎jz0ȁrhR$일2(zmKк֌vM\'"95*2L + s +(T*3#hqB`=7 Bz\ó)!kAe*ԷékGwog0/2fȻʮܐ3mkPIrZu{3tzXiez69LĜu'6TVj3ˤ==_6$UQOpVߴ$ynXww6<۾Qqs8㊙sjK,lmV{rRK<~ɨ;yLcs9IN߆1NSj`u=mv6`ԝyf^^=B4 u +4[6]q7Pn}7WbrztV`x#-=L[9֠EǷ9֛8=_lQl㦚_Jx~Ԑ.#b$bmjF&0AgLg ]B +L"T1ˏͅGM(uhlM< _;~BPQ;n੔*'%f1T@_{D ul {hT{ßY3T:NJu +=U]8a'.% eR0}zgkY!?mRP<쵮٢|{7ˬASCl-xY,̺ݶVPvLpiwuK==]84ѹs{K^ co> U*x5Ī[H9]PW5X>=} q:66Nt 4Dl+AOvfe1(x\o7 ׬ M?Q0ȅ*\*F/ӽ}NjZGVqǦ]||>-ٰBk=tXcn~ܨ5WK Iy^yV]Fxdu*ԈϤ#sz+[2=tGo`30[4nAެ㠑&6o!o .̜-N)'an_@@Fxuu rMucg֏q;6kװHc珊jLD0e``߰ʼбM)[oMW\8Yb=Zubrc ':KDy9q K\ĝ6 }:U?&*Ț;7Z& C]-wjz{<6Os*DMPtf9S[8Xb0Q+Vх@HntcR~[t;pţ*&g)VQns=jbzhiءيG5nB;A/%Hvг CYﮐ6w~|׷7h#iXF{%1̴@r؂MM H"zWid"v_wu'Ջt|ڪ-XPo`g: zV}p mo+;j;nĒyv=tj98[?N&Zjlw~Joa{+ +۝tu>R4èg++a£{)x|+&3ubzZAB],&X$\&*)0/N]=ݟ2Fs*.[E? +dS$]nL2َM.ZĠ5ˑŲ N cN;`adߙ"VuWC=p3.y݌~:Йa*={>Z9SFޤ*BݱS!;!2lv i:fo\Wlplco-d.3@T;/ [sTOc ? Kӡd0wgz~{Y0ǽR* +ƺG:Q_`,'M.BA+f}K'hqF%])cR,Bun|.МťGCس]hR^:gfBMZL=L۞B9%>u=@v[ڊȤtKɡekQnQ ](kax>Fdߟ?I2l\ge_@f’@RsV56^i!}6m6 T,(Hmĝ8gFN6lΧc) nOe2LT7YDM|Xn vıWU.k1|$mмL?*%9u=*<(,mdj3X=-gzqG-bO yԘ|Q&c؅x r +/m[5읾*oxAv#jqWVF, f)35[Y+g~9 +2j +y̝?c?3V;AWˌc&bO)Q'G"C`xs,-s'LH)tyS&MEV(Uz9ݱ< y;L䞖UÐaZv,3 J\ekۚ{vԵڍQ P/m 6L)9 oX+(kf>!e{g2]R3*50ݳ'1GrCݷlhClH#A j\~xzԸU?F<?dۗh<3Z'`7K"e 5X MyWMWj!aL̎ &]b 4[ZQՊTe26E@M{WW]_]lYZ Ìyș'5n֜PZx2$4=w +볦`(F{q׼(mΕEVTs01.gHAjykS\+Roq߷uC|SlŸ6mi^Wޔ +=Q56Bo@Q*ۺU{+&ݬhwEb-ګzZ-$zvZv|5,bRdy= 4;Xq5`n.iե8840gvuM}:}zM:VqA m8ݒ ZfکEԵԊ^ĭm\KN CmF|v5ϭ@Ӳ@ _:m<gHT=YV@5hOSD72!M"%Zk8[odPkxp3mg&J$ 9]nG\p-+kuDq-LՇ;2TQ+L XIoFCTeMWbRt ʵ՗+rNJ#N2^AOF:UY %Ȇ~|~l˭'eYxh1mu<5yW_{~bƉS,w2Pδc-#<*}U(=+R#;5v#b~m>}$4V $-^<鯽ȟ0Y5!yWu%ΪTdCsnBo=Wd`,RNl F9$NZa r}ypl{%'S[** ugbc+$݊8w'eg.IRT9.*9㸹8bLla  /^tFGD>7a4SoxPwsl 3i car3J|Ogjb?\6&tRhGEv^hOv';txwÔ+'pU|(oqzw8 I';kv`:*]6Ck4l{~m1M9% crjփf0(:#_ c*exgC-}}qKO \=xj11Y 8 \X gaJ/RRklӓ[yMV2riAϝuĐ0,x@ߧrH^K'N//ݦE?@׹Er9CfnZaҪ(Ѡ[9vIͬ2F>ۓd+RyfqYwGxȮ لFp̩k^fx4;R5Y)!1W}u5wǾ ZLO騛VWM|[Upa屍GKAC6qh%هȇ[o 荑sزוXdpb6 AjmZz +μe#sN@ g+9 \Poyix?{k3'}8njm);p*tQ1r B:4 7X^Nf6G,/ϘЏwN!S|XT,QT|8b'3 `\w_RpnjVzec1ꉃՓbχP +bp|}URX +"ڃmw^FzY3u~}܈s$qW7j ^PUgd+z-GoX$` ++㒁kxHwz 9k=:+1\3ƶ~ ԳT8zsy%BJW+UD?]ٚ9OQ?6k=/Qc{Dk( >|r-jf[H%:YY̴щ)ԶEe!o⯾o;jι۱P|SX0@t9~pK}i`|iϜUAvsTe^G~ 7!+S=wqÏNraIzt}IY( \x. DfXN CTR^u[ILAz}K~t`/GaxGe h/I qp׈2c42tiFM遑 KZNpfAF g0L7u\N +oGmV2QfsUGg&IO!m:yyRKخ^xBG2i +"0]Wn]9tlʈ#RG:zڻ~p@1ټD<*g9rQ;4w@Hvۏ4tGw w3CB{䒟:=5팩`- NZ|b: J9 +jtH1̛VQbBnǾW5gW+\aC4ZIY%-|1^ +{EnH %%|Jۄ53_tjĥk#'fz~V[_ /xS:emtdKS + Y:kFy@;5M~It`tbpV2Gc~7ipN፧eya{r^^si!A>r ~~;oðHR;%Cij^+K~ "wAD1!S;]|_^W["D*sLX_gUlh\ݶ}O\YMFpiWiT鹭(>*Xl5VҧYlt0FOAe'}kxWuEIkƥTX}gGڹj~cɕ~zh_OqrDTfת6Yyt_]M3ipݴ|.X%Ւ҅nWAwBY/Www0aPluFM<L 1DcД(Sޣ't\r7(tmFB6*o-V XYY{ +%&/׽=)`6u/b+ytx4Of##a'<& TdְYcŊ J%uy=>%nKyNKgWmv5ٖTV7i_2TAdw3#齫sֳ+l:pI=A>nkZ3 r஧>޵3Pq-QZC@:~e'|R}lAo^(qtE-2k +뤉Ȏ[e=\ }}~ +eXf +(xirḞgQ$ e5v-Li6Th3k;z;?zN͆sM%D[uJTK1 $NDNU׸,d!o]C'}sK濜' B 7ʜxxhᗃVͱJtՍ8nuYdX {єN=+,ՓÚ ?_?.$ç_2+~ƓCTkA]7w :~s:*9*vϚnY,WKg1F2t%C5jq#d||\"=њ{Bya? @lXz^T2{8꫷awYE8z#m|\lU9g0M%:L21OZJntOa1 0#KC!R<ve.u(OH<ں,[ FŭvK󏮓B[ +RiR,a3y eYJG$aҀO^LЂQs.mM3/ <.^&P!}DMj>wmg, mNm7Sj#"VD^AQSt͎p"Pv ]D|-^@ԗT'LZU5s+^hE>%3MF:;W:: a Yw I1pg~zY gC-4|N+-1Z׈j!xJN+gIg]R*Wa/҆p/|=Qsi"B"YvWú9.g~@q:6b5>ߞK.l h bfH+WzޓDBG~t<$ +W$\{VAˈ փAGI%L5,(Fڧl캛sn&C|jTu8=l1۔-jvv}l+AfX/1XD{h__@b` J*\@"UsFUkvFDpA*q7*s⋸)Ghb+0 £f0T j#/W.Vb Szҽ~0pU&poa3X[(bEkJLp|D^_dDL"UZvjouW ?FbjrZ^5D 4^@.{u.UPŴ}?ĭ?L[8jE =!>3Fx9UO[(\ë̋}l@Op.K%LYǟ5>enc~<~f655'5r)D۶,5@rI[Y +U7^Ku;-T[;!f*|‰b?fPnAV5,+`jAoxLb QW:u_'1\WcP^:B\"6M`5>Wk4gsFna״ʷ +;9.ҵl8NGG#EG5:]?)[Ne΁p,\/1U{G˺'ADHT6L]Ή |[)g2l h#[@;}6h&cT3;TVx"w i$npb72LOh!svWV!CqN c͸ۼxL_u%\J>e7ڹgN ~7a/ +- Z#}1hݵ+S^.ɭ:Uxr{[˓mI=)[v̓Kl 堫>&js,iu|s'0 hB7Nf{-^6c=m:*w?IAȓjKR#sYXgFENnQ{HW\Ú{Gl8 {mCmqP|ZA)ŲcR JudRwZShpαa&ˍ {ȲUt*N(j:u"޵TCui. ӂ3#ESr+0 +\>-=P0Hisj3V^3%EʫZ#}T+vHjl!,t\R$DE {.p +X(5)tc;̓%Cbb|6\Fr<]Vbm +"mCؤ'ʠ*ocYڦC_q mR)G=f$T/ՙvZwA En2p7١ Ian7:Kzm#Z喺=:hTi~.5g|IY+gj:?r}ۧRpÃ\d<_~:@T`IC +-gp=( +<y;kjTN<*:>|jIS,N[Wk|7{wюs=(x54h}Q7@wD $j],ᥛrZ33;[[_ +T$@WH_Gu%I [v|[DvH$M$'OEnX45rcFG*QRsO7w.[7fO֝ah5Lڮls-Ƿ(&VC5M6׺tœVAnom_׷TOBJ6_3cٴ..0T%PS-Cؚj#r+r%@alNm=ܴ94lX G>CU3qAY=sΔo ^ְ<`1XbfOhH}f;' |MxS'{|ldcFsHway `n"ù^E?=G Pz/ciz[Lz-}'d3]#ht+$R9w!^P8AS˾ړhCiK/( HCl2)A(PIQ )%_Lчk{OZW/StL 8>3>ÓEHV1ȹOqtල`qVĀQE}~ܚ*ߪgL4 Vw-HJt( +3#~Qjw?Ɍf.twlj}YP/t%8aQwza珛t>Q=H}Z;jJD[IA֒VTѸj3vSМb3(&Ih6yXRt-kɧİan욭x-jPIE 5y z"V)RjT _4KV☸ͥnOoZj{rfғ"ab,+s]KO71Z|;޻Oɵ"͜1#\fnKn'F2`}c={bvlԦd 4 ^5K6z+U= +D1t>gڬªBb̓WrFd!ن?i&2D7&w4`y}?.MX8nX M6KX U1~r1 \P$K3H0[}kY=ZUl2$(N/, +Ӿ}VQߎZ-Ф|2HaTKA6˄)#_6NU~]N>1&)ivK \)ֵx]QI-} '%9!'y]a*މeIէrH̯f+c_2D %~]rD񵪛)5!ep7RE@7Agʷۃm/Kn$Q|ZLȜXh㑻m%j[{\A s;eSR5`}}ߒLwB\ta `Ka9 9yK?گU&=l|^g,8XUR!R F=eL~xC%ޑX9Q3U>尠Zp$dtOU@8߂%o-Fe@,gfS'gD7<*|OhZFra~hsȏ[`Dhgv'- L:|fTlrf~#-2-7I|*Z'˕Y3lׁo,&Ј;Y1KSfD $SRcxS946 kJ4Ỗf634t6wJJIWKޯcbXnsgy- Ѽۋd"s]j} rZVݯ!MH/ +ݏf 9+Ū҈Dza[uo-5'c핚0l%HߪVcfhٜ}iY _=BS^.Y8{@! '*^c@c AԬ[h2h +"g3M둎NeG,7˰`K\rYov*RƗUtsLtQF*bSsWɺsg$tq3 ? E GU0 ,r,jX8q*(L]e,Ztpfiw?Fi \nR+*V*ͽضOC\ mFXՂ:nBv+}6tL[L!Tk@ڴ]P%ĖLag%"'ǦD[ն^Y$ +扥!5[t)K{eRe鄊YUmvm$wL3X|\*ᑦEc,#ө5zE`m >C1%m.PsCWm=QoFy=jꈣlPfwofaCDRUanMWZD2P UnQ5G9OD׬_l +ZMEixo4Xe$,Umb;w= VtOxk% y>ČE [YM{[J]L\5$8Q40@#~b[!PhF_q<ϒv%^}=e@OSjT}nEST@(Jl>2mcQ n>\a@ӭN3/#"a/p|`9e9k ο; az—/*@Yi!4J^uJ=4@K"%[c##+!l[(G:(<&i%TuZhctpY0' Y6DNH+䍁V ROAm=bj`B|MP'JiY J:]pB) pxuŲ>#WG3F/eRDN +3e|A|tG69ޡ!5oxZ5c*Wt:( D^pMP(4~%aj!NW?E'kdjۋ<;l;Z]ĜiD7n}A +&6~IQ]Aa@'+A .qJ9kh&ޣ&AhpmM33Ufde!&[evd-^]V-W>VMmTwN{Y+gpW;N lxQid'&{i˴us⯴c?xsR*QV#24ߴ +Zi/G9uE(T3>9历Ц`&]-nc(@͗} +TG?t{>&Ezv '9Ņ/tqX)+ת;pm8|nyGydqAtS$x>GI'mZYDaȌa̚)V7IiH@ n^UOR5vוV\g~:lwu< yD{tk/"Mw,j::P" 'KOwMf ϋv DOwX]86Ӛn+TQk,zdSF1ʙ!=sLqoGe74V䰉kFFiLM7qƉ&`VL0USjnw<#hK6vF֍py|5ŕIgLz UISz&)r,s6/}*1g7_`v dLjYڋY)R`VG3G\-egdDǺqamzVw{ +t?g5ikD7;@Nc/M/#{zbF[k8JBzh;2Eģx\ڙ_BhmJ9m*tiv:~ж s*,Z/{ԜɭFva,,_ܒ<'l^.yFPFk{*C_uۛۺtKpՀ}= +8-SIW4ȁMF 7IT-b4aYUV+ZsWԻ_ C +jH=k ZX;ǛzRՖS-zmM,\>(Tá-ж6ial)KެEڻ-I( _=5@(o\m_k-r1ꆙM6ez2i+ȖXUAEX 'w כ>e٩c25_TL OmiT,t TfΕOf6'_˜=`WxIr;7ȀuTޓHIA&w߮>7'H=8Ν!(`"l]Q,VL -u~> :f3{HM⑨92!]k$ӽ|_=.x\eUGjL:jYHےRP)zhuV `6SX35uQ4({LY?;W(kbM{e5kg'X>'Wp~!@6XR9o}jB7eY#y}T_8JN?Q*QVjکǾv؆-X%͛=ZR@]co[HBg#!WTdg1d%UmezHmEڧZDvWld13B>tBYc!G*{R+vh2< rX D3`!)\T~/ft3R!'ˬږ%L~ZDܐ,y$jӍ 8T{0J;h@c9J3}lwLn>wPk$jLQx6&o,M]"4!xv1\} wm7y \qYˮ4zN*qu[B"h퍮45-zULj=5.|9nK[$Jj8c4VϺ&Q wRJ5o曹tjvBÓK{X@FězPl; PP@L"kҙ@x[*-04tgҬii; #D* ôtʛJ;"ͮ1g&+H|ڰvIGc-Whz5 +J<&*Y촧leչ!ƾ139zP.jEE4lXh`]Ui-O\ن*̒@\kN;Zze0F]TM/8j2{tn+ǚI#QPہZmuZvh$LGdı؇^Tۃ%:U1,jS4vO G7DP';#\qLDXmcDfvK}B(y!hŭk[uO ~f#ښ[tD2<8E|OBRvɈQGV@ ʤihbd0yrG^IVABe]ejO|ʱMhs",P5 r%Wt `(<ܾ'D,~V~iKTG-yG`jF(]Q0wV,>ptCLPԴUEw@J w費~xI5ߒ)IBfGI:<5`DSݵ &jxXucHZ}ȻԷb~ߦk-f͑B-l$Uic啠 wEgդUv~bGgp<UWH)*um3hpij4tyEAi`EwHXPTׄ*@:똏P-)mcjC,cHû754{PPap($H;tX/I}.2$9l2nMV2#P7K#ՊZttZPbo1b+չKam*d?X֦9yeSEp*r2){BZ1vd/A4ҕ1u:M~IAhR g((WW؟5 +bےH;+jK˽[q~-U#ȕmix3Z=i2Š;̧]xXΊnRcXN>P q5KP3/ctMg'Lv=/ښCƤl@W[ϖbpܫiN IU6Oj,dEW\6ÑP%k/,k1tJit:٭Mk ;v|k{Xx3LPvOT̾2@9էf֛OxѪs19b5R݇# Z{k{=qv&I1y%4.)@N'/O'NN\J{A8eZIUF#M:ٓ>..M~$1j:`kpOM@'`Z/?::+o ^‡{;=RH *.kti4ZWt3X.2">uJ7/8L5ǁ-"<`uڭ6iU? +P^|rY8.U ޅiOEw\f[OdŌaiz̞dhƖWE͜Zݸv:r=С =oon}^fxʮTZ% +xQt(:i|>Pz:=ѧY +8r.st{tvꡋm:K;q뵖@(P VkNac*w9ܲQJtZu:*~T9:+za +崟>$#hYiYlݚ纮?Yztϖ>+bVpQa2v9w5Ph2GQAY j AEC≹&baŬ8wW\?n,׭_FV饃:N8dKv,ʃFhrpcֽƨɩ*IЩ +Pa vm>>j'"d 36|SDrE6!7}waMd%n g7E{&<˽We?&ej(1.6 +띓d$T߻ $J,I/|cowjAkÄc?_+ wHxIqVMks^p5oDܲx׽ẓνtyGwuV &F$a'W緆cw alS\>>mN@ 3b4g{5õqÅY=t]@a zY")ʼ t2r v 0^TqJB?JD避75Eڢը59GUv5§z%>lⅦk !<'K_9$uAX4-/:@ hۗ3'rUD$Mj;lHUQsa:{A6_/ӱ +̵w*}}lg&XKÇY5v*W뭤G e:md]NWNBH#ň."V\n<(i)8u4~̗ hyirp(a)C>uvV4%)D 3չi=Xϲ-V\?Lx^4r^;0:pO/yU,Gh/5MR5j}vfprVEZ8)ϛ^mR4_R]K"i==Qj&xEQFN_k[+ +5r`RL>D״.ɞJ{=;àu1n`Ihq ZʺQe/W vA[._Z@aua6GݻL! MJt=+%`ٕ%q6NĔ}%\-;Zv q&ަ~=JT-+hHnGL:o :y9~}?eJ^ Sy 0Z)ɩ_>(GGJ")3bs +Q +LSËJ;]fOiTJPoͱ`oߑ<(^pk*A@mmBi$mݎ75w] 4hb7Wc_%ɍ {B(g6uǟ(%mcd&p^fN@ӉP1 ˀy8WD47 !2p]L=VpiOgΈ @5ثJB@ *(nNj8w@ܧBuĖ`79&kI!ҌdHbڮ =Wsx33د;yg֌膳^g^rlM<}1|* wE3> Sv/D6 V)ol# _9UC%T>POC3ANyȩI']c~29T[8>PiY_ioD<Ɍ+߯=}νGZE*K YS/+rqX˳h9VlF ”2 %eo-m?HРާv('!@Re0.avKi& țwki`Ghԑ#+4R=k=ԫU"L=6"oK +XPY ɣ#p5&LjhJhG$6 +V`Oa[ 嘃zq]qLͨ Va7C7'&GiR(plY0zFLhțR.֬3\ZZLmDJPqt`r䑹`2tHK'#ıKy-M(yqC.,Ȩ{Ҵ&LntRuRbWsy_j߆γ2d^{?VNY3ux5ˌɻ햎D՛ó*X/<{Z{DoBr{oО2ہV/#_!w*1jH0D9K9+hJw-~>,;tJ<IÂvZY `+>Y#.+paqJ.%msV`d#>%&9!~2KPT#-R;o|Կ@26ѧKZ׾T"[lkpߡsl'8h3X9>asm.Qi=nSUfzu$bASj=g5O_e8+-Rؖ4\bkIki?o|l.~ߜT^ +IKeNgn27L"@Le-gLmfDIAt# ͳ +J~H`Ōܰ;* /U`$^SRݎmتʘr!x:*[)Bou\t.糎fZzʼw4zUoUJp&v_#Ξ#E| ,Um5w_Mbɤ)ӑmmG:$Z_.WO{Bˆ˶x0*Դ⹬,dy +m؃(NBei SM5wƀWbPrf4o~laD:6<`{ˋkT̪"1Ί ?e2:6jLeod^w|tsnrDŽ?w%*oطC)N4 +u1:|\ e{)ajW-v=tzniYG4zI.Ug6lm\WڵLQ3v ˿)y9=Цϟ#yP:̲n9T9U*pr$eN7\<.TK*e_tj@V]ZQ gqx5zv.y;9ViDž9իF .!{שgI}m 尩-5Ldٔ0~r sd6E˽o!R#ݩj*ECϤ7`w;d1ŋ4)8m\Z x} +zj, q!d5mqx te= Pur\<#Sa'OInHd97UHKڂ[=b(EXg*8A1$-7bw~77ҝv{j&CIJDzczPk<+ݽw +5`[C%zߪETcl|HPk )<~pG賜7UܬA~UcA u',BiM#sd:kWW7!Mq2$$C0=819MdXKWkE~-:R{f.zdxzz(SyN˷S!%Fr_ZQZ3,%Je-)%j k|: ,Ofb} +^JCZ.d&ZSvDqx_gpa:?<Ԟ5u}A,TC,:yv&Ef^t J9|8m>9o-xfkV_BўnIP%jeۍ/LLqUZ8‘!,$Q +Jy0ٝT0^y6sӉK :ʋm~bn&K +F'چ@aP#=<ח7;kB'7^9K"U1 ;׽όw*u5Oz!-p?.fe5OD$&6`ҊP\-5TG$N ?}wʰe}P3|4D2w⊝͡q:YE?xhDz+璱8. +in]o/yS|nnU)d@AGY[0B.`bN.sk LkDkaLk cUe2S?HԆ>E(E[aP6c]Y'#^sg^XiGqL9,2x׶5O8v&,+|$eH*iTaB<cce<F%c"ȵZ{bjOv}a{ tF[f.1Bq핐MGT"Mi(445@ +&ovRt0Zϔ3$؍d.jn&F[,ZWJk:Ʉ5KqeE+')P(|(- Az0UMOF1l;`+Kll $ KKe~l#_+3ʱ/[Pܴ)C?ٚ˪et~1;##p{3J0`v8{b3^CHݥ 10[r jLW#d8yYÞ(C$4V)E@;i0us7O^ L}o +O_jS=)^oMERyX@~y4S~s_;! _Od'ͥGwiG6Zs\&yd@-2Myi4f[Jc$lV蒽LU_U9{EhKg-)?ݏ,]ؘK#YRĎ&ͣp;FW[|&«bQ|v bh.v5J@rD"נ>m@5A?ڰTKW]PxȈ,R]p9t?rd\\i/\fdCNI$ǖ:Ż_|"EY`Q^nmJ*;(4y -*f?-H6TUIheKs`Fɖ~yU΁f*;dQ4Ŝ ]~m8[*z*0FjOhH@םOvg ++t9zO3Tnue?l޻jX7sb'f!yTRpc^wWE"6[Z9,z許Hè]N*Dbс׏qzz0Ɲm4Xxs`ٜ܃NÞ~L F;Y5y<ՙ8:l& 03#nf`.#btC-hVSś"7mndL)HL X&cy—WvrL֓kߺ79lUJ5=<К$a\tI?\ +^L0HI>)VwƵ@]`ɴۛVhU & <[=&yʂ*} %f#[ae<uVZ12Iѭځik`.CIn3EsS?r%xW˾ُkxtbji~c銐2UFd; o-5eYnl;͉s 30p=`1 ӢcnXV}cN!kK'kHֱ!1a Gk5Bǖ}HIݦHZj0=%-[=uNX=kݧבho#D 0rFi\Ko_FJb׈4_nw'>Y0Zxzh84)SN}x+$:-ЃMu&{ŝI4:p4Ko_٢C |0kNه:S5ϵ u(0I(\0zt.1O s܎ʞK>'k>tzP"*v +c=)a(u6qHlDF`Dr"?cS3zo~rThavj +另H=ԩ=Qm v蕍 ElcJn*un` lN @_W50rB4r֜`S%Uu%bO1>xVV $Sg&tDshol_h 3B{d6ϝ w󭶝=)$SKuGԜƱI$d wĢ l)]ׄ`[$cp^m*p=;2EnFLNӔ܍4'oxfyV3erLC?#Ӧo)?H` p8SRfLT` |&e.. `#-a9|E(iݬzج*,&Gٓ'TP.lÔC("wN!~ގz dg l0 cmooښK}yL)ɵQAUGI$/q#b}ʟz1UN~_[5c:;Mý'm̟H/f+,릆fs5 +N9 DvfJ뷊w8W?>⬜7:I&ab0 IW%([Sh)NVzSvCTg)v܈ZZw@׽O#6J\T˚83Jy?g\{+T]2|>=?޺H[v=FI07?ٓsd|Lni+-$PeDPw6\>f,9Ň.Mk忴2tmKаMdxsǩgccL0+s3t9QISݽJ6tsxʉĄ|skquug03V#ֈms4"5o]l7JOCt:c3_}X[̻Aߧe6c=( ܚ&_:+|noDٞ՘s̶jTt4Re P<(wmt0A?euE&HTn\Qo1]ԋBܫ˄qtR! +xF +wOYj8)G@bC ,uTC~@)U١% Vߞ* L%fipO> ǭ9և4&S^.I8)j#;Y-eִYoP9?'ǟZ9뉆]L߸H׿ +Țj#FN[|H̶P|p{fsTk޷!cJvG<ŎwKFfPJ즙(ji5{VX$q9q&mOʠC_'P )~6AoKtied0_ +x6;`N"1sOrv'N)jwSYjU1 +d:*V1ݣ_w;Mҏ IcJck۴l}NWS3dV\m۴)Ee7k4{*!;%w*m.De,@6eKd?+v~[Uϵtb0¥Q]Udf0h]/OvԷ&1>t0jܖt~}j#K1E]{Wa67ׂ(Wfv7W9B"+lV UO^xzd>^q/1KXi^~3EVDBkӯÔ|K/)?P9{SHר3JZkٜ4.qj)RgU9LםR^0ycU|m*捦 +҃UޓmuqX[xT]*0@DxuBg Sa ==ݡlYd!:v)R){=ǻ4;z&?jWCmGHp\<t. i<]d7}{9jsK #lխ~VjQesscj>c +-~qoX}? @c%y?C7Al{[ 2DP5DLj*1)t&I%OEUK%/D4a"4\]vxA6Cˢ-AME9"Юtt鍶@t&C.fA؊lKC8WGI;RuӋ\x}E+6|1stªX?pcmXe ̫^ C}t5d#uf\P1~Yz2VVS|S'Re,s?}p,as2vdA4cW^u9ڣmU$䢗v[Ө7 ,p v3,B^4)״~i'XV$ݧ`` ;ש*S@̦-kf;D6Am@@^ZK+ jWV^q[ +Έ#Q;v"4l:$qqh?j 1k!2LlqE%.amlKm&+C+\vyUlcC.FF`{ A#q16Qɘ8iL뉛vls>Z + _ghWe|i=(q+Z isc2zߴ \:KRw0zߎG8i?qt_O-tln]õ̋۱NieA~0nQ* *4)pU~kB7yTFS8U,z\_άpW49v r"$rOy2ԲxM$F/{vC6#ES#;M%NO |3n7Su!ew 1^6 +PP!5T_H\`CG#HbN4f1M韼) +U:.(`Ņ!KNA{ Gؚn+fhAeH`zAEVҶ0`чo6eg~*P*i6bb3TcP6@4Do+A1s",efB)Z[\˘Zj57 56l<۪DL;7+U} #5 QU$aRWGu8/26{}b Cvuن|I2OF 7ZZ 6kg{oOaG*E??l9@E`^6ީaUfntvXv%`0tD&섛xV[(c~5O>,/2*e4~>UZAeϻo6/j/rRT-#+Pm ?CZK> C̑ˮFշ&ݪU鰒v^5:/gt=Y:7VJm@ls X2iMzxpVjva(ݿer{S-*s +*cGh5@+ ;f넄aP÷h?vj  {mͻ}L'W] QD-(`A2;i6S2po1Ic UOծa쁟oq?^AQO=yG c.6gOsf8J k24^s<*t̒zqS'rGnVGdŪсIA:~q26^i꾱G2TfPTJIzϓ +nEH3^>#.>F#eI~̶ {\̷jgU& R(~J_V=0,63XC^O `ïw[@1SJ(-YxUr] >^%zQL?{ipgRy^mVqi<3pJe¥+<^~TP|_6˚)>QuOTcg +Кt,t~ g"^ޭ<4w}뮙A;8<FK΄P3U`9\vVRa}# +b#57=D/V9kbFi1%C~L`C'%SjZ&\"cpMP-:*:10C+ B 6xI3BE]fb^p֋4YNG i_d¶8h}r0Ls zljĵcпfwU-!LZr\"ien;= -{?XQ;<,zuHl]5@6{E}(iء M6Ws<תT>h7Uj +|<}$dTí@Nicuږ" =8Ŗ+.xsq{0ʢXsKŰ{TW3%%\bc~O+\8M`9ܰQi&?Ŋ/5N[ue3N(c+*!V0h"N%CMh_¬, +l:ozD\:^;r@8q;o!&42ζ}GxiQQ%Z6cm:Tlr|BZ|<=͌[ʦ/ >%k@b4 +>;bn״\ ZkIB|k2N~kv@j&-;L:B%D8uj$= ǿ'P +gieǸvutGN>oV wT MGwRA +M,j,.0Z}QܱFN /@E|ыUtC +bFb!% ߽Lr6s#Z$>:T@1> h@]҇# A[JI*V9|$<>C!U Ŗfeɇi#$PM9_^Bb{uc5B*Ui0~PrB %\9MAiFR +rp 5YPLa(54 :XtQYea"ŽS +>?{q!؋ $ #$(S6zabqGX&Rϖp--|/"WG( +I\'&),AB +MI |"N7_]g)Lt&JmϡfG{7ʫMwusps= zosF//¢~ `9ϦKs} |[-DTm5jgŬpc.PV:wV>s3 vsֽ-SD6/?p`({`;IutTgu # FzN>9_mW)dHݟlJ-. rx,(IHpߡԾGda5&37fxfdWYB? +g2.`pdqOƯ7JyI7hC#sQ}p9˜`|0,=̣@:_F^X`a>ZˊXiA&9 StS׎#؅C]YvݺM,%PY" +fVVc{!#LJ{zɉXgCE5үtJ5=EɸtZ`n5,la>SdwV?4,L7ȍ65l)4Mŝe%$jjZeO0K|6a6~^T+պxt& +oJ5ղmНB碞M6)]7YDKcɮRgo߈K^MpKlua=@-} 9ᭃm_Mh[k1c.z{J@d@vqn}!bP MdN88l s @4LzmӨ3 JrԦFKFD:E]DΣ,svEln \q3౧9r(_Q΍`cQa{˻Ϟ"g0iU}y:[}bɟ͑M>6UROMPf!l^ho-R +ޢPkO9ݰh'r-N[FUS<}$V=YT½-jŹN +PfeR-/w̪N!"m8jvή:\hX2iaƝf ڦĞh1fvC<0W/ksԐhoBkcȓa֣ ac봿!U^Pø> %\_֮ӎҝp56ղ"$Dw]ꃖ'Y 9Oڎ-*g'-rTNX%$?:I<[-hN +dE˟Ե;2oIvIz>)VmF{WDf 7\bU֝{ &X Wݰb_%=:4:O8 PU@Zωt$Wk,xNxP&߽"{&T6O~›h?b*&[5Yi&0VS(wxqY_fw-/y;HSD =bglf϶$k>>scĀ0f~ncل;cXWul5 'O?=߹}JϨ6_g-ߏL'Z>$[JftfUǮ$h).7Wo^1xJ R =(ZڰǜaN<ɃL?vXߝMǾg*anI@h2jC>t>8%o*jLAX̴l߽Mt0=λne@ "(6x}͗=iz~^>$~5 !(.n/Z8Όt.śeC~T$֋ +Q{T%C?ѽ^ :|>Ѣ*itLs-mbez?LD1On8ÝzX҆µᲚ>wL\nly|Z +ahnܙufTqT PIvhFǒґa{EӡirR=RMtx05 =mڎRHŴvwT9?VmRM?dH$m-8((Zq7nKE$¡MO[+%`|/#Zƥ.w7)wZKb+V"%)cJ2b& lmr.tC- +b7qiRqKSu`se]ygĩVc#|BjYƱe]yթ,0c03^ 팞Ҿ'Uյ*BMPc;rNzKx(yҾ,;qVzyBaF%zqT#+lOc8]d{Ns< B.t $ҮFI{d#P@;/+mK}P)N49 3XkE /itWPؚAe A3F/}'~zPӧj^Z !KϞ7l("g!/T8uac OJ>>4> r ioX~FϨapKd)8-\-xA;&p긠&`*9z␕Kl-ɃkE]vjc~҉ףۗ`mmДwri*uaN-hx9Cx[N e }Bpo|{Ǐ[h{XbȌseA%I;Q7( 򫔶%:徙N2.j}–E;oq|^(Y=Z <\MPeeF]xif8i5x4(4\ۭB[Q[@K ??̄n9}j"jE\8_f/.w9F +PR,-֋@)I"D۲w{aݳ=Š+:( ik +Van+:狙W@e~}SH}W3Y3ʬ2Ѣ⫥a{ݧ(Qem7!1|[9t{5M(mdn5=R_/ i;I- a$4 +5jmz:рyMpZX\6m9=YA0Ae"]Obp}ohUIdNCS|QN̢["E~e=Pu!^:\*;:hFK߽rkEu="@ 8;=ء `R| ?4 .+y9c7A隆PP uڵudqs,JLzS'&4a-훼+1_cQ:?C? ^) +P5Eh^,ϦF\{ ANx̺V"<ïxh窉g6z̤loScc4i>Oɂz|Ɂa >No7T fj Cd91x=xs{/&"^Xs%GWIݪ%)L㍵e]1zJ聡.0WFKFʔx̟Nv<_ltPZ"M<ةy>;լGݶCZ4gr6Y hlM&0j{R dakHӇ.wQz{o ^SyⳘu k|x#şm9OŽ5Y׼;4$r I L6M  'Aݟ}O7fD쏡@2O0D +zMݶ2vɊ9_.Uj1O+aOv<^fW†l{z~83ΕlgDUؚ2gtf"FIϣt|N{22gө[90 ;HbC`5![*Z^*o;,WgDx6vt1 W& IDuDyRrk4 /MS[vac$ܒS>۵ڶh7멜oit΁K9A.4@:<[5(g\:}Fy_hJQ8TxacYW/'z.W׸`Ց04B>2TC> pQaeVC8abS} iwc[;`RQ;ܹ"_%IYM&~#mӄ$.>ކ6n,Hé"ğ}۷7GnEor߶W0|EY~lYhaj .nO<m{PyGK1n$Coeg5C󙺊iYDy +ԷK t'btI,,} +-Kv6*NVT%tl9aDTuʨyB^*xub8՗BkC-eۛ(v*.m0ɨ{4u2ryPdIuS²i`ż։9˻ե|vE?_:H ++W<b1yիqPZh=ln%=`u}R +0 uu(:Y8;vfeQ`f5%E8ݤi+s1D ,.סK]-^Meݤ +ab1-CU-P8z˵P'P?g kb)w#>"}*?MTX':V,4wuKYEhhN]M=斚dVHLHXMVM-=Mf-7˾.@>P0y+E fk{_̚WUp(бU'_b; q]]jB,߹0GW~ + :cCǗ<-m];jN}iV圌$T1|DߣNý@;-L2m;bwAfr5\Jhk3ܙ) _t\t5ύy+^(qȟxdF0q`Sq ~/U!H + +R7*d/@/ 0M+kAE3#1V3՚m˞nxCi?\؞p7X{ˡK̗Nr!WhEJq8ΎSTT`@-U8om0ySO66 xߖ 0 #36'h:O0)Hzf;>7ZA~a;-TO?)E35s7{|jz^*&ri8K4niy6UP. ] fAo5oZ xFg͞mw6K(!!Ki~[/I;>?UUVԵAs]?`!s<:OII{X5$/hIfAbMi^?t]7LoK]_8=@s(kBmx0` pN39QxOFMC2I;`ў]-:t4"XG-I;-cMr*d7 Cլ%ܧ11l҄u12#FHNXkj|vӷk1W( +4sT!|W<{ИaMJZs˂A3 +.nw[jJťh'aF$։ݶWC\?p3q^&1*—ʱ}Kg.g\z#/ ev>s^X 7)vTѴCVHu޿xĤD3~Ǐ1D2Z??}]@u?K{~zf {($Lħ6[ 'n6EWrk,qeOyߏe|s~B,UT,/֎(^LotC ä3J۶ +~/Nх7(DZ^:/ѡ-\ա_밖'GC_t.YK,("ݐ=ql|jS-X֐^HXDIOʡo#}?,DwWMV:o7Jh0# uB"цϫ͵Vr1,#0l"'z<-1B+qރŝհ2=aCw]7|Ɏim9 aӧ ie\o MdE"?.kq߬36 r>͐_O6Tk3HKN+3v+6Ȏ{Sxxe0G8oG{MA/QB/!޷eoS[7'gu5bǎa{n]Pn %~MEmig#r?p4:/`2"DU\iZ 2p>Scx]%uID/5oܼQ|TE:ncץg*_8UYmf7AXZ=bÅJ>`ˣT3&83f?(1/0;+LFooUE 5_6^OM8棍åmښN?ܙ>fNxC>.f{,ǵ'cWEaߍ4vWY{D| OtTޑ}sm;m N/o#|!ۮƲq}D 6HM^VQcI-;]RMEEՆy w˳MKwnp p{Yq`u,QzX!ʠr8KƫWIݐY,WVxӿV5ކ +ל";u4t9eNN$]]_9HJ *-2旕eHixmMN.'}2 V:K@״1 +&eA08?OacnG8 =Յh/'l`R; SHYYjSy6hĺ:2 ^4-6 Dsސ6Fm{~4;5TO=S 5$$s/xF8 +?ЯYt ;19t{F-:0ˬS綛N1;5qm> +ӦJUmW3c5)N,X[v3Pt/H#Jצ{͉;5xXTV\,ǘT=$a>FyHX-K4V,[Lv*aIu` UfƦI諊™-gB~`s:9GE4!Zm8msWX! `n51Y.d@ifԓřy_lwa' H$1ᢚ W-DY\/x8^֓E }~Ǧ!lEʖl##lO3'5YtPnᒛzBھYoF98j[[%W\,L$ 1z9.*-8Z=db^y[v Y# +t/ +Ð7ٴ2X@5 1 [;N|zf4b4%Ä3Lz閡4v|L;0N3]6/Nx6Ըp K-S-PLʑ!F9ըg? k&^Pckd1.$œ4At[/˺p"CGUWm9Cx˸7On ;:43lu4+fZ}v)uH#Y8|swR>;$Dzg̋0CŘowc>D\]0fAI\$;4,AΔ_W%7F z2kfaGO;s  x46ZHsCI$\TTf*3mNA {Pݞ5;{kΆ;:4)VA1c$S="{ːk3w\'8>`fn#Ezİ2;}R5]t&RƥiRdc0 y}lrm + g%Poo݆ϴxq]R+;vFqƧ#c ֤k-?UaRxŢA=,ك#`{9x`xꐴ˭͞Q7*oԱIDi~> \,8յFsz<~y9C-n 8Dtۿk CM0!ŶZUZJ~P=yXkc/$#ܡryz88^^mNn:ԮwYoxjKYwiZ60L8{5S 9o~D a!׷P_d/ rRlMHpVն:օ_ !u1B(CP%ÍcQi*'@Ճ&S]!NV!g10EjĝvTFc귶<{e90m,-Dn`B}}y`o:gAǴX0h&TE @*^7w|ï_PE)WC;ʗ{Z:ှW?^ \G?wZLNTrzKЫgVV(xᔗ8酭DI:Ψ@ˢ}/-ׇոo׎B%2l6,l^٬+QB$ %pf%=Ml]<_l+ 8KiiYmj[ Ʈ617KԲu%}$@Se=1dD쌪 ,&hގ+ɴ%ssEvYml5wZz {8KSݰ-`BESn +&KHؼ?wX7ޓNLdעM|&QHf4S6z٘H@ɧ-a%ԙdMA;r_i6aj74 d4Bۺ855أ.ovE%{{H&I(eR;c4Zj|"Tu",QkK +@ۙ8;YIy݋OFQ^fɑ8S*# ~5*e Pq *'ȡ`xqFZ}|c +"ϰ7aY9;Hrr +v8Iԫpw"ߴ6nSk<"ǥkG);ѭj6uyQ=,MxĦKȓR!3fW?9iZnv+| MBMS2M[ 3a΢vnxO5U*6o 6&걀H&LXI5$:k{b:[Ԁ<S0a44V"> +\1-8V%谔b/(J 2Ok1G]_ـ<7-B j?fƶu*Ӥ +p %9D^~TWGB6d9d@loݴ6v u;3fj!bYդi F噃Ֆ@- +O])TwCR20[K g!Gmܲ/+U Zz +#BbЌbsQ(,CJ]omӡ֣جEn $b2,(-zv +ky4#} +q룸Z%ԨN٨Bwۺ3bOK"@<*ܸPU(-K jiwn r( ټh +dH̽YbkVɒf/BU<-1?zsT͖Y1_f8[tOz\;W,7三I!y e rTS8zbR37ԟY /e6bDK 8wrEJ3ET}5GoGϻ, sVj^G +T'&圧>=,M+Q Il2:@e0O܁ of?Ql[Gᐦvg7-MY{?V_{=`bbZOzA3mBNS V껔 +i:DžIj^Hs4s0iZ 0&ߕky:~&0BKؐBX)b%tOGM:rFꭐ ܍ss5y/j‘lW5j E4s7©g;o#݂ʰvG$pXf4?5vFʜ}P +F+rjMM<`vBZ!䐏#)rS2z0jvYV4?\\6zTsp(Օ͉NfUJIM?:f!;*p{>mJ^Z9*^E$R5$g9ug/fvK W - ;v)_ ^͘&< +q]=~RQ4ɏ]Ւ  2%svq3+w6aRkQ.3?0$<+sc63w&UWe6-yKtgBmhE5:X_ HfsL |ͶF?5 a.ZqL{zWN7yӤ}ݬ̭H3VFO#zIˈ +gHgIhFz`eߦfU[s]b&hG's'3' xVܝVHMbi_.49U6Jv/2zI1itdhq6U qr[MEr|l2mֱbP؂9 so ;C/mg`m+aN!+XU =_aٌpe$S9]p@(_DtթA]&Usa9Kʣ!ׇTM+yC4}6d[`wyAu~J=jںۅǯ /$c kذ:_4u-M0#mP!|)YPĄ{Vh +>%+4~7;pR}:NMu=Cщx +=ʝA,Xv8HQʕ }c*6#A̶`qȴx]s2^s۩ZxoU\w&OB1݅6^ԿWJAzR C?_M:U>hǓl`yCK$ɅQ-x lsAlC ud'ɳQ,V(Ѭ2K+Ԋ.ÅD/UbS?HLqp)=]MHw&%w+1,8BX}<}i}}1L] l&,)?%K{/Hh3YrsD2+[OMVE~"Hp7Ό2hb t+,|q*my`KI}~V@~mPv;!l9shtVLY0X?f1;z4ҥ6vrm"d LzluF^gY%idbl>x)&_6}VJV 08*%qL#ڙ걸$QA^3_ͯ*? OiM` Yu?:ղo9,ǯˏ+i24[_F?P^+hpsxObݷI٪NȞ.1eLV܊J+CM00`ǰFKyE0fQ`EQskXFE2DGvV3,x:L>kQ\c7;EmJ3J<) SXP#}/tvQ`A4< y`pu%Kz(]prl9D-߅W^ M4{a,%e ΨIT0Î|:4W<66Y2{A-t{KuBw2QU| +Y|%tǩh4| 9s`ԖO6vmmRY; xKCϙ\݃ uǴ0ʨieǸ5f*s)YW:_lueϩյ:f44bꭕ-}{g#CjC~{qU*AA۳NlRnmu% NX;4E.Gրd©&l~uY*?vwmn OG' [},\[HTF`9Ǝtfv1mDc~ jVcxG˜.[ԠagU.>!i=n.?UbD%|sD\3)a(cU+lp'd?> )ɧoh~ԻYT^6Oݍώc췌J#h4pH-7L=Z~HA fLJ+=`1, :Qf^r27[.]>X&9 }zD蝫g#:yG:8]B6w-~ě_'t>YưϐWJ|<}`EvEQzX0JFGlj6ΰSʳoQ(29_Q5O]9hA5G7jMc,(iv9$3}\+ qI͋]-k&BgYwmY1\/-< ͝05k'&>'@8{ ax\gQJZc(bE5zHZY_b#Pm$dܣՕ!b9Hɉ"f;xE%ّ5.*a< $l,J!2T64|7NK؁,~VV8sYbQ,\evΌ٣f؍u<Zv;YDr\%s @{gNQE!pv_,"=`zeb [ht<]ZtYd<߭2&`}=|g) x8>g!a=> `2_\gKTQصkzyF]6FqzPeޟƷiI,D<xDVo{dJw0R ^kp^uoz$ngN_~vZ5gڜ|[TdK{>Ap8vNhf~,{C6TlVA3mĖzUrxžCY8Ռ˅:.~uUH4s}Ԁm}M<j$SJ,ZlIOПC/.%?_$4^гO;5w[c7:׊_Ǎf:vn۶Xa~y=X $h*.@L6§S= +=Cu;>^a6Rt,W)u_9Pvgtɺe1Q>kg o+nc*Vz׾wջ9a9OtnEindyjKJK {fJ6q+|m^f?iSBܕ2-c3`;:tK'vۘS1n\λ(+Yz t .230Eæ\=2^Ehg2-]I?XZòM^bZY ET\f\=hd%lԶ同PAm\ܔ˰$ :|D(i_q<_RǢY(EO U/JwU+DY|)K*:l)%cl/;e9O7cM[ՑhT e:nv xAv74| _eިV!Is#>fŖ[MOƙfHXt7y4q"lawo'e"LMM|X.R503S_I.ٹ6Ǎc5=Τ#xqL։5-9뇫QhADEW}4)iQKu[J}ֿ4㳘@fMqꎍxIv{wgl RŁ)&#_Gn41Ƶ߭s!bwEkjBnI9.K# %qH߂+^Umck<zɫ>_p1TZ=w'w}٨욃nٷn3쫚Q'J/a҄t>%GH{k.Z(#jai{ՋjߍH)Z[r`8 YwL1ٺٿ~zM4I~XY_JgLGF%{ȍo)tfF %}mq~P(SU!C`67yGJ3l@#_ .J4FZc"}*G LG;;4Ѵi?# hW;FN7t5MSN{:n(ǝuBOGsmovs2pZzC,4Tr7#+@ֽB^DjFխ qaDGi/cC9z^(+%5LG`VBڈ(^hJ[foqռt7C WYjB{p9YWj!Ԇ#[E!|Bȫf%ȸ%QuY9tje\r~Hc6BU{LJ80.ho2l4,W9q;qCQ<{0#q,h#Г$b܋Iiѧ#CZq7(a}y}״  RP0Hr7B(#icМ,*v_ վW8}M30j[~^?׷cy-Udݲc-l&^GJ0\ۡta6yuB/X t&?3=65贰*+LdyeۜNMLO0]=G0#o-[WG?/&q@.PE m~P\ H]Krii|k:5a;SRsUIWB>)"ӵ!/fψtr!¬XY~Kgxqkmy{dq$kͤ:N&隮aoÓƬuCou<=e&5`KFE +YT+:>/0g7+x=(ÓD,pcX2b|kt.QMϰBlH݈ &^>ρidUqB7IOQ4O{8AzKbk6a8giFϹ+TñKR ^*^--qW m?d&eC@WjSm]8Qms&DUIDLNqwݾH1ݑf !OhkLZ +gwWt m2xlSߖ(n™HPjar Mȏw:Pӊ(2L8'mvh ,= MvSWG4wM휤᷎ƪ>_MPDzw՝cIeL )tCbƝ|GMRzSȪ"C䏷QO?[%ل7Ro9=Zi +16P8D vdmV`BF״_mD;Le +H0SzO0`[+=#t\߀n0LT@\Բ)$\nb+KB/)x^?pJ뵯UɠolAlM5yhIGW%eA<>.XL3mĶ6ADd.K^cYZ\zVOmKAPm.ain~ms`wz8JޜZM83g4A5X>15SAO8θpDvj"Ωʫ[Vl.mDU6{\*=w,mKEn8u31oj;%Lc~:M;bN7qsLrGR3}KQ7 ;ao ݪqP9QSe>dzI) U3: !ʙJOqd:j{ DwxFկ$4aV?1H{IإpyBsߊ>eU|ry +: ^ĸ~{Gj[f*W>GR6[*WHJSyiO)i!)m(*ܢwڹ*@JQeNWB{g:m!Trϩ;>Ǐ +wBx] # RmfFR/{豤jB ڸd"6]Oi/׏ѧ3&Mb#ڲQlCɛI5C#E9R0J*([hOҵ<\/'֭ޙHS2;զ7vk86SxONOşfeJRG].\"r7sRzn*ŕ}Vl~_?3z-Ӽ-CjҸ*khFwW?n{mʋ1$|@As,:byGv5SGBjL߲ a}_yO|*rKkڌI93"YspԞ`[\[Q3Y#hyYP]=0Im i̭{0''#wt$p`{T>u>USxmCn;!JL|5MoBWGO]HVm;? +)_^wI475ዺ\Y^-ot5ةwLoڔ3avrCd}޲۝NbJcA 4;fyotiF%eF(ө}jZ ]=% Uy ~(zphC*#ޒ 0<ю裺aq}6tyWmؘ~&lzH9曄156̀JŖt&(Q,qGkq*$Xp 'Һ+~P2Ŭ݊`lk6 +ǖtHT2LTU߷r{X5:'ъ4q/lA~k/#{=>iŸ2^Qe)_yN-[ +;&Dk'|w~cn 4*#ru5ZK^uKvIA%0l)]oFt;#HsF0<cI:e{=9m_z<5֫邞+V"v'ά򚞈H, 8+:by&Rq&]Ƙ43xlx^cW%jm=UqVI~:&}KbJxp/P +gT6$m4l [A>GZ!mѠ?T_OXdV* +v731&ئ^~"r&7g +7h3C|yu `Vџp +K葦ӊXQnl~~Ya43F2Ի [|& mb[B1[C:{1HI"g@H&ӯ.s3Ś 96?-[I-BRaJ >QÄV{% Vf;!@HZw%i$kC@qUhfLMߗbט?D}q ]r,qv:r3:*2C{s Vq:QLVg];N0'fOVihWvNDQN=K(P)/ָ[U5%kLSNS8J,>Ɗ:|z՘JҲIծFT*tm@bBf)C% @I#3%nk~> Uպ^-f0jlu{~d_U>گT#4-aPq L`i b} V5Y`VB;Ɣ lfyW˾JML<1x̂l  fL_q]OfBgRnZiĥze-yIctP}l;EyNz< U-9rZ֢8GwMy>z;iZDw8xRA_ڡWeREs"ksÁӓP#srq5^M$QlΞ79hXtS(:f Eb/c, 9i$N#o ~Xr!/eǢAׅӁ[Hz-ڻiG^p#>v`8,gX.g+We h'MEMbq$с93+lZ$GTwiͯYi~r,ùt n“ <`aqtݕ)ƥZlEFiw$@wݭrH'9/ڛő 1ȸs.+Y +Em)/s$9$<//i2DUaf1rҭM[qWA'Fܝ"Ђa^̒7}KO0;{ J{O?s~xSMWU./S~Dќ6Tyvb}=F}eY=8^@!;h8nIJRSX_#`y02ZɆAHjP@gF,//iTP1NYQ3NR #%JL[|3E˃r 1ԈfJ(u⊪M >n3!ƒL0Vb< ?]hƨ.>11W4\P]|*猘h(ACg4z޹NC@ڻVI__?/RXs}kM(3,H<)Dr3_Ӝ'F;k\ \zM*?#V"r ZS;ge*f&J~4f \vj roA7б~턣|48S / UC͋sS1 Z啒w'ѵYɪm᝻ !UQ0X զN-b=aߡ2 ߯_ +\&qv[oXa?ڬsZ|ر"f4b,ds=Z8@~sr1kVt]-W SU808R3,1U+4Ld teHqֽb1߁W'Q^KZȖR5BR7'd> q]Qo\C6Kg_tx{ Ʌ}y-陞)uT&:0vRzy)m:cO4w V7^Y_Rxu 3JkE{xrB Zx]dᛙHfL.{](pUDێ.չrKluL-tyg1[p.\iSb+2M<5kCPo9nVY^"Յ`)t_bŭ!nr+wiiq%uB4AV<ݝ"kDz4Ќp}cTf#䟑ô njȔ +5xw`H̏8k$hB䍃Ix~AN6m98NcPh3TwPُU=apVDhık}/ܦ5+S` >e:WYAR?T f.R6˲A@3jٕr47-;e +Q!?Q>4y>Ԇi.ٛWlt]B;X44* ocPSSa7qՒcќ{C}8XY}nܕE7T}(˶q٦Ӊ,}J~*+Һ>Dނ`j;s^hsew>fl{rV[7mV”ě$_O1"gbjuBjvBQӵ +3ГyI}ո8?x3 _y6\H @ѣ!>u/~jM]sow?#!2ˆ]ȣ)l)MpaZbSw07.~cCǘ| a 2>T+]=8H# hH5}^ά9oA˸֎m?L7 X%=],ZVlJgҩq3L" ,9I3?tisݪ%Mu6r fgbecZS + NM_lj\cZM%O%9`Y@I ZH]6ߙ`˪D{ AO`1{3/IA}N<_^w ԰¶B2Y@ |䯸=c|=:P{^ˢ oinD@le iUm>X</l.t?5;PXUH>"8f^}t~zѮa̛iT]΀;ZN9 T+$,_I 0[0[`ļ-"~m2 BB5sE'{_.D ?صCC譮ݿ[Ԍfܴ:l6Qơt3b,Tcd.Ajs:BKHɁ }}zX-ΊT8/5×v WȏRIJ %2t}HI<5RԎi* 8Ljhj 4m8˞rb{N33ڱe,e`$rY+iz!'sQM"m{E%jk=;1I@tRa(k(W[j¾=R +/R_ ټɤm#SG8Z eMM2ȵU[QpFƙ*$/`2Nʛ EqFTX=jKtNؿp M`6 U֦y2 a>/\{g4l +vk{zf~Y O|](?f4 ^xh +6vޜ.됺M$5\RWi/ lR .䦮PѤSD4|J},'A{\Y%q\:ZC_4[;n_xxz`vNSY\N-*r>1<=f:5n,c& ;C_G=JZ wA i_!KH|+J>pxW` y@jǬ\K FIˣI GJ1nt$og +WIJ$|SUScYMSجIiyPx"MtLnxn. ց) UUV*W' }%;ZAk[V`Ɉϳ\+r[8+4W6\` a&zvխ%nXz*PsR2?'Eq popg1xhCxߠސ{|pbTlj<eY½Ӿ֋+p-[^^Nenš셣W- RSL=U_I2-g=&WC;2kNM+C(V&=\ޥWӐrGnDٗ`hhi1kZndj `hqƹ4n̦=TưuK)pL6:2Tۄ>\>M]fB]zBv![t_OY'mnȳ]>7_x &ľx˽ PZѮ,~۽К]#@EgyzLUIjEQ/FkwhιFVï>`L=3}ޓV$ɩ wG@,7ڨ諙{=@M\=aa;oTns~S$9R5^М 엿% RA[]+WI u&4M;׵mj=oMLd[y9́u#}9a<9·56|K'G61/`y'& ?mFn+::wq:m݊՞j sq1d7ai@SaN+w ++=yՃlJX9^}d0de8 cOc^x*lOcVW󁎖RFbFA'xfnnli/H[͋H~&g'GvOl]T/YaxvwV=Ɍ W[x@Cd}-_sǰ-F4nLjVjըb\=n8-fl&u4۟V{,v{FK*^|nH??C}2Ir$7^(1Y߾"3K2&;6g߶͹{5k?  +w&$%]Z:]zޢٌuWA*+Bzr^j0?+t󵧔zh0rNB,ZID124cgWI5mVtYݪ:"*D"ax)xU/j5'ZC5c +Vu6IUt/ݣ+KS)&!lF&u uU#f n`M+*zU +S?tZHT4.A+6M +UV%<*skHE eD2ãYajV 4u£2=R#,v^GZBuD1OM 3""]LOjx<$m񶪮;# <6 +(vR* mz:%rrI" M&Ή*0.T 񦛦)'}:Z_ͰTSE  ,E {Pl|R;ĜH'rd^GczI:F%'Ps.Z06˭;J,otc +2an0(m"љ^aujԇk =-Zו/e]3" EwPv(B :)ϤMv$aBӈՇB6Wh/Q+!Nԫ cP[Hmym7a4f}6@~G+]tԎjL5ޡ&C.T}hݣ\Xxj^ͅʒ<$ApJlxUWQ #Zێ0ݎ|u-Wp-/TorRǧ݂)9!rWJQtT*=fUx%+g%K/,@gړ;PM %V>RnfZ"a>3QvjoְD촒haw3rHv)tݺ.cR6r7~haTYEmڄƶNT/24hwkJUg oдyȬEi*Oڥ.vlDwS0xTXQ JTcö.tj/gAJȲ)qt*0(FDGig ILmХ: @iD[ڠI`d0LarIQ=/<㕃% mԝ;Ʋj x~As6 +%Q5 枵*)]P[hEݼ湊<__X>5ُ͟zrBa:r,d?_+' #uNVz@1<9p)j0r[|NǺ1è$IvLJ6Q0)z$v pyIM$k`$xQˍvpWV5Uh}#M{!鿿q}3H{VG75s_Y=zb +6v 9؟c [7c,a1!E/i4ޜ(нX=>=0FTIs SlWcא^6^#eUʢh5RazJ_JIYYkuPz^ g>uև m)3 C_|V2b{ c|,=+xnK9Su#z NEI1GR-vB`x&V jPqAMxCjԨ1)I{bV`u2S(b왤`X +RC%R6}NcDSSAMhIuam@.R;jh/?ƶ&}#@4lTEO K\Wi%ƻ{dBT)JRB h߽dC *wKLQwЧM54 _/۵fG>:~̎I'^#*Lcyg+fD|jh8Mr3^.ɭJ* +2=ub_֙hdAU0R0)f3߷_!yG09%cU#>^ v&ӻ=t3t+)mٛO&ɾ14=Mk]_oVbj.C1~k4ŶG7(roŸ\(aAjt<\=2Tơ\oHS#'#^"@Ve|ɯgڦ\Ųz +>'ybC4'M]^Αo p0HP ++d$c2yZTw(e3]o:Q͵I~\K3lù6RgSxy\r@OP,\m2QD1=P}f* *b+hǵ1Z!J MШBc>w\uU#~bMr#aUI[ԍ +{=vv%!*Vh& f$٨˾`>8-|)TǪκfɰ)i?Zn$~EnhAHmwv_4jo7h,hM%9n!nx"= "xYAz;@qy;+p?}{r" ~(Z<2o0D5+?s8Ծ[]u2"g8ڈḰ'L7 s?~qzzFOI(N:~upFriQcϯ?C+mJm,z~$=)QS?Yrjbc?u[,ڟx5<.@3Im܇[IǘTgyTk%ڒb5+9eŵ,u~'l 8#iXu('C,iK+(&O] ]ŗΑ^"K2U%?_&U6H!6uڂ +J3zɣ=73V*W*F?:jHy * *~xVDb}uD=Zw‰lp gYǠVw&9M=npy9Slwީ A0`Unwht_RZz%΄Z,{;BNׂu|Z6>n)^68>;'t1&bVzI so'#NRoDv*f?k֕1xwTpnW_뢀eE|"*٢H@$p?133n{J4<U1Ae?d]W_EHajwRuXTPnn1EE6zOMM&^$w?@FOm0+Q A釥x7{9^K3qmW3_XnR\$.>Mjvs!c w+Ȉ(Gؖ0oi~G׭N,wET̋tF{voq Aܽ9HIIvf%xBI Jh ٪{?v%0L{=;3,8iέ[ON zi֓k8"2U:h׉Bx +Oz跆~y--IYK)֡fUK-fQ%0̰udq9NL԰@*y_4j*s +ؓd2YQRdg:va&; DX~o ;4y@c\tøY%l/t="Q7{m/$U5y3fv'u'TkpkV2y惝-wU!vy42X!HWu&jz\e')nBXFhX!Cm-ߐ* a62x\KeVDaɁ0p::pbN(a„`t9ك%jeH\VNw@gOANceؤuw5.b\M6\ʃw'R}UXVXG>X"ԍY| o$G=j_Y;o<6e>e Ņt={MhhJtS3:؛fZA&\2[_ߟ iVگk}4IwSRm&\O6Vúdܬ %Ev"m\z'@ŋG2upvӴk=ZL3BG 9FT +_v#M\6EVtedS /+ lv[wtOf9Iz":@ +?@:0A'zkmXguPtc/((h!/RHՏ򩖬j Y_SgPIEw_{HA=IT}y0r͌jW@u&j s|Ո㺹0MKtٵNAlGH^my=^lI8~K\o?u0ߵPwyahKA{5u"Qr%?}ْ݊vY="m?կ} ʜa>aTk>dbӓ~f -NDy|A'{;r/?6;vbS &ђ歍̶֝P>!᪀ȩw1kVlZY80 \O/&r~c.;8Mԃs,j7SޚWniNhp4}Xo (*dH䙞ݜk[5 &J:νI:CjJOۅ7iUEm^ ++ƒ.rMD.y.&m\xwy_ o6m.ɣd$Et՝;K6~ռW"piym>:]pj7DLRM>1ddKT yXӌʱ +e!ZP(t>\~%c|z - TpZZXF2tL=E>mc]Ϭ0fN5xf<&Wc;`w tUOp9msV+af_TNx{rfǷA;seĠH'][7/ܒ\ϋo{`?#> fM%z3oޒ\c+X<o^=>bG*,Fp;=61GIpq6oxlqmOC &`.{Z D [^Ѕ3,rubIA2ڿp`fIKWJˉM>5w'?l%W+$Ha]PU?F^_ H/ajm0D0>Cny}eֽ# xlo7f{;#! +1_PW{q׵} ^8JSˢ"mqf)⍗~`j[ZTy|pfB~5T`}.lie99L=Z8D_@ +fl=cwEIf)I_*GA5 jNJq9f|SrqbyjI,5kbDa͓ERP(b9w~{3@M)r ! [lc3aE!$76bfP?tt 1lGoZ4=DNusz1h'ws :ҡZA m)O¾te,$~t@~EȦdgWP]aܱ,A ˼/=ȅmv4tf ifJJ +@.j̱R U[%^u0ݜ,)HIՍ}\S$>9xgrjߨUtAfuWIK?ZBHJ\ k]ZV/M;IS[uUv \ Fsmpj=;7goE47ȉj{(9K#I/,ȸXg +T@L9#dKaGߙ-a\7na,tW"]#J}ʣ +CwOq EzyFEI͠OU5BRϡrͣ W<^5B{<4$4q仭CڕEt-N,kqݦP7}*^~Ygcׯ/miVc& #8*۴s.'Rz; &T^f͉z$w7q#{7oVV'M}\%%g 5+_}zmYT/ ~6k5K]Ou[#w$)/ȍ@;jAm\wjv#f~(Q"U@"%W2ǹ&g/Vq8=-Gc4!{odj5Tf B$I\q(Mex2Z +pqD,ww9wTN:Un0\jxmv7xMeU-|okpd҃U"ř tp,'>O rrɷEdOд#ybs0 **@{l$Mnf,ȯ2%"otN<۱ <9{@m(=lt 5>|xn9ZO `TAUZN acT0#a?ɽ4+IOvc0OKDOص^<ڹpٓVvO"O- FܜIކt¾kɨ/>s2ЌN2h&g;~<"-KjyyX0Q ?$D2EsytjօԼ?GÒ|'4NoYJ\oL0FgnoVn[I>Oj&R\~EF,o8uU\WjYLX} Ӧ6aAuĽ#'y<4(Y|3U3LgrJTs! + t7@Gbߝφ1Mu]sv/ev\&nkoA9ai՚5&KIe Iݔ {!4ǖjI/NmD|5a 1q/>I~n+̪ 2$FJ"-lLꘈӺ%4zs5.V6./;Jy9"ږ>/tX8=/ޡ1sM"gT, TĠ}u$k(zµzd,,=L\{h*!J.5 +蝓 asA{s~ 4BNOD +1az1GI+v8:n# 0t_(H)R0Iˋf0ռq?=/o#ΐ +m ۝źxM -l9R=t\߿@7 +Rlbdž]LTݨ`X#џ8\ziHX?e1f%n󙍚cSyDB; +?{-Hn'cԱ%}N5Z)aS$7}lkk~xFR5"ցoV~b{*Q ?'RHq L'j>=MSŪVPvL5}fo\u!<*vدNPЊ˼!~ӻ2j5ǵ,'TaUp8Zo8tYλ2i'# .y}c0᠗J"4Ov klfzAfϭN?SU$L(䐦@<+ޘ;ǼU&CvtSuI+p/Itzuy{a(BuܐaӟjG:q=t!([K[}%&=-ѽ>"_5P<3dr[JVv.GP]oLIr!(|zJ &UQǨ:F}r7~J+,[ "z25ԝ)ҳڠCʐ&U<`Zyr=UP p%ݯ'6aSjNp itZHW5=Hi+#a ^t*.KԎ7ϼؾC@]pz6WBnt|̶f|xG85"Cw A{M`')Z1}JH? .vN"N-6 ~*גPj/&X,$WfGy$G@*@F&ǒS\J_A+ .KA˚¶0+`$tۙ[1xvM@ai\&S-prY 5%ͥTo(* +\mYrJ|'kc>[,˅?m)n JG|'Ͻo;gpc]"JK*~毝zXqPg6;}蠦jX߈t'js]쏼ѫQUs||[ Z~U-ϵ{|э{E-֪qyc966Au\]$$h4f=hGm~r[+;9# 1R"L~ϵS.k.irCJw9.D?J̶:qjqt?u8ZPrH e/W_UM Gz?pzk0Y^_6h#h&:ȯ + × *4bDO4XAZNjLQ%a?Nv(®^tP#B&bxJYfiCNsn +syK͝VV & uxÃ{25<9Oט"C =q5"$*g`.n/96juOɗv!/jR5+;\u*D[mo3AnVTv'Jnl%2 ˚$͍)/;= itMgl{XHژϟv$]7fxe9͆lz Raͪa>~t=<Ra9WMh&%k d=̩eWpJz'#\|vvNtqQ*4O*xp9:}cVėBxۻv JHZ1wwQ$p Ųf:P~^r!YX_O~kvQ/fa((}IRK A"M&:K1 tQm|= X!oa?xlAt`T(P@1~ș8;֮j0M5إSt"[$0y9(EhCh?iGsSr)d +^gAp:6veDi 7:}}~>hhF!]~˶t"qz\p2 k7A\[ +,ЍބOߪHGwN}XHt 2C aBp1< 2FE%`>6eKXj8H덝†t\n?|Z얊PئtYC>O/)K`%lWT> +#鵞ͼLc;>Ʊk}D;XJ8OŧV28pXVk4 _*KAhgːa|#Whq+>|چ1Uk2P-RK\ՀRƱ6w5N.4%T8xPBRk2(C^t6Ωf&!'#X87~3nUgJRW-,0,́.'@L]7|OrU}ݶW]5> +@4v6d0Zn67_cꥥUDj6wڌX!Ăb Ev8J_s>ɾΠ{Ph 0n`e M<ٿ亖tONtK5~;IDb#`u7_E3mɬJnX8h䚮#&N{lzt=R4Bs aLַn29^_0g~~~"C3Ɵ⥮# W]L0OpeÿM!=^ Rddւ-pQ˭w*wMt6f\q : {#z'QtxRv]UH f"J7% :rr[уҩP5ӇugmYgCc*/7G>&QMߕ +tM[N;~a`䞿M`H}r%PuE̿Oi!9~HBSP0ncˍ[YF6{R}y1]A*nX;P,‡fNL zn56f[6yvi3ju]?Δ/Niv_wXU3hW|p߳A9iF~,Z5 8u `'PJxhnF9-S6n=0S`_ן'@&n +`6hhْ)Q֩+b|oٕtfa[, ;i>1ak4gdT:ґWAcZZ(fus +]dmNsSlQaj$=ajʎN\A/{Hҩc9$&-w:\ZiٙBK}-}х]teWqV,s?5<M^}{nBgm]+bH%6I[#*kߨJTlB-DjCA[}L)߮)qF"AISciA +dAN1= rnv> ߎ245{H9POoR}E)edvlE Ok}\BERw2IMߕ-`KUXN871(DPUe:NS_,cgtͰ]LB8EWuU_-vDt|LU2]KM 5|GJZӷAh^c.^BX2ZngLBivtT4cM f\$|ih]m>NNڢ}`uf& A3&qUƝ|H :8 "cܢ*=Mi7 3eU`5H(ۮzJ]"Ovf'%av,d#'_R?TT :$LƁl#nȈMsEY4ojų}xv7CCdMqgv2AtsέW=8+r6n0vZ;Nx +SN3VTUjى[D".Yn;PTViFX +;Iˀ.8~v*zZ[Az2޳V'; \:[ +^i$<.\ۇX3 +ZC{n[REvPJ4@Ԉ8_ KH Nm^yMyrC'$j5S"#̆)h@>ThRu i^\q5VG_'6F/3aJ^]mtҷݴ:Hx\y;ͺ+K>.Տ4MtG%[~t{=. [i;VOOi[of ҞO&Mzkx% zYdRӖq֎jȂ+nK zvGk. owWc6qUpگLl-NfKx agc{ :0̏dk ҷnȄ +!Bgbx <QS784fP;I>1F01TzY*~ڷRjGt7mb7-h lCۘtON{>l"EJqytf''HJrw G`f<2/p]jf.XbaxQջZklT}ñ Z.*c5N*vQ\ xaEǁdlBT}`z;xOYqfA9 _=w)'zeȁ :X6l_`{BtһBݞھL1JҲ Jkm30) TJ9"Za"\΄epnIG͔6,3k9)T l\ Q)uis-{MhkCđb7>W 벻7 x6O{R F|Y%fv8x-gnF.L=K\14"ݵM^𪙆>UL d=&bE!̺(Шd%64`.(/q$yzy0K̲ KTQ$p9_5} w n?bKd"$@;n`W:U1h%?(ߴ-hӪp!Xu};Y}KQ©a5 ϙft6SB\lqG%t;lt8:a!hWҭwۆz,o +bxGrnL1Q: +[o2 zܝe["ĆZOؗȂe1iy[7 ^ݠc`w[72?^ -408 R|aBvkNv3.T[6'}5&XJ{%,'Ur\1漗H,^ jWQ QLQܨ!)FL\, HÅ2^E%g,ԏq/L."o ˡ1GgG֊F0lvcqܦ_Wn뉤*VX/'1yjkN~Tm`LFzkM}vV)L%Ou!HiӀ֒g"d3+@9Ir{GUPU;*wf8 -Xc :ZTwW2b$ێ< #N<KX[:è?q( =?.j˫3jcXei/Bq 5ukD0; MDm&lDpKE81 M~ق~=>" \2/D@c^[;e:1Ka0G} +B{c0:4ߣOݍCfAzT1>#&z!F/YyPMGRHfC"jz~'香Sd(9jŗXDXız17ڒ/p |&UoMXT@vN6bD(Wj;ΐiWP^&٭#;Ux [OMk7@KUZd&fyݜV53~ @h#TqhvFNB yڲo{9 P+ħ8 A)Rf9. 5 +U}}%UiՑiD5̢^#4?@ҋȼtkd}0kJ/oxd?Ō1]XE4{#ndLϏl4b*PkֺepV6 l0v:Xq6ӒJżІsTe}&"v:\k֥sGMY~ӴFPMb[XFFst#-agm{\f~b:lrΔ{ul;O)Fdxlw:T۞4#nxOpbу3V]Q *O|&"Y6p{fKB/#&Bkfd[`3 kwEL JWk&"\sV lSNtLx!D`"Lf ! fy h*jhEJaԙ2)iޘ_iIk+|4@i 5}ӱø9&&s}'ݎ?\ݟ(g,Sűk4 vqk3f5eBJwfnFzcVTcXtz4{G^Nʤ1үv!}:ќoq N%oep /][8}215_o(sI);tc3b=%9a.(5713EAŨ.O;>) <}E-tFsdYC.Ot}@|7b1qM  0@|Q쑓?CFIsjV9wMCur~#N&~x\WLb~M)esf.Sۂ+;>D\cfE®Cؒ.iqf'o )cRjƹf(ac&m|ݯ3?L7VLHK\]n4LJG# c<\M'Ud) =}oQL[U ++WM +D6XZg SwAlhTWrg4hcӨFKxgFt`0:NZ[R8̄ 6 e WɧuݯAfFTbibL?pX 74s>: ;{y \LJxRR4? 'l1jk:u<~p*8NO*f-'պɭ;^A]TxL5^ ku"0#uWqS|a W[JG?^n/E)9|aT\?)A$x!#LBGC.xXwbG# " +L*d~ݯ8ɎASlFn!?z(11:0K <0b`m_FyEl,oٸaa l|$e0IvDP1TfڠުM]|~iM0ni +׉J7{ΥW6D,BrY?alzEn<"BXrVYYx|dVs*pA;襸a*É̘`#bP(BPL +LlJ{\kк!e݅m)$|)ڢ.Gjc]"nbtOaӆeftlz#Cvz" b{S\jO_NJ'zh!:#qX/-Ʊ!rZ 9#lLY3r``OTqgL0;:LJebpG[KFn`,O%kYMa/hdN;iѾ` gڊ }b`$0>,+~iEwW&"2;Ȓ<&Gu"RI&.LQmŵ6IsۇwuJw'dV̶gٗ&B&\*AsaAioe]=~}M!eJi1R s۵s^r?<6%zSw㪀+08Z +Jh/aHncfLgRu{-l։EW`4"cJOLZE /2T3.Wp["u?Ѕ&E)sMpTd.I'@|^suuvnh%Dd[d2)/^ ^n13nf3YG2U +3G; *g$&>_C1.uiLf#|1U:Mz7iEcm? +6H,+fg[Wl-5Q7 4VL'aMe) ->S<,s?: G~S( ,fӿRW8QSLCuȯr9vXkm'jN1ߢj10(~< +Χ{3 H4A0`6]T٭e /~tuQo)y~vK_u\4ߏWlE +[wE38T|@@KCZRW֏rWPI /${N.y}ru2U{8B1wЛN#^b[\j/ۚG$7QC!aZ (~#D4{v?Pt Gу C 9SVܳk\-4\_m ʋ?vӨ}?c.^^k.lae+-?̛ly;Oo}xyywij{F@ߞrֆo+S ^Q%*kWmUՂ"m2(At=UPa,yb%p@`9'9C%~ezk x~Psl;fyT|.օaۉ91Ƃ+` (Ro~@-\{ +uɔgw^3NT+{to[U^OlՓ/ q̼G33R 砃;΀̈=j.m.ɽ(бޜ]4=󡕙)\޾g"U0u2'Cw%%z5P[utpba,"9zPyIdQ]#mc9~$_&H h9YP-!~DlDҚ[h{gtcpf7; U}ڎ+3Hfu\]u@4l0yƍZk o)l*x+47)h΁D6#\iSEKdu>9֕ >@װb}8#[_O F~lMLF#uo7ޔ#"bfn@WaDӳͧlU :~* ēX'HC_jz/6x9N=E_)H[tE@4e_ؽ%}+@cjΊn7 "$Λ3ѼJ[hgrAc4I}Χxkvb(D82D{ 37b"ped.tC;$b0ĪӣLSMn_1avTj! +Ę;ɀu!aT1)`(ܙAiM;T[b+3wK7} FpTW:]R@>B|@.bDiwO+<^̌wۄdF}Գ%1u{z4mlFt- $lw3䃪aքCf$6`KET\O!=q2O<{(P&^3ع"mנn4V\vA0]d2ŕũt5_-OO'l::SDY$ym4?C0ă_66PLV"Oni#2<Լ`bxuӬ}mXًs5p: 6G)Xv¬,pQ&׿TquIw DA8|F2CV%u6/:klVX;l" ?(phQT[ ӊuqEަ*E3WA.bZ)[vU:c%M@ӡRqzq`ܒH AJ'UY>6|]3D9v6位,J?&=`.Cy=27Mz.že^ N7+(9w/"O& qáQH 1}E~#NIE*!.UǎCe3J߿,EsKZ=oiׁO.I#MeՌד3^X_CZ(F~x&M.i*zY.a[B2zg 85lsѺC}SR|)^ՒiaHZ2ѪC((hpC!hǤstӆG0Q63T$mo! +5G~#'ke;|D"b'UaP?x3j 79vé#ELv~ʦy/K@R9ZdVr_}/sG/}z$aAbؽ!,+fׄ7Z'Smq`KE$L5W%<K?nZi0OܨJTaPaثXꏪl^-U[ԱU](el#i_mt|M8YA<|c-ۚKze,bӕR"AuPT F,a?H؅7!BVOZNFbk)/eWN fSZsPO_Ibp77eJ}Lki[+EMy$dyB@yqj~㹰C42ciJl\8S1RG-GiƀAn?D%~s[O&Tm%4,.Qҡ GO5d6{E{1>7znvlKa$aY1ZIٙ>x+f֝L.%c&Es F,1},g c>IHK㟺$ۡgȨ0h5-(~:}yyO998=>1S9S3e2y5~]JI bϯkGԥ8x^TI74NoFe_ n * w}ŻD +(+oݯ>9_Xq_wՔázV9^uLXh*4ݵK23?[yv=O]'#CSutI>nإը ;YY W[ߪGuF&($:jװMU82ͤtWXvk8  0VVd^@89B؍XؒQ,Z6RS<xhC`C*^YUm2e}+uwbGO2(s?D}e>fH?,Vzőo:}_~Y}bd'g"|ڕ6Rƕ +$^;%7='mM_,ف8ֱ6) +/7 Qv [icQ%e}.-@,>V`Ǽt7N܁_HNcAf0r,C*!KU(`e fԜSmFZ{Pm-#G0=ǫ.BH_̨ӎ9b ܰ.z.(zхv wqP99.N0|B]ˢ1%.᪕v™p$G j&)fWSvk:r6 ;pgtϟ.o*!U9;39.h2mldig[cV\8dCi!դT鰰u$ȌW3?j1VMH 5tէrIދ3e N̹gz88fC4OG*R*V%&73ՆzqUf4FlIg[ڋ\+s +m٘o>A);Jſ+Ό^1_+^hEӖFZەޒ3wLUxV26NG sN@fQ;muLgf4eL!цd,ScśޘK|JT= ,\.' A̐nt2ɝJ.;O8KQ*MmZc*tpKw~-}xU%.a[Wuyg/?ׁZǖJ`ݑ +g1% +lNAHS|LʤWI9qRvz_?ڇvؾbS;ofz׎sauEG&U¦F 2ewgp5ty#.^rQyH:&x~Rb{5b"a@[ݙ ,~3̹C4 iIZX9a^Wʹl&D*Q +o(_ 3 +3~YAf]l1Ӽlɷhg9e'72=>*K0b)Xn?5\<$QF9+Z񴡭4Pfl_uo4K:gVGB|)APmוoLSV YUA-? J[7(@<+O#S$R~)$7"cf<+suF`bNX2.;3Lڜ?󀒻ЈirzK22De@֚M3JwwMN^cVv*2;"!iF)Ohtp.FӝMuSGhYY}AMJ|,}sҋM!:㾏ze[{lYߐxBi[J2KRnH雝>=ca-_x Y/R!A iRB)6vCHߜ&lWZ|cv::[!E^{f Lc=1ko$ ߚ^O=儭^0$I[uM +g4 u +D[ {1y־$~DYossuV1K߈diO"ڞ)N~f%ؑrܥ۶p@ȴDtl90#*8ǃx// t6Ж'جإWSy"ɤdxiN#@$[ +3 (=ހCwczpyK0E[:dԸiDwsXkW`Y5[`a\hҨ6Q2 h%Uw/+۽ +mcp'~p/s6Hԭ,?$bKzCsJJs{lZst|>1^A(N‘w_Gdze~ø A_V(o}+o+Ks,ޯx몟=LC/cDRAМڛym=U.˃f/N׀iۧǼlQޭ%@ʅ~ΏK_-18fνٌ?-q#[Q@YIP=ҕ=龔($Qr~nY9G$ήYyyd4ҥq7;*{>hL81 <fSi{ַ =XJ۞m魇Y@` +%NMR5KLm ]Vc={iv`7пz0͵o:emnEEa۶<Ȩpr/ M{tDmxޝ |elΪPڴRA<ƭ[OaUL[Iέtә*+ 8'Ue1O/W)Xs?w5^D|7n!ᢳmݜB/Z\& i aw1S;zmzY"ϭoGF}/KdgٟRU~{~]2~ͦsoAF( X\+&ʰ;|!nXtuUZ w8R޽ϗI-gBؘ7PX&ڶ9_TϠP'H϶,Qn@OUF̓0۰$FoĴ9, C?WU~ͦĀǡ &݆39Ѕh(4rײ)Y;bk6'KB꯫@In>6cPN󝻮8C? CSYΏ/N3oDw.tTVsyyъ=I;rXHo|n O3,RmrXU +..}| Sp~іD^Uw~T>) + O\(@rB@Wzca;ƞLֆ0d M?s_x\L^u˨j.IOڌքW)gSbhkR3^֗}&i˜Xʹعޝ>sv"U'"_}ɸ> _B+|Zb#l5"9QvV(^yVbh]ڊ@a[mʺ*j(AQZླྀ$Tˣ֞(XYIL{'UpMJ| 55e]Wuxĺmv=~Ԁ4=e=pNМ8rB]>-;Ĺi8ߨiFrvI9{A)G:aZ1wuCIRlZ"g8 LCaT9*|[q:W55|ŝ [XFfy +n1rY/]f_?cvS'Qfnɮ !**IV.K^Y!AvwE~j\à5hqṳki>y!phAդ^}jmW a.R֩/ 9 \A^I#Eaon["A"ʝAr96LvZ?Oo(h`:3}OzS| @86iw  ox(4ad]g9Md6X'6<Y5Vf +O6اK>.X\'tzͮh?'BbH?s]9@c!rڜKwgj ݾygUnqj`Z?Dwnyw,iG Z/F;FzI.F:j&^no0|NDYoeҎ+ 7a}*{n"F]6Ps@Du?YS8 N B).2 + aU^~(m(*{QwsnuhIcD|=hϽW(]PE7-έ!*=c oG7@ͽԦ(J˾@A8vIѱcb i'v!zoYeVzkK>{帋#d +'FfٷJ8 +鑏ǞPEEPŵҲ&{W^|Sͩ<{O?Hតn!8N5\K#w;:3kmW2Ma!] Sj!NSehx//&G&'k$*Ux `38F"7zmn#QAt)KYXNIQF궢kYE\ܳ0Bst7uF_Oj" mes-VlFIypZ'٠0@C1{Z6,{~$uijFyϋѫZKmI'i#l8)E׷WĢ r3<*TQ#׋c  &W2 Kdj ï M&6+:>F4wTXڀ̴սNYwX|۵1Gt=)4n/aNRLKЃRޛt M C`4ɀH^\H;WzNyN'ˠԝCHTpne7d^8EBE`gt=0|JuZvX[e\})yFhfAҾ-EVd,,d4P'3!f=s5wK/F1|j{1מSyKhYDkΛUrndBe^x&f[+ҶlZ#G)Zj ~v)tNt-mt.1i'eP$Hq4F[Ů {%& oYMa1Ul'7(zcƩ4PyLG5}Jmjw8R0 Ya5>ާ+D'mp9ع>^Z'Ue1?Kydn$1tU TSoo\z`O$"3## hVIN%, ,e]ðj.\VWem?f1gT Rx"ٟV f0^sۓp<\*F%C؛g~liWSCUK A[t ɉ)Oݗ遬vkX Gõޗ)hU6H`r?_k9x-_%QA1SΓ#+g6^`ʯ\KͨW4/Yk5:bwj^_52/YL!=!S!N{}e-Tx-),G=9 7*q:tnp(N욽98,@:3d`#SuYU`}`SߍDC|bB;[ 3 - +mB%t$+J5qFfE&ON9aĽDYzF|J_7R)}fɑUk +w^}ƈ|aǀt6F I~sl3?(Y Dc5<6MsdzRuzrEk]uկ7G4[6۫`HuV]+#S@O 1@$%#IZ4#QZbHȜ7=XڃH"ѫ0z'_fkX$ l۠"DfpQVJLg}ۅ}p' tk=^ݵcV{lNBe:4k[n)A+jh13yBB{|ZF.ҠPPgffV-9)tv_O +tR`irFi~+5([A+7 +UXH]-M=rtX۟Ì{!B2BB)_hWFvK t4j~8?oUpYOr(iWghZܕ ]Q2kibg2ty@͌Fh]-V.~~6ssͣjw$uY1aTOGZImi.3c3͙3uq<'e֪Hۤ6Ӻ5©sNmx/:bv=,1U37WnݪԄ*\px](t-hKd~b䉞b'qXwP0˫m [6!ki\C#I_8c/KڽʚΩ(㿘jn3c{'4RG֙[вyqMy&l =dN͌ #rYU. V9w}O&s @Ȋ`Ut. ?;5J'p1yQ%7m$X\fR=mؽ gGS0OJ?Ks,l[gB);%v_i]jv#?qGio SSB,˿J1HA.nm%+ X:kP"N]&t;NVGO{yub۵?TD}juY +I?6hoEߏO]ngZ`g[]Jv lC7~sjR=UN=WG'F>#xt OƳz\T;jgiBgf%pá[-Xu6|g.3|dͬa.>F?{ ~ƺBZ֛ն,>@+U7:Ipf@Ӭ4 ':a.Â-.pq+M)x`E6i}3]s;vX`Jכ5NIpME|^U=K9}z}T8_(6N$ލ{*ƂX" _ t`}"' |)l]: +k޻@P,־K}2halVݢݝ%|RK>`=j TPН*:BQ?%I(VA~}i7"cmmk;7Έ\RKx1j(Uz?-1/ Qdrnj$b6F_%Q{^kLI̩>Y`sC{w$I@t? $H<6ZZf9MlFFF?6  (g]>0CyhѭOleTt}l,TE0gϊ썰e86 -!?XT2eE,̓^: +%P!o`'Ip!]ܾi7< D8EHcJs1CTx곹7& +SKW Bl?˘ q__]ɵ& [JWw0\_{,i_aĿp6{/f%˒BGUvjAI Y:1 l`V3W;ٕq1[yʧkiBFb8ziWo7,e1~ FwO9uArBIxXv܇i\'bEJw}zd&Sa]U e1S ruWهH.F4ږE4vXj۩ϟqop҅Ob)b'9aF( W脷&FUIlx waffS4#lTi]*UxahJp.dCtDqrlM6O\ښLQ V7+>>06M>pkc$Ô'S&`|fpϚO2~#LnDDtgm"mO藓 ȫk!ӅΟ-IӪ0IqJcPZiz~M$nm!2)9s6uq;+Aa_xڃ=Xee'ͧv*qW5v8рw?SO!Fz Ll?f\@fF;*NRcůL.,_eK [LkpUU0(=ћT׸albl Y?| pÊ޲`ayeT$9u05GGnvG +ڰsYAgBz'N@O8ޕi%x?o:Ѝu#4$lmzG؇%v5Ya/"%Uݜ `iV~l;9A%݁oWjzl",MIɤ{t&J(tV!|GVB0`02 \iqvSb٘50mDZ un<95jĜ F`cK" +DZge˔먻- AwٰgC9P6_ǠLƊzѼ tn[qSm;nHYL4,ݣvwC,L)0VVU!JX۲\vKW*n-٪+(9mh&*'!äHM[eϗԗ%d>nYCEqp"J1µ=A ۏ/a@!L8nW_yK36sq뽊!a P+r<6Jap~4`\Rj9% 1;DzN A&%л@16$D 9XS8Ul{ b +7 hI߹=Po0 O +ʣa:|i6neZ&-[^O%iҼQgG(:6gutɼp_NUc  +yE2=/(f{ܜ}akeeΆ qD?6-OU:\ j2jIJvhkT+^|0jC`r -@`\uWiF0F̊he:iF@ +'E]H: +L?;!)Nl\v + +ki"U$xkOzqE3p6ڿ~X8 IM2&`EĻR"ޭG8^2oB] W9Rut$;{~79-<9lTPɡ)67XZ&P09P$]\<5 +ǁ{2D8qpB1xIn "a 8y+ƀn]3CiEѤbjzȴT"Hh +o%WE`Ze7 x)ܬRqݞE -c<UwFܹ|&fhZp/+C@PSjWDC%3aYtP +i_f?'+ڷ"‰P?mPu"O)>J31rP(W YAUaN~ Zj)<+$cE7F#'$ +Ng s/FPd'Lk. '~;?[0dyN(0Qq"֬;U`sݵ{٬|a"Fsgv 9k,&B!Wi#7hgC;I4ws,to܍݉V3Wz[tb&컞:vޑXrI7~NQ18l^Y,eKgU5'ne0$yyJET^ҭ ~pT"P sA U_" bgGJqxVO*+sR"g$Q.uJFwOU"@:`>µ`$T"ZSP&|3By*W Wsh+vIE#mpY0V*ˣz5@ptg +O?AҌ8Fտ vmf/xxȇGVu_HxM80?%)dY+2#6tN)%ڌD.;Ȕݓ:"+u0}Xx%`nѦXdO*E`=wqW~_$b4J9{Ũ._XȤwJ)jiaǸu$(%UiLt +'NT Ϳ8ZOK1q܋I,(`4ĤYX+z!N6Cz[&ha(D8 -_#XfkMWM!u%_pLlw)h#uՀ+phծ|0297Sg4;LNҏ'6SmpC{jOuN#/?I~pmӦCb& Cvlb@+ MJ3"(i z待2<R?/gT0~2?%z6ؿۤXqݥ „⺑"~b2eaќʯ?:fL`31L>;kgLSt4c 34 ۬V'OaxigW1c/Z۳` +_[Tt>JMTam~*'XA1~7̙7pinXu. r?v?wLu&A_. >m.l8pjOpyk&#ĝ5ZWa{>R-#W'3Viwe3qy7O3]_Îu2uZzBC]Å_YXќsO(G‰ hsh┛h/g2#RܠLjշ zkz``Q1z~-fIcm&q0@ٺ92=9C +E ^6=^yXmkEm^c߾mHu(7㖮@V?xkoA")O<*愚]7xѕI& Y ӛHf){9C56{< (ɝ{﯑oصԚ/q[c'O{&6=|`FAbфcfߺTOib·]E2ߋUſ^z{Frlƽ1FXrs0B`2-X)sYFm1d3i{>{OcYlk6ks_I"ROBIPmYjTLLFXP ߨ:/e֍(>m(˨G#pJ,¨=xg@_ިԬVaSQ;o<#LE4oѰ1(YlzpR8P~ :~{o˶ff6g:r y:m`g".-7|W*iOݞC<];rm!n* nJ0R.VUt4Pd(MaՒhb) f߂խHgy NzIu\A ܝD}A̞W4G56>Vk(箯m.LIeXlZz 2=O:\:]Lo횆mmQ WDBY'g"= +c-">v6/ YE&f'x#݇ZxNC\4**j3r{z=]8qH^;*zxxy+==`h9DӦXpf-e89PͰާuϸ<tnS| Q&tsBfGӶ3y N_Nv=`XJCTl\'$ʖ* Uӕ1nOs4?כ_I&ex2QSblȷdo߈ņb6Nl*bV cF96wJ;mDe>]tl1< Z3x֑cVŁ{~*MK.a7aY$%ue$;r2 +N:}X+\W0J@)D; b@zFYxoOK[捺I'tQ7utRsL3H-~Xrgf㪪(/8r$u +^@28luu@5+Iz* DpxiE-wYGïI;wVErg5Ln\EZ(J5r>[<7bb{9=l6bl5茇."qZ;[IgϬF7ҭr@iïʀj ~ +Fjr׍5=a2h<'09mNNtS8#7:3. !73S:C̉l4>nyW/4hm*Aװem?>i{ FvQmyhkZa4'$L[r굓3$gξm-p= wL^LOjo!bӖ0<%2f40v&.Sk#ߞ:y/ͩjP?.|k.p6EZJnUY.\l낻3KMn<˶4G:2p)oiҡ=X {Utk[ +|ek\-3iyYhţ@':i?~`nRnx%TK}JQ71\.uRDskK!B*dC[Ri~rm?M?1qHfpNb?g3͚yIBEB#g1bׅy//iO:;YrFht}Pz&+L%p]Èe0 -ʁ); 8a bh$Ŏn+|e[~&틩ul=<9jY܏ N<ҝ2$ܫqi)شedsO;[씏hCJ7}8YD1;Vs:Jb'\sg&frOތ3;?Ӯ=io'SBp>L-M^ue3?k}kGXĘ& +hOd]c4:.d\IhͶay$F'9n!Ȗv7tF  +29kH4Ej)AwqGր9# me,S`)5|vZ"Uƥ4Nӣ0A"3?q,#1?t(I7҆U}738:. +}DCx\ܠ Fخۻ\ t0ɂ#ԴO_JzjwUkD}'<0 y>l̓?'{k +BǸV*wn]u%èTDri6仪Rn֫ k!2!B[&P_%jKe͍zRzL[|I4P1MXoteaHd2>ǕXKjhԙUH_*KEkAMT["֒mWͥ0vul+W59 kTO!ړ;S(mW~kHfnr(&6:\evX'Uψ8jҨF^r43bnB6>:)ˍSTGĚ(PscV9#Xp K:O܂\up%VXŠ*r+# 9ڇ:tĦ:wlQry)]p کVjoEf6AւjYN#e8tZlo|vM6^t$`И=jN,H*pKhMwM}5yْjqhbq.^O4ztg~8FXMl"uy|˧3{P}ȴ\Y5Uf?ukٶ\ٷ/!U4K%:dRܘ^6VYvĨ~"F>+}ƎX7fm=?j˯R19^L|;O6!;%{h=qŴ?2݌㳝Np842rezKd=H~6n +Y4ԁ2(F  nԌ&4ȳJT|t`3u[lp87aCVg0u:a8\t'2Wx)ٴH( ^)a:4X7czsߎ,둄[~=B5Р+Z1Υ?}Zئa^x"AMӳ;5l n!z%#( /2k0ZtL xP14d62N+YQ +gLjlV޷U׍%9Ҋߪ_ >喥"nP%>MK\qM[6p>9/$q#u@yb2σYEfژa7Lau_G] [4hTr' %嬼ogǔ5/cū!QbZnWiV 1mˊVmRHc +m86m?ߗ=֑&zVstںVS c@73\˱8صum%q0iXq%,$?]UEն"ԺE~ ս잆j]Duf86%ʹc*q`(aUxM˪;[re75zjXfѠsLTXܶ{fHZ43fq>~wsfǩ+eW%kK\7[ӸYq(X)W*K68>[7Gm yrңOB],NKMWNbC1)s.+b8~kژ.ǩA4yl2Y[[m^9]Tm?ڨnG;P?Ss_qy1hUJi NQ|dऐ}3q`$? ^e5).W +58鐼=Un!7yEeb +#ʯ$w*G>a45N WÕ7dd-^[j[Q;dԿ; ] &0kDŽڕ69 nV;A[e!ԭ4k8| 9/9;ˮsNNs r:+ ӼAFa8MBC[&iIm֮=Ѻ${By=QZɏ&sM\')jͦb|}ep=* 26++H(42wX΂i +]G!<^+|{t|0=s^`Еmq~@_*{vm{%:{ DBn>D n1CWlE5h*/p8sgWBf=cp=+׷;Ɩae'  Rǥ9kTXX?Tdq4G"t<䙽.pHV:X^ֵ}j?XՍ +0P\Zn*: axy^ RN'&1p)ߕ@KfP*;/V qp9&ït(gP3A(ux LZ ً@kcǻf'{Zs./!AjkkwLYO%@lFVpB,M ]÷{1+滑zc̪Йtpr:xb:RSX1!$0ٖNېa'^˖J5S¿^,`?ߓ1~,6t;%&̙u?õev^tˊgZ Z,21eƀUSN+^C3WGRYNRTXz0>hn"P"KO}T9r1 {,}(:)pJ!TY _,}Î*&Ly\ rh _Ww"S kthO/l5_Vɂ$ +mI6.m5T1rЦ z.|n;(οt#$屙dsmlؾṍgȑڀ`8IG:.\"N&bX9٨'G;O]c V[^ aJbVmf +"܊^3U-]dM;\B HWIɁ 'plnx/`.l%cZZҗx˿pv+#ݯ'_q`D.ܭG. {\A3μ i ]#dEO6`^.aGpgA-[{,~po[q~#j=+>kr{7 ,xa[ZJol2m43;&yR̯13># + =_:Tw=6kR"l#@O}a..ʺB>XSN|e,H`T_#,,%WK ϼw<ޏp`U_10&LڬҎ7Kz.fn$ܯcr7EO|448Y_{4|2lX3k-fo<<|7i=kPuyeBE hFaͮRt lx9uܨHO(Uu"Im t-3ش +}]^&#/TaGٌ2š +WzJJQǴ~KLJ/q-f:,[^FlRarak9i)kxb'}0k3UƂQeo-B4jeJˊ--o54LwsHaJ{]˝9ґ94Ou1b=C_9}}A}0/ B?/;_VSbφ"DQ|9hM$eXhH +M* 34MiA,> + +d lB،Awb"Y>| +2(mkm_ZЇz|; @^s1u٨H-X6r| OZ8rn<0 lopȒ{/x,O/ R.lRZ}X~HׯZjuȞ8IE>2mtMdu(;MUAB=jLVkrvqcK2\u{ObI 8, ]:툋de h|Wɮ*Uaњ: E:zTZ '^WSPx96L.a{O` !}2qO.ZǫV%^]Mt^uZxefBruyϘ ٫Oڿ|ڥ:LʴRhEbfSooNfܭzy4__'qATH D{w^cXubF|yf+(e H4ϊΥ$ ,J)ܳa%HK(\Mk +{4 +5ef%@iUfp:vQL]X8oa3J9;N8Qڪc=Ǖlk+!LX{wW_A~u3Z-kKe6]-Y V " <dN|u(>eL=&~óOlQϒ(Ol[SZ_N]Yk Jb|18c% Q NS&Q.u# a)Rڒ٘I}/jbY xn3g UYy#赘.Zu>u߫b [+\ֶ7y659=թ{lzwR*:oIl/t7y8c <t[U,RF9fyAmf}qG޳vx}ҔLN! Y[$50 *=0H_ +?oqMWyKŸfosBQF #yXhIb \Mf%~6KǭӶfvn:$cY&-0V3oth. YN}e$!f1`)!MҞgd@ +c_UH:^pSoC>+e*iwb7fQ= \ =C;rNP)**}j> jO*pٹ (å+t3ߚ뒖Lʰ[ڳ[MF.I YUT +W%~dPyb?k̀p7AYFG&#[Ca8׻r?s.;mޮ_MImuwLx[աF꿡~=\eO8Ysfy} hϲFXy\s=ldδҎ&3/ˆ0ĂݙdI*jjuRJJCvwb; hҙy` +m.8{(jt ש<ͷ6ÔqϯU 6Q޸ $ [kN$,mqbP׹?E^:ՒT΀x)8I[&4xphY#Ccv眏TppI"hIPQo &?ݳe̯=r gݵx6kQ+$6OfZ2j\og<9gӅy}{b嫣 {:.VTHsUxvZqp+ L#scxryU;J,wr8$񺻭}2ٓm<+}WRZNqY?SnO_)`˹Pgg9t2JOVxYfdޅ==p-簟:; 2P +Ae81.fˎtieMjn# wtӛv!zh%e +R]%d-1icÀ.ldW'#e|ԥCv@z5#% "v&4+I^?Lo'L6>ahxq0|.SOeV*oR*!XiFqwj:ljFz濔K8D>E_ +x3~eQmQp`-JPU/ G_F̌~<.6*]AI9$#H]e.xvJ+<t<[J{pXAW֘n$e߻R^MGԧ9K*qK@;ȿ􇝈 %[?ngO, @uF:^,wz'h5[[e0VumBg.92KN7,"mܗ葖)R6Cӊ~\5~2/j勒F狕gBWJb}93rel׼y nk-NE쩖bEͪzA}`ɀjB쌻_W h +^ +j߁]'tv)T5'r}76Fo'9k{`\$j:/#i`Wt#9M~ 6+"(zѠwEi!YZfy?atvscMEpfA%M6#GaG7- N3I'Z>7v` +J?&dɽ?7OsG}VΝK,kK6LR+v)f,5F- xvćC}}.jakZR9g¡eVfV*q>T,sK|:yi#U(oUՎυQj*Eʪ@a5-c Hn\sI|U Z*~bޫC*! L4OU\N yXtɸH U9UU7?-ʯzk%=jMZG%ԪY(ўfm ,I#Vb) {qיIIOGTTGd}R<߷6[l tjnqw\tF׆=K(왬6ƽEtF,ECNWkMm]J5ŰA]&.՝tef +@"աQ[Hw ƹ7ʈ5-) Ǒ7_fXtwJdz=狊z7ZOj|fTQa/W s[9%~.r;zWV{A$:.]MuqƻUtoj{zVmT'7F֢x-lgGUAݫOt#Ϳd.8լmYڲ +sF\ 3iwڜe[N7+4B_p zM 꼚yDz 9ǿ׷s&+'v7?-{m/}[wR7 χfPtkMȧ*4[-JY$Oo:?X/j~(^-L搤 6'nGtNuߕ}v& 7l.Ŀvۀ֐붗mI|N %O;`z[=w=\eɧ{.߭vgg.jq +˨|]W#{rGJ9;'L5zbؽ뇣poAy An;S1`c vg$yX>S %GB݋8^?2<18 D}|9X?sVȡu *vk`=08:Yf&1S 6^L$I@g?odzPȡ:#s1 hj'c_>:LOG)N + +=(ڝy!=z?%!|~enᙙ4'nK IK5Y;k ^o%{_eӜew͠UZNۦ'[eb\hF3XÂ断ߴZݯn= m6yr{'GqIVR#c ˚b^ kYM$ϳ^!"oY?šaX%TdEXAUQr83E:Uvpe<@,O=R?~6=z,~,Fv6n{IV'ڳl5JcrR-]\tLѾm;9$k%]X2 w%a Cp ,=ZljO-qȶ׼ ;_:]qҥ.8݋"Y]>`D* X̪ :#I- %LsQ2 iGq}ߕzeGsY2k3ZZ$ 1ټŌ~JGM+riǬt7ksN~f\g%͸rW%BgLJf{9=i|NGs-wʸ1w~Q}Ȍu/N+3 zbpqPBS-P8iF$]qzX%鐗ZTfb6qP”aӰ0qzb4“jK⾆`@*G:HlcF֧cU?JZ|4W#Ͱ:*6WC;4NGd|tGY{YzDz`șu{|.Sv|nR|iА_ -\j`mwkBH+TIy~ <\+ȌDm.3zԯF/Q-f;>i(:>Zԥٓ<7{Zm&arrY8i;VO͗ewN vܳr Xnv+T¬6vlj2Jww"c>cYSŴ0Bb펖,?eQ4saYDbq54oNZDU+hIJxY7t3;iMEu ~n}텍IVyQ?p•=-kf\ gm%w>@c:p> 'kEY WSJ51{,+VH;V]xZ=Ḇ=oRCllEҖLn uu<7rx ̶󵧡-T"c{v4N\ǽO8j =#ZYκ'[{\}x#~2KU~q=Ν,vh3GP p-J2J7F'O.0CDI~ߩt{1 ڗ7idӧFcyC*:wy~`g\KfLf6ifWܴ~W L]9x3Y֗xsr*3` $ Afb|j-85/͜ټ2O>#E-єYlyli~jژ-<3Q8i'7花we_~h)|Z,%2%SM zs2*kFu멏qroZ9xwI9ЇdT&hnx{<ur 4]]NUR5E3E,g@1'o rh C*zuBg- FEM$.ǁYUMFtIxCjUˌ˖`<Ns +GkK^ /<̉doCv}]XP?zd!7 +֝gwF_lvernu\u 䱢1 HPP;9*'cUll7K/} ǼǍ l_}IbMCNJߺ]:m|YiR֭kRp}\ Z`-u|DhOk}yֲڐWc& +`6j^+dJ'>`I;Mw2 #u'B֦Vf n񪣑xt%H0}YT -wSUb6(lz򑯙M_5G,V2b ׷^h\a?mKr]`X_A^in8ifO [Şzf^3#f>D>L\%SĮֱD%n9l5H3vE4ݎe9a6'<α 19Ӊ$3iG'[/dusցZgn6X`)B фRmҥd[XSq4mMMl鮉 WVa!ColqpwInZΩSQfHMbׇujU˾ZTŜ2S\j7})0ש/sx!!iȮ2Yt8-h*;ϗn 1N3T3 +̴>0'> zCߘ6ABIV%:7_Fnq閎?(;ʫ㉊iT{9p u}9Vgك60>s(*kNmEzX*d%i+Ʋp.-*ZzzH[TuKU_ {-d]ŁvTK Dʵhr-F]ޯw@'$585qMxi ףaB_q٧7g"5;iӤJ3NݎJV͗[\[QUqDpA7-Ys>vD]o8gOl&lHxNZ;탟WCwyxCh V#Mo&teA?m8Z[+#!x\ӎpZ6uv`ȣ@3q2,k)5Xu:ؼI)/w,7v$o@ ]8chčDK,Rca"ȌEk^=3uݱ0޴Ao0T|a}xԨ&0юDuydS4qq*pz'Q/2wJ^|]SO>i @s8s~j4F'9UmQvC]~t~mL>|f.O 8 /; }0w}1Dv]5 ic|9^̆PgM=lW1p(-0qUhf.߯LXqXC>Xp敗`gTPgGUcØ\:tnFoujS2z;Я=BJ&ZOkM˔HSɄLMK `Gl}sWƥjN܋+" tbEerC7y14H[ת8ƴї#F u,&3ЇlF5[6,$Vv[Gzss|g+vo{2s>Mp< Jz}1AjݱR#E(7j<إ oI"LBjJML-SLgx $ $ΗK7y +o>֒- ݚUѶ$*\#s +Ȟ޹)VDhdU`|fI%f{:!DFߜ]w^w$2lYq%țcWXE}X#9WW{m &”2wW:֙C&\I([zP%.FlcZ[h ѝ^-FW-v<ޔ4m,z xK!h-cI v6)'l_ Ri]q fon;Hd:ǹ[CAs/AJX秣 Ĵ%ԙfQpV_t\5g+)O vjB0wB48o>zu{P1Z}Φ'o +)qce& s]HE,ЃcIyyOTl{J1l{8Xc#kP/$^힤W5;m,W京Qo_NwH?1tx9ht,CLpJtWOPX7X'M灒ūt{\2FȌM_f'?Ǎāҙ0\W(ЍM-,n`! T~#H0V`?6L2 +#=WD;y/+@e2 tZʇ C^_jJ+NPÅ$ؐzp\,8(-pIcgXe[kc5'!jKH[q`$lhǙeOJ01[' ^f|*\)ٜғb"ɗ\ 7EAƿi\A]]>X@v|>)`sC)ah8_YRP5!:PS2}[Ham?ڷv䎴Hk=ܶ$|.l]ip wzX7_yd=92>4H㬀k桙){ B7SR?vf/6kfE-A +ujO=t{:"Y/4q(/F#}g6ZrHrP@Kz ʊ9O6s'{BZAgu/0c'xiLgʅ!+]I,ó|."_Kњ8c~iZ- .$;a+UӁ:z>b1Fwr%_á<=QS4A$>;D2Ss(_Sj lv?tAC#{Yu{伇i7mbmJҢS6/Pϥ({ׂ4vyexK{ogؓwkq5=$zV%[)1326N;.2UfӖmFyoe ehE C'̶P`^rDAZ>Y̖uKl_ AUIR{vv "# 9!f_5\%&$kc!^϶ZڝvΣ㸍 q=QNd\YI`ok|ڞz k?JB9u9f;"A~;QoԩpDϝݺ T)~ʩ.xVnfdT٪6~4 +?0ھ_mVjS1G.)mv6cbZ:Ta˓ tⰉa#RXM:9 >p| S[rq^˽ ߯ I57S:"5uO Dϗ?۳dQvVXh<[<~3uP &7P 0 U'L5` YCN f ݥ2k3&ղ$!f_{fN멻x]/z^Sfx@@K=R:%wPDױ9ZE< k0ɯf(6>;c[O F\UYM=8H,0t6WЈϧ6wpZ^sљ[DQ?^>{حhϼcx^d'*;bth_z͏ ?%oc#R=ˍTA:0jց+z|NkR]EZkX#iWȠaZ7ih_da``IN.˯rPys!ߑ=?W?^G6;z^U1&$5gH_i/Rgr6?3wTܹ17_0yyE%&ؠ]Nz6fc\UgA&ale zIj??}烦iC=jy}jƂ/rtZފ?wgҭi4)2|#_C-upWȗ:>Ho&T4^#NS;ՁI#E^YԪU(M9G«)IќF_֍-n2ǿqkjUaN>ۗm.pp rlUL<pJº_h(-߯$5{veys}E,n BRQAq2oAV X+lPs6i4p"#n y1= 2WyFgNJ7Y5$[ٟ.ݐs.d/0@cM,7fKGm9U)Hv76 u^%'ŕZ}pzd86뺼F=rxzSu=46w`;IYH-_=Id, +;J۴¹A=gE,c1^<>13KD¾;>NO/L)qL. \?c$dTqi#F~蟨VzuRYM󢹐DtR7l=D6Ο'hYlmYvX(@v0#2eyl>,qIvM_Cc/)Ӹ'5D2q<̿&*zvK rps[a)iאVƁ0o[{~l=(/Asϭ0b>]`/䭉G)G:U ;wD˭|WApц{%p6nDA۩&Qk^Yk8固7#~>Hy?N8W& jKw(%qP:h UsȼW1LO즿Ա'eM%vXthjɓ XN,n|EaO a<e9-Դh#ƟJ#78E/t:怴H}Oښ60ZvOp֒+7*q5m3SGrLB#trPo'b +bMz!ǾvеJ!!g?c(9;ifnbz, RUKhIh~OpDDVlOĀXsKR?z?$whBefV5#UۇX?pz0Bε +S(Oț?pv@2P/U/`O*9Yt!d"Ra͏M#Yy SZ^ծ, XmA֝;{<JMoATșh?qHD +9BeIFMndHaok}ݎ#{"<3p'wQ4FC_F+˅7d1Sƭ9uw9|.Sn J7/>l2`Ssb?[qя末owxdU 3Yڐ­0!/جLa L4ll~{[DW˘" + &ny9 D.~]<G-pSFO+vmqnҫqG"Y3p.J&N- בoK[3>j+uҴy`MvK3e6찴/.Kay,L'-OU*5kv)EnGr)u^4I>*OsP=Ci (ya^6)lf 2-v+pUWSаLQĮg+UJ6O[ؖMMYug2c@d_tWTa+_9!lDVAЕoi'q +~alV2Ûy?XY-~{Xۅ/?˛X) [7Ru#& @a4RA.u-9lHOGD'VSSdφk1ڧa6oBh(MWIMT=r# 폑vyIWQ/:k53Mz 2yj vY/hby\bN[U~!U>O"(;T~?)T;tk71ib\#JLA@Xv_NmM+%IiM& @W9Ыp8Q r;L:bǭ|fq6UHA&r +lީʥ;待']?^~ʹwy,= gXrGG껰FpG IMohc4ԋB6G#ƒÁz4Vt״t}gWK! +^3uFٕDX? x#8^'j%Nq6[pHSF*wqq=t("h^"vDʎU*ƍ[:4(N/a6FMs@nlZ婛"l̋C>hqɫ3(\N&UL ]SUGRhUɋbL0ϕOif꫺6θ ]O +3=yhqλ4_D4kmiS~vk(l6|cVHKz~rhP]}X ewR>}CtdZhfs[Q:-جJpl`96|[qeF|L[/Qi:ן hc>.f͓PÔnKKX,+e tLv"WVǜXm 5(Xwg32<ߘJ O{fmjj>rt1Xa" LgŘר3;|Pn6.dr.f(BkyʓH{(r4pwM ~zq,"$=5nݢcKr*ىS&iL{Z1~;-!Ɗ4wq-M;@viej+f8Į-҈^rZ,8jsb8~ȪU2&)!tfjqױ߽Fv;l/C<8pW=dVv{yf,F/^*:v3hѽV[SW?j}_xXكYh2<ˬWZ!16sܲ$m @! 7Je}?VDuvN f0,J&A7w?~!/yt@݇IVMi=Ga%pfRiy'WTAۮ&\ V&-FhyziYkq&=>CK&Z/8j3-na,F!"sA,cĦ>m_u2ú2ce~?w2m-d-/)[:KỊ{E9 GC)'M7Ĭ7oNAĒ).$ O. B݆ +th +HLaDUۦLj  +:];zs?wba?Զ:-&VHƒzWo`5\JFll{mv2L8BdDN[(K{-EGzF~fv`dn0n3'_=fW;B#t.-/,YM%p_%3T>P_. x5ޯBaU:%Nk 2g32iioå*<1FZsZgtN"a8z~m.|:,meUfkY5_.=s촾Dt@zOEC6cf)8-[J;4'=j35{b\F8-O8+[DH?onF +LЖ-Fِad[$Ɣɪr(=j /D|Ȉ/ %ǶXpZ;(L3 Uר)⺋H@7H +C K:g&;7dq4: [лl}}S*g)c}_Ya# #w3y'XFID9ڒ16+Xh3N`H+ IM +O#a [V @; 0[RY kbTMەǗ8HXgtf$ !Onlx1~}Moݑ갖]i*Ij]uH܂x<ωv!LyT WOY M$f o RRrRhut5! WZjH& pk:݀+ZW°8d}lVqίe~8XeߜT\_H&:=z .a'F;~I;L %2X0u;cic,'-l]Pel0Bն2kDQ =1]9U判35WXpK%N顫ߵtS w?αr1?z?yHv!\m)jIEmDX2:t>mܩƕHW|$x:pSҌjU<ev!Fn0P;<Sc9~y2iFs] e굾k$X/^vY=3,O.ws-m\èKZgr:`HFBƃjF6L3~vP}*̃Rb&R|#dt+!R-{W6M>mj<%GяoTT)] ?M3o-[ Xֵ)p.v77TIy %M6h#S:5rZY,{TQNMgnOb~3N&Z>Ge4b`+QnaUAlx`iZ߄غgCc5Ŵ-1"=3 YN 0%#=nϹOؙPenN/<#/Kj+`*rk܍nW֊ga9M%A)FS3vvG'_ʺ#Q2O3~=7i D aoםW9W>ptxck@=$5f 2+4R赟>H3JiΫqyIU +Hzrx%68n?v-zr4k$4@GބFal9hG& W՟CAۃWxǻAJh{jxbg/׽yw:r$g@ efkb7goitA#4Vƒ1{S@ ng _?οv 3ԎX.fsL]m5uN= t]BW51Gw6԰&G8AD CUD)CI dQ\#yMTע/ LiJ& -2;%a{b2b_?[Xxmֽ&)U[nEښN%c~^m2ɑ}^Q]u#k4sb|=r\ +5̿[Ԟ<aO5,(\x^xW]20}ɟg-PЛO[o:g)ZnbBw@Y=;=㎆s#%oODf aÊ9Yxy{}aqkgLB W˴;4MBY6+gV1@w"LAp2ʯ3/29B㭺`z\L坡D9=5GOnkq&4ochN$iQ\V|Y⌦–\,2פak{8q_OɪTZӶcUk_1§9ۨ;ghGT:;e`Qj\8Ii*^jb۞Qu +MnYur٦kEGbzg&Y:.&*"@}t.md˻657YSzk`DKl~ȕ6T3ؽe]~TLKhF=xg̖Ւ-a?}ܺ5`M=eةUj^G3g>$> /E`f>ČlktÆξSӯ$⣏P7}Ԫ/2jF{q|^R`wTB6m@f\ǒ=umMam7[W>f.3cJeF{('Q)9L脰eE~U`cѦt^] ZL >`7ؒ +yaL2ܜpꈀ^Tn1ۥ6fQ2y8"AJ9baݶюGV"I5:ƒGOUϴӝ6Ag +{;Uf  s9RFݯ3Fi_|V΅OdR[֯;FM.8[v=;%g"# +fٱǾRʆv΢zﰸ^IB9 \Y.~/`^pBx*^PG 7VՒQbJs?ˈ[jyV$;?7ß>ڍ!sAz[NKLWYzeONcȐ=,-$HU+ÂIm"w]_Qz>xz7(70f:WXXy14?E&l) QI je  sք(BڣRo 9fsmW`~f#-la@-Bf"k^yS8`<5AV,7U4dVަSE'o˨Sjt 7#}RQeIْ4+a"/l痰7l>- _-3Jj{ _+6EbpP\s~z#vlۡ u(7#l[ͶngzDr-jzz@ +D[R513-_W_>4n1Ȁ4g̫ 9 Ot+, 4 ]4saDiSx U/?(G;ϵH{O&d8A&h;:އqDt׮5eZK.ʴ;L;7̋= }.g# b!4xbz=\"y[~Ӂc8F-z!{xH>^+.>֎x5U?OPEџ?7X{8?K>Nz'5,g^ [kZ(y% +|x$4t7gaDz/$l one!\nԯ!`gwGP~lM#BȬaY wLgw&gմ~Gw"AL6U[qe{؊$T^Fz^=%gP,aI+{{n`v#UٟY#{$9p~s"AU6 +=0j'5Ghn%!" :3L+Ex0Lx?q +p᭫s͆7Pt 8OkⴹYΊN֛wP)*|s!@?Mmի<\qjJSITK.+PP ty9%/ ut;zkfcuAs,8LCjf wڇMx!5vjKPk^ 9%YE"G!C?؄f`y_ejk1юBQ?/V&k9if40_[U{W_2ߝ997ޝ군3hxtgn {;;4Wa™ Pҗ VRCP$m/-8][zz%ύ; R@(H3Rj& ְE(NaajMHpWtWh.Լ&kN;׷\FIj3x"`QD= Rc4/CaృmUv!զ7nHQ"JK :vnHJg%{->QS9*i,i"Pg7,#V۝56 +ZZyEWsf6\34")yCyI4~e6QO h^eu9cďD)C@ *|N0ŽtOjtl5Izq05сz {QZQ Lɯ/{ [V/SRf 6АH~\dlo ׃~4-$?R{z}XPVb>n^-S72j[$_$K7=M/0JqM֮Կm/ OՖ+}bElwjz^߃$+K뻏Zb=9m{ا`3`JZ l}ދ1ca0LQ[ދM^J1`MJf"|)*5YJ(SN[ޗVvDUI|w(~G lʦg Tf +l~PHk#j"gf׻[Z#j4]pl Wxg]n^بym5 X$&A{(V:[ujΎP]pmȚ;st%]RȈGuLFyp}\K/i;چU.h\@zZ|!նKD%NTm4@ m sF_9~o#*D\ַ[n9Fٙc93ɪ..8rJV&{nɤOng^y!SiaT gtմ1nWnlS2W?p3}op 8F9 ұ"GAk8w5%F]aVѕ +W楚}ҭ}7 +;:39+ެaM;ZD8m$"T;_b/2 Ėy]Ӣn QUn\ƾ3[2_bL6rXTz {Ig,S/iGҍG9`LogfAΘ4ųSn}5=`tW{Dѷ {ǽ Qm<~mmL3>u|J(!b2~qQ~Az~&+Dha/"Ma| hnЙẁZsLo:d+cFSvڻchnNV۾qIJq8J{GĞ'V7!aM`?LDۼWK JDކ֯f)W>Ged U2n +.eUNgzt[xWuS?Z&@DŽ_~\לSQv^h^T{&M6f܃[Kߎ[;AHϴ~m9&J_þ.љF7|\@4gB34pF+4"\z:pN!y>S\KXuW#n_pr;C&.2tq"Up8ZdC3ŶlzvCYhgP荡U7a1Ko5)=`J:7ݪLj|AU{Bʦ4OwY;5HJ;mԖ\߿:U`AESsfbuM!Xt"/TFv)dx; TbYJau +t-p{yBpS&7-nb`[3q d&7)"Ư}k3Aa:ۯ1$C@Kp@k߱IJ2j|-YQWQ>e'nYFeI б9~EkU½fydosz-nS|ͦz,DŽeONv&ZW^y&C4w~[QcLh5T=ji-}[MʙIqs] }@ rkBKΤAiado~]_.03zX7Ý'!cҟabҧ^ӹ Z\ @Y`*+y#q9ѧhXpPX0IMez373TRԚa9{='p;=72|"@;+\z糒bc=(gῳS.rpi8ckUul:Q_1:sawtѮwbmwYwV d=81CC\R님B{2UgE|@bQ/g +4w*~*|Hle0 ˥nIvMf]3/ScۖG>\+rm-ܫ"_,& GI:м(N$d Jvwyȫ-ka$.Z.(lm/cH4H]k7jOZxef^h {Ƃ;QSXnF[1w;"?|RP*?x/dϲ0,ijI@ND vd)h6*SXu&zi#UYxlKUd1Rt}l?_c6˴B<ݧz+`7(t ه:SmN^`jLã6į{ 9iUiB`XPw>d ' \uXOW=k~H-j3kr0kބd wd6jO;3_yvsDX[ +7ꎵ_T t-{̹m0ṿ ~ąӝYfQrPۮuMժZT>QEw0iLQZ>NcdK:VpL|A[:Ǫ7NwݤjVIS|3Oc r!}g@v9^rj7' pW?јyj>u?Fzi > "0^VB8HV` [TLbxy[?\SkǢ{qMk,ZW±jWQL+5j 33㸧 GBd>T9؟B Ьd84#TtEZN`hhog{`y/KHp0DVTX:yҙ֙~NvQ Y>5v(x$۠mr<>kO:?oږ}@{g8R1oa$s]ljJz +GF +58^k~JrRQ-ZL>V#i9Q܎`E_]xw9Ȫ/bv.Q˙Fm +ߺnึveLb>5;x] D%LNLi6TVdE4lyuhTqmJ,fIb\s6RoGrJsql F04L󞦕CpqSH7 +bYcLZ=1o<į`4;HLf3|W,/{Nkl[3SK_}7GgLGz87m8eָʐ.I> q.P%N)xӎ_4'#MN JV͗=L<& {GLuUoGVV-2no*=D?ɳ:m;ґ\}o/Vb`akzqվ - +I+.`CQ6ؽ3!qi +ZaV zWY^S#ɋ$FAz4nQ gX`!.+apZz+mN1. kҨrOdӮjMi`K!r/\lFz im#R{2f#_w: +^ :mbO[J7WV2i9NgUb=f'ukV k ekМVܼ6Aa`H$gCNJ 9`La'+$2 (2a_HWF|ibUഥ)z@αn +~!gZ'}_u6lV5 >ڨgyؐnU.4> f)m. ?dX<͆]L䴔b&پ +Z)!+Z{:C [IdžRVqo+<eaY*dj2 ,o1"'=I,>WE+EmLY%%R݂*%z&N,XFIWSX΃l]u,K:uCw^QoYP+֭tvgefy%;ci!γyil"$TF >9#նP3hߨ`r +G!7`XSm)RQ3 hnc`.*N[P඲^QtӞ:W%7뢙e1m1B:h .]Pq^)ħ:w^2v?FӗEW`::7EɒƁާ ؏ 87 ?*)&K6L%qi9^Z$*(zJ)B{w5 Xx>g%T@-? +Znh~/|H{(> du3V[mTZCX$)~m)˷.4MKtEߺ KcmMqx\&4M?R5xb`,A!Td+d:yTPRy5},!n9>Ѯ>o(,giQlb,qqXn윤|j3r Hsg}f7!kN.3朰Σq[U e.m>gC6^4ǛҔrLR5JBWClc#s ~x|L<(GvNen:;@|♵MJ$QUH{;;r$ePY4ٚ4벖w-o +\2?@!{D2acK "wKwcЉv֒-Iw29R>*"wBQ q/׽V)z5GvXNWCM{M(1sERm/'aĪSshH?PrIeF>Q´X5" d +sL\TIf3]RyK,@]jnDV=$cNeL(Zr7hJDZ]vE%Ր$]yXHn,-[ Whݔ(/p̰2R |r3yM7VI+ >eI`]0V8ʰ,oeb+~r/L0C\ +ݏW0ؒ˔8-~0d(7B~1~^{rBE?rK5_ mltsB,SG )M+f? ZuҔRgYJ-EenA@u"/8EG-s_ EB 3Oadۺ +m?VtIY<>HNj N^ͧWMȎ3RFÈw`VD,mfJ0|e#B>GIq8]1 yżSѸDEN<"#ȳ:_/cdCVF/we#2g0DV`K9JK z4zE9v|PsGX BF2t!(GsMEhxuZ4֓5%!h%}. /CwQTѨԴjQ'%#r:GN'.K +hݠ.\ST:ҶCC#_:麥Zn\lKa5]EaU)5%V 6KKSZ 䡲BELYu͜.{-DmvT(.i:#<(T~?eGL޳bz%1x ru=FݧFzzpe#NX&>R΃fGJ6l@(|^RN@% NSXцU_Nˇ% 4:\ё;|E0~N2E?a|!#~U1YbN{;< IaeɀDIpX$~IyÅCԂ9WsΟ6\y?~]߷螹\?Z\;lϦNAt߆RFGov7Cwv$AuMu7ezu},g + D{2:RѸ8L{'vz]>)ݮQ~fC+ᡪ)u/*}{J34@v-o 1zj;3"ƨC&!;\b 3{2n4ǁ>O  m^o_$glJ@@4Vwc9?vޜLwS9 $lKUCZ9|,Gr\9ʧ+#-_{M_w(ߟlYԕr6ygh1#6U ,ThKVJ$;퓽⭥T;e?*%Nڝ]c?*Įl}˖S߿ ͒'\mSs׽vmb;%߶4q:E@6L0Et5TiMd kǵu|d/.Coޥa+rsK4/ 0ݰ׼w^.mXrJJƥ+?](XqȑUGOdƵU'\I2=Ѳ{O Wt<{'N_o7]e:bD81͂wյ+aO^]gu +=[A8`D\J}SK+"Z[xLT-LSk5\Rr\[[Wo T~$RymG:dVp%ωkT"f" \O%vGvLg̿&p/ԧ㯶RBuucib#94y:~x aɎ(Kbƚoڭ9{cHI` QUa:Y9| ?:QM>١v6˴ ,]j5Y >尢2ޡ㛚V`mŸxmrAE8y86sa`}K,Fs;kU73}|03GNlgY@US2cqEfl4WJfqSmXH[6ˉg +=Os9VV#>UL3$ij(6feX/sw5tܬ4ˑF?fG;Ág5Åz U}>^~Y-}>ꢖi M%'ih#G$Vd',)N |£Ղkܵ&2VShBxd:k{1;e~;pU.D=:+EZ.17^/} <ݭ.֯e8WIJtSc< 0T eX,!L2!D)Ú +Kdg(ǶzDMG3+B8P3Uq:M􆇟v@.M}X2QRN&v5]ִg9s!~@>N :luƩ tO2>W69>]9sB8rJ^uY鈛N0s_C-x6iKn0(jnG + Oh{t +l.wѿ^?( r1s7YӛԐ*hZ-íWFiB1Vggs;\u(dBA&&bg V3S/ a{j询cY ̊BLr;csU]SUŁʊ .REF_CJ +9E<ϔᔡȲ6"mUzt+[Ʋcc*/ +!^ԋboV1z,TOU Xs Tί'hsv7 jCtCW_KuDʰ ,PekGBؙ+uUX,_ױb@/,tvs}U~~VgieW- Spv@k^7v8k6b&F̀>mr3%nt Ancox%T+b<l#`ˊ:h%}acrZDI`$dG \!/*x1yE#'R2ٵKG~eUp$4;`7ĔN+[;)8 BX *)`:-Tr_?r]S 7*[\Uɜ+ׂcs"8qkYSWᦱ  "\ҫhim}Kus-9'6V~H]n?K!뼳/sb7뿇qT[T VMGZ>m3kef5.(#3y-\nH@jŶj],׮'M5qt4#@!TD3p @H?O)ʶ/ yť-J);x+x +4s1g#ˌT+-D'XV.ym,5mwkݐݮnhM wW㼏*l' +gWX޶M pKz^e"(9> IΫꢶ X]Ǭ]_Wa'% Mg$s3:{XD?õګ3NnWin7s65;}3!fl3N^֬I=}D +ET'}ou])pHČ ZwFѢiҫaT=1^ ;S2@W#ǚW3nYh[vz(ojWN۱ZnB\zyPf;U{'4nW]Y<f΂ay<3qːsMssU?M}ka1x͖kgY0A%u:)Kj=D,Yf}4M +Jƥ~Kj-C&6J9ռ94iRH֙qxeS#ȟV5lA϶m!8Fѕ +t7}%KA>u9ԃ.篡? *$^~liS dCLYM/!,AO% .\˳hjO|Dm`e^ﰒK(Cs]iVW[7Lj舖6=-NY`X~mK3b&am_ǵ"y.gEkJi)mWr8ErdQ;֠Oi+<1 AG VbtK׃G*>&! p̹%aMʶ 3h;6,s4FkM?Yk|ohLٮ|Twy;$NQ@x;OE&W3lit{b%.aF$' }p8Vn8TRI `5bUquR 6 i4 y5P05}8$mO1YAbiHՔ׮1-j\K|;k^传t :[̻4ٶJXT~E 6+XaS$1=Y+f=oypU4}Y&fү\7cSG$©=2GTESj:YrZqj H#e6vB;LC ֮iB.).r ጰl4S;N7\x@}0%06:D'6̼Z#XShUCG#^LE.g̦X|33v4"Nۜ^g*Utqc\9+^^-&m#H,Z:l#nH ;>kԌ| ӛJ`݈/> \3,!vbdڃ猪%k K%p* (N<nF=2qQw<1<8C/[XTxJ%IY_?AmX%WekpRݬH3\ B0Kuv.;a\)>Q9hֱR;u,ӷIҶ@گCygE^EUa:U,eAyԏJN9Q/>!Lv JӸ8!4@_N#4I xOʵMx3ڤ>}IJzq`b}.ʰmqEq)po;AMojMs m~.Wm{9b4ר222nALZOIҖk}'=blxbHun+zm[ 5B6.nǠ4fZ|]Ȍvv\pvKET,| ,;Dhx$5DtuFѪQӭBQcE4I/b-#Q1WZukkw \o|I(!G|V$Oߏt#9DCj}y +R]LN2.F^ $'*շ6EX EQzT9tt֕_ gۥO?1_rD.HCg~Ke~]vKn[;yL08zsoZF0" 2Y9~ƍf.K4Ӗ.ze&B܈ףUke< Pd-!ũ™J PrTҥe>0p*uA*ΫNvu 4axxXnm3%O` + d;ݨ1-ylD6 IOЂP)ōtP}x'FY'0@}lf0%!8mpŅsUQMTFe-/2"Uc])E{t\f1 +WӸK8s;47yϸ2O4Uǿ)"2 +nXTߌ hYc44%D\Qhg| +FF*zM=42"Dz +dzْEu-fO@+SVˊbA+FqQ@(Hl1qKBtU,#8}Aٴk*4Dx 6ڟ.C{ehXHzIj#ij%+WzJK/ zӈ`hT"Dw-罭)اsIjOBB߃<8+׃E@^ i7nAߺrW+G2Jé @Nc02̓M `Clٺ++2nhUpۼ0UŖd835m=ѭdM& +"OF!KJ7:<\ +̓k'4)ֺm*F0M-=w^6a¶!n +d־^~eI2e f LIβ]i NC?1maKǯe;BxJ`Ia g=-p>gpRL~ Va1q kizUPriH0=Y`?Ѵ 놕=3vJ5]2>:u=[cDI|ny +* +4eF6Hn;\B;=R`J&v[ ]Z*XB Agj)_udL3ƦB݄Ը1\S.Qo2dny^[Bj}_wQ:!*_K(@geeb#B"RőS +5|[#%f!E1;Vס)aJ}|wmcw0L ;~/m'"jSMxM-:1 F 0$dFW%o:oT#y1 TZVZ4JA/}^y+@"(nKK/Ӊmzn|&S#Ms&.2MNgX4fת)6x\Ӗ^"m_Ψw+Յ_[马Y-Hek' +j4*/ims_uվ0#]M?kzI`*pЄ4(s\e-ӹ` +Pa*F㢪y#n+AEdm ': k:a@kҰZI%\Sj_a?C{.=݇>LfvftBMՆաh[JjTѥR^y E*Ws+{թk #gC HR#Z3ŀDz AK1N{1-ȉ:鉃UoQZש2Y Zfm֡jw1 aKOy$9@t_ d {Y#f)B  q[0B@=M2OE͔QFDLg Sj6?Te߮ir̪ݞ^lmb8wBck@Q%*:yaIwtUmtGz]A6-d.&@Fr40vzzchkV:r4$Kㅉ+E ô6"g4߀!^ mKJaH:/ԞX9Y׋|ۑ jf~`e֭J=8a+8jrjc>|GϬ')&W<Ӓ4,ϰqtmYu%ExO2ܙ7^棲#!$bRc)Hhm e'6gܺDh6doZکb2Ԑw߹;?NxX6 +~-=6Gkp:l@_pգX,=įIJ%Y+v1g*,J<> +axxU6NsRyyHi8Jl,ӷ;GD7B*6TS:3Z 2.=kBͰRUcl2) @>v O{mOiFd+@5βf5&U×f7m,KTHFV] (4>$u͔UmDj #IS9|g (aL{kWhx_$l<Ár P#e9G5sp"e6#٨B߇62o.Rj+L|4 _0S`qNRH5sPkYĝ0Ҿګhs)O!<(?({ճ%*#l _oo&yTyT*{A{X",ȉ恃;gXvs67bzodDCG3I$J'vUNWZG!_j+,d㡊 |W\MR5?. lzn(i3pJ'=ӞSjnawv17]anvf2ϳUy݌,R,v(G;ҡUo<\^[ nwY1mOgVMdhO@*hDZhezô8z"6,D)3ۨ5=I\LH +7XNbm9+ wYi&[zbcyףYit:,@Q#xyVx|v"s񺄑,3?F-*/G.Nξx؂LJ㼒X{ h`wC遦Ϗ2τR 'GMbɧ\G0 O![s &MHLВJtՆGT[YV8+2VQyɄ?D!Ϫs_ rXT@CY*#J3ZǛrtCH܉! v Nmp !a0ۈ{LڴTg`oij\է=⳺5ӜPigY\NU~4hP QeBiCڭc2T;tcA l\hu"-UK3mK 7!hZ\U!N_!y@/96k5w3{O!n3O1fw?zu~ZR/ +*ΘҎDZ4k*0s6!^S1i>AU4b~,~k<5fҾ}8Gٸ.+/;cI1.s~tvHg EzO'@ 3! C3 pi suy}i;ğk{ĆOL.' +#eUYtnXfܟ,e"&Ύʹq2&L|~cUssi| SV<ܲm U4 Hܓ7n խLcc4.U#W +"%zOEb=G] +>Ǹ &Ox~- 8!|ǩFl_ɩ"筕o#hƮPH/gr% B͗I.*ʭ*"P|8iVHS%~uT[S1Rqnc8OqtW$={{T%|$ U6BgBd$_ +2 -x8w]J"i7 +5. *V{ß,cSi0b琰@Lb7|.[0͆xfT4ލQ?o?*\9qGeTku0sH>^Ӕ ļM8Kʫd/eخ6JaN5U=nz Rx|\f3s#ֹ{yM:qƉ%}ѵqoK@^+Ru#MzGn;vL[gS(3'mrk Ε1)yJe|pc=ISN9h\03%QU!/}=CBN}V < h|٭m37l(ױ +8K{(1vb>P8gUwDv~?s Yȑ*7%]܇vťym ܹh"::y3j+{>>x=ku!H]2Uw>La-m4Å!|dfPLVwV'vnP=[[Ƹճ8w$:o6eMR^N/{"_ M")-˻#֥]߶e*_N$t+l'X ~S`E$ : Or`Pc&J +˷f{:3 qMyxUUUV]z?'o%XH=ȸ`EKr.Z]b]Ќ%C(iwrf߮`H2)}G33J]u4Xo$M9ʟ#*p:!7srGϒUޘjK{"!Z>%gESn).ׯZ<-:>‰~㮯mX4mIјH[(uyR!%];2;^S4 %~~>_4Ck?4Dd6& +WR(W&wn +Te$]q3pu);*H6+HDQKCap5?b߯d?JW|UxRGxkwmig QrFzx՗2e"Yęόy_*H\ȯu5TCʱ2ԒpJ{Ѣ)/Fr$ ۺ~yIUuo((э%]p]ư4Jy.x1}c+483Xty;e}*\cX!_~n>p4Bm߲//6ܮ[$"[3.'vvλLLex H)$HsV%.;-Un_ص.K&j9ccozQ_aRpnmn:WҞV2Ă\iӷNu6{IeտͤWAaRzB.5)"c8ɯER"Pk4#n'#R3Mn+Z^_#tt|Odvv=݁U4#"T*rɀtQ¤Bװ]4i!TVDÊfkg'? vי3 і9'B}Ms,1vr{C$Ե_V[0jjY-g{{ߙ"2T'ܙAT_O>bDcS[2uoؿE&Q]A+.t>~26~36CS~P0&9f,8o8bԪsCTUJH*J|y(=l$01+}1yAN;X9IzZKbdq̓gAV@S'/~1!fDe'Íp-|8\KŘPlh4$$[ˑr35K)-ҒtI& +SO)] Mppʝ6XNg##Xh;[=$WT-2{:v#= ΃H+*!=R!Jú=C*f*x 8=*Z.vdJ׫%tamh͈Ujk l*C2-b7tӹ==Y~ylƚdǙ6y"ܓӅ{p;*BoGB2-Qvnݛz&YQF2NcviT5[O >wxr0ǂDS/,ރz+D:<#eGT9.]B}A-Ê+m[96",9Ɇ淮vVR{7rVg @EbZ>a/ެG:LŎNavx۔˛Ω1cv-2u%JG+/FXP.qGW` ]+~KZ);f[sIyfĎW`th:~?diNiTu}l}yrW4&Y5f#hkNJ(/b^@s%(zO+Y +E3vCR64 e[G/z  AeHHoɪ.y/(OuB b Qkt!I%\GK#oIZs86! +LJJ#E``*) ?zW-:ߟUC;>V[\/e8N1]P!LJXG:θ T ĶyN\??^Jtii?M8}Vgʟ?4kW?0H 1-IR+ydھ||gv +@c\L=#lRfn$]P &֎Jt n%M[" S h7Mxhw:{R\r1)z2\݋_O-F *0N$-,j{Mз(\|]$Oጞ2c-~NÐ2WJ RbNLk_߬nOf1Xbv~7IieɊ2_#Pz05楊v4?SjqK= 7/ݓ@RqslN o-^ϠDFBmsލ0* e7Q32g`IETKLzư5YrZD qaOΓ >R;ߏlH xQM~('Ƌ)U%wR>P#=P{$2PګkKbǙMl@V#ujaZHr`!{/`X`yN*P99YQ,kX0m.&ϞfL!*Ỹs|枫~R6 ӬǶSőc}"ֈC?-vJ0۝.}Dah[ŧ{`,fv Աs:j32^/27װn+gWΥ` Wm'hT->18 +HŐ3Vy -_ I_4ulꛞ?'?|hS׈77uo \Fׅ/b ĭ@m'bUkL4YERDG*X5DivJDb3."2a$e<]Ղ晘%RlCWcgbJ]mjw˳N`g )iMXQq7o]2 }'9QUCg 쓌{܁BHsyo=#]`^fUMs?NKL{0 wRC*X"-NUY24/Ob +0l[4Nl fzEU_yюd]k蓨|9 -:a+F %jPWkYsninD$֍=O{_1eqY N-[_]I8HBus@fvTg6G.g ʡ@9akAms7_،>Lx1EU{Z7_~^ q@oK=0O튤#`./ݾYͧ΃yd¸[ęd֟2[l'e[w}l]4us8n˂c|Vϱ6l*&y7ikn^+b׬ ў]\ hRc*ʮۙ:r'fA0B|mf: z6uzѮ Y_ _`\0]B +[-q A}6*1eAL7EJL[ɥ`a:ޱ?h7'J&[ЄCP?"ơ'rxW3<t˨ж2lpk+ns[cZ̈W)εSklqam洖-%GsE +I~[26)jvs2̿ǐsaX{ M{Hg refy+a愬Aj4o^.m4esȻ< Jw] -F#Q_hz^EYǁh$uJ9{!>҇hٰ8{qWX߲$˓xR?̮RY;U\3+MY1 uצtk%\`Fǝ ݃k%ߙeq?( m[@¶tj +08dgrGFd. >p]r-RE)?>" 5Yw9*$ƴP@biַ5=-vj58ymb<ُKgNq bnbֵR9̈́;8sfq؇SW})Ϋǐh Z=pfz"`1lH2u}2u*yWs0+p."fҜzj#$N|6.5¶aKDeM[H!ڐ7rŢoH+-GhH23o/ kd +d}RZeh;Er\F@ON>D#>Qbb2z+ènV;| +*̺K`ۻG1okdG4'2fd96mws4)ײ:f+':Ւ(`RUfs6r|\U&2֯Ι1=1$V{W cl3ٔfA6R| U׆ l'7r2Sқh-ýSGҕC۪+58p 2%+2Ѯ^169P7-N \=P=[G/UW< }qA|%"#-Œ8']˂ kdTv:X7jfuN6JǶ uzy*Ok qQ['ԝDG>ҨUCN R1hoi9? O%E_1ĻYeb.-J9b÷ZY^Wv77wNנ!a۟5񫹝~SylE}yOG#Xt.MQ?Ys 9JFIM3{QM$28P&]#9 oP]KdWc\hGW^Q7Rhšɭ:P DlygcL +Dmcu8Y~);\;6umlԙ,E~nG9_*;:Z0!5wi{;+n;qpnţ4f ,c7բV7l +tIjIWR7cy2w, q;c:mۏυ~ X[S=i\= +VAz<[ݚ=g2I$6cy'ѝb +7w9o xjBZZ ZsC/ e`ߚ\E<7,7;jS(F6ൺ s8x"Wq^l;d]> XmkҧtFA 㘞UAuБՁQӣY9;Q _PYWA隘ߑ&!fyh{ߔV Hu~4647}|.zv}9- 1zӏiz9ԣmk8-eFCΰv0R6 YuˬJŢ;VX2|W6-Qj66~??54NZ3<Tuh| 7i{{R}6M\j\\;!nm{E-[tgu:M96KmYVy~> ""1ZW3WlD~~}_ sy>M$YA`X&oZ/AHx +GRup"6,&%3ֻg1eY mw[5{Q"HjFy`hO:{*-s2xn? /pWMm%S2Lo 0)#KvKlEo=[{*mNr gn GP#lK j2ΎZ[(Qc"_z!oz(HC(266cgqi!9@\׵V{;;䭵{žv`6bW!$+AypVMcTZB +#k]71_ aټ0ǏװjI| +$."21%Mk-'lir +6@T|pmQ + Fv,E!$jޠ%RA[ݖ(L 㵂ilڑNxvJrOIqllb:4EgnT$ߚOs^*qw4dn zEhNZ.ElfAͲ$FxњNڙ49|{aF`|DVc18VcqKu¥$n +}*? +SgW zxy6=,.,ǻV[U[hd_6y\\l +8ݴ2^!s|JJЈQ#  `94Bm:Y` ]pCT1cV9⁼#Q`,f +$)vG[ +iU2#,&k[X&5,X ae0q1PqR_?ʰQ& +pւ8 7X._g0>2nBς,cyol %irMiXK?4,k02x]>d<򣴕P%ݹ cEAwk]mzg$'H%SDy#8רB,Ux1s,nr ,kj|{J48&Hq~oM8r&˯zjׁb/ V8z]HJ?Lj&~) Q#ant)Aߙ9SЍ9I@3Z{]J}-.:B.9RԈ(dP?< e)&CTt8e}{c +1Bs3Rszu^wo9>S5Lu2|Iќ)vFmm?jdoe-}||=X(Ӣ=qm(0x: +Qz]u+=WZ֓BI3MQyq-ۉЦ"Cv X}Sk{2\`rF׋}z0WM5ݲn햱:pgZ,:YbbЮz/:`slֿ$:ukknqMַkӭ4C-;ڷPxO3=ŕv>w9q3̏N'liFjP+fkki5XF]b[a@ݽ[k7͑[xj"%=ӱ;nMq4D;6Cnf8:f~=3ps^T$_~Xt.{ʑU~$tv7*ڼȨIB VY| L9C{`8Of ;M=5Uޞy|oLE4խ?,]=_wO]4v.ßq rBnהtXK]=÷ԐA"pt|_1%jP1 W>)eq S:Fw_լ5K'"p! [BTɢY/DeVN@ҋ>'_%.Cv* ++8D5G8$&bռ@&D´.Ά[2iͧwf YXWbCC+CԮcb@ư3qȂ-Ԥ^ۙp 0B9M'2t γJ7(k4~őkKVg[wuxQ] P:>TG!9k}NHH +]ILGj'a ;63s2_oy=q-*b<:TA`Cvʖ/bԝPً}m`+nyg*XѓqMK>e㜧, +ޜ$r_dsH;F(d \)YMCr Vbb׬cjX]7Mힱ1H-`NPdmo6d& h~؁4̕t*+!5lW_se)ܲC;fw4U9qbVUy'+=aT2VfjuJ= XJ Dgo +v9崞uZdsYawzI_JyT9m6V'm8udmMի~,P>ZQ9ÈOG baUL78Js+{Ą>^UR}Mt3Q6٦S#R!{&$,K3V NdeOٔ߯jڹ^ +D"}m- Bpܚq",[ÚP'fX`{id7k?sy|H9etb%Zgiw]my2v_"`wyV DŽ<(@7'lz9>ljUT-UOL4:yjm5pL7S]tLen0 ւW̰UForik\߻OI]4a7{ 32bٞx)F@b g#vۃ5}Ra8nM(|e.*zb垮^}rkEdy5ES,?/ <6(pW,d竏k~zx>"UL1OTO[>יQy{ ߷KC&35u3ZϜTv[m#Y >:hӷC)ҐL#$>߯ oRv#64{ 4jwibr׍ϼ{ bBeZԧ{ru<23p6KG091{U#k2̺i);'y ܼ/ sVι;zH]N#RyS]'ͧDD9?f,1FXѶ88z2N}䊏YE@:Κs{#vYPi0x&ƙ#i|hbŃ`GF? C˟k{;uL0됫llC\u:+4l˽=/'sS)TO=VɸPhUZe\ɤqRtؓui>LVK?<%/5s*~h毦K߭h8Q{na4X[*[Ihަ=߯Œy2?x7g#^G Ze: 3- oPjedΚ`Qm<]Tu8 "Ȇcr݊{ڟ5RT>=0yEK*Z UIyW=, DD:l$ӤcE{N[ x\f xeX\Ƞ}oSBq@"gSmmcCAȠ^Ke8&Q\<Y%so Gnvn<-ϴ]@;&-hbծW( wFmy ˼$Q1QCGu\2"SUǬ0~ޑNQNܴ>Ռuo klU25$F:r.)VEL9=}{hO+,uN]<}gI\cxw|RI2"z-Ozov[P`]DAi;qLiɚX 3h o4; +6Np\9+;~Yx#P+AXGP<xn [H7kHLNMu,. VtF4wCj鑏2]9N&|P_(@.Ϥl43s21y(<`"pwi*Y i^(+45? Z3gtQ׷N_ k1_nKI7ܝv_Hf،s{u"֕4Y@5c@ocZ? D iBgB-!K\t29J[ZׯIpH+r+؎Āfth[NߩoUMSKKg6}_8?:7{΀C0Boİh1uGkyKvX.ܻtu״4&!ܗ> R ȁa_'luC:W4Nz+(Yhf`Nal~}B$`KTѧyպ"n?ma#0A&/x_?s +(Pߥ.ĬjPpwd@qIL)J kt!=ƋhڴM+ R\at &q*J-ƕ$_-jKM8Td*%ytV b&h~ +3-쌖k+hNǹt,@֭?(00y=Nвl*4Y gF7LTG3vսrfsG|wfc45cBI'^T:³G!t{hB{~}`9Ŀ3pe#iLhYfĔܣY3SH','y~t`N/#١kB}0YYs +ӛftHr=ݘ,=?#q'؀[ru.']Omr.RO޵'R3iw:TsM|Qn-7k I_yVxElI|NWݹLb@bɛjnÓx"8$w*j~v hTPSԱpUӣK#FZ"َB" Z|J-m.cFjk#.:ĒqJ ~ (iZF5]8M|>~j58 +Գiֳg.eOjiK0' X¹Gq.*ozEHd)"N Y~BӳɁ[sā/E!C ~~nuLO0q:ݶupbfl鎂\ݬ>C8;jR +ݏo +f5uP*%W j[OI VSb`H ꊛ`O;A+hUkkT{~x]Eq3 :VR3ծE\~qwS!;dJ0 U,y zHB)Γ0/ke2rZgbTtAD 5a(\X@[ SKl/T}d 7 XSGop.@ːxdM?eXVKcIԓxMҠvu+Ir=0|=֒3I"o;m[l,cMF"fEa+C{ }y_{{Mf;6exc<bm5:SG}4^nt!I,}ĖVL $%dⒺkd7'6sxꯚF-~7R}j;\҈M51ї ~Nhgj[Ja^ymݹόwa;OV1j-J4Vd̯Aʉ`?ŌQ)j Zta9g9 "$aw-}BU,݅WC `+эi~Oh[uYU7ܲq l`{O-0etD(5n*Dۨ6 Ӏ,_v` ]{}sHΪWA0Yv밳 Kz_DafGD޺l^W3, +)HK$W[3:FH9ini [ͨi_SGD? 9Lh׏Bz[>^Toe"krqK5lFHkbgEKI.}XփxbQ# lH0s(؁ + I;_;E(32x.BbדxKFeo[sg^?d+\E +zg60AoY#N*6N깩a"ybNV Ϙ.Yx6k'wq;}tʣxHڸIjU[xwuڊ"wbL)b\?׿Hjڢ=`Ȗ(-kKy +%5 B:.L_*ǘn:ݹ f#Z9sԯG'>jyF͆R6?CjˤRg6\2N b5gQC="n i|teҎNʾ^ PM)R]ISAH[-E'Džp@/(oPITBpcQR-=CF5%7!K6rϗv/ѣ4}N yPuSeW @ +HG k˃ŁW΍愸FKk[&28זԦOƧi)]q1_0Z:bL#wy'dFa!n>!W?,SK;9uB2bҲw2F)-Jр/?v518Ȩ' (䗆qJ6z:aC%1u5COοcdyb<6sa0fZc@C7DaljYӋaBK@ 4BMsu@zNԍ2>1GqȁDmrRC:8r[U-u.,♷b^ |~ɦu֙@kܵ|"HG;y1@mrhشS ҡ/ʢV͡+c°v5 9ڠ<4vH6&CG:TXY}NRfPA/U_QZVV&EV)Ҝ(Y٩@9HcT@:Ѱ gl:ܸX3m+G+, ^G#X-I-cN>ȒlJj߉*e^=T*Ig =[Z'PRfo9o 2ϭb (؛B28uo[{ovRu2M=e_poiRs^w&.3 {!;ѩMhnV3l#U~e - +0>};}v sMYX)B3`Ǹn<ݢ3*h`: K>]\t9"3MIÎN5nh%$/Qޖ;}YϚ!0AYZ&΃*,qx]h[Kt$Nz{hצU/Gjh穏isyF4KȞqZ=7<=8 Ё N!5k-aX5@/t_jmE'ZKSJQX0 epVрjjxdMOK18ҊooQZ]I+HAa9J)'lUͩ(Vmm \ :K&l]va@;?(ys}pPE`Ki]ff :tiob=cCsGD6_+a}n[98݋ޙDRrCR [a?fGt^k^-m;|_?N 6F ؄8 tDҪk:XukO.';~B%+kG&aJ6į45~˟?bI!ݠqiA&@jN !oވr-A~[D\fu7s(aP`8гxL)A {ZaI"i Lþ?IJa{M}!I B۸OHˎKoDZ5|K|%ItĒ$ WSb^ Y")b J9o`ζ4[E7U:b} xў46|7D2JtᵄP)",k7f}GUVp$kW6Ko>ql3[[Y0n â>)YP,p7_{sEw&co]0Fv85j^{)ۇ&KڛN׷luK?u;,ޥJЯAMv8N'e~VLWd5i 6m&#,V2Վhײ8F$Jv'mPMҷfgIJIL%mXM*65θ+\?08t[^+WtB蜡z5~GɪAáCй[L.Q[Kh$ŷ_ߔ_vs.WA3ZzH1jItr†&& Lg^H8ZҧWf65[nkwQftioql;ޥ:W-^Lp;a|]\.'`(@z f-rnrfž]m`\?F8H'I/fj:E%۔V8j~o$+Ҟ> g#. +6metwhles-m K_.Ђ^ZHoi\#OfmR9"Θٵ;IQsyk[Xݽbx/Y*06֍Eg,+YƆ } Q f(,]W= (Df߯>qK:~\#{dj{S&r5%tkmm; ,ZajK.adI.:^fwݙaj=K0BA46;D:#/ #-,PrS'ÓNR/gYX^UU;gz"XaUqn@1uY[V@1sdYGyd9@t_ d?ÝoRjjٷ1 Lh(Zyz@{4Efb3vA4J5?AO/tϵoÉ[ n~,W/izLI]lRb +6V&d2rۡB j4C©6ړ2i o,EE?JTʢ;ӭ ]Z\`ALoYD-^)}8 辤ۦV^.nW=jzD_;Ȫ`Dv=vI aS@[ c^Eb`y AuWb~VYFf?V~ӵu2ǜ͘-RY<^\?I&wweNo F4Z}[1j.m>&}‡No7rۼ-JAMeljݞ‰8h%;= a՟[}b)jMެ9lR^gAe"ѐ +T%9ӯtf3T^ʥbN|f">y:jO/$A#M[ R̲j^zQw 5kZ'ܺz%P0j4*J:o,d6L}Z1zzXnsHXq2ZQPۼv gɆhWF&]GPkf_t4G{`]4_f)y+, +WFuOR z Лّ< b 5c}WTEw4,meNխknOVl,,EA!!X,isK;QIwp0SP*k]I,z{W|8zk{jfs"jC3 ƁB r ;&}lӖjB,1?+khD +Vƨ +8%m\c֎?(]óٌ"p$6`AM|Qf¾*_",Uw*p?5#wAXD]CD2m/que e&5dUsă"1)%!rOo.PMK N_UB߿A+_VGlhO![<ئqXb(I!e jY30@Iqh~7{w#[Ug+N޼qHʬy5 )}2L~U~HFX6{1^0S#@ Yt>Ç3t8"pb 5PQcYS8i[Do},hC!.Z9B=gE">;A,Q;҇q_ ҳjQ)'l: ʒS'Es,/BG }rt4~n9, M`bv[m7"1 炦VXHTQ]+vˡ*oznzC*7 fB{"jղsFE#b&iv~ޗ+:R7k'1qi=JDzwL<£  4nOO ->7F琲$*[bFe,ONUđOEaE_YG3@9C) >)ًV=;tTݜ/ EBq88i<3P$<H&`08V(P9nyW}-|׬ew7(T&Z<^  pM3r*J R@ :LUG?w3z@ EjwSZ81-.,:NR$g;yla"|M/}el19u|p E(O FO|28977ɵv*uĨCQʍsܺ~c{svxQoffjLqUM +of F\&:]-h‹k r^:&a|%L +]3MH9sQ=#.m%G%*F̬K7ijUy +6 +K۵L`:2{c{A+x[mǏPljYꬬ\}qW9sݢR: +c+]=F+:"ǨlK m#U]11&$UP| K'7G&3#)ͲGǽxOS"P0rLmvFC&/ Ueh(O'oYYW4#2KP<=1ۄnyhnj18uܯ_T09>yJ|󾗕{$}"i S4l668=qN_/k^rm|k"( Mg[MV]`7lH6vҮ x0j/75Z"rcYHpq&g LJA妙 +jӐzRJ߉Z^*6J/(Ǻ$tcj8E=,z5#˰3Kٶr:"Ewh5|:s֒+At<{=pvsP'lJAZg0_<\BH7pg`jCguY$ef eG!햤oZxΏ_RU-}t?, +}2l1)K~j5J1=&ϷrjNeg$}L:?$+dHɶn5E粘#29jvp%?gfCՈ\/(^_V! )]bN&L=M +;Y8!ΩiSWZR)Ru9Tά­ 9DX;zNlՔx>cU\b;] ,tv{ %c> ++ EerUkn;jxÌ3Nҽ Rmx +"% +Ղ'qͲʐaCX4w4(F Ȋ@ա2޲ 9[}%0p[I 0jq&tV%6eB#Y8t~YiL:ub kj)wrg5LIUJ)g|Law;^NU6G!u7R?1N# jfLmʬ#46YRz '~Rn:?FnfC*ÓaH"em !wS4*XvЇ7]ګMr/r*7 p ϖR4RNʙU+Iih tN_{ Wq#ܢc L%tʌP*Oz 33{5*$(TZ4[)vLji,XNEe +>.P%! 6,)>sJh `y +Z|mAth6='0Sͩ)-bAiD~Riu,H_ +Pz'1=ciJ :[^f Nt*]YcX21= ӌzUͧZ*` +.i3všF1/~aeIԟRbГ\n@̑~7Bi- +:ØU60Fk^߶*X訳k*97bZ-Yod7jˡr3U-b ^W-{ .қÉ}C| f28S0 H|vjW鴛,g&ӹ=7_-9 1}Zd.9az#hcRӞ}>=8s96Xb`tӇٞξ-$1Z{[x-TȴBRH1~ wc>ſgC~3r)ƛwZu@8/W.f] \n_y${Zf#9O'Ix=p qLd=vVU cyiMo<'0#Vyς2M@m3:-r)]Ω0,>&m:8z[c-W+̘S0 ]qT6NeTn egCKElүVQE:bL:§lX%T/Qwa?]Sh"i@P+֋DS6).oǞ|#fgɡuU",1_drDjb,Oz\ :JqLzaы|TL[淄/amfb RWgA(J{%S;ºXe{zB^Y}$X1+g +:iaVgr-= nb*8 PemS +:K1tsRaHcOr%a-'~a7;`*yQp'i_"aI|1;/kܮꃭ]xa,(9GqY\&2\XĵX%HP`ӏK=qffn6Lf?#>E2&17Ml yR _o<.Sy#c-p괭:Dy"XM$%_Sz3^>ZOJ)51 2Ѵ%w@N^(L!.}[jVDj7\y[m!*\#f7B`L":&5OFNUS"ϼ~(5*ζvTSI͚Dis[Wk9anD֌tq>&DeG7^dZ;=!Nq.x Qno':҉xݟyrG.dX|iAegݶY+3mj,ux1 +#EniFu,h?lxMq4X;Wyp,Z W4$3IvP uO"ԓU#[s{աbljhjӚ'h*Ky9vE:9}wʭ{Ri`FZj5<Ȍ_۵t >.ͣ`*8ISjUIJevһ/jVh j>VSdHlM̋ I|J#q|Hd ojï Zzi- Nuf)L1m2u]jN^W/?RO[۴yXEK9kyaPؑIi珹aPz'q ZռkYwI"͖c]8u|^C(Wrm>J;dX W[϶-F4GplRnBm.Tن^ Y^,i/u8{:"ɝ'YG?"<̈^] τkk฼G뾓VrY+Ų+m3cNni.o{IғIAIeeþv[:۟ 6"5KJj/Z'>ul8#GE2.۝9*>9DA(~r\:lEj)/su;A8s”J9eh?TmzU?Q]UpLyg =RzիA~hug3/3ey+,ӧxɰB8&ڧ~ZlptsYP)7.:*_0&TFL}:%N|0~5$4i&2c}(fh~:h\C|L3̼q Pm`B[EX?LlYËև,9;e6bRkJ1܎/ddxFNd[ɋ5pMCy}/BZ`ҧHjEUy5#Gl\8GŌ[ 'J!i$eL/9:c4M?#ekel4JrJ]ZmN wYvDDmG^kN} I sJFv= :cJ[b\险+/E/`A;ɗ([ݒ'f,@@@%O}>þI`4RG`]:=hi #S4y>0Bz6QSH1aR˴i>jLc5e\^ hQ A&cjg[gz1AϑZ IZq43%UomZfD$EjY=KӌPh*ܰ?\C8 80ϊS[c>ǼlB(8M#G>&df\z&maj4g7H` 9Q$u2,i9 Dž +vRUL7^%Kߝױ~ů ̜9y/ ?m; a ٕ:vL-ʦd[m&~mv&B6{0CqPm{|_KJĻbKݽQ8a"x2˖'-XʜnCBXf4B?n'a6./$ rMt.}u;.qv3)v4[ οO f*C:WˑʵLj'zQ9APϨeB$ty׳3=Sטfs[P%kufԬ韩ai,O:l>ނ2geb0? +h\GdPeNix ,Gc~wsJ2 L,3B乜nz\k)6mby7xex9Yt_w5ɷ8+9\{mC[SX $Pbh-%ay(+Yi44FUmw icӒ)d~; +Үݘ;| NzCzI{e\9 au^^t-ltjmqWw\_( +иb|i̫9m?) +Se/)*.D!$(CGOF^m_7\40g:6-YOjLPÅ#ҁa'U/͈0o*1к]^lLǒO`k-NkO+gE ]DqZkEݖc2KU.n3yؼ0˚|Q~;P y?/Ԅ1d1.(0FVLn*aN)1<ޞOp&(D4}wA=6ij€8m:!~;=* Ҩ9noLÔZ SϿc,D;.%!963 -xU2fr*m$Ă4D9d7Sl/yׁ#<!zt +C*)u*eNRWz N8vռLc%|73L{ٽi-Bc]FRDU])6ݰoxQ,<\s 2=o G3:އ 2q#䔌`;-QF@`=7$8oQp0RO :^ [;-T~$ۤ |)a__FmI?̵Ń͖̄uOG7!q~}+^ut4N#tW9tu{ &FPxDdܝɄv^-ҧs.om4߯@XrWsi_ n#X9t70SHCowl.ձ"b04R rPaTUr8aW}rPVЬmg95״'n{ۊW/I{w<|-@A/-UFew,5k;mGeRg:&Q, 'ȬW4neZ"A㬺y_JP^!:=yA[c8 _&wp{vÑ2%;WwoSvr=yE@3٧-nn{On|\m z_@[|jijF7-qߟs'_=΋M^Y.5* +4|c[||P8#"frlBGƫU8 +ݳ*W[HyXUTklö@5Yd쏒Llyy,ͮci[7,1%$U /}w jg0Fza~ZĥCe &~3 +wMxhUAzDٱp8$%˫MdL>1KU*.6}}kΗxp8B镫Vy9P%ݶ-?o${X1*Ct׬gW%/}EONVUg%]Yɝz[gb[>Lm#}ӆġ xƽP3 /kO8:Bm_)d~A:ϣ^H 2Eb\(V9E|K!;4gS8Qn3wƩH +PHT"z =V74{yAc/VVEtwWݏxs=~]ǂ"uKt5|m4ZpHWf[ǰiɉ[ćO~,w>Z;oG\l|Ӱ{l @ ?u **B G7"\$O‡{e9ֵ%¶79htF i >P,ET*$OW+.0;Pk?4rj@96ߢ\ UL4]<%@/E"% \t92'Czݬ{enj"'t)(I\v)oR$wyĠq!wub/1H]`bs@1,Nrb-{!IC3S^&8Lޢ +㰋Q_0#_v$MmC-̅#k1 n1BMTjkR}"-]oh,}e3݂Ιp8 h^]*|i& {h0[#δHG^#[ Ui:q$55}bE*k.3/h k44;M+L-%`x מ +]֊X>ĨX,hK@8K.s}}115LWӅ6'nAFm YsW# ;!I]wN2 e淙 ۣ'-_5qvw74{ +0ial8K/x+k6$S+ΌWq0dAA㍜zgu4ܴ(cs(z^q +=ҁKR=7x#=aSBlc24Qz6au31V+*Lr,^݉ڞȽ܊i9!y(>ד阁]?Bo\|;O55h\G*74Ǵ 8r7F1$T,|"J_*CЮEڪ|]) Zćeom?'m/Io&pʆDs95q{fҌfcWQs;]ɶs(쮴iIa/! `[Lb˷Dwu (;kLu3$(ʴ\%p9ܿmֈvM|2IF螧 \ˬoeFKs^>WaGsRo<3Hܠ47A \*FK:zmη Cx' +3]@Uҋ +(쪀4aǩ?oUEȚJeO#⌻T+~FZ"͗#d,Wq*T+K+mmNں~kZͪ8W + +'U˼Vc%,t!W1ȹ=q3pjЍ& +8w_JWO:zr[eOb9d4c:kE7)Q\:>=y2"i|S6&2w[K !4v(t|6`EAd_ЦSZ&S(u0}вj,HdOt3 ׋9qd /eb}ʣj2pVDCud6扭N%rY0lFQށarzs}V<g6,JMNoJIXä^Oߕ/lڛiqxo@(i5y,^ +_f/|7m 8'}--2z =[:czun׶tk[zzIø (S;ڰ!ǃ&fd0a +*,9آDCj_@Nz]Zێ^z2VS sD;n_)a* (^ݺʛf +:㭪yw! +t[Z]&)9Tx$f Nm֓iwNx4}XXLbO$0ͩnbr~v%IBXS`i3;IX+ΧeOsݤt hО,=4.z>_AG_Q<Ԭn +|y:As0Uq}uK4ech˗ +RAN woi{;{ڌTnJ+?oC SjM'<6v%oXwB*xaT|86*v <ɧz؟C +4`:<.e(Su~R,l8 Οi~~1ؼ w#i.kϜ`8BAiyM܀s7\ZȬ ְ^'DY;^N VhLTWi*e#.w7icijQǭ2ŔTŊX}Ѡ0|k \_w3^tL&T[qAqz&Y}6Lz$I|J)XU-cY%'6l9seQ:Z'ucR^jquiؓ[4_MXΤe-IgS^aǟ>߻"LMyJO%-dṞ*WO5;mq]vwxEOڞФVÆl4[O!wq&ןͅt̯U;U~+`28HŸ_O; I,#yw XZRW_2q}~t̤ʧ1 ѓhl +H7æOMQ{L;ľM5@vϚaR+.eCkɨHFմAdZg 7(ܠڡ"(~:aeN?`~n'pWi&KpK4Qbo/Ǿa9\I>ʎx1GG?dYjhHkC1 +*yj8 w,OӵHкHn6AiV*@|!(:1U8ɴ) +RȪ;FՒ@͸DYu7c$5Rt޴;Qұ_$O~MO-l`ahBZhSԀZ0c܏nt[Jdmj7dY#4j ˦y6־3F[(;\<ڷt3fWզ|Ԡ)Wv Ns9"6o[:whTrKPH"=`cUȆpMVcwU8*OT=a+`{((:)h(1uFRK5lT"tclF^5\\i0#ZEmPF7AL[0Sxll|mvlC6Oy%Ǒ#AO1 NsG]ljCU] 3#UMWc +F5eKd6o 0crS햋3w{%rMAGg + K IF +>.C9ϵ2g S5; +2`:.W%m F+W| nH#V%e oJ J倔S) + zo싐ZDNJT,uX1<2Pً*8!pR#l[VH{boٮヌG؟KUY-_t ε[7κ 7qXm :-i:SCS .:Iq<%#üzE1~P/R0-]eG+Z[h7I=Z(=oaϟtN)T@ڵ+`e~?SCLf DytV1,H3a-&̠SHzt hH6?oIAbb3*I'@{Lpk"@Q;0NԬ&Z +T^Ov۞6ú) +Fա<=_Mp8ꙥ#HZrAIP3B˪^V{jeôNte3,eUW"R*s@di͗u|dA [yY-Gwp r%ɚxZ՚ȶ@/|ڍjP@}x3sOk@Pk.X(^Wn/E;$Wi5 psOHT^9(+0Z'sp 1)In*=U(Sw@4֋Û8R4i/wuɺDch/ jf75:lY,άY~N3=K?PR4dj>Z7׃ Rr@Fa (R *_B ހ=W.]}-sS@{[ ¡p)pd=<(WDTF6CF7ӼXic3@Əl$~BK0R57-Pm }ŽT6!GÏPS"{弢 UAç+6(KVSB+W>uiZʘNz,]ڮ.V@疊j\v):Υk?=B/ 'ˤZ̆ZFp^92,מ96+,[pTI>RT/Z_fC^FQտ5~ZQ YlzNT&VyR_mkMlUt >֍␤A˻:~l{',ek sg9Y|^Vhex$4 +/5/h !Jy#j.@QʃKL7|B|=8ꥺϴ'-ѭ|K=ptd~.}9 x?_zuRijF9mBA4ÑR"lnHڜYMZ^zMx9BJgoq{mc{O@(-c5el'mrChݝ&@qd>5lI?Fx&55]+R}ԩ;ek#!wf3E#%`c+Вoa[/YJZxwa>NX1EcRS/5hFNMlIꃉǯKCઔ Y +1^V8véuʓDc+M=*(dXFZ"Y1$?t)/Hn$u +^FztR֘zSd@ . w@ngm߹ n;euV ..ϸY8ךS,uIº۰Mǐ0i>sJbػvkGNorN9is`@޳דPU +t~ Gsq:QsyPhJPU/㝿O=3i5G7+ 5l`=wEF^fW,o ++x]u\uR?KT+85ЇƊaW.xvgjp~*v |J8w; L꿨0TOL[9@-,Aͦ +8l*?>ȿzt׊0EƠ%PHZS?SMz6rϔm-UfA4.Y۞\_,_'-=Ę[j>j%2=JnFjxj[z@ƺL=4׷{^ἅ'Q;t1d] 6f98ݱjc(bW29_'#x@`Fd] )V>G.6U%NNٍU/mOm 4û]inxw`)HarF ` oD*d©+v{&r{枪JN}~lDƘRQ|L9b8umpls2 i*dI"zIcFu:ΏoWz28E=4(eb naJ摫a=Z>}tSN cMQDȂsjk7RZKI%#xO4Kbնn=]Q +91 "͝ ERoc2RꅡH'O%+Y&uJƤLv$gCչi= hp4-ö5[e UI,7 I::vWt5V- nPv[RC;)2m +ng Tp ?a6_j}Ysw%ɬ~=i-್&^z)j eK̜*˯rն";Ub+Ʒ*܃ Td=ºUS*8< BS zޮL_, ~vA\W:bz`fzO@>UVwƶpMZ˨FHXZLzԱ#ˍm(C\'}ԎZ0CjVeF&i$u39gϷiCr%|]^-˽Tn#3vz3:P.2 ېB-S9lRe\cl(3tj+\-Sjw3$PA75'{\wJrR[ /Ϛiw^lpT?Ig+` OUGD ;|j~f:gefrWݓ@Zn='q&=}&(5)Y)=p2g}*yIyag!0PJGGD{JdG&ͱz(2,͊ЊhVG6 +`5k=Nz0`\=4\mqƄn0N`}5c6<^Kfw5ƔA:0 vizy;s}l;yRSs)_3i7Ͷ.O"Hxp)L-GN+RI֭zOQVS{ Jyjm (AqO|[՝UBNv +6Ȯ{[C񲽝7|Jbj_>G^Xzqzefʎj^XLl&y\mQ٭t] [NY]MaPc:P, Ggq{RF G9ydq9@)s%c{KFҏk@|g(2ezN-CߎӖadtTc4H绞ƧBעh>9c 8cmwWV3O:>ҵP[F4(9Ȥ<Ǧm}ד*TKXϞR#7~HLroC[u:fp Hnѭ Ix ޽$V( $<s]y^0I_sOZ{F>6]FLcno?d1K+~鲙C}nK9i5qN#-GRMV$ߴ+@r-ѥ?ncTw<21:ڙ \`Y;N )_)ViX>HXvox!)4.?tV~E +5"qqSPL?mZ$քY]睍+0Na;y )Ձua/ݘa)yb1bAV{, Zӻk˒?7GH 8[8iU3xv ߘ%Tj1*OG\k;4̃^RpC, E +?pi捿_r'vQum1jYf͉ r4`ba;":H]x-*5Z8Ք*`PZ.\&R|I ) Bx[z_ʊB/ڔez],t&-H~a*73yXpX./n|Wu;Sȼ{@9a Y; lUlx4*gmW2bqpI.:LݨՏ';|Ӛ-՞eB~9.0P&p0 +liuıpO÷&{1:lu;kMq9b'AB TmjZʡ2~+8 Q. S: _w%. +ܑxjnu\o:4c` G} +LdJv{G%mNs>jvޣrTHkS9IJqL付 +v"cք3@ʓi:*|~ vqQ;X9hx=j_^l%ihg157**joN4|`;pm%3%{S&paA5^:p[{9񻘛sh ‹߮bscI?7K)0Pc]2 .)ZYd)#G%nX0 N3v4YIz}Zo )rTD: j.k6SI簒ap~7ҷU+pHDkyM++?LE0"? -:cw23ܚ9j4~ͧ;%H|~_MQ^ X\EI%ӫԖj:3+f! Y|n0QNImx^)a)w$ aU"~SPvōLF5Imz)lT{_pTDV  XfJg=mu#5X+ICPz#ꋢ˗ci/.>Ks7֒6H:Kd 'Xip2e|ued+7쒸eO}5)94ty)U'/PKv3v^V;r1xs2D_RqH;@:.&1_DiegɄB ]#BeIUs!LNިU067i}V=2RMHGD~vp;yi+xiMcL#a2tWuh4G*;^.?_kVLF3roi;\_vK-[^KN$m#c=s-K% DǾվ҈IۢFQ3@'AxhUy;vğ%JW܂nV ,,&t8^I=- dTWg)~#-Ͽ ~lG\]搒mX=H޲-9Ө8Jhrf5`Z(W/YKEeיX 'F:7V[VGIgvQܝ<–3 + r c@dy}7z8bhK)r͂+y4rFT~4'n ӂG+j4(z50}nr v҂ +A2 !%"]iHa#o)gFQ/~00+ ֭/tm ߶P +gVH '] Y9ޫ~)]ZIH _hhP3gRYI&&3hlMlRǒa. _^w{lۮ؄Dk,Uo^e|ٰ3SxÜZ#`/pSET ܰNQXg;tGjJT\\lHD:_~jo'59Od(U&}v^o={$ڞU JdPL4n6x]nX0mu_P+ +F;ޓ|0%aZ't;W΍b=&h`#'I|^"} +߈<=ʚi,ˡH-|fEk\CYbj.\lVq#VmP}{bך~oCs8T 4j\Tkx_pfЌ[rKC~z9Mo]5S>]&al#i/d.;:p7W!-syYi]bliH+F^{"v VSח8q6WpW䊹~LxҚ4]hZ]Cl>A{J9L/̝J8Ӗ]'fYoq +PEhrj:wJ+3y{Ćf<zo>s@FP=dcLJ;!E!?Q.w()%*n£ӽ '&!ZT;X#6r +Xz +o"v#'|0ZOLd4 c`yIQhfeܤ=L~ޠͷVs΋E $g#cﱻ CM[e%yf=u'o# CTR#4M]Xj\AN˵<lBw1ObRuddXf*Zwlױϧ ̯MkjskJ}~Y_)F|D-)]O3niVC'ÎvB,; qX#lu?L^cW:FB`!QZnu,E{1"ph^@VGr6!>orӪ)r Mnn8B_fJX +mZѻ5-vtq}*9#n͡+[~!ᾂ'AzI:i/1)dKyX NAՀ7G7fb KOtA:WfG?&%%XS7N^fdG50 hUͪVu8Q5([wws;C! Q8f0ҧ+zI:QwnA#6h,ޭ mJ/Ukd7,^TґNaZoX  #ippшLquѩeC5.hsGeT3$ N,Ka!cB+֨ytgXŶe]̽d96%:tI5^ɻA֑|RF〘*/jo:4G-a5vҳV- ZWEqo"Mab6b^e{^: ֘Yzg3U#UiIP*+p\pNh~!4.Z dyh8G6 L>c}^51L2'YwJM6U_)YdeXVJg" `(gu%z +.B>#{å3H=]0U?o0Jf씲̑^SiJЌf֯P Q>-xc4"NUEfGl Ƚcɝ-rvǴҙ;UUPNYMw~CpEket ٖylc[VOgӦR3I]PeDiGCa{h5=6In{Dg[tٱ):'eϓ7I(%h X\t$;FJ$kʥM{ϙy1AصA7wT5Q Z" +FS/0Tq] QWr`QM=D4׵RH݂l짪_jk#~2:Nhўq&~^i bO'Of;!A=gOtf ɍgsB%ivT}s+ Zy<50*X9|vc~ 0gLZ<:((KI76;48M] tfK$0RQYZiC+6}F􆣦\\X3ŁInwy _"67߯JM LHYRz5-TLL8eA=>"lKv м39 +?YD*ަE<䗧Ry}R#({*bɳd /W}YggZ`&aEWVsIv.¬\nI6z;jʝSht>uC9MfkF^禥޳EemG8vȗF`f3&rKWMĺ`G<Sb38`NLTsp/ =X{Y^iGzde]R>b+)F|pZxf:==5ȌbJ6c8ha~ATB |4P9| ;QyZ(5@Wk)lCVf.yZqA%"i2Y[դܶйb?YF: mzejTj٠1LEBKpRIaRqX5*"uhOkgw +_ןCQԯuX-ٽo[^7eU< +ew0IP~ `f/\ mXV*dtRZFY9ta enkc3zgeӹWw=ἺYjdpInh30R"V?JKAj߭lV` 5^rVԦ/^34'jB"^Vw"]1p_fMSUKi~$J"!z|ۂ~1nϺzP;=GEKQm nHAYt^loUnqm*j,[Ik>pŦ@LYu_A6vGEhvūsO> dtڒ?/mpa_;qB3ݴr؎Em^ +HܩP \OGiV5vty}'qpB)W>4`nM8=_g=>_Z=ϱPcא.V+G2#%dulEr[&[/U5gDՂ#L" cͮ$+)"=++.t0RaOfpr@6MK{e\ƌJ>uIQؚW|A_Vno5׭Py1r;xjF+Sq)Fpf$:BK7b>#p/t O2jaMxbDLuY`GOVZpXC@r*sN 1of + v+gfzs.J٦+;6N6⩠J>.Y֒5}p `blFB9D"`m%iZ}.W~f_CtIkoZS弧u)72u$ }]KS@վA*yIJ K00c0Sۨ)pR>ab@c +a +]>Ro`뗵oܛ[1LaXiQ8!t٫1:=d@;Gg^ע 9iߚFաS U%"XSyFғ +D$)Z3v-K(i.PN43pRntlǹ _ljOY0ӥ Ke=.EKY|N尭ƎeO9߲OzD?<PENnxҹ|_N1L-;d&ߠŎ1v( ݭt4|o>(tocA5EtйJfQ5?&x +Mv#ݓ^N )l@4lV9?Lmǫ]~i9ӭtsr9ͻ/~4k (jQ~ 똈d}9.j:<3m-bǔS3U}".FNK=궭߳^!ڏh@4-"dϡc MuNm +K3HpRCSPYHM++M +'M_|V%{CԴ@F@ؿ1$#eaULL @Q_A1,4&ąvefxtZgdLLifɢ͐V&>62W?NmYZ>3H1>2S9K"/dCj(jzNߢeU m!dLDf2jt4U-8b/Fš~b;gĽjhD19vlEK80!\E0dY0(b'^1ZHT]\Hb [t,4amc٪M5ghqzS/l?_,N5L+U3TqkaI +1ۓ2/EvX<=6x~7HC_?=XTLC=< " Ggb\IBV 4WX!CZSHBnjDC@ױҒpi3SKX b!xu1!27'+مl)/,9yт_͑ͮf6IfWVVp7WW/̒Q*z&s$͋p y;˞I諂'g.vn3믒Nw/>E1*4JL  EySJ΂*0iڍV=V>v}FC!|lxZHY+f3-`i[ۭӲ7jDr6/s;kJNn6{k o ,tv0ش%XPg|aGTEMD7 +Pt|vԉv߬8rz"# cxwm Yݔ$Q^U)=a+&ʼnoc)#%/v f 4l ӡbZ*@nM^SD̟HupYOnp/'z ^I&"*U&'mWRKСT*Wuy[9S{vw䍼 +&7h:'@]/IY2쩄&*,PU^~\I2gert bXE7}Z5#V߷U 6O{(E5~89*lq0ЪaJdh.jZNcFhGUӜ!snwYQ.wW<, oXql7 odr-FQJݍi9"]ŶnZ"&QFC{~^Vtt@hv5ǴWQQy@:TMYRL^1N2&M71MgVi j67SPô? ZomjQr 7<2aOD [(`]< + t fW%)N9cF`,a`?Cjgμou@uQ""I+964xКK\؅?oړR4l*_MGWIu/TOM}/=Qo;GٚKeO"d4TԇW=HOCGR7i/(ĈfIԳy7fO5ڒ1ƫGg=OAZSmJ4X8p_9nT%A"_}?OFkUKV>5^+ 9ګ=aUڙaYznjʜV67Ď٘_` S5 mSg7KLCPD1nY=?.t)@WܯF-ʻi%N{3:$ڪc|`He.N~q{ju WoTEfW[2Yp)֨Z5ZwLãgN("m7 +MR[ T->-lvQo͑fkxA F9Pz/߃Lخs~x? 4&jx3|Zɩ"?x}xִ#ŢtIH!^2yPe(r<|Pa6۴F|xud$)~uoΗ4/Kiy{BQ7FnɫfIaQSa^w:V_I#Ϳt۷g| @Tͷ9Nm0cy:ʃƂ&/6}Np>|g6s_cEQ[nB=l{w+:DM#,}SG,4''$)IOBCqbilzUq@U-[z昆Iea~<ؒ0+bQs2)Î !-[dg?'^D8c:t >Nܭ,ފ0Ӳ5,q,S|\l)wHɑ;CQ\LwM`NazE fX@4gzK+Ggh"2VhϏoͩ);kj U)_Y8b=/ꛨ sHłC\\B霜wL<= S +n\=5tmi>|o[jGo!UI@"_syB烟}s9ـAs:M<8^Gʥ}U&ӧpT1/SIa쎣^FK)Ets0~ s;MSp|G_捔5jm[3~O SSs_eeYH{GjX1\" !!a y˪/y8ex-:dW&T)ßW;sxff# I{vuQ@HL8֕W?fYX(YV}P-Bc,UWqS=?;F`#|]aORyd K !07ʵ}8m xf0"xWNJeP; F硕5"NK3jЀH6iqצ:qt "KsT?WEfvvɭ6¶zv)[N?;!5ȓ~nw'H4YIwD7L ǒE nާMҖr=cF;އ*+?%ONњw ,;GV#4y[!8q#fX 蹗Z\sƧX()P|D&%Z{KMWQ$4BDHu!\x7`[%򃌲S׺sm<SlQ hB$4Rkc5|+[B̬HqNmͯwoR:T^A}/a%,:vtӦa'RiualYT]/Ёܨ+Ђ`ѹ(^1ueK6% mz+YpD]g,fdX2@~z `8V_TG/߰dmt+50)p6@/c_OTWmVH$z;@>=0w +'*h=GE $gZcaO"a+ӥ﷽6Dh}5y ԰T9>FdAQfXv=OOH wfcVHesP]L^3fe؃L/Z!w0ۯw"C(("39iy9?u$ +gQi+`[*h=Ðq6_E4,zRk-s5:'\I+ISLvÄfȇİaM[a1ك̕)1E`Тg*]{c0m+yhb[=걜L jr.F˝#zX$EnTw|e1|A^TLpyv 1*DȠIXu7 qIi>LhC@]:t!5Ӹ IuVQK$ Ǡj3n*f= +ST:i] J/v)>ZY^׏&'ՑYo@ u&MuN.gkK?972|Ru'dq)>a8LJymSv4&mҀI7_RO^?PjbD~bvm8i8qY'EV:}>܌#EW=mGmN_T] f/ﻄ*Y+ɬ/Z󄷑)aHݵoeix@.:H9Z;+4 [Vu{I8/cez`.lzL,N ׃_ח;Ǐ8B"ۭ{}6:V e"wu>=sNCF哰*7Hl3FvM©WtSϘJaAXJ8rzRċ:&W=1NfrLCO1/iw&֍JMIhA:7ͶHlYNy2zg14`4kR 4yC,3 GÓ7ǵ mCfNY;X%#F;nZ3}+zaVF o~?lނ~tۍӝe}pt!2{nD]EX}ALױ-IѴp"U:@Ė; z1eU_J+5h BP׆sS/`MkS\=[bđaDzKUBsllNI6YRZ-+h{JC6V32R(ɊAǢfj6֊BpKU)j@sz>ݗFNWBs 9^l:CK=8èЉ;(gd{8>q=fMw){Q9Mw7~ќS.teKTVwr?RQF^]nk>dBCIXXؤssvsQYnc_2DJRо):؟ 7gTŽFn?ݦg Mm` [GiZM|eop~qgia $6j=jbNE5BqJyկoMU8bl'E%>oL|+ɪ +_8ƄĄ4i+I8&~S7TUŋLhRmky4OW,:[rko瀣 ~J0ur[3[zV*W(wZhOpdc _Zh֢nlh)+ t7yX4uVMC;eǚ,(/].2ryΥ[#T/ݭ~WAرf.suŎy8UfOhf +{v\4;J^,D5r$ҵ7ݝF٬D!g p7z|IЬdf@]@4>>- o~%cdj= hFICuAgg ,'Z ikG$5n 52ѩ##ԱQlPC+_Z$=)XmOuSٳ^"mɣY=qFѠY?ys#Xt5P2n_, g)BmTᯛ姺fTU{e8ޔF.L;_u<6`F[AՍHf֌m.\xp`;rpO3@񼼞f ȷϯĿK5o魰_8lֳ@6`1̀z1~Y yd{0gFj۳q50I{s|z|=mƅc3gGV9+Yo)0L[ѿ_:zZ\&f4|ec'#d)B;3d7aV֯9 ]<@KQsJyW +h&-nTX''uAd\^q|jp;M&H#;(ưHQ+eN<(ud(Zc~nX >O x ia% +4~7Ej9Ǚ)%ҭ&§A%LOь|/W,tmt$S"װog˪QS;o 7hπxUIi\`6cjIq#24Us +WyR@*^6ŲZ4qeo5Wvz<[UAD:XyvknoPzIvu? +q+qL*[;w=VR-mwبH _{v'giLukό3Gm II=0Yjqπ}G}PU qqSX FdAt%rETf6D`Vq??/MIs$_ +G> rvly2ƗIj༯m[6`f3#679BPhZǟS"SeXd&)I$Ws09A&ܑ)akFP,pN _zS| +怆bq.y+LsPo'a=X݋꾬݅^ XsѰ,T-Kj>T + QaڽqH@~ juJ8U]j ynYZeĜf!7lI` +wZss5٣]4vvS$Y>cZٚ0EI8qj:71ՙQuZ$) !:u4&8a9H-k0dηARDA"4,cw :NG&PYjFTZ6@h^wRUgf,F좃x"A*T= X{z +mΓDzϰ ]r +GYj|[igoHW9q?>aRc8h6&bdf!UQf.nLCOu/dÚicq(tInB'`5/GHK3y3_+})k{-_-HaYp7RsB:(:*֍*g 6 )jsT~I:o>'-{Y+2,@ _IJwq[gKf7nkڹHӪ??>%NӨnqk<{Ѽ_U_NA@'WhI^|? -믅qFӷf1aINkJ5 + !YĠeέT/[v +mNPK'hu6Cno:BJAG wSZ([$Qb܃&Hd0+w;!u=wn~J=:z(s4Jusk# ?ձB&bخӺ+.knXJ܋aҧc78wpL=z&^s;Z/r<~.&1Kri)f9TW)vXxD96SN&Kmy8#Ust4wU'K=(?؟Zsg4bu w`՚ vƖ ϝrwBr43MQf8 UU;{]22ppWE#3d U_n.U6ds,p7M#\UKdGEHֱI iA`5 \%K om7KeܸV ;Ŭ+`K +=-2|$_0X]j-4Ⱦ<]_%!cʙ]9#i8I݋@IupvʨS+?J."t6~q'8VnIV:)Ɠfʔ7v32j8e'aQg\s!JFޞ-:H[t;':Y\] 73KUa6k; +\@ce'qM?=f[\_#GȒ=XYh ex^WעDbh(튕g<ֶ#[f\iȎjysٵPrHI*ܘT/DUs*;y#;-!$Hl6Hs=R^ 8$WH!x70}'p(, `(ÐNLw=^ĭ +cP> +094@cu ݑ8Mo ]w*~&ŎDabnR8\Kѡp0MRVP?!rGrP jj!)hA<ڞR{QZ,38[ۓ-?j//jě #FUҤiQ4-RSӈe`>ƐxT+"P~Q]|Q0hٲXGv мHXêZj]SZьFL1Fh>N~*3OQC^9v[4U%R66l +ne&ViB\iCt.dD*jl<6E⫣8jLN8=M6Vm#Gg$)V;=W(I"gex\:w$95GZ'=CIS,nW?ut_ЖS^&ɕ8)t|Z۟V b VΗ:(piHE&#3+ı0Mz?ZI6D`z  i]by{Xr~m^˿f< +`}xtm'á|E%RM;ݞ47G" |b6a5 +U&˶@oA??ܓ$l0ѰF >-XBa:7$3+{̀xf l(^Z?cLw:J!0X;3]*4' D=n7Dsʃp3,s8ߧ1lHk50M}bs+切+)`{h]$N$\{mz"8ڎP:8c֤?}};6_*g;gwj}q-H,#n8mԏ Ÿ{xb@He8V͸ű4$M>^'z"n_:c K44PP/a+sM5x٪VLۧh2ܥD7s}D-S4x`;YEp} 0ma(Iwxza3Pr2Ӗ}8~!RWO.FiI=a(ɬ;Ei m7y/ 'v,QSЋDa@Шgc_׷5R&ԁ6@j*=~FӫFǷi$&ZZBAwZ%njf ΑSy/ +ֺ]ΚZ gäwPaN6d;: h,tcڛzfIU.NHVr+mr8#ѵϋ!m9h7W֡Ԟh9O 82XGxAw*9%sn +XK2hЁek:bo Rbc! ;lp1, [BT`cX) /ڜо#P$rKPWKڌg.WuFF2N̛ue`%dU%rtF_.DApz(q٬p~,eZ.Q,L7MI/#<@z%,~î5t[, +=Er۲!|ˢV|X&os\ZSmn<;_֨t'^mkRK77F$g:ְP\g03Z(zЪ1Zͺmf7 ]#_(v x\.9GveFO0~$E訥 +S6 ]iV6v:L0㞔"S;obPF7F +?ϊ;4\G}cw⡚Ա=lv}p9hɦgکbҤr&Ͳꀥ>Іg}X`zٌ42˄ֳd1矟 |dYLˑ2 nRT-t![IgfVYg>H^I^ Ą=TԢ50YeB;i`mN$O3XҭѨmsItQi4¦[ l~54V|iD m"?315a!4V_i펨Rn𿢞Bz4U&r3 c_IxPh|uAZLDftajwp+~:8XGdԦcİT+HE PI{:u|ieb"7-L-_k=i.`$D s(zS~iR[٥=}{ fjsM'Ä|1 Xl=zUMaQ B*E Hxw +Z Qo^\[atcd7;X~awN 7@cT$IagKsk"4QΡ"tpp94m[2WU䱬6NWNT 0[ P}ҟgFl +`f {V, O0\!<1Θ vqYQW4z$PGfNŶJyعFiLg#d[UsPcoKu.Gtv5r^0鐥?+}nOjp&da7N[8ߑ@(L3y~Q]͖Zu{? EPWk~x+у;-9mYp$ƶ44=;d3^`_}}+X+4{G;S^%;rA>_10_A>rA}Gl!$ٖi KMԖ-6P ;MrhOշ˝ER +ɰv`GZY 667"d)5#h3CۄW&p[VH$%)0X^vmIbiGk}9P;Bʰ6  Y 4q9Q(%J('CljsZE !\.ĸЪR]\BuZCr;W +t(Iwiӕ>꣠SZ< YRB2ie0JEz9~Sx_YEZ,p$\M:HY`iXnvZjCK ~}`&afJ;%3l_lvJpaL7pknҾr3BGRD.U4i~5űwKkH G:gf+ E>#9b9 lKӟT_[a5q^N0P5mch-I4_,aD5S(1!iZ^lVTi}ń̰3W?E]b'i\q_&G(Q--I{3㠆W08oYMW}i(U8C 9p5ƴr-;iSCY ^e)278M հ= (}VEɭlYg0 +ު8+1A+uIPJ|;m 6r5 +LTĘ&^QH)  aHAXpR28FcXj[E΂96*J[o34>nzfljGo&=j +lY ^uĪM K-d]4BRh\Eo,˹WG_ <=Ui9 1Y.u䵑{LCue>"O>x|ciX)C̡s\l2>ʮmv/᜔r$˅USD].??ϏQh?3cQ+さVt釷_]~{+xOǕW/z^#8^Yfi>!?E 0j~_Pp|OGIzT'Im|7{>ߟԻ!o޷'={ +06' endstream endobj 14 0 obj <>stream +8;Y8d8TlUC%#"'.?GUij7`BO65EGb1e#c8cnQhj)`k4q*lTHi11n,e<l%qKSW#&p(!kJ(_FcW9[G-KUP1 +8FDq+;P!pY%usX`G*&V*)9*Q':HN9o'=G[: +fpn^s)X^@242-@c_<;*lc]F4sQoZQe;CPG;h#P58m/PUO?Ghg,N+F,o`EGr05C"Dm +U1(qnMjEoMT]T&LSd+'8B8T7b&Cd?PiCYqbPU?ML^;Io[D%(0KkY"!3@];Yg-F"DI +^_Z;Gqr1"SE.CA""'EmA4un3HK4=6_2f-AmB8Ro(UX?DO^TPUI!5;B'E5f.Hb:&TF`72gq"q"f<`6^j6<(P +15ea>CJ(.$J<$;'3qJpfNH?l_"7[J#Cm_a&^")8`Ah\J@Jr +#S")bH)Zm=hc))4-^d6odpXrm@sAmQ_^fD!]-nEQc$Mn3eOV/KNg+&s%eUUFDA>IE5Kk!@MLQ=7aV#;JGk/):<91G?M6/hYanj,W;Sbdau3P*UrA=^ +,Ge$!Ud8HVMBRDM-p3f_.mGEnR.P2S+#\aeVO(+3TY/I(j25IqX=F9tN0N^Hc=6O` +c&b[HiSTs!ZJ#M\O-O4Um"sMVNK[*M?tpampP/s#/(KJ/88$AgHM*r!//J_^/t3[_ +0k3!;4bdZm/2L9?%Ve6C-Ea3N0qk&="Q#t-5Y@`@8q"3Z.LT6j=[1B8%^;tif.ChD +oZ^L#d1U!Ohon/$YY.XhD98AZ\KBP`0*^=.RYYKqgI?CoA)^[$`pe%T]>PQ_-n?d:;'#+,i*Q%7W=F;t!*u +fJJ`-N:FV6Rf3n@/=[rDL:sN0^*L6TN*!lQ_Zc+WUn"54P.Kr8A8*W?>Em`0bTVSq +Z^,"39*33u0BMWs+@GFMH;g5nW"b.]%Wc074XRo^\u6lM1@s1Qc&Q\os1gn27+DQ# +$OHI4N<;Q!/gBbf8ao(s>h^@^M)/U3Z-W4+RH;/2(L,*Gi6J/pj\MYVnX;KYbh:qr +?tlK.Ygo(QS?oai1bK^4 +?l;i+n69,1D==A"=/m4k_6]7Q6Z".lJum:@VQ?QX>TJqq8SQp![;q"r0(rE/iFTQs +E%9jU.Lqm((o1X0c-CB3hd>-K*Uer17TRr&=FC[KIKN]X@[AX;J&bMX5+H-lphp!# +%q12jf'_-KK8 +o&dV,)IQVum;!0BroqaIY%-mfo7(/.)r"\%X^s"K$i;j>?1[QQ[9ScLI=&@s.^3L/ +q;sd$C<-0Y?/jVhCt@1+mriT>DmqIQ[k$66ENe,%*H`=mP3^B;-02*%UR6JIlXou0 +bl.^Aoe2[K]P^a@"`k&4$Y5OFQ5\Lcg/ZAa#I`K,FY^R?X4^L][kc=4;0,)VNufj; +rpebn9_R?>.qi55cfY+O0tgamrQPoHDo)1C#DNUJLMaHl>/I endstream endobj 15 0 obj [/Indexed/DeviceRGB 255 16 0 R] endobj 16 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 7 0 obj <> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 6 0 obj <> endobj 5 0 obj <> endobj 20 0 obj <> endobj 21 0 obj <>stream +H xG7]8C$.kҒj%!!KIRD\Jj6Cn۰4ވT.`u9K[tywμ3})E#:wla4~x7uL1a`i@F\ -q3q ɩv8>Scn'%D&yF'Ěr;%;C0!i{Cj{Ldyj*GWhTo5p>4}P{aV>_~h wݘ׃if4_Wּxf +H&jA]C}4@CM-h0C6h@C ڣ:OA^Ptó^xϣ7^@E? Ƌx a +b^p57&Fa4"mD\x 0q3&a2⑀w$LTLC2R#3XYHlc1`C,B,B&# KaVc|l kLeT>cAa=6 `#I>G>6>[ %5Av`']؍= +-؇ppGp=NN JqeP +T +!}y<F:QkjC(ZF+hvA{~Z3-@SZCEh mD[;[vT;kׄuEH4D]D/_ kbH3EYbX'v[8%JD *D!#x9Yȕr 7˯nY,Oy^Uy]2jflimEjTgCU05JE15Q%jZV&M}Sg9(QǨg dsFcd0ҍ9"ck[,¢[j[YZ KK'XKeU ֖Vu5mm0p]#'kw>09::Z8z:œNV@RGGnrhmchPA5tk5 XZv@;} +&VO4h':U!٭λj͕_8W;X.12 +hg-pj ^giqQ +8N8.iKY\uHfg#1O_eiE +v7ZwtUkTT_U\Y=r9PT5:gxC;S~jW =ϱ'p%?ōHUF|Sm7m"[ o_-6?[i$WK=UqP\_ \__1"zEDS>g}O-d'7zQkoN>q?Gt6>1o}qEn_k嵳leO=X!>b=gSc|VNs*8qYo` -68L>YՓor˵2ܑ,Q{{~92>oԗQ@]A4>FCez0[/uE{; 0|GPa ("%6F1c5jtijWcS1&1ƨ1V?>4uuY{߽9st9_4h 2|ߢi5i*TNPlKIch<=G 4BezfKh>J ۅ~M"w~ vҟCڃd}tQ:F'SN!|~s:GC_z:O_W5]@ƹHhr!eZJ FKT +שޣL#h;TGgQ9ȉPCg!GXgOY 㥇|Ewx0WL&v-5}e)EէJ)ڬPSY@]ẈHIAmElgNqIuȢT7(ZPٴ=^ +0x٘a,6 +]7?8bCcCOArG߬2݈Pvo3<#ݗ9%ZѺ3,)>Β1 +>7Wq1)Rh:7GQUMJ- HՍ>7ݝi`x݀1M{֥hINMIq gQaπ>lo DM(uÖ:hYq}S +Mɾ~}1CX\v>#ɂ6uy+ႎ9 t jmKY򪳗 nۈ4xћQtu(S"Д^2a)9Oԥs\;w˄XSsOrnXMvn8Oכvujlij{aVt!yQ}4O&1',z</+C4xc*LӼ[[Kڃ~f }-RD=_0woCC⌸NHPcⶑeG]voe ķ91" w*߄A!]fi>ѬN:<( +) Xu;䲇C^֐Rs?yN>#[5|"ΊR%JȍZ;)jbc,m(ױ>(@$([/\F#(f.-:q(Cb,ۍ. ;^F\!PRyh2'N*tKõ2{ ڣUϯ&῅D,Bp(umWE fuX`> ^EUm>Y= +mWxb{%r$OG>aԫ+b؎qA|T.b P]̉Rp~+Q&ȢTt.~v!06 =AN&t 9rY8wo>2YؤBWIE{*]`Tƚ4O}t2 ,MCZ{XJ=.sD3P&iY^R[)/OPg pu䉃 EF"59A}9y~FC3ln{<49YKEٞ[h&Ӕ7*M9{s%ދȑyc1yarP?0Z 9F=@ExSy~asT}C㠍FmX>2exdGl2/cL5lblS~’U6҆5! ?UT +S$ݦWmA"W}TRrVqS>%Ζ:6n㸥q@&ڃ0k;iV 9$H9߫D}:y +I)TN rqNrfk@|܉hHh]V.AJ-̇0 +@vP6<(ϐ$}fh?3$?eb e9 endstream endobj 19 0 obj <> endobj 22 0 obj <>stream +HTiPSW_)W!APЀxH0H&ES 0,.XUZkkCv9̙fnq89^~vIFSkV2Y'jB[Ŋ8,e7cg)/dl8+̌Q$slhV:"IPhX_Ī mK{8etȠZlQjut4)Lg|FQFDl}FC/cX 0? .&bMX:Lup<8Uf2{eͭ[h$?,-bX ܽHND,H S,1 +qT$c8*4xm0r8rƗ klM?#9Mm.*d+ㅆl T^LaEM? -P8IBige=W:dz6"Z"Wmgza1pTמrٍ]s 1Jߏ[v잉z]&iL楂 Dhr2ɔJˆ <B5䏧BBAҵ_ +}"7 r#SلqХYS-PӊmVH:`-p5> +=©-|S'z5>uMX-!{.BţK ;bA &UoOޛ<$+؉)̜J!Y{e +_ +D)~!Kw,JlJ(!7YZhЌ0T'A^ 3xM'*EJ>7oc#>Udi%d*iypܣɰ=M%EEpw(8 ^y~'o`/{G=ĺr msƐ444;LX!b|40Xw5h:2sXO Ip{e{v$ 6fs57eº!mbF Jvv^9GP'Jp1AVzxɎ2{E@mř3%1Q"ڮnoUg°%ʯp! {Ŧ~W߆2W6}|kЈBߋ#h~E.!2|J|-k#Ⱦ+fjҬ ` k6CC6d(;w&|+o}<'U69-E'D8Nuu/!Ȳ]OyXX^*%|4qT׿5by_  endstream endobj 13 0 obj <> endobj 12 0 obj [/ICCBased 23 0 R] endobj 23 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 11 0 obj <> endobj 24 0 obj <> endobj 25 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 21.0.0 %%For: (Kenneth Reitz) () %%Title: (requests.ai) %%CreationDate: 11/23/16 7:55 PM %%Canvassize: 16383 %%BoundingBox: 2 -1314 1022 -6 %%HiResBoundingBox: 2.92169833255775 -1314 1021.41238881833 -6.5448391922846 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 223 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -1280 1024 0 %AI3_TemplateBox: 512.5 -640.5 512.5 -640.5 %AI3_TileBox: 224 -996 800 -262 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -700.448496019698 209.999491681081 0.396269427797295 1350 720 18 0 0 10 42 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -700.448496019698 209.999491681081 0.396269427797295 1350 720 18 0 0 10 42 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:112 -940 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 26 0 obj <>stream +%%BoundingBox: 2 -1314 1022 -6 %%HiResBoundingBox: 2.92169833255775 -1314 1021.41238881833 -6.5448391922846 %AI7_Thumbnail: 100 128 8 %%BeginData: 15058 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD2EFFA8272827277DFD5DFF7D277DA8FF7DF827FD5BFFA8F8A8FD %04FF52F852FD50FF7D7D527D7DFD05FF2752FD04FFA8A827F8A8FFFFFFA8 %7D525252A8A8FD42FFA87D27525252272727FD04FF277DFD05FF7D7DF8A8 %FFFFFF52F852272727522752A8FD38FFA8CBA8FFA87D27527DA8A8A9A8FF %7D27A8FFFFFFF87DFD04FF7D7D52F87DFFFFFF2852A8FFA87D7D7D5227F8 %527DA8FFFFA8FD31FF7D21525252277DFFFFFD04A8FFA827A8FFFFFF527D %FFFFFFA87D7D27F8FD04FF7D52FFA8FFA8A87DA8A8A852522727F87DFD30 %FF7D2752527DA8FFFFA8A8FFFFFF7DFFA8277DFFFFFF7D27A8FFA87D5227 %F87DFD04FF527DA8A8FFFD04A8FFFFA87D27F827A8FD2EFFA15227A8A84C %4B52527D52522752FFFFA8FF277DFD04FF7D27527D5252F852FD05FF527D %FFA8A8272727525227F82727527D7D7DA8FD2AFFA852527DFF7D7DA87DF8 %FD0427522152FFFFFF7D52FD05FFF8277D7D7D527DFD05FF27A8A8FF28F8 %524C2727F8277DFF7D7D527D52527DA8FD25FF7D7D52A8A8A87DA8A87D27 %7DA87D7DFFA82727A8A8FFA852A8FD04FF7D277D7D5227A8FD04FF7D53A8 %FF7D27F8A8A87D537D27277DFFFFA8FD047D27527DFD20FF7D5252527DFF %7DA87DA87D2852A87E7D7DFFA8FD04277DA8FF527DFD04FFA852A8A82752 %FD05FF527DFF5227F82727A8FFA8527D4C27277DA8FFA87D7DFF7D7D2727 %277DFD1AFF7DF8777DFFFFFFA8A27D7D52527DA87DA8A8CB525252A87D52 %F84C52A852FD05FF52A8A85252FD04FF7D52FF27F827527D7DF87DFFFFA8 %7D7DA87D52527D7DA8A87D525252F852FD19FFA8277D7D524C525252274C %527DFFA87DFFA85227527DA8A8FF52275227527D52FD04FF7D7DFF527DFF %FFFF7D52FF2727F87DFF7D7DA827277DFFA8A87DFFFFA87D52F82152277D %FF7DF8F87DA8FD14FF7D5252FFFFA852A87D27F87D7DA8FFFF7D7D522752 %7D7DA8A8A82752A8A85227525252FFFFFF7DA8FF5252FFFF7D52FF52277D %52F8A8FFA87DA87D522752FD05A8FF52277DFFFF7D52FFA87D272752A8A8 %FD0EFF7D52F852A8FF7D527DFFFFFF4C214C522727F8272752A8FFA8A87D %5227527D7D7DFF52F8525227A8FF7D7DFF27527D7D527D27F8A8A87D7DF8 %7DFFFF7D7DA8FF5227277D52275252277DA8FFFFA8527DA8FF7D52F82727 %7D7DFD08FF7D7D272727A8A8A87DA8FFFFA87E5252A8A852A2524C7DA252 %7D5252275252A2527DA8FF525277522752525227A8FF52277D5252F87D7D %52A8FF7DA852527DFFA8FFFFFFA85227A87D7DA8A827527DFFFFFFA87D52 %A8FFFF7D7D272727527DA8A85227F82752A8A8FFA8A8A8FFFFA952272852 %527D7DFFFFA8272752524C7D4C52FD05FFA821277DA87D7D27522752A8FF %272752F8277D7DFF7D52A8FF7DA87D522752527D2727F827A8FFA87D52A8 %7D2727A8FFFFFFA8A8FD07FF52F8F87DFF7D7D525252FD057D5252275252 %7D52A8FFFFA87D4C527DA8525327527DA8A8A87D27277DA87D7DA8525227 %7D27A8FF52F827527DFFFF7DFFA85252A8FD04FF7D5252527DA82752A8FF %FFA87D7DA8A8272752A8A8FFA8FFFFFFA8A87D527DFD06FFA8A87D7D2127 %275252A8A87DA8FFFFFF522727FD047DFF7DF8F8522752217DA8FFFFA877 %52F87D7DA852F852FFF8277D7D7D527DA87DFFA8A852527D7D52522752A8 %A87DA8525252FFFFFFA87D7D7D5227F827F827F827275252A8FD0BFF527D %FD06FFA8CB7D77274C7DA87DA8FFFF7D5253FFA87D7D525252275227527D %A8FFA85252277DFF5227A87DA8A8A8277DA8FFFFFF2752277D7D277DFFFF %FF7DA8FF7D277DA8FFFFA87DFFFFFF7DA8A87D527DFD0BFFA85227272752 %52535252274C212727A87D7D7DFFFFA827527DFF7DA8FFA8F8277D77F852 %7D7D5252277D52277DFF2727524C277D7D52275252A827F87DFF7DA87D52 %27A8FFFF7D7DFFA85227277D7EFD04A8FFA8FFFFA8F8F852FD0EFFA8A8A8 %A9A8A8527DA8FFFFA8A8FF7D52527DA8FF7DFFFF7DF852A8FFA8A8522727 %52F852F827277DFF7D277D2727F852F852A8A8A8FF5252A8FF7DA8A87D27 %52A8FFA87DA8FFA87D5252F827525228527D7D527D7DFD13FFA8527DFFFF %A27D7D52272752A8FFA87DFF7D27F877FFFF7DFF7D2727A852A827277D52 %27FF5227A852527DA87DF87DFFA87DFF52F87DFFA87DA8A85227277D7DA8 %A8FD04FFA852527DA8A8FD17FFA82727527D527D7DA2277DFD04FFA87D27 %2752FFFFA87DFF522752A87DFF7D527DA8A852FF7D277DA852A8FF7DA8F8 %53FFA852FF7D5252FFFFA8FFFFA87DF82752282752277D7DA827F852FD1A %FFA8FFA8FFFFFF277EFFA87D7D27522777FFFFFFA87DA227527DFF7DFF27 %27A8A8FF7D27FF52277D7D52277DA87DA82727A8A852A8A87727527DFD05 %FF52A8A8A8FD057DA8A8FD1FFF7DF827527D527DA8FF7D7DFFFFA87D527D %277DFFFFFFA80027A8FFA87D4C7DFF52F87DA8FF52277DA8A8FF27527DA8 %A8FFFFFF522752527D7DA8A8527DFD27FFA8FFA8FFA8FFFFFF2727525227 %527D7D2752A8A8527D272727A852527DFF52FF5252277D7D52F8F8527DFF %FFA852275252A8FFFF7D7DA8A87D7D5252F8277DFD2DFFA8A8FFA8FFFFFF %7D525227F8A87DA8A87D282727FFA87DFF527DFFF8272752275227527DFF %A827277D525252A852277DFD05FFA8FD37FF7D27FD05FFA8FFFF7DF8A852 %FF277D7D2752FD057D27272752522727A8A87D52522752A8FD3BFF5252FF %FFFFA8FFFFA87D7D5252A87DFF00527D277D527D5252277D527D27277DA8 %A8FD40FFA8527DFFA87D27272752527DA8FFFFA852FFF87DFF7DFD05527D %5252527D277DFFFFA8FD3FFF7D52FFFF7D52FFFFFF7DFD05FFA87DFF2752 %FD05FF7DFD04FF527D7DF8A8FD41FFF8A8A8A8277DFFFFFF7DA8FD04FFA8 %27FF527DFD04FFA87DFD04FF7D527D2727FD40FFA8277DFFA852F8FFFFFF %7DA8FD04FF7D7DA85252FD05FF7DFD04FF7D277D2727FD41FF27A8A8FFA8 %5227A8FFA87DFFFFA8A8A852FF277DA8A8A8FF7D7DFD04FF27527D27F8FD %41FF7D52A8FD04FF527D7D7DA8FFFFFFA87DFF7D7DFFFFA8A87DA8FFA87D %277D7D7DF827FD41FFA82727A8FD04FFA87D5227527D527D27A828527D7D %5252FD05277D7D7D27F87DFD43FF5227A8FD06FFA8FFA27D52522727F827 %F8275252527D7DA87DA852F852FD44FFA852207DA8FD0BFFA8A87D7D4C52 %52FD057D272752FD38FFA8FFA8FD0CFFA84C527DA87DA8A8FD0BFF7D4C27 %7D52522752A8FD0DFFA8FD28FFA8A87DA87DFD04A8FD07FFA8A8A8FFA852 %F827F8272752527D7DA8FD06FFA87DF8F87DFFFFA8A8FD07FFA8A87D7D7D %A87DA8A8FD24FF7DA8A8FD05FFA8A8A8FD05FFA8A8FD04FF277D7D7D5252 %274C27272752527DA8FD04FFA8277DFD04FFA8FD05FFA87DA8FD06FFA87D %FD22FF7DA8FD09FF7DA8FFA8A87DFD05FF2052FD047D4C27275252FF2752 %52277DFFFFFFA8A8277DFD04FFA8A8A8FFA87DA8FD08FFA852FD20FF7DA8 %FD0BFFFD04A8FD05FF7727FD047D27274CFFFF7DA8277DFFA87752FD04FF %A2F87DFD06FFA87DFD0CFF7DFD1EFFA8A8FD0CFF7D7DFD07FF27277D7D7D %522127A8FFFF7DA827A8FFA8A87D27A8FFFFA827F8FD06FF7D52A8FD0BFF %A8A8FD1DFFA8A8FD0AFFA87DFFA8FD07FF27527D7D7D52F87DAEFFFF7DA8 %27A2FFA8FFA82152FFFFFF7D277DFD05FF7DFFA8A8A8FD09FFA8A8FD1DFF %7DFD09FFA8A8A8FFFFA8A8FD05FFA827527D7D7D27F852FFFFFF28A827A8 %FFFFA8A8F8A8FFFFFFA8F852FD04FFA8A8FFFFA8A8A8FD09FF7DFD1DFF7D %FD08FF7DA8A8FFA8FF7DA8FFFFFD04A827527D7D7D52F852FFFFA87DA852 %7DFFFFFF5252FD04FF7D2152FFA8FFFFA87DFFA8FFFFA87DA8FD07FFA8A8 %FD1CFF7DFD07FF52527DA8FD057DA87D7D7DA87D77F8FD047D27F87DA8A8 %52A9277DA8FF52277DFFFFFFA852F8A87D7D7DFF7D7DA87D7DA87D7D27FD %07FF7DFD1DFFA8FD07FFA87DA8A8FFFFFF7D7D7DA8FD04FFA8274BFD047D %52F87DA87D7D527DFF7D52A8A8FFFFFF7DF87DFFFFFFA87D7D52A8A8FFA8 %FF7D7DA8FD06FF7DFD1DFF7DFD07FFA8A87DA87DA8FF7D7DFFA8A87DA8A8 %FF7D2727FD047D52F852275227A87D277DFFA8FFFFFF4C27FD05A8FF5252 %FFFFFD06A8FD06FF7DFD1EFFA8FD05FFA8FFFFA8A8FFA87D7DFD07FFA8A8 %7D4C52A87D7D7DA85227F8272727A2FD05FF7727A8FD07FF52A8A8FFA8A8 %FFFFA8FD05FFA8A8FD1EFFA8A8FD05FFA8FFA8A8A8FF7D7DA8FD06FFA8A8 %FFA82752FD077D5227F82752A8FFFF2752A8A8FD06FFA8A827FFA8A8A8FF %A8A8FD04FFA8A8FD1FFFA8A8A8FD04FFA8FFFFA8FF527DFFFF7DA8FD04FF %A8FFA8FFA85227FD097D5227217D2752A8FFA8FD05FFA8FFFF7D52A8A8FF %FFFFA8FFFFFFA8A8FD21FFA87D7DFFFFFFA8FFFFA87D7D7DFFFFFFA8A8A8 %FFA8A87DA8A8A87DFD042752FD077D27F87DFFFF7DA87DFFA8A8A8FFFFFF %FD047DFFFFA8FFFFFF7D7DA8FD23FFA87DA87DA8A8A87DA8A8A8FD05FFA8 %A8FD047DA87D27A8FFA852F82752FD067D5227A87DA87D527DFFA8FD04FF %7DA8FF7DFFA8A87DA87DA8FD27FFA8A8A8FFA8FFFFA87DA8A8FD04FFA852 %FD04A85252FFFFFF7E52F8522752FD057D2727FFA8A85252FD06FF7D7DA8 %FFFFFFA8A8A8FD30FF7DA8FFA8FD04FFA87DA8FFFFA827FD04FFA87D527D %2727527DA87D7D5227A2A8A87DA8FD06FFA87DFD05FFA8FD2CFFA8FD05FF %7DFFA8A8FFFF7DFF7DA8FFFF5252FFFFFFA87D7D27A220FF7D27527D7D7D %F852FFA87DFFA8FFFFFFA8FF52A8FD04FFA8FD33FFA8A8FFA8A8A8FFFFFF %A8FFA85276FFFFFFA852F84C7D52FFFF52527D7D7D5227FFA8FFFFFFA8FF %A8FFA8A8FD06FFA8FD2CFFA8FD06FF7DA8FFA8A8FFFFA8A87DA82752FFFF %FFA80021277D27FFFF7DF87D7D7DF84C7DA8A8FFFFA87DFFFF7DA8FD05FF %A8FD34FFA87DA8FFA8FFFFFF7DA87D7D27A8FFFFFF52F84C7D27FFFF2752 %7DA852F827A87DFFA8FFA8FFA87D7DFD07FFA8FD2CFFA8FD07FF7D7D7DFF %A8FFA8FD047D52F8A8FFFFA8A827274B52F8527D7D7D27F8527DFD04A8FF %7D527DFD07FFA8FD36FF7DA87D7D527D527DA87D7D2727FD05FF5227F827 %527D7D7D2152A87D7D7D527D52A8A8FD36FFA8FD07FFA87D7DA87D7D7DA8 %7D7D527DA87DF87DFD04FFA87D27274B7D277D7D7D52A87D7D7DA8A87DA8 %FD07FFA8FD35FF7DA8A8FFFFA8FF7DA87D7D27A8FF27274C52A8FD04FFA8 %52277D52A87DA87DFFA8A8FFFF7D7DA8FD07FFA8FD2CFFA8FD06FF7D7DFF %A87DFFFFA8A852A8FF7D7D7D277D522727767DFFFFFF7D52277DFFA87D7D %A8FFFFA87DFFA87DFD06FFA8FD33FFA87DFFFFA87DFFFFFFA8A8A8FFFF7D %277D7DA852527D2752FFFFFF2752FFFF7DA8A8FFA8FFA8A8FFFFA8A8FD06 %FFA8FD2CFFA8FD05FF7DFFFFA8A8FF7DFFA87DA8A87DFF7D27527D5227F8 %FD0452FFFF5227A852A8FFA8A8FFA8A8FFA8FFFF7DFD05FFA8FD32FFA87D %FFA8A8FFFFFD04A8FFA87DA8FF27527D52F8277D522776FFFF524BA87DA8 %FFA87DA8A8FFFFFFA8FFA87DFD32FFA8FD04FF7D7DFF7DFFFFFFA87D7DFF %A8FFA8A8FF7D27A75252F8762752A8FFA82727A8A8FFA8FF7D7DA8FFFFFF %A8FFA87DA8FFFFFFA8FD2AFFA8A87DA8A8A8FFFF7D7DA8FD05FF7D527DA8 %A8A87DA87D527DA8525227277DFFFF7DF87D7DFD04A85252FD06FFA87DFF %FFA8A8A87DA8A8FD25FFA87DA8A8FFA8FFA87D7DA87DFD05FFA8A852FD04 %A8FD047D52FD047D522752FF4C52A87D7DA87DA87DA8A8A8FD04FF7DA87D %7DA8FFA8FFA8A87DA8FD22FFA87DFFFFFFA8A8FFFFA8A8527DFD04FFA8FF %FFFFA8FFA8FFA8FF527D5227527D7DA85227275252A8A8FFA8A8A8FFFFFF %A8FFA8FFFFA852A8A8FFFFA8A8FFFFFF7DA8FD20FFA87DFD05FF7DFFA8A8 %7DA852FFA8A8A8FD04FFA8A8FFA8FF7D7DA827212727527D7D5227527D7D %FFA8A8FFA8A8FD05FFA8A827FD04A8FFA8A8FD04FF7DA8FD1FFF7DFD05FF %A8A8FFA8A8FFFF7D52FD07FFA8A8FFFFA8527D7DF8FFA852F8527D7D2727 %5252A8FFFFFFA8FD07FF7D7DFFFFA8FFFFFFA8FD05FF7DFD1EFF7DA8FD06 %FFA8FF7DFFA8A8A8527DFFFFFFFD04A87DA8A87D52A82727FFFF277D2727 %7D52F852527DA8A852FD04A8FFFFFFA87DA8FFA8FF7DFFA8A8FD06FFA8FD %1DFFA8FD07FFA87DFD04A8FF7D527DA8A8FFA8FFFFFF7DA852A8A85252FF %7D277D27F87D7DF852A852A8A8FFFFFFA8FFA8A87D7D7DFD04FFA87DA8A8 %FD06FFA8FD1DFF7DFD07FF527D7DFD04A87D7DA87DA8A8FFA8FFA8527DA8 %A8FF274BFF7DF87D27275252F87DA8A8A8527DFFA8FFA8A87DA87D7DFD05 %A87D52FD07FF7DFD1DFF7DFD07FF7D52FD05A87DA8FFFF7DA8A8A87DA8A8 %A852A8A87D27A8FF7D274B7DA827277DFF7DA8A8A87DA8A8A87DFFFFA87D %FFA8FFA8A87D52FD07FFA8A8FD1CFF7DFD08FFA87DFD04FFA87DFD09FFA8 %7DA8FFA87D27FFFFA82127272752FFA87DA8FD09FFA8A8FD04FFA8A8A8FD %07FF7DFD1DFFA8FD0AFF7DA8FFFF7DFD0BFF7DA8FFFFA82127FFFFFF4CF8 %7DFFFFFF52FD0BFF7DFFFFFFA8A8FD09FF7DFD1DFFA8A8FD0AFFFD04A8FD %0BFFA87DFFFF52274C217DFFFF2152A8FF7DA8FD0BFFFD04A8FD0AFFA8A8 %FD1EFF7DFD0CFF7D7DFD0CFFA8A8FF77527D27F852FFA8F8FFFF7DFD0CFF %A87DFD0CFF7DFD20FF7DFD0AFFA87DFFA8A8FD0AFFA87DFFA82152F85227 %277D27527DA8FD0BFFA8FF7DA8FD0AFF7DA8FD20FFA87DFD08FFA87DFFFF %FFA8FFA8FD09FF7D7DA852527D522727A827527DFD0AFFA8A8FFFFFF7DA8 %FD08FF7DA8FD23FF7D7D7DA8A8FF7D7D7DFD06FFA87DA8FD08FF7DA87D4C %527DF827277DA8FD09FFA8A8A8FD05FF7DA87DA8A8FFA8A87DFD26FFA8A8 %7DA8A8FD0BFFA8A8FD08FFA87D7D52277D2727A8FD09FFA8FD0BFF7DA87D %A8A8FD38FFA8A8A8FD05FF7D527DA82752275227527DFD05FFA8A8A8FD4C %FF7DA8FFFF7DA8FFFF527D52522727A8A87DFFFFFFA8A8FD4FFFA8A87DA8 %FFFFA8525252212727FFFF7D7DFFA8FD53FF7DA8FFFFFF7D524C2727A8FF %FFA87DA8FD53FFA87DFFA8A8FFFF527D277DFFA87DFFA8A8FD53FF7DA8FF %FFA8A87D2727277DA8A8FFFFFF7DFD53FFA8A8FD04FFA8525227A8A8FFFF %FFA87DFD53FF7DFD07FF27A8FD06FF7DFD53FFA8A8FD06FF527EFD05FFA8 %7DFD53FF7DA8FD06FFA8FD06FFA87DFD54FF7DFD0DFF7DFD55FFA87DFD0B %FFA8A8FD34FFA827272752FD0427F85227A8FD15FF7D7DFD09FFA87DFD36 %FFA852F8F8F8277D5227F8F8F87DFD15FFA87DFD07FF7D7DFD1BFFA852FD %1BFFA8F8F8F8A8FFFFFF27F8F8F8FD16FFA87D7D7DA87D7D7DA8FD1AFFA8 %27F8F8A8FD1AFFA827F8F8A8FFFFFF7DF8F8F8FD18FFA8FFA8FFA8FD1DFF %F8F827FD1BFFA8F8F8F8A8FFFFFFA8F8F827FD35FFA8FFFFFFA827F827FD %06FFA8FD15FF27F8F8A8FFFFFF7DF8F87DFD05FFA82727F852A8FD05FF7D %27F82752A87D7DFFFFA8A8527DFFFFFFA87D5252FD04FF7DF827277DFFFF %FFA82727F8F827A8FFFFF8F827A8A8FFFF7D2727F82752FD12FFF8F8F8A8 %FFFFA8F8F852FD04FFA852F8277D27F8F87DFFFFFF27F82752FD04F827FF %27F8F8F827FFA827F8F8F852FFFF7D27F87D52F8F827FFA8F8F87DA87DF8 %27FF27FD05F87D52F852A8A827F8A8FD11FF27F827A87D5227F8A8FD05FF %52F852FFFFFF27F827FFFFF8F87DFFFFFF27F8F87DFFFF7DF8F852FFFFFF %7DF8F852FFA8F8F8A8FFFF52F8F8A852F87DFFFFFF5252A87DF8F8277D7D %7DF827FD04FFF8FD12FFF8F8F8FF52F8F827FD05FFA8F8F87DFFFFFF27F8 %F8A827F827FD04FF7DF8F87DFFFFA8F8F852FFFFFF7DF8F87DFF52F827FF %FFFFA8F8F82727F852FFFFFFA87DFFFFF8F852FFFF52F8F8A8FFFFFF7DA8 %FD10FFA827F8F8A8FF27F8F852FD04FF7DF8F8527D7D7D27F8F87D27F852 %FD04FF52F8F87DFFFFA8F8F852FFFFFF7DF8F87DFF27F8F87D7D7D52F8F8 %2727F8F8272752A8FFFFFFF8F827FFFF7DF8F8F827277DFD12FFA8F8F8F8 %A8FFA8F8F8F87DFFFFFF7DF8F8F8FD042752527DF8F852FD04FF7DF8F87D %FFFFA8F8F827FFFFFF7DF8F87DFF27F8F8F8FD0427527D7DFD06F827FFA8 %F8F827FFFFFF27FD06F87DFD10FFA827F8F8A8FFFF7DF8F8F8FFFFFF7DF8 %F87DFD06FFA8F8F852FD04FF52F8F87DFFFFA8F8F852FFFFFFA8F8F87DFF %F8F8F8FD09FF5252FD04F87DFFF8F827FD04FFA85227F8F8F827FD10FFA8 %F8F8F8A8FFFFFF27F8F827FFFFA8F8F827FD06FFA8F8F8F8FD04FF7DF8F8 %7DFFFFA8F8F852FFFFFF52F8F87DFF27F8F87DFD06FF7DA8FFFFFF52F8F8 %7DFFF8F852FFFFA8A8FFFFFFA8F8F8F8FD10FFA8F8F8F8A8FFFFFF7DF8F8 %F87DFFA827F8F852FD06FF27F8F852FFFFFFF8F8F87DFFFF7DF8F827A8FF %A827F8F87DFF52F8F827A8FD05FFF8A8FD04FFF8F87DFFF8F827FFFF7D27 %FD04FF52F827FD0FFFA87DF8F8F87DFD04FF27F8F8F87DFF52F8F8F8277D %7D5227FF52F8F8F82727F827F8F87DFFFFA8F8F8F8FD0427F8F852FFFFFD %04F8527D52277D2727FFFFFF52F827FFFFFD04F85227F87DFFFFFFF8F87D %FD0EFFA827F8272727F827A8FFFFFFF827F8F87DFF7D27FD05F8A8FFFF52 %FD04F8FF7DF8F8A8FFFFFFA8F8F8F827A8A8F827F827FFFF27FD05F852FF %27F8F85227F827FFFFFF7D27F8F852A8F8F82752F8277DFD10FFA8FFFFFF %A8FFA8FD05FFA8FFA8FFFFFFA8A87D7D7DFD05FFA87D7DFFFF7DF8F87DFD %05FFA8A8A8FFFFFFA8FFA8FFFFFFA87D7DA8A8FFFFFFA87D527DA8FD05FF %A8A8A8FFFFFF7D7D52A8A8FD32FF52F8F87DFD13FFA8FD08FFA8FD43FF7D %F8F87DFD04FFA852FFFF7D7DA87DFFA8FFFFFFA87D52FFA8FFA8FFA8FFA8 %7DFD04FFA8FFA8FFFFFFA8FFA8FFFFFFA8FD32FF52F8F87DFD04FFA87D7D %FF52A8FD057DFFFFA8A8FFFD047D52A8FFA8527D7DA8A8A87D7D7DFF7D7D %A87D7DFF7DA8FD32FF52F8F852FD04FF7D7D7DA87DFFFD04A87DA8FF527D %7D7DA852A87DA8FF7DA87D7DA8A8527D7DA87DFF7D7D7DA8A8A87DFD30FF %7DFD0627FFFFFF7DFF52FF7DA87D7D527D7DFFA87D7DFF7D7DA8FF7DA8FF %A8A8FD067DA8FD057DFF7DFF7D7DFD31FFA8FFFFFFA8FD09FFA8FFA87DA8 %FFFFFF7D7DA8FFA8FFFFFFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFFFFFA8 %FDFCFFFD9CFFFF %%EndData endstream endobj 27 0 obj <>stream +: Z\dDz'.cg$Rm<6uEBֲ:߱S(Ӓ +@ H(fKTs&女M~9#Mnt?x,{W3K0YKf4r 2ÆK?Vo*LDޢ +#iOh1G&N&d\갢x2/XVh\ҚA7-*٪LeOPFVǕsXs 1 78ﲇ`|xQ (ΝOyф=#n3%VËk GGyP bl S'ȈXYt}Xi G3YQě)؅ Fy)w݂~Jtt MҍTMN&W vDD6d ·UAjA6Y[ܬ4Լ*yא65p{jNn 9a Q(EZBtYĴr;H xZt҉#4F9]R\~ ([)Eُe{0QV&5IlM&șC8W;s4J`3䬑5\8cZ8+u~Ld`x'dgVx j{IPq ehDV繓"_jިhyo(n`׫PJ1RQkR4c(V&\!Xrs6'`:Z/ލf)XμFnهI3"ͫ@kT&E] s=(EF/({NښhS4Lw"bپR8?s]n9ؖhq(qТqT }ڢ +] fgwhVخ7QwCWOĤi+*6\&z5n>ʵe/6~nPl@'L ss9BT]aXT<#4:zh 򢓱m?LQn߀J y1ʲpuRǫ=NЏ{D@ ݯ;\Ts-MUVU!]8Re9r0+h8؇/&ʊ6krh^ǖiGt2ä ?r؟B>݂Orr'Ա LT1\A*"wx!7~X9w]+6 +-,#(͈MBFw_Iv.񻭩G+L=jW֚`/IIZtRyy阮i8}慇1i;!ԎG 5Xsq"4yv&`*jNzj!j*"WkN*(:gBTdV5<\բP\ qI: y`|=I~msf;q2!"M 浧Br8]j_ǏUU*A|Kz'w%Q<3~!kϤaeZ'TP*|i$kEurj`ڂ-2gB$=('HgQ b`9hI,h  +<=բ DA=[3?^}4x@('f-Nbݨ]*_#NU z_J+b3lì?yW=yfnnS6Jk6Ouvr |~mS\N B2skF,GN3`B5}Oě@^!`U?LrN0cU/w11xb6NjDGՒ3pEV0nmxCR"}̽p?6?7wpKExf\A1u""_r,aA;yM1|%Ȗ=]'1?~lKC^Zi!;^2} l fREһ~K4i")W՘Áբ 9y^0=Ym=WBP0iK9'e0@_>WbdjL9)պvIļ! + zF4&dS4owhTVhKu~gZr Ͻ +J]SdK-n ȁH+1ģ!po~p*881#w3ܵm +~ 5?YQ,͌WN#L=Z qd9{uNc,Z-QhA]1Fb;unsR1a5 X|d"p;zkҲ}Fm(xFj"nι=%I }R[^v=BoXwP #]B/(ZSH)R{( vZQcL1 |/_Х*jdͷ1!i6#FӤw>v$"h;W[BU dM+)ڌǐeLjv^ pwM'C +>kJ"Vwuт!<1Td떱4o7G :lȥTgl#q}PQ9;oT* {T;y%dG~Hh \7ac{ 䵭BȬplmOI,%rK̦6<.}+Jcy])xWx7+czfR +wICI)ޱm)g," UY/2d\H+F[ FfA;ŚvL^+`3ߝNw6 +H~%)'~e C6Zczc8Z8zWu`+{x*BjzJt{^%,i-}F_1C4 q=e-ʥFU,v]I7Vq@$vĹk}_u˕7~c˞1O(i@ΓFd&}:8UβϷ +yE{}ׯ(%>;^ը%is~#dJ!1?jV3jkY79t٣LPE4$ݖIcVCWKFc8uPaĸEj;^YdR$('}N8ΪMcoA.IET]Pʶ1SɩBE~]A?^'gLF]=I!ZB_i?bQgO^q,O˕[6hBMX [8>&؎H߽_FjtmZ3 ޹0h^鑷!i;lRT(m QP?c˸("Rv/bpGôOƅToyAY/$S4:Ez'}QX7nL=tЧ"Ίw2˩ ΄ސ wiSԳ>w̮z "9Zko^OK]Gpn-fp_'ioڇ0hJ4|@8CR?QdCKL+= C;Q)DM p):YU9s`q=.mTٵUR+V+۲מ0cRRhahPP[jٕxfN/QiXɆKi#5,oNg;Pz3G 0tZ -No.n=u1!NKu +ԚmuP iinn\SBe[[. 3bs)Lz:@9iJei-1 8M9$S +4['(78(fu#3NHwǎ*T +nsr'MvlqNyNYq#a;GH|=Qz0K~ # wG@3 C,y6]pbyP;|uA`߃ly$16M/j #O8C~jROWKn]m]Y@ FLDg<9p(v#9J`貓e;oh"7N\ՙ^#^h1Gm%K 4*um-Jt |w<1Րa@~kcE١7w;:qK5jEsFDR p8&(Π2uuF4#?ზt#g8/6r)l",l>@Y^xl炶)a" M0'ztv.dee~Qr(% +rXf #3\b-5>Kpk^D% Cm5Yng}=e Tq\2S|RW [ҵ7k~-w1GuZv+lAAD%]'(XĈ: MS9L}SlNj^Au gTEߢ=JWBvoo&]apt\w>W5b>h52d#G.Wki#7ՌUB)[I=\AAcQy!B3{Λ/TB ^U@nfӻbA7Mi jOVBT1qZ p=౸A65ˤB[l-TlӀ*3q~X:cJRغ,'p;mu5ذE受 +ޓL *+J+C"0*WӝFN Ih*XHʽhٱS%/%Z{9̞"q_5t9LQ`":0{)wDJr~-CnU_+O7"zWxJQ4̼' 6*w%/dX8Eq۝h9#(9K k{꿊V6gT]IcEX0B +RLFVbG,",^z3lv΁t&'`krmd79_3~&-^F'v=/^ΨMxh(skwmBkȞ05SpG*h v@w_Q,7Z@w#h^>$&XN+-ħcencmUT={ai}I1 +2J;+%SqKjl#qjuOGiJ'X NJm8~ܐ\Eq2̱y&O)J)`N8tLBcw_D߳ݜ"#C4-!3ij̯ST S0y#LfD"x࠿d#={O7BUV|HW1L-*02jҽqGg){ +hI,wIz[L;C=Thrl%W|wLlkbTdVIٕ2V94J]J 2BRF aЫ"7".\#39(qUn{*2@ڧ͵_gxt{VW5%'o K^蒧sSXoCzFxGuc_He> sGCA@?<1~VzRhj}svlѰ4 +qh!ʧuvJH0ыH\m8yuً!quzQיqͧA!/ DVpSE_[IIc1huG;DNWCkn>[;tZs!~Jzne=w<̣xrQ15 '&s X \}xso +DJic]lUA*}ob}akps`a(J F#u)5k\z_MǺT*[n%ՊL;E9k~).I;27=~|qvU` +Edl{NV'KS՛ݠPXr8K7DuS x(jQ}ܹ*wVZx#nK' ]@5ڋ[!i : sTH8jߩ~cCt6)],/ȔzgЎs 7V-˘\zU:V(&9f2bf2 h0>;@zrIzw%e/G/F5DRKS|b9]19"?Ռ|,3.6!)t +yGG+J@9k]KM+L-pj7?ԙ%R52dCE8;t&x$ȹ1%, Oƈ_%̀TE "^_pHauh4і$hZG/#J;KQ _|#~q˥h͟m'E&c+XE2kBSU+YFZ)Qko!ZR"Gr,u[652-g/s2@V\Ob"zJLV..ՏϠZAД~U5Oi{zH?Un[oU$E Mo9F,D?SN DyT|D0UŸH+o.mhCMU7"bH_{MOd*ZiG* ХoB! P˾"{콘JF~*!d5A04X;qX<  s]κ)_cmQTdQCF=W7v0[_2 +F]?OhhɰPg2=b =DeRz͹- <93Bc.1IaDK#B+}!yZ7݈ud +NL;~W؝-lYVS/mN %~~NǢІyZ;;Q-@\7z z4=皘WI0]W'6byXҽر\j sEmC ?28 jO[ODߕݼrARH!W 1l~Q# ¤\ltvMa[ftQ&3+W߫Xs`98Si+.bjifS#a#dgwk4}Q]canUٝ&|$T3C>qȩ'tE"D EF Mr7p*cVL)2ˠt3 @0GQ`C.G@ηq'̭nOIom5wuѣ߼P P[t +4_'jX6@x <]^GhÍ oF{(=It*nnw(ԃ:Xp07-o'lJs'+k #ט9p9B%؋?k[bCC8;܉Ou<9 `07e+{V1o] 42%x㢐=sUΕ"}Ǖ !]{i_,CJ)7G3(󀆕@Md٨p+|d>O:,t[^#+qZ&mDcM˝Œ!C+cT At&p"DIAb*tsǝDaGmEL9][Y0J &!̓/a1q9d}l +a A)igf2EN0$fAPvC C ?ϱlw|}qv7&lP[,h]6v"k;U ⛰b!||nEžfYbgt"ڈb_(0-:C:;Spf9 +7UB [{w  eQj+;!jg +b&MzIx=w&Wo8^^Gynt>NEeN` .='7g^*vj5 +f يV^tdߒ=n +99d^ O`ͱ"p M@#9ݩꊪƕY~`=rE4(| bO&SH+@%%ߛ:}??w6^K-zn6ӑx3^>Dxv3N*Rxc-:EuAwT*JgQc>57K]9AQ{3Թ/./swBCIql +`#vM\Sz^w4d9v'j!s|SMhvRVtnEW2Rt,R.së+_b;T8WQ + Oc۰sթ@rLYiSc1ou)|el3mW}GU;R԰s` 3qKGk]6zAQ!s)1Yi>8ynD+rZnRJYwH +nǥ4Gs !я~#,#"YqyCp$sFkfF\{uTp IU.[91 fq |F}fp!aDmĆPڙ9h3C +M_W)uQT >8=C[H4:9qyQ|wz,;UO_YAp@ېk^>qG[W̰dL}}M +BOwk 4ݢY‚,/?8Avc+*TZ'k!%Գ|$5dr:9ڢ +y( + xgU.4#@HtEm͝ÑuF>R)o_JUW?*׃q\K^7Rn56]9XIe 2V]]$}*1R!UCw +m[~> y1(lsJa+q)| 9-7i1S-&3g>g^#`HգFDxL_m|nȎ?W C=uq.NAF?ij43[,p舜?ъ>K-=߲K,=|:yu-28U F;uH[T܊L ZX/ +b}G/UO3w +2aA%@tDHTRGVm{,m `5m{f)h[~9{$Cn:PLVi{8{sbgߑEg o99bw.T\h:9qrrTKwDYҰlĥf5>d/K*|^`~bF<<[9}P3oyòj[2&Y_j+’^\*ݩ"a;8/lIow;cM I]mOD١ ǎ3t ||4ADU"<*|\P;Քu4B$тNηv@Sg9EE!K5Q.ftLE  pypٿR)l`ω; %iPS)M2}^zM.!~XCQ_KF勞zkGhh^ᾋv{0JNtlW"EbGmVoR鐘x_2w(⯔z-.GP$kxjV JF1Xu.3J%3p:Gbz}אxLVW@z+bXt\aSq(쪣 TY4_fܧ%z7 yō02s׆k=a:Qy"MPqaf[+ދà1xnWj +z|ă/{5QXbL?I(z~m%'OLNtǵa[!1p.w !ϠVn4ȆIK";Iֻ!>Ɵ`;*3QD~5AHeOTf+:7_tn* |qsۏ@%3FgfCwϵ4*ti/jK!8vO!W3z#϶s"1H43%_5gp*/̽Z#(Pp +8NCi;Ssr'܍|͌ eeWHͩj5Zs#s4W߰iy'*j.*v]$se$s!k% +齕Ryoꛞ }k EDZ"qSOK\`[a].cK1)A_(܎\şft;?nHT]΂}eR'e <$ :\ '^h8i4)L򀁲i()ǁalC>^ |1p4T'CFA;f?M' /Yr3RU؁~Xf_bq"*cAϽTJ&x8琉YRPՋ0Ð Ruʕ|객-]|* 5fFUR5H^C!xE:jm!"93Le`k!`|B#?Y'ZC.3X7Dm[æ +)#BKy9V\Q]x0)"@5 ֑%k:ˠqVzb5`:VV5 v>DZ3~(3uFt+(=:õE{w;٨\/Ds .оJeQxb/.sУi= 9Q[$KV# _ojV6w\pQ_9˫ݫFMm5_~Z#~{[4B49C֖ m9MNkC e[󟽴{_nZJD%YpRՓwW3UK +[?fD"p>Dހ{a10lm[RݬAomF.L4(SBW:s"(ˑ!wjqijѨ5)$WF[Y/ĎXzRZW):sG,R)y{uO#OV[@$cQV(jǥ3|~D`W/ 0jY):s&6?(u-mz DtrbRa0F#LRXŕ +GmEqET]#rhV$th88'׻;&RIRnU0K v.s T%ivAT OpW|"$Pū= E^f +Ft0=)@AO@+p.瘎ⴝ4V/).7VmK!骾aWFdd&-R@:dR_ X\\2?gas5茙3BiUDqo*dhZ77*^#b]z.&(\ nœJޑ*ohg9+DmG5#DA&ӓ6=ŵI(Sz Q:B)T΄pٯzE"_Sݽ7!+iG OG^VB=mSb;Ch%W\ . ib$Em(>`9X]!&0^WMYӎkAԤTt=4̖D|rp_[4a"Qpa>Dՠ%F ǻK`Awooǁ8=^_5S@N L-;}etJ}j +)󗟉u=Suň{;7gsmdVX% + $TuKft}KLoZ`)ۇRaјBu*˝iU#նCW` +!]UDnb\XN j@xZ)U[c:EB-JTVu?>DkǡۃHDɇex;c-h\9¾d`^/@wdWYL'|"TzK EdHҢ,t(o<*5 +ɋ +ߕGqN#GϹUp>2ym=ݝ9лRp4`].%gZv̳>N3ϗZtC6 PQhˈpң|-^a>&NTD)۾Չ'&/b_[eLbT'5Cw? DkU$XXd$%D.U7:B**{[K#)`f=v n|bLYqPÜG_P;A!5|w<=LT__/R ڜb`-B(684j |R C"z L/{\)b\W !T!ЮX'D*/CS%~L1EioKNu0F2/ Nj"n(΀<<@ejΉ݅C8KKv %g|n] R%lE9'JgEU?c#&Jti<=AJ\kL rxU假M X?4iHL}G8'+;Pሀt8L{r{Rg>@(6C!8p?Eǀ7?vka!S*%i]tSx* +Xio7G0L’É ϖx0cpq7qgu{^_XU6Ι+qT3_ZLC!œ@=~ol>`;ç$p"W9Fpa[εGkF1UIȸ"k]%)ӕѲ6+Ȑnlޞqۯ# SiUOb&?{ Ȟ!ʊѕ:Y -z +WxFx +6B θHQf"xD2X*qڍk-R62/AU[p T('mWr?l(bXHU,&] c|: Ś"H}l)}Tc8ŕTtߦž'<5M[f><̥x֦}H -״2HVvALV}Ẅ́-;`6)6þoKH_kٟ{F0C 7t9w噻DI+x :sbL]%*U_&QQy0Tמ4"|&z4wH'6U> :OȹK4 jǜ$=ti'=\:Rb:S-Zo>(Vо}}KJrN*!hQߺȓ>r!a\imCgh^G} v= 2P9H +{]:6 X.d4=g=_'n+/!HAH7[I4]#xKkq:w@Zm]G+EzmC)z0U'3D4IfZphҕ;_ Nܠ0!3̜ӥRO @S/RV\+Uߡ\hNowd& ʓ +ŶmYL(}NtR½Ogmnzכo$@lKJuT<.s1YzS?FrI+/0g| hCQ/x\ZP^ɭe2`<2|S]X)Y`Vm>"AeV'\yiNJ2^'kj;.Ӎ>s ( QDVWgsYO3_,_eQڿ-69{ [R~ ky4L/rS$k;L8SK`{\a\ [qn}fdY}iY5t@ħ)KX~ mgS>6ےl`VP~gوF"_͘;QS!Sa,n&@X"s+ogd A}yK2;fK%ct.doU\xGJ& H:?rf.f>*rNvy>F艃Gg=7"e`ՙiD=n^MhFMcr"[V\ ;C3`2zcb@B!uX4%?JyXKY'/5|,[2`*Tv +Mfe__xv|~կ/)FT/6ן_ +6$.,&puNLh)m]D'~Uة{_h. $4VAĩOE빌'yTTNCgxѼϐTb{9 tzf``:"N%е2d;|MFe+ʰ'c"~̠_FV p0G*!(2bcCqR$O0 z9<auQg6i*< +l29@ܑX}H<H qMK͗7?Z7:= 3^(dS u##8ĸ#Tù_)e+wǷАMp{9a!Lb \Ct#ֆѝ5 [zN {RtiHUZ^2;[5Gs[ b%GDjD kF9Sh% +y̚ 1ywc !Π0X4H5W~VvYZ*#+3ѡL(#!jGu{_?7R\yj v vZ#.Mb۳NN> <&<{r'rJ8W$n)xc@CHLRJGShPm`):H]GIvв9׾_,?ᗂ+ܵ`خ8iY<OBzZڵDLG&zњ)8J7[ 2`{q% }| վn|Mpƞ#lG#¡Ţ"lWYT"1<Ҏ@w{*!$F\zDu|dA6lK8Eϸ|sy1S ZS}daQn< K +%oAp9h_ [4{A3H`a5-܏>?G8e7 +y1cj >N1DFl!MK,}#LQ85@=0b"d( qNu9h~* 4c./:άUuYl6^=]T bA GuRu|#zTHuф:gem`Fx7МɭxM4_2[ȹO:p`wGE$q +֌#N J+VS+ lmF +r;P*P=0qRX}D3_VX`/>Xns(55i[9SNzvlAwLʒ3QCi7W2sGa(o7P6̑砿bBh[Fĥ5ٮI'%G?(Άk'y8+jvV" vD(PTZ73F8 4}諆˹j)=@WH ?}&Q+ +4 ҳ`(^J= Bg.^!!f5$}GI{{HY"zu+t[<`Mjfs8A<܄8TҟOvҋj! +pfH +l nu4.a>?_4܊0'T"ޛܑFO陱U|hP5,gMȔJh]Yf€X)HVRŁxHԨ#i:-3B3=OHW,ˉy\;vd>sʼn G~ ɽÚ&[] az7 #bL5B{ÙT9)p=#[,^]DWv\O&@aX/Cgntfl5KGE.0>]=#\gs6OAd!>2(r +%5+f"yu-~G~߸x?ڌ(f) ה`<3oh !2_e+Cj59Ȼ1@AhÕ zv%"FYH:w8kQUnoUsdr2G.fc8OT9"fdx>,lQxJuB^5j>"@ٮ!:piG8ܜܥvyNJ}L/"^L( >tO''9 B=3+ܫ<yjK3 CGzԈ f;F̜s+vKO}+Ը܊7FmDDr^\UOFC.9j"碓swŒ>愀#XN-:\=VRϊMD~DičagGDMz1M 9.3VYK==gf<,^yA⛤3f!N6sG7{.WI(ڹ=Q]YG#Kː-.CcqINji2G4$ y8ܪi> 0F+kx5uG٣D2c~#lгFh%`({E*?G|Y[*2I_/ɼ!&1X9DN4B~G&&p7&yŨx_VibL|0񪝋iEn님w|~rpU +;)"r"QPiAZ T؆3OQSwOi'?)̂:qZ1&K6Y.?l0WC6IS'b73(|aT ѓϭfGqs)b-b%9"}|9h-W=[EO0$~Ыy{|rf +,ۄ*4֝^;STUcbd{B_gG>8_-U>TB>z ak9wab*ڨ=QBZo${H=Hآ{*rRpabPcE*ǣ%=H4hF5u[C\VDe1Uw.>B*Ԗq tдh8Q/ `€Q#v~R| ?(# ѓ]GM$E=`#edTj\K.e֩ ¹T6S)VMWEpIɍJYHdC}ݕv҅:v}Nj}%[ m`|Bf9\AAZs7?J*Q}T.&}N^Cv&"Hֱd +j>^ޕ`*&G3eL>w22:zMۭ8xWm-%V+ QW8Z1O4)N_կ +=+ܡgϠ[m1)^+'}#.)jB= +;W$O60Os%;F|@dB6lnJup6k28 5?F,J%Z D)% -).ȭ~tLه>v3q/ZNҵem.ZrЧ,W,%-'>!~ X3=D5E1Z-'?x JļK [Br1^?6"؀\Oq'C0F ?e']BK(5jxiǙQq@P~ oT,힒)''x>AvNvU.h=(ť'w}! ԝjYoFP#xvR@R~}dySX4)4(z[Ql0z!xhc^Q9dň(v3DPvg&@̓R-D[LV;sVśN:zOo7gT W yJisӣjQ:"+s[d54;;oЎ\YA\ɃOC|%95s8@<&$/< +ϩ+wmZ.3< q +qM`z4C`M*p;2I9jU=CEal\$I{3c/5Ikho)kRlzM[il(#T*2B]:o~y$9*px@r^^+ +m^2z 7q'䍹{۫Ӯ1Ox)gh>G?~X:ãVyU)sZLs֎"Iq;? =Nu +]8Xgʠ]\WBtzBO8=DKN,E/!L|wթz Ej5GB$Syy{BaW #ƪ.;PjM.H'SxPTռ/hc͹6#d ^Dԁ;^Fa?=^Ƶ|ИјjǀN`^{PN0 ”?mzI: {A?! HdTZm<0ҌɈ2\Nx)wd`Htmal!rJ|{ XxFiG[zA' +(,2/#?!Ef,7GwqS/Q>cV⛍{=_Qk$? `E- <0g >e&qL +VE~ *I!QEhKY!DX4,Yt*qyZNfQxifj M]$aL`-+z|fҼS eJtݏi J ^9nfH5_vF±=a$wԂzg H[^ѾѶY8z}_f,BJAmQtB!|<*{#9%.-ɳNn4A4'əvΩ LOq%R)hX3h"mq紣8Hҧtr\6BRq*=3C5WGl_!$6x Eұ r* ^\%yJ{xL\ w1)H +J8=\v?RYzDδ:7G埨GF8vg˫mGqW ѯ%sɡ&:_zV'Œ[]yA\~"G[ hgUi>?16h}cM˔oA*NM yޥ!ѕ%$9-8_ ׷ZTן ZuT4}4?!`b;ĭ`y +lZ N0/!U6_^~YVq ɧzk;ˉw˷0왋 DkOp- =Zӿ8>ԇ#Z-JE$}O$ZvJ 9K;Zm>3iDZQS;ǧbq'ǕcS6.V@eߐPКUnRHD(C9>7h偝Œ2{um.~:F@fNa_^Pw& 8N]91zpG6rS\1?{fuBŀDgKٍ)><}h  +&.31zw<"H2ڶ`++eͼB 9n_ϼ79=hD$7+|kC$ +QR,{H?$ڣ괫2lˇ;3cDL$WDF?db<[yk[_xOxZyA3'poAM5Z#J+aj"g?cksɱ *3fE}aNxO]֑)F\)12v C8m{: @_bdTγouZVaYF} A=Y6+Ir*)D LQ߃AV%(lP##@$/=4,:[Fdk)EICdq5!tjGY/؀"v'ueOw9C OSnՑFq7̖-zeE[SaE$$wEp 8 F +լ4Dێʂ8k#*_ ,\DGrƦuUo':mF=YDS>f=/-9}pm7kjaI+v&E24oRkP'yDj3Kh7V- +$Gڑ2RP-Ȑ cHgcu I!|[T+쉹qZڦ\fzcд࡙4Ahy=nD h?-V]Q:^Wm`iRT˟4n^S8>7ܧouEZZw#~=vֿe6K/1Qt%1{yɯ=s18ܬtD9]aGy(Ɛl̚x\ #[qcڊB, NS/sƱ; +NőMzK][ {XVZv< +vp<0'לAl\$hf"r9A,d Ki#3#х߇ 5m·V7Or ܋G{&+bņ tVמ9lSg s!%"5eJ?oҋ0̀@vTqЬZnG2U;f_fݔpw*{ xB՝uF%6a<88F|Ja:!(=h8I(1w(\ֿ^Gg'Uu鑬 lXV*| 4Xa#QI$~gHl֩n ʆF)֯ZEIo1GF:)U?w JSOuI&Uu L+MM Aת@>ƫr,vE^*pn((`}?15 #K& }Gz?ydlbU iɛJw=fEѤ}_2w8µӞ7IVv7:Z`M~ t<ĝAND%Wau>$vSMRBzM[=U;T^^OQۄltl- @6GuvRwyAC83Uޯ?[BS|Ϡ9ɷZn|vaH[@љg່YSK +45FlD6ӫDTFc[$`!MHrUaLER;9[ +ףC5Y!rm{VسVhauUf44+ESvV?1O&6 R"+!^@H_$X+ò14>hmSoLڪԪOҬOB sF $*5GAJM{bK%5?Ϫ +H=x]ݙKuB +m,5Rj!Kdz?~]D{XS%!P]X2$/ڽSBVÓ|w`·J}[ ޞF ۻU2/VBun@g@#Sn{h*| UA<{zZ!2rWž,jHHZѾ*b:ΨGE$\ !oM7ϐC6|!#wvA!rD0 + ?*bݞ9" [w,9,Ki = +ÀF[o9}!iG=+w8SDU^zj|)on R<G(A[>H!Ux01}=#,-f2a>OWc{%1 THwllˠ@\DK*.zt:駎Ok7"26G?jA/]uX=TvAcwxߡY`K#@U՘bJ~>F~NRQ @)3u'.inO|(PUOnBw(nz\3%'xնk#W*WjEFGxbނz3$U#>KOr=JgL0ԌwͲ5W@]&c? LbJE{.Ă*+>9TSe8)|* ,s3Ouw)w~~(ץp'ZCKU|$ ". F;l:=cԃf#aY6WPiWT.y6fOk? iH߸ +A ˃ 4ٕo5<۷ LWOhf ^$VFy'Zw>oSJH0Miiax-!dZBQOۣ=7Y6I2a & iᑴLɼv7s]xLD=fg;I +WfĥXLQ`nͥ3r.)EBkא"xja + Ş109HfKec@s=Ȧ3; WC5)ٰ;=ɒOUMBF_/Fqqwѝ|~NFHV ǀ])4mDT>H`Itۑx!BN5`cR=Cw)ρ€8m +djX *w~;bb עAO[0?6pA e>t"ƔffsH7B!ޯ>RcH>X6o)[hoo!%Nj'Vɛ3j3nrA5\[9ͯ(쩊sBDz}Y=~^GA׳1Q! vZ̩+Kpdk力d(vu0w+\PaFwv|>;LPLert¢<ͧb|vjȦ).ߑg ~*MazK0- +6-Q.BDH%g^S4_* +M&lAb&AI:yb>t$3Ża̫8s0)g=h$%KL>l-ȬNiq{wӬ9DR^q qEBdx'# +, n -SQh5+s $\<ϸ=札 -Zrdµv $0$xAJM&זW<:I׽` 3u?kQJ3p4$W$5j8i}?őM`."+(QEpa~ rҭJ[E[`Io6'zުB&Q05^w]g`4GNLSǫ1 9E7̟֢:m8w a +8T5%z2PX[H?y&'}MY9UP3Vx1S{F[@HnLt#'*](oϧ;ע:T=_beCx5D3y1)}JiHVDgkȨ$ M`#o* eYS)k_!iDVpgztr+A,)/63*_1ldd3y_[<*i'X Sw7X u{vW>)-c^f;"l'o 8ܐH.=BAqzpJ-3SsX2ea Cjҽ$'(uE/o(K&=g8I0wrYH0'e:zQWeBˇlPG7HffP#TݨC}b<^iX`f+by8R(anj>dT!ߠJ(cnXqd$CYPNK OViz>޷F|uۤנt4! փhӽa:d)WcC[WV#*hZ6zĆ貫<>q83ʶķhAQě6⾕:CzxrttOqѐSt`G-i)s@խIa_&R +6ʊTX7v!L~n+48G#x=kӽN$H>2XIoe=#9lAMHy>9=*-BnIa@Ξ`T++ְؕ!s3\TG6jA%؍((^!0 ++\(+܉QIɻb5 %6>Ma<O[Ȍ(~J! YCiu+#mJ!%\#X@'0gtS R wBg Hnd]M4?, +߄cu.,أ]~Y };~9—3̡Bx$Ow>5c!G`T|OU7Q72\h/msQ'=UCVE+qUaq0\5f \zͽ۟ƉH kR/ Ыvz(JUE0kjWJ|W~ UTOL?Xp0EѨD +P%YZ[hXUi&&<p + +YCWLI- +$2Plw2_CIQN᝺hJ_7Z WQx囧rn'2q(6I3VJ+/ʀ߮A$ kp-zuE{󟯆v]^]aO{={+eVOCm9~ U-2rCQJbމx6wh[¯$e`ꓪk--:We<]?IEPU*gpRo:{# ޵6«}Of fa5Eʚ6%ΫSRF7l-D 1s_Y۝vm9h|fA™FC.cK%\+BKPiVL yc۳iIKqcj J ZntX;{0 \|yh pJGzX!(yBrXDgJI$*rc~rq;!4e2ygCuz;[;esGv 5z{vmW̒5l[!y4V'Ez =_M}/A;9l6P!q2) O6Uiя( CV@zq!ߋ`N@3U^FQ{\"yH䉣@Olr<}%SPZ7!!*QL9$ΖJW4råߤ2T?C.YblT K lE/]~JN u3_x7*-\`bhE?nv%+Ȝs)v'-9- & w! ]=STJzD,jW$oBI~Pc/'WvD<%m1+<9vs_Ol@4l_Eez0Mp P \Xmϵn*ly=T,vVsHK$W4PlpiJ8YgC2BubT= ;cR}6 +i3w'XW{pBcڀ5r[@M"zcs@t;L#%m;x[ө"4O%77 !k͐N镌YCj;KkZ&@o{kY&3q4 ZFʥ)ϗ3)hوC\! +(_'CsD|=m ?AwR2{Ɏ +w>aY7ݔǴN Ф?9Lq;Cm|YPs3Tqnݬ* b!;lnFP!*2'n)S +2GE3artԴ*kͭ!-Lkei !@KieEIgV2RV+{FG_ghFIJOՔa %ǓK^T]b09yK&׷qw&wBǢwU d蜣B+(!HL{!NCaOq?B͌g.0|)EY^W^E*_oޏGXw{lsC*UgTs7NKI>Du'}4A^ΧjY4́ pQ4U;>e$,{ )lfX pm$8Byb\t$“p}mz!Ub`_h-ll3'Q{OYH sm[7E)Pc66Hss&ǒ {y:?*{Fi!M=' ڶW%\ɋ&lk+U̢9ޯn~;K%oQx3}Z#Ɨ+1[73#\1S/#*s+t7ij6VMrJQ~0ްy!=۰S-HxH׺>!n=%g AYGfPL"Q6Mju6NL̈́?'ǐ'Zt_ +wxΣٕ|(l9DЇ@{Q %@hYvwֱHS%t6r/!PRzS2`65q$Y>;L'sJ-^::Ww+}0w˵[}Ҥ\G +HhQ_9 +1Mٓ([m0Iҡk:SGc0}T U ӑv!"34c$ţu#I 0uQ,nY"P$xӬ8 >p"G/Ҟ!'Ja:N>HW\a"娅;^ȔR*r|2""9&'`c.znBx2ov ɕ:ڿZ6Z +sV~}zT +WȈ5v緰0zJ2볔E3h᱒3]D-֑ZeyQz8"v$FATci#{Hd\} y9D}XH^ &:*pF +t#aդi=@:mv'yfPѤ;@>êH%(iLE +#qI~]յ+Lڞa'Y=%.W$:BL B#jd)5[b:a|aLuP%:ir\}@םp@NP0v=yhߘ_}RuN \]#:5zz~[A_č͔ȽJ_Eh9ոo(Jj Hw%OZ!ԙ+ß\]ޫ~ _wc+YC:MD(7R;#cIٵYoHwfӐ+./u7 U(Xe 'F}F*,}K>ӗ'dIms"9jc=$Enۏʝ՚N?uJ5u +Ģ- )n~#gԮA{*YSp|b}2'JT{2'H7 1/Jrwx+T0RB *8r|6ǹ^i +X !Fmڬ/y5X +s5Quv䬃r=ӟXS?'÷ '=i>Hmy{6t ,(:ykei(_C]`G.& +X'dX3y*Ut7*Ly=")Yk#آu8XϏJ +_>qjK68$""C.>9B^DY%w8` 6 l4;ޥ.sZ2P< }N$&` ׽+o1N&|&&ЎyD#{ykݬ+ 箚C^/v_N5p'UK׎a'JJQ^wi>(98ӹ+ +l `Y8"/(~nj[0s0/=V 5MX/?JUοheXo}3Ƶl+j`7"S.!FZ:_zYzЪשs!27u3+, 2 b Ty##|'7k;Sy4H~%Y1c@Fh1%T`Iuw .$ -x}lkqzǩ +gbXxJtL-#)n)70csIll+A]D"%)ìpu䨜I*W"+F x1kVa IZFϚX)T(k;;UG5[`hPrDE6j̿Es"D8ٵe Z\yg^Q-*";w nFP{G$6j¬svw"%h.`ǝ ;zB`9$Ҹ6t#ߙd_L>ހPlSm{Ê~ +8iǤ\ :E^kV$_Ğ;tj:%|xIa~'Vͮ( uBHFץcv)P kۢ9=u✒ QTr}GwN~RȥZ⹷z4bKڿ)dGIrBBylTg8HfRT\ Cl~=Uelti kkҤ7U]F Ҷu&&[YKCH-Vlޒ]u +m̉ +À\c]8W#k60}8go0#X[oZ|-Q[~X"bѹG݈/hYiO-~>bl Ғ7j;Ay=ָΕr@\38չV$~+麬WF AuOe#8ʣuSm6? r㈩+_a=?hmP1b:I69>8tBzYzWW*ĕTA߼wS ~YPȔݹkKLYQP@A>TV˸5*N +-{eV4b>ڸe-8_ Y\{ +SN{?Ʋ? I_;'U"!gevA}Gt;ŠhУBʝݞ -nމG ,zx#JnKoA-Frhd;`N4_T , g|~w AYW 8OW T9빉٤zIxE9 w-mkFEZ=zm-OsSb9GV_ YKh)>!gMx9cIlĪ)_8cV`mh"芄z=iyw$*g}I|c:{{t ;>,( %|Dŧ`SzMn{[TC);v#<&&KpKX)cǛhM_/2< ~[kRrXK}ID۟b̈? 댌DHSD},\%'\xj쿑Cu9m)gZ; A^lHN- #n2\ÆG:*#Q9e4[U Fx{zMA,PM@#H5-9!K)%Th$[ɦIoٗS I +(CX:a**ޅa@"MRg5נ6?.\b؁+4VCƾT *EK3@qX#GP G6K7=- +\JcDXO+cj@j}V\+,>e6DE՚yFpot +S2䅔R&*7Zvb%4%+'lQ∧/C *&Q+JDh$V2әfO(jO #İb3`W?l^Qx _-R\oAo΢ yTST)-6q"S=7BJf[0s.Hі8$a"uV0tV3)s/u> +¾A!0, I*UH uoGOiWCm0On 5-1LtǓ"fb8s<'W4f' +UsYq-pp1O@z8jgĴ'-P f)W3*糇JDQ#OD HKQgىC*ڪ^~G)3b*\b399TU"JnGjȷu:{+v-30͑0\`{mŸ+֟xWg9:;JAYI,'Ωoeyg8 +5Ȁ?kW=(i,AzJDkW!(dȕN{GR|4ğM - XOj+  H2Nz.ٮ+oSKh/`hƣ+>(82kk1\:L̆ +EOM] +oY8(M:( Ab]iFDT%GyW4Z> &?8c&06 =}#}x:yڠ3 +%еcQVG:W$:N}qPS̎jЯ\󡈻c[j!Ngoҕ/$xL(Mu/*Kt/0ſZ}jPWt :T\9Xd^ԵqVH(rDOmHkГQrw `|"(W5f]a}9Ƴi +tiGWlPձS0J;2kQ)fbO()r˷@6W^؈oe uH.аΠ@b!dZBZsaۋl*6sH)omdMe;ku[wiO.Y>AD0tY ;~|c}9zr3^#$ U1wG $c73'޿o IJL8~8sypM7]ЌMtvW 9$ B{)I.ft+~:Qr M~JuP{~P;߱T[Bz8",T> ({xǜOwpm_F]1De1%/ I'kKӇÌOZP199^(NS:,HgA4|_ Xވh^#F<R::[$D^=;Zb[g_$B#z)[g <#Iˌi{_A-ysNoJKz%81E?}q',J*Σ Zx@Fko=LAW.]աE9WLO} $:K dscv܃>AhG+R;jk*2o$WZS඗ϥ)IB,::+@^iGӤ: 5 =)$(U{e4&n \0_' R>/dċH+0šV0Waĝa1 ¡K^]a'=XuxEO n(E+E^J1,„h|rg)i OJ;r`Tz*&.a2|/6S֡=l97dԞ=' !ӰT ~h3V!L$EЎy;ؾ@ ɮ6nbtG%wo#}jV@oƕ.^[GDS^>:j0ܝ{$*Nx)CV BI߀liMMin b%#<'Z߁S@Ÿwm~@> A}{6~C^_Njq=5cΥ(s@ ̹u(]\l%LY2 +l鴖]:tВM>nwD !Y RAwK׍V$`@ "*WxBԻ+H}֧<FzI.SG@FiKnۼ$ambH#e9zKTbk7ZGf@BEYvītb]hX7kPOlT#XwiePḞ6R#vl_ W/r<,43䪜'o>%BV{cBQNN F$[d.nA ü&²K *=]ΓT$c9^ʙ3z%>Z.uI0kN7ap,H?T>Q $6lU>bpH9c#jnᕑ" ۈlo#m)MxR_3O]W9Mofg+ srQDT>e8&1C섀M]rrvT.IY$2qyD(S|/KoŚ+D7_m^ *nB;~Kżw$Ĩ-bewb'A}vxc&68XMAmGi4dN2Yc: +Ϟb +^#*l$lQfԤbGC9!۬ϔ sQT_iVq]ml>Zm;=jҿjԡw^1M?q$_)ޟ~![I$7ESX{1=EljR߁Q})J5r|e9գemDzo?V7]\mre +GeT*r%TK j +(_o"?eʶm_g q*W.Έ\ Qz:J&T_\{r"DvHGV.ZhbAY +pI@F{|n-*N_xn{^m0x[+2եEg\ޑi6 x7>UAW JaXS,w X7ijef'ZY>NQ]##_{ϷV~D%g%Ng vY9# CQwۗYJ5ZHk߼Iig"RO ,SGr +Xi~˘!CrbwEE"p}'cvsl .{9HR;[^ckpQ߉Va[%Q,Iie7;R_}zwnq@>Ri>uHN=?k~kBH⡂B0qo*t3Wj@^3-1zEōiZd~ z0Qw#D4m\aޯu!MB&TS }>aDĥԄJ˻JFpqt[͝3/v'\F ])O;+;eň9rI$(Urt>LO6+>Ro`[\\!kr&,!S~fx`0ЕMY!䪕2j3U`oWT=&5JT(J~f[p,;1I(! Bˑ,:SNM}/C"Iii4B\88F+Z +A_Q*絥HˑiIJ^W fΓY~p<$U# ̙&@'T̋GP֋rs.!rPs"xs!n>l| 2)7?+}A: +P W(9fr]4HwGiT9 SgK8ʓ-sPlS:W9k>"A>n'}l9ai! HZW887ȗmBi?c *;U'5pV&A[C(Q-eDꎳڜ Ȕ⌸Ulj*ZكS08;+_GȐJ #8~&)rN1pːpz5dr.up2kP j}.ŷRǙ1@a^6$+l:{-q+-fv"ŎM7OoC +),EXwǪu֤#.#ƱDrz8A0+9Yew*.J5g-+ hrO0#BOFIk?vh"(G5/#%D4CV{x!Mr;I..+q[/`Uj]_sҼ-i>hfUf[~и1#+,iDSPkRjuS&0l'ߔ`:(PצI%Ę\IFwd9p AaId7Q 7F=b_ҷ&= }]c6->e ̨ݛ]A +tUܿϨ)rR+ԛŒ#A$4#1~~ dAIFDgrQ3 "g'&f7@;M_?,_Q8zaM6fuJړ *"H4B! +۪6#~q֋\uUآ]ލAJp(2QU/F~Z`߰0]*#$C]tH;%Tq;+!ǖz,.#X~6]&zW}+~if-I?fC!UG`O⍦ZM/Be='^~[ː@Q:ƖfTKR8즠0/FL<8 AFRߣ&,cYC2@׉ވ%Pevrto9FS*R'ĨfݐaZxIgEMgCpR'"׬Jbj5J#sD BmyhE r66_~U.i@Lp/W.^oD[[|Эa[ fx;XWz͸rV;_LE.c;!A28ļy-j87z2|R_ŗփEryfKPi{k.YF_ 54K 9XӠm>:u6D{d8JL mYim=†Ci}Jt>B:MXk$XyfLłHȲmy$x֧KAAuX}pb +X[/'~S@~KGɄM zz&  éESgJ AF o Yk!W:)y)`&t`kQqQì-ZN` >  MI +UhB!P)PKz7?l-%c릐8Ɖi=7m qĠ5}.ͮ(@ +F&.T20,u+CT k@Ẕa/9ћ& {dtzP5gW>=*gPQ圼p5yR(gĉG:V߀& 9YfaaE~@ +_Q"SG"}>&fiLʹ'VvMnÁ[01+uuD&K){pzk a5QW1) ?4ؽd3YKvv*p߰AIwC@ m騒E +!A6t۱(D{o@wPwߎ4~ke#,^?Ud Wo +yQ4rLA8cXNC srOFo7NUW,D[;-LS, fAO*(xcYѰv΀uch(|J yՠ3O;Z4ݓ ~_#MYP=JB! +i=۷?gxM&~HQ/gaޢX`kYdTBCt +ŎdD>?BWS#Zccv鉊nW*@M+>9i&Tzmft4YO)N)E#0,*?]j4BSl_Z;<߿ouԑ^ԑ] 'nZS:=WoZd'ߏD=zTH7` 5-AU3UMA[`w[aT +dap晝5X=T'v|)zʊB +<4Z:ٯCISFcyˡ bD1WxEMutjk)5q5<9'h:SOŏK4K{~tם0.J3G[ Xoʥ脢1慯؏·iOAW-zIi`m۾7aD&7̔u);# m];i% 6EIߡ[W4Os;{ӭ&"lD4烮?jyKEiScאuу֊ .brA2nm;fmkATٹӕcoYP5?Q2MMX)# 5Lh*npZ<-Vb*m wfuxGf0\8BL{t/1籥c'bj_QJ-B2m5fQ2C^`V[QG 4gSpڊ0Ƭ&57Xk9Hi>DNM"c" *'Iǭ<^V eP*}33p7l fZWYE#J.0$#M?9]'"X#xHK/,mz((NK1]ޤGl3bNwȎ.]bE0Xq ֣߱rLPzp:*uu6W:_kt̚`?;<$z(zlnq lxj&63PZYTCO᢭ !~}⚿JHOآWt\ڟ)q+'ZwL(~|ۊ0gED'+IV|cE3˃gfYƞ[YDqCR踟ޜWׯyV{MN~ǿcBK\_ok+fx0@Y/62dxVx L>;ML"uZ"ձNcg\cS_?9\l-՝37:} Z5zat6Rohcdig#߁%o DةE[;2hXDYaDےs^P,8~v"^vc|*"q7 &D@ϯ}XP݋-FYg'!Y5}dLd܃yLX#hw#׋kJAuFjAQzlmK #"@9~ݙ`qɻK좶ф~k/w!dl:K#B}Yg^Ӗ}DSO 4 ' 1 ^~e澳XKZZ$=^kTkvt1<ОJeO0N  됻2uq}$zQ^=l |y|TZoUr{^I24aG,0N/;!Dt}6ydl!L}H#*+xNȻ~:~8 +oɗ/ԏ+vGT5T^=DoAgEP,V]*Eeˣ W-S9Nh CCc'*G_lӶD4(Lg 1~G>pSI'-+r?eFupGECYUd̸m9!2T*GdI eVCx0X%/RLWA{莫9 j`g*o;&ΐ$h~4#KIvl>Ԫ6/"3n-X=ҿp $3YX#SI%L" +0 >fCkР<Bud kV^;Q`8^;%[{`GUba;%vI$5odY;|aho)*(k:`ۥ(&sR1FX?kFuڰZ ęVCC5Q ^CqVUCkT؟|O4x1 -hwKϽ5>I0C)+ek1נ%+}D9/pŞa>]Z{*ܘDF+%R=F.}h$&lKAEP-gu=JEzDMf҃i?C4nOa\*GeI{=9s۷E~ 2CU+>wh+ۼ#_n6=FA=wr92(@cz3g*Ge$阞ABf\E35R1co=驵 OS{fHv{>y'?CisP3"okVfFR3Bc72$iaL9S& +#k|M/4:FB0;bRFC9\gQ& kJ̌x:5+^|9"ssMJf̌8Zw$KV֐:w#H Є;O@:XWC*x:D"6[##X.[ *;˜;R3U:גq$kSԤ 3ϺJ-[29~(ͦlwr%]m.N7}r1i`M*\JsG>|kz: DD/k$ou6ן[{Tjq֥i.{7$ ,)\o4AW%>JV>.Uy7ݯ/$ռJme\FbwI S(eǫ3CDBХZ9i( +bfѣeC@͟X(Hk;*=֞6"[boqr9'dBY{_ sdNH(bG}< E+=R+qZC MR7;2טu.葽$h$:6:V/x7+͘Hʩl↶D~q7E(@Y%=vEG:ԿGm"J$>w\ I{I"tSq!cڨ} :zB[kB+D*b%BJϓi ["#D}T9fN4ġmM-)T^TQbٷMjt9|@י_}?_P)l[kVteÇįgDeubQ[z{.\wjwGp,LHwqK=#*>w9X=g"#<~sD|LkPŶٸuVL$#ĜV0rR>)Csd y4\NJ>Wz 9Q OT⺒I5@nJ9d$W&g|Ǖn3o7QUۻGQtIZ֒ywU4ߠ̆]٨@TIWZÎj+ c +}=s) (b "3hCGSYL*_ +P,Ni<[fG Ls3:#WtCs +!1ATA)~- ~=b|Ѓ6S~ V:- c"+Іcl=|+KZL}QNpı6ƑFĚA'㞫9x /+`,~@cvZ۳V JlĴfҬ}Ji,ɺ™#L4KLX[S4&G@4xYFf/+tLj۟YZ ./$8?gȽ1A +L2.= +/mՑ`M` gkt]'ʌ@dx[< #" `oO ZK~'$yPd F ~3KS +[OΉ=Ao|hCg~ӊUu4mI"ې:Q䈴PAFbRD|Gɴ;rڭOrD=2#2jXT]hE\?#oHH>cgZƩ`XًzGI2JO᭣adHvjzDn)ψ&Ne }lQgI. { |}9i<`aLV^vG[XU Bs;/‰ !Y3Qp@ĕRjqT!XB FԆy=RzubSn]GXR:nЕN5;īdtpEx6B1Z-;5~15~L[(WXWjr˜Cz2%$*Ӕ~R*Q.~#%B&\pCcRϴ$jbW<=kY`3%Fȳ>f{vybH:;,'i&7EPc#|-U+n;-)A9; +M f֨OF'%u>.t Xw] +↾^/ +B`)9pid+[."Ǵh;eV%U34ݠkS,$i܅DjFC<ӿH  NH5ѢryRr{txV>f tz2I{#vq?[X!*Ӫ4=@C ȭ tzd.GfMqE\4#=.J#rz!QA|=-y:c'‡4+ ]F![~'. >hW Ddw#:76|Ct+-2w@~%jmuxgN.}Yu7gbjs(uyZm_Y+udhQ튁*ŒUcHWZ:<ϯڟͣwu)UkR`T8K:Gj%o :xQx/W=eh|mO|$(HG' ]լw~13UEem5=Q -wUSlhI՝|#ݹAX2$߁!n`<: 9"`Wny4_`86]lJ"{{Ezz +\E֌] +G΀X401qFFeJsT]_(Ӄ5P!`ΨFIx}utWCǣ܅^(}d`DUڏn -*) WfwŌp,U ֩"4%= +ﺔ%k*-pe6voDfDwd*yD6+a.E bmӻp:pp{Q˛dǁDWAj{Πuቤc(?wxrw+1a<|E+FoC#ypz1g&^>9Jk>aѽw{~ĝu[,p8%hXKޢ5"mtqԶ8ĈB +q*5:6 +t7„ FͨZi" ["LĚJmuEգp<TĨ:wX[ʐ^\COUQۡ?汘HmHt#3s=zځW̃u +BZ׆NFljڴPS$^ȧ,uJ#?*<O2r%#Xguz;(Iըq JXW9[ΈfY~ ]Q`k ,N[N e(:0f*"%_\=/ O+i'l[I.XfMX'Ia]ٟsɬ?Qh&Rrg_yFoqIFBL'>^8f~G_S@+L2@` $yK! B⎠42m(x_S&!LMwe +VF#Ie)Ӛ"^ @)oﰉ:߭h1a II >U)hx\2GO`ErF0hrT)o^/,pKљP=sgYMwb/sjnhɊ').n + > Q]2b֖!#i6,HMsWޝbQ +yh +;v:M ߯eSQ>JoSy8pϔZ x4j|Ki|&R^C߼ZVGiQ'=ߝ,ИLʬ`p\S4TO#$x޸iqv 6*`um6YX O!IiD=hAC; ;k!o" +~C(1- I5猩d.ޟ'Cf"3ol[m 6K7%֤8.;eؿlԿ|%Ut +Kd 8$/n[ŏqV~F~3S + x+G"fzD-,Ҫb$S%B/] 8~ҥcѱZ as *%=3_Gt߮'‡` V"VYZUeR CcǽMs$b\s\(w5" w/T؞X΍GQO~,/kBEaw>|Jʈ$"e^VzPV?#,nxTA8RT"nLQAb3!W}AMBJm 3kF|E5JPL?nSshxLmB +nr^E+ߑOrA:Ph?y*"Ex}N2_~7 +<:qƀޭ;]2Ebf 6qӸU:`5(I@~h* %d}o_ n}=7_q]udNՊ1dFR ` %w m]Ny72E 2m@_/,gz3d3ѱT5.o%;?sl +A,0m2) np7ps&q./bo<_wTvG~Q'ݐ5-kPQ+%z5 `d/{a|&YNٓ^KQK=)GT=9c"\y8T\*x^OQ&qi^gXӹ,&[+"PJGmєc׭ipFӭ^*BQF:e +<X#` ե7=(,;J2HӠ%hx tR,~W_|_+LwSK|-UH'p+)iI*p _!Ih +Jg_gaU,&'ę3e/PjߓC*_'\֦-ݻFz… /'_%K<_qydt>K5F +Fv6IR W 1Fk()J> {'H"$$ڝa>`z"T#"N|k6ք.F&>pzy1p!Α&Qod=iYC:Z ։!ܡ*_Ep~sz+;6GOtkFx΃@}"/yPCheSnA/l,Zx~ԓ!& Bb&`oLXfTD 0?is(#eY L4K; 7 +:xwj 1&BvX K*ĞqIshY$rpo_73h'2F"G Kr Ӕ>Yvz[`jݚUdgȑPI@I\Ƃ14^^2Z9ܡ]rs:ŕu2AJPlZ nU͙s+ זH:>^OOl,Zpi-r'|pCF2)wqRώRPBG3zw|yX*LqEi&67F.WzCyOF5pY4rה!ZbQ:O׸L^X,hLgy1kT$\PL] !QU9eJ@P?3! ۛƵ5\H 9?.lTq3vO)>3dMz*Bhy3M8?SssݚC(Cʹ’nw@r&GyiQbWL 31;Aڕ8Cn \"1뙴$ԧE;=T$*x;oj B*p]t\'}[BT !կєIcu(K!CӴgd7R`K3P&3\J h3Nt^ +{Q9twݬdܚܢ踲pSC_5r>wRك'EYw S?QHYaw4IZ$q]=0u e(fh]/7gijQ^ojlOh>{ "1 +`8U6CY8#xJjl|{siًfwxjaؑʗ(I'ܹ *"mwM.GǘuEnp7o>Pp]۵'1;IO{G?5Hjx@^!gwb\pt`#Rp]yR*EDH/'IGqcK6F瓧!p+ܷfH +*PYg5GN%Jh׺UOn@6kP4SX͍&h+P} ҳ9/]oi͠tDȹd 3hŠl\ hx(e1d)BeD${BC!㑢XR*ѓGcK|ol4wuy(H$pjG9dOްBV^uʓQo9a=@>JrD2q~20/Kzgw;`r]N{[ >%M˒蜜s82 q<`=DM&v6tfK.+H\Nvϐ歈-3"L# Ϡ.;ԑ A 1Wo5Z;<#?|WPʬ'GG{'O4\JA4}}Ci+Zv"ŠhU`3spS5`נcEŪt.?WUezY|[՜8]ug%C^ -ww7$l-t?i8<7(د_b>([,VO!XJ$dG)^]'9<=2T3 jHfU:*EnE0sA- -Ax=Ցqw Ksbh \NTTlhm'zҍFKH'C+ 呌nvϟW@UZ-)Xa(lKut[4%N e\!kwOh3w}?0GthD7n5 aL ; +iM3z=ަОUag%P KI +&ŠArC"4*чNğ+C /yBbw +ŕgքc;ױ]#KqgL*ϕNHԠXfH#G +HB'6ř[tdH**F͉S5>stream +L}%ב͘3xpU?^S GfyW07tT*pξRv5R+;rj)F@ 9EAڋ8l@EEY˘HLqiF6A=_XV_՟=H}MyzNv!R7@OAܶ=uzU5$! gCtu'c Tr8ĵ0g7xA`lƿAE"Gf%uϥfkrCO>|)wҜՐ?68 +Ul܊'QvI) ~pf<>^Czs`uoW^M(F M?_gvz.WkV^J[5\U[`]CN;XiROŗ .ZskK%L +V8| (TN+e"d_B<7$D[.lyO^/sU*(8Xǔs`tQEL ioQlMRHߙ5d{AѬҋ5u?oIE$~ YL?8-w6 8dOI(ea)N\#rŎ%M^IyT;TJWBi&bO~=lAB5jU0G֤:~PJhZ컣\F`bp|M^ %3^noL<6B>yD9Y{dd6-.͇Ws{Z7jEl_b 1T^k]*8~Dz a 3ٸ&'$IaD!k`wN2Hl 31:tY koGtYmq5Ѹ-ѡ(9>9Z?8~8J?ce=;SRۻOK ڏv֓C">\hQ YK aYlkéA0k[nL1Y4=p("?V#L%f+JxPh_uּ~wSi{F +gnmaE:4 (Z~kY*9u.7S`KGOF# g߅0Z%Xyiiw] z F)/1δ!`6r r(TkZAز[}S˥W9FVI٥x+;[wO}w&>ސ] Gqmh;xKhM7q% 4?YJoADU +McE{vJ,R*YC3h[o^U@Ӽwn,0e\3͞#C.rvuDay9q04W.[Tni\]q(JDŽ3*lyzBOP<$;x`DXCEIf!iCF*WmLQ\a| +C+a*v29kpGQPT`"iBFɇ r~Nͭv^ۺ\ +eLIObʷn쁡hJU?^Zj"A@"$0F[<-zROlJO ϧdC 6`8'A8 +TiH"e3Ko +0U30clҋC.繴s'}9(et~m8h%%G:hlgSBw,YCF)AӞ8MլđFХ;dΛBW΀4 +, }~P#qDut(*Ki-?R%3ueBbNw$=ݍ +VRBuU NAl B~hRae2]?(`*!T + :&OIiwU@KʹЯV΅9>'쇂nE&q76MD0EeJ6 +S;m ]|6]M?|KNc[5Z*je w=اjpL2B +?bIH*qU{o@WP:P8ѱp"I1583Dd N-k + +6tuKaGUJTpt(&;n СyLNdmrܙDy(a{=yE9 na` X2>Ys˂.Z5TL7t!5֐HҜoF.8tpA-#~hHt$4} -Ĝ{C9* =#]9S: ]2hwe}z"`4N0'jcI%UtPۆ(iè'\!JDgN3pB:)ںG[f zoP9^Jc4|Fz`Z*ټ"T9Ŵx#jЌUrB:j2; +-lX{Gurfw z>Y%9H?1SG F_J AY4c-ߣFʶ)瑼쁉iמb^1c?9;,xyIE/H~@s[9("EI[g(-9~iLf~C͙{E?Ұ rm?M K:mitmjxU4f{;Vۮk/ƦhvqJ\&k- c$zc2C#7Zȣ(m +gG&['Jo +m sBx&5446b;6:ig~%)l<z_y}>Tb})4't{f-gRdEUp涊Ӿwl-솃EC\L $fAp.Wr}DƷ- 6}}IUw=68AT_A?QmO\8k3!_Hqآ-_k|O0#fc/2C:1B3R&±KFʅ1d*Qٸ:I +S M3 e AP] +]p3wkN*,ci9c~w a 0m)a'Iᕒ[%G"PwOVL" 41>v9"Ɛ\̱[=$kȐ"y+Җll Yb|穞zw6"}V=6pR#>ө+EH-Y|Np?ӟͬuAB +NzW1$?5 +îs,0ϣl}/V}li'$i&`gq\00iʃJH&cs7I5ʫ"[WH84^2L}FOA}@e"W9}GͿ:mr +y xSyd2m!6i'X:p@=2ȻuX +Q7h)=S J2M93_Ú*X9?@ok\)]Au+&X\(2| Zc6ͅOfy5 +GtdLK%NT"#[K=50eOTƙ"%1c ) +WCҖbXe&KW %=pOٯ- ?aTmSb>g䱛Onk)h;d(I$1";!wef c/3>2F7XGuC9CxG Uߘ& ىd57`9A8MO +/:"H3,GzcR1W_aǝL'A:03I⻥BAjiAO9ran%EX2gd+j >!u[΀/B9A5ۘW8ji(cS/aV<#F|Ie"l%\v-񄃶PY= ЕNCI +.N;𔰹Vņ St`t؉߫Tj!Pd8` "G1WLx qC2nkQK~ -kr՛l@C*0 p +u=e/)Bl$\wħ4@GzG/k#I|G Cm{aDo)2p²;AVji3>Tr*m]!\yu𤊠F%4„3QX6QѦ#oh25mn~@+dZl"Ɣh}~9 U ^;_Ql-Yᆿt%u8j YNH,U/4OH9|A[ %GI[!R44W59#J- 5Vgj(K$MzJtt=x+W! +oda\'D.ڃ<Yڍ[yIwkcҀzșEͯQ6 Ȋ S |OœkAl:;c"Sc O2$;8fg@%NB< ;JFD#dM!HBߏsvӣa s)F$3jdP_N\=Z-y7O3CXcB(;A9RAS?-܃ХSr)0aùBs Oaf3r^-ߢ<#"J |nR]w=˫Fz01b^*C<)>jZq"@ JlֹLxui kb,sI<\ ^Nc `̃!=#30fIuM.5AgJ Î B7Nhy%c5ď970QV^Ղ᯵[^FFBt hg#btD]6f.ܘv-׭ xO +q~G}ԐH`//@QmSzWgcr'v, 9O3g;ߙw =8G0^v/jṂ fmR혀:,Hk^@èwŷzYWNIgqI,+r@۵vO U-C)5S+I;_tfr0]Jtٓ{/z>r^={pL)e~d#ղz:̓7{y-XXCE~8٭`YnEGwU`,v=a@n费l/qNy8ZM~`k_(ZmTh{H +29YM/?bp “SӘCG7Ÿ] OI)2=;GFTd(ى9 +ʠ>;{g 0eȆ*w^UsAjA8Z}!4i`tynl_C"#86Y7yDo懶OX3?qk)eB +S8JjYx EG>ZPBpdC3,bC"`OCΉՐE; wkD'#S7Ч| pi$|3A`ec~pt3Ht^%츓 %2oЏ7>/Qd Ol_ix7̓tC)mt.|{8>.4U0--ޅd /Q,T.,Z !:wW.iQWZG{0DK($jnAyk#=.O 2nJT/CNmT 7 7z5+8|:yLIOG܂m&qolu}tOq0?B  CH]^#J(dE+wD @dTEf6?'F$;'QW ~3NcՔ+1{D#ƌtݔ]bD]JIBfu5,VwWdQ@4!QhmV`X\W" ӹ:(uz:%z<=\Xݞe`2{O؂6N`(~G[ LcggZIHBzޑ|d47d9U'?ؚHQѝfK]=Tf};|;Ba 0ja z o?_48;*>>*yo!KgYQ{Wiu` bͩD Cxk|/*yW#R6@qI |i HP^yD*̭/)t0;gdqk*A1{#k^rDV;<ABS ;cp'-F>%ӆSߏ`cGWz;)adꁯAĊo.ህ LKj_ߘo>1de?N/9SF#%3.=ݕnN#-ڢAǙi@PÞq E j92BX#.q,6yϐ't"E)' Aصk9uxħk_@hy&}+!HTE +{͵P>: ٹ1-p"L2_?7荝jK|?U9i~ty:sHĮt!Gșp TwrVûndx@rg9=accgA]ii睮FqAd +`3߅8]&Hc>PGό`-1du*ކnRːM_.̙x]QpԐb6c2^>\>o}7cK0 풺BB0gso+gF3Kޒ#cَCg֦uX8'#8|QүUW)j;#LJ'/ҞvQ܊s^v& +tC@fUs\L$lΓ2M{lvCvJgN*X]FDb<'D3A{nz`5n=97`AaHEDX0FF.4[Q``#7(ww<jbSG10.f1*dU~?-^`1n2;x}bݣs>{_pʲ٧"'s@&|H)";]p8j>j#~ =s^ޟ"q zr( r[ѻT]{Y Wv = +7qv˯żG,fjIvlrQgr=|(\eDz\!^ Յm/PW+wЍ-6jWHEE_őGzEvy" iA%[u읡ѷ:w'p7!VS}|}S^zieƞׯbm9-W1XFlPƈ{s ."dB;b/:Dc/LJ34TŗF! ouPZ͜eF9{HlQCvp-ĿjC0Le! lpKHf=׉e *QǀfgʋW~v>iSrm.h :GwIy4ErŮ>+ZoQ1H {>A#aua9A6Av_^֭FW퉤,2$ʓ.g:+a#VLcK{4^ȱ@KN]@Ѐ$9K8kx}l(GlFϟ^GAނ!PX[K61WO`SbL+:Ղqmިw33gW֓K|g3{3ef.Iiɽδ[zf2ҝ{M4Z A%rd{XAfn?r8cl*{ B)Ue(+V>% w OLSW1Qc1%Bz=zŘl@LKAmPகZ=X] a.+hm߯T -RV#T0h9ī!*0f p ֨5h.1Muu>ۜO.<{94H#sD&VC>CZRo :*ftDx3>+mlx:txi7ǐW8WѾaA79;%1~wko׈!l/˯)g.^ KARaW/O`~;iJWU(k䑲Uj{]zDzz}K)sobA KtUzP +cPHcWPȳ.%J#nI`#61 j[="`pl#]Fb+jΆ6-ň#'%OUNhbDܣMF +ȣ >F2Id 25lyԹ0حdUbE= +hf?>wnEPX|4qk`^k^Y5OΧ"STr'7,O%C:]׆ZduE>~R38G?C?siM{5Ǻ ew B7ˠ N,Р&mk|CW/TJud{GhM< WbHIf2Xk?,׋nU'j 7^X4h@ 3=klUQKQardk0Vߊ|ƺFFМb S r7]sYz&5)oHd{+٫jkݑun<{mmTm_^)0d2./q2Pi 6JE)GuKŸ%f0YTn;_~+邅?BNZSfԿ3$J̪;TwH/Ϝ1f1l4 YιvD*|0uC3zh~q jt52@HWI?vz('19amȑY"(#ع Hc ;Dm4$Qb no:"ZvTvvhБL| M$zJ'{/_0MxO AA752S,c+-PGT/ +xac-Ank"De#GDxk㘎z%:=-Į#3+32H@;?)cTFݦ 8LY$j-A(/vS_IiA)ƀ?]`c5wآBKvgȐ-[BN+k#48:W<i$EŔUv3SΝD+[)+x6W ogPD92,qľ-rX@I0ɨmm.l9H0?cEh8$*#ɔJu5<!d׺]Ȕo +ET :5*YA_s(Faj3@TxyNA? R ڢ ?c~m 1+:"+ )#w#*s}#eR R~w;D(8JS[''ˇ;:t=JIjg’o!@<1~*I:q17܃f]XG6e* +ƳxXQ[_ߛ sF:d,ͨG[4yF7&!xHa:ZCё5Lc#ѡIyJ?*nq~0(<CA7G|P[\L|GO2/*uy]Gxf|KxJ ࣝ~ 2~(wP +mFۉAˆΐyE,-A B~v\D9[>e^dw޽;W8_*pPB^gT婶W5CľbϨ![@^aDh,w ̀yn$}%uKnu +(QCBlF%\g fd7)Gm6*5=_G^io[}cJĞa KP[L]FaE(  c\1 2N ¥`GQr.`>=:NkԙZЌ.͜|'W!>nC +3zp=x֭,=d1ǹ`#v'%diq: w/Yτ!6W#ZԼ*#>vG|G y?\S(#h-ЧZ`:ܼu$n /JS(CGCƥT#[ Drn ǎB^M%- ^]X!/ %`B1 B-ď{^b<2#*ߘ|*8*3j8EDlp驐 d'a'Oă'fҭ2(B R&9dk&#z[J]kWGVd"Q!(4)cA4O4?S*"x莪y8kZ萇!x!rԟ.6$oYj~wԊZ8*L7#ʃ#F- !V*McoS%F*ʂz`KOzG|s#L!]=/NkS1I`*=o19CĹRKcuP\}hh7`3ͨy$V>Ww7(pҋVd?Ug۴wإV we-MR9>\6+}]ۋ-Cd F j4IйyRxk>I H:˚,ҫ|O]L(:c~2$-p fa+P>*h'yUrn$]#̟D5gI1 %ӫwDR:꨷ B #!S)dp}=p|S[=SVXK ;;o~)쿩]gVjAMaUA@w+3v:T8SP%9Quv9ߕ=irxH^rdnPf6daR}e5N!_-a rTU@wNFmHvrc *[󷆜iHJw+SUSS5}}`)_~ux:5}~U42"qj론9>3רTwpӏzz&4^tDkXUg,[Ҍ!uBn9 #84ُFN:\_O?~PQVepsޗ-fҶ=*?7ad+4Yڡܑ]{n{b9 !Ɋө?u0o[C.~#ZeBjXe2 \f)=ø>*Z<} ϵ k@wRڣ׻GuԺ:slͬ\L3X4펵ʏh^VYםGS3T h(IO' )_N䔥7e[N#z\_>?hyq׆\GLt+a)N>?#Ԡvj ZyٟA<2k9NG {+q*u'dE0׈DZK砊rW(ڣ%AaASkQfcٳg53j* lQfI4!`U԰آuS*jsf|􌷔o#.U)buP 6D9| Mfbɉ%_eSk8,8Μc{@ʜHiʨ%'0+}F +abD5->8$P#Ռˡ5Vc AΠ݋́5Eq:aI;#1K#+&گ +(\!MiH $V`+u!WU<Izk>fFL!`6jcÍz\Q[aDDDqrZA5 ] ڮhn]v +J3({!W? ضhd@(]n@y+FE |.^QE.=$l{tn]Ȱ%Zj(pqomzj~2Y0ҾR">ʉ'3̨R5ȸHq^gy23tݝ90G`[ScA8)2D,2W9(BM x?^Ctȭa@p^#*sPG_dGm0xx5}NמiEѸ2sf%Q_ϐ;hfxLNq^y.u+b#'*'%"oD z.""W߬Yuw/Dz+iTosw9!k:{q=N=6Kľ: (#|9)W_aU453Щ}dw%P@=Wc3Ĝ&\Jp z4͵nJb2AR>^y"Vg%~!5,iGМ嚁6,V0{,8\b< 5@C>62CJf at&?CBJ\aO9],M'@ș\ CWWWIJZ8Oi0$\Bɱ)t$I|UXɶ,Bͭv w f.Ʃ "c'@;;Tހ0#(빡!ŬX.7[{#k-MYӐ0nq~&I"tf,4{Sd=HTF[.CCȳa1FerqgzN/?8 .?CQ=ln0ο+ +¹xsDoAa ;H}RqƝ|D:"=dH϶͉fdӋd]h"r/t9>dO4y\礘SVUgCєq]rXg!تh&AH |E*QO}p<+|'zxoXSP'Rϖvb\r? LjH+g<3 +&:,y! +Cpgla8 }k0j*U0~7YM g)C<2TU#d.DNzp>ump:ե3w%=SOl m܊Bu*ͩ` R^R`\`Aܼ[v7S)>z#gYɿƷW>&И:b}/qRzN Jf8KazF4^C4CZ>?r=q޶#ރLŦɐyA>2K+X߀ʇH]tFlڔ7NSXIԷ%lSAxdtm0𰖀ߪ;7}1b2p_ *BȆpSv5:͌'ITʵt/Z w5cXR4Y8gx+y{ lE'Ս`#`Ё*6#R7x?^LU3^z Dٍ=bRGw7oE=Ҹ<1\4]y`$NMg*ǽ#4d_,;w/C?d"-R䟷,T 4Oh ^H9?>]7* +fsHW~?y&b`Ak}XyGT2C׉I+<StGcomr$U8Z,͘L ZKlz^rB5?kvAf)bWd-댙qf39-٘Ο\?5NInr0||gF`B4>gJD|3_}{) U8<)^謟DHjtϘkAZz>UdkHk5θk1u*e3rU`[3Ub0JE7qhd>;m_Vo^ɜ^-bWL'Ul61$=lZsC{HJ-KqJ@s\g=<#Nah<15]\Cs=J "xVOٙQ{省:і-ֹĆ>P"E w,)'qSf8-lb2Fnkfb +q2DCio|M3HH1?1T0M'rj/i 0Gih+dxV.4 C>~\V>ą1t}t's Ww ̌Mo:PEF3} 3KPŭozЩi}6:l65zoog$1r͹M e_Ytu[^0P +k 3##=^;`o~y{Ѳs5m? ;7_FYe4V 8T͟}?q©K1pWr*I9ȘhOBL4)*GRQz>Q*kNh陆w!*\{8~9`*mbznDe@^+kk;{7xEG=+ +n#`wtvԐM&2ɷ>~ 4BUNalUGLs %}!ϻO4.DIX"Ҭu2ԫ:&BpA-_ "._yꐍ uVE-CtȸۀOWEҢ`]PX +1{@Q>Ye,?t;Cʏ1-t[FUj,s {/6{9񊌎V}fDAYW3ik[o/gI]PH[ҹVzwß)݃zyfQ}7 G*3jssFAgs# +MmέapR\S/' RJ锤W`jp +߷@Rmfa~cw::+~*Dqlqib]$~^' +Q좚E;i{9ȠȳjwG;H/ vsz]ήgRGxö`̡Fބ<Cq{=l vq5+U%%X̹^-TWoU`zCGL0)tWԧ +N 2ANhyióx9hC +.ݿ(ZwgulJ huEg%U7vNn#,-zmfH zv@L{dK <b ٻ~fe^GG'D ߈#nwܼ6"ۊJBvs,(SBTzE?{H R]6}g(I^EnaT#ΫdNuiEˎqn(XƗ~ң/0<"P#6H)MRv{!4x|ynX+]Y c i̙ ҤyP>o!FfvNF4tu O6 +)2ɒp}繱5}3\l +( 6ww$x]T"Gw=U 4 kp-͂Y͌hPBUtMҭ7 +E{~1f;DM#KU{ڿ{ơbPicz[U?v4Xr1-y9"J ?:*)ѐaqL8W(rJ$2^c&,8ڊDoTcp("̒W`~[BHUՕJJ#]ɄvFA; >#m;*+_N#y~;B[3K0be@tj\09\XD9/;%zX\EJa3Q\E9DjSc}s+y#Bsrٌ<;˝`=頻H~3s.ܩks*B-oC %#ߺ[l[a)Pi  + _ + +-6GA/AvƕQwI-9<{0` +ʠgwdij32N@9:ο~1Sʐ&8p+}#z֬S6J@tQ}Nu]mq,kpo[7aMY4 ;_V w QW.pSh|O|Y8U4;G4v{LH<1@ +./ì8x,5o["24^Fv @+֘MfAM_{SCryFFI~s̕7cpE͏\Fҡ _*Ъ;|θ -Fĕ '++4Nkpf<}2w>_rҭ&YO|$ eT;&kATZ|888 + k6#)r46Zlc¼f?4i&H.ə/LPnQyqGt_Q_>"}hh;u"8;pR  dM C!c71nG[2POF-Ck0yO# '܆)Aj_D9 E,y~Sl8\1z[ZW =6aBuIfi%'PU20"  (H׶dCF֍-` Ao (b^p$gN@ ԱXB|}`,Q/!+TuF4sT(QmV?9vhXoʌiR +U/$:f^f +pyRf2s<ܡ4<UPaU|,KŒ*LDqsRY{,lvde1' ƞPqS,CTO sX(vD3R,:4E[|T=k!(;\'>YPJmB>{)˖~.2n/# >(rĄk!=db#NX4nv!/lQ^˹%]\5U wa4ŞܪۢE vV 6b.Ȃm++"??"[ݣÊT-UtKqmA׸I[ @xXD?$gCҖ PԃYr=TJ`Or+8BY2A\$Zo:M7N='*'X{9ZWYc#ia >:%uzJ葉æOVj8_`zQi: ^~Isn4^ RJKa{la &ܼhQK3{ַ5B֖$:vEddmwmqu_;`wU/=(V8yp=k~Üp/P +%+Aܚ?µ'[YJtjQ)D# P鐔[D1ruCVJWע@XrVWJM;EwDn`/y>g\%Q\ $θnIm2빯D`<(ٴ/YR +"]F%68_45bD_i-4 * S6hzZAs%Sj6t0Kl; ?˜C.C5:>>2/ ̮>uR]!:b3QG&lG2%ꮥI:cJR _ uq[$JSDIU_aQf;}o/TEgK}?b3a8S^U -S|Ɗ0yYՐ@G{tb#C7 MWS+B +"l,\38nk\4|C*:Ǚ}1aSň/pЃhm d+(T ! ۮ"zQlڂ`a0䥧ߊd)x%QMtGRL@PF8ԣzFD/ԣGs#l[$5dV5 9D.JON}ո}q#NUG +/#F~?3B'HL@r3@ $U+ Ճ %@竩m]ez0ʑŀΨT1=u 2f9Pi]EJ?{ܧN1R` ?_$}Őyк Q;jqѝ{CUm\tqZ*r\L v-yȰ[k*$SM0\Ik;#9*f,5^HVefD< +D^Zw֜f7;"poQ1*;CJ|,r c=}0'Vmڢ9ڢ}D@L ɇrA<ۤzCo`h:b+A&sG@AC!* +1T8c^ GdH`RGY#1KJh W1-X{w0qz_]`6`Zg^6'O&Q`ۅEDp10T8cSI,pK`TI4jj9kڂ}4בS=K0&Y9y6gE,YzǵeBPmX+z/5wl3{4w΂H]`D5 +l&CR±Mf gK:\MjQ*gBDvW Xd肍|CS[ԯ ;9geZ) 1A? ZGuΗNWԯA[%!At#ww<:}#K ,=c5_m$Ry+O//?֠!)d&g"ĤjqµxѰn6C+؝,to ǖC:wRC}[^/_eCu+5+ i;Tqh(g>:H_Hyk>6+!O`T]^}b?prA++h@SHi=EGd%0p/Uz*"51a L|oL6*D\6k 7#GUkVRj/M-Ey#2z-3\8e{cD-ki7<Wf *\t{@;s'_x;/w{FI.X voU^DK LݱiM띊.WE:'+v9 r3AF+ۑayB6w ;Nx̏ߖ4cG{E>BĝsAIph 2LO 7S0C\SNJ/;v\%rNDy4'dzE>>8=}@\SaET(\4OyssR"PLY"(@sǫٗToEW$论ҟNKni6 +ٙt6ZB'oXh0\ep |wg.A8C@ 'o~}CeJ#.!/HvEqskw|M E1b؊(zZ@ӯ (Ig؏ͫJcMR!>U2C;g=*̄@w롤tnc晀 9nZ ͨG8_>1UfJ`k* /(c~uO/2 v4#4 +B{&JC 5t|Z9 NtKe:᭡_2B. +М8鑵NH.Y ׹`t"(\Z^e/a$ `繶oyEIc'*`h\pn$gfXd^w>І%#foOnޜ!_SJY8GAQaٜg: +1rEG_ e7TZӨE(J @-b/(Oe9P%c)TmTgzT5wҞTo 9cP|6ROD&k&uf BC qxBp#Q4cM7d;aaOv$n;#UNE*rlqa)cyTWhr]J?{`+Nr33?N ȕ^D4s5lL]:.2ƅP" rrAЪkZgom_Bq4.( Qӥx,m9.]Oz[ IEL{O v C " Rw~ {zp]L0vJ"2!jyӁ`Β +K:#CfxVM0J%'0n8QЙGju9-KNOfG4:V#!KS'>^|>(wuGI0W&7ҢiiK+:NMC[mxwSg\PA[6~4) \mQW(}s*v!Zr bTxm3xجkw3޺aH:ﵿJ4 DhZG^\]?\m3DV\|.&شb6Q6= ½/\6]; x8U_\w4-So?AKNܠ2cvg̫"99*`kF!+x hQM%\`kJcXd94L? j?,C te3xyQWBHVXVDEA^;ϺL',"m(9c׻`'JA~eAjHtZc%߀#2sqNV<ʏ.řQp# ʂ j{>97+D1LAxP4,Y%akA6S(sCn./g9r + 믒6C48@k-͈VŀvuYd6?czFYoPKJs7BdƤW(X&:S #;] +z)Hh`ĵB=3;"}:2zaag.E۶H|&-Bӿ7%lI&s {nW=l jP@m"GE?Im)7XE&}@HöKs.ڣ[x_p w3/4בihA@3CR#ܯc]K y{L/dp̩񌘃NAs%1A'zŪ0VtALrbZ^W2MvgDq|#ѱ#]Ծao&ݙT%9s[$ȗN)"#U;Le{DGuYGe8UapDGW&㥓\u @Jgԗ&fߛاނؖlӥdÐH * \P%B+vBcP葡\"cݔ6h~ п8u&pzk;&q5ԁ-,Y3T`z G |9Ps At 2#2 !39V#(X?X OTi791sվ acDv*vN\Ÿ0|a(Sk8Dn;SA'gB7۪AJ+8 xDMA^TE((i$gjeQW8W50R;Z@B\u߳"a$M"^.>W0 +I H 5`Wz*1,pՍfiy  1jtmfrť Ed JZigzYjXpP;MJ8Be6Hh6"΋ <WoUni`%W;r W@:HA0ٳnJLqIxh9A5IKs6#97_W`G3' 87EMB#[|!nCdvs@w.d}*w̡*Uȡ ᩯz5ĺcApXk3b$2?EC0!V||uffW$Εhhuz†v!Ov?G^[_2tΦ;#$ Sא-r=czx_ +0Bs +,捯 j8hNGKfflv7MI@Q.a RGCi{üyk\i +X5D^Tk>w]RߠFP͌44u<~ph'섒|0C0+,莡m‘J?AyyV ^y*MJG !Zmx 1e͸C¨*y^p]V>@+`gژO#DؘHl擎[OxY4,s\#V=N5h$>BÅ;ox3#̏C2~FGv0E-+Ny=Kc+;0xrh5OO>Cwri"ZXNssô ~Q{G A>Ghc<>kb=R at .&sS-izRP5JaԺ {2%h-)[~HN;kMblTKʶg_qAY3hAe(,y^R$9z.FtƳ&sSנ@O`0g8Yxc|x14.7>/ϐ˅h]m]۩O?ZC:W{gwt:z;j}P,ټ?v?nΚ|KR8sm:d!jHjE3EA&I4@ӖK9iq4_2/grcb.5 c"_h2"Teȝ-z7\2(&ȾDVb )̪u `ގ 萕[mx}LJ5ɣ-\HjR='\m@EӨAoY'a{ĬUa;0]x ET<9aH]K䉇1g!oW/͠dAA:xV(zڏB`~JfL ×J5Ͱs0ݢz3b.DTuOV +EsL+f^ZA^Q4!~BP(j={oU~j`V`~-*^[04h{/M=% <(95]Bjx{M^6ˤk0\yk|faP^4́|Z]ʙn(Hg`R^7+kr8-Xi:S3LPTmjFOو܄-xn}DA"1"jтgk&]sD9/QV.PL 1ID^)O8f"1yגaEVA5w9? ɘ\G XϽ(ms )빝sϵr>xzmH^2<#~U! p >&#C:4w8h{a32DFǐ3O=7?~-b>3iDK㱥=G<޿EngE=<"MXObyg'ϕ(FPƕT63DY!sG#kh[|֥1HQ,#-iKc${X͏t;Ug+m $S5H^OzF$jDmįyI-"GByR ڍOtrX{k,=bjm)h4(W'y+&ۀd6~.Xק~P#~m3VqxcHAb6-~jYUaJB7aF8W `n}PL"}Qܪ>?''4_B9[ fafH9vz-+3y^D}n-y3b%e- H HgH^u A'ˡGq*@| g;f9 +!%["qO b_=LAY.J_ Jh &-?SGv0#Nj,8 M<#[0ݤH y萖vndR@–,xN#v(ؘ;=yr5,L/Y!m!"ym[s Nkj~k4Ֆr4s'y)X Be4g mHלwmLGLҩE) L<{ےE5=4{W,,;h|;ؚ;!tBrJb0:ݠ+C;R&ܴh$R-aek~haq'4H9%͘~EpD:TxP >6L +?((TC`k˺ls5Et9VR<>3}2lR3p t )H׷Gc*ȩ~`'" 7CȈ2ﻃ yj@dW4(A>2re#Ecn5D?O] :vK'৙fSG32t +\T'_c0[}|3L#E12G "LnL4wYSۑ߹ۺ\Qwt,*i4& Yp ȕ9[p^x|MԖ)OanW$Ϻ [U.JÂU̍vP#=+,];gzXct- '. U[#x7ѯ|۸ˈta)v6=/̓],HD3mnsbxrd噎oU|U4Z@̡͙徃?b$V l.♡PN_}Xl\!c#@4#͛9 |p,'EKy tW|r{.9uBd'Nd7q sNs6Lj1M9NxV` iql)r@ fAM/TBIiW~GN&;}"3)۔'WRS.s+N1h7v0x>yG8vqrB=|/#Y\"Nh1IwC#%M垝|.731w".qT:agFR#3_휑(lp-BnLE7RE3cdo m{yuZM 3Qu4A+#sȑ[{~)f=UQ(]{Sfo?B]Mm4g\T.uHi_^ H|/r:hҸh7fHS#Lw1:x4*nDKSxηAs2L,r͛Ew"qb8#{b~']h{V~zZ70asHC9(w]57}v?hLClJT6LL;]^x\O:7 &g[EoH^@v%:[\IsvjZV4 +uC%BbBc-: b~i~D9@CvH!C<* feuk6#\/늸nMy $d@*@)7lINU1xvZtZw8 '!eDq6k!j;u}߾|O-Zד3baNJ;4=Š{"WG(2Cji]uW4/YF iZ/=9YK@ZKQXhŊx~ESapFh654cĐ!Lۯe~EҞsƏ^ 3MkQH؟Í;;ͥ';v71@λ#f8;C)ݹSEn @5U3Črj6fȉpVZ,R d+])Aًf`V^J(K}ݰE(AWO\t0*>B7P*x +rwq +W'-*sj0iWf&r(J)lWuj]N%4H p,TKdWxG$P%5h%t 3Q`5^ rWWYkW`tfLIN6tPs;=\ATَ}grA)v f"6KV[~nw/"@4@ɐDX냫5hv>oJ(e/XP6XG_FdpH1v{,0/t9yY(07h|!+fMr:䇮,JH~[}l{^/(TV1;zrpϻҼ2{:K+n10;M!0S{l-Gq4)Tnrv SoA1V6@K]F9\!JH8ДWM'/oQqDh]ع\#>Wl͔#;WKpzeqju:ac/%y|hZL]40 W[h9byh=s|j{=xBɽ|+6,+$"r1c8@/3* "ȃ< +~97gW!-3|BՕ *;fؕ LXF-DOO_$@8wz=W CtbnMpZ8L0؜ Έ#i̭)5`Tr0T Kqhv@# +'х;<CHuϼ/in GrYgGyqc$aDP=10Hx0uO[9pZnhhםdgg;szEp +m;Mze |IGy"@Dvdv !iBF3Q4i*7C5/$;$!*PIMp ӞOzͅw+EU"P>Ьzg+s%zԥ'UNb_OulGZ4[v+P)!㙵K!"l?!M! E_{?k_܊HDZ4IDmO]>a2jH0ؙҮ T{'zԐl B +zx|h.n3fbbLʊ܄Ѹ\' i9+?is`F*Ow_ +WkDĚRdBQob?BmBާޗ/κ|vtfJSu/s_Q8|s\^*+=.6GQ!(7H4s&XơFH '==yqڏ|{]Dj>(ߣk(I4i6 Ã<GLqy +OԮ3+3ILk f c7b*)|FA3M+(zp3[aYfzH$ہ>;vm%TOŢRg~x#SHL%jP=k*xXEE#Qݎ3i/{ "Ν + WƵ 4"X#Id1< H/D?Au1)n?>pࡐ9ZrXƹd*Rh]j'pj{/身 8kȑ"HIn[` +a\r{DhH0'jrgF؁MԠfes6Aq M*[lȶs栨zư||'F ܂*CXy#p+ɡ\$->{iE7P,yC"fz[0&7Э6 bΤNh=eHxʕ.Gg -Kmc[ryb.2`4/AoW.slnZ3eck`xhMI +Yz÷JN:qXʣ9 _|y Z[$Je⹝A`kDq{Es׹Bx&;FY辥Z\[4S/zGD)R!r(bu$tS4O]k4 !8W< +XjvN XNd]T7įy{;bPd?u^5#~QOknA# +fC-`^mWw`xs9R ~}+QjʛCWfFOza^GIn{ѻ(=U+%jG}&eoh5[XG"C0q=dIgX{ V߂PťhAp A:^X1TR^B8K^j:M_wa,@QJ&e?տHI3vr i1{(^3}=ipwP.r"dNkǹb>rHd<߃wt;1syM_ߤB<^xeck0դx)ȱHE톩mCM"2D=E:FMz9tuqC5@->i4*i_bg$]lAp 4X8PȚ^шu"'Yyb=9\ɬnOUil/7CGhFj'/ 7O0x>CD${QGy(sÈ"P :1}v'Go AFXz,rkW`ĆP>w01!O{ICvThS+dr|]z[b&9 FauM;hаqbM} ct^EK'LP`V_XcL ļK=X;$h2r V$A):Zs +z䥳% J>55&8|m{ T'i5q~.Җ쌶 % kIZ4IIdF#ɭ꫚[yD# &!́@榣*bu|Ֆ+5t;a``hJ$e|+}i[B2^?D@[˾cKȆ~~GJ Ȼ;*g1h9mlGn]xF;J'=yCw#C Ѫi5(GЪg"NMLOhA={<'3-tv=s@JatwW*ZlA+Q UiI"3EmFĵHdcgh<_&YU +du@e:b 9{EV"˜3vol 沃zi,}&}&TM8RuSwD  ]0ޤG'EHW|hW,o'jRsWHMk%HэЀ\J_1ΊJ1f>Ŀ%og6[z lc@Hhr,WyG6w\bHBYK-+F55LJqK\(׌T 64F۝ʐWDQIHBܒ2˽YS <ꑎ2ROzbS]y̏IJ(wpGΈNpSI׺[ܢ>_sG{a3^ߝ4:k$*g4zsscCU6`~_Diw8^/̏Bo/P-:c7ܭF;bHE>T =$~Xu$EΈȥd2$ + ]hXjr\U>e8"l{7ԌH\NmcU=eU&# 46(0Bŝ)cƿДM Qv^G?3q6,BtqLl <12LGW4S$̖j$r;)Nr<@!;md4{@ofȥO^5%9dF7y&ί(9DIfȢW p)8sGzDCG3 (>6}3t*Yl +^|]vw35٣ՠ-;1,sГ1?k*[[#@MZXL +,泂6F4|ua\A`lvkWq#c.2{"?7ؼ L'ApD$}ˑ H C-#"Wyd…K.I[6Zʼn5vfEnh,Z#RR(?nv.YJA<7^L[Jv=TاaƓڸ&;G$Va{e>Qz([K/ v( LR0ѓQ/FSx?+Pijg7`qj䏠)eL0/#}k/D2JxFTY]%6{WwvEJJFO5/A/t6O3ʖ3JE5H$0%M==_o%[cPiE0pp&s <$:v@^\N[y>*'F VE_l0X)@^\3a P:7S+8)3OuEs/,{#]gO|Vv#Z $|B~gP_~K?/]RՑ~z# Ϝ.KY0[$ m. +ڌ/A=kRë  xsv+.Ev ` Xqo N+1Eez*Y44=ofHΏ$hVN"]Q)Eٹu):\x+­sb_cIC_*.8~⌫tPyHjk쇍dcx(X"`v. {d;붆s#Ck&ɞOQIDh!11vv9ŕ@55ɪ>=bVmvT J{f[NJqoytL!o>"-85FȀ;bs{nrv>6*+gCxj .y}LLؠ#o(30s wT4-87|m< c&E(u}1 芴{m2!6qf^G3{$٦ic!_?S~?:tU~x1rEq$3g gP!JT73H!->qpDD;4o͈x<$sAD^פɷ-Ye/y?"t!nwÊs\Q$HIU>SKi(Ip^*ɝ_Ef©D4c0ľ7ܗЦz GZ_xxЩ%r8HcPr~ ?[=[bu(Om u>q CbA9QR^ޏD'wyz +LX8K~iu?0QR/4dLZ]*,l@YP=c0(,x̂xAA2.+ro%C]!+rQԖi+dK*8%Zu ɷڒ{ Q_I ^%/=| +]T@ܲ='7&*c9oj{:TrQ+0{B9 Ưg#LKw{BZ| f$3;ƴݙI\Jd'1^\UP; ._ +=O+KקgNTٟ|twpwy迋!ڣO?RPH(hS6%zWie; :ҹᕾz,ܰdE(X oB@ $e^8p}c`| Rvݩ̩!9hsFq@ TXz엙2$Ψf I(Jfyħ(^V.|.a*/Y<{m ] X zzTAc\+NԩQJ~7F.R|iH%Tg`Ή>G!W`lT*al|<oSjuXw~""z邾IOǾsWCFDhr崼O OE?WPsq硴d,^1/|_rbݚU圻"l~CeEgՕoY|u^ocHҁ 0_Š.ش[7t׺- IJxR"ry֧\OrD@vC9n1ϕ#"K)='+*axM<2!#lɏY3S'Idm<&d+!\*1=b1# "4q]Yu$94O4ZzBzgڼ/UwPhb/Lkcx ~ +XbX<`j 9䵜:;~vkiz$ 1Tg\m/Lz7$&֞nV<%W ,zM/PgЕ`F)PaCna'&̅\ +> nJ wp1 pDӓ<Ƀ}lۓ7H U-- `H8q{u/Ncid9-BĺI8"e]g+Rټ8\()P]C|,^Wn؆`=X^~'rE;g|MKCvDX;;c܅UgMG.(A3(Y,ee.rDBvT"]Ie ծ>H;"~Aϖ_/p U3Q9w+S?3=oHa :|2Ռ:!] 2qZD\?@EgV /B&gQ,{elJ3WTGm;LT!O& 8"43~BPr# "Lej,Auo+|<*yw6PZ"!?/uyڹ.LwwȲy$Svb't"7mdL㑷*j|LF"H8՜=G) <9)zt{1CnO0w)/}$;LL v[TW ϦkBe1ЬVmo*G;Ry-zEE2KuA(Ǻ9ºR ^;C8D*(^^_S:Ug.OeN5M$&Io]*$|B9zq`G + YО9wQ*-CH0MǠ qy{wi,2B=j6RJ(ْ^FQKEQi-꯳cSw +pOlD.qc4}1{1Vdyt$_?3W3Nix0GPDK`})Ր”U&nsCWF[Q}'FJ>UG[]ؘul"-zŜpn3̒"Fjj)A~RwC`&/tUSO J`Oݘ:8lI]RۃEtBetuWWHTA(0 i +o0n1(Lmob)GLnz骟2˵/C@uky\ŃU* +YQ5Wkkm;q}ff?:΃8,ؓk˔S=;!6( +p{eix-ɞ1 !< +ylU}3ُԻVo ˃"XC8CpMCH:0H8 $DSvR߯BLoZ?HX2HbN\0 q*:ή ѵWlHj5(\47⤠QA8jɐ Ɩ,;0ꑊ´e2os`# jk %TСx$%e'*ŀ@U.[ I *CPI<@R'"u +:p*#`_s)pUpM=']y&Cu)#;Z Ѫ_ٻ|[XtY궻 D~Rٍ2^Q +8ˎ%giHrVn,pdϵWp.X9֕"JՓNFH +4N߬O>U)^RUMUo7gDd0 }I))ivٴ|/Ede~jO&߬e XD@jkOG~Ge|;RwG^gIAb b'Q"{sBI{,{wU?Cd79_6{5MbNoGu҇Xz񖃿⡛_i{ n oCb]в,K$3x%J^ܟIH쪮T` ]?No*8,'t<7T{ͱOKi S,)~$39&gCa>~1zF`KB +(@D\(-!O+b.MFP'Dxl~u3(ŲX/N0p=D.5vTj +MbmTjc ҼS}=AYE8BTW /ݥJnR'HYͲ`{ k<>R w9ל!+ksf^bdMݹ]#A (llkbv%=S:agKBe4Z&sϢ0suPǐ#׺*Z0B16Jp95qG`QGӻmTLb͝VIzNfQ= 1ٵ"B@H( jނS?#=G)+)E[_G zȕN35ʲN48K3X'7^Hs61d:?;9'B(Ƨg +GQZD'q6ްg + ײh( ً^ +FL=rȃ5-Nݰ!=rV"{fĿGt4+r]ҢGU)܃'FpF+[]CfKr&c +pSrǦ/`V-G3S&]XX{!>DW1HkHy `]{3eҰ6&|O#b|+t=G)I#@\7NաSwr;eg[[VP.<vTl;pRnPCj|RW]9KaZȶnE%+հ c?s%d#_2NMb-{&&WBu] Yf] +ίm5C% &!\Yf́"6|R_;l. 8Sq^!9ke+R.?+p%ܣ>(C3'oWzpU%h/,|1?3i>R>@Dqe.RCM]rf)۝չ+ 0м9J?}7ic +1B((&!OD07RLߙ t⨚|Ԑ'2iU/KjqDrwδ mPL>¸9p3m1LdY5; X])#Lڥ.EiU{g??BP){\D^j' "+o+SjX S?F5%;i,Λc"C$  F*T47DQɰ'dV.э>}+ ^U43F*tK7km;l_lx;p k>T>2c(*ũՠ+b[p*q#6\Q8US'GyGB_f})X'_fOY4o;}ݦ on2d| (@j{2cJTϫh%ZDbUGmT{5Gݾv^c0-Y &Ժ;h9sѢ{y|={ʜ?È49 IlRRd#Rip[zJ? @#Oj>򗂂Kž4>{ VP8" T|qν@k<;)G_`G?EHZT*E"`7BNsmzȆ=竘;׷(;5œ9vmumELv#= r!E2#˂]}?m-l'z`;UP(إՆ򧒖MCs+9tÍu i{2ץTO ӥi*\!RJ̫8׃F:Z +tXmM +鮷@N}KԅA0`#EdW)P &Έ2mBbbe׎i{v$N4a +l&̘!u[ HGp+ 'Q;Ys|1i?aKv_~f5obAjW 9 8 B-k=+H> +*FI̐*m{);cBh>m0\i;B6|ǂdב$cyް'M/5HliRGc-9.F 2U= 2ύ +~딏g ЗRuj ^;'lSNRD@4Z(|RPc05v婹v bws%6st0N`SKu=; uL@gm_Qt'ߓbda~j[%w<ശ .e"~wOc n,nI E.jne㊑T!89ОwNWUL0U +._ H0Cͷ`l՟{޹Kt|1Re}>d &!ysKWF9?''Ⲇd.ԑ.q2ۑ)~/-]RdžGlVW|Оp8RM}qcm;Sy?eGG:DIV-yXC w(0ϖ>hd RcwVs1_zt)Ny`ukUp e9u¥ͻh?o=ZȠHQ(Y"tF<>ઞXLmE"u8섭r!Al8ޢXՙ ˩ |TMEo+t%Lbjg9:|s +kY%{a +g[^0)>}}嫔BґZ_Z2s~)S!{m auץ`R5?Ygedv@ZzښSZ{ÌxLypyx9Qe-  gvy=euث\'2pס_^ +4jV<~p(r©'F9,"z-UWCGЖ0wgk*P? -;ǟٗ)T)M!@VƷ"wGI=:Y{#6׽(k N'?K;j> ߗӭrYX62\%քb>Ouo^~hqB?yHZ=Uz{U﹦hS!^QN:8c4o^OK8_(>,_}_k-jeBŋ֕ݪL ya鑾uSGNjK*1ڏ~JH[@_y6i!7+ ی nhoʅ:pC`/s,+xޤg$?xL|agNy0eM +rY3>2RFBk6+брV.r͌y)bP)'S + ܝXc[A' RQHinh}&O}xLGR8.@a*xJ$c{̘;GGF oVE+'b0I86&ҹҖATR?Wp@סO.ΡMvNK,S5ur_:p DUUv&ͮ"XomPJ|wc7%?Zosx5fR0seOy[eC 뛛0(ɴ9ě +|mU!S U^Ԍs,=[=ߙ_1UPA?J<[Xpd;];:MNtC$7஄-CI1iLݐF1PĨĹEvf~lF,!u| i.}QeFH5^R׼='L`JXK"զWX4g2KU/˥deXqW'#j7zB27 #<{@fvGv!K!^OHV{Hh̜eO8؃_yusHZGiB )۴_x)OI7O,"|iT6u_JB{N".]]i;_w@/v:)Aʦ_[X-M /ғRmE1WKÈEʼn|4xig0*`ncF֯ ®ITyL0܌sr+$A eF?4펥KSfV39zՏ2gz.hrI"Z9DѫEN˼y_&$sc'g;('~oؐjW|CYC9:S堉?ZT%#9ܥ~8/A4h*~XhM击20Ekx1;Vgnw.x8'<ᚃg{u1'ڌ { ii~ 7q&y~vha|GftR:`0ЊW6"9S4?!#wUƑ +,qh Oug?RbnC=t:RfwhHjX?e 5s FlP&Mا3xG~fd-lJ/*6t3{C<(V (ظңTw)z03?D@3VPpSy!'Hٝj_wHEQKfa$"n̖" . c? (㌡SMGLVi~ƣpT`p>j7z3zLBϞ!oam~)*Rn94 _w*&v35DC0A IĀ +=uP9_+W"jF󆋀w<\ 1%#oX?95K'"~GvFբ1oa]B3;P8D_ rPԬيbz/}W[;'||٠2G8/'gFʈ \i(2[3fvtAEbS q֓Xv8|-):I'Ғ!w{Ryz|)/; 9[kO!SvgdI}׿ + GHt0 RTlZN-ȴΦ!rQL܏`5X "۬S3|+ d`G^y !3"[};4Q'm1HN혢WMMmf:N+՜ ?ϾqC3:zyCG}ߙ/R9Ehl>&+X׉0YxV(`dCljrm2b"nVu;r=&Jp{|uP:ӝ18r&Ѣ1'\Ep?%@FgHaKR0JAYHrzKrQV N9|?38C)Xcr0Fy’.JntF¡9IzcvKn&PldQ}a"(!wFwE.mG~Gtt>‘C +,=^W j_{RZRЂ2;ֹ(ΝWhu\uYw̳jAsxN@k/z.EN8I,?sH eIuDӉf# v@ZtF{:O<!̋?Z$G'E t8wa=ƭߓ#Fճ '3I5IGޙyMN4Q<.b6@ASlxфS&!y)%,Ş80`/$=TS8p^ dr'~%l.S$}rMLC( WFIK4@\JzѾk%mh(y(f'_k~˗nrī[w*-KU4de7Y&1`oo.iXl5* 1`R|rWNCV/89=q6|l,-IiCL-;OtX0f_V\&ͱ1!ˡy1K #0r|^0[7ކg`Y,>Xh,_\l',5%˃?y֜˾IfqpV|6z~V F!VPj0λ>I6O O t[:%DUId4ۯlIkb>X_ p%".H;3>jtm݉L%#i5bD 4m`Q$(UB%ɕ9)*@({#!Tc;YB/۫TJ6g(@iЪX5wDgiiD9wǀxp)]F4"$xܤc9*"5mE_Har@Wl%4S +Taݙ1wo֒=7\AϿs\5/vqz +Ӟ">@G3)y'q9OoOyBW~M_kf|fӲ|Ku/ꑉXgmf]392lf[*!wjWߞG*ˑQPy-Gh1iH/3ޜ*](@99XMo a׳Ȗ>!-.d_;};U:iĠVdlβ Ak;ku#T%fmqpѱ!gĄH8R d,4$JLI +Wxq÷ktD}="u>1w, yHReEz^̯[ԚX~R/hR>JwIB&bG+-?%&ᙍ"5PiTrsQףېNș.pLЀ+J{gq sтUJh 6M.ay8N,|ejS+Imaw1{Sz D'>G PAvƸ[׌q񦥦yy8& +V qL ֤AW/䄺.W%[^Sֈѳ2h*0B{Ϙj®*w*߶Pm"v?ӃdO(wGv=<6e΂ 'ߝau{:@H"au+ jT;ќ^Cb~ȝ3b#Y*ɕnmic\hQGNnrsuZA+%Ęѹe]&m&XDO$30g B> +LB)TGZ/5dgB,|G[ppΏ KQ J+'Y_Jpliw`L[L ,€̝@bt6r& ;0aFPUy6=}ʀ(ض͘b pHC 0!gk{Ǩe8?ԯ(gll7ש/ 4]p9m`6E#OmH !n*hTM|?1W52HQhnj"\N6 &# +ʈ6 +R:#[ 1wYm RRI h{).8ӬV/ E'wmb\ RdyRt~q8 +%Ÿ8u!o86s'`٤^ F +*ghSy/:,~&̝cr^$ˡe{D-#>1+U_xG2A{ +L>qKq>t9: +Fjeİ*[߸ɠ4Jv Fxϩ4/;A=B۟Jov}{q-}7}Xd~HmA`6! +8VP}јz^A1 bў2yJ vH_Ω?{T)f[a֢@g& $ϵF*n&аiᕢ- "Gas(Hnߋ@);~sT=ڔJ' +pu/kd|U?µs(zlq}hE, o,HD2eEXL_ ݎ{m)F)YRBH\pVy+^d^пpyb/#U2Yڄ5\PaIh<)ο[#Jz?G?C-wqҘ2UfnxU|p:sxgmת*H +ن_DYBn:'s. R2d%ݠBS~aFzPtHp ڬʰ@dI;\os +Y'mY(R3E3ꁞ\W)k[U>ndIt | R7/oqul]ԠB/XlKWI @yK=]O(TV-9 PDdnVOȡ s/oϖ A6Bj܏BrbPUC9Ql\>J7TGB,ݣaåN~^qj.7k>%ǝ]B1kn1Z!p%njP(sER߬g@oΧsB7e0"=D +D艜֟BYǟywZDM13X BWR-$BLc_k![rY3`+g2"1Zϛ%_a@yJe(dHk e=޴;&"MuúlsJ`}AYC@GGW R-L /Ђ Ć@gW>cDC*oh<"ۙMH&΃4pX|&Ur ĘJ$CboFzy{dͪhtey)tbapRǾHN3o-B4ѿ(VP1gR2؉;DxH/q_gmمY0 H)qq#mu^]$6k VO% -&p.?oQAA#%ĊTV䍲B^`+'2S_3([5Ƕ9e F:S{ET͐R5C6ɅbiIօwR4; QśA|"O0KypOCv!{h!"0p'x)B(OJ9}+˦("gUZE/3VQki{5*nL/[l+PG4?ŜÜQ?:hy8-LrXF^dJkO0zr)]V +EdYV.>ȉR,#]qXvȬG:U-%[88m.׀oFkzM97YN%x 91{'/rnUgst'y;t㫎ķQ:2 $y{-i&sm/N?MC`k{K솆;_,M V !2}DejU_)vPm{]J=gn˟J/HͲ!$̕XK%S0u9paѦMmt7Y,iQvMA#:#HR$.u=cL[UIS?D\R[<ܨI7hvZkf:j@_aEҜN~{XߏZpIjI)^>1JdcΥ̊ 2_|tTufGH]D_*#7tf b!DiXqJO!@,$Z<A`J`y{KQҠ-[խبzxFG# C"_^;qi +jt֭='׺0_HI]9: +pB)H]Y3P3992NW*"bm̕F'ԽI<0#AcNwEr 7g^,PHǾ27[aK8ҟc31*GeN؞iI`@ږEדoy$ƾt(Wt4)$T+nB!G 0ͩc)%7u^+:Ԣ_\x +t0y9u/g'#\d/ʍEvE&.&FTq|f-U@o%;'[ݛOl%--uOY;y GMޫ.: ~.Dvtj[P t,vVmrh%~2 + հX?xB҆62(d0h) EsߔH2Wb|Ԟֈ +UŨ#*%fR5 #8.2=(3s㗷 +]d)qp05'HiW7j:l`ņ$ŦKr6g}> .W sP004ܩUm?!Y:Bx+g^m]eҠ8" !mƢqoi$s]a 'e׈<._lEK0lY|{Rg%xhgC9HcKqUnTRT]2c9IC%QZXPKyhn)Ƃ[ +fm=]'L-,֖`w:TVY*Y$G-BF.r9ԼQݏNs{?zA')sA(ug._i6:*lE]/Ms%=!'?E(s!!Qk{xq),b+'_po#O|`{yYCZ_)z e) ^+*t$PJz~,{& +3-}5iA:F~ѵVHD^omu4PȈÂc_o+qFrD endstream endobj 29 0 obj <>stream +4 JP7 (Evp.l:B3}Z-*żh( 6620o՛rSE,t GURX +Ohi,zznXSءZ":#yqϺ^F4WnaLW|6Ncr|brXnع  B+zս]ڢ+[_[V5[žQ! ,E@?mV:񑠢 (8/7 '!bK +?d u^:6vscӄINH2VQLKJtCٗ -3!^@'mq X¾7_>j.֥%o]=\5cT*K54/ G,D*O]c;vdbL4\Dxdc?E%~yTҎ(yMhUAWՅjbrEeI7A+Demg, K^ۙpEZL㬽mHncA~G Eqm u:jUMfԈXo,MBV4WB.f_*gztx}*־HJb6ʈQҭUx߱am;׬??}7~J#fQYw'$1 t1+k/1$QVa2K MܢvIl<*jxvF5Gb*#w JúTNdt5HQ/bu@:l(Vv,څ5=v(-*蒿KwWc>QXךm1Bc30ܫ֦ $BT~] HıDH^j_ >i n>{ RwfJk@+%J@wo$O\w+wјF}Nz=E0G-fxHk|Ohzp,g~+Z9!K-Tm9Wq4~d,qy f E,Sͻ@7`WN?r2 .C)t$5 P'B6qd"U5K}'& +(NI3!< |}-< Ɠ3wVD!!v5=-]#pPDΡ:0MICȜ 3gq\qC[{";^Q\%I&2Qnv g?>HW)h!Oz;l9e~ނ$ +]>kб~d3|Za?Q#q DjRsHא}R`T;$D:&Ԡtt̋l'{1 oSq#Ny @5by~0ܞѵqZh ;'CNubDaW'+pH̜#QCH?| E:5kC8 Y%i-<Cp4B5?wP&yא(pa\:S>n,ϣÈW3J3 s:Og| SLʕ3Q +TgR7yJ ֹ AoK1sӉ>{KG\6_βX!\ +7jkhYW'vYFK*!aCLɈM.pZ2]q XE| ْ{}M58!pr}I3T\SNZs? wpi3nu1>8CKB:_IiRaۇH}aK+GB&)vt%G[ Jɱ%Ơ0g\+GXC7L;"H)Μn~#fi1!@'w& e27֚񝧃v?VzMp^} nFsa =㪾>q A4f@apͳ4'fqQ6?Lc*P羃5$-KȻrh6VӜLϏ38ԆD[ZiƸ(Y60JK# hb=-ՓBb>F׮a;*~"D7/doe|D٢WA3Vbi:9&wp؃ IyJ1g=7r26v9hc_3bZsY a]#F;q҄d_%/j8\.?,Ba2$&x.zKx̱HЬȥ"`xk-Ko~Ϟů\GVAg~$svOCM^d 1sԀ>\F.лvIiddI8?fJW= y:A

9 @tF3GZUǞEz'MIPpnKћ }^_EgV5a$=D#rՄ"rTFet>RSA^0XC &–lX9rR&(꾪\,ŜcsR\Q&Ð \LE;6‹)l!x& b`=߯hL4SP<6#.<?iGGL1vUeV0OwUNw—c lP8EUBe:LjTӡ!-k%v>nH ÑYH0{"rAz1W ڇ +!pq5y3hy1GԻ±qc2v> qUHcF})8fH *uB=t @=7^#ȆvvdN/zm`s-QR5.Jk$1^D/ }p}.Z4x|4SGzL`i12#mZ3[ =#TֹC#5K"kcNj ગ%XCT<p'!J = tݬa͏1dMChF9ں5_!=y\֞*$Kkn9c.uaw-zPu}O U=rWnVC`^=j`{jYdWBM+9A0DX˱9QjH\|a?ڊ@DaH8KZA#%X9ǽ*pU,=GoU=~[TW|Hzji4yaFlĚ T%> (fp/`VpT8Բ=TUu`zR_8ԯ?R!{>V|RhU+yg bEjf󝿟ץlQ$dD/FY!jUr^d0,57| {brl}J,Ui4lt'=wVo(؎TPO%^G#nUAa\*7>!F}؟{!x>KC(ڴ!㵰 )@ߔQ{M@muYA?r:F{KF+n#xiri,YrknGdH0<bMP rS*]Cu Gz`fpQ42L};'_)01Ugө/odJHm4KHGP5*+="e@l^4*}w?Yȩli{>',@*Ȣ#(#}{tgV6:-d+Qwn1xNbtyBAǾXEIafD%!(â-*Mj:0Y(S(k0+{"o _~&ѐ[d +spV?g*JqHL+TU:3JToe98rX϶PA#{E2IM(ӗ|U2(\\syzNMSr.R0 H4׃%kI*o|w{G٣%3V)985f IqFqAt +ľkbkEEĈ.xA:נ#'D[=k\jӌx^A=* I2Vp=>aզhx|UU/Aw2Xg +b89Ưv2Nd.p,ő</5!\53w2bZFJ`@l=.8Ϧ1gtkQNUƘ&B[͉VII8Ena2 =mU6羭Aϐ38; "l]g9vO%h?J^p`3=A I^ĬRyYoyҬ{"V7"fs[朥[R!TL&"G񏲧󮰶`TLz]=JۮpXLP31`~O7 Q 2i] QCx`?]mx6>jBFdƍZ ޗ+O;[2ۖӨq5Ų"+#E˿{bJHmPZrd]W+֓l`L7<Nڳjsv|ɉl=}5jxJzX9lGAZZ.<D +ﴈFrbwDr.tV xKI7{cE7?oQr"a.eSϜ3<%IO'd~ꦰ49L O5vfs%`⾱2h|c/7awc_]4BRCTd^V1Xyg52|V`h`0a ;G_It| 5V>KF49MX!rἣm|a5Nz\;y&F K6sWaF9/hfz mk"8zGzrJ)2q؎Bgeu?Z[S+߉7k٘7W#ጤ~ 6/?ETJ)aQgm;TZd3J~c*7r A4҈ 9gW;5{lx> iq, ::SKzʮ${/WS:žO =s +;kȎLFsW rL@&Ԣ;8qնI2з}+[][DI]n\sZm~%J>'H>$L3UsߙsVl׷ڸXgcg]&0 2J" +oS'&oNh.DVLU +_(iUzδ7if]f^6A͎ew\Gp/bЉQl Ck6b=gϓ"][I|ww#^JhU +2WIASN +s:|sJSlW;zo"!CR{$~&f|yիtsY jsv\VgH;˱K\05䎥EK|;%VQx V&GHE^Bu6;#wERUzUGXy\mQM)Y"9Fy(6:ߔ.xُz1W5R|W]7VOLO =AsR +W67bvmpv +<·.L^E}1<+Cޜ0EUw]u`08yDb`0ݿ\2)6lҵTLx}qFar;1rJZvE&8RN6Vw3O[I]:;xt1:`w{Z</-({+\#ҤE?ch`ϫtD\$1ReÜB'Wݡܠ +QmX,tʹt-xa!۝?*'<Rܪlpx%]E†?0c >T 95!VzT2Bl/4ڂ0cd2 ?{ pWF1F}XX:n.Hty<'=NmpixjpcQ~TbJOqA;d:W۱=*(YdRQʊA-Xwi~Ir)R~]l{3eذK:JPR!ǶW jNE8k"7FM[DqT֐&8^N떱vPr-W8oMxv@C?| 3s2x_2;*fZͬ xHvU5gUEr?lQU;iE7G02m8^ adzyBOtc7-Gq$]@sO@USxo1Dbcb%83jd٥ٞu7#MF1NpPnpA $Qʄ plf@#O^>_s _ 5r'3A<5'ںUAe֊ӊI~.>[Yt?tt}T.4\"]=Sֈn@3~,5C?q]Yӑp.Jlu j.j={B΋}^A!nv*7gswQCR_=2,s/|{1UXm zQRDiwa^il'  ܾ!yA +^sᾒN{#;r2T=R7J9MJ5'VimP 묶3I0Q=Nw}0)M־>/'2)M+/@@gzGZxP"E yD4GUb9j5^kz(fL^Hf)Tʾӫ֮e`[ *k j} KCJx*$ lߚG%4'vȐ̊ ~Mwj5x T + $  zrƦtw fM\cM:dU (9Gʑ}=^&`=+PFb.K^W><*mR}YvO~ HX o=]M|0Ug#W?WkhF-$bG%U7?@ͶO۩p'ŽE0| +AF܃m-[=jȈ,MCCyQ3q;S,ƞ x}l`<kڳG$K>enjtT zA$I{ %lr<2 G'bOT CNF@U4@͡؃KÆ)9KPȈr Zu)ZDF y +H0inrP g_P(R>Ps9/=|;y j*r`md%-&zQDh]_T vTr9毤{X{3> z:v'BP BB G BZ59RGWh  ASW=_q!1h.poOss>Lj{Y،PMJ8AeIܯ?0t"0yU8La,ހOM=2>ޘy VK9YIzZ i~0{3/_`Z!Ū8%JVAhRٝ42/&.=b57iKgU=kߌ5(G&X:dbL2:.-}ʺ9ޥOB57̿{YR XW4jlN*Cny.a6g=;UI)q)'~W4&Om"Fd<Vȟ'QxXAν!/]. rq];`*&"Vj1&) Qh"1Ҫs~pFhptG +W{e|)fq1h>@ţn@W҂/D=7, +r/py |G#Ѡ&kfϞ>hJsU U_9$kCV5@ѐ֭DiT*`kɚx1xoz-D/_`(n]*P +dوz|^z[qy.t+=Z |8mT?c9 zpգbىUz+C*Z~Ԝd%b.(U|rɅHŎWwHٶh uPɺո%T/y+փiS넖K]AA߃)U&#?VJwI^J (S="q|3^lЌt0&cWޟ&V!:$q@Qud}a//eإ,9- +@cj3Z̽WkھwoEiiv\yO2$&]z]je,nZϷ/q99zrd +GJ7Cðёx۔gr''[[*{z"uwdn> 8uא^oiD ]7U۽^閘KoG%aZ(_jϞM,r(y7cS7laZ'/ `<4i;LW1Lb𾫄'DK{U2.ūzQPmrVSJk$*b\V% ڋ0XT3 ,^_u'h3 C /d||}HaA$%zt|ML&Q?"w7{iFQT*$u3*#W[{zSbtXA#lDM&Z{ZXKNҥxcV~Kj*NQÀNrx^ogaY?* +ܒ@;@YWh"!2C\C4_J1|`*F(o+$tFx9mj{]=r ЅtREܞ>܊7J?zQDn{'}7"Т'œ5_Fw\Jqt,~'vA:)y!,26/YC6P25&K=s)/n #:e3"'!7#y˘J#Z% {ۖ3 84%?S~.z71L~\Ho ^%q<朹~UrP<2ş5> +EܒQ1U5ɾ0GެiB`:@fpOxmzS]U-,P7-^6AxO6;nPr)(T2ϱ~"K@)Eh>QeU#V^~e ! ݟGc :O") AXr+oՁ?Oi^uooPbYv9EGWms~Wb$;i?(Eqۯ`>Kcg#WJM>p]G 6YhVC#I9hU2ntYA,yVukf4 Q(_R3Du#f +kY a!툎o=ev%`vF\jmY2zpJ|걊\uըJ𮩹tD[l黰Ze5,+ji #$,ٮ@ٮ$!';&}9\M*`>jԯ%WRU) +Bi]uf;ۍ;,SLP lѢZ~)1Aߒ +fABM]4p qFU10v#q)D>_7?3N~m9@b۸78 &$@5w[ݶD |JY \ܹR@IQ@gǬo ַ>V?lpkOAҡ +Wv:E ƏC)s]~.1\ɚyC-_}%`p`EH\[ZO eDiТDW2ANtWK +@hےЉW?@C+` )О{Y Hs!=s +ϗ*= 8C +8wGc<8((zB󡎔կFǐ'GP#3r|YuoH9h ru9rZR\͈tRi8"HtABw!O`J);$9sU :Ι3g=_+n>5%oa +tz<z) ]aBE RvbՉ0ߐ-܃!Dtje~K#J*@]LCfvCԽ},b.R ƨ`C4tXKTC^ VYwBR/ 3CG iYC:#NZm=njk<%ȕ СQC9ś$ӐyHc_BRF0REW44%.?'n3F9r%D|}7X<@a.ԻO\j2&1_I93RR2.. g +pW}rGt쀭)%cPOb-GxtFٟ,dl\n$x3x8֮p 5ؗC@aqǏxvS2GlK,~j(\{3tR~u.Af^&p%Z+41`؎ +vOACpw:\3$X=%3܍g?%7G-"?47!T-37qln@7r("K5b5#߷YX3hy+u s +!_A;2?LC3G'x;`ڶE0VQsX~3We k)VQ/J$]ҦyJh_ZHӀlHv߿"oU0R#.rj ~{4y`e+jHS;y:w:\VFU( H{XrO=בm;W +A5g{36=9ʷ[drCScU#9Ý Wѧ7FŕeTS5'*VR;(a^t +{]0QN~^yT:H|jCd@T "$о^.jaWyOEm +/;y(L/JNvLʊVʫBIX?DD|'ř*䧜. W_,Cqbm܋'@٪mmymu Ч;C.rLc.;:U֣n!e a& z%1O]hۛD3ʦf&*xpC!E7>gƀXz:ʮ{-q#ۏlOttb@Ȃ;qTӅ2Z"M;/t`Z-NGJvB BKNܰo_v5ҢZzOR;ٳ+n̝sgwPyޘ/!jHتCYiAB18p#; +#J+AjFc9Yڴz7r*PS$R%jz)*k +=-1_Jw*ﲗ|Z޽gZplJ +Y^sUGJIl{Tb$$IJ;="*RyB+GiJ(32)Sgב̟<-T$z ῎߶H Ҷ_T&`[Te>d"Ը,<ɚsT%"I쀠V?%ҕՍO>+B-<I k%&dA =u %By{χ_#'t8>ߢ0IǪl?4y\,]7Os~"#[H'L+zQ싷]bwSﱿ#ܠ%覂I'<?#E4,u/v MVttzTy#^sU0` +H猾輠J4KJ =̝LXoo;橎:GI`KƹiE~Z"#Lmvl=Q*-|r*֙%VvleډyDV:&i%X\!ƍF`PXR1ػa=|>_ @^MRzHXU #Svby 9H:.c5P/uw{?0Rmp@ojqUöᎱdI+قj6E$kw-P?ôw'T-G>s}U782`LWI2y{!kMegK&W%*ޠߵ`6v.x€Y:b̷Vs- ěEN[5=5X@ +)x\ږra *Ą?fXvY_^ Wtf)֮OrlrYKoC:kr_8h‹٧J׫zJ[SQg/$ 91!F%=2qPe&ChUzG䂴84+]μ\~B3;ˆv hYaLua~Ԥ١Bʬl7+*M/7b>`}oV,;2>ʈ)8sMzfcMKK'c"{>c[oQҫdXxnb=~zRKF=Y1!PE&QdyБ!Ϭb>q>>|&eki1mzHWJN;|{&qM.&wN?!#dX#r+nzf"Q2AZ<[^Nx,Ƣ_Hnr 0_bb*vmAq^*ye[SG[snK2/b) }._Vv/zju6YdR$ޞ尾DUsgZ긭l+6'qMD6Vx"Q`,kru] ҝ$?%rPwYb{GK +t"MyF3jߙk1]_"?Ic96A|)Xd;+k)jcVM#AYp2%a؈ϾSRq~ ԊR[|+O"¦pBnJ_%*L +X*kpG9f"m_,hL_eH#>FRݾ}"፩ӯ̘\/; %vKO1 0%R -77yWH4Aw*?̂Z[/`:,Y7Sh%6G)sS\kLtt#4W[~%GK|gor$x џ k8Q1}߮B-q^Ph ߙ FP׃۲HxJD,|ƶ(1jN,i__SZQ* DYa~-J7+!@"'mϼd;Sfě¹"O[lk}U'=@xyn؏0ߟ$)I&<@BG5P.$I2qi~/wl8_tTkx,]QjKw!umq+?3.!sp<:H +濏b(nixvقKc5?zdXq$Xug|MC%}[la2BYN )W_i},YY7q\`sJxڢ!42s,b4WߦU KN%v~Խ'.v 6{uFcIJ/N ӜZWXȑX[^sEZoB<^-oدR(o^HTv<.%O)~L2t +Ϳ*z$AAE| (;)m2!qY{_ĭ p qvT pYu&+ 8CJPIKIgZnpS ~?qamIϱXygOj!1;qٶ_6Ojxz:wŶB Z4@ci@gAT%iDYI<8bS~飾WjL7X:vƚ#)eUZoR\L]Hz%'~^P^aX6R cq@(h$"4{ kοc<[w7V=֒ink! +޽`ECdǰ{Tʡ;FJ`\Rp)_mfe)p8 o_ p/Up0Ce2gj<9ƾ+q%s==U$ iB_xUg Vp4-2wIh5Λ܏A/㨏B۔հ;p{LDR;hoLYRyѯF0M;ә6z\<w,! +{D~W {DMHQ;(ٻ=.2hk.A`[w 8tJ撎 >>sΓ91룐Y}]d#NMD )K˹3"mvi;HP\z2ٍ}_/'5 G0,Oik2'*@\d(q_8;_~CXuB3Rz2pF2;Ўoy(lF4!+= +v6mMJj߇ ,NotGB,?/ltˀO+[_5ݨC4e~M1K{f/3868neэkCw>QO:ƪ|նPpK^}6A_JOcimYA*3B`]C> ,b s3fJ^˘&y 8J܉33.-Gz.jS9 yo :" n[W.y48? D"R1ˋ$z + eK# H3up"n3?BQ.j}?8TiNBެGhPB8ok eg}~`d[l2xRfQ4 7DTvfA,3me#(3.y 1T/kю}/;;"DHG]C }Yś')۶'P:aM)qY;ڈ#<@[v+ҾQf([dgE5꒨qjv`|u9Q`&GW +wouga-npw2Ou_Y)lBK*'֟=r o8Y=nΪ%gg^2NȴS% O3[9ݐiޤHidc!3iy"JjU࿮wYQk ńgcO8:Q>x#°fMwz=P`ll>k!=ͮH2q4:~̐5kl0A:=ˎf/xP_۶]g@\`9̼!R88Vg&czҮ,+ec?y(Y'ISn8TC[%{D|p>yjx},~.]Y+HQw!#(-sFL\b<6#<)KarGi>"]H(/XcHMYYFYLuF6}2Vz'kDh wY>lf$U># ȱY~s|6bL(Z gm 1l4IN]8tFau>䱮mJ~SۻW}1ҥw☥ov-rQvMN.)%.Q6A,$ְ׫L*6$? +5Hkˈy ZőKX_x擼kvg-uBu,0P?s\IƂ=9Mܺ}p(51o'ą8-\ZՌ. we6a5gL^GTWj^DrFJ6AKr*Ѿ ,)\j:'@Q?w%QE}"6~Rm$|-S|qvcۑ"EACowǘP,'6To S[yhڛ=E)E YCdwvc8OPSf{dS89P)2 lb]Q:;g2xyqU]`"u0GvǝTkfxD)nDW^g{58>ؙMn.9z8떎Z66]QFҘO߈']_/|E~[G%c]v'/}Ԇ^/%IOjw̃^kC EO]+FL[xƺX/us}R -fC)x"*:qXG-8aYF-}3k1Þ +x3srMEi-U>#o-"/6Oa/s`?+ZYxrV*uŃ'ۋn`"2xk +E! :涶9o=i +~O6%Y<: +IS/Mr8͟']@lm8{ڕ7x& ;V$2/u8=s7{neg%FE2taȭcm`L{!v[GNp:) +%J:eo Y>y3č |=#_be'];YP I8Y.{}pԎFhPXƣCoaRt,JiU8Ƶ%;0,^k^1Z&AءIQȝ *z[wrK룵xI ;4<-C N\ۀWڃVh#$ݼk]:&auQٌB?ttƉ +^mk\:֝xda5)Bk3<257W+5QKWjNmp>շC=Yj--sM^c< dN-9ꎗSG!4FF}> .Wܟfu~#Í]%,uZz,GQ8 +, Asp@wv[$~ZU1$'48XMEzN ;m+p wC鮊O`º7R-c2;h/pz+ex)T)_=ǾBw,tcIQn^8o$`] a9ͪZ-eyG_8{Ye~+gIyLi䅈*n]ŷ> +Z0$ W5J:fIAQ{wInm#w^:'XR>j/K1'=٭+>N$0إ# Ln EOPr!8 >K jiG'm.do,{,!Et,&q irrMkj_Nc1nf(CM*fёBè6V`, 3+P%ܸIԈA$|[ǿÁ2Kp ;4?I0|T9SZ;۷v) |B^XQlSi,^Apߣ:hGeC~iObv|Rh DDgB/v%eC '4ixq`7A `*m҈L +Rp,;C7!ca-۵ITNdJSۨ: +p(c\Sskl^v( {EE!1Ҩ]8N枀6˞ᑘҕB=#az˺f-?<:=)"€6a-d{8eW"]8yg.4=^:ܛ:m9<㕹O/ M:x^OL|Zg frQ2z->.9sOqVUDf+ՏHx._8c]jv<]dG -}vPn‹0Uw]Bt<ڷ"zq;G,Lan4NewL҈^j/J(k}:"9"KSϻ={8Q8a_1 \>!W Z`# K$ jQRKmOX| +g+TovO*jDԱ{"2z3_ +M 1m}&Pl!鹌`Ǻ%rtĀ&7&ˮl]b2l9'{<[1 +lpFt3k}W1ji +ZG Wζ#/|4b &?4KR!bZ^S97-8IZo34S7FIWwlyhFF>0*F9;)%_ :}^5\:E Q - ܹq$7b0eV>BkYۮx䬺S#d z 0+;,{}Ԧ +g=d7nگr~j=?T޺xW1\w^ (rw?=S}ptxiY$ {o`53V%TXh;BQ)DD0m֡Gêҟa>_6fҲI?-vp,r*sv#Db̞D .ʹsmgfCT`Bg`gDłBByGz2hB-3P^V>>g5Px ZGx*"vyM˱0O+!O_7ܮ|ar%jYڽ~y. E]%i<;xHߓ({xzlr@ZcTp|X:-V=Ya~.h4ŀ훁[ZƘB]0_>4y ]Ɇ# `gsHPd>POc+1ĭe\h0u܂4 C⯧KXV6@RvՎPzxq:B[HLѫJ5"ýIZm*mcHܹ smcu\P P!])X#&%-jk+Du[dFuܽμave"%KtF$/v:8BM]:7zM&8 +4^bߦkO^.A\rm6;eH՜aSΊfo~>_W!][\#h{@g. Ily# )|(UVe%~tjCkS MZ)@g;}jYlX+fGy{ΕBv%hY;bR8wQ.x] +vq()L Mt9&쮂*)2Ң jr)*4we*.GR|!V+T]c[1ew7 .B'O@Ds.AqGXMl$ח dua}hcMK$LEMw_!^秊;l188d}a";R?b= ?Q~^~٩qC5y#}mŊFPe$K3 XuV('ɟpݛ}oꢁ}>gζ!3ؠ7%lY4G`dTak^>H+E 0@"WzzAR)ң(cUV2tsbV$2 E.qF_z-Z"<`7>26P sdHB*E`$˝a˳&ǻ4Ж1.'3$u>+SDމKy@fy1Ss!J>o_-G kxà .m)y1Rj;$YhY־T^e;=J+' +Gb20w$#(J2s vY@Hpnu;v|&lAQ,-kO@|YKOǝ+X<rc=͆\5Cgc9e|llU$ yB.ʃ +d9af4+1 +;)%=)d<,[LjbwݯMr-Ǥfͺ}Wmh 0W]חgmGSxf{OdzO#YhM|h>V9+코M;'Q'Qhiׅ):-YڨP\,.D +{Шh lQb+]"by1=bA{%"*de!%Æ-Qj!z%1 acΜ )#`37_>Բ@o2J.v<%M"}Vn΅-H$ǝ֭9/<Em-v#s'a>#%(*BTv(2"Ld >J0ZD!Ö+TEɼD(&:.Epux73aܧSwӘb}@6VՑQpL c՝K TV&k[wS/^װ^ַ: u˘w-f X!˔q[68q#y!g|Q"|ۿf=5LgzZ'JX7,hIJKmѢF /3kkROh]HauΗ:y&8]`[smV՟3jgxKvUL6`1h@z%DF)sM>I@*K6qJ;l ΐx;dQjܠ蜧VE3k<;ܰ{ V/')jºG]”.٬HDnW'3g'Rh7q˕hɜɠÛL{x6ר4枷)*u^H[`\ ZA)#si.YxhFSJї>ޫ+/1oMq?D9dWe)e;S_=9R?MiQң+J sE3]VxCJ6:W2cOK( 9N,``f=bMf9&Pir(Ȇ9j=21 h +.C94W{2⁵L;Cӕ08S۵b3Y e;!EN-GѷA~xmDQtۉ\]qqI+dj,hZdAݽ8qgsKZOMݕ')^2oފclu}MG˹*R^W[!Y+v}˥Tx9|N|Ivǯ\mp3-67>m"↰ƉȎz Mx#O(d6vEQXhc}]4pg*qظLZQ .y>nhcGн@=es;wES4gu#׾ )G- -:sPM$%~ޑf }P ;}X +ϒqFR?y3g r pYK \h,όRx_1=S1(L]\a^_ʘwGAc(k OW|w7L"̈́[wH(xo,쑹P毨D:IkKO.欬_'jmF|Uxaz)6Y䡫 m͘L2bs!2./#NpÕh`P;uH/#% l)TBxP[G75-=z4vfUr^"TuG3-PS,a$}xX:4[;9ߡA_ֿ̳h ;1dOw1>i tҾxD&z٬㉞)iߞ=T#:ʆV{G^}g9)HVOWQiTĴqY$]7ltVHԔG*l4C^<QoLu͵l'ˊo nԖro4:O/层"1We}U{v쎃Sm0+vRzHF4EjOy{Q9ne2us=SV^jqlKW)_g1סgU'SF)@/V1Dx\/LOPȞ72UHQy#ԂAI˕0}bZ;~#~"s";< +U$<#5B^ۑk];ap.&^vMjfR]e4o|Nh'XH +OK sb . Ujik!AK):z:m9?QvM,'zd /e~YVn85I +c Xo:XVt8~u#bM{Ex5\z+1TGor^jP ܤz 4wTlq):oA~?+4bgIRrt+7w_f$lU8v r.'* z7PԞ^K>2??Ryb|X%d,i6̜6됞}O}2ݏOYFO%)Ԩڮͯ@ï .1>yxRVm@D^ͣa@$@;%MW"HGv˜=^`I@}1xi7tp02QwaO`5@m +Ȕl~Fe KJ{9δ| a"^ŬLձ54Y;Zb"Al#oI@*VסĊ&}526:;jyq:k~&r +=YWWݙcJg*;윹p9%`[ߏo2 рb&~oR$d!%X<|=j]ZHdGYJP7\z{5^I&XHׯ8xjuFU,ƺҪdsA\UbCVl\BP6s˜$!S )ܭD +qr؟磾[@ѡ Ho^l*p@VVFr!k8FxG=(ynu:ij1eKEhCb1}$r ۩ 8Dz;9>Fqq,0[Nh(Ozf 7ŋSԝ080BqP T5)Is)Q]a?B<ٿ/g6s/BKB$1h?Y yolq+mOŬTUOÒ3]J1H~kqh* E6d΃(ν#{l4eC~sqf`+=&Xw;.Ub ~5'c]2O%(\r=+jxA>lKqEY1Ĉ=CyuL0n+]%yDMѺ_ynX#a,!rQDhr??c$Fb`K-G_w6)Ko<,'_pS()L#: `c}h Swfı=u yOnk/c}vDx>y] Zs,36onEH3pZ:UXP'`[x'dF{|/n/ϳNcHGn2Df]g˷08oȮs ĚrT}Uf@H4!X/749E9܏ѯ@=R%} _.  +DPH~?}b!(@[X* + uPb2rHo|?S5(%5p|0Pyo1Vc3:10DPsZ 9;[5f=b>3*cDg0Iak'/e?ˊO;<"J]w匞,#J\"lRD:l8Ce%cmrE-w:Arـ\s =2ߡ;Y90k>6* L#v^FP9; +~ϓz HzH ++gmaIa"=BEJog%R?k?\ ]O7zW)B(Qakj̻C0X+j_'AkzOI^1/xɑZQ]*ON޹>edD̵R Q X P_١`IXݽaC`-krdW(OQ \΋KktOi%&DF<]ZGNɔR}ܒ>WR|k;9hi }Oa+u"R:uw)94zpZ J /eG_e|T"!Ie [YQbmQvH~.[Xx/7V8X\d\ԋxxyM}T!3|̟ `Gp7?s.01*5h>,t@LSc?>^.e~z;3ܤ3+7X_QT%jKIDwOWLt[waKe u+S֝LZ U.׎#o{|\%UGygk[QԘ9vV/Ͷ]YS/ciƵD6A% *Ǡ 5Wnu~)_`PV|G*`pXrwҷoi*J= zkc!$T+A#aQ:f2Q|$B|вmCï:%X.ԉ22g5+}W!B4ުd sMnG(ϙsp qFP=/z.ܞkvk* 8g:c/v_ƼF O=yWÄGgFR勷kyeCV5brx.̫%4D5L Ll}rIR$ubOm%r~qvV}&5j +nReӈuU' `&+#"y?`8?WgbmYjr6=n8b] yz}c"yc; @>nee2#̕OIgE<s:8qSP&>I3oCqtaA|>X3L+lN=qY=cN|1#i8;# GgA}ķ 2)F.R('qE8"'r Ȯ}KuB@ێ]RHjb#TkW򹾏tY\z;sjj^aBI䣄G̻6UC&.'0}6?Nc2LFYd\I92Gq|_۩g 47?]PG0]l:ci]nOZ{ٱY-J\6HU&.a'cـI }]Rr:~^y"B su2;*{/# +|w&dZ%F~ ʒ٨`rϟy fg-<^';`2Q[tm%gYw_ +D G諄,Ttl}7?zVGb,+J/|+Rt-7ah,?Jfw ; W8d l"qݣhe48C߁:žS^N3qk.Ux\°|O^yw|mZ4܂ #͗g%}̯˾Ҋ}&^R"({fS;1yb,+rAG%1˯Ϳ|MGԱ齼֍0]DyHO)ufouϲfA#;fsoڗq,[Ww^b})v%̑tz*Yx'Ysx"o5u~J2[ϽWZ<ɕ vv;h$NC4^@"Z; ۚ )/P &{:EhzGf"2[چ5p<cv\o= FʯLnW3;džc>0=D ,,=ųE!1R69OD xCWw3@gf?- tiOz)([h̋A/_>aIj-x-~a1Wu[:8i?ϪP\> $YS1U]5%#~GLGGNrV\]g=~qxF2yJEvz- ؠ%_Vr!'3/FG.eq~~J6QgFMuNOIY:3" +uTe߶W]"նbpT۩?e FouC~Mz2vÿHCXf_zfݣ~Җ22c"8"R#E*67wı7l} fNHcT: &wr@Kk c3ɫ+y$!zL]#XCWq?1-cpb +jKlWoCBĬ8" )ksHiғ0* _X10a5HQ} +?e-@_q4( Û{N Q;Pl#zwS-N*x6(?\|mX@AGF w9Q~vA_=)M!4djH uz, ?̰;Y+H2XN1$ٶcCtP5)?3z: (xȗͲ>gO`ê'v"ygw%ᦍbNivu㳚 *Mn}j~k3j?pQ%DٞqZqJwNk2Ɵ^|P|2?~-I9ۨZtRn,QO;2w,4Nm9͡v8;G/u*#|1Ew.'?|RXJ}#\5h[ʖ}JCZtqUa|qb bWm_oX|G'eAEhf|+$^T@fde)5wSqWqɆ[{fsɬĐP8Nc M} &]O\"ۃR<(Qn_"q@#]dŔXb>_Z޺G}x`7DE1!Xvb0X.EڊK^^<[*Gƅ9{Wž01HcInۗsO_4#5_KY]M. }AQ/z *z07/jYE +E]a?͏`WDLtS+|W݈pL{wi#ӑ-J{YT}wqᚠa~5PوφjtJ$\O,UCp] 'g\7ԫW<;XdvQRY1/\9}z BOf߳Կ,ϴų@zS0&paDt0 8\Iچ\^&ZsԓQ݌)OpOtwuV@pmRZGrgLJۮJ/3ȑ4r?I2>{ܣv'jPytoWW{X؟Oqٍ7odaQY3p|_."3]r #Y_.)[hȒ9=u/; +|z+r^48%1G0pfy^ITUÄ |*!l*am?+z'(1Ss(skKj0m f1Z@ + =pb}xz*sW;VZEyE'sDbO;6" Gj~M c|?8)E'4R8Q(iN|qflܘf\wr-ȏg5͇|>W}.;HU^T_VqH ixd섧&2{inmE! 6u{oVGr'4J^0s%3`Zƺ;0{q3r1:9tbP0v<\TD2V"ph%+Is@GpCy\tl9h(!d,S]9oL]6|lȺS(e`NyC<, .!95n8Pn6`xó} AM},-fW= Lc Jrdh!sFB+=LH7U{d z4ΡwAC 7Ff#sq2} f "83@w +4{A| .mKs &Ot?6!$ɀܫEC{$/.kWVCBpǒv/CLˆyl-#!g {C=F; +9Hu4/il_ +Zyx /䐖r #I"|_k«FT;ONTo[ /X~ʓ;wEļ%o⺍KJ6kF},*CzǏ-I"ҭ +ݖ/~^D&8j?G7Dr) 3N XwziG$Fsq Ӝg-7'Xg p6Ȉ)"yd;[)=tyݼ,j[ п/,EPqV:č<_AIoïj/;!aX KVAf)##-,-e"$8RGMl4.guHs<}Yֺc& +*€1Mw~=ڱ/g5-9f,>M2NDp !IJ*ޫ? hԭ}Ut$=ض8~?) + ľ㽃M0:-F@jH:_SKUݦffQ$!5qH5?̅2NaF7l㒾]8[x%ک^6[yhm_n7 |kr5`Ĵp\p#R1݊zmz܊mv3?bN<  +&c(kFa}bd{۰j#Z/hXi60o\ yxg4>߬xH,8d;K7-ӸAMy MwGFq [Ar9?Wes_FgOh-7Yfހx~1B6xw{߮eTc #>qD ĶVJ,+ps.+_QyuĞM"Wc%?h +K09).noY1?ؒM? #< S<¡ß}$̶?mIOᡗD1UPN%4J-4#AaQ!SH/peGc!c"SQ7lݺwS*-XĐ]ڤ,FD֮p|V|Y$heC7cL~5sqIwd M\;+˸GnIYmv +"xH$kk O0Ƹ-牛d7M,t$d̃!6n̓P6^ęyBZٍP2xH`H~λwD?J#]\Ó7<*4<|ѩQwϙyee$vos}RLOJ2_Ű|_ʚ`,)m[j!o`~zmĽ2fna%J 2Ɏh!s31U-qŨ+Rԓ\ЪTG`3;m+߿ +$Yr 3,6a`W9X9=N[ XP\ yd d܀"-l`6D'_d>> !$Mհ.Q?S&FZE iëz-k?&nO +{E<7, W=Kdy=Φ1#-#߁TڼHxְ*$4' +>< ~:wg\w,GCٮKMX~*)۳c"|#,CE{$t=Ӄ3ȟWr 0:}ciјU*}Đ\{9TO얧83~v}q 4s7ɹ^vc8CSiv\Y{\7HT)j?c!=̆zFj)>Gȧ{ CGj"dؒ|g=-ޟ܋JᖈC2Vs79+P~Wvð)).QOdsd;8N}.v;%+q48lA~N͜8#F`=&Bd]yFP\.x^,5BQ!/3Jgį&ur,tqԬǜBY|k•3|';-6`^RxOOs%6﵎\ptu Ig4$LkQͿr$/(gCg5lsW1Úk!4_]4eP R!q'?X/\f]S!zOA',35)ym_s( הksD&kF?Pf4rѓt׳!r+Z@[5%1JV^ x;t:/"^cO IǶ[ޝ}DCjG]"Q8e\t((Ff7G+R@5 0$r؎)sli{[\dA` ߾4+$|?@m(tFMM4T/AU.?#z9DQ_V*.rgy̯LL(?㵥nԇOy;j4Ҭ)l0E@)G)}/++ܼ4FUot +}4<\.aDC5_`A؛GD0gmk/yߓl C0}56d_2E^%}Z5onP1Oct׏B,+HOpo)G/cɕż[lQuG[t1K|.q( ct\. 3(GgHK ƯEpka&{Ur+!"`w$ilRWAK+6a_c/j{}ن-:722ݸun9sjYN '~vl%D5 +46>=2`X&Nƨ`iy/`{+T;MH+ \=Ÿ<!Hj8b>?lce;JNF%uUbo$$ҹGM1ħ_!fpKwNhŚsQE)?*x@L +/abgc6?>_Ysۓ5%u@X~pXb*;q>k4?&wi:K(ؔv<9X>11BEFak&1V~ud$ak/:^v `Q`LԂv.1]~|_d, B@ĖUj u-ܙ+P#xUw[A控ŝ]K=R~M anlx +Vƈ|i P)b95lg1|cY1gW?Vc #Y@kjc{L!SG=]8N6d +(DkfnB<εgX?h FP8kPZkp|]č`2э}l7#f份ZeEjJu%V&z$΀]*gLlmX@ׇk l,0BZ8\"m KGR +Obu|,'rx!u)L7{x9ZM?I %1[]BFpteʈ'W=PU:|s?K`, gyMn܂\YJzn.Ař9It &ݙ11/ '80Б!y +S>VU*ԙo}N7%.fzDkeNȒ H[I="شۗ=HvVƳ7bA1oo,ɐ>٥#&yNV\YF7 +U5>V"e=7ES}XcSZT= {NUܯݯ2<~[" ۆQ{bOja/c"-cf!s&«# D'/ߔ[0'q}5Yx[r4.ܣnw]9ٽc15_3.{ۢ 䆝xR$[fW.1嫶}i|8% v->i ^K&z"ug{Ro.8,}D I3u bO_j0*V>A* +B=*%y$tDqiє.y +^;Q3aK2Gq0V$ PgQU[`@gp{_0P`Pf](k@^P_tnE;)mb#?WqF)lD43p3#W}ϋ2> $-}ϴwl?$ZLgzCDLu/kIVVͬ'NG +lp4 Q;ghY%H~ +vk lM8/xg⳦e:>K@i #$"Zg+vH+^[iiB+&О쪁ooEou|ѷ@|9$iA16bgsj"\ +rN#\8|$rn +3q0aEc!%L2|/F4`2^(cxD?KXȘ1Tǰ'-%VPMrA=ٽS"}n0Z>: N>#7-YN `Ҽe׵"֛c{17k<͟7s^_1 +DN쫏-#xyo]$n+k}$GG9G}az5LӸS)pbx0xy%GY .l +ϋ[jA3vVn6 >{:W$sOJ_q=NfiGFZ/ 'VE=؈ϓzNKg,Q86!ڼk  +3L~橒02 BthSRAS[{vt'oٺolvxWRdZ Jr<;Ly/-i7*|@ǽEGq D{_70 wS%wO :5Ljݒި~UnD  *N} CUE6~mvfOZ3g>5[Q:,rL%goVG pm.2i;Z!J|0GL: E[UGs@͓$fnX<*q4Dk/p 5evĐ$ŇfCFWŐ4-k{e~u?sTbwGP\> Öo.ݜ.J(CcU>P#etֳ Da~ZǍ U +q}~<7AYz~VI CSJ\;EN~X__/b? +aq%M 3tt YuV^ 3V +޵MI +#d(۱(g,Uu0n P۹m#prsb"gLVO*;ZB"ѺяÎJ r-'Ll`ɣdm H:7\P}Һy-x-JܑkC{K^CeOO H:MٳYJ|sD%L6 +"K;_\)Bwt5foU\1'gven_m@uj`fBqE{B߰T|ll/Qg%33(g+͇Bv,/'i:KF(Cs={+OD܏\~k.3igKupT2krw~aFۿ̯tDBu=]p +jSOp`_ I|Qtas31s8&6?Г\z3֟@XEC[}!&S1`S˜?:̎ˇUftSu@a=D1ϣ o?nr> rgX髱 u7<4]gr (0|MG=T˚Bo_LOҔ6u$T@a#sH?̣k6N6ޑC>F70^{qt"/ +{&|3;_ 65$`s'zFo$J%Ky9(mM+O'F!z%FL:;;\9D8$!A9:CfJwؚ׬1M[~Xw>'txb 3POk'plڃE2I#$*y+ts' s3sӜ$¯ߜ .3ў:YŶH͂ɌBHT,tϩ9qx "Z +;I6'N:B]$YB65*_wQO6|Wy(@3C1$q(->=YOͧOueՠ9KyϜfP4$5bЭ{H>O\qԐ87MN.^ƸdwϹ8Of@ffNtpmi[Ő=MYelm7Ñ *I;mF\ 6L`/8<.mEPViVJ +Iå,#<5n187;J#'F~pf=l֖5T?a#zg^J^r.HsAca<1N5Hplmn`756gj,<"/mo~i;߳$-K,7UXXg7JȐ_|Q'!bI5$9y#A7l[F\Z!#;<=QƼ'?j|נrә:GԦoL3o_Æ19'Fv}QowrsNKn4@1wBq\{y{=?G3p?sPz/=n Rb#c]p=Cn. Ց*ls?233s൑wO~ +~x}#d TؗLD&ཹ-T!?5?]1Cng#q|,iAqT0y<ڢ-9Rzm`mJ4|əL`wEBFЖ*z2Gj- ;;dXXٌ@y*5m1^C Q(iGb\#J-+{6q{De(u+R H,QfA*QBO7 + yz8O֮R.@(ެ#(p*g]Aʞ"+3~b@4>_%pj Zؑha~6lFp<ɂMs_ ~WnŎ oY8 m@gugZűrgcs'*# EnJj7!`%"~JKk_>,,յrYb4tF s{"6@klcUs2m:Mi3Q}:3o{T6z+Ȕx$=:S=A~.zUt_vLAa>nZD,RIEl0n +|ReҞf{eԐ}szibh6i <#H+󿑥)wx'^Sr +\QrO7XuR"8hBhQÞ?wsǤ-3wDa,虼CNn]IvKn +E]"._D9f$<"qGwH Z#:hP;O1 +Pa W\G;w+wޒEȝe+$cL\{hͽA!< ɩ{7N[:biKcʠhF(uKܮnexxQx~:fbQ~Iy7q;2p{Ve.b1wJy~DI0BHi4`eyͪ3.N!!DSG R͇bӝye$a|C,v~wf=!NN@( (MeJ6G\D`5:-=GMw&54ybxkBB4 rS- 4,M̟:*tϧI9D\1T +X텓~UG'f38gѲ!/:Jpտi!W|я="njY^cr˛o$ +RW!'=UhnPޟBd.zЙꢥi ~*iGxyZmKvt<=a] }:ϊ~ +1C]rA}00D8l#end)܍ؙn$jJHjoRc0XHDl~;L02 DSvYx]Vs{ΨmٍO%+Bͧ腌8P3CTV>ϓjjCKn!ׁnٶTŒr7X̛\; J^_|c*!Q jKGF?G9J倅nHЦV5*!Nl:-aӀ2s9PA@U-/QafOQڟWLU4P +ƻH+GA|dQ89 R,DEfD,m:ah:r~nq8 EBST]O[6xw˼]$-tn>[`)s63ls40F#/}8SӏX =ŹI8Dz&"NFă[ǨTdoA.C6;f:ȅǯG@:/8s!JZf!d^說h!=S}ߑO>fPBuS2HgL= ː)&0O0 + Tk,ص$egD1g7M2UwZ+SX~j"RVBvkek9nGr2+\|' [x]?5S-w9'INodmK Im`uFR ʼn) +@0EYdoQ_3)0^f]#.H#-r_oQ5ov ` >Ka[TvgQ9{|MmBR?3NR0/sGA_%c pӶL^=?%: I$;-߹$oJ-1襲0ۥ&Lma 7+\M:7:vv4SWļe@u$y9'؀YӫǴi6;\qF AsCy۝C y Mv>\b + nאEz N"4JpA<)7J`[,|CjꚚWC2ibΘ_%4-[T +41tbgsӶbSyH@< f.+%:֙vAo[] P9^XP/d& & |4C{řVAkί+ +6 D*R{R'VH, dΧ+a{dWOi kwBC)e>s'i)JQUExޛ/ 3U +M*"#lgrl( &iU w']^[cbWFl)aC +y~Zؕ1*s?9)HԠM.Z>׈v@/e3d~fDz +C3Y<ЕOBj&Kta)_hkЭ%I "1 $BnB=#NUGl6F6A4|DڙF?⼉ `TTDGyP5 #$bW{q܇|CYq(7"t~E^cbEmخw'OVt3t)/fa{7ĵ*Dj-D*ARƊ9V/gЕ~ŶR *9S"Lw-5@uY2}]hv*j>2TM2h爜|1AdKv)O/.Ns<WQ/ӈH="]? X؎Q b"E誟QTKlԐ)wESu\&c1 Էi쁤UTƊ{i\ӑțCI I&B:(Fa>xA%;g8l:h ^B\D"2#\`يRaq }ftçdCĜnQow.LS(|tzxUydphgX3yi1!Gh$70ց='}1NZr+6r?e(N<=a.M WSCxԌr>cJgr|MEC"V-FH6b3ӈ7}*MFtp\>PT 3/l^gC^ЫX77/+JHgcСx* [xs_xKce?U^ lgl`p}h@A@ +o!Q!k +S#3Uu9‰1zwcDW'Yd6Êû" +*z7?M9.< C6IKf.:@iX`wUͬ̽S)j+:?jJ{]3xVID/ m(Ǒʌ{ T4ߞd G5"sxqIwq1I[7⨮"fv;{ y8(!zG#G y-퓃?W (#ӟrx4J[T?>4WdveʌlW*ܴb[Yć8 ?W'_,}{^j_-2:>v#bW80s+|9g@r6<F\_j`8:g}]?uXvcSzt;Vl3]Jy,<x9&XSʊK|!5D3_}~(H5%f(MK`^2'D,kϝh#NrDhHmTG.if+^ z25j +!>'T誧\YN#UdOݲAy`Ee|COir z/_Se ąWi+~.OƔ m]3ʼn +*{=/i0G&]E49Q#HrGaqMѹF +*ED(~B5 mtU^:$ZtW4B¹b2>$Gu|ΌO"^QO:pařE' ǐ3ƅaPIvT!F2 lGJU[^tr7Z^N{9O:ӸӗjS7K^ n_n n@<]?E21<-NAw:EĦ{KtPA +Δ@7f6 |ZM +ѲMn~jg` zLLʱ~".zRꟐK[T5<X1E@-=(h-Qj!ݚL=kM5PI GEt){{( zs^6$#]^dÀq?z'lBND:`qC8ٮ0\4y+Holɟ1|_wJ}֒r*dI77@l.[l -4g\9Sm Wl9vC!אC3cϠ0dsXşbM\"琐hCW%Od)Ri?{jΝrgt +u tqUQK|]3e̔;Ĺ%EVU;C%f#0 FG7 s)+īU&S= :ZMdE#=Rɠ'5էSN&I/e]БL)v/ԭPYJNZ= aTKTRY| j~&qh(3]UTݰi3uR++O݃9ӶT,xuP>B>XoG3yKzf퉑,WKcVB|Oa+4C\DooC@{Gb3ХbxKHT1p}F)̹Q#W4Zŵlռ.uB"EftqH}*AA2!-0";y,=m5E'ՙ*TL%uGD󉨤Xxn`#G tkv/O2YQEp!b 冏!H/@#e+7&rx?䙎pMY]pƺ6baD![[[9e2Ik>'D{] v$;OW.FfQ=Ć ڭ5"; W.A>2[^"]) (uZs5HKiK9u;S {d8@ڑίI7ܥ?|h;v)ŢX8h7wǀfTz̈CeڹUF>#yp)weEYl&^Aؤ4zԣBJZٟnnq[ca!s]Imcq?j߫,p f>GAڵ`Q:אмLuZczjg`K X9"8뛋-}!-sూ 1fjh~Rf-Mwʈa +4:,3'lkGYBx%6FG GRvYGx fֆcU߯e>͒PC<X|  zñg)g\ՀA#?x8Upef նgUȻ* w}f~Ua:@[hG{C;TZgrr,x'O(7 #XgOᘠzѩ\7Xc_ܗΊ +?zY*+ #w<)8Síӂcȗ +-[dY Km iO$PA[>sQy)j8= Wqi9NjOѝ[Ԟl{t;sڬ׳ЍxQbY`D^غa^ɞTyVէ$`|cE1{1yFȗ*#;h*A$fWék~NwYbnÍ0'Mik4DdG>rz!!*o +DTY## 4V!W䘏lUF +J]Tncҷr oBz?(bKص)r%QYw,n49fسr* (F ''U`<bM:j E#)ذ߶c~*B$XO[4L(/K)p# Wvttؘ=7d :i/7.8U/s$E@Ɂ<@)8Zk TWR --z7y,R%yᓅ3g 0{u3`įQ2W?eYQ 1OE2b"y&bg#$hp ݡhA"}m9;C2p3hQFdGtn!C3+N,*;k l3|t^AcLO$#<0Nu@wGgr,]_xNb91bXQ;oQ/"894.F> +i t7v +Wl[,˫D|sH^Q_X/B[Bu)) z(NƘVOauGGInB:8"G,.ÈyF9)NP}SGX^ǘTrX~FO[Z\K +Ie4dt$_|W*4Isd@_hL}o-8uշI7(yz\DOu/%|D_Lo\,`%6N>BT|#DPRԇb۳Xa!yž kw (x\$p]e8N" +[tߨ~M0LKAg)1☶<Zh'Pg.*& +ΐ# j ?YSd}zzIӧyXxxjHi|4ąorW#z;4߬ +맼b>A|w:ηǵgC )U &m7GF:vS︑>H=.F`rgՍǷ!3$nI7S}E mDQ9tJ,$ZlwJZ~KW䵝aT#F[%I/ǠW*irA%H7r+J +3TȏPP]tV ΆnkBTTآ+>UeK6Ϥ$CGD7Ng}Hn ko)v+X-5^Z%WPlBn??ɬSȓޱԎ(+0nLED-Qq+EqY2g$m-5DYDs[T79U4<6_#}2A>)|lz$u}'UQK-(凳h9Cy !2G l椽]}"ɏii+ԉ2C';I&U- GA-*BTN*) +|A_Ƿt!7˨uo,HldԌp`*V?胢}9,c^zȗsL29^v4!t%aKiKҘSɻ ;)o,RV0{f[aLG=(u<]aJV]feP"ZF\xK'ftr~ڲIjr'hNK λvy5Smj -yJgebS*>1}*6"E)lD)!J wx4Y'^'~!YdlO0+X57CΜ:nf|յ~ +X$\*NGCHP4sVfODvrTsn?k~R~<7%x# M !1F:vnDH*CaZk柇>O0={OPރ gI: } *-*m$Ո;1`}2_X:7ՠ%@e6RJ>#2\Q= "/9qyj+EdVw<܌@IЈC( W#,#z>X4ӹ^+A,#h֯rФ0-lxY~j՘;~YJh7VSL(d` )?BNPȫa# בcf&VO")`L*j0;_6,SlfA[Ϋǥ-*kyX@]N^,t#ʌiꑙD" +3grOgjDBb(BVwӆ[Et%]=$ɍ-59jLyE@Cۖóv`R +7/15.4Qee%ʙ¬ѳrJ)3fHT= w7 +%`5Qy9/s;fxj.jVlW bO U\a_=NȮ~z($h`F 3ݥ1^Ct*+o]HрgO Z&"ét D ^ )NH!IHwNȹD"yv*p% pԈee{b2gIQ$_@{5&Bg*gݝOwV !ڸ)5fלzwQy_o>}Tń!eW sف;(CnibYu!;2Y?ff@Dw&*lEH0;:-8K +LS24elPi 9.Ky(X#̿͞StaQyC>=r-}˯ Ed /0ru{BUjVE6=ݕ5XSsed`AJ|E^;.#\Ȼ}mt@ + +om)yc'_b -|hŭGOE2ܷ{]G:I* ]WavB`]պW9j ~R|^QJ, +sozaDio?7qx2x4L=ƼsJT#󀌘!"Oj$Uf `EOEWϙC}˗;3)V3z$Y`}Iw~y 7k6cS۱2SzͪzsGA'Sm9s(Peۘ+GXuLBP +γ[/ri'-?NѪc,MI.TKmT>_`3 +fZb< V[h-NA۞F ZAA瓀qf.m;K,|j.%kaM#rߣmW6ꎄRl,, ȃI|drjJV\.fJ 5{R8S?c + 7F/ jzJz5wY-R2 wȈ&ot躪j?HV̟# c9eQ˱w-q QpGN}-JwԕO6B©.@>stream +wǤ +0 H<47#x +QJo6@Ր9\9QKȩX-8'=)#k $.(ur2#Ȓ^W]ǹ +ǖ A[AGx1%<?"@;8lG^O2 +_Y[JVoS}aAt; EQK*{ݓPk֞8^IAb)WuhbqnFbqxa^&gy.F80 AV~p﮾=K"#8(C⦠pm}吋nj!Jjg0'B}E03TK8׸{ -4 L J+ktfUzZE id)tG;g,77Ruy VKI{x[JD>\b YzOu`jQb/_T<*ck{L0-Kj m B4qrdk:d2:]=|gA07sE4P +vWƒ#5s Af&+ʐRt ~KGʥ!৯؀Y|hJ S?'F wlI˰Rq We7S` +Je) ;l)!7 pX*E7Qסu3(pun`>3qtNX.LjQH|*$q ĤoK MV=4B%|Mu,9LRi հԉjF@lUU/Tcr`e=q4c+Xr=Ef|wQko0Gl+ii).9p$α Mܐ?[鉭#RŤ!{yɩRH(GDF\ok(#J7!<tD0$o%-4T>E+a\xrZq:8ybȪ2%4Lze>1O905#fI8JPH)~\L:kRU$Uyzk{D,{d/1wZr'dUC6B;Ϊm;/6 ku˪s"XU \s!󓚧9N͈$ :,ўx! 1u_ԅ!-++gi2q![fPWT0udz)Jt`bHBP"#ev^QeJ ߑ &<,g*hhMXV*kZ%dB(PbQ4ԙ~/hI':Z˼[];VH2_1,Rs} +U|&G!)>HydI +8WNlB؄h6U;qr/Pk̳ïT9FW\K!CHYxexГtC\ FPa)Exg@,۳~Iuз !o"[*Um]˂.$hѕ/'}`KZ*)s rco̗Ps?;Gȱ4xmq/և +(*TFLnN`O8i)ÈVc8J >p~L(#[Ţ˲~>YH4ť1t d±-lS J',+. +;l~"a>{#lElʧ(Z 9^z#9ϛF\AuS6/ZK tYb,bEC]aK!fY E+Cԙw%|!8φ^U61Arg)#P\XX5 !suâ1 Oin;f2d{B&,ۣ>~g+OEEBAq9"@\P?ƈiS]a,s{4#viNjW%GWY6gǛg{[/ BtjݓJ1=  1CAf|5)iW W'fԎbE4T78AX$EQy^ P~Tg{9X,`qX}O>LaFɑ=k'#_DJiss*fw]8W\W&r;˷Uۉ5/xO$h@Wc%B>slI5$'gCTۻ򻅟;3#!~ɹ$ 1+SC<9H_-%:wՎiLbmU2trˆҰ~zܦU}=PWeq.h8\Y$&0猉U0΍]0 )a4Ϫ %}%n0 cLi\s7&߻NwuWd< wFD/ruP{7#H VVkϦ2(LzYecyFvAq+䜣Ib+LTR:-]r')R ͖B7%၉̩{ KcdW54$P] \4yģce|n;q9u0|V1@+zFtP.sqYn)!ᏅuTQ^Ct~coqeՔÜu~#q^7;a2Me([&۴[=7ي#6@nψ7?$a |ol~gҟ|}t㠢żU&31wY!@v})ƺn:cȇqÑ\!Z_C3penc1NAO}1VXSFv VCJp4?*lSeS|KX-pl8v5}8BZHwBD&]D%-7V,z(ȡ7i/Men7gjHz +2P㬌"b= )1(z +s[:E[d\l3{^ǿ)dRG{/NTjR#yԐ|i}~T ҍCzRWOE>S +#"^]:}FLKiƘ#IfO\ ~"hbxM- ) gЉZ(׹?J? +\<]g3(_WRH4~$H`A1:wc/`vDqlAPa>*9@O3`T_"p튌:֎麆T5B/Ѽ  , + s4b3Odx߀WM@NBO+w{EÉfꠀ|=|=7#ۤiFמ_>#]#mځt-S8Ul|8bpL輅Ҿ~a >\%^!6g~騝 BAE:e$K֒GRVCثo D羨}J%Sj0z o_Ic(h+ -GrCQUyulgzAzY㾸{\N7jvh"̨!Ȉ_Z0"KdOBCV׶i8Uo ]9t)/^ ֆ9_wrtX4h8a8X9vmafqQ=^h%0G^6T3*>cȰ3o!pć:|8duL(pU٢{yy)V,d>&2nڢV8zTE/$ooW;/@/G4cp6P!20%roHQZ*O&"j\g*IV)z<įN HDPsY3ՅCT`|u_h!dAaH،1]#TpћG?f6^|1!,oHsL 7wQ8nmV[~=}щzwnfYBsyMYTZGy6X]{4i)M `+nA9B0ĨOTx٘|RC&Q=4a1Ł5K_͔Q+98JIiN'\Eڄu˘F%9mQ)ݸ}ISƜ^cj=X1@՛ 'n +Ta<2}0(SI\b6dX;J^R?#.dޯJEP5;R”xPT;?|E%<255U-dh@]|:^K5RAQrCF<윓Al=j6sVmKL_7VT?B iL4v@M!a;@Җ>h")d*W>s!:FB "XqVL#Kz̏ s{" OUF1GYyC!Q qgdI }}W:piƫT9lb|زߕ!7ΠN:pEJ z + .Sm[C[9 +A7b^ȘgHyѪ׿{Q+j?O:{WX*.R*^P8}҄*s`6f + OQݑ-r>m ~K!EP$mBjM4w(NWvIn?eLa[a^FZFv 7uq*ZF_ϫيXǾfvR.t4gvJV/Ŗg6׸5?ZY\'`jwrT/LD2O<9 H%!7Fk#͹s[@G;ˮ3F5Y7Lw;l*?yul՘x߹ +SA /.VWm_NF "k2- Z}ݟ[D]= 927+wGxXX+#oW8vivӡ}5䊑S qn(яn^ńbCL7 0ꢙ, 把r_/ng0IA + v5JOuz2*Bs]>MV䶩S?pt~R, +<.N)aS./gA44<ZϿ4>_BAGhs pمK>kC s<"T6Gjl)j%JSx=6t^ Rg/žlB*kiYlRBj&nf~7TGDPԴ.;-u@z7v!P+*dZ1 +mP$TmDF? &?տgSE,<a& +P ;2 FV_U$>5(Cu;$$!r; L-@|*UrSmHhqDߣZ[S<=/ J8-en?.{L쩧2"47WNS"&K:]y? +J[ 9|>y~HAQcHٔR[#yTK.EpE))Br壘'ix7}W{8gy͋[N|$hT1cugAsc bz?a5=TYdo7F72tus*5EO%Tu̗ CS!zzJLԪ+eo&i9{/^7`|>ADEZV`^qg<Υ,^aO1_al0YҴ+:@1fWpKpU@Da={ .."K$O)`q.k*D|iEBzwc)v{rֲylH{.#Dwa- (wEV>pI"@C( (#P`b'rJ=5I@6DI$IR";G|U>q@ 5UM.jss*sŽ ""ޫd{:{q+; To@G犅3w%#"7 cks pR$]ax#V­/ğJF[Iӿ8 +~?QuKSv +ۣѻ5N\@ڦF3Kq1VWH:b=v Rb-s5:Zlg)tJ[|p;\dkϵ DK71a*w굒ڮH*܉~}նNtk| \0Szs'&C}C>[8Ţ$|>Y鿍TXc !{ΨE@+bs#h•Qe +sLs^W7=w~3dP0[Ɉ_&|bWIav#IOVD׹Q=Y6m~m)lʩI;"W<{(S'2{!l5$삏S3o=DCN)>U#@PJ F/,ߗx +a7^w]-ҵ!h[J:+ !pνUj38mi=Up1>$?Y +%r.SMᠨ3cWp_<QCp*$w rFAN*zҙ3ep3sxK"H\d +PAm{Tc"F* ϭ!8`E_%+gge9bi2=r|PbUB$A$G䩸z1 R SD}|0Z5%yH-I!!8Ws#U|->hjy>B惝MT(^߬xf0WsCɇx.1ą&=Q51!~1Sor]0w~5F3M6# UmŦUޕs>VK]N~%;jD9E^߈nhTpH*B^zBu9;ͽ\4bҀl}ϿjR}{`~m+x%AaQS^)kc5A\dF7>qQB?c?>(/#kuwB.).mO`{[Ҍ*f D)'mb/ߣ`ۋD}@UcwV$GU < s`,ŶʍU#2|ll""ts?ۖI|HQvR*9@^j+1`bXA֍Ծ2l#"˜Alf?{5F^KJǬ>bz6&ֹ-F7: e:UDמ\e@iHw"9}i +_t} 56GG (+I߶ @2sxfy}Yh~)TPS >.lΞhT4quDz^wGVb2<[D<LGN* '\ ʮiWfJp{69+=睼AJ^#׹=?Sٛziaw"1=gqW]zi2V\5Xdq s0Ub!iw['3tlo9a ?@HO3WNҙݱs:^<=QmcBjbᄮx8$JȰNzp{&M-'Sd*A. gޞc,,V+p¬UE{H=:*,>oӊX&'Ś+ (Amfp}H⅀ѵ_MNv +,(YYhXΠi-fҩCy$Ԙ9 +UNUC3ࢄPLMwUeLC> h4!* ͉ʓY_ba0O zK8&rQ5\FIzB: LȬ:K쎾!_{-ĊO6[J~4o=RFqQD#Uy +#>?`sLU:ǽ)9\C;S9dU=c7)ZI=PN1z"ʧU%TQA#TU|ɩƺ+܍A-~L+A +O c7?`?î_%Π'mi>CA+VZ0aKֻ) =jB3d PzALH1+c1!Q0Փdk. NɧRߌ(FPyOZDc>GJLd@ջb I뗶X6@ \M;n+^xɁZ*0JlI^oێW[4YRɒ;!]7>3pXlZ+I qOu6?4~@$MHpo,Uę@)߯*OĵhRi1T1Yr7MgWIvzi]q[Ӏ7Y's_:O I$K<; Mh^ i ? ɥS_ :*M }AU͘߹ExAd:gh 3P 0j0~8?%Ir> Q@65jHx%R/}`nz$)|S3UO@|fq!V}(H?@ⅷXQ d)g䊖lc6^xw~BmR__F:q*>~*uWnlǽ_68Aĕ͖gO C#Ɨ^t9ߌ᧙# w;x검٤ S]c+0HUoe5ݵBw@,ٓ7?>y!y _D#: ٞ"6,Avadےl=JqGT+ZQpX qOI9i 49+\2nfO,cꔺlo{E9XwRS:#Y|=mmHÕPb#wE-1 i"nZ3QڀW=RizSJ(߭V*wfgcG87>S%zp6֭R;>InNO/OOk6@( +Rc`sm,] LZ*QuC8J3MZjrCiAmՋ}>1+Y. +5׈Ҍmed.@/s V/D-鷤]UV`z\8^i٪ EF +XՀf n\xDlAr:URM +sǍ +ǡ^jၑ:3Vqf({tg]?iiaB#5`}b0B@%,<JӦo]Bfqn%}0Fm>?>8C2nɕocH}#m~Hap +f?'dШݽb:[ ,KSg8ˀVR({Cp$hOzAn56H>q?k>ҩ}u),cǺG7msKw^ܽ<7^.K$pLV g*6BtH7%hu]̀\y!%ضi[Z/Ol b"Gboqxl/]Il1t8PKWdr_e<e[*!Ɂ~3^:XmEWQÚ? ɧ3nɟyWLuTߚ'BkGGrNbt3Rq=ԗFSxRqh[sFlcN(סlQWL$oVcD!mS+ Kowd.!:TeO;lWf |CKڷ +f,*f]j%o׵e|UV=oP(=KI&2^\K: ;#g a^_DHo0lWnE9|.~Gė`uzDl^ tڳf`uNly[ʶD@R1&jjD*]SPaǴQow/2Syql!^1M҉=)_/-۹<J셤q?9 ,J6!{ +k>ogl0 +FՂ 4dRkNXyan^npwG+@㿭 ]ѹo4շs-XtUuUTv~1C +&~bTFL_X_ ?{aR-;˷K"AB}yCesKF.x;[ <2ɕF.URǣS*;̰w69%|c"QS;ܔp!]|^ C_p?A 0'`L bC(<!rMzdîNwdY\v܍ *r-rf3 7@WZt D3p3L> +?;uEYN&/Y4GXKj茒dq*D<:v\󴹋DxD_:ym)nFPd3kjŘ`e7Hg1_5MxbAS̻#6_c1⛠Z Ĝ_4dI(KVXT_`%8f|K]{?'ɲ#R,If,2F4CL%hǤ Dyv{tkщ L 15gɐ5f&{Nݑdr+<4KB{ϸS̈1꾑<չ1!yIT|15ԣUf*pzEKR% +Upr"̛wU_u>_QOB\$=Mvowq0K]J +,z< 1‹吭eEa, +uoz qαY$HOhլ6|Z}L^t7Eq@XA%VF"9zc]ip1fRN;WFN[P4ƹIb@QK[s/o6I$< ˨ +Dw3RF}Dl{lQB{7e%EF1P\ιogN)ļ[I"C_H^h@7"Z*HĞ |;xНTmCBS%eERV`uPFÿ 5xOcOwcKE^ᶰ`{ox}#N8W";a;x6L^- $2(Gy!ϩM|_Ew\7=u')p`BayAHd|A2 _\b 6?5vq;7d;zb~ "f~/AgT7'<;׉_Ty@hzH/G2nzsW8[ܑK Ep1DsWݢugeNa^@P̊'0K XafBBۂܚ-,m!"A  !YUuR'j<5g 3_#-Jx';V+-|!5os|}Cc~?fej<$5ZqQ@Fձ7oT$'^?qاOVߵ(dgD[yN-i=%9~+T@9bp4,P“*_wǞKUjF>u\zfC!_pD^#98%7xK2添wE~6T멢:8*K#P^\ 9Y0~WiCk +Uj)5 W2Tۛ5.Uh/ QL9[Wk BټK$'%bFKsRmsfܭ*[1n3T LD%?ɍ_7@)s`ܜuj@s\E@!zW~y`(ha3葩V OnlH+hT;F[K߆z'\Ko<SZ]ꀂ$W1ۏԋ=tH .~jT'0;]Ru5(i6t,F$Ģ&7tRWrfǪP71'(1a8qYt@ [Q1i|Y(֒SGn] +NǥM/NXxuSjP- nFDVڼ]0Zf{{X8Jk]){n@7%oCپmpfY<M 4 YQwGCuiQĝ HiWՃ6zS;UD/"v 8=-)mA"h:W4Fo v k_n>xuOQǝ_LOBO@ﭻɾord%ԞpKKwpWi8|[;[Udǝrй ɵ{;Cd:B/}.->%y>!gM*yD +ҹ :1^ҟvlh!=RZR0Y֊=;fB[%iA>s'uЄuHL~7Nx`$Aqhe-lvb=f6ny"sJaAe_pRqe_d3)K曤RKuΠG9ZK?c^~דq^,vɷfq5otE{vc Q$TבӣxD0w[yTUco+]"c_|QKb~oGEw7)nZ`az?o +1tn5[{F|VzF\>(Fs""4py]HSn>^ uf"|CBY;-H*> Hg@tʐ Z5>=tw}HJI(gv:Q\Ɂ\I<&vȓ$pKU<<)Q#4'H{96z(23I}PZ +I5 X4\wxwіG O܉q5,*oDmK"k1yX\M<*]ÏV3F1bK+&<륍5Tyw8seb?3 +&#lwӯp/ ;*snqO+96\(n ߣ^4{^8g?LFqxEzXrđ]&v$P`F̄v%ӴY!|*q9iB' ͅe9鳂LZs؝B! ,U-L"GÆl@G %m +}^=6ñvF-Cո l2"-W+ȎyBYcyD +@5-TBߢ%_km~P +d0d^y&FYzbkH"Sy~ްy&vϒ}uͽy"Ȗk%p9Lb ++aTI(zxeFv1dJSXdrqp +a0k_0ad9XDx5LG 糧uvb9F;ė;W2) Q6h9j1]鲞ϔZ_(|i;uH #aaɫmsN8QJYNÂCTG:4k4&oe ,qJ8gGFes=H%e A=-a@C/3TvZpcEOȃfd8e^O:o+e! ӲMEy4&/OH~F,!dzȜG3Y$9ڝYkz2=kH)Na)_gId WH4%];VBdXfRu6<wz +$^'XKonUОs cȔ^O7˧9,䍂C@IhITKȣXex݈TYd:5c!0H{+$WA_?kE.j[C]2kRZB[&RDݨ2u$\5~ r~?DT#Hϑ9br<<ƪGyK76D^Rk pkmGL 4&㓞| j>G4וTPD{+73{2wVWP.4]2gyiCW6 Z:$g䌋Q+xj8½YTAn!屴=ax}X t +-|A舍"rxjIxǤ *!ISE/({L3} rUeOFu]%jkh˕J6_ +szmy{7!R(zKS:}?;(AmdځGD]'7ٌͅt'+ +zy[WX2g|]{m01cNpv6񇇐R8rwgPN^K<.r:- "¸-e#baj32گf$a 2"&<' B=q3=#8D +%~dzИߞqۏn5IFGB{V);dʫ'S7mPb jSz 5v?h,^b>W3ݨ+z\I0ŔH9" +8~:t!ϕrc睨EqZqȊľƛBxCOR210+hR-U_D´VDq`~BoKnr> *ZfVAc+t q,Be6hjrDBE.2|-U+JX^,p>Mgv]K!tJyȅXG +~'IZ;ʕYs@=X_'lqxQVͯwC;!.1RxjtM9tD+-,h uةW kHjMt,Y_QSE]K00pޭF(3E~SP`к/,q1U]{m %8zNVCW$v}D"pFLW_ DP^̍pKG#`z\*-3}rEDxD Ƌ4_=qGxΟH!)O} ٝ +"^UL +7l$fH $;`|VאNdujƛ MRWDbfMOa$I38wHѧ9 +IۣY:cQ2<_2:f@G,0ЗpUhq3ulÏjPf1GĠLH'VSznE qjtBFr0G()HG4 1dCd3wt{ڊR|"G32k4RpT\#s5XFD7|M.]9HWQ4wZeшIL/n3: +yzVp 1~낏fF"MY{ Aٓ~in~wpQv8m5ȓa'O F0)-B0b<:&B)7GVưU4%QC%mkYeF0D *}.ˆ8,9ZwDUF"O>u3:<#RϦ=堁zFΉ ]ACma$gal~A/_c_~ɼs.zW; 3,H&VY '?FƴtuȢ8{/lP[\I~PF}|ߧZAx^{%P=AT'wnLnZ1 5}4jp-ٜ% ߸c5RHz1ܬyBˈV}I玥VIsGB{QÒ_y8:0ϣ`o&-V$:BKUfcN1ˀ; -uMx +K|>|.AC AK?e|fT]MH~g+ SzHX8gqzqFb٩w nPl ( %63U X#;Ԕg.ѝ!$?^f$H{E#B:pYn/wZ>L: ,3=g|/D!͜%$+T'Rþ";>~^0ڻ)'xw2~ vXvG~}+B\.!ADjuD+fsk,xFnPB4%8S2 iPKi=ˏLbVvt~dȫ< 00Q|u.pP WhzYWN<6 ѹ`$Wљà^ +CMꄒt&JL %ьս0ԓ)^r?hlFY,[v9WmIO*YɹaC)^r}M{"ݾ8 q[,upݎ;N\ +ZQΫa30O{(N\淿y #tmGF^2j.ΕsPa %z2ax[V`*sgzO_0 +LPXqmk8'~+>drSBG1c^)BD эi ¥ &thos  #vTCV+̩HW*JKTa+9//ݒtRݘǿ(8q" 9w"wŝx>TD:Z2 ĤOrkKy>pe:޷va-75+os֫:'{xr3?&g}cOvw43 G}c)M/wGI09_1hڒ.sL5y E{~d -V-3^?kd[@ e(u AE2t8/ǶJ;q͍Hr]Czk<8Ax Vy %u%;LKO9B:#~SM -}`*,|A_Z&8̋vDmL+h9?XN!j;Loutj9ǃ1 gg1Q9$9P 6VB7%nM;KMFXę\4mLn51 +OoF{| 6&flnz}Y/קmJ捽Ad+*r8h{ L3֨ؼ%jpC9"xdIh)3T2G>D:=]t1R[Vܴ)=D$!K8 r ErK1#Z6ipktNcq3HJ{m'M>7vfK+BmA2oI`6ʵ!]IYP" gsҊՆȏ E@?k7O]#㿻~ljwy/Ķ ngH$칧C=C(#lT\(hQ :*J{z PQ2Hgb?BUǼDZl/bUZ /i9{ѷhD\,;<2ˑKQ S{nO"||1=\[Ս׳+;gTA),2Q)#i Z}|-Kb8溒W ha.D8N#AUb}Ôpb5Sll՚A#OSϻ(awJ `-i%psm^sJ(H,\Cڴ,C_'d̺'] +ߞ%LP.43+]G/w='*zKPWM zP.Ia=0_dS$RQ^kHg 3\KDZ=lN޿́Lt y%ˢ'5~Wk!AlP7W4dg7cT C>0T4ը:T/zE +!^_&(okCNԘQ7 .mE|$a1x#t q UtڨK4AUkH s +럘7u l[fWچވrbjoYX'cYYA|j`BQ*T SApcT>0:3g9^T{FpE%@&P4nq2ރ0܂ f +|B)܃/D55 oo)t6L=lX3D݊xvų6ؒ2"c%^Xt˘ߝ.7 UTb3lAT +YmOA$V^u9IA +3Z7ϕr gGgp1((Os7q3)o,!a^1mܯw( PHs۝Nݣ&"]WJm̍vD.y,?h=*Z<.`,}HSxeaIYDhV @O$r1q},#M?$Ȝv^,gg gpx. O9Z=Xp|(D4+tB*a5lS̈׫ l4ŧW$@-fs\f@?VAj{8jߌ.eg {%hE\"J#3bxͤf$W&"@q{M[X|F1_KnY4S]+j/_-eEHO7^FOh ^}\Iw0|pCmq2u3tj1}4/8͌ 9CW!9fN wagȷ0F6/]%g'$m*# +BDiH ;ާ>A`N,'JNdt2̕c~~)$ـ :H"K$+#^h+'-?43CpGIXmKk\eTu}f5ŞӶ1:yMHGF߉Q栆ޓZڈҫ|v~rt5sKD@‚̽skM"j~Ɖ1C%B$sӹ*}Q9 Ȗ &NC^u(<6bN1vx4 +Α2YŮEk̼3Ϙ7BT!lv򫆶N}Gh0'ի~Q"(C/E6ͽ ^'rs;{\δd7un/F!3Sdw<YoyJ? E>wSZQf$~tʙTWoS~X +9/Zo4>6z'F Mt]:sj!MFĄjg{gɮ>DE| +k5xu)@YRbIw8BsrY;> +7a+g %Jh1Taϥqd;2S5;5/p\h*^nd˘$―\~V#D&=m + "{Ԫ=Ak+0jV!\MaѰ.avf=_m;>d)j #Cw6G&[{ +?]y&W9O%yrQF=$YA Pu̼5ayqbW!L#{' >ܣ0Z=e?{b 9rbXlW[Ӓ![@=H:(I D?׾DL}O 0(>(<|xAëe*;OK% r q?vm][L_v ʉb@@"hUn 'Cϯ$YR@{s;,rđGG[ge,lo_pkH^9u4Nv9q15c#*{vf.:1[BD(X 57]Kǹ^T3&-K2{-{q=g|Ug4 +mGh悆|߈y&UR +*,,h7'`N՝6Ϟ!^xëipg/)zR'fy" -ccThE &16*GP vS8 5s3aDh4Ap%:7<Ģ,`T!dեPf`zz\p}즙uFP+7}VľT`L^#qj2-Ϸczڙef]@INrLdIBN‡wхvދo~8~8FRkVkUbt=)GnȮd_߮A:cgHibbi +`JKvZ}f P0sNT ""7(M:RI9zfG=SWȻJS`3i`jh~V0) 4hAW ן-R\=MRۭ- op.B&m=$ c]`wR8 +i9, +ۓ0SK>= pfXwqp{Y6PH5Fyf U2D,lY__%~`GbgؒٽIg=Lt'ϨK](#'q~+Ckn"O̿LGNgz;*c$1!n_>U9K0ZjI_Y(b%Orhr"xPjPKJ[k2\PUõ{x=Mn# dN_ɱ"N:(Γ%Y4C'jϚC:bG)HINϗvHb~rׯ򔡷$M 4AV e[ ڃ_"G0*eUܿ;@U-~C}.qfO'9y%9 :|Klt#dx믘J :?/<ľ`Q +Ji +W}vJEc38;YL:6iKd.hm,[!k=S ZUATNEf/,f'` ߋ+b> +!;XUi1^{wDPtwۀi؜%h2}~V 捽LJx,EKشw-9W'`J6%q2N;h#Q4DYё @qŀU0e+KRrD!K:Srv/'-BhjJehʑ6kbcKQؙ>Wi\Ȉ_KP|7cD"x2=1 u|"RguNSzj$6|V@le2 /7'^G^rnC!,W T;-)!K]x|:TSG4?|ʬlm2{:PX޸RoM\7[YA(Zq6q'N~rP5Ü:iZُJ~$xVQDe?O:NxJ`0MhP!vmm%B:N|\So4TT!-#jTw5>_Q*wToɟ=$x&\)&K1s[ -_aG|'~zn;]6kbQMA\DIJaו`?A١J."ֲ-=rb&j'`c)Hw1OZ#&1 Ҩ"J\絳pO3R5? +$ y.q'mzAza ߥHǁݝ >Sf̢^/#KyK/ ^r S1p$7'\x";$ѶG$z2~gN-@ +~27`L 8}HW\I֏yVtߐl^2:tӆZ5CE*.+1_5;(-2PɻB `h2hIO_q}p<ʸ +~G}wxˎ{n>T_Z1QV>%H)|/2HIR䦑G듖K$?>_AtUa(mP[I@WKg ( M\=eZdt+#t5 {ul02Fyaz1Cw\ ];?{yC. +HշmMz=XԭY񱀁$B0[_hN"7c*L}0^Wg7-$r giGL!D>H EFA?݂T/3ou}hZ&"*!f&Q4+rKy2'Lg-91rs1:D$-`INhgP$A&^v[BJ:eMx32'P? +#1!#$-)3rזvN.+ݰJs0j*BDg gͱqu-ZFjsF&[iGpfYrʠ+x-aEZ,_8hm!d+rPQ pcWVgpy v #2>~׎,EQx|1XGn-}h^&ws]˽8yoٖ{mG;<0^qn|F=Pwɳ:"Y)} Rԏ5f/ވ1W +`-t}n ppOǙl̥0qrirkR{0ߔuD#̻s%qj1Ұ"Iu I9@ʴR:@w,iѫxbq,Wܙ׷0XASop&Fi/AOH_2 Ta&F=xg()K*FlU\I"f ;}Iz*xO< +]gM0bO nmόGlee{ri@.NA**A'S[/ X5|mأziﺔܗ {6>2Y|!-,sی`ʌTl`tƑN"67iD4Iک@u~\d}%/FOWIqSH@>͇.;>*?ٶԚK +w{UfVYO +0#YtoG!bڥ&]Dh3c 'j_c $`PٹGi'B^o|Ehy(uYc(6ƈT8{5TTlHfxK:C%gr}D>cY{@p7:Q3xr<${g~Qiwۖ=I._g!rn'e~c'9q; +<9fԿiXG=)q=f، j9*eK2|FuszF`7~a +l'4/ /Moǁx" {hޫ)1n_T5+ +8*%P+ ջ#X@`ym==]D-!j*{0J疓]gmtL9oGN_mGpJRT>h|2BgGs=ś:a'9( ^1E&zdb_#Yu,rkZ>KwfKv݂8Ca])<s xG꥖uDUt7 h~ov7tמ*IxZ#y+0d`m?|35Gc=KӰdTҟqR-c۸"~/P4>v8Vɵ%~< 8"LG#ꈒ.ΆZꈔ yMC XxD + c JfZnq&ܹ~_nA\p 47&̌Y|nƚs nv 1*Zv8n.mۛ 6˔l~wsfꭳ#.Dvd]kd7pjU?,_Lbȵ|\ +g~*)R^h,*$U_wKpn戌NKU;v@k%!t] *x;j mWUOn8x-|W +qw=hnLF6_=kNڮzfQ`yTȓn*yEQS0^>f1uS^dࠇjqN?44@@9UWPDPQ?MR!?*m561 d7D]1s`,k\WF0#~8wuLWs"ffbPaX#jaɇ}/!&#z~Bϼ4$&ED%Dhn·80fh +~E1m)Zaf{\2H vz zAE}l:WjO|zF2ZN+$AKy)>Ք +uT=ƩB_]0 #2ʷ& nؗMkzH͖zG)G|/Ojۘ} L,G0cDۘLyWze)W9ӒɀzX:%~pپfۥCR? kLj )9=絔f Jn_+XbRYqA\tVc+;f:%vFn΅Þ6:˪]@iOz:x]L()t@!\-+wboZz,ah8Kn(GKw^{#G״}-~_C%KDj*8 Z홗z{# YYĜy[n@&6 PDVkLvL<2ch&P$YKUM5ג5 RhPhR'xtt&(jG6dKT1wWhg Ŵr#䩮;Ŗf4mAhINϬd겷l6{dH>Qt9_xXw<H@Œ0匲u;N1؞k&q:ɩvXy"YZ jDDl)‚IxB1E1u1BiLԭgu(_N@2zFZyH|SS4膠 +YR&==PB_+ 8:S#&l;W@Gm-KCĦ3q !mu}D?aY-8g$=3^!^yg!;S$Od07\rґ;CarGB9I`G¦jLunKz·ǰl7ONAJ^m6@&wdpF1 J}Te#oyjY M%gP{ԩ8>Ǔ> dzm",.RMlw83M)5fȻTH: ׃9K!" IIGlu6w%^A 8-tKFIZiT?5I=&EYZ>=磜x{/JLAx)jR)sdE4{qErz +|j#7_M]+яw\&P(S,VR leC%l])11Q&%a7^G:IS&sP*FX Y.$rS;2Lȇ]CP* )=Axyqgm#z{TyӚ&F8 ~T_ꤟXf#3ŰGF@R2IF)Iw\b(ѕvwu%lڄeK]vVmKk@eGn\@]4Pفڂ5#o \=34f$)00y鞵+}'RZmlOh \Gݣ.Ft`Q i$SLcCSހB8 +__f+ + ĢbdlD"ey}ð~7 L[=Ȣ,뙩8mض [* V-ljǞGcOe~+H#fOf΃ 6IG G^NL\@j )b1=hև";YO;zL!P FK<01{ 7GT'湙 Ikټs)d[/_ S`7n4`ȁPz!T Y8ix} +^D/-Mǜ[ڙ5gt1 +D|IQ\<:܊O)u a)XYzX9@%=<3]Ծ݀E8ߌouiηd# {Wkc[c U8A<="c̏!tR5arbP+<>gnd%44b^>vĞaqiF퉡Xy?^B "caDER A6};R:Y[_ žݙ LV'R3h332$6R\hDm)~pICo>A!~(2|h`F"@̴'cO%(ECb2G;1}b90YBCYt;Z׀7ϓ +. U?9WN[vUFrrZ$XQr "hKb!*=0m3% WF + e7LE=?A5K c܅g8z@; Z#᫔ +ybO 2h79Y|HzpR2T̥/t ìVF0=6} LPl*4oޫvl0ZF⨧cpk1mSn<6~JVmj;Ĉz42Š#F7%TM +]\=8?YBF˘gpݨhqmh3G܇Eٕqbr<"I$il%iaبSF "w1sF;U>o߃"Yĩ1K=ΆE<?3Hjx}斚_\QtζFd\| krBK -l(.c` WMś~I H-%hyuU$J<"c}F%KLdѶpX8"ͥ-s~(f!`FT`,uT0B\1Cv +1UR/`Pƃf~%Tq?6z0Њ{YSCSC9soumlAUsrT(uz0Q&QPInAOw=0J1#!1t/ K:/)}J#sUɗ^,);1ό+L#.d/ysUO>,#\O&Q3.:M4>zPɱ!P'e +|*54?279acrSB,DX@ulxc&>tV˷q(x^E?g~9Jd}eĄvsFenfC[cN)`.]9גyF ȯA!3/zV(;F^'0 wگO)q&O4\'n>gkTSvvcw"ޤ+)_\YD()N<%[Z4W4FWDiYʔ̜c{EGw$vk ƮC[2x8"ٷu(Yi9pRUK6j q)/ ץnGdϟEU6Ž爝awؚ?(s?jըѱ83y +H{W͂Dɋ ay+(KJo+eѭN.O@r3 +QFl"s?cr:YDƊ΅FP=ss{-0YfXNfH'V8҂yj5E92S:T/*+K&hD(݆'Qy +9oE'؟2ZېRѹ#(xgYF?fWP4+Q ?e;,"}Ǧ箊ǓB-'(FU=>]1HWhؤAx8OfϡlLE +yCm<>H>*1`N +|X^n4{8ZuS|/_9fHS< i +?)PKEv:b52BGͫeKJ%sy"pg|_# C U]K]8EWԯCcɀ>Bت-td !|Apȑ g5{lk,۴)bQ'DOa j4QOK{ZJQMw-zY_,!{{.f=kCb{~{Q05FKm7ճ_RpTZ&z:؁`pDsf涔ԋ@h}a ;mzP p=Z]sM8tIߘ>6r#Xj]^{PތW5ʇE1_h`P1KH;G~ daWK5M,hms}d_s62;b|~D[ SNu!}; @̹iD:C<j~A1n+7 +XXLFt,Ag9'2 P<^'B餘y1]SD P1N5դY^cVZ̒WK=zk8Щ<5Xwџj<zZB=߫=5H"!9Zncp0+F keꑰK%Ag[P'f A&L(:l1=jݡ=b&||yI%7h@%Rm%%9XR(fUhD )^eY@f8 g sA+:0cbK)-9hwmU#^L_|=L.AFϹDoncؾE#+Ա 9: +8>{$=+tC Nn8mn9lVteEv)(7el ((#QI#Qeb|J#b?l˜e9a_}VJ3F:b PUf%p$KgFhGd.j)kmYA^Z\9k0A`59 -m k [iT~"{= yzrE&CB7Lz(O M8ILtc0ALvV9nh-ˈZyb̋:8_3~alUS2tVdo)g.WCLSQ;ҶdH@Fk Ϋ'y 1, +:)ə^qdC{L?=h=D)V+:Vq,v/͢ d?ky$doW GzFh*Ui"o(\llE&E#9JŕW9_?IZ-"za2R6 W*ьqm.Wx;#v ?۹ZTш.W&Q<ȍOTx>'k[ދH-Wq{)Gn>QdBfCtmEO`!5?E;e2:3e)I%9%dfOk\p. w_8᫙edŠ;4>Hs}"x,VlvMNhbH z^Q9F>w>o]t5P*>n̟˜_OW_ â pہr6>2y/IPHi~6׷d-4%ZAjt6[8NJjzbOT/z:Rʊٝ<05O.f +pt\NrU;=X^vG>:,!Q81m9!ؘ@<ץ6$ms\nhv;;l5?YLf +%B&t]0ć*ycN)#<.fBEst 4򒒤xr/ca9C0OfiܼRx2 "<)~vuTXa{9_F>"FL2@[r%5# #=Ŧ */Q&:|O{+xAj=d#OyK< +?#< WV aS'?GZbٕ"BcZ>5$"~^5EzШq'fo?u݅.h9DMgD5u3J2 'v&ZuJĺ;[F)7<6IɈbv4w=C?M δ=tVu`>tϕTڠ L_{H/V\#GnU4'-It.u[9+Q(Xd},J =EHVo3A wE^}Lj7OajdzrڍGriv4]Wj@topdNeA5ou89[h?=d;U +Y @o|7Tơ +S葿 + s.}t$ݎfFZ=y<"ڡFPa8I;y QXϫ? QX*@q‰HRhP +@Vv4a"֠S{τb:1P^C-G y*!rdrWlץ.{ 2=&ǜ61= ډ@t4, )!s:iSD؎LZy%Gk&.H' =QK zƮ;!O\S{!+R3a8@)2a0u+2Nf:;ؚ/ZX9tm65& A<)•w 9+̋:jǀf*0vnv񉝏wX!Dz@vyᚩcv^r H#·XEHb'UsrZw h4V3RV`ݮ5>G\AyqzW!)p@oh_3CrMΉ0W#As=7({ޮD> ۡgSw# he[R^6?tYE L#t C`Ҽ[f[0p-e,1c<{Y8#G0GJC qV7헰G&Ҳ'p'l?@k\D]O b#!,vk?CxoڝBɀwQq rwz 3q&]$4i:28ͩ^`vH|'+`v"ܑP'xiiy3g6R>uE3w$j<\;?c%$wEO(;$Ѡk·,x!.ōnΛj/Wq6g}X!Po EAR23NTCO= {W7CF<'S<98Cm.')hx=o!h +\йaI HLkǤKג׷9{"5bץ>tɬSl{Lя;鿌5*.*Ϸ#Ŗyw-.vFL)%8BW<˱mo mwGj)&Qi/. ژ|Z t(.1(O&+*þD]˙C_NL3@u6;tALo3͛+ 8~)gu_hjK}_|3كSQ7н_D˾`]LVT盿1) <ιaD^O#~QjĖt=ط!ɑ7#V쑈QA6|9ÅLWY"K-!88@Z[|/°c9Dj5+ctHERy-LdOs +R׾J{CTfRdD@0;}WC.Ɏ& QJhh)Bm %EW7#W թp~01M}JJ#9īIuJ@_##3Rkw!eH p6ޛ^P߉Y.v }G}9_i<ǫXFN@T49`̰:r#8C{1|E':Ưҙ TFQ5ҁR#g$-.}YCv<4sR,]5d^^m\k ?R0E+Փ3P#\6ZATkj.gAڌfI$2Ktl%#ʍAkȭp:?7.dx/Z!QFG,Q'e;nwJIA:_%l"Y|yF)"B.P1#O_@ I ѓ'PÈ6&f`6N%y2Z+NynJsH ? +I4[w/6/aR%-9߁R 7k +]^}E\kT;qpaCrC4kr[U91uIYmbe]+XV)ms*[{ҊIǨRZC+Ph3O<T4T rgB"K<ě3[^W^DR23C%5`0 a䌟Jɠ\Um{z>Xa^ jBj򷄟{DPf~7C9:+qD1őD)\2$.W@ƥ>pW+>J<HOX= Ly=P@&=BT))Zl +^B&>cU/܈AU{*- +87< `]e(/v^16=i8+Ǫr. ,뜄mAskc V3÷45'!STQ)Jyg44ׄ!BjɐXM=Dǝ}db-lPolM7|y yS9sj]9 +"D g+ÂNab:t2$b5\#i3w oQ;mOPG +1ОCO'M+$ZT';}ȅJ▌J 5D-oU>Xl=qѣ[NDv֌pFdKT8oqN o^fN: #L-'zK{=~>MP +D=wR$^\p!' CJzQc,ACw/uF&U;O?8s Vj: &J#3 b2ʯEl _~nlш9ojP*(546|mȘԄՙgޜ(}1j| tn~UPb:"څ@-[4DY+ۮ6U(ʦ&4Eԗ,j!tqW:s0$j`&?K4)Sе"|ThB*~x}cb'o_]uH^(-:Q݂qBJNeWΔKO$GJsǙ˶jH0?Ȇ쯣oR߷g5c`0 QVk'\ +_+ +}K91:Tf<1Iݴı{Z!*;J:?kL+WODVn8[s4ݘH I1xcQ-8>OYG ˓o18K w0hj'6o^"ԏbJ*(uO,Ic\qoڪ$,9^֯77+>s&|U6ۂRG|^琙\{lw0{ &`oҊKECR xga\ +Ud~GI)#Xꏇ@;"kuĝBK 8QniBһ~'FA#+.gRK41Ϻ93feMbGP{[Н F\.L-R'keUJyG#_{\âaM* &~H%cf#[X7,Q>G)_OeνEĶ3k[~?߫%eģ:)ki*VQ,(nPf=@'>C6+Q_ҾU]`{nWenߓ4@{g<" + ܹyՅU_~П_@!=Dڍ^(HWAC=^C,7d݃9ץ$k:uL{ kqO_FD5IHbux3쑊h,w뱖&>F1Fõ_r6VjK>"B3dUz8wLw㴵WGMt=u/Ms4@w Lel}8zDq !W_Z %1kx%C)۝"uUhZފvgtRlE4*a/Ӈ/)eޮ#X-2_9{ՙ vFWB?jX\֠"7ISG):6Yi (e>〾E"ձBXp&5#r-qG/ y-,`EG t+II r=L#~]Oq.MpKIR_vDmDKP#ޘk:x}n$<3I&:?;@L؞A|~}>k{YvRS&.[['Ex8ơ3 88!G*xOAoD)W8;zw60s`wCgwnpp7zs7Gpd,΍40<{6OylD,$zB1Z75-Wv=~N8 ӏF#[ׯՕo +LrNK生_E}GbFX lbRsұ-pcWmte$BR @?O}/ETaݜ̗³>dh~u&yz`I4t3kEC1=X׋9}ե :լf@Ac1CN06`*~U}xɹ`y3 ז_}h!^j5@O|*C 6}{b*COet\Av̢> !*-gOB( ; Z̵H*IKQ^̫&lWgԜ~~w7c4]3[/}Mm/-~yd),Sm>VLSxؔ2"-3WD$L6r˷ezfvi,MLB4q +C+D"޻I+zm;|NeRx%6Xò29i詯)3R~"ڔ|-K1}i('2S!-VIX_x:yl`A@wR*9B!?i)kSWлLZ Cd{AͳmYE:EO{1ƾ>a%R[ y8Yrt:.3 +=ٕ}Y7vԻh :>HwYAʋfԶ-4|SQ[X|9 +Eӧ 2'Y.w@ABZ/~0bQ_|:٫: ( Y"}tGOA.-;щ-3#啇]HM@&߷:<wߤx']@+ wHϫ*PGo( :15>c@D'`nԁ +/y%^Af'qT;gFtYp$+Bhi +!8B#R!CbmKܕ!)ޚG8 -[f@'$2S?3nOm% b ?y|_v|ŜZcFG|/mpPzߺСPoOtehM+th4Ȩ=yڮ'(ˁnDv+jVgx gi䛄=+n8K[9{/ 5U*;0*t028!!_--Ļ5xi?>i;}{Տ8l/P$;w *&'e9)&ds +]!oY\*rV>]ل{mG AൾV\Fe +N(l! u P'aSwOWp~cxr(@ "sU?(**'`4By^x; AEE7խA3b(L;U0-x{+B7$*@KD +IxZJEC%~%m[L?^<^,bV0dI`&sڥf0. |Kv=[׋6U|uQBˋwY])BWj1ETcw9 2jAh9 z&[ PRaIEb8$@ΰ'1!ϐʹI?5 a6dwԢx 3!$gKQiP&LH?Tzw)u +,g7(Qj[t~ckzGD`tgb$C},^LCC**A8J6|R\iζ~+dA _/@60Q42`Im!,GQjOM[lZ=gp]ero˜ŁJMtn^Iue̲r"|C`A< v7\a'9z=Ԝ1n+K!l܉(!+Y1^u鞢?o|p !QRQ!򬾣-wCOv;r}"LlW?G_m+GS`Ԯ],!0gS 3Eځ`u^zPI}i& ݀wxҭ +S"p pBXcDlC[3$tQ(3D^tDybG`vX``)w6d;V\?VHTx*G~h0"c +~Q\/WxqN}&GL dEs7ZVZ(dUzbV;huϚHewY ՟ TiKcI(K{bR5=סּhI=Hٺ_ȓ/2^i;oSt˥P2(ւ.Elub}p<'gcB#!e菱v/BD4 s5.= iˌzhQuT{sg4%=~_1B6Xe9C8UX8*l[817BH{Exb?(1 < R +KC[8QYx rGYМ最њ𖻽"[Dh%be 3:`z p然C~̡?:^s!r%kVaFT)YʥU Lno,Y ?"VE[rF69]Zkms%'ȥ҂5 *9@]v +\9ݠ;chdg8!TrWvsAKTm5J.]33J&Gp$l,#(d-Qޅx7|ݹrz[!^e*`  𱕱3z5{)`q;iO8X!o-\ {rof{cuE+8uCxzh&$L[A&ξj5g]HרS=k֊!r ڳAnw HYkP&` +aEX>J̀2],d ?Sti ϴN@9?o(}k~jo2jSdA* RC  0&qn[ɑRH:Tb% _ݕ|@r;"ڄSȢ {ae,%uBvupaot4"9i!b׉ D!G2Oۈlphĵٺfi͆w1x~ lҰ-Qi !ԟ7W%]tds,B܌$#2c3. +^<bx򦜡靳N_>8~dNtm\OW3D{]$k|>bZ@rZZ,23)Ft +o=Yf"qN7h@]C9t Nhø-mlhf;~0Gh btx!nx3hy>EEsNgǫ=XWp* 󛆲msC@.κ#GfG\u!y(5\3WgCY +G DK^_\2c<{l{=i=oǍ鵟;#.'MFUWBwD,ٛk&CKhw|,i`SKŅ'!|tHN;4m33XwvUba¬Z/B:?2Ŗº Zۗ+vYw?֥4y<-#t^r(QHM} +sՑt +鰉 E5\LypCRo4'v$_~T#ٚϲu+nBYM3EM@^P|\jD2P[Ǯ0 _xFbn,ĺ/woxq:Oz&#W"AtG$R+Oq?1߁G]znDΛʭǦ٬|@h3e]g#ޮH8Nf$ +8R!WBBBKOlh;!M4Í$#oo*ϲd<e>bM9h;FZ#lM(.3(3e]t[UNT*Oq-ō7 TC 3VJp;KE>;@AK~?͗S^lHg~rrpbjQ˴KB)xoIDERߤ9A& D;5խ `L;q3(uQ$./3'_*(I1Ǘrj,҇RXW;DRO|yoqY,~ǜLvD.N{O)js1v`٥:Cg^zUnѾKS,XL{ɇרҁ>QZ^U nPGm"{uv RW1%4ψU'mZlC's"6 +qM`u +R +Ff Wi*Ζ͑HyneEs ح1n9(4MB4]\2rܓM_RDn-;BWNړN\ ꏬbzS=+ 0jkUrV IsW{R-lYmݥSb!3*#sv(.J,EbJAbY`m}(9+ϙF>?n.O>> QڎG:5 憤2:k:cC^?%`ONlzH4"! 0ԣM- + gI)O1 1o(c+Xw?`iE\5Ȧ#.OZDsc]4GÌYΐ~e4Aj~i=C(l/ +Ef1{/%W49Zj"0K^lC>D"Jo <0(lqR*+JMcp# | MIC&I![xH`+ Ԯj[3uFFb~S dGTONf )ZlClVW|@jtwNZiQ D%hYYq&}%cjo̚5opCy2d +4ŞG@k~&NgbN^&^߷/fQKFdxkoSTIw3X+c+3yPb2,Ҋs#v ƃ|ϖ|ӑ 9*|0;<9h2"-/N@+ :y%&^ҘI1r’7lxSG䋗bÃQ1RNΫ`%L`IJJ,+戣jܬ)s[k@RaQ:ЅCDjtj +QVJmyp{m# kǿxl~6zNm-ᣂg?"OF6-Fu}-Qҏ:RAȭG+dY&[@0JR5͕y##)S)6@ޠsas +,jt^J ŕDwKt{$Ahr ܅E~p}RB斦Tnt]UQ7U"Ξ@P5dD^M1mxGG%J)C\ۉOj?p*xauĢ!pC!ňЫ.eɬwf $وaEH_Ji#4EWdT!wكc4ǡՇVz_}heڎALBpZ&M [̱*`g@!^y*H6{`yH~he"=&-s#kG14sCU)WHO\m_@zgŮ"6sq+fQ"[8-W`bt6犙g2jFm? H< R:"cty9,UX\l4>rmzB+ O^ TAQ2O[0MP"hvY/½R{M2½_ +?AFLl>sP@iQoX#W3C1 C(q?mٖ3"x2}Ab-$z|BA +UC`4y0| &`_TH$dؖq &zN]#y^(2߶gncɆ)4ܕ1E>0J2L%apʩ(Pc>gR=Y@DfgI@nʸƷ@F!BՖAxEl>f1 ~O =חCȈu'ng4Ԅ5O +P-2$T ,{M/e8މ]cdž2Yij>VGc5O.O>}hy1 ۣgYKt;*ނD +'p\cR\QK.0a +$_CxlkͨGK2VPz = +]{5h192>j J7W+M~9סc<NUz$ jy8ȀNǢ[Ju)O#*0ŇV#wjFIn$}ckwGr$QмߴSo%Q#JwiGШy̸LQk患5䈚!X:29>?F[6n!/tG +Q5Ν{?Z'68kS%^]2? +r~F^= j 'fL@л "TND'+:#VW;+Q}"dRJZdr=kyľt/O6_JD3 C *3ڝk/Y뿂%B +b:39 R l u$jQ$f%CvB-u%yXRz旇#/l"4u*;hOP18%Js +z +D:]#5RP1B'(G\zSc| 6b۹ih zVUw"VvEӷw&X%4F5Mf#QfJd;Y~y Ӿo 8iM^31dN&*mg"1X<-("C'#%!'iȋˉ7^MC;0 .P*$ OQTH&9xӃ$AGBGCsibթԡ!vO/ ۢWEHɛN U>𦻊bæɵbf++ +\H31j> +`c \bR +aS>Bq2ϒz0J䌍+W}} - :V-Ës)朞[BG M4~"gi@7rRuBݣ*U#UzHݾWd9 `*ε:w*V͟dMHk:"$TH΢iǧMvVbkLrNS5D -R;q=9;~6=`riS2<.v2pdȁ"34dKq6&曯k +#}~12G ~z_ѹT;\غ>s%lc=3^gnSw\S8kU '3_JV ӥ^G)'bnnUd9?|W=~:x_G'BAT_ d)1,*h`Cd2j4+O}עlѯ=FJ{,MxO +< 8t_@&lɋ2,Kݦ qJ; +iVa4؛`>c>yҕP>GgKJ,`Ϗ1n!lvy"n[L6k*ܥL<PI.]blZڱy^Gw}0T-3Ҡ:_rG]#܌Sy0FCpl)wآ_3̢ +~,!5~";B*wL<o] F0Q]-KbVW<# Y\I[.4}53'x?iK\BR=m0%RFQk+VZB]" ve9X?ှv*[<^,L2QZSwB 2ݟ,dPplnd^ (KOs3;_ ׷Xk( 9'nH<6Wx(R-WGn A}f0& ~YYD=+>8Wh[+mn[ݻ'pׯpC pzb@z]mdץk̄-#xT +A!A+{{Ȁ\u)oBh.J%TF2gSKQ~z7}3$';b6m;` KH{-S,y-;&b!#9nݹ.eoLo@bŶ3?IC:kJ5s甊Gq.=0K=_HIΘ-Jnٶ+ϡ۫2ǐ߀@dʂogY "tD {[冡w&%[Mw3E:dp5u>T,| Syod:1YvԆSIMy.v(I_e;ϋ+s/?eƛU{Qxr|7OZެ^+vD+C{-TփLbqPMi_ݿL7D9$Ȝ,e.nHj*?q~Od͂;|ǥ7m o-=͙ n=ϒD&Lo; +BmJZXX̥Č#]j{ 槌:zuV1ʐ=G$ uZgcyVjVÃS[SG|mE_Sa´9D9 +LVKA;n9vEpk +;pa~MF'\K?ۤ,=z(Vܣw^}J MҝS&|@j To7ğ,NOlI(04-cQ!gˤϼe1jrB+H{?$-턮)ÁS''9Efq$1vf~#"jܽJ:JZ3z;N`5qhFQ יa@;Jjj! 5# Α2]}Ch8 ̏ثOJ{+ <mQZ;T)MZ,O8BŸ PL+eQaΰ?@Czyvv +Ş#u$Us)Yf a:Ər#ff{!Ͻ63ڹHI đͅL7 J/5;1'vSOKw HO`D;,fsrCޜ@޼8Aʓ|uXx;BTr[ErۘR5qyF'>*nS]K*/f@ UKKG*@o)9L&#< Z^X)h?1$\v9V4:orO;•#|/Хh3r1`}TC>"s ]GN{tN4䢶z=3dϱ~GoXͧ{* *7# 5d5['0la`@2`1,-R@s°\A킖'i4lQ郤o/[Hq_t[sUWYR쳣$QZXwzDG^U52W zH$vɪKZ!:҇ x$Aկ! H^>HGKxExBI[>U ?DAGlYQ. [rԤқЀ%ehNeqve//mb2jٞp0UHHŶx. K~Db=ƧmN˯ƫ|c!Q-#K@XGTw~ghSk`5[_f wW^K}toKR˳DÜL*rU9یd!n㧣Ţ7Gn .c](1IГ:t#@PgmIo^1 lV](@Cĵ~ΩFax '[|]XWw+&&~a1l[@J@ -?^K8Ҝ-~A>wJ~ym̽BF݅(o?zmErHΐ'(+>stream +fYeEajpNHJgRXp( ?Vԕ,|e#_@rUd!Ƴ՜7SFX(q +3*yin@C6GU=|ϻ+c "YjKygKfm~ADhSϻ߬%JEB%MlK`0J_!/IMLRLtcyVK_h$ Kp̕v;Sb7s6eؼ+sӕtvZvP!0Ek !ٍ|A Ibsez)+/(hN[evSM^:nqHQr Rw< * &cJu|Q\:w섀Bg:an ZuH<v:"KqOd .[1;^퓝=DR/9CS p#. +.qZl 8v zJ3jxK YqYTP4|7({Vl%W&S2Mg]iqAـ+YF=>dUXr9cUyYhQu(^HrW[31<2ٲ?ЁN3jo|Nt;rix'AKD>3{vu}8I DwqM) kMdjEwSa D%XFYAk.wA#jE @"KJ[&=>Y1M^С/Z՟-%,idC +E4(ښOZ^ +\`[ŪW%Bו (ydj3qj HqFqWumi~~6 _#Av vd!*VU}˲!ɫoڃK0K'g*q)^$70WmaC*>"6]ϛp0e}|fZTMblr}:S0;{o'-"ilLl `W'~SKl*im؛p*cJ-( 8j/əwL^|RBX +!y=5+S58ڠrD:G?TĥDaI :Hyl5e LImw2:fˊtN++tYўr6C0uڽ!hDX<1 +W?3̐*2{ZJsIqKmIٞ]2ܦX] ͩj|<ҬQ=e/kr<ճ$WU,PxPk1'x^WD7l82# U_#뭔ABx[ +ظs6&%*W݃4BRT:J-#э}vj8+";uT\MW3"I#!%֟X9,93y`E\ ιzI0{_juιJ -Awԭn +k.xU.hiDAQxiJZ˔3IaކR\w9GN7;Otc Dc:w ޱ)Ci:Lsm_o'sPjo)BED:SBȏ)P/rK 8 Z@Q$U\ɛZjA~GonWo1 i[_%cJ'nfLb:t+d1DW4Y!*("|jGܬC-o'MՇ#=vˣFa B Vt.d-*E +tK4 0$+ o7}R`9BD1PJ{Hg@V ԋ+N#;_Bi7FO:ˇ 1@#`ĺxŦ i +~Gx 9E}*2G'jfv0Jkq*;&5!oxK\"ֳؗ E8{_+5U/inq dƉ;s_ޒKUDvΌ=ȼ%X12Eh%;܂!t%Q3&?)# ucT_.52E2GO0ȲrRpDi<*J"R,z/KS ˌG?ARa7Č8#g=-Ѭo3~s;[HOvNJ&H;xץMw Paf~Ko;dU*r tO[ S6A : z'S$(*4&7DN?Nu}uvHud& ÊQ?Ȼf,DP \uָӑdWMe`^mR6)Uy\150Eiԋ9ufxnTy; U\2)m8MqӀT.5@h9PyI +T=_<:e51[ +f68NX[puj[D{mz9Y'vM}-zZ NN'Ԁklb( 3mf[Z!?u ݔ}Xha_tJ:C<8jP;2.@QU[Iй?E7t>^Kt}Tc{!*aWXSe?cD&C?Fv8i\wq#8|-M`5 3] +HhOg5! 8NVy3J28Qƫ9w0ҲծQ~}gK;ݦJC>@Y}۫A -cB kvPx^p@s"S#̘+̙zC?cu tT3 E!\okK 9* dv90ˌ'hԵQH3ԝZ Io LHY +~5ylO% +|W/`~`m?z |`Ǎ9xh#]r?Qwd̊%^ك_|KĖ[t6818}ťӢS|jQN@W94=4fNϢ4 ؜*]oG#yW,%ai~.U.x{2Pq/QvhQٲӜXliZmsKB3R^? ʭQfl̕~pAhX0ݑ3m d8>w3QL'i_X'y!D(U#Bds^c7|CY/ +q RDҔNi + ǰã(!tV:|ZszgiI3TB4Gqؼp@ =eO3ז&8i6TrtD讼6gd0 vv⒝gȥ;+ B @/ +,ږ-;'M]y([&KlAjD䓊`rݬR;Iq<~U9<&T4x|qp^ia)V!b;_!nډwϸrp8$b3D&v^t^R=H֖ ;SosUcTN^9ʆE,Bu* Cq ]s¯1?nK%Rr9*"Ͱ +#Lj CK@}yi$3J{kzYTAst\^#?^P srEzS+L+:;%)0߇mڪ8V9-e} t"EPRUn0]vJn3^H+Cڄヘ2A5Z$"~#bLWC&UO03mV5jBDi{C~OvM2b "3}Uzya5DC>\a/2t~WX~P^cFjzNx}Mi]@'Ѿı*m_ TP] S2iR|6/h7 ]?W6A%MAU.!^ qbj~S͞Wbj"0Tl#;O<wꄪu\ !SCك1V҄@mpr q묘,r[(*SlAqa/z esʥ~* ySR;v;QC1d*[ƭOqyY+<{կ͢kIKԙnQcw7N'UNVOZg[3؊2"q? *>|Axp;P* \2X/@S֟`Xs~ש_IFPh}mIG>ѹ N}!4]z(ml~?'{u%,~&[syq-<=K ,[*(^~W3L?`,Fq))# ZB;|Ug|v ‘MTRC.S@edz [378 ж]1]ĕn(82E8?R1l]ʮ%Rv@U{|FJhΨ ó$csVٰςH~KZKO[5!GcEsbW~Q?&\m)W$.`3PNB`G' D1f9%ƣ ~[m]{}@^_?u)K^V`LVQQ9(XA1} j73-"~'6ȷ$I\彞t,}';ahC玮Z;Z至VL{ d~AΆ7 tVPš+&g Rg5輓)>̄; -;~)zgRP% תּ37r'ޮMZ3봐l&)g3jzq]{_xVՒ9m>sͨv +r>{'me~:ELJ.wky: W]t>?*[q ؁Kg HbZ=F4 4d+=ӹ(|NN\^\7_ uU$ROa(A1|^ϕk+!cپX[ !=\$: +AB˼2eDSXk)/NQӠ!jQ !dp [N߅bb|@JЯ1qPaAS4a +P(3V;G%:h #І[Ksi,݄vcgZp5 xu[RPڰeHEkHe#bwmv*])&KSAe& KI8jxg̞ʰ&I \P=fǡsӚ>L*o^ +1v :tg\ @\O2HI5{DBE 4UۖOhky9W +!p3n]^}-rG\9Խ!,iw-T1J?#b)polkQ#Ls0O_ ]D{sz +ꪊ=:nrWZΩ}r:.W=MkqEs'!"}G G=8F4`_r&3ˢf;SrC-r"#%*j7c^ bgX#y v C{EyT &IY?C3~HbG:_`+#%d&.) v',Iq:C nբ2ɗcS5;;&ب]]6o xO!vlC0\I#S~B1RnϝbhąV\aY=cNO9ؑlw쁯5 `?Z)'i82<0h'EW2sؗvíƉNM +fw,1CNSjH_u&BpzYJQ60`zDZK3x I:݉tBGڬR:,U4ZK;?BХ"*EWbNGɫc?EjB`fԵPyg3AJ "UBe< ^|wfh^GeK^!1:;l<1;Q[itR҇SWG}Gji1;ec?G;b %(k7#O%+p=f9ǐv8%ԛsCK^IZ@xsM_9p)s?Ü,.J:>sΫ8aN-[Fyemh8uP5Dwx]k^Jo0sBt2({`sv8Ћ-rTwDw[|F2y>.+Jg+A}NX\}d*cc(4Z zq= @y{]\*NCvP]{DA"$60vZ%512(j=|1+Y UC@Gb㬂dd)'Jp!nER +[t;s0#Քne!\*$]Qe+bR{K ;59W/+CLvE8 \}2#oHf5d)ydD *Jh3х=:刹KЈ~$_O}3WMK2$F47#CwpK 73Y1t u\"|-˴PODZg Oa۟Z8G &'{9ߌmM-g"?ua6bRJCn؛ֲQpϠD1">%3ٵ GaϞ(>ϝЁ~&۸)vC͉FRJ䦈wªcq!OЎ"xίy0ɽsfeno.uӥ M#Yu@q):vLtb`d}CRzһy]oVq!Vs._s//C2x*]̎sPes58vvqY'pIxDwƁf8Qk;{yL:nEy&Jy65wN8K>.{pP#?a6Q&v.%WFA~ /"L}Đd+. )V@ ۂq-L3eЉT#Ci"l[4>dXϤr +8rLu(u{6]SjV%C =aǡSv70$JoSHbz\[\ރI.|l{b.h-6i+EX>Fu)B;POI02NWf/S{Y!$f&/Ir +>Ҟ#=brϷg/~/ }/vnj$#:9Ĉc-vt_ښxJ3!cjM5y}@(w û@sx<3A};0DP4)牲۳\,!"Pj-rWMYf%DĚZ>0cbcJܴxTGu>i/(w| ќ,(S^C_3!GE'z>udOc^VpAYA&s:eAļH,`M)m{R6#ڽ_Jfw0s=5wċ"2$^FvtWXC%˩ +-9Y[2n)z`iO%F=zL1-oooQf5c&~շI +S{Pi6x7jwA9S`"Ƭn~ZIl|Ai#6=^VpȋsaP^49;6 **!2DϋͲKhaR8_(\yQ#^늗,Q'v"OyV|.j;Ja= +&ِdꅫgE!dzKBж˗\QGތ>rܡܣ/ظ̹vJ~]ƝB-vշ;bNO 1 +6H8LwaYZsZیzlKL-n>J`.挞"ai 7,rsnRnJn T*ڡm'cd52R•mQ=0yo4+AH=9yULjvB[,,N)ageIbhjsX O]ʲ>ӇDW\ KuWHS@~;%s{uWI ٳ;ۃ`lm0bB" rU}UO.{eS^3%gq]GncL(c9̩ogl|p;QFG coe= GFib%ZOKžwi{sI,bvscQ ;Zd߸C!DjynVb˱?HNʼnT,txɬc;A\`"P띒 bϸ+ s2Yb )@瓂QI( +9R؜xZYnQ)tI/sqnVcLSyX.{w@l]ѿSiu Okϯ蚨#ϯ1c9Dg4)yJrw-G޲P #IAZRo3TR~'yB֟?Q;a Vp+f' ?Bԯ!sЮځC=ƘLW<ʲۦNfH<զ$_M\Q )O$ЩBa0ޣ]5! cI5h +3iDpO(|-5QD2{JE0ξI(*j?5y35(W. YU)~O"q꧀ΰU%<VSWLh35MZ?Z9 +hD<}Vduro+YCDSxPK`@n]F jQo@Ky?x<=ip"6)fىr-ˋD?L⿸7"ި0d-gPRvvOĬ[=Q2[C+jvx]N iB{T E'{1 -[K_^7snK&Gs.^U6B8#cPguaz*G]ᚣz+č6٠gA+3I1x]J -jV';iU9 )BpKN܉ GG\5wd-vXMN'+35ph驲%V[uS<|K +W +y¹nkw˂l| b"[=) :blK<'O ~o1_\v:bNgW δLe9y۸5k?]/@˓}2_.`k L%\5ihv$;3SÈܿM"?A5_ESQVsoܫp^!.$,A9 +:H5IG4.)хƅ9p3?}{ /G !U.tK{#R^~U4TPDF!1TRkg7f}0ZkJrV707j+gꂢnWjZn:O8!?yO}vT3;Ӣ-$&'gY+e-NjI|yLߕF5i_a V9B7$ʮ#xnts"F`pHqR +H7\C]=%˜15W=o%]b[E4Dj\p|=oC'bIz`{[p"3ŸG|>kHׯ#A˝Z&3@ɎY"*h=f>@HIR'}j %ы'J !FW[|A/PҶhH&d;q}%$%,ylva^)g#r1-gP5^SNzY4C"C"0kHcFV'/h=ة!_wBv>|36͹_T\!wx>W Sls@"3 +N,d&qO9'_ U 7"Ja/e?uNW^K[V4 6qr$56aU*GeK)ACﳚ(cN/B˝Cn IҐ|}j:vnĶ He =9"&1 "PYaJ( ొ !p:}Z%tN||*SI)(ft$ە4oZYy$zHcw^ۢ%gT#R`iWa>/+_YjcI}b-9ܙd/r1qqH1밥]mo5Q[ Do156?=ő8ja8 +mj(34̿=I*iP\ϭ8W KUJN?m1<`cvj/BjWRVC\>RW6nK;vɼkDټ^I{b_x mpq I* Ʃd*y%/ylew4Ba2 Udf7׏?w48kd\}ڂ#|Tȩ@n=>`$qɐb:"1rPQ[$bOAicaY2 +ߤg^SvAi +m +J,5l[.pA#(c`/T!k1}2iBse^@f8lgMCX*POH(0n jUGR׈d])d[UN 3>W7+)PEM-}Z 7SC?/q8} /ߍC=7ETȬ9W2D7De֕NvpjD4yM'Ѭ8I: +~kzps8PۢUe7mOHNʫ +Js%<2cRRfTSA{XioNw۽G +tu볈@>+1[.;y΃;ŀ=(U5A)`|5 Qηk򶠂=48WSd+hԕmFHsS0XYP73qz)0&y)}-D:+!(H'潭5U wԠP:Ϻ.6uZw*]nLF тGΦ @SZ 8Xmi/ح܋5ൟ߅qz] ;E| %IKpkj2B S楞R#q!9 ЛЈoٯoe}#l;ۤFX7d"ov=_s/XYX$7'U;:5e}9uWQ!Hf,0&V/oCE(̟Ct^V *тeUu S;GǷ}Δ>wם8XB#95"9H)~6jsJqw>ji'~O]$;y.3M: ^gp'ZE\ * 8"> rTwgP)5",Aɍ$N֛ *c˘݉37| +܋b<@>Cai`Uz}(DŽ? w1|@3 n {w'//bs%bc8 ՔHmAx+,݈W)W=և-G9'4 |=Ą!::YT:($U$:Jj+3*i+w D>bgwjѝ;8qMI HuF>s -WP\!lH}zty+=r^3#8 &;Q8)R`yF餵^<OUTּSp?RP;=2ǦG2}UtS K7ٌm{RljXaͭqnFBKL?m`I-T9 Gy$*Wu][hP+~µ83|8-@Viui o Ȝ{HuC?i 2SJ[ ȦVB9䠒y5:$2'RfV#\au6h*Kh@K5#7~^Ʉ,k#qxG;a6^O|> +¡h4ӦIUV@ⓧFwEoN>Ǽ.2Ab`7n +]G dG( !fȸq#!њNRx&K@"@5w2-N,"AsaNœ\@dM%-,Q'HTn{r4d#䢋;OuPX(zcm'R%%qDK +Ac>y-F$++刃QQ;P+?&D f +W}F~AhUn)(FB45S԰HĴz{t{a⺥vEDžcy!,P],0I!NF=[ļCf l-h|yDގWp߄Kg6zdܹ@;,`ck\QNKB8&zYguVߑGUM ݹWWB7( w朾L򛹧ZƜZM*C1UpP˾XUp7Kt @7cl2zW(3Ǔie=^%gPK"XyyJ4Tc/o\ N"M#x|k;e"p}XrڝV)C?Nn"@<ql-jEjAgCqG}5Sіײ=y Ό7RZ-WBZpՙ&JJzcuީ̂ `;?'`3k8(#E߷HOsSImiBZ*ƤU=4t-舝g0 ̙8PKK1dqKHؠx38@Er<˛O5ٯqܮ̒ YnL7c Z +^\it?݈TH(]=KG[qݢw`~Wy3g=ֺ@hbXjXQ2@;6YXd#X͈9`U7 q;0?!U%Pdnz +PG.Ss)* +Q$H{>\)]hDF Ž guyAܱ{7O 쾲J,V%ֈʈ8qĬȹUAwoAۧ>Ɏ)!t Wfݞa;="9/(G~Uj;/jSx_77"3!_֒ȯLiW~PR.`1n#}ɾf\ofƝWǹw _Q7X;7ؚRDZܱ,X+.%Do]@fl^ _.Ao{QTek1tC)]ZBڈ5C&(ذɢ6igR$7|HjZd*Bc+EAk.y\yK;H8kD8' +ŵ0Hu?C8- +q kb*i_kÙ.սb}~JK j (&5gH#~ג @# "v?JxꃂkBOԉfSv1uOlA iʧ¢J3+*&O>WԶ +|P!AoΧ}=5Z2̉2F. +Q W%1awB]4֕(PW-RpύI%UH4'utȴܚ\cyVM48="b1#ǽ%=C4Ϻ it ط@ 5|PEqXkmg!ĸ/hXS?#՟g/Hf&= +[$wΔtC䛸׾FfjHd-+<#D4+u]*Q!#u{ r3;ǯʚr{V /'\5k(,b` U~D]lzFLVJDaXHŅwrnPAFDgz45D0&T C^t"\eE5f35nR.U1+~ +&ssQI0eI_ORfz4M 3N3d,bkEW%CcO&8l!)piڵqUFp*:jP/TzvyI`7N@^ybqO{'Zkdu+Ɖ3BKRtOXݯ߉?\CjCCb!MMJgE׺Kxw\;vo'3j{Ѫ6¼1n aզI6<0Ȅ[Zbtb֪+Zt-Q=f[TZJ].6݄9i鈩U-j +#+=\T"Q)Rz[Y;ɑi@ >FSݗ?-T,H)gP4w!'d)zXj+lm;R5%~Oe ϴZ{DYa/Y"i~yrD^1%Re85Tp?eAp}`o]RdE%voq`do]IOW˭՘jծ(bl9-y+<;C3]R!QPxRLlAРOs7jt68[ m=@hwX +UET ߆.&sE2#)8EfkЁ\c&ih$b쌒%Ğ[^Cփ=X9VWl/M`nK! wwdx{ʙnD8W0\FĕZE ،9s<Z16]XB 2o3V=^ύYER;ӞYQ+Ó#h>;EvnIX[>8O\"D_a0XJUr^qPynIv*}ͥ} +ܹT\{S;[I{ےp ~~/(9A]([#qNW*]WXW܈'w q r17b+Ʌ^j[s=,K:jx%ÔBR +r1jkHyy)x}L`BK1D,Y7[=5%"sSQh^,`"c N}mԐ3rd¨Av!$6 7D+=M=|-1(teB8v a <~0Qwsv&j+CsFDS{:C{Rh q{ӱT@1)fVfl''2U!\PO>IhɆFEzKC0-1( )qbPELZh?I8bB_7IXFݭvz8C҂sFBY90ҌڦheעBF먅WD[Z)}o\AvlolW:7;DJO}G#<=zWo_evON"3Wj\9 ՅcQie>pP +-rޅV.|K SߓsgaE Y© +37|N;gkT3V>KBheyz}^+yr#Ȃ&WT<@Νέ@sIQurZ9+Τ૬ө +9rZ34_E^/3n1B1oOUo$[_?Dm]IJ$7\]_ }ҹZҠWMb4-h :b:U̔玶2gRIB"'!Ra N9_4h^ʵE~uIzm 6$V^JS](!퍽OE&m.lnu0i:ܹ~gsXDGFs=x^doKbjO=0@tNu(iFbP:BB =fGp>T':L!n' +5D]$9Klz+GEi<6e(jGIx;xbDdYjҢQߖA$9";דajs˷&5$9{ ;h wӼץ:6SRp)N;'S̨I]ehndjʹn%E=L#E@w"dmMf ]vUtAYtuj͙3(9 W߼UC븢+k'#BFzx.S¥dy3,pבv=R!CȠt쯼c֐+g嬻MYsn s+FFO~gpq`Ε'5y_"aҡ&}}!m{t+BbrH}q^tF`?w\ 4m8[兹[ +5yĎusQSc;g1,.i_n4e~RH)櫗(!^*!T=.l@G zj] L$ahnE$G C\.Ri\bBtΘ,YRc*A-%dzv}BBly%~fm7w9zr%CH ʜ˨˶oqE6E |䅧"t10G;" ;u09ƞ^ڵt ֌3vhi2x'wxfN_c~c Hh5>WǚHp33>3u!LOu7+ݎTPGY8)-x&Bg2CTgvcmy:R|hʜUu(Gq:vL7ܙ,dK^}P~AXbk?&!gSU6 U;/ට M)]Y}ϺԌ?] ^O11cds=CǿdtS/r7aU25M=tuө)]7[u(SO3p36C_iy3Ϋx>۝G>J֠kx +ːL=.[e56ෘp?%nxn2f=\/ j+-W8+N@滚KZFYA L3ҊmP9FݶiAǟrUكP|s8o=,(Z([8;us>]pGJt6)Wk\c|78f|yi^"߰] %KېRҙ`hxJ2ţd~A[V6&c`6(xCV.!hV#=(F?HMB)"p M^ dGc~&Ig)R.yчm˫\fC'D-mA9]f;d=eߊ9;NApiԥ>冁O9#2W'T"Po{U o7J?FQHukΟIq%O:Mw=!0EN5gM],ޟіuǒv Ԗ~q + U&ˠݧ Fw[p~}m9'\H B9QaN R3N&} +1fU=5[$ ?<˫Gf%Wt=T?PJkv>O O*"fE$KK匮C$s{G- +9ڊnQw\dzk9JiÐdTP@ SMgM)5X_iϴ$f'W#R{tR4߂}D{qyAJ[sr&[{G͂."?ȹnRBYUsHm~{ot78B`bݭk< oJ$-';H&(˗YOr0}(Ƭa^ m0+w'b~s6e'F*z0reczҕgfkΑ>ć]k|VB~RR 7'n^#׊T4̉_CbNDN-x}]j8wھُ([dU6=#uvRv3[S.\H< 3؊K_"$;V~(_PV) Jܗyãu?Jp͜{LowB*q +c{4gXیBڷ|O:9&-q b(IzpU_R>#b' FqApomYMWmrK(xcn&*%QD瀇1ECL>3%=[a\?W>+;;An~G`ڋp7eQ'ҊR}ƁVD̃kȑ5C3m-n1 ǖmfY!okM7u`QOD̖8wOϙ'\uUd={i 96Zp^a.{%O=`(ׇ9g_5nAߙ ~t1v A3)d*gd*%|2zIRPBܜW}W6`8 +c~&3׾:itC|a p!GVZ;XCkUy e{yҁ\Ai٧%+8-'(r4Jȃ0U$$N(DG yb< hD2 {kȑ)$}{E9H)Ag5I`fR`\T 'wEվHGt>; YK+jeosl}~1߃*$8oZାVVokfz$; ލQt-!uW&e*0zbUPLٴX~&푵_Fa}n;j z.Pxk7'x]\g]iAOԾuV.j/'ZzLn'_|)!,L jUYy; PHLToq;AiS,2-Q +-148C'>fΨ-ʙ!2EU?zjVDVo +jVND T|C_hc.Q 8.aWb99[*F2!z&l&|ܶH8S!g HZ1p稟9lU.(+<֧@QN@m:>#$y/VWt}zsȑcBagܲ\яX't) ՒTphugLC3x*W֐xoq ϬCA{{[gh&G RZ5v7=t"w?oDۆGVcPXzaC#!7+QHiQ)oVoڤU-RdC5jhx!@#OQm-5 M<4z ͈{BJ 7w Cz/v j$&5D*CjEӖrKAa=Z\1#%N^h-JA"b ]AzZ8-#FE'4=C57WVm)y*tt3Z8"D):g(j3!Ⱦ +; [*h!&`/krBpFR x:;Pb`1JD%%aVg{h6gJ4bg!Jsѳ/ֹm TN3D''jkVv0d`DVCcI-pƶ!S٫~]G-%ke0ydaCw;QNѪch8BJ[/.عIEԷn!~G:m0mG/)lص|5 2sGrkՋl;Dꓸhɉp]ȥK^,M7X[roZkk͛卾y [-~/CS*lx5W؉>UCϸurp딗=FP : /;+YOqSN /g^hv Ėv<=w_5(6≂jHfVDmۋ4Aڈ$oD5yr̓bpAW U 8) +8feve3=O +*Ac) CҐǰZ f{D ]Q8(+i Iscaq .# {EEQ$SV1$=wѸ#Vxptz7j=4la3m7|eO +CWO? !SD Sj(Wr-mҷo?X~#-ŵe.m˯_kcFʮI+iL]\"^el.ʼ#m`J"KAP#LLWNcLQوCRW RC,ىH'2t:Rt DH#hH]j.4R2 qۮJ:*RfΨ N8LHm+ƽ$|#~W;(woZd"%dv]bbHH®KAu?![z4uĚj_&8 K]_ۛQKܽ`iE苏aqDM]MϒydrR<KGmnK{ղNZ"njH? 5_o +yx+F\jG3ƵIt`TtClzCΛy~9 +9^Wfx9b!I^(5hhk*_I8?(7S ?83"^eJ F-lP 2!bU?Ud +|ǥU_#Vp%z-*́]Ė蜿[GWkqbx赌H [J:#fdHGp +&ыrc'XC}y%DH^E^А~Ip4 emU;ZZ>_шsMNw]=W&IbS7Ϩ^;D۪݌sՐ?,fn]tw7?,(*Lt` ar(cie:`^~]dV0ݝɱIDxZFdsw,S\`輁gWOmSZIp-yۘ,r!}{}¹w# t-w:z"kuv$1㜍TsTf~!N+jhW~CEQ?!Dg4JG9L %gGQ[MGt;@Q[6Vy"fJHUN|ȓu-8ۛaCB}.uaMVvqGu:ῒvQ !-JKEv|>{mJF8ʲ$x= rǟxd]C@j?J{ܱUvKQDG~n;m_$LWҷ&nl)RźDgj4)~& r/ˁlEU'uEN2k[LJj87WSシW1pYW;,!>'a֍3-U?3\'#>±ijTGUJ@ù^Q)Qœjk|Gq˫ŷOW-Nyp̯Nq $ +ɯկ rBH#zhˆ+z0Su%a%SX/+gT%6v:c]x=FRyPvϜsH/y 4lP_s&**S5Pd i=jCi[&9wZv+ S839ͬ1h*}EΣWPOD!/ +lD1a;3 {{*ǟW B SH&ʼ{pQF#D}}Zg'{ƓvPK7䉀i'$P+RNr#Ym:4%Ĺ@_pAew%R5d` ,A8YtT&ԣP'Nm=f u7*U xgPb]z_^%db>L$`l@xӢO8<#XXu0`=evrOE2!l35sDm40V2k %g#oR M[2eMJǺT8/e>lqiY<.W_Zc:VN(P;Ud){->0gW B[i[{!vBH_4IbՌdFD?H e_CH e[v:{/m0 O +3UMr~I+@{=ݯb`Ut6W" vXəg'!ˬwSt1#xD4J)h.%Su[)j'j픳U*oȬ\ J/#tȏ 'N,>S`SE;: ^( HH2ޥ N7V/f$:然A rn!fHg8"Wke92|rdz8OR_hICHm͒BӚJIک/)G#!~&$MGR7!8c@փpGJF0_ lds5U={8ۯ;\"p?,Թ$$YkbtʿqʭN0 @+!:6F EC&l򈾃D8n Bd$8- ay w?iI#3VHuG;C~D}@Vׂ AVYw:(ːVM<㽣@Y IirYeҊ6ĨW=t`3@x fOAdV_EjB_S}"yZ? ŰUoT˾5YJbQ0$h3LCђ g#yfowrsJ+P,lV׏wIjjM:i|<'8GF:S14#4j BvIH&q6R\h"ҢA]S'`ܱ ! .oa{WJ.e #d !F֠ mI"pkRY2}A +6vyY-s\JO`[,3h/蟂k@(1Ψ=\COaJ pV DYoiYd%cTƿW*Z%H@" ԥ^BGD6 ԼHd[X/JgY-;lXX&U7 n3kf>OGHS#BTM@<;Hj"+EtBB\|_MrJ˫pVV3Z׹**EgT5(5!`O߰-Xp}ҡOةsdVKl*)\4X%wm^-Z4q)50 u7d#7iT$fhb"#ȇ}nÏോ9mu}I"  ȓ m.] V'|_ca!Qlg=3$۞HDP<{λ6شFS()A6]6B|M#R4jGfW2*WE_ͤ.2nni"lvBwY %-JEOr~2C~d[Ys77v}!@&ؚ@ +\O U'h8;Wx~5:CŇ8kȣ]fŏ>/ֹC{hbm:/E&^JEA?žHtRM̠$v]&;pnfwzOJQ  EUȻSKaSb]/44C2z' fbszXg,J۳%mX }gnT8tXE8TR|D֣@Ҥ!DwY+R` ͦ#⛾z~E:O9 +u+dY?zԽn[tlu,TMIT3TνZO6rD +v %|;&q:^z4+X..T'T^[2[:1bw[@ּO~D끌jN(nnԌz*$jɂs jB\sSZ] v:8ȉ0ė!`zE*i`VR*z-NvG ɭx4GJḄձs\ܴ#o;Υbhw}Dii%"giYeR>5zMҹR+/N=s~bc2p);3csKX=ybg:|IJj{p(f5:M,ڈ/ +p5;"jJ^Xڝ:7sA;09d>7IQJtln)wҙ=}Yc3 ,@ +[~%J1 _ВUR} M _اQ +ϰeW5P{ dG5)*, v]FKIPwYsj-[){#J⎡S̕D7 N:CGߩrTK4 C)-0z/tYœi.L{_M\mIQ˾dz|T'#oU~.AQ_3 +|Uxљ|G:?ƍ. al:<7v-07$'Q:{spz˝ay$gqoXyLUbxgg8*7R`%xT*Ð^TxD\ߓ1HQ7H @_ +R/~$YNuR`I2uY_nu1~ +nz$>_ M-s%f%&0ʠy9R*D7'"DU˸Թg1ٶw1wD5ZߐuEy䐾ˁ@ @boNPsWR竃e)C⑄:'?x +JUG@"A-j.,yL+z)3)T >QO22Ʋ 9y:(C) vw.ٍe[@GL颷JCï&SskE3c gZ)ɦCRc6DN>+?b蛧<-3V1ۂ4,ϱ*h5x^fB]HR'ׅ6>yߖH:Q+4? + ?j DqVUo-hū2njKT A/?AEx(qf;7>ղ &c;gU7MJEm +Q}I7aDj_{oٯcT6<':GYCysK,A3vw}. R$5 +S,6&7󕞱8 AQUfA |WtkJ/) 1C`ubbtiVn">x;'V  s۹{l G z&2DO/'LxA +XMd_N$C7d}p#nZM (ܕ:V'!tl%rîSb1ӭ mpPd=׷V}k%Oƙ~;./}',ndv42|AF"*OJHE ()9CzweP(G h]64w)]{HP3dSWUuf)H#e<\Zlj~ Rԏ7 2bv,_)%nW36)M2UtvEܵUv/^m>}gߘi PV<N HhW;T>O' +u"CExtq0 =`yޒB +"{(c{ *XFzG#3g@ dPۗҙgRs(?~ ^4bd=veFV)>5E‚nts4]tqhiv- +:w|9SyFĹ`v[²4s@Ɋ+)!EO/"]D8)^ b$iE۴QOCق#(ju>'S F|{]xQ(jEkIɳddv"%` QϘP ԇ>? `G[yryRK>!;JŨJh o7R_A'c)xJ1 qkhdq +~7brTe"c(랋^UV kVw֫(N\TnKг'k֯򻺞U~_4lF׬S9_.{灴^66D܇A=$obs n_c[=I'X';geԄ*o,UG6mX.(һ4={_Dz#6O/s,?]?^P|Յaj= ն +F3b^GJfU$yFZ +ՆuUocZS*VcܣyBZ۷oʉ'rp`x|br}t_yd]=Jءo鑉zC)bsZW<-йJB#^z#)"!,q2͝(z`Js#oVE mZ@~1^U 'jYM򋩦$[Dg ݑz~_4$[mP x&>4iQ\ih?P(XDZ\6%~ ?ꖤs δ2GǏw +nN(r <Vo٣HJW[ʪO "qb>8^U#Ecӌ?PB2o/?Qs7pR`y fuwEcK|po_Ǥ"eݳq ^OZ?Ս߬ޭd.>a@:RȬ 4 h,pn9a38#$w=, C}^z˦ Q2C J SwE,He'XUW8-fVؒkg`̒O}_D""M⿸+;l)`}"ㆵl+|auJNvbqQ[mOGa*Z?{+ZWg#;a`j>eTzEvP~L/XR#' w:x9_6Q؈A3l5_=2oV"k<<k{0_^M1]F18/Ku \8>2If\7jT ҫKgylptGX}67e'!>{C@Yh=T b<%b|b|yExuE uU*_Vs}0ȭZ,XvN!;Fޢ9-' b1K %# {L6((,<oLTNeN<z9WމV}|#PB}Xwb 0F|l4GSr@pQBQU.ތ8C~|h)G9躨)>D)c`ܫ͟㼻|48&; +޽.N.,2/zf@&aoQ٠sͷohR|B?w@Jn(jsWu zarƫ3ٙZB$> sR"JT3ai~3La֣C3`!Z9CsbNI{?Hi9 b[(ƭ;82σs'LMY0+}^˓gƂQJ4 ]@X}!1Z<_N&K٨JJ#n~i싮Nz/ޢ"(\ߗbmKojFg[4@*,ژ[QV_!_QdRž.AdonӪ,ETrX`^J+vPal|&]3W!^QnJ[ylxp Ѫ[GGWNjz* 'VwfFf,fT._\/447>й^nVOrGDc.1U/#-$r<{铖|%ws B ZN`i.5hd#ΎZH0\9.͘2GruN>*nEAbԄv7ICJSMbf)YHq_r1sPB^CZ3n(޿Lp|M7ۑr}Mm] D_|;Z!^F$]fOTW ih⩅Ȫұ-ɟfhy B^_'se-utTxr'>ȊՐ< +Fn +/֫RF>O0A bE1ZzH{$"^tSZMo-Sk_{( +?򯲛TbN- R/~% \-?/73ob;kJkm@eE$]W(u%M1Z +p t} Z&GUM?)Selqƪ} sa 1-h>W턨uאf5Lf4̾D\غ&:3Y7 X>1Z7 +ب;8 ,yPNŻf|5M {5k}XAΝ&%uG6 Lgs?`F/Z5NcLV5///4N +2U2u%wSsC b1C#1CgF[ +:mc!5j ZUzrBhȆdA3si/I7Bo"*sQ8C˝:?Pma[Xãύ9/:_M-K>,JBI?['nqB6o 7,nF!uJ:D!wQUS39K.㎬5Jv;. 0ߵL +n4Ljub25Ss}|?X1 g*DwP.ǜ[[?MwzIj8M)*mXs_cA4^Vuyö/J.\*XL:ňOy + 8[ޒ}jQ.RP" l +'?S̾{qCٵSgYP!{ЛuƐ|g.!?>1Y!޸lp>р>UcrP?9ϗY, +w( Kw<ݯĦFXU":if\z' !BT͟J(y{Ǚw*zA9bȮQ'm$^Ruŗ/_DmRâvW1MQZmS߂>ZXSRu!]P{ yhh]UF+Z|P6.3hZ#2Qmy_OBș)Cp1ubNe^n%Üctx`C+;hTe #2YWSŖsqD<N&\45x:>ϴfSxscþMj!.@kz%v[toy9L*w. k ^"\:BDxT*TڈURQ(1ナmuhP3I=aC:cZ$oBHy^-б6S%j5DtDڜ@KI:gh zxa-y w2!;=@D\-H.N- +Zc_wI9[<\(=:Ue%5S}'/ **~#kB"buo-D&M?wi]`2DrEuywUFUV0;l]_!7 :C45".x>^ DqZܮ6Q$i`zQD](73g`Sc1RA49$(GqĀ洛SDnYpƧZSxViD;Kuc:f^Ǒ'7 # ?5gMm̠:Y^Hawocl֫{Y[%ojOLQBas˺2-aɂ֐\yC3ꚸm5q_k6Rנl~Fbc"]lOs |bX0}F~E d ~~;bK@#6!g:GEG$_LT!m">c^c;^j2*dC1egF'{18'û^tU * ere`V<O},W%iWttbt=%i2X|ed"F#N ]Hz5sIP v FȒKU!nՁksWj +͛[Ff.O[\r3`r.|o4_H9/XC.{«֫ `<fߔW=.ydp7&Lg!hSQ RqUI&>'cy+D ;Hx^ZG\ մuO,K숙j*ܕv\ g_t MJ]w&J@jAҗ;{"Դ)u #LK~c ʮ":Qd +j3bE]+ Ssډ~S&GRZQ_UΥ:F ɈruЭCGYJLAGȏ +pu*Ü![F IQ|[_tWٸ堹ie[~x?H?zKKALW_(chP`g|~~vvÒ~ϥ_iĕV~籊:rɆ%_\!pwqdǫz,(‚Ŭ+WQ%7EG}sP"s\N?+{QwZDґ3= nؿLiG+,f" ^Q7Zʳ`P=c5O$fݪ4ݤz*ffat +1:'BD {)Bh*j)׸G2Z (!5|Zo;R Fo@5'RV-|MLe`@x^^:}`I"6 -g~lӗJ_j ~GB&~vB>1U2f9B׋G|xKsU-~<&_3~I~EXPuW#QW m7/5ܪM][^;2z~qYV-8W~M֊OmG AF${-I {gom8xomK;Ľ3sp'1jHn t(߂OYvc6Q)-#:KY]+XEdk ud!!,?'$ȐIպI_z\sK}˦>7.<71vPG [1}$?-/?ȝ̀[ؼ0f|B󹨩|+υوfLQmw6|ڇ/*) D#jOV3kGo6y9lugEI 0_weVCƻF|ґi2>Ne,9ޘkS=r]ʃ{TwDV=vg@<ՐlTh}>O9=Qj c`Y}A2>}’^I\EQ1 +qĆ}'ْ΍*h˻"!JS0*\!YBw*d|Vh4(w(3oAʽOFUHXz-AjozAWο:VIJMqun7G=d^&M"~/l΃#gcU.+\p +'$/DX^0t' `s% Ga Y̫޳d>j'B7s2]aNAHﲳ;bR !uxxKR) A@EoCk w[C:.g1ΕsSS/~ş}$_Ǖv^9 \?{?~m?Z$B櫠vBb_)0ZL&0 ?s'M:23/. Bz!۶TuGxanբUa V@,qA`=,649< \1OnuE2>ez U"(tSbn->om{+*25IͷRy|\!VOp&j,|Jpg!^˶Kјٖo?߁´.at٢{'y_^Ĥ<Ԗ(d,ߊ Z=M3Bm[ڵOC@Ψ6#R}u*Q!sOl30q.hseGɭRo@@WIE~Ef}G7*2V$5 JV9*#M{r\ΦovG1kl{D  |.qscwGJ%z_ޜU⾦uV<1u12?8&\Љ hݰ~lUԊ(mG2h_v@[2sA$h2AH`#^h6*@{u'jTt$ޚfE^Ziyg?;W˭a +bHsC=mc}Nf3\q$3;jR^2<˖Ka ="%rK &Z)1q؜OjykOnL d8XQ4Y}"e$LMf8AGEb`M;e:k\|0l^# =q-l=K4MnHl*HWA*P*\g't6z-T8bO\w: Fa/Cl0@홐!75Rp9V֌ډR8jSV`F uhP0BH(;)㭫{܁ [ACwZrtb빣$N^[q5^1vQTJř{nuxWuc6!7PٗܦVLe0d\ 6GXɐkb\C*Na$Vcكy` @I+HxE=&H!GPs#(R+LrNXUb3/4YfnVs{c-FD^XжChz(VJ +lBDbWX%|~ HWv! mzh@a^/0X=DQ^=&(Eѻ1gQK-`1O]Qϐ>Qǎ  6YXX3f1E{r̀W@[$+Bcv㡩 qPRʉPR|*qi<8.)B$?n60f9FdT&SOφp75DYb{]Fƛe`XC6s;!\Z e96|)3̏Eb^ lj_x>w awo_C(@S$PN8 T$ #23od9#53(>0 Af!iq`Sk?AQzOD+Ǚ{ǁL#%7Q"Ŋs֢ QmռH{PjN]VwR kZwx~j1>)>LAփ\xȷ{"DZiHiG򹡝EJ;~8]hkC"'s1XrFq/'URІ0Wb%Cx ̄D/#]"WA >xHX7I9"ݞs X_*a בV=eFY~} B+v$2g$2[H:[ ap "|~pBI5,H?>zU=DwH JdKMǴ93)F$`2) + OƸCj)u+thpߘ/1򍱕5 ּ_EZx95/2s%Rt>{x+zg6<#ExrP(]F~h]<>TE#ߝ#EםhLڊWpq˳QF֫tܤ9WZO)UAJO1hUEYwsΪ:O2[{EuFz1dA +ӭʧeJ#@jA4 RoL eA2K|ŵjp˝fGӻ^ߩ=U&Q5q|}Z,5Wm.UJz:tUS~ .F:'6BL") %[G&"#bMDK}#y3^d57ȯ5og+IFP +qn+{= +z/-U7=!wiB?ep +G +*bbiбcY ]Oհ30,VbN5"QtW5@,٠+#iEɚ)n"9 D!rKI,;8! r6sQ=T#nQ P b*it$zk*[Zb'OCGFzhU2{h[ITOqD!/ķI~kHml=b/="<i%O9a K殴LZw\zr!-u4OƁ2YrN:@M3Q .g:rq@ ȶ(IG< '䠵UDal~XFl QΒؤ;8ːz'xfAOi(SQ Xkui {ήl 1TWu3Xo|5NM*O_{=$Zѡ_vEt 1U +C/ow0C )9PA& H봆'50H݋uA"C4YK+%Df{"("lf6:WI:f''o #)͝99jJGs2k(DSG& F>B˞"E~D >l?5h@E8w`殬$ +k 0qq}G8FdHs'KN 25t|7K][T3= +E\grAPA_>5s/tWY[翼1BxJگ@kDxpclg33{I"0O`lጽQyV80094S_n5|u/ -~,Uj& `g:gVJzWCJ&t^"-_nm#:KK\y?!vqHxӵKȽru"ebЅ'qYHߛ-,N#qY|@: ZRM: vKj>!&PᱏQЫe\f]j:! + C-!?%=R+qBdUu4y¹U ^p⑙;#sO s3jYi=ĸ B" m}* +OTY&H4 ew]#1fTyQ:̄= uVht&uݟѓ +=bo])A2-w5W:J^q>6y[OwNԊb٤ x 8JW~YX<(Muu1h5jRuXJMw'67[B'{ܙfqgD=KQe+CNLmᬇ"Z7j*%[^j{{chԅxd<)=+(k5\@vzRjG*/\㭿, "ڞ<8c`6m} tHD g01O4_D#_ +gZNg}۴§.ЮWšʍ[,b3< NG V: *w -湱tCv aͩyEBi~3tO_%_ "_VTXZc-'R#7YZ'c]_8H[IJsLkDV](\Wh3h9Ys6<>BT+df +A0RH-#1S2q.>pTU?:y~uju=Cr12&pan7 +G BlNA{\-DK#UTУ*u#nvN֝%zP."OQy_l-@ Q Q]H]*`ޥ/Zށ +8pD-5wcO*ZBw`>n`Bt=^CrTjqٝ/@i1óPT 4* )kfE2dנܩ 7:.˚-{`t+i>s73~d)kv-UCRrK}Z# BRB1Gl 4 !-m9XHGTpnB5ݙ[:42J'㛘gc7_cV;T1{ "7t֨c,C vҢONQ|UJķC#-aPcǥQM4ӌbMvÐpD9$Gt=bMgBQLlhbAqk jf^sV!gT!׆M&ҿ e{crE/{ +s[J@Ɍ^R49p@ Qp6Y8Xd|1c4;i.pʘb= Hen=y+R١=>Hso?w(3*E~8w'P2~譸w)"!,Rr!#' &J>Ȭ6q4:uINsa+3e-x$ oE/U øfU?j᩵,v2B">CPL5*FA˜lw4dW{F\iu׌)XiZoyS{U+?7'hm/"V@72V³^6ωEh-nQkn!![753",_U#F$3Dbn<~Ů,`k'AY4t &#TrOM}ުagڄ8Z̓ٞF\Ŀ$hG-d쀧 m-ǙDjx<yVĽ cc^PUB NuCCt1S[C59d\8;BPUڢAE;+ԛz p0ԅ af=7_GAxD֭6"(ACM1@Է;P\8wv<.<6un "/{yb@#δa A]J>֗9?R?CJ20;@$E>`4kdҦӣ9'Zi{5?1Tdm א¦l1 G3*U!ʹ̉;&y]!V=҉a׭`D:C۷8&D_$G(! E*='gH,B6@·_T؇+ukƈǍ(I̚Q0SĈ1j^/N|zeJ- qCR ^(gaa=Z[25HfcĜ_zr"bQ(pNg^&:-PI ,誕Q]M5,yX=<]}; f` %r$԰-_Mݚ:jKqfﵗC.vUN`"ͯCnI{Y{D*F.]rCUEo45x|CDܒ)uẂ} 5'őT]G 9^% + l"YݡNxpÄ" jgI$v.j=ugxO.*i[Skz +U֦ Cw(fN.1.gPj:3/)>P5s3wR]ﲩ\aCDD~%S\$Gʍ<%POxOE9oGJ#myQ*GM'ai3Ο77[#0hnv)Ȯf!lSCbB 4t8 +F08ɡCO fNV?x)AVH +:Z/󭿼?m=}Tg~TC/smU*s+C%Zz>2m\#<Ȋ4=s̓* +i^i7MqpAD!g#=""-n^Wgms؏Fv +82;%== fsP -/+WOU,ϜD<ܣ3I>R=gfL-@sܢ\u4^1{7?G:3h}|o)a|29ݣaV;G;w?W%qsX 3QKч['8DhJ:RO xg9Hyj߈c,,SM_SvG%{bsL&GJ'N\u|5 )Wg~Us B+Z"(Ln!@2ulj޳"aEPeQlA* ֿJ} +|]ZuTO=+JU=E_PÄ[io%V#;)uAIݪ8#?O#]kJ{G24vTj|2%V[2'%WQ*oVHM9kW?R12_Fcg=jjF*ךu>7gI%GVOoy:@l#@JʸN٫*=+HTCi#f18SkA%vgZTh뎟?Ւ$ R$~BjI"eb7thR=YH=3$uu0-cwdC_xGzZ[OQ>pfi؞V%n%}"LtN3̨NJ*GA3&[*mB輎Vw׫NSխzDxj h4S -< 7jVb̈́vzDФ4*e@ƨUCX?/;LFZ]d|&fq4LV3729Vt3~Z2Y>FV 3Ϊ+>|B]}!dbz. [G+3ã䟨DL hԭ9wrKsS9ygB^!xL7r,F |zпj9)}O)ȋ)W{F?TxҚSn>u QB$D"FҎz * {Vh2MߠT;VQrݨbϐ=l9$HLcin57Pi; Qa$ *>]2Mi`NK; pTX;y=~"6;2@%m9bIu֣Sl'AןوP t7kWB<*0՛nGe +=+ye:x7`{Mhp%sO5'АAḳmW +|)Ow + +s&|XBrT8}8ͩxQv>cb>0] N<3W"h+%CsKľml˲ua<`Bdu3,DDF~M(~OG $IzdKy~@>31K/qϜׁڢUu{fuvx?G-e~/ѬG)hW hS1Oэkix-D{Ec ;Sd ?3Sޅs7 [M(XD 5b?#lكm Y`^R a kVe·mٸuz_G|z! AI˔\ԮtHpoX;3G3\fijB\|Ů tC|{v#:W)!|% KRbS)su~`xO?]nv HH䷎tJ:!Y-2OīvB+1#Ϋ7t#8)Yv+ n\GzQvґ̺ʓ Xc78MC\GՕCO*^Ƽ㰴qg. 3J>gzk!܃$_ Sg>? $f)ʧ;:; 鉓h,i9ȶu̫#&Se!#'Z:jsB(a6M C$[4Bg;Cfxhpz{֑iυQa b%(2M E +h!f&sy  'Ж0̡\JtP-10h)æ9kK#.9H/a⬜_ΣTB59+=V/vNfxZC$icaX-)ۚ 3U=svNùxؚtK=@@uCkyJ]=0|g|CS2S=`Evk,WTI V0͆,~Dsj^MYs#@<+3Cq!t`ڣj>(U (["j,' ԹeGPfk93}bRoud#@67@vwiww>F9ՉFxngl3mC=ew+ gE*:2}#RТFI Ңicv6E;7|Gh%>mg[)pi%< I;b %RL>}-]/FOD3͟QvJYl5(FNu0!$"ک9i ԝr]!ʥCLRP48 +H2*7OJ(fH +*#&'΢a&͸ 5vE' K?ݵbn +)`Hx$"ű @z;ŜY1eK =2tVl|;FeK+K jQ62fryWNgy`~ 6Q0ӗU A'̥L~?˪tnx)q">Q Ae +C!pƁ$V˕VC ϢkYLȤx"enuJA՗|.E_Z44vgi5(&暻DŽ<96D0W*b}Q1F_2Cq?7'W7'f_1%[b4:g 8zbwRS#c\&qBM|(-HG$qԌV:6~i^PȆ4Pʘ).VG kh w[2kaˢ^_ + M5"AQhh3^ōc?n=@z?{@"*=߰z澦YdP +bŽTrzh.=A[>5% +g\9 JjGY81NRXC~m@RBSll+<ӝ$I{ǂ#d xy.)<Fs~cCzefD u,(΋]!z%fWޠ){oȗy_xf쿒iG{X J#30Ž7Qܓxoԓq3$~487Uo^MI"#͡!ɤzNbZ3z"S pD@e:֛x+HltQ鹁Vl`ho/F۞!@0ωϵmҏt]1dȢ̔ :fi;1Ň :5`/z5Z~+ۤRAҰ,Ż*ghQ=ii^/3f>F_?^BJ2Ll 9ĊBe˚ݸo,Zk\"{;y¯ce\}Gzx +-[YK{7]VuG3Q{Tr0vg.>>M촴>:%r%+xBo#t{ٕ6s8eL^?zڵdW1j&U6g-GK3K+ZW~kAz1l%V;o"J[_QMгVF[y@-r[!F:ݳaԕ]#A*=QJGZe8a +iĴ}J\GWDZ=:zu +:r+PP>U&_ ڌ2Lޜ#J'fˍr4NgSM+j A+AH]F.K8P[LQpce+ƴUtdI}W"V2|_HĘYHvT~Wp]Tܝ$YcY]o ]@pW3Q dQddf8C?wh^s:j[eVjʛ2I]뿬hڭ +AZyB#oy~ ^r endstream endobj 32 0 obj <>stream +0iT7C1U}'e +2(_DG{>5K;[|)9Cٵt;URs Obeiats^t4'0Hʙ #2"ziWr֖ PQ\dWzsl{ InZ>;C36Ĝw1K8RYb[wBj^Nu#FQ[Sn8|N|@a:aV/;o;|w]is|lƗمۧ[ݴ45h<]E +W aa +z iԝAjV^}NM8eE%Qܮ⠗ ~4a:R__0.!iFa | +/[pwF;I(wp U;-K^[BG#LqV [1PkJ <^j[Ajzإ-V4YݠXpRp=~OW`V#ZP[ !,H冋>C~B[|<5J5DIYH#48 g[gWqlwzF!qz 0*F1ɴ[4b9)ܡD }=]'}bߣsrb: y +9 SyQs_bJS{ onsF0";L)@{_C7]"g Yٮ2~G݇S}-HAA+9 lpst\S_D#}ruJy'9-vޅX֓҃ +6I;-Eo pgAd!=#HjI ^=Hp[qJYȷoAbL@"JI\6E)M4W%!@bj7HL"{MT3a\&[wZHM&,ñD@}O R gۿa3F-*nofDveu_[N!9I{֪#O!y2I1V3ʨb_-5 ^i4|HZPkgEcgԫu2VY9\x33xT/lK,0+^# Bnƶ- _i"27 sVҹbCNU +wD[QJ}>N1:ƫ. t_>}_?gi}RzFp 4Mbp {C2:4WNOvhJhs6όJ>q.ſt`Up'"AEH`Eda^ &4RĠ׃++y|1+#/7Gt 2T6 U`DY[pK36#'^XSD4?|Xhf$80$zDKlEԸ:?֣6{S-w f-`%2crXCD42]5ػ[<ȳ-C;|{_*%AGΡD4 +d*=AL+CQaw>OM74KN +i%Q`)k*tVQ#5Ԃ fAj$&΀ɡ%G#vr$C.plHϪEft%i&R+a CCBڕߠl pj,P3$X(`a^!lT7*Tɢ$~9J+sDxRGz UN%RxWMmNJ#Ѱjr1L_9]#|suradqDxD=ѾXNӀu7ccl+Z#[I=Ä@5]a4\p {zRw:\my0OJِBfߗ,םTtzg'#WU, _{`3)tei /Чa?X{~ldX4|Q\ +8q@w5t$s<\.`[˨ *K2IXԉ :{,ֳԘ`G[/m`L=gVQ#7$==a$7>yߘM{n)uJq<ظWٷ(f0r\pf__N(f @"!rˀz7&5e!/JF5qR~( `m솅s$DF|WEч)=s"?3qcFX߈?BCSD ꩧEeh恺A)Lr}g=Ǝ2<~Z :Uwb- G#Sbx,H!?w[(*k;b =?Ӛ6bl$kYQs͍BT|/(- )%h~-Cб<[V7Tu)398>{As9ُ}mTza#XPvǣ2RA0I0hd,xTړ2)+:1!>ϓ=U{F]C};8ۣM~O,hk1jʌK{Sg jL +,,3b&ހ,L-] Fl[zDȎ=ː;Y7̽"K"^*G JzcR;O>q@ ZXOu> ѐztv[pcZ]?!k-XT Z6PNs/(B/W}Lb0j)EUwW 䵅mH/a8FMҋA(ކɯaƕdcuPbokW"1>9X A,\k8{ʚҿе̉'}{y"+]hA 8188!`sZ&9x/햁G\C. kO tjnW4rpRh +s?92EO˙MUk:gܡZ4w{-n'e\&}f>(Le 碛;tk^ &dQ!73z|&4&|C.şi5/JO{^  .s7͐㊖)͕uck &ssP厲:Zt8΀qdȗ/E#'}F uѕV#w(bK @TW1gL4#XA0wbLܟ(bUKbDK?TN> Aܷ8 M\V0y=5H%&PMѐSSQ#8IE: W[8zo{|1࢞\&c@2+ !ҴƦyͽGjn+MW 7t&۴x"/;nO@\EJD=8m]2|4LwDBZo;BNS>Jb݃b8 ]%TqNIR^MQ˅ A>yT4崃3\!W]@ b!"t}v7 ҧl+=$2bK+h 1P,?*:Fz1F݁D~$MNJTN鱭; gڑΨ#kH%`1ҥRGnp)K7<PsC#@ЪV;_Rw*;mX׿,}6(=f\rڈ|ɇàQ^*ՠ(Ynya}y|w=~,5g-hw ԩ|?^tqBv}'>>fu>$I+TIH]uJk%K^LHğ> SZb&\I477 b(ʟ}e1^ߘ}oǘ9F2)*OvȋxZPe],h8?A+'ƏcR2Asa;+ -nTRd6j!0+#C!\v͆Hs?#9+ bE[&q@98Ng{*cskjYf␐X:k;1i[]&'Gg/lw|IL:9u+A] @#VxRG/U1*% +m.QΪ!d@PV]j sJgkB+9 s}=QT%r+bJq­GV+_=u+9RZLnqj@p9~H 6܏AC?"SL0]3h5dgըJ3Ә*,zj >kD @z՞$j{/9 + u1uP>jC$i<"J/ H7Ň!9)p_kטC@uyw{=]!PEԍ$ng=֖oN;ppWUu+o.V=;vO5w\N:Q<P`ddÊJ"Ej,O壐o>sa87GUҾ?掜ūzZgwRTCRP0IR0EKcUo! +3=aψ0sv͌>( +Fțzp,KA[6࣎@XոE\sEqg9rFb;^s;=N\p!͕f0PAuZxfΦF}F!\~clh BgyDy>Cb`"7'ۣy+ežץՃ2.3Do(tq + J| +&9XsϽޫ8ڿ*` ʸ3K|tl YMDذs<UsOif}cW}~*b%Hr=}*޸3ѻYBE{ LY$n +(+BYCCLtx5k*GOZ7^q=1hlWqAsM ]yҸ׫] A))7O\vZ14|< -p=k$ + sQd`9R|[)%nPD=ngX;'k-;ȾL׈.b@L4*Jy^pH STFA:TdRFZԉL-1 U*KՂezxpwN2Mxl8 y̙BQ0=3ӳ:w-Jm5qGAO Ś-\c_yh!P"- UGj!"ޡb@\{⦁T9wEK9 +L,_Vl?נЊ1{ p͏K#e)V!SBQmߢ/mzTjD ! +/ui% ՗U@Uy#5̉0bL+Pj-GVw⓲RιKl.ܣ(:ٚA':W7J04%Ne+H? \Q-WcWRJ?R)-vu1BEaAaQp?juɇjNOƠ~ŏ+# ]yrFh \i w'kNWM-rG!:%Ns"`g "|Ph?}wuÄ^02HWwA #MQx|O +z8%fT-o@**s.㬰Kzwòp }V\Mm'1Ut.+eJ =қ3KD^#"X@'d< I9Τ; U'Dn8ґM;GRr; a(d;,S#(0E:'R}͚ o̾R]|[䎄fP͋J@u˲q*~DNxǪ逈F`RZl^A tޟQA2sɺoϾɬT]7]_{|c*5-~ɽ۩iӏGCn!Z*s{j)$g +}]Y\IT%xP 3Ӷj=B>z^oɸk#L`fɟZbp|jٳiՌ` o/ ]kC&OR naNBK?hvM +?*$ߐQP *<-RN{kkJ^,4 FNMQ!$ob ͈/Ly^AV[ُۀ8D-D +MQ ,(= L|MdUNѥjߙ [M!5䖡݀F]Ie=GGS3Mce 3v\;If(|SRp^!#Y=ϸ2s?Aץ؊~\׺.Uf]K33t+fM +fbRQdz/< whI dpxT/ȍ=vOZztZ&JQf_r[5gG!̎.q6#!!SCts\o߫"Wxޚorn0H0rvD4J]ih@1^YPl+7T=E+@Yf23p'BZ4;|-m ΢wdG0sV~.TlECMvqa87&8s1Vo+:sYv7Iu;bVF9g#zcwuz\2c#3G۸]N{KvQ. +G" 6j{8V2V3xmЯ'e*XݱVZ#$1/z^)˃qԈ`0'gezW9`[U+hW V~UPǥ_G$z7e6]$}bi(e I.ReBԤ@Xkzi\E* ̵Q)ȋQ>_p0"DP +dg<tDzėC|=VCxBϐؘ)W=1g+iϥkAoS\$g.p[PxEs\gh1}݋޸T/4eK_;85(~O(()tZOғoX5й3oAh#455ƯlX]z3G~%o=O'6UfU#t1S<TuY;FP%"pJeLwY0F^L~| +A?g mah@ʋ gXmdwxͰ;jX dL 4m%i*q$U*Dm&~O3F6κ' TM9?p͞7H/!s=fL{Bz?oFI9ve]iSF&.Č 6k)-22=jDbUJiDjk__7&+os,iɼZiqKC]P}`OͦT 4U(;膾:q+tC!̐3~MJ[2+E<`,_:U5WPpprAgn:n{dt>*BkRpk=k\落H+O =C!QkRyM>C +F>I/&`x^rGx~}y +bcT2z0s/$qEńq;𶚭wRn'@v.}((]I&,zS +*M-\ۙc@AS}'TޣzG 7nJǶF?>|qcyf +OuDo|+s +*.xio|R]R#6C}6D~u`f&8)&;1A}V,;QWrڡ{c9Poxg3ܶ(jHa,掔g8Y 9>M2SqZw' g+?vr6 ) b)S_=mޝ5N0tRoP@ɐ3Fmz$wk^ y!%yаݝC\;G;m=|9ɠ R[[h!7;9 0uȑf)_&-BBݚT?+;8֗L"T7-S +ӾԡрxJ'{rg ̉=%fdE;F5MTFŶl51޳5e P76JD^r)](ıtrT;n$|xyJP%G=M Ji_aZQ8w=pbEm s`6JjpJ>aL: d-sJsiw2[gަH@IkRA5w6ag9Z&7|sh ; #A-tw:1wf.l 3( \*3@meZܮ @3CꙌk@8=2IEX\_e-5YE~(pkYGn& CPHo)FtApוN:W| > E-z͓6!;_1EV"(;\VMX1OeniQwlHߘ~oOv]vK1cvM6Vyu7|/V? @^G?3/?Jݩ()l^W~ښ]@yAk&+:&1%G]J|q.Ć"=A1z!Na U)7\ +;Hyg6cĂ +EZsA-啜C?@_A&Bcz :H?3HvSShe!zGlW mӐlS:q`UUP&U*A?p%NcAetG=L%Dߣ<1ܨ!_~;'݄`[M=Ky>w~scuZa$kϼ!Ёۗ$)Й&葺ZU93{]رE_|Hbփ^4$+1a6CMc~{6G*O;(*~ϻ#~@Gi>q| +lBU(A *J54Ԝ!M=xj##zBCc꛲5[%qj8YfmqGxD=k RXap~8Q$Z(.`C'GVIb0U.4Z@#;E??:ܶnF;VZT3ñulॉx=g0ahJѽKCrp4D׃ ;* pW>~{tэ|BV ϲwj,T'5;šKvr\ \0w$\7eTLa/:\d=1n[] 4FNMluXQcz':>Es5Oڼ /r-IdZw$`!TgPIȇ'>~Xyd /Ƽ{hO҆m+Lq) +BsޫK@oK(jz ڞsބkHNrU}5ewBZs`؝a٦0YϪ"d +UnhZWhBІ)KlWS.]1* *O ϗL0d1cRx^?"I$ ^/qP/g]v[MQ~,}_O]~=l +( #sь6bXdJ} )b,7JO{ULr%Ur9j`ow8vf 2Da< d^|#mY|5 O1םq',ְ: +~hel3ځQ ?;w,d%oRŀPxzV R#t~<\'FJs+Ȑ,P>sj`%AJ>c1Đ!ķi 0+q'YUM5XgSf=#;7c3!SCyC/%=%9`鯈ZwO)nSΞ-֛3tlo%*^li8bD-u`ĔK;$ܶHy.ҿ4EqU?FsiPegg[v !, adǝI45:u|JeBj7(`C_xGƧшR/%&E2UMT?#e9d"Ti"+h~874ãՐx־!%95^}[_01g"?:մ;S0#7Ch,sؘ2J ? T\d\9r>Td Tey +^S?-r F \ӟWO\]_q:!9fܨjvD8r=&[W@_΋XIs$i+ +p\skYDQyh}hD'҄ I< .&">n}+$?R T>2Y#Ö3t =a7#ܢ8g:Cianf!)s=̍D )WMn^GXeݑOS8*Ch`N!-XLU_ ] +nwGnΫZ~D[ʗNSՠjEȞ8>$G<=3AqҢtD rO1ItplV"rvx='Ki`0ΤKte([VGa<) Q{ڏy; +ɼŕC<"jfu,pce !Q><=`lN?֡A#&Z١û/GgDZq\,aMXAQz2DVF_$@*)[9% +Nx$ņ؀RP;t\G'+U,As‘a>I +!u/ZmQa& gwQ 7{ E97~SthkG+:Ų͛R +@#T:xQ .*m{>(l)0z(bXuKtl,T7NլS2"K(SbmפG=Z- *)Win-yQ_ :*jvrmdG?udxx"YTCHo ҙS~RFB0(; Zʘl=xρ]%[,DF!;[m8 Ѻ^k(e»$P;[5ƻ4S4i~v씓5fz.rAqG+ ήhQR[h +l{è..M[X d; jG>|v?c@OmגWۨ؃#=v3jw}*Ag\7]i׈7݋SbɐV[{`\3Pv>yU`R~EpZy{?*GG{ ^<(1ї)5[a}Vq*G%<0rpo,@2JF3zs-U>*_s/>1Q|{:IߕxD{I)Z`@ƞN#=Uv]s>RPU'aJ\p3 Dgr'۴4 + KNIϖ4c~b)iUti%_>!Uc3AW7DMFc>zI!ٟV-Z[QW-:%icV+ډPO^Iy +h h-׼*kv:%1SW*y}5l{}#%L) MDoKXvFSZR'q [?labE2&[aCzrZK{8=yBJXwe۲?C4nZ/C>qm1d>8(Gq& eGPQh(c-\A["13sKdesj +v@{#1RxB'UG8b_虴R e'qFDSϩiE}|VO77d;"xDL7Bd(3A"}k=(Ɵ'0"CS_šk1dȓQpMjҲz(]512O"FZ#8ٝ!<'<?Ynb-~.N'nܬ8LA&C#1_gwz*Vq8g&;V@Xܱ *ܾ +f~g޹rmp>\aЍ*"8c2r}*i^uѾƫ?H3G~CIo\[f +L堇yE'0X1fTKɍSv'rĜL rF}^';:)Rh$ +[ 5idf883F*]a>EJA#$jC{R#[Sme?LOGҚ!Oc7!}կ;dQb&"$ӫ4S;z< LU{ȥ豠@ k8VeU {2dgoʌ EDU4$lh޴4W#^5+m* +ZA7s$3N}?5lq~L' {%#MMGt=|Gu~[P:Wt38 +e(["YMgnWv]=N*D(ހ}@ @Pb3vIΟN7LOcDO43KRaP>ՏWI+%?Mo0ܠ$@1aEՒA=FR-2cCR[$(oݹ2x />t0p'.U=WJ +bY2`]Bȹ:/?AU2'#7.NCW@ `xwjlC-0Р8(OPѽS@+9s#qVD&ƮdNAqqQ()1DNAf0?RXN,PA|2Fs I\iyƼ +hL|p5w/L$ 3~Eo~IDDKLe8W3s3 H`g(.51O22*$Q5@ D(w@?sD|PP=֑'~\+C-_ŽU&Pގ +NKŊqޥT$4"W%gfh-ɕXrRK." +G#0F9S%i%o"=E,C-{z;UmG`;yL.*}Qƻ @XevB7=eV5y ̿+l5Ff2+7{h*F@y:ְqB^01aRNI=}r/LqNy~/~9R+a22+\BCFjgdԹz{sFNZI.`>:]4O]/z4c[T`#fTQD ܞR캲n7.[%$تyJH"*vս:?%u0HdU<۹!}bOVAuH}`w.Uj}HO;֭wߪǔsm9D,ѹrNyת3Hݣ(sQ8}9BrŻ0C #Ț' Xjj5KR;b2Jr->לgwf~2~$.ߊjB $p%K. GrWj;[kB}}[舎 ?Qh~̠tԁH~WA #]qgNAZs; +T;'RB#ۣtEKE +)Ar,G;@:IoFA\l(Q1p !8A=7] #;ǼzsVol7_TKhtgDUt/9@^y3$$5W}_q#Sd/r5 (#QZFI?mWd'hwióWb!Mr=}=-ʵEŋNIxt4~$};R?#q^\Җ<<gA gu9G} +}}sdn֕1}qf +;JNyG-S~@PKmI[2E/W^Y0a5j?[`%|Q(gQa=ݺ7A$Aj6+,DQBbH ++[ߦlz9$^*M_'CCa"Pm}>rGc'4#GKICWK%Kw9~툏wؽ[w΂Mo^-9ZZ3I)BƅBuxnS$_Ž:SeH?0wV xyϽT"1U"'jgNe|vpt9*O_TK87Y_9.&P?^0l +% vh1p,#3L+[ f\I +H oyxe.kT"DŽRNF~[G! Pѱ5"l!0t2< k;TkO/V"g#$v 6n\+ E{S JddȺ=P<@8" xK;A6ShJ:P9Ȗo-H.ME[aV ԩ_H_XܽJHcq%MMq0J%ܯ%N}`~Uʳlb jؑ$c hJWPǾ-c"׃x4 +2]Cc#!ېvk R2DPw=7SI`q0zZ=[k%JZDt-MPUq l*]XO^5!DU֞L-g1sLu+\EV= +>AFa 'yoϵHbZ}`b;2E.wx3iиSoh#wf ̏k2oYw=Z | 5Lu!+Qm87Zƣp c Ը~0k@Bf >BL EC!ey@w +RMM+͛!|-6{< }U ~Sf?d f +vD;fAMpF/̪][eqHnev-ӽx 2׈ye#؋<8E0{!V +]p|I"Hmڻ9v&H̯h>{\Jj8J A$Y-^Xni^[c@@.>lZ/>& {];KHnR ڨ)z%.R}%DqP aBzjq%+PtԚr08e}y/TˇTG" ڙѭh73}*-s3`Ŀʮ~FME*)m.oRVeÓ~k6qO P(: d-&5CyA#'w؀U8A$:[Y5lweewrݲ`t' ^Q|@vRЌd!(b??%TUbķ~I-D"ΣY̺yk)G(3S :Tk䡺|F 5wa.lVHĪ"p_q`* [߁6ɳ;xe˔B8.-itN=d`ByBP됸|b0B/fJ"4KG;.XϵcFXkЮwU{$$=mTpkid+<- uѰd-c_e]lsV5hfx*/ +5ݹ{$fAT~Kxmd70+wj13" 璠=I[QfJsKt/}$݃7WEJDdNiz~wK?CZ{{@lEHӘy<^ ~ J.eWxśrdjFjXXͨ|ϬMׯ~aԡOX{àItFx3'bRd^1k-8~̝֛<#^5"} n 0/;X4툻R}Ky+v + c#ǮfV$MKfaq_Ou#&j>K 1Z&+;*qlYH68,؉Y-k̈_鍞E!C3)YmWvfXrJJV?N+:V.K bw@%!|pMsKڎm0@ysCɟ}814,4X DW+5,h$gPvs@yD5zE[};l?NךLrㅤ窐+!>~i|{9F?vKlȌDf>Oj@d7xH7U@+~6M1H#RJSkV{{P+7V'4ra )GW]d`{=w셟/GI~D^qG;ygRvԑ}P$ܖ:)H"Uj݊/V:$wHҚᤨ;Ԍ^}QpW"Qm +ۿ|^;$1wԁW;XC)P&Ϟon1]<\d0 +gFG3gythFt'IUI +IQ ԩ,gU( |fmXŞ:J),ՈyaJ`NWvd `O +j'AhiA쉚6`q}NM1:ȰpyWwcMZ voZ-1WjHou0; 36 F8!ŷo岙Ǿ/iW0mI@qx+CJ p&+CkAYc)7dL>pӷ8eߣ2;Rqt{޸vt#CpP|q5¡Q.En3{ `#aѪХ8pITd!ID5(Xk09/ kJq+sկ j ;Re aR<#`#ݘΝTɔq4猚KQ2XBGf,j՘}=QtzR + &Jg!_Ps/ӯo(9r3E|wqTJh~eهXeEΔOsI A8ļc~1* 7uOHցTMT lV>1#!Get߮2aa#WiL2CkQZC2J^zg'$ 8t,1+NwN% +EDLcv>E͂6#JL=~>x:J+״ 'OAQ936B<:eyF }[);[&F\f +D"b!q/8gVpb0F]&uP.DRdQ ?VH_Pdz~t+" _Uͥq aY!! RpgDGjs`%FMȹC,8 3y"',: UM2P)+[DJ Ǖօ [9*߷DZ3ԑbҪD:3<(A^B #vn׉NKh `p3׺e}"%sbeOT^E6SLSjTiyE]I ~" S=Dc 3o (뺮** )S( LsQoQǬBǼ +\|qlOr0Ab@5[;a׳Vc\E*<י/8]շK޲T(10MD份G [qk:ѯC'9 +rAyWct czy,:ڊ,0Op&*hjB `1LAMM!\IJeg\p!0/C*>g24Seɇ!sAA*K{n)5`:ABC'NWΒi\:CK_;^2\%G>z} `0 l~lJ[8(m!AUzwtdz@T~>ls^!]Y>o,ܷF7D;JɍvG"IPZm*n{@[zOg'77+I.>A jOi1}ZΚ~<** } Rpei2s9QS@twМ&?W"h%y6 ka[K2o[ݓR*n2iU:F6imwPij hfg^L4tN\`y"`(#^,XU`nmu6PCS+;5o]qDΙ] 9)}qXxf 3*9YpCЩR=/؟wV䠤s|D>ٷM!^J)=Et?~ #1/+'9@b5/"-F8߁{d){ϫo -)Or6| .ٯ9йsNtiPD &<أ˷J E_L+P>imѶVHO-`b2=C;r[͟,LRW}}f{?\GUU9|Pjb{}e{ٖIXLzʯ2#QvJ6wq-DbD,Veo,X_V@x';օQWBZvO^fW UսU=mDb#c>0DJb`/"'$ +9b5lOɸ2j]XY(k20{;QVC9L{bqaD٠|cպ3:-dH +˚m@9ߊusÀ*L>CH/Um!TjDO-Ī#IoCre/4o5dwDb3Tؽ@AqZrӎ6ڹ8 ac&C=9UHE[HvԴ +>[ȴxMwEggq'P!Mʽ-0it _^\͟O,d~;W=kjZoD%tBtHi7D*ó#"T?cR4aFY_Zʡ=OrsG٢//!W#ۢ_Mnڠ'㉺Cay,nyh+c`i1Yle=B80hϋ-;"s:EUlM\21{QϞF-۹\_ MY@t9ЦD:*vi0:bb=!No?؋|#ogF5-;Byx4RRwyQr/M9YK1"Ϧ;(n1mdy9M'MW<:I,s+/ɡBCelRpvZ/YW+zvjˮcB7!gz!7)¾0:;_XgRDxD{̮5hWgTGߞ45 3P Zͧ4Ÿ>"`Y +cJjvn?Cc5Fya /!KTӁg784'hY' 9".l;}585&AN3eՏ`!D?guN2akr3zzq . +pTYR'VBA9WI }eӮ;) gA@2f&89Oτ+6[cD\KV223Vpd0| zc B_z;e*5 +:jBV]I|m2\%<3:!9X/̩R*8n3hzV/# v 0+]ppT3@#O^2_n֐ KvS_K5+$nj`&CD" 3Di;SqY_;1? 7;Y[-!m% NFsj s/wӟȩ + <@Bx|9Œ )oea=wYP}o@kU|s`Q%UiE77biYSI=I~޼E-p$c<ڨM6K*\-DIZt7~ǘnb M}8cVzHßȫZ #B l㒊%N)4 hdj/ +J2^1V2:;א2MxOO[)xVsqc^:|DVm5K\ o j_"zM0#<Ő[6!оg@ Ka8n󨸶16hNM*UKq ~)K6GU@$Q`="pƏY}axtf_FDVB-O΄7%BhY~pw`]Pp!i;L1|HwtUN4ݣIմLQwp{F\>m JphNAX(kq/A6-y#}Yto Gp }GȘon4 }1H Ã8ƳwPYeWeƀ[:< +4&*&Dۥ0R!d{!;Գ/iR.G4nôuX2~v۷]鑳2B +`ه=NBJ= |R$k·LK.ޅ8+N=YVN΀gFZ<l b S gslkݐN-fY; Cߩ mj<,VȅK1\ _,9dPu%F YWZ7}mc;W135-re=< +$iGI]h_ EF߭AB!'<4^^_8?31}vn,.m?M4h.A׶ }wԂNՐy;z<͔\)P*ہcZqL1R=0XA!>U٦'\Bi:u^Ӝi@7{ 'LٻՁu#^qzGҌD)] s4Kt`Wb0~*a +q aDefA"R;3vu*6UpHsƈ4"yBDJ !JLxWCVL!SJ9^Rlj_y%:{Df9ҴȐJ9C\Y+NMA q6O%ghgQU'vhaq31*Sq=ڤň%23&Z ̹me +2oM4#'4n@ +4#%]sKioUWPIJ.$Pܼhp6ox$bPMrw2F`4Hcw(te&Hdו:kPeKc麳2fJZ-@?4aV@tq<;oqADD;GEAUd;!k5d9\̵Z 3Z;\H\lIj3*1+iA|۝J|5o4 [t&@jK2<;(/4c@y9g`X`Г:%RLwCF}p+ڒ{ ` -)ɖ%U(2KWyId`D٠I MJ=p}V| +&z˻ wP|b:ʞQ+"1 dD1`AXnۃ?C;6jY؊*W2^d=mf綶MwÀ_ +M;3?Y8/yg+^ "_,њ[gDqbnu)Qڗs cngas)-Q\#7m % QA:BS=51ǸBB%@nH4"'udb&FRxKKr~tE= vc~ZD:~/ۭǪ?Ҍ.axdB-Fs=]Q׌ +4iR Y^EG*U5\>i^PxW!qaZ({ۏ2-/X592u=߅k˷2y;sOgQj`cڎ3xeŐ.`uguy( dO!4 PQa?"i{7>֌J5|O}N~]! +Zf׾[pxN ǻ+[%w-wER7JV(BSAx< +iR!ClAKm}Waynar3exOd )Gt2vD#.@v=sbO" ;F_#Rgh +ã0D{(z 4P +9nabgdW1 C!'-r~^CgDqn΃\Cw_J7s+&֣VDj$9C*w+`M8?!~'[_e%s@&l킖+Iœ]+4&Ek* tC R+Vo!u)[ b1L_A;ߕÈmhe PX\!\iжRS3hU@Rs'.nFm JYvTEHzz,%*Gx;&I0VN nP-SKCB?6#QY=.Azp(^3'^Vl)WFmG{SpһV5MwrN`-Cf Sl!r6d#`1r]l `g 'C{:UHRRY5` hKسKU0kK{FZQjșh ɂBnnwڷW +LԕV# }B^:L'WS\dFirTrA+g1f\:&򏠧/I\C&}ae*g3&7^5L{(IVL4DrolFߢXDfU汪 d{b$dR=Ϸ9UnΔֺ5G)0\.d[Ku&![VWIpg/ hyH>: fBh't}|c>o1nZ"~'B;Уe )#M%X*~G!ǐ]}y$>Z6"FR>*@)YbR~oۢu+57Tev xT2 ԯUȜ5@7'C|"G|Gf +-m-ǵ![H8<wPw^1y8W-*tU"/"fF\J)n^+=*fW Ĕ]/cmF#dtW0BOU PMֳcU졥0hck@!xry7ΣaЗ^4E)~KQ+T,&ΧT] +J픭fH,CZRL T0 U[2x=+q,Fll߉KŃjp˾TSF\؊Af4TP.u3pA?Yf9_" +cZzm}2C&&ULT/<4AYE2e$9?Ta1d|Vg"7%*'WڞQb0z5g6qL7Q%4p\ B+ +] %UsU=?BgU큒@YAp ]W᥊(uhdF +݋ ݹ!YAͩ@r+%'^BZxa!1^ږ?:)+}`̩_,Z-,hFHn=JVA~U=WH= +A5ni)RI3^,M{ -p@f4c(Z\BF cPz]P{%[` + ۢil;əTƈ"Ϭ-̓_YtܚO(0-/_5K_!Xo/7V_lEd#Gp +bjfga;iZ+#~Ƣ4>aitaio+FEx e woGPIFՎx-#Rn:.FU<5NjQӄ3>td0v2ࣝ~OٻAhTȲZ`V v 2+)Z5Ҟ%!xH +/D&rҡl{b5(П +hȌƤ]sX@O +M=<z3"r\0lɷG~< +@d[ !:ZY`_Nr# la+棂unXdJ0U{$H3!/}D(ۦqghkδe1g҈$ضO;7/iD L+dޙMfxxkq;k#owiP7wϤ"vm|v?4cM*DdpG(lu[ r [R'ժ1&Nu3~Ǿj89K!ZmP3dT{[38A䎿1^q&g8ԼYD>ӝ BvH Y ?iΊ/^uf=ץtSƞkOe pܵcįL$ݖ)i{%.4 6Z*rY§kHKQ_=\J }c6W9R|hQjkku7@(\<6Ge@y*4 +Ñbv,ϥB9UPuYh[~1iڀf"ٞVxw1p{8| +hX7]ߤIhW)-+8;1{,@ `PM[y2qO [mKE\rM1-TqR#gبƪnU+p?701U"=/cwCMe\UfW+p4C7BN31\O6g؅P ZH,Dki3\%nY-b/ѣoٹ>MX OddmXUΘL*kDWr1*A\ֈ|kS9Gnꏒk. 3N7@\$Ud'6y$XtZ(DH +l,0$:- v@!'/3wxܲ,4 &yJ_UkI96eǜ󄳊ݧ߹o):7hg "m WDs}t(8t8ޔz!xe˱RE-Wc+ 4E iB[߇t(* !T Ϩ_w~6+ X583hq]Hi03,ȟK/(L9ÿFw6Su31o$յ? }Ti9)$b>R6#VdP@z3-ONf.o9&cw)\O9Dg:anٞUiD1v4+y)x¯ E~5a8 l4ޖFtk(bn8Ƅ apK-}#3'C(^UdjDf[Ki ;aR#9 )k%ι_є';ઈŽL_3M9/1 )xU>6i0 ǻ?Ŀa4h>?!sZxD2FDDSV %s"tDY).2nL(! /D5 B5O1cGbB_`ι=֌&0# X񘕊޲EЅ܍Q̋n o~IE%}E]}ƏlR{=8!!r)cJOG1-s}1%=q m9`yx}H*Q`2ƜJo  &kҤyWFƗk-H[{zAzAYYH-< Cfkwv'FB*Ϊ僃dqAmjA`9ٟ +T OXsDָRhs4kr]ki{cYR٭D&EϧiY$yFjh-Zkΐ-䏤YU9 6_WG,c6ta&?y qjf"G[$QV]^WiVEX*ǽxkl=`]Ko)Z:ezfhA\)Yeq(.m1P.)hTDTS-d +|a5+mȼ- <1yŔޛ%ߞGĹJ|ik?oF\V:9g\n. |[:-ʖ ̏^56wb;wM]ʊBh\[{Y%sTJ`RykP*IJ-lJUb~JcK*|>BSފrk]0^RHX'+'hiAUi#&@hA2#P[b{x 1 RVi#ܶHD1QPoc<ҼMGژ'&$s݅OҔ.svD&%fѴfP>ڡ *)Uycd\f%%RVwOS?G[vm3W|k=pKxO]-%erꃠs@TG#nW0i|e >tμ)Xj0 K}q橡:Fu뛟WA;TFU䱍š?9} |޺|}7Jh("r;ĂcWi SQ}b&75 +b ik̿PdkBieU&{YR.owҝ \H{yB9%:GǷfm`Bg %}K3/ҢwEk>LF"}C RtQ} +s1$ʘIJjpy^~Ձ}+MyP:(k:a׋;' $K!t!R; M9U"s9 +wkt՜ѡ^^0mnU}!Wb]27h=zǒmY~7NΜS'K瀺c ӹtoʠ(mPH>O` !za10XOPsƫhʽ ReTV!L0jxߑЩ>"}v!WbKrsJm59"Jak#lBPyK2k|)Ĵ(&<;L^y,i眚#nD-W_:0>ᇻ9bJwmG b%KxҠ/q薉iU7_?lDmQ[YGIāDˤ]jP9<狉*&܀Du>$Sb 3ك-;bR!\>u5L;7 .5_.fuLuG boZȻ9Mߚ;s 6ݠ |Ĩ1 +z~x쩩%'?1u^`@;tNHί4蝥pt.0R*6~d>*0v6Cۏ(ŋ' +}i Ge7ʣf8Im +2 <%b3>qf<*+0d K1g|V o-ecFN˖d6]H<~~*T 3Z2']t/vqU` r qFD60PHֿ\P3LM7 .#hs9yBݱHܗXC;A=QaJ)3`6O 7ǚih:KIZΈo7Mek쥹- aګ\q^Eun#kr: Uw#l0POY*K~Ȣh頹9]w'Fl5(^ۛG4JoՈԶCu@`S{%,bsa~)4g> is!{cD*XQM\%IPϜk[qE-:TVOq- 4?S+iXx(:Lh:w"k |_a P6`;)!gf~sp[|t`*}Ģ0ѣl J~=Krߕ3ʞDPDs@k~#{>km9=C+@H4Hzҟ˞AI۸"0L\N&C42b1!A*;گ{s&R!sٯwêU +-g}#bzjE>ֵAPC3 {߱lI㿱ͮoܥ-R<7wnt#L >)&^ZU|XϷԵEROm{peAq _R Lx Q̄3[͐Q!},+ £l8h(8֮FIsBX4K,$늊aKPR ͯu< +þuQ#؈ +x30 +\Bj阗bt 9qRdGv*}@>9~2jLS?N +=ڟ>WG`7DBe;B 82cٌJ9;4AG 2_J` E7v^]CpÂ[.wP~2Oqq5) yC~|c] (|Lm.NPP0Z*ѵ|isθy: w,5xQ| +#}6 j '/󀔢i\g 5B-{Ep8"PjS[ؔs{*Zʛ[=*w\5DC.>ف±hkNڽfm9`[r;0"Qᅂ!zڑp`eb 澮[WUTrt7A툯RmөLԨ>з)㌪k 칞EW)5>ޢ2_Ue;S鵫{ʲ4t~  ?ϻBqYosآkKU4K3$R^#vR)1 +l s#n,qΰd])7R53wpjpB=\2"'&OH|ʺDX*1)C!>jD:$X;ٱ + #iHω.U度!JaO.y d~P Xn;Z7=l@-'|qݞTؘbm}Y#Jd}M=Ec>r7&_- +8/=DﲎacY$yLwMw"W%V.6߽#3V˗-2Hjp܈k1tr.>~p+&Kճj"n>3 U18ޕWrvz=-=U|.+ck̇41dL.C[ۛKKQq13AT﵍@DOJƢ B|&UB NԞ܂pv4/=BM؄=ߨ3Utd:$J*)r gA2uIU$oLfE7~sˏsSѰf(0S2ת-lG\XS hu{ aݶ$)$nӻTrp{G.hg:Pk8UMNIʶs!澯6rUmq#HiPȯ\_'JF6r˽ Yn5\ +u+ȓ n<|^iȡO&,jZ.qn\y*= CHvқfFR|$N}}9\{7Ȥh+Js2ν]H׾,n']2#^9QnyS.a>Wz 9EJIb5 5(̸LewjLju,hm BJ:!-&[ԸDMsm +K-LžJp䲷fxDH9DX +xR#k/™*C  4ա TSZDq(ۥf!Ab yژ>Gߍ6W'x& E֞GJ;{,;}-r\9pT3-Ca-ljK5%eҪ@㎥P όŐx 4|e"JՎ.jƚ5Pbjӌ2UQs;Gʘ_!L$$`?T[ (]'ƨ)2Zڈ)KaXJDvū^O,=UȁWm,#*뫮^7ߤWU9,S/'^;#&B1D*1_w*-D4[eW54 ~vTBw#qw:!EZO%NJeI#zd12% &t\==5y# d^kȯRK*LgP/R)"ujРL+Fj)`?+EJ~%bԱ9*k\p(Ra^zb@QqV9(\ض*tBpX霚Vo.Lu]Բ$>^UZ^<r76Q^',I(P:M2B{{byUiqۙUQ+KIz(#X{v򶸪JymD ҽS(yI*o<7m=Cd@]S}bWjufLY/g6 ^/7OfɨMu6({eRXѨ2g'X&7QT>9}oV֠CRRhc6Rvnulw^$VPXH¼-RIsi-r[ 9C+I9`N< pݱ{"$SAu Z0a^'~ʧIxs1tJ4o<'LEp!|q2`;"[/qa]_!UqkFȭK!C',˿A21g-|yL'Kޱ)s4X8#FH 7];#/PJF4V'\()^qǁhrV$+zJ !܄ۻy+5jyz +kcy>=(%~7O0x%N !ynPb_5T=!i,ԟ{ tm @/b؈oթ~Hu&8Ei +'\ pTVkŠsT}%VgBź'ĵ}=քA-vPD2D3XW1q/^B{xHx\j.#^x+ݠgMaUX* +!|ʪ kCъx–կˆ ,ܾa6g^J!1:P") = + +-]vn*!VvOTG.1q1qٙ79bzd'/*ny+W7!jzLD($b^ټF,ՊuL=s')ޣNR#1vwZF8,'"s~EzRQf*aĝnL˰ u Wc]/!LBsQT_HAl"է̅gq$-V%F{)x!=U3mF$aM my o /}Ub_P+Ӊ`dWnÅ~ ٭(q!\d]ψDܻ}7!tjŰ&G̊ŕK%ԋ㞾7S=32\tBv(L[ӫ$yR(E`8߳㦛+)¨4ċ.$A4/C@?@r|r9eB6'R-ܤ}]U$q}We 8 y>پꝭ+XJ@Xa-!BW쓣-'3{gu)I=L ]UZMaHTNht + mq!G^mHpHᕎrlB/0L0!mOl+9S6x~mc\cfhl!~ce+?;Pw@^hOQ܎c7=@ZL~ᵯ3Lꦔ +43:)#ͩ#PoQ'suӣBC} issaz9:rnqom+@|=TT9o(sYw1HSI6D]'6Xo|3P"]~N3? +zv.}F\Y«-ThЦ<ү_y_悍j!+3#p[ ŬX҂Oym`$塚j:GlIlxԐ:nmCHI Xb)]&?s~A)YQM;*x**ɥ22 UDoX[^ +*RX +1Y7%5$lc x^A:2P3V)'d"BJ⊲GȆuqڋ r~js7R+"Qy7VC.Z%%=+ʪBk3OBaP {OmG^ŭ*,H0>1yG}F<ʉBTeLd' --H'wsgWȁQg>ZpnJ'^b=x?-6paGٸxEfJX&r?k"tÝnpKÏj +1|O=w>v`* 1(BNqڻ{tT)9AQ'OWdv#`ړ,̴D".Cd+!(8@{?J0 T=깼r:b-'+оMV~PvWN8I6gJ$|qm#u?ʰ{+}w[~Ϭ+>ìDNa\meޭ9SYU.N{Ig7W+)ZtװzT-QEL;:R$Qr?rEA:[_3!`. +E 2|=@6 *pMXAv+BVPU`l+ZI]fYƆOG8ͱ~7Ѽ1oI}Au)] dU_OwF#i+G`)Cԛ2\/ʿ/]1n}9G 멷>qЦ tWZ)'b ,)mwcPؤ}6بNYѦ2[^آk3&J[%m߫4Dwo|EC* :SD+5B+N4~N|Ql~Y {eC]Dn!hTtgiYlϥ ʢr6]gDu]әP|ʎ*<'̪D{ <8hY%(Kv!r0a7D׆;5ObbC+̻)IcҠN]p:~S2|]쪑l"yx}0#ұB{!,ˏoLp-kov81y#m}M^%#K?N3Wd<]=94A6{GB ?+Q5ק* +(P}EH;{+Q1 +CI /ޞTC6sխ":NeיNd~B9) +]+LjdHŊeqj_>n[V8+-hpcJg II ?O7|WR̺#+_/~jiEBW\xҬэ1-vFNK&cᤏutžiB;jWt +?W, lEvIt#PDx6WEF5i\?u{oꕚ9=WtVͦ$CIYK3wK)tvw}9$u@K@ - e΄lȷUbt#iNԠ-W^y6)g_YgIoux ߰hen`Y}W#Jvj۳(#yBC fjrO >60 uu3?[)f?@DY"W"X)(貹pb?Nfß +a!AT:h?HkPm((c +\b#EF|[VJ<[{i ^JgRO3&KmеVr%XB0Zz4>O@{e d'BF;vzBӂ yROOAEِG9.޾eDE0<F2t?/?,Oo9v'ڵ4E'ߚރɟɻQBX3mP?VnKfٹ|.fF}R{{HƹC.\ r6}Ʀuo&E{ҭ9 +~">v!+~z ލQd&!D$_}-tآvm^3h`Uf(~|)Rڠ_J\,W6cR~.)\=hѕXo%:ࠒ\|z%?xc{=uNH@w1z^Ֆw+MgRV6l^E"o[h[P)Ѹ!3=y%ք#\ط@h\su72ZJ*kڼBviwPn?ץ+n4N9֡xyÆ@ZSm\lŭ%ÏdW{I;& *?0f#Umj%4I^6d^~wBLY؈/YʟBc.,b?1BŅL)43ɝkLz.gdqAGh +abm$h ђ%e"mxKA7$S}Y=o@[a]htr܁0]4-~9LKnjZU6qZv;L9{~LYw~VpQ6 x|zP.J2?ŀ0N#ʹbEf}ڐs.1n+ރcECz)jKqzȷ⬒#HX&WGً\]/tԢ]9wcPVCfPV[NU-G[&ap2tH]i{h`q)=Q-C uA93Uj{,dq, ^u.d"!tn=x,4fj_"/88H5,h7>ՋgyR8A9tK(djY,ţ}3'$d q3 jI)'=$!z\dsڌs-MIc;Y.<#@! ?l7rl8f^L8FΗ=XFS[}E4< {"SHp&>,l0#6y,cRkJҮ[eB2O5ZuO#B~\[R_uf/F}}] + }ZX\¼:&"Kvvny f^Kv{y\vR,eC*3-F)@s]eX_v *wa.t-<_f>yr +Uo;4YTƍG*q"'z=m5/xꟾ[8Xg[zy+6z>9_,e?o}G#ܯ?^c#%L0n@StUC.zOb il)tkШ#JBW77g2i,C+QMΣ|q{(nA-̞ପPb \aF-Hkh;NsVھ8[Lk/)f{nj] +%|t`"[\pFXfˢtm*RИ~jNȽe- AH_͜hC^+ƜO»H[b:s&6CGYT hxpݰLyQUzbJH`l:a&#m%7zSU #&{03mgJ;0_5z2gdeyG~u`lY}WD+Pl[Y8;, QxEEt6#6j-yVγz"BVkaYJ_[p͐Z!rM;i ;| J3 #npz&xWioq^*fݹV{ݨ;(Ϸ`zEaKURAQ< ԂP#.#ҕE3e}GcHJ^@5{8{3"%A;/;w2Onsp.%~{qTpy }zTZ,]}N`a&%q4W+}N(!% 4/LGcHUYe Sm[kV,JaʋE_i&׬^5;a;u JHW2@ +54kbZg;F$h.Ke @kҬ\ I0KN+$z"&*Gi~sREc + 쓧Q`y:`xZ#[]/P%q/l6!/Ss[PJ=dp!Vs!!^ +҃!ad!,Uv9ķͻ1A#Zf ֖H_Uoq ͐~yV{45.Uɇ.Aiev[:|ahY'{xL皗3v®Z$>}#Ց`O^k&<`6p=jǩ=kZ; AJA2<:v9Gܻ Zwi*C[άp 5{Ek7 @[ϐWwe@} YOky?(E˩(xuzyr@=rĴ}z=swYnCf/ {,NguTr %oDA-%*;_AOp6"njܴ( OO>$RuD;|63hל`b +{UΪ}A]oms]/D;!>qr"sƴl#)aeKrp;c~lrO9D"CW +Zbno=Tx56Q"R[#%1/?DQ]ڟeq5u.5BOX0Ѱ)u䫤*]Y4ߺ4`uk0oKgg'r6TMU'riEK3-dtýK^_~Hjt͕3F\$ 4CܵXrZ*EP+ '*vGb}C)~ yK+'t(8\Pw [Iԑۨ1wZ}z΅XGyj)P+f +yG bg 3^C!єb7v􌘡kēﵠ۞&?x]g5M ]#xEߏjp!4켓3 3B]s ^A. \|rgӥ˛q_*G}a {F6ϴ"m.R̠=DcM2BG@5Pf zQ-> +PsA4Π ÝQ-tua^HmEh9WmLC~5\ PĐmU_d4Qp2a57Vx>NPgIwl![o+N +POlk)Wr:>2SsbW/9*,lLWO:N눥(mz<dz|FVxf)dŗ"*'k8󝌊:Wv&(yc__aM{"]5cr= ĝ0ԎGXA[9/0R^X2htBݐ8fti%:@NqK<!y NJc;]Vxǥ"fB~Xsm?kx8XUUN&va K4z^JwfRڂz!LQ>nRQs1 )ܷs+8NzdS-W>^<[w([7Xr:c1 (NʄTf7zin[T +0GAkݝR ;kPc-Q2ʣl H|Wtl3R'$/\9pw +8qgR^ŌlLv촳~Ax 6j)tW&4̃L>?uVwI;>IQΠሓ_~n,=Es.|A;>l Z + YcHgՐݪ---Ōp{@WBbIe^h۴pA-e?MD?TCJ68n >W8/q\ +ycnaR0 8@?@?xcTo 4V#KtTX#ݷJ[铳?|HO-3PFh]1LڲAU)}8A]+B󁮶O`۶3&ii}-'[3e@հk >u3G2:Pא#Ξ(R _wXx"g`G^' ^(j3ml>ho2ŤP4fCɠ3I' n>moءA$Y['kb-"ڀaa}NQE:#/G|@ ?x{k[Sz9PsP܁W* UzWܻc+G߷Um4RAGG0h\S8>+8aj3 +ɭ<x޿#oxiV}Bbv4b f9JqL3꩹uHNr}uw?6MQE2p)h^dR@OhzfBNgJ +o㠞}h]l`Q+bEnP"#H; +4NѶٳ'W`A D|N٬ 6Tbn%2,)ݵ dk&uԭNo">+4!C!^,7{;5\=}~c Dj8JROlz.aD("Wu?KtMr~̰75L+D%n]Jv,H)ASdaͣVeHêbt=sǞv?jH +Or}%.Ūġ&>]FZl wy$G$˴_:.,is.B:&@̛"&& +PBH$2-~vQ~'s˩c:Z7";U_U}u۵(*.Q. RI5ΌBavvnቱɣ\:^̊9u\ICJ9|XFvZ QFQv kR6hL{?VȀX\u[z[oGU UIJ,K]RuDE}i/H7)NS\ + 17iQWsD R"-08șu?UuU{7Wq49Dp_F*iԫXMZLl%giOTuG]Uac{5p J " +;97n%)d AN'ăWIb:<;*.ӹlנM].ȋ#m s#*?3KTC6,I {:Q4F4~:f~?G>\ <59 ΚU*-=lr ԳN- +4 D)p_-v0`[a gܑY%+"6w?R&`S9!p/b{'> 0쓪n*s'jL4JGuV汢Ѐ/}nh= !v:G5Ye’wƙ~^GIJ`TH +_^n!Qu:>pd%9 +`-]w\0"ZM(Fܔc>~6}Od~ P&gw +*zWX`g5TFc"$TCPU> }tD+EZ=U{cʴ& +? +Nlac 6#7]!lQS}%cDV~Wj•ѝûeLQla J55g?/͑&w'r%.|"*@l}/҄d*&/vaǹ]4CTU$8Lц+}TE.Qw*nBykE%ү @f4f29U>55ڟ%F MU=| X P\VU%P9Rz拹L^9*$klT} Y^tZ6t)(<5h~7JsъΠT^+t#Hͫ +X ]rRwC1Qy9^jgX +݀_O܎v^U2 u +-dC> 쉷 Uh8-fKAj)F@ެ3G<O`;Jdv?E}iAA6?XU'IѨnG6a]׳LƐogH@g endstream endobj 33 0 obj <>stream +,Ɛ2݃FЋIJI/XviߓW| !z+ƌ!`=ju8axC<)[-bى$=}-7mf+"Fv`M~KiY u8c'np>Ҹ (w1,٬,u3WDŽ~?F3optx2hIN]hmgk#qɇz$eGyB<#zGJ~=pvU} +򆃥<Ö{!C-/b?dDomHQ<a_7"X\W[)r)BqޘJ^|Eu*Hf"2oi A % zK-W_'ywtjubX&0[$W9 +Gdn呼D͂43py޽E,a s#qȱZ,+|J& a@%gWz-dkb4^ܶ*oOTRK:);ݺIjHos[!6BXnqnUA3`+tD\}L|wG(gىE,iܨ- g$b;Ke^He WTL|_ ~NܱJ73d-VZE8' +˘ tTQ+b5U72W*[4+omʅt+Ҧ +\hFZDYB@*M>PԮH (=kZf"I8+* PQTtM)غ=, zFH;uD/= w :M=*ͧz oFDK.^5au,5GԧԾ3X?"ͅO/(W4 `]4!{ب%Q"QtWPP[8 +/dG#D)ةQCٲ3 2BCxs!bY;G~ Z WIa VȗA*auXC9>Cvt?sF&1jO 8ێ0W||~?P#4@:.Llgy* > +ҟ8BUmDv g)?gn6$|?OAMH ċld#u<,T~FOd#ڿg| Ys޲<-ĝal+ aGj`f;µ mn:]Afܣq_^{I %G%z60<孌2/=cOJB<PѤi0:O + ~"au0+9mv1?$ʐ+^*x`+huuUhfvʬ@ {)3QAs:DHȐ!P#SikPnٖdΛ{GE>o): chH-ۂh_] 1SG6Yjg8r?RI|!7G8 V|;H[hƺ|u6^DTw"A݆d:ڗf9Vd1æAn[_댳rFiU->a>Γ ZS +:a6Q0Ja,@Dr!l*ߝDFQWeK|q3amTA6NHמ[YM!>CZݕ'K^H x|q uv*3&jLg,;q.fW&.t3dnylJXwխBCLzgB{r#x{g SJG v8_ +r2 J g$ܞk=⸓ou#>ǓBI{qOveu'C{VFw\~?:m@G\"/80}tCŪ @sF%/Á +בnxp(cD)YmWt"uКR+j=JAb7|d0D2%`o49W{~բAYjȿ-?UL8>Wa -`Wf'z ()z+>^[O7oNkOd4bMuP5RٕIk w#{u.-p#\w}w{ʡg)'$ӞX ס>/'N)::.R u/.1f_ROSjBЦ\:7ږ3Z8cj^@g[g/-j\/9f%ViٮrSU& 뎖=`y[=c]5-n1^~|~szasCm;kh Hs̶ќ\5sZD7g{#{+ju3bœw{_Ct-L7o~'mw[)Gw%8LKR={(G,>{XHWsO kQ7ʣx&ޥrX@iW%KTe"zz*W$XWPy1AwQ9kŃD׭@ȏv?؞sz/TJ7dHz#7^fyV܀۪j̆4i$V_Rկ߿4xVo{mrxix/FkgF̶&-ґq/PqaqXaPwqrBw}nJ  !W vr57Lrkݟ9a=Uv}uնpyqC(bp;4WW?}Fbx}*ܝ!&y˾KB2-IC35=jFą`-glDVgJcNr1~N y3 [}s.0œi7UU5m'+,,DZ*sW.kx~oN|K}Xpr +##k 0t'Bٷ̏4qmucWcptz=iBQB4*/gy'5.i̹quDK )XMaˢtJ꽗,) +-tR '98P|SUMsvoM+ oIŊGM/LS]|Φ +hs"PdžI<_S"X"+WUWxl^%#FKĔ/^"Yz 7lBE!}W0DU4a#:O}@=mgh)nxdG]fanN6,)%4ktֻ^ٱW#홧UgD屑Juc֐'[||HuQ(~AW%]|a!{4(ǝJ_p}A~7hߗV5 z!x mrXA0wZ^`9Ss{Ӡi=&{5v"C)\{_ :n `tQMf +Fz is +wMȜaW `:\[L7,4CJyK.F"?LNTvxXr1(y^ p +hn[&)_z#d8 ޯ +@b@3f;ohס06LWmgTF9FIՑKh` !Uw2[-"_5:2UD =8S1Q̿:""KX$rzap֏6#O{ re*n cYK@wIq<󲞀kZ b\z 1R2A^%f gF.!M5r#*RlJv3Yơ3J!jA:6}B=¶{V &uh)OvPL i "Yw3/@ +#u=;(&a5ˍB o5#Cy]]]qN9J~ωfזVC0A6) QG"L;V1O +q-VׁtV̕,b1UIUq!{ġWnEParWV[xDi?r`-Ye9-0| #lDF$_<Þj=HoWQy)ܤz% omqڀ7B}$ɤcx)Jً|^9KmK j06"P3c`35Жz ,H8%]bW,$"A9ɱio`$פ#\8xgr'T[㝩W)okp-L4tgk8(O9 !aSE%Q=8 vǓ}ƕ;{5("1zhg6 +ρ~E,\h{iSn{MppFgC5+48.v/dԚu/$R(Ci'HkٲAWP>#Q %ܺFIRS]]W(u*|뱶X#W8@xUtd7sc2hgi2ɤh?_x^ߦËpN-ˬ+Dϼzw/Z"Ϛigou8nyKpMQM E'PY Ua%?EO*}3&&Dy$T4ϕڳ8R|Cm MFln6ݿі!.SoOdnA_tQۻ5࠷Z g1gR42*$ʐ}ԈZ:4lQa!,S dP#+oh+[@a6`3E_'dc0L2(0_lvJ+*|HܙejU +c#h2bZSC mu?,^"ǿ +V|򗔁Ĭz_Pt|g/?_?x񅡏!|6^b?*Sl>IҤ\Ht 45{$jfbq+CrΌcB(L;#[@@yۑ!{37ڡbjr '-4XUzݹb>jPj-o ]~'&⤖OmK(Jtt+xlIHCˮ/̡mGKdYbdE>t'44L`dtm~e x#D {*cFiE>*|32J?㰋#&u4GԒ(2Q#h,4R7*]+ ^o <J+2U!o( +NE|ɯ/>(dQHb# hA5f9 B]<4$br8BwXG̘.o7དU +jΔ,@dI؇׈@NFqs+H[ }GXD1Xwx[y?|R=mu6 ʐ#Ѣ]ɱ!dMvAT39A朕r+^;%:Jq8ْ +,ǻ(* X\K_S9^LO@ -3B=kGIR'eP/q: <?$x=R:iwV=^/p.l̛^ +S;|5jU[ 0w]ij9ڹKk &G I#1-u3I`2M4jQf{9)<q~QĘW BJ_&q?Vc6 Bh$?au)Q1΅91C&#'FݚUyȡ!C(3gfT^h*4LꓫP8ly 27e\+|/9l ݛ7ZQ i3j Is\ Ȗb?׭i;sN%SZ=;|¡l˙$^P'?^MlQUo10N_i=TgJ5RհBgNЯ$wfw"'-Q6tkSgGJb^ȭ GP\E: 9(0/rZVG%[BEIK,r*T8ƌ}>OVUCe~T;nw织qNܒ)T(IZ/ndRL S_ԢMXh*]=}U$[cJE))V"Q@C)m +moW4H4WX€/sřBl,n$t9*AM=u? ap +@[.ϛ~rVA76b%ّf_" ]K +u|VlQrFZwР"fwUv?d1vrjψ+A ^9DeZBx$P}` +`@pS&3WZjr⢇SڕlnU4~x)/^E?eD:#}%9w`8{9Iudªܭ@x*5:dn/q9ٱ jm8e"w{|C|?-I lH=7C]ETK' +7?th\kc]|dz*xs%&2ܱī2eGbD5?iGcMU6e %7gXNBO=;ubҶQ)EoRsZhe֗iŭAӿE}#qI`U>|YΝ*:i똕vƌGv`RS;`/BFvF.6*p6;vM_eIUG9sgB4/Tڲøb|KnʠAwE=ܾKG1O /Q$x @f[u()Hz-z7Ezz$^BHVKآY.RU-E Aܸ">&qk _rq!|"-;'p|',\{[Cf*Y#gQ=Z#}cDx߉DŽg4bv-#WvW#aՓ#>Fb; PB%#V?\$¨~5DXQZT1ɦ,†2gL7X;lut)Xj =F +&%bJb#$+~{$F3$.U1:3u]3BDr]T {8IκQGy7ھ6ߔ?8K};+? hY{ 16^u~R%ݥN  +,.b]u+L)#@1PqhоzRcZ `ԠOS3walP2B iB5o$7OM' Oa`5۲܈t/Xs0 p#FGU[o/ +CWXz{ޱ!Qȩ;jKG,xktdW>{CCFT{#JJgyKM0CZߒ|C(-fwf1VCG 0y5g@ a7_Q*/lx#?B=\β+imY|9c+v|BGfԶ& +DJSDl˛&_ym1 Xw|E#=vBH  gJҕRbAZ]ӪUTH /[ֽGכ2:܂e¦J6?FJ=g3/S}ayMo&|FY[)'|.'ZL*9A)XJ i1wiSJSCVZX!d(#LKy3` sJPip'?Mzz:h#+i+kD8rB"K^?? GطL(;g&㇈y"+[Mo釆,HQoh!224AFږDcˣmet08@| +C.DUpLӺ;?85]UWHA,u}H2{{_I-ӛ6+'Y#WiTWupôL + nY\n)YfȌFIZM9tO^}kHl +(|6~Czط QlD;pǐz-ncQ#фGxRbUGXk4e";QD1=U9c(? /x;Ȫ A#AŦL]x +6#L\@y21 5#}e: YL-~٬ԫUdPQ5#vd+e +&U]%s®idOl5n4WBFI(4 xܶcH|Vtb=܇9!!oQZS]q =< {g%ƿX Jኼ՝P+jC?@8P2/\}ZHP5_/>PQ\Sl_ep TJGzzPƹ^vjr٧˓NƝ"tE<)g1{d0,X^> ((HZ=08cp[UWwʈ6b5;{/oEqFFgBʞH"e9 ?x'6ƩcN\u*;H*qkH5P,/j7чuK3SL;$?s+DYH~C7$VJܑ&$$*si59 G/NOb@3Fx1u ]zPJYB:hBnݪउQj{9KdOK]ca*\oN^Y].t;bz%h@#NQ3'T@9S@{Ĝض@ +R Kw]񆃒)u6hQLB +^[a`҃+wPPGipΖ̟w&1FnGdCw]S$d!I7:O tGWs]']w!l(W7`+ר "AG";wle8}Du xP @@=O]5 Iy/46>T>i=C08Zr:G:ʣ 'f8AuRJ\ VZH*Wђ2Bb`,~$ᡴw57Jzcj2%m3?2ث^A$Ӻ+(>ZCs}I92!I "u +Նn).d憶zGDvH6FEy *B\TDWUUp,xe8^ wOno{=vhe0b ՐBtgW~+gW7\fyJzAzRiGUG欜NC> + ?2L$X90Q3,ոR!V}SZïGv*khUゑQoP:Asρ#eaDՐɳʁj{O>Es% $hE >+D!Q[3i'݄ c[I0}%-M @,n8c!~&T](\by%7T sCeͦPPp$Z?0lҹ2NaawTU +br\:hi`:א쯐1-cA +@)՚\wL҄eJ]ׂSF\M9m1nx,evd0w[Bgy} 3TlIPlqv(=JlEU:d0Uby ߷a?dF0wJ! KŻhU4Hw +7v\%ғo,TkciZiB1Hy3|h)]g"+l+fÇkw*h&,1T}CVC5vU}ᶂbF, pyw9BvpQ-9~'OWx=- U%'` ǡF{& VUbk 7BJI7 Ke]d3},H-mA#]qM!3V$5BWi+s\&Y~Rh`GG+;: 5Qhr_xqٜ af)SEd恜f~S(w yWv7(ڹ(ba?YS>XbEtS +*fq~FP/x/U96#;!(#ovGU/GuA(0rTT]U=-CSq}yQ_a UVD]xa(z ս" w:=&FKe8=1GQ- >As PAnmz a`2)8)v_b֛'w!-QT[/}4n|ǂw}&F6;Ċ2cMC" y4G?jidǢ9#J#*`*ċ.@ꁭvQ/\%ʯӾ`R'AJ0y^2|+勋~aoY$̎홐^E9xs"9$h"w~l' EXi>f}  _#Z͓n?ƙqqkjiQtF5^# <W¬G{?y5Zn%|%Ek[dn`5"^^\VвE-M7t< +Z<;ڒFQVϕ9 ba8^CÉ)Q#xєw ҅ѭ^.sv"Ip4 g tpދ}\ocL,FsJ=:zK="f1%N6VA̿(Q'_Kt5"4mؽ* |ɐY,Zz'⊲K +pR ߱*M-Vps&8c9ԝX, f4jȈ@~y + FJ1p?g8煈ydhWĔg\#T(j&x}\/*$\5!FT{DR|,-㉠_LIۜbQDń.(-c9 U*Q s1kt*<{-63\=cavC%l䱀މZH7=rv qmtCz {d QMJCXܠT(/#ڽ ܩ<@m~_WNs]  ~,8}@0+]j!8XQ;DLh[2kUtMsz F~#^0qwdP%w +?8Z9P +ȸQF +g&T7;TT/@΍}:[`h^K\|^AE禐!fik$i̚ Rl]qHG2Vz('TE:G IfZjca2gN2a@RڿyT C XY .Uvf{D }"$2>Ph1zACD"NIԷˤ9|my{n? 3&齖>EsTS8E۶Lxwh.a$BH$%| BTʯp$8(DL"İu}QC|+#FV +$iR;߹аbgP()-Em-)rn!PG9$DnIG͹ /?y\ϝׁ% kU9c +9 -Je'="Ic^2EQG<~ajJL;9.[׎I +>s#7h#zq(h@̆UOHpc9ĉyG&~!!a$OjԊpZj ΍PU2(}N`B_;Swց:jY|k%5+-y醐hAIuJg[C!+LUF4:5Hs'֞GHbh}JB}Tff^Ī}۱qThz^]Z#ƃvKLiRQVCy~Tt*CaZ%H[Deff֢XPϔ"߰y3jc]fu?-qAj U؍ lfɺI3o8@[EU&Қ~X udX+P +"ӟe齓 ^lHoϺb53/:H@c%GSnKҊ_~D|c//2OijnMB ζW_2۽i|sr}(ޥ-a +YC +':uo'js)ᢄ/`?a_5j3n؂eΕAʯkWG!(/L X9@F-.P3la=Ԧ#hҦ: K^.?㝽EKnf:TÔKMRR-o:0PKc%$7@I*yX:֪ {NeCԓJ+S=oϭ]ś V 6eCpz#Z,r7?*k1Y"L#7ӑ&ˮ1C C L$w#bث9aVx .n{.Vnt:1c۾+x +(@P1z_c[-/ƧAbFCwxu_|TJ {,p|sayBB Sĩ'ehOJq),9Twx9Ex[3}Q0(r#Zgw!2dG(Tk s0ָI13N&mKy&A4%`R*f'?j Zi +_'OYa5 jZjW簹zdT[ȏ!O]"}#{ CD 8V;#蓞A x;؈8R5yD}+fvY4e^eqW9 +(syي$XhHP2p5FL`Sճ`@Hݵ(WλQH}nTFj4cdU2.5R7sOMAXesȃsN͜'tv+pXFu构(7W>I;By]qӹ;-*-o,Br#(;N)zPXޤC* +UbljJ0cUF g>=1L=}8 Q"%r n<*(V'#¯A݊ ~u}8p.lV V*Z3jF~u-r@g:ͬ^bO`i̎ +Ln珲y5}gEd2&{sOv*֬"^{WI>,ŕ?n]_-Q2 DDh Ur&WV'aO-GAs -ٍvNwSi7'uub̝BT%s;"BթO,l"3Qjޥ +|2':)(PNPaHC[|=dD,ŦyZ|3Pr[z?uK2S|^q%Kك |K[?\`FٳVf+M竇6#DV,!UFu +f{x{*?h.J̐X˭M3V>"&rnq' IͭXLܒ`&F#ڙ=$#L&{[F]<|vΪS2%AxaS#@RN.+ڗ1]W]o j:nnDsWЌK/A3%6!6" ֧WXQkAp4rx\ a:ŜFѕL+$^ֳ)Qא#^Ne Kc">G\\q{<#T`F2=Inb9,GWJ!?T ȓr Pq#iIn1UX⼻`, []`'!iԾsʊ&֝4j5ˢ7A"XtP=gՀLC77OEk?-+$\QסWG&VҤ;`V>%3$MzA0fC2u59M vߋҔŇkTXoE 9@ڭ/Ycgi]rCݩC!ȭP}%r.u;?E,Z/Hgf+yt$19|@6ݍ;CT |g`JMB?-5Y&hH!d:-EhP&pr̢ +ozE=0}*hbJ^sG0`tMrmOA7H'mO_Te ViG!c'Ϋk~؛Pkj*S;S4Ydd2cRzzry{:[R +Ά`բgjKO.-ZwL{)2DK(KC( LZ0x2!0xؙS%zNdYKJars5pॴgɪ|g^:t84 f{W"&}RgE@2ގ8u{6wxOYEWf߫|1aq\ax;51(56jMQ:j7d+13{wίol[wFcE!}_P{3{>{36kW w*n{N[+Gd«9l!GĨUjUlQ)1~B +cenlW҂*N䄽U Nk0g*26Нb}(ZOx&~JW,3YnkW=hWX{;7Cv^2#C1roOXV24 cby_Ķ.iŵ(>q if#!+ɧEz|sV*$zpq J$?PiIL 0p U[ ykSݩo`]K2W IDS@뽥^>0P *RWVR&BBs(QTP{i +mBR\\ſ<6v}噜_bCS*/+AwϏq jzWhЈߑEkB'ؚɖ GkWrRɭ^::o cQP) l39m>ja,ih;)u+i&rriúT%{)c뭜y5 J5A>1h=JO1殼~֩`.Ue{R@HŴm)x,(\li~ У *7 cjUX3W @ڳs KB81qB4c3"pjy8I2XiH,Z.O/7qYzTJUzTI[_!NL-XbIZ(_O0D}8w[(!W1];&-9@ܕ]Q %Qȑ6U^?  cUW֎(hYKvT^>2?Mr}cw$gS3Oe8f^n7-FqI +K\'vBCN('aUx`tApBP]"=Sǽ}-+Fl`kNF}VU8xU^ə^qhmbL1' +AOH1/ЛfUӛύtꈉJqbmqʢIT:"~:Ħk%Ȍ+$E껡 m TQ[p 3o'pʛtHQAh4z̪3l$-~GDRg (V 9xAS%eV&6(AKrqwH~Zs4DE$q]gd"yDçXwduYlNB3LotȮKJoM.=Df.ZdZ8u+šUvRp238I +ح5g6ޙugmhJRvJ3ѦƦG?y0V/Jc޽&^cqB1:*{:g ^OعWe7o|8H`d}qڶ2dDx?)\ʣG5w.{W#QRy3"X)LrfרZDocz +tsi`N`=|+ťXk2jQIޙ@6̗M^' ?_q1VT6ܙQ [H$`4j2$a]ș"Mxʱl們\Q_o])$V@ќ#La?'`2DFLȧ}l9X%LNS66ox{ftԮy[9ٿ_ ^xmYBȱT ؏ +;(hoDa̩ (]qQd3ĐfMm0')n$-&1}ӓfJgZ3UU6?5ϔfZ"gæw¹\)H['eү@=֥/܁,wA!C @|s㑚7ܨEv2PWzM2Oj[7x+DO;92Øhcxr^|FGlN0˺k=2Ϛ +v.;&aE6ލ"ԯ wy*%h"O(mb0yt^1ש\)GϳmTnL£*XG +Y+T~-؄Agtd(Oa'V\CT 3jbQ|;̟ݯ;@)-QB8f)@h*M ڟ ܎c jh&tSu&ꇪbQ{٧#v Y8&p&JVYZh䚔;HkOG}v +ƭpKi.[)5< !+_˜?egmc%) n8:oGX`>C[d8I UC5ou:$if=冽^H3 +3$ޟQr`E&ۀ @];{ɑo}c4* +cg960$w}C|U`HH?~ _<ϾXZl̔[&N+̌p\LyJ3)@#,NѾ,?m}8c@;hdgw$x׫#̫92:Vk56W-eАWGG|=ڸ:ʗ6$?ϲ*r35^ĂG<Ҋƣ!slkS /_X.))Wj,T7*8d&/ l +Uxj#Jh(T6,$.X%bİ֛㄄t{{gȤ7C)*YC:J=KA149s~ַ|| +K1QTzm0.6 L NR3fbbS0Po:/29@B](=Da5$Mj.#\'q㌈cpt,F|2AY 0h#)׍ h1H, +&%g~kDGC|BXJ/FrE$ DQb!g}bG6gA@tݿF ~D>YدOSp&X1x'O0%cOdNTv>q_T([F6RE7FK#y>2Uq1yGLO:ڪ̓Ÿ0ZA'R/ ;5+(Ws%֯o̊Om`:Z lWIfӣy?Ne J`AYwT5S7)TE9v>á)֤{VVi!c " sTuT +ݣ9z/1l<~rjP6[3bWTϏ~C:DeIޤN1 ҕ"TxmsHgZ]5,ͷ:洅;M4>3i ~ ԁ sZ'y>5^t,gn~~#'niA!F]YcE1YY +p2͸G\)nawx=sIPO~N(iґQc7RTM}@ ܟ>yƎݸVyQ噪N,LfL|liO?S7X + 5P\zB+nC 3 +Fo[$5bWRuWC6rf +j].kIC'Q)Wi -|~>vݙ7tԠF"/ i쳥]ŻG zg#~bj騑]];d>F޲xmfrjR6A0E}'-C8~ުԴ"dRYicҰ2'VR7f#O{6duܞ qc煜rl-bʅI7L0MJ&')/7-^=bqw~K|>RVp2cXp䤂-3Ev:mU |t30?pҗcxO2FWOb8IP}o?1 bUzGHkG*`4'kiڒ麶(}#Rpe`Gc p43`fNIAZ8ao4kF+xً@ʸaz땍&II_0^[`3La隈bclGV1UP'_ InOBop=A1-m#d}:)ܚ +ܱ:")Lev*`:bS~Cf?v*(":U _<(x7-k茇t&/GHsF9(.8qGb*OP(GQZHJ3WeлX? +ܹyCnhD}D֘;]KǬ.WHʼnf򷈔o!81-^iKŕ\D~WOI1 n~B=#C/ah@m3GJ|v'ZӐ`t3||x @}*JU!K~I 6UĨfH)k߮)RaB|}(RTN'\VbQ + {Ũ[lf ûR|Wtũ=9Y7wQ7 +39MRNEE]|(dMur݁8nD+S9]9[eC5Df]ZqE|X2In!X.kK:gdIMoy;$*!ǵ#AqBNög,_5P' -*;g-:<aH&`!HOC\yDj>WRϯFCdEk3}6zд^?d-gu_f5r/Г]5" mu> !Z@,ؚ!#ERϚGşI"]TG/0;"ebB:t4xa}:x.#[y`R T='}ip%=h޽+謯=FM* !\M !Q߶pŷ66ZKPz !"G~d y˾b{(.2k'%B8їAèWC /fF/Y4K9N3׌5Wyt*ghq?Z?JPeQϸA >XQ|}xc{h,'pz!c~tM˫Fx1>W 8ݮAD;1.|ޏ%]O}xa!EZ\0`&Qf@]|_YVO߯0"O\l͚87NHQ^<:B j!C[\2~G9S_tdlR{͋#j +EH+e:vtc,ה'#4 ףyFxU=z'c.svN[ǚ>U5 z2x#xg/QT/^koEH/ޙٞ.V]pVlX}5&vyL3/&ƿѬM^R35J?w3L'Rs шeiz#nFu(B 7B7|7@oew4BrNbPw:G)I2l3+;V Ej,m7s9o)-eml:Lö?JjO=oEQ~Ѯp$j[OTUZA,eCmX.(zL8v5H 5ē33BwGc)D*7wG%~E0h}YQK6|mЕZC;}{;oU/UFUO*7ͅ[  +S\MDsU̝+#GL>MB\YuwWA(ěbi mUy.GV3fx}ȫB,Эekw'%7.V4CZhО +ak20A.W:A`z:tnDyT@]=j +GB%&wQkπoQ%f1(a=o?+ͥ֓@Zcg)({iݦ(oF`:o2׷D3SLesM}X@4<,OYRVP8# Xt!X&Je< {U?!7 >{ǐ&xC`֝C6-z|OY&\6)ʻ-tŐ'N¡j-ÉVoh;ڮeoW»Nq=ģ<ˇss!@o >? oBvg'^hڛY=c熋[ęd0TD J5Dn8-!~PnKB  #L3'}ڸ]Țk9kݼKRU2s}Yj,iϷ>r9Az  atV3#|݌M]_h؆m+БAo=ZI+nyڠNV[s|vs.vgu⧀?gD?=듬haiȻ61-7ND\`^j F::@p ԾW(WW {]jܒjtS$9ԲC-"e0RdڹM:<\AK|Yrtz.FHiďfHN^$JHQHѤuԫ$*sC?2ƒg/@aE kn p#ǣ@J)8 2oh+/8TC_2?gD  c}O-A ͷp1` @=LG޺YҙZ9y@Pсˤ^0Z}H 2DF5do\k7F>[y[2-wjB/[{[}9!;N Zp!QN놫`N.!⧚A~M봨)cJNaD;BdB.rkQ&*ZdnPt"uTBy v +E5d$_F'^u<8G=I~em8)=ٲ!<{JrQ…NUI I(;xQ50nQߡ\;шqTw}!JѬuv[H\-fL^/ thw2fc8|}l +UAĭd?[rR5&Eyn'ɶ G=R4G Ǭ%/jTP[A x#hrY_K AFrL@:)Re{\q΋V-$룅a&۫Z1C& B;2)sI#~!h&27׍xWy+Otohd~9 Y{R)ՅҙsxV]iF]9,gX^%4m4aQMs٩R>#5٫e)6OCo=5tLvB{\SĀ'X"~)śClS [Et↬@-P#&1xWTdg!HgME9|D`;-f05PXJ U~L?%/3|h(֑)H?ݙTAƄ~tC{ +8US;y+z;b/t I +>{>u!;{[4z"PõJ dh\9[3]{ O?ޕ-g5I0c<1u2g~S>23[hyU$ś'Y!ٛHnՅg`j@<>])Xӻ'}BZ<~~ j&BȚB44FznkK`[5/֩玭V@Υ\{pVOKRwBMO*G/e9zGjmc'V] +S.Jh1QQi15[|S os*f;x5 itzY~D!1͓ qXըsӆAabҳ~ !5u/ҋ~)aRǣϽ aCC\q9܇Ob;Q*BN_~$[f 뛸3`LѼjg0&P/6"vUZlobd& +,TwTlGY߈tfgc=C^A-׼5վ+@ڹ=A3ju>:Gvf];sO2z8S|0{"+ΛGX}om+[_܃|IS衺I)fW0zz-NPdHh\otԮ"5}2pRME V )ɷ_V?|"kg&L 7+?Eׄ 07J6OrsyO݁fnTE2 V[UJ]^YliAE*2hQ 'ŗ=7jN٩Ȁ7ѝ#<2!q/FrV"NQ( !⼩/кe|]w+M dgsGn] ƾE(){.$ǀˆY#ԪXCZ wJYgolVy yFJ9y31՚8?M 㔒`]RR"K{2lsꨔF3%ҦivX)'ܿ f9JgK~/k9h=K}qOqK_|-Nj<` +ʱc Ra cHڢpRҸjL .Y]Q_~Q ^]˞=Wj+rC&*snެ[ [wC%!˜FZR袇xodx3tЋ'׼]ǹ^d_z9 Z/s%,CUo U烸-Âo[)= !<$ڜcd>[E +8@=UvG$.WuG Avu

Ϩ*ʃwH)-O8)_< p%*I7~zJ!5~A/^#^#0k tE\Eݎ@bsF.GaM{gPYPV {c,sk~KY{|2+wuܭ{ֻ)DYk8HfT;c]qǥ@:w]*hsƠ]W[7pGl7gw;}e}eA| % H!)n{ Yls[:#*D cg}VZd ra5v +rnxAQД#1Тx +FrƖRi-C  FAMw $D;YAV1֝1섷CZؙK`-ve e^c.;/?chFh6;k> V 'EVW=WU g[eX&-@HNTlԭE>b+H26)֐BصͳKC^i­2FhE迃Ӈ,gIb:f$)B/ i8ɠG^6۳'hl5|H6~Z $]B tNb4(grC~z-rCR5Ao>e/0 7D)͆inȐxs +3>۬b hD#?6pDATĹ8Mg}7I.VmXK7%EPz}EiⳍuhzCK=bghT _[S"Ѓt@}wjz(QPZT+; + hbs3BQ*\(VPQ9 E2cDZ:AFWĜ=8k̫ZRqGӺ5),L=9wr ~`VkJZY1jk1.nr2RNrO۶_cLzkG{@Dzz ’oet}ӿDA3F:>+y:d1)_bmBXUSOA*+t[Gvwէ3koϱ )%& ;u?S 9GZ׈~߀׼Rq]1Iۗٮiu+AcfOBA0u1ЎyK.gc+Q*<¢8Ẃ+` 9q_: +u;߸X0z~@b??4aUqnDh"SpR}$q +r?#3cH翈]P{2I0:ڤW<'0E8=G:NԁE fv⹦ T0[M8β7BT2dE$n|zpWtcQqx| @yT$#@< ED{67‘u7ʼnpc?;q%-t6бqo-QWcD0lw8nK R[^:wTЪo  ^trUYOkk? jq*f(I, UKEza¾42pStw6ZomOtZ;+|h#2*u8XQ<T[~ޤOBĴ`>A.G&)JAwHH>TSHP^MhLUL"%u9 qjyŇ=qa׵|&va,8zYћq1B`ѣ,TqorS`!t$9ڸ>.8i݋6B}rR:v֕Oe8hd-Ec%fp)K.i0Mj0b]'{)݉aE hCc`(ɐ+V`,Ǜi쌶+DKT.jzQҫkر66Œ^b7^o_i Wr(5^cl-KE+詨/)9i>,$;'u8_`" >*) \nzH:"L BVQm#h[yQ=h3 bRuIgdO,Y쵭[2heboi⣷ ήIN((aM0:Ư,e2>0j&d1H:w_Ǻ~J\"|RHmx]lOnT2,'#b<^)Κۢ"  k#jz2 +rdҬ3?-B cd[cJB/QwBk}}cZll#8l~$sǟdJ. lHCe!]ʻ=JrEn׳No@9"0skT4CoӞ(>(/ iFL'r}p J  +ah +8pP"Mpޛ6iO1AOi毹[BIlhXcR{ۓz"8D=6lc7!抢\r{%v1ө,iXюmfHU,1\P:G5zЬYE/׉]oW oڀ}ϗJXw&lə/ ]Q@[VXk;uD)VާrҐH,y=ESg6Ed<֗r8C?cݰVɱ/CzgDHzK }E53џ7VCǭpί>PNt7 w|+AhQeWǸT+ȔV$V8(+2TGZXד5uzIEiOXׂp7Zc8JA_4PRɼTˢUK Zʍv()Q+ī;`E|tTYE*xTd_ Aۣf*&*XqgcA']aҵBR4t !01kNڷ>,@J75QZ@3͒ |i=6p/IauHK Nעj#*"JLZZ˼10rjC$kԐs o|M 﫳`9 7Cᚪ"_o%&N74_L~GM"5~H믵wctYvE?7MIZib &[j= t9SBLM!ttp Ts\4{5| c(Ԁ-i,Վ46ِsrvPD"'f/&&PX>3Rcuk2iܭAQ;JlTSM5.@=%AՏ~SZo8םz J+HyVq)];a^ɤ7hY t`7QE_ÿ'8R! +wZ* hMmѬ'7)ߕkѹ/ +V\Pyi%GL̃sGߊZmX*ip 0x"hy0Sx:-@y&r:BzB}Xg~|& mHbdj%=6=D]P=zQii*rU,ԠgvVL_7d(qא']YkArʥOQMB0)؇`,ONU, sϚ[vksxiF|O+6,QNSP]ː6ĹLLƈsƪblA Rx1FYQhf4q;;s){aUGa[$ +$ԭTLJz*C?J72k%0F_|SUSJ/] gjtr0<+礧U<錄SSpʑʾy=tۗ N80kG +c@j yr/1T`+z{ǏLMepv,hhaIEdKJѿ?Wth9yR,LtRC81?Xkl +tZ*PR}hʼnySpG 8xz:!9#irPEv('%.tȒy<1I0?}:&?@n[^fCf7N"PGSŲ +8(8WI91Ahּ88뙉mGGBwBZ£lz0xo:(o``3$oH'km%FB_yV^tXi,&\Z,;P޹ sٕ(b[Z1sg̍?^nEdGRC8庾q/}%0jwMԓ~ݣٓCG]A /hU3D6NCS2Eu`B3`)_HP8HB$&V{笄ǂԚS5s}eM@L˳FYԶ"5AVIW}|Ű|y=/=j*|-Y;V$Qc&)!ĨhN5V_kg-j)B$} cmįH5 #MO¦!,ꆏW~oKۣNgg`%*Myi?3Ě0^ @҂GB,A*/bϫe@jk>FE٧>Y6 +`t jJ Ok)ήFډH70yH4;c8pT{ZQ#<7^wzJ_O}axo=?Jcנ?Aic:p;rYr`uxG{b}C܈䓆 ;0S};hq%_+IcLsM^qñuu|K"+tfќ>j4?r~2i'm8eȅ)R[*3pQk*'& u +CH1S^k* Ͻ^C"| 1[_!ž]Y#RuZc~BNļ2ī^:Z^ FN[/eKZ&` dzT]dϐY,Q@jN +x_"YRQCY, PEï4-lXzFuv#Y|`V폷-,acB'W~H-UW10T n]G%v N+B#K& ~CQx vf\GizNzD.7mCظ)idybݣAi* I "Qe5gMS{o,**"|A!2(I-k +IHN$L;AI^I* > Usy;| +0(z%z:5##Aj0i|R+!$7RQC5&y2'4q\5D\B{ƻ* .GW:ɗL"ܝRm]r,>|Jnmꨚ;G +~Сpĺsab<>TZX#[fu< ?3]_'4+A@yM7vv"B -OE^pF}qψ4[~%#Eem>u@LlhiL냨+b@ )l'qӤAIIvy$ɉ$}oXveVKy)V9H[';BwM*4ƌh*icq; +I>(V(IjjxyE#,,p}g }k5\n9#A:7H6iJߴaCѮL:SyME軣FGcϝ:ZQel>d +wO4Z#ďHѸB>&/mhCX~\z{u6"V=|uėJ_QN#}_\6e^o]q트BvW=<J4hhу$v>}[)i%Ot}ܛM]ٱ<q+#ەVBf9!qg:ii֝K 4ЯM"e3M5iȟArn/uFd$Mo`Dx:Z5j\&d~$ +E2(_!dIO@ "]C8_O_2V0 x9@ T +2_GC~пG Ecbx7UֿjΏV4jNLcC+ŌO4gwyǾѠOk>* mBFV}~Œ}2 +1u+Q_p>g@_19ױ- +9yq_*ex'#yA#XvcŶΨŎ3GNA+- D-GO݉#")f"/zW4ջKJϞznbM4cU=ȣ'\uB^!q -lQAl뺮|4u$Nw +'ޟz+ek06<Mjqb'X fF!ш(R>W,0aGzI(K@s6qG9鋭&"Y!kdV ZPU1׺ ,mE‘91R7A#JU +~7\f[+Z˙~Aרy?~'`7j:i"F08+Ǧud!U5̿KTUb{X\g'2io)ު/'EUn[Z{_t|dd E&~%W},7@<.&'̟y|~G5Q o0@ fx'V6Hprq8AZ kRx3N3"J}8g$ d)pr'!7%EJs~9A{<0%mg QvZd +YLFCz4D=G:I:NL5< +/%$Zi|\ +AwRDTP7{J4Dt&u#x13!y +9kHT:feJb`C{̎#>yW +(-'b4 로#mJJ~9zl0}hI3qNeȃG=F'w%aBZU.,ѡ3c?Ievxld)G14ynq[_e3Dܧ5/N`걪K@ՠ%yzRE97fmhb [_hu7!+ŊE`{ ή>Z iq#Ǥ1z8+cԠ3K4 Pmjry7W S!v?`1-kIȊv֠y܅/ +Wu-\p(9QS]#2?Oײko[G3uWԀ{0~zGt =BO%u'!:qAy,QJiՏcĦH^}J5^$ TV[X9FɆͱ9F*" Z?IkI$`HwBvT![K8"lAOJ_u[g#kaeE(:F>O=A ~~&|'5DRӫ8|% ~`-'sh n_bx1tE QH@%|'r<#x_4VO U'r>xKH6~y>^?cZ)CS8!DѲB tbdk1 a匈B2;#а:)G{} 3^' +rcd7iF>ܩJ{jxb%Mo$3$|i]:lhB!}ZV#y<ek<.MŋQIsWR zqE=L&{P*Vw^t,ŽX3)aE. +g)y.I6VxiQ)w*VO1!=WSpBXȣ쾋2ޘ!٤RhNmab%Np9&V Tٵ‚}#bkzmF%?PdODySKSwFN5C^~SHդ`|N2Ǽ=Bg)rΜa@N=BDA_}~`S=] o}s'E5ofV?=CV +G G$YW9]I[HZ:(Tw'orkC| $?}{0/O9_ovzA׻9ֈ%9Ssdb_^3ҁkR(0Vg\36[e@-Hb_1 +w2I^~n| [<6ɪ7Q]:`%kuQ*LNI3]]_p3қ#,z,T>YGivcԉpG16,SObʎ  ۻX0v]Ś )O<#Q jpjqDBf`ot<#1N#إG35 "` / XvxG-o 6smUC \Aoi{2%ۘbD6Ã92xWlZg|9!{30#o tp$[{ *cۜPT!Q焪KTN$s%۰4׫!fj/}e3 +W`/˵c}4<6WʍSu­-IMP"`ܡE Q|2㞖V pҳe»c E};el3U#7ܟ9]D^#]"7#Vw"IJf{c[5vcS.R-f uEoc ]Fࡧ%'QJWnB;KME~8u:7GļiF +2J"@{`ė| +<#a:"B7z(06VҁY†DL|KCc%g:&N?gnjx8)q1\FGo~1֬¸@ _: bcmg7#I<0<+N~P_1 6Jej5D(w_!Vq!?3irQ.$6IMM< CΐF$uy^dU',W_c&EUdnpâ#Uت"*mПs:NwtvvFH,ZK|N.TgMZ +𤷳8^7/s.pI1ҰezmQ$5HTY8 =ɑf.Gn bsE6cVp:IEx"1zQS?=jH9y OjTv8NBL+@8~stR{/7+QG3$KH^eEWD}?kơ߅hB2I(N*FI x>G y~m'ȜG`G]$Y CUٲp!gPgZ!yG&{tfT,Y 9сc 6&7g`#w6*LUd(b I*5'e \Ms=+[kxSr +F2"2OгF6AplM1i7ȚۏC:ڿb%ÙDUM>3-'\#?PS$fYf&}#~0QZdw!`Q cG٫OFNĹa(T 6&P  tL{-x_l6jL3nJ`9㊚ n]+*(7ѩ܃e[+#%O!>;ձ`y2 Y~L +go1pc-~1x z]6T3CpÿEG2vsM8S)$7( wYE}ɀ$k .RWR}3!bVF ~':Y8xd7@@dV[9Au(LDGJArƜaY|ÇM+_ 0uQ`٫Oj%Ƈ冥!˪^' + +{VaKNqL݀ϯWEOz@t0<8s·`ޟyF)d +syB ,8&}D  zGLZ1P}:% H=z6, &t^LJΨ:zƔ e4bGdޘ=,i+wqU05O:3B%A(,J:Z4C"JMLrDLJ+m2#U~z68i7 ^?O56x:Ot&+M]oC&w\:{9$)+w4'/h@/20Ҵ)U!@$9qmEri3U}hD"aMbL1pV^0;LȹP^d<:uaE FUK~; m]#Rv{@>̀,\C a-W$NIjQw@0~ir\ B+4wQ$w4ݵKdG{Œ-ȕb!'աW>*;3JK<==Ԗ#t2Xq]kw/,m(7WU_=A?M{fz2@&yG +JMI <@L+p۲8Bf VC:RuCxו*Jj](6cB]_K} %>Pn<0n%vג@a BׅdԒS0+9TwzϹ쾑J!N b]7Z{c5v?HQ@RR9Q-{ pm yBl}uK\ +teGb$ J)Jn. PD{F_aô')c5GR0}JY2I10CɮmL_ak!ca`{Ro"I}*>Aq#9g0-bė܉Yld䶘9/[Gr5%S(@IH9'W@tU&aW'd-*ݰW)0cGFo>7:=')sn -aQ_:2^nr: 9sϕxHab)Cͨ]* :GAT̰q0R\"Zxu~ύ y˭%8X6Pwha&9F٩]b85"G#7Lخ)=B8: @fɐUJ/+|I__]˹idnh?jokPeڣ Ccl]QX^P yj"4J#Ea af] #zP(kTEkj!l(+~3"o`G8dV$D8s'% +'3ghOX"ED޸C>ayb#O0NAcG%̑Wte8_c0z%+>%Gh6ICJ|qЇnk{E9ݟ FlQL,% Qo^0,{L̫񑭲 ܓI-P {"EH -[Z4 %aӕC=!pn>{vK]&lbuCc665,L2<+2{3`wh]##IBJa;z0# 8(koT$4?̾?5.F<)5ˮLvG6GqST Cy~W20hZ19n 3sJ3[i˃4!8yUrX*?e(mBJ4k4ANt pƫyZ"7N'z*( +p!2Pa^v?Վ_AP욬!di\b.AS]=/1HS$YW > +E@ 5:uE,?g @iFw|NKD6~/CU.e*!89uK[Cu#Az']:J@Ƙ]b] KꈜڮN66EͿ+&G ~:Kv &>e0PڇJEE|DIڮk$ +}c&ӎ@v4~&h1C9@=s[] *H;(3YssŭL^߹cT si떏~%UHJiZ b^1PX;g~zZ4rNURwpTTy}ܑYH^;(d PJX/?ݓ#"/=`v:W . rŝ9^sŸnTBBZД8( `zu٘6qZ,ƽz0pH4,vtgrVA;K*;JrҖpkZXObdOFt  w&MUL%2`-' *hLyip5HϤbV>ȎѮ@+4MspBGf,eRa%@DEAi&h<7cܮc'dcC IGb讯q>K-x!7UҢbIM/;3n˂w +(==N`1pF :jtZ%lOl2HZVSl=ȱb7*>ɷF}"*ap%-M=Z$rgwխ$DK"=o…+/Kljf] Y;_rD}~+2ηLFL7w՘w.m?s'$쨧TIYH;#,k k 8vA]9h{_+Mx_3v7k+ws|#%F'l _m~&5[Ԑx%0|]}K*U)\YRќJ`/D"!v?E ML]#YAz ;\M Q*%#4>6=P +#wfیT yx $o+A-ߡyhpw@~tnЫ}xo]N +4ÐzJHvhgE=ecIhB|q+^#ѪdeXCkP4I5E)C5L[ ̵ueK Oewyhg]2o!Gs\۪{Ъ$d99?:/;AgQ8iX֫HW*k̶(VZ&3Ї=ze Cʹ/!I~/2ҹO龜Šjs%wMD hY!vbOh[-VFUFd@5ؤא@J팽"mvErV՚tV3)S74 dW|%\t”Itb8d+3zw3Pk疌Ǝ +bJď#ae݆G2r.iu6$R+wP} +mz/`=$+f'[ `0T]j ,;mp8f3;EGY@g.5ǜu;U~,!wYbm _ʹaι8r(rq3bkROqd1zJ[+F.0 !sZΝ!DµZM1S?yˁȂZfbĦ %JW4,H0%(Qrivj1ӕ6d7'nKD| N]PM*~; +TYѧ:!zH.3 uǕ?~?$BS}}M=#2?g_)zmςE7#'CBE&* HkP +NM^0o{L Bʼn4E {ŅW_䍐t俒7"#lKW"7+u ZĽs="$9e%WI%+ƀ^NkʃDSdzR-.Of (gfIruORگ_A}D< .MFZkN;=Jɜpu!YYZվ?55'rMFPQBO6$Ƙ ?m9;(;,G p Lګx6LbGsjMљճR7]cźbB,Y%9dҷyNw +lTiI0יyDw66䆡j-m%2\t ^Ӳ= Aʞ-ms#=~QY{CC{ +q]TNBx$|k'жx҄ z{5Õ⩶ajwI #EeIen- hFȵ>}*a} [}n-^9 +fߝA:D98|fhIgҀɼ2pɠ|Gg/pPEGa.Ԣ<9ۉ ng>Yt/ 5FTAg' oLfRwVDc/FK 7=oW`oz㒡QN MSiLuPC@ +2 PzY{‡oJh3Eoz"6:9ʃXO#C: +o*Z0{^c"KaUgc*=f +M2I2 CoVṠnM8Щ8%V{vTENH@r5Tߙ%yNq^ +.U[<rϳ*B41[Ѥ+UJ2y#Dzs +}b‶p<FB)}C w*T*)T-ΣsO;. 9l%WaꚖ08dkoɹp!8 4NP@l&fIƼRNQe%"{8G+& =#-$__r귔t^_ @"Mi!; +'$ݨ'ؒ^(r[ +ΉFF)> +}}rP:zK ~~wOcE\X6[S(! +Bjk->7::ڋOU 8$~C>?E%rufD #0G"YFw +[!mx7]sMA!wix;Xu߁֥L>F[ b),~D}aZ{䈊hqHyE? ݧףcd7(=+S˲gff=Pb<_VYT{0bMF|X3X3{#PmOeKSU1Hr% 7ȕGgyyobrRc +qo!TT?Gn?~ϡ[bZ*+dEm->F6NEfPk*'0umZ( dˍ- G3?Ao}xNJޭ]Pɍo5@~E} wTvH&9ҁnrbQI}-Q'@Fdơե(QUtG,3฀1D]dl?R5Iө*(IZ iLF{^4>%b\NǸP]Zx)mxeܝ0I)pޗq5QY["B쫖hte@'N֘ (#6!Cb׹* z1Ntߐ_yn))ԍ\(?]?vck6F.# +] UlO/W(O@>zUǓz!+WMd&6t`s9v:)k$HsNbę" i +~i{خ#<}g +6JYlzX +,~ L{I@GePɲ_7s"=ґv{i̦Z䊳0f1X.oX!J.oFSLuˡ㈤T˦8u28݁jXhA%sC3=8PЂA'>eoK[9c*DW_yd9_Olkͯ^w(!_s|QWkV +KVw<{PgJ`| sJn5(ux%)w m_ĤٜH7@K1te- j͡erLG$/O7 JHMҩPowX/.cE ɽI2WR `LAЎW͕ q{U 5CZoٺ $[9__Ӂ>oAM+=_N +"!6AպKoLOdCy 4V25Xibi]B^QX4vMV\s!V6;W1G<^c"U(;Xjhޛ 0w^yLKja}_؜ǖ$bJDZ>`,š>jlPp5z*'}u:~31L A$f. 0j6:1$϶ȼK*pu-֫w[tp:Ps2PYfSr8QK9%*v$U +M;ìھc?=2Њc-7'= aGG}>sq{iipxx|mgfya<0-6{ h_ ijyZIAv>BXGî)xABT&@|gV20wI=g46@%-r)oy7J*z.m{_טrN2QUAfK+-~QhD@v=\U&F9>O +8䱲iq!qщ #{@S#9=!D_p'U 5}So"j yM[h#JsfNy7,OL/RZg6\jNEn9f\ #PgJ(ZWK+W~# my(,'zmukI4'DGW8U-SKmԧU"47H(m} `fO8 p.[PQ{s#/J*02Ա%st[SkAqw@s:˛0iZ-M &n[dNԚ9 j +0Dk5rofK\zނ/-7< H0J{nšsC!)OZ[1 /۵E/b^4rfss;;t` i>yZ#q僵e_%E>س7]564!]|ELBd#͵)ZA.D&2Rƌ K26IB-(!v~H.é\I qvhwg^5dF x25'>'#̹7 -%R<\,uQ;mJ0*?%fxtSUED0Z&r0_*M^|#9B,f98do"]{:C ? +Aےf/pD|Ƙ7z N7:R/X]O:j,Uiɥ_h#s֒5䉊IYbΏ30 +ʀͬaCCjDf:(סACP 2'?l]Kߖi uAh$,[Rzcce7nx]|!=hG8ί زx*3[S})6GVv+Fr>殪57r%z3%\!!0]Ę\GR?1#hAPE*q;6TJhۂВc^+!Ѫ"Z{7b>O+xί~ΌST +Gd4ڊS8#!BӘ4*7Ѣ=: Y1<˱¯'vݨ%!unVa:=˄+n*M ^,p^NU%VCi+DSwYViXR #0bB3JW! +8eKaj2q|~ nI!x")b.B7 'Wd銴NHP&AS@'/Jc*ӻj4O3)̝C'H HΆ!x=2$+G6@FmA|qV.3mOu${ B}&8(Z)L("d C:۽k +Y~H)k^ Ě{]xp7x̂x` k(wo%;wt4 ppǠ}\SnNyg1oXek}JA OՐz9LǿIbyQxsu- +)WML7Cx_E5FJ@GJǛ VeP9+0ێL N$muAkީMR C4:\ +{.po"1I +2̙='NpͰUZw+8 +齥''s-`Kl8 +J4Ͼ4-85]%83n +N N endstream endobj 34 0 obj <>stream +^L:SaDea85JVLq:#ç<#ꌁ#4Xr(1p.\*51vXo>Hݮs%Sd ?yJ|"8ToƏUBZPt}w +qx!=[Ġ |X-ic^1y:ʞ_pGLi2 +\D%ς ̃n?#E^nulè#b}Iiο2QF]QU[&ͩxaujmQQ3ةOs+iekD툃@9y&Syc5#OYs5E#ywAm}i{5B=x;{0K}]ꎤJ~f@HJpH_\Xk/<T/bǐ!K.*^"lQ[]A1=UMnz d7eiHdXtBKf7B(dF( pӅd=o+"|Nνr-m:~?u 5J>,z-ŚSG苖Ҿ~'43$fm!3}X/: AOJ +;2>$*H 3m5d$},Gܦ5YC X~oi͹BgG/!-^gmn->Gc1 ?Oh%=ݣ=" Va..z1r)'WB_qU<2uUgmg" +_y"P{jBDzOм5y~GVOuh€a0]jOlqهGSdYQW9,bOQvRnB !Ww~g "h 7aگEHrYZN`O +htQe9 €$UD8l|b" vPc?Dv@n3!xyy*G 9`+^2Q;bwՐF9 m-W*zn2A VUY叜Xؖe9@46QtA_ z|6# ZjMD }KJ2u-N@v>#-\ : grȳ<Ȯ}PDa._@o>@ +4?=.7֙dwT$"q _"LGY} +˯6&:^%EġyO?C3$jJq=‰#02šdokHtGrf\#9.Fa;躼r@dLҫj} HOn\g! Tw&he6[> [9O(M,*{5y5^:&qTӫ?rʅuhF"}_`"rեoR:eGrh*D|'mʀ@ւgP%`5VfpZq}`6GXLGՂ-Up(: TD_9CmE# E .IqfW#33'2%# +zP *v|YAtDD?-‘=V̕w#n}zJvFКH?R3 .lmYS>Jy1U>݃geb!C]]&L â; 2"ld :EscU]yT5y?2b֬ rIQH3[p9HPwiwyXΐ2E\-X:DvQcgs2fR<M>.W=)h݌8dg!䯼xWHM\'ӘoK6d򞫗pzr?gAO೯fq!{Q}@lgNB@DkN%p=RԭXW}4fĢX5tVgGT#}QFd3ᵴ uHxվ1W^1n,ƈCvPG Xrɑ`d_xi?xɖ + Yeդ3ՕJ+7iW*b]lDq0에Հ1Ճ0F]͊t{ P_Vw\=ÈЯ+W EGg7Y9Jã`K[q\4-ks>vk8s dZM)Ɂֹ\Wt"TMz̏ɹY84)R#ȅĸ ǐԝޞ{[(З3MjgI8=T2 +@@UŁ"HޓPe{,3~w/:Xx&X%2)UZ˒gqˉdH=7z0}m6tE]25Hhk#/zLF`@ТLpuYINJͰE_+ۉI>Hd.ԥGJuC +Pq a'lؖY|!`"-M404"þWg<;efiG< I.$$WÏl`{ܿ7{1̸)TGݯvĐ{ZIWW$Fϕd!*HcL)Йwe2;9H[n+ʘ-޻)fL9 ex {.8sg8@O"lf=xSZ)lB؜OZJ&zwYz(CxkS#v31K[KEGPvlEK`X[xmV}92і6$̸Ƣ4+i?pt +sVVAsN@̗}(}} +H] ^=m;W^=#G3TN=})#*$jw#f* YGi}|O”-ٞ5MA 81؎zaXD䷔v"XFچBiݱ`ұx6F浬CԾ) F=Go:~%8߰oKhX?q8􅢷ϭt< *BEKG+u2j<a#w w|lfzxgF^xy& >[ zejqlw@Ik9$[Eh&u\Di34qb +lF+6w;"+e~ֹ}M^4B=RQ#Ty +(R&"0fWlX#%&r־"mi_yH205Dk&2 4# ,Ѣ=j̬W"^xrպclߨ^!|w +{lPj#G(RY;^y˩_!h)j$E|gXX!yJchnTOL!]J9A֧l[AJk,=6՜A`p^)ay(7)&dq\,'wdِ#Mn/Q31C{~gN1fu/Uk:ނ;"t xjO?8W|z@n93؈0`QFL/I(2P[tDOhi#[rt°\LT\"^O4d'3gIjJ̃>@e+FuPdnCPS ⩒ 9s~[^ပ v3 ƴR~^7Ž&j`z; #r#sVvʸ=":T̗vSL0nvarth{Qi\LjRkuө"2g%9v.{CtKvN+"N?bQrI;CCdCOdI%A>-J7d uN#ݥO3s$Mq+f3J{r@6ydA}D.]9?)aR,b)IXnۯiD$ͩjzۺ՘i/;w+Iemm}\%0%X!3Bx=^rO̒;T/5EYﻰMH*Hk3£(RAs@Y)Oڣ[ϚEpJ8Y)a5.^ l@}U`) zRrMOf8 T<G$Qsxw+pr܊g/EC4SɃt/u8IV!Dzp@^ 5y:N[ b }_z]'ΐ+9浤W6yiۖ dw݂>lYn P̓ lU V%9nH;BDC1RI91k<`_'i:lѯ&Q8f@t9G Q){4zboeDBX~or,@ϥ +"CGY ݼ#~ z`S"Nj#tRj΃+vKq"c%v4=WsOkϦgr@ye~\(x|5;3 *.CN|}z& 顲Րt؛KQcʝn ޙKm01~'EMmF%RRc_hB0 Dz>۾zxhjs+т@Q:]T޺Ey]x;j?oUۖ!1EN?7;C"P Y %U +ѠHUr iGF ΣU.‘ +{0҇ +;BͮxO/`Ѫ+n:1s5 Z23[w$ or(Z[s㧐fp Z:w6@Eʦr$'fY=I_lJ,据Q[9ZOmJzfR,Uvw&j^qywʾ!RQۢ3FO$ +\Ntv)Dg% tq[w펎hR '%/:AU_*&_SBb!gS&f$Iz>4Irw(*lA5懮!8H)d{UPQLE/-kے HJr*u)kKOqǓ$?yF% 牚-IL> +=N~랻_۰~"۶PyMɣ]p.> Swb&R3=I:#c )a.M@STH6Kd ~L]܍[(cǕ"f't?>}(7cꆯ7S'ԝ'$_kuZ41.Hg2$#jbn9E Xae^=t1cgE +$# +s:ksw.5Xw @S3$FZ#r<1J;oo&%ԍ>n&BբjȯᄃkUg:I[2O/;&[|Ali}1"9k70}m1`!,b nNqhI7ThvI5 +хyVi(f +)ږZ}QVE S{]HL% +%YI+'v@$ZT^]}fTh驈b LaJ>:ܟp%~t~e ۠W-Ҙ\jB>ww(T.顕S L'(G=zmAM-Qa]os $Xv +(ݤ +f޷5ag-R-]jGиfzy]d|"O>uTp(4mѧK +un(P%b?Zf0w`gK bP>C%7x}_L`m-LгRQ҆t/rJi&k.!'$#qHđ\KRR'Dpǫg~57ԋqWP5Ɔ,Ϫ:_}VHҁ,"kHOy:ٶ5DBxǷၰ'-M'K!)/k)m$\(i'ͷ HĚe%Z" +dήV(U47u8 +4f̈́ڶ Vێ+.I]6IФVAƥ%}D 8QJF`&ѫǼy85+ZaΣ`}~DYU͘۱kiu͙!q0D,63" W]ʭ/aX HSB\s?b>\6Z=2I:Hg>g0 |LOhǵ5BH{D>9{ rCgVyXvR| zCVEO`;~Pr{Vd@R7HO_9Ϻu/Sr [l΢>=?ӲZOQI؊hn=mgnJ;gkTo?i<S]x7n:* Ww@:ˉ61"bv*Wgkzg1<8TLL:qlͦ{>2j,YXf\-4Rۿd|R#1e׳I!X+yHa7U`mЍ>ru9BoכwQP%yL[Ȭ6$}]Yn *x3u'*hiXKwQB3D82ܼJd\bIiOq~K68d;==u"GB. qGRU s}ng;?j +fͧX]:qG-wBi$.E%ckpxM(=ښc°Z=~R[|ɺ|tƻ`n /1<+-j*ӫybTp.RubƤUbߚ{f ),u[8z5n@H +>?x{﮲34 1A:{$7GtҀaGP0>z96 b--D}:~zT*/;v0 +@2E<2.'x.Vޯ|V;;̎( +I䈺1gO ${,!WvҨlTU;p !YRFֶ6mMP~ RT.g}E9FVu8%^@QiǨ~Jxjsrv"3pml{| G1n2;AV"wRb5L +>j1m34a`v闷P_E͠]oTwBPr^j>aB"ϐS+j> +F%5\Pmu%/? ai5 +<Žpopǝ B),. +|/-/bCza ;=-Sێ,Ȳ̏mPP8848 &{}m[Y, 'ygjA趇FiA# 8*|hx0֣{L wҍ׷d\<0'VE9&1&{;'O XJD3 x#nJ])6)ӏWX\f| .oqe8hFgLbgJ$*DTT뫮BD) (Q%qTAe+ڮ$)iBb`e3C^Tf\I< H i$Ec݀[=]#(!*n{ +e:(բ.+b5#Q&o}~p|W~uwEhm;ߴD?KA ѕTY쑊:kp1JعѮ^**[r A"z8gBtzc bkOKJ08f!ܼ؍cMg.,rc>gGy!=p&A6:u͒yk/ 'vZ;SD+od 3Ie*nmf#u$sІfTXe*8x8pE2!ˆhIk*Q 9CP+Zd0%<ųa('b)衾کs#d"fV>gKWF +M@XsV.2\C8g4kra.|~bKNR 7!96jCh {*_!_ =*wm US9ǐs +>d58V~5l^(5)̋+W\`n"+񁵢 =yzd=nc/9ppS<g'\sg_dsa1F8n)D&IC HIJXV09:G[%yʹ[\6"F273,i~Pv,4mqI. {V'K(UkDc7FAP(QY3")ؔ1W64ڨ]lh4ADwN9 $v1TC@pxΠ4 +8 +Cꕁ1w,WbK~REyhv\ltXN-ƒD̫7ĹXձ7/DkL;yKFETvk9g A5=~8mSU^ &8b+9qRf/ sSF`7vۍtJH˓\kӥ-$7$< l| L՝C?V;qyPYt .eHtP[ ms^ =׫!DTMvA`MGW$"36[\[-5 gKRާwpwR}PaX Q1Y;獓g KuدVԛ$W{)nH29MrPBL-0ZOO4¯O,OQY0E*3H]fx;=ٳ@RWKQ=ju%xSFЈ10!:iO^%Ou+9C{`r.< + =kG{F[s&9KnmLMO(_@nPO9{ua+xmO |q?_Y |..EȥR K 9%͈!çI( +a{:O +,\"1C +e<Jp)4PZŒ״0UB18u`VVGRp yK,fב(v+W V韈WNqP|z*gQV#G Րr3ݎgM2]ePC %;* sTύpuhl?K#2ab.͂ALEoؼAbLxXpwp`8%'JY{3펂LjǙ$F(^)bG~[Ȇ":cǜD{݅%1Gwn|Xc7RUxHiPyG HR͵cqjDFʐA>f L4k,ebTN6]J)ݷRwnQwPl00GŲ ڈfQX@j x-m`*k;c`7iǥ5!R'jK[5$"wjweKt>cqeO1QJTo{%qR 4- !L#z.|NhUA2oDOO;tj+UTV"y{!_7rI~+_!L*ϙ ]udPkwã!w.˜ŀqCΠ\t۴-=J EةI$=85dnw&*ȶa1sݧ +x?JO !5z({d(d8≠AՐDx@q&33Nn+^XP6q">vTwE¿gysMX'M))* Cş XQX5}Sy)2dc·v܁Rwb*:(\f7{=P {ADD%(G^V ­:> = +*Q0K5,!8MwBQOd̐z;;5[b;"+@[WΆ`vMe/i$_'ԭxF*ڏ7?>&r8'tqckBho"Ϝ~T_".$4F]G2QG#ƞq3E@urNr^H<;^7I*XD!@7ψrܶ7 +_H 8Rlu +! 0;EPbfJIҪ2p8v'#yHSpcKr.bB^E*AITFx~Xڣ&1~vh{1v +#=ֺ͡*N4-=[WlzK O7[gDr΋DzFnn'9@KgcVQ`_<^*_[,3dnSq~Y[ +(F&|TkmZ5yQ׋'7q5Y=k\ + ,Z>*9Bl`HBuOJg`J[32@ EcA*7/Hv*AB +nK*lK;Zr3A&+!8c܁ +R ms|vd|~IJ#&jcN: ۟Ou4h.ȶSr#ڴ,n8*5DxkQ If}#+r+k{1aZ=\4+x4_@rѸ̤TTvϾZ+:̡+[5{anګc +$F l[&jAQVzJ +1*p ,2ICgvRTKXOUSjF{AbK/¯K/gRU#g|DAaJ=)̩ }buTrͮ+SaD&mӤK eYS;JL oswlI z@*BF pfW"%UIa2ïvhN7aN]qr'{ڑ{kJ1mš߃>dO8TKp(Dt=k#ڬ'Px:gew4rLa}upBԐ_7pX? lCҿ23rT} ~XFqEe~h] +=ƢfRP̾J&@NCs9(S#_>^)ߡÊP[$đ$MZC:RoEK] yڰN*92WB8YBrʈfTglYTJ}͇I+Ӝ.`-#fM:tm kpR .~_=L1s63j#:h4n< KъY +qaD9Jvk.H٫A"0~*5$eSYHm͇@[CgCaQȒq[;mG)%CҜ9"( +.12$%,SvVƤrq_G /}bTx{͝'+ ]K«Juwr)h;gՐQ,1S~MVگ_s= d -^G(֑7 |1|Gəi3II2p10e0D1>E g_IוZDB&P_ZADVO}GdKkϵ\w Ad|_{p]R'+0G쁢AdgNy] v:HO93.Rlo.2jo;^5xѤRC)Ã46x tK+=T2s;Ma,Yi$]YF}?7_9#D{.ǖHcG:2 +HϚFPp*6(i31 9Z*XjoDDlW?#t6 7ؑ"$]'ἾbS$ Cb*]%i[tH0>ˇ,JXe+ Oaм.jvBw P7y 0c}Ġ y(RK~>0%wx'F@SN[V^|@j:ZìWiH#ks+Ng!c|brֿ>)]Y&ͷr+f>-F=  wx2z+i[HH3!@ϖN' . b燩6qAܖ IN#K]IO<>i9xǃ;x@/@HFQ;}nP>zy]rmo]gg4(s 4mc@*vWW➰ +s]J\A(N +S0 k "Cg3J! :Fp>{zd@xw"NDD3TD}95tYu +q./jJ0hzKxuT'UI?L;TSDXoW1x6G)n!( +s)J!`XjhDTשaȁu\)_\h/z?`ykPPL7%eݹ P I{&ϴ68Q{/O +gӦq׺ ]`MY;ӫQ|'s] ua|mvR,J,WqEiCVZ[ +/0ZlT7VThו&)@X12g,jmQ%$n2mY=fE[f<rF0)fEcʍ,1 Oz>}<5!]Zَ' 1YuLKPj*6jC`PaMEj\֐v 5$GaWl֐Kar>v]?q/P'u<#$>zGʏ8|S%ڕqU;n/O~kwZ/$$TR+[3bj˗ukpWA\O7eCl +D➢B{dR˴(gU!eJUj 0N Pu]9sՎ;U8lx,:-7bb yZi䩬DGT  +tGQ}f#bd'`miZ&{}!CPԼyjoHvΎϗE37`/%|6I׳J 9yݎ@eDsnC.u3Nj" qc:CpXP@Fte1C\=olLIIL;;K@lUjiʯnPGmMadIk4U_U5[L[; +O\#C&yUY eB +!Pg 2dgN t}ޖts ?C[y舺!Bx4 $gZR'I̒a؜{GNiX⊕u'~-X۫ىH$T̿ivЙc*-7C9@ٗ>shwz셸D~]HOُKwdQƳ?n(NYY쥒)BkA$bI5=|U7Mq (0#Hf+:> + |/Y3 pto*L-whIG2sqiv"QĚ2CLkf5r}+|saEkH"ѨٯAt/B+HǓzD+)ZG +yCuTQhRrpfyW6QӰ n^-PMȑ!oFlmoYxR+&O%g,@=d=Rs +8F)$o36N̵.% n=ޗmUj|ml8+oɮVPvzW y ƥ=,oE9d3xrƉ<ٳDn,󐓁~yzRR0$dwp~ŀF3g{I:$5,ar7]A̅.ԒnYt=/N=Gt7V>)x ؼ Q"O,'7@~iXh]-%5AoІ4e 逞#9,I +`u ѽ_)Fڧf+ѣT "2SٖFJh%mL>no<&;ڟ<ۺb M"o +-o}u>= Uj&Mbgڦ_*rW3ٮ,bW\|UQc Yڧ7J|+ۏ\Xێdt5kH 3a׌ +wsoh$8boޘcD@~͸h㞈v~ؚDEl*hG٣`Dϛ8ȖrSx2#`O?e}qLx{@a\v)2gzA$TGta!OjT79# 6nCr-Bw=D(*-M1u)ZHEQSyZSh0f+ $_AuQ +)FϜV:kt[~]`:e0+{W/R7s%)5ki9\J{ DSEfre0$HQk Em3:wdJwԥbH-t:U= Fz3(RƐG axCQC^e<s)ݙˎ9kfi%u'*)ʂ< DPnNӵ`hR?m1bI[][db f3H/f1k̥.Fà5 (;JC)vkFtM"PyX{>\B~Eć`%/eʘftH}=#J[~+@B7+l%W:(bφ\dOֆY;/?PIE{FI&P89jRu)REu+C4iK ttf)nLdAۆ +ͺ-MA*\+Zj_,Hm{ yp +kzQqA4 r3 !- Ve2m|Z:5`(C*$f n},js"UQ"+:W%ڝڭQRGd_8HtNGܖœvxI!LZZ755EkGzcȫys.r&եA8h,%Kx(BWܜTDbԉqK^(fzGWa wmwn)Foml0 +Рwo/MDݣr#e@ :~}՞Bl/fJM52-[B`S_VZbRgo뺺9]_}BBmHפ@Tz0FN@P;Yc.vё4I +n58gc^t阮ջFiR8+>ӂv֯L袌(6=UB=Q-nČLt/ԃ #( J+ z82&S:z*jSbp/sO!1-x(T*׹~BRo| +4-_q,ۻ^ '5F|LG*4TlF,ɃnZ~D[g#:UH4 t4h@v: 5K2b,:{mWTןπgBO8 e +\0437N(em jœVG>:# S8KxSfI%N2jd$k("VQr,gx-Eb5{*Bu\ ]+;6HMTBoW;Bk _H.[,ļp%3nQP\F[Z :@{K_qd77NBk硆%q 8tb۞SI6MCkEܘLCq׼&v;)%2]tt`?GmE!R=H7>yJȓ[~q}s5νNP +V&0/:V}iDE/yK0!Z/Raqdl:UGIF +'Gl<ܿ'(]?;To=v kN5 +5̳P&Z,LU6Cq{ P]  SBtԽ(1t*+j[# +)Uvkb:V)x> {*g +DWk#%t_mZȏuFD:l=>agb{)T~(́ hvX#%kڄ 1]Scc-FP)sGBto:qU0 A!zEt-yfHu=h =g9@8G3k$]C=Mτ^H{ +e[vgz}R!apvEMH;ڤFeG gGCpC *aǛm[ >(Jbc8Aؓcd-L%3${3B(%ue=R"fij;Y-G:'c"##LFחWV$Sd=$`2R~'w򵭎9{ȩI +ľ4`xi#+ï|R-萵dU>z f1u) +Es3n.fhWu8R۫%`#1D#3>%:\^Xwph/8Ӭk=J("ߊ>4CԸ`w&Vj5kk-ҥH5'>@51']V5]stwG88X/wֆ'gLD+k$SBIϔ$k/:~>Uן*5X};/ƗO414*d-;#Jh!0's܁!GX[GI3-)m?`#;Zs|rWu1Aȫf@ӏgg36+z{u ]Ը )̕XFuSI{M#tf`qfum>NόOCR邓A?Ԓ3kA?y%_ YZ싏hA܎oH,N* 9%סh^u:L>vP]xW ?kIwqB{Qw\pB {W[MB.7_[ja+˒d5|l]Ip%v1\eՀRGџiV 5RX{{>fk1bD51n 4`qzIWh{x׷"^Bx֒XKP!jrį!ΎlH uk}"۳ ³3ԋyҵ) 𘺮΁KkE +ӻT >/ bTn0jPrళdFSc(H}_㰗!mR'=f^{ ZKjFb%sDY?Da7K +1v׈5FഠƐ-%ɫ)@tf z}'FGh+7XWn{0'%noC'֥͊o;kQ3R٧'B,0_&wGm,nʥe׿QwK ʦ[E bct#kzan䈈SϚs#`0çڮ- 'N 9>oH֙{`@d?'$._2DIiGW8̥V/X^q8ա9x0)kr#"aCTћV#WMjoeq@f&Q*0Rl}`Y=(Ő]";$k7%z^Ǧg2-E/f +tT\iUƣ7?r 1r[̺yERc[َ3nm83~VDnbXQ6{m39cÂ)iOx|#GCR€6*4 +C|-t;_5"%* A~U!B?ޒC1h%%URf8KLh+2ŋb?^08zTdzS!l%JYYT1)ָhdpA_AnWG,cghx k )[WVj!/DγV1?V0*<ң}wR:¢_ج(w1C3[$~W2z:v"W-nɴ[n5/^->1܋l;xfMGzAk"\K FTh~;#ΰL +RqPڵ,"{wAx t[OMWJ]㫁,eڑɡ{>ȟLݩtϊiEfh"^UC Z q+ +W +{V~l&a´ 3zh +m$w}tX|T7n.tAD *Gp9"L :-Hs9Y27JЉxEsk;?N9kDsd9c?h]9$T :U}ęz?)Mt{U4/ ˍ~Ԥ%]=Q4J6#Ku{FQdzQW*i2$}V$*lr܏XR03 + +<D{relg1R;0JI'\"]JŚBlAYCt&S"Ng2^{ P\j)jEw=tMV`R 7Eځhz!=~Z]ߖϓVhzA{J(^ aR1~RMXUjx 7+f)ӉO~2h-\R3L\K=:'\9l9kfv9(P0K97TQ˫TO~p?zjWNP>ՠZ<鬩NQ[DFTw\^02UKzBVaҰkW0gnuv#$=Lɡ5#aOߥ1Fv9<@eUYGLocUw:#^u-dS7Sq17LNK'QBrL-ݫ!tD m#6{oUmzN339[Tz2Qv5pD1ϔh|+βGwţ2N#Ď3Rk"8"X) a'1XfޟF*US>Od!p1 +mg\aa蛜ۘ +/[ i}h|}l[67 $ +5Z %,XϿ&`b֥K2a:R[kV3 _FPx2U^P|:秃JCcV"`dA2əS@4m~2{C;l|meov*r^7E"ر{)/A)##B2n -=JFh_E&R0̅%ݙBqp?߯t'*~|j?'=ίqVf>x\&H@%jʪy s9Qx͙)kS/6MͷKg#ؼ#ʑ_ms40"̝N1DYa@Lrw$՞ 9@Y H%]IdWMzZ9˥&/QK7=\n3%Ź㴫"!!f//?:~m;2CNuX֭3D,C0#NS[s_3Wc5WlL[:tgSlnKo%3**|k"'(#Vҙ9{Ҥ3f՝:a=m zrHqV8ݱ]Qo=[z(\j6;m~ S3\8e{(XL.ެ>ɱ1M?ϼ?yL'h1CYdK}3Y,Yh27 kV ~:ޕt)䊂-7%w|FO*J9 ٝL;2O(txљvk_ /ZbQfp)z;V|q$#zow㭙@~`yKZ@m+ƴ_(%3BSS+uSyW~qtePyاLSME135~I1NMk<[lE_(H?<_,A_̱⑦J⌺),W)9" ^H*o~?g[ե@;2(Wx^)%w>o>ҧG'H.wi/D{}>» I"id``Vˬ{+cLa _2cb:><9T0q] LRd?"EwђjۤX[vp(a曪jtQ4#عggkPMj}o}?$| ȹXr-`rb/L /hL{g˳9kd6|Dl],kݑ#DS5N? [s#ͻjqWـw͏/f2fr+ +:Ņ 1}3(LfEԩfƪ<H)i8͘HUYsBɕ7gכ7,m1y ׳53fTAQkR\=\-}1$b#S >،BSCY * Tn'IDT7ݴ sz SUw_ʄfH9/Ԛ*u,-hzgY@ 3mH\drzoz51N69 +6(l\JrVDj1p;Tg5֖%3q[1RyU2Wl90?dr]DEnf鄜+ԾVqN #\Nmh{ +2OSqr!l@!s4"RgP{ow\0*p  oMQ}qL@!%;9WtT^BN5v-] 3F|Y,×#y0; _{wI?O_ + n+Ns뽜E!m YyH%vrX_IWYv1(M 멷8z^et+"V6ZiKB;h3ˣh q? +[")4Fo'e5LR[41~Bf\p|v~'gFH` $9 +[W+tr@z~\y{eZQ\9P_+s"cu7BH\<on;38"wXL3@LihsM+z=z*QƈP5"v3jqr,OD YjE(\^LĻ9 HVQTokbū1`7%P)lm=^?q*H) +x4j{^VImȽޱΣZn(޽bGZVuאjHT {9خ9{ +k^b+OWTxó..hjzj9\NYh?E[oc?XwdpYlzOs) pA 1Qgc'[ƒ^ +イ#B{ F)(EJsXYOpT8f/x *;ޛNemnhIZ65B(ywn@vC$LNznd3NBm!>Ȫ5 1׷54$@yF讄eDF}x'm#a!\hZ5{4ZAF*)B? ;уIDm_H$tHIp1EhT$Lޞ|Wؿ1G`3sph6d,߾J@JL A:cڈWʯWI0A#xI7 Wp7W ^GB&z7:Joub`!J|UH$DQR(PN UCOY#g@5*-×ځH[M613LWح`{(H^7@f^ΊJ!_6v][njdNq~; WQLz>8RM4 +h8z.N!yφHH*7 ݧa+:*᭏ҼA֐59^\ +o`U,#"v-lO I^!TCllӭ5I;[N -FABYhҀ>~iQ\tÃw=@#z+2$ y=,+/B\ +ZZV +LAw ̀;=dS8`:B^f#X5/tJB]V Ld[WD4r\J$?c CkS? h܈Ϯ֧^q"(D:2Y=MGb赪,j!)0gA, R/Fj ׎`!zvY:g7l:Rxv܈/Z?~>*oϼ?sh䞦mWy=@$;Lge)?MuHSdJwi$=zpŁ{[WjE:-Fbܕ+K뉨e.5.6 +M؈'oW1cFz':!uPO' +}Y) R#[ (:[tm:v1 +SjY}AbB_5}/3-,2#y/3t~?c6ox<+PGYِK1 E\c`+zkm ŕ@IJNp!x#҈~>WvL7u& %S#`FU>s[' -"C;"'zo$8k^=UH`mjQ%y[Y[> DJ}ܱ[ +!;7Qv8C}<=~l8+n(?̉*I=RUS3W:L*1Ӥ}}}4n}2Cr;}gEڱ?X;vW#4V)r0nh *%}^'Q 9Sn%4u~߉<­u՛]q/Нl(|m|sl1z޹(kqydw8"!bQ_QC&byUQ1;</;!I!MDɤx1N鏫!t)t!ۓ&Ixpf߹*kDT nVxGK/ttb:; +;{|x{ [d]i.`ey?9BC?#w#_ѝ<jce\ + dlYS ='Rk@:hFrM +!"VWsn6cEmaM]`ݰE}dAx7MEIP뙠\_hd+p$Ndm`~A0@+QgqKT%9b4\)SCYE Ry}Us"D7j_ Pxe)Zխ|yZm׺N-22-.]6*%0 W`-](J'++T/`5 hC_?_Σ!b]ϥ*1C2czmH{=1fԹ"j~:du^ʻW{+'x8 'AxV jKTk*.k@8Fؐ>EגF;0N2Gt"e}E8+jx{lc~Ar&iݫDH[+ {CUwQ=E0D,6xeˡ$tA_1k9Q3#zxYW6x#4 GlCWN|] Lvm%mXHw&dl24pI.O޸xx]KE`5CWl9*kFGT4d冿#&JX i^jU9 Uf2Ux,l'a\!q: +#wE8©\4޲,O8s]Rv@U]S 8>ADR'7B%Po3D)U(MCh?BQyg{{m :Rf + vėmGBQ  }!y/nۏ nW=mI Ho= K8Z(Eu>Etv478ckwќXwG}ÓQgx~d* hI>raG}e2sV3$Q7Ѻ!mF"<}Ԯq@,dF[Rvheևxi`t5DTI`2'QQ#4U Z`~W촦RB>p|!sZC?bS‰T|!!냲jhLT.ƈHܗ֊~*~MRQ^sD1es`hkZOTsPf#^Q,YY&pg IsDqe^)7܉rFty&7`Y >Ne*g:^Կ'ǎS32gc~~ޱU>gCʚ 7CYhu3sݳ^T)R45,iQ@|>~jӉ ZO%t7-&䒄z[ t(v>}qAĊN}ڎQogyd μmWf?'hČZ6%^?bzE*>yS3_G'$̐W%8cV~gT;CX7S!m5EwZt!;t/T3MS5> |ћxnKinay%(]:-jXWTqsT9uF%`WWVv@Hkdu'k〇^J5ŖiXWDhԐ*:!joEQ8Lk8}7C򅸑!,+\Gk53R7onV}KKJ`ͯ*.jGH0"C&0iT1!U흒cQ:WH +X5dmGxߘ>uzn> `ztDJT_^BuIY}"'h"(ERS50 ؍HxsO٭w8:<2".?I)l䰋DQ:ujHc~` y<ڪH+iFn{)݄ ˜u7!@6 .h3=~1#NWiNͽe㇘n +3pϘ?~}[՜rCb![yO;~ +AMqsIH5|^%q6Ea A #oܠK ag}_`̝t݄$I M9dI= 0ܸKB'Nd!QUgz{DjeӀknmUOzeVd5o+&N'{n&jlH۬iX#D5s%{K^l/]{'׳fAC#Y"bl +KU1C?#5hvM hvnvݔ~P9(#_5n, zo)N!=g#SBNCEKKS>Rep*GjРC$jHIB c!bp4d_WxL4^8'Q#np+-ީƭ̃NF";͙"oMLsJ柶eM׾T(AdzMů:b B.Υ ?ߣՋ+p_,D<WB~~D*@6.,]J +n,ҵFcz}7.򖘧 b2+f_+c6S%8Ϭ{i(\y}8>-m}?@ud-7Z($ z\" TW +\q咠gZHsβ{8(r)& ^т]}Bl@I~y>5;R`G"1on<"!2'TҶ{kPmf=2@$ү~T3"M=ۿ&>pG%9`v2ݗ: +ݮ.;J c#׀FxE#+w{㾤FeK1Lס5$&P1PG ë]+,^?:\~vaX_1|E ) ElfBK4spEP(Ǯ|6]ja'YT!U xO5#uN$SW6rhq^s*9|`6q+;6FL~s"nxYf.2&5BQj .wWhmw_~֏bdJǩVQ9!hQYZw01_9uu+1ڇi'g|6nMU^Ula9dd5HV3[8ʩύM[vOܢ{\]ROqۢeB88it7wjPgۀ~BG)e#iV:OM5/#3amo헜Ci"_ڠL@oCZ11Ž[ H'`Hӎgoi;9(ʔ5|$gmI 4KQRӞБ^;!p'Ʉ1c#0(⧘۫4Iqg~'Bsv$$؃vnW8;~Վ;?v?<\Y#$v0/ +`L_"K"qSK &Dѽ̕}x5 hdy&.yi{3m룰 ON()'_(g0 ~G&1BgPoBRW2^`[,T5ۻc C"9Ʒ6=76s)KMN?6_ \ڿz7'"om}P}7AÃXiYK:™gj? ˜>U#DcFR,=DJ08|hS>DRj%gk;m_m@[1Y.IS،E|eo6a+E}Uv޿* 2 :x3H,M>x$Ek2笹i-Tͻ(A%С7.!qx3^:]C[U+2]˱xwDTk20Iz6}S&Kvܾj9س3M0笟@¿K]ȭ&o"3j ۶#o.2)/)G!_ĉϔ rxÂ@ 4ܸ7Z2u3 Y'n]UI_[kh5t]Gi:]ڽ.Apk<)Eh,GO%jxB3(P#rTZ[rT~dr1?31ow#YV(qP1o_쉔vmȕOxo1{t#% u_z2эϺSCD1kMJw#WŞ RkC +TRtMԑ^peL+iqwt6w-Ȣb%#@lĽfM"!Ot^y +s˽/e{?o\}ҢڙFW`9i{DGOyk k]dRG #IZ-<ȷ)'J|FG1H`6\qn#g姎,Z}1Wұu";h'@Qz3ku By32%%}X@v wVԞtLl?{*-T_GP̴ ;;1,z{oED SɄvPpHaJP{'&> +3S(!>)z}xEA[ψ\z19˗)A>Zi;(tf#jmiWy^q'z;D,@iKL"$ דA 0JT*8W:Ah57'R/|w93rBْ'ջ;uHX5ȴ'Hq!&~8&cXWm+1W/իOLIQ=.oА"GZR.:8'X'6͒͠ 뿢-tטA#/]%G,@"գ@4gEwy#Ei_f< Z7z<(jjM-GD$(5#N_!xz9 + +@\_(E9_q[vs s2$ FGwFc?y_' >Eb'=U4@R +>g7ɂw XewPEՄ|DfXz!WRL!}6rV N +L #X'Q(t$(Q ˖ w0u{_hY[1/ө*]tߒ_k[fԐS.lR K7A +|Q_+qǬ}tE|!$j> o9Flw +0Dܙ/ ]־)ԺTD'"hrBL^Jr'\B'w|G!S%zDBWJJMjMeRȷ/͑E]}X#;[x`a~j"[3Rűi|xMG5734#ff<$o~Dd%O g촐m(4d덷 HAU% n7֐o˼I,U$3 =8ҎR 8bҒkNX&mZgaT3#h%?pYYx10,oW++츳TVHou8xyv^={nDq3:O$d"Vq'[aIb&0WFG޲ jO*-C] #L#*AZʮ1[HC 3~m8WB-H}b[)7=J4(KOM!D#._'iDIg+ƪ$(mVTs@*@? +O1oSQ^;M5cB&S `lyw3Ѐ%Xv͌ Kku)򅑹<7쌑*gEF DkHhg՟FZqG?v +-5$a +J1h-] 6R% +DxzPd8?W\c;a5$8 \GzgQl;?w(ℝW= 2ƑxT7hUmddG/n,'xo1;޲᝙c樶 Y?r9 dVL;K7M;$HBp6hv_ Qb`P!y4yfH'$6#wejTU3'k_>Yʄ(4ІUK-< +hc|{X+^SI?/|fv$B ؛,!(e0)J{u0}gXק}kA'n3d-ѐfrT5S;@٪ЗUgs tAAaIW0#~Y5i +1,?ǯ{!{j,Ln~ZdwUUN`>23zvmn >X^%wr$Jh^iw*Z@ |{E`7"5U`kӿbW8?;證qDe;(( EKg=p r[LIe!4aj%>%< +r0䳸&!hXbNm73G_vM w2"Y001iOJXc`ɡ12Olɱ3Qs1R@YD7moaNHr-=R_@ Ӯ}\qKF{Ԑ[Al8՜%{d}SbMoiSpCf_^*CJX5:)@[9L;H#M#Uzm'0d1E_OD#0fqj84HJ15$ȿK.L>_/oZ@v'YdGo!K`o&oml^gj!Ƥ]#TxDI:>S+3ruoR0'ڕFs_)Lo:"Z3"bE;lۿsDQR!۾ԤC{X=_4١WZW\O]jCD3kdLq] 0b'q7ߪ5~>&\1# ڛ(qs.%#*Gc9PH'!a]DjԐqWӼW,TnޯYon{3c8E1܆T/pfpکxƜ^WS|ܙr쮯'G^:oNw1Bۄ\pc*1sE=θK~ET gK >:$YI?|I0ZBX89ʜr,Ӻ7;wT +Fد y߲JTΒc Kictmh"J+#iZN캷12@_RFJϢ: 3q25,qߟ]dƟP$3p9tfb,r0xIilBj.-^.wIл+JS^0ccfne bu_8<J1p3zmcjΞJ?4c V:ĻIcºd!=H,(|R8'JsE~,`Q+. {޼,/UR\ +&Z zcN4; bE_[51SNstx8hRB4XShqX鶿k}}7jS(l I`uj +-nw O'-it̠B!GF,OdbcO] Oʙ<]P-pQt;̊:c{r?32K 5&LAmPd͔ȴR= 1{`b?;=9$^҅Lf#ty4ս@O.ß"\Sr6A Rg.SskVOQoRBJɪR?a!rA01MӼ[5-T>"_iyNQf*=cÈNHnkϝ3γeLBQѪ[6ԣn:gl`/^Jy@Wyx٣^5SIs7?B-^^,<V.&CgN}1X-*!E~JrbkSCٟאȚFs N-߇0L[C=FQpQɏ^>^+]/i fgK|wC +MiM%-"OK*-{n k+Ik^>]F`E"jb`(6A>a:;hڭȿٖ>3PbHD@nV;#B&ԑMv&}"QUty ю]b8&D)u(⬣mN 9ލV,^zSX0ЦxT ޹Qsr`ȓ@m hx71ͷ FLDd +ss\?̈=5YD9X٬2`B^֚ +#LԺNB*-^"r"VfR [wj=v(ez唰H`+/̥Kmǽ9G O̼XUpǛİ4fl+ɾV44g8(8o*OfEbI5Eό8cR2w <hxyFI|؜?EjqjMqm8_b6!)n> Kjl雁{@Ry$83-7d ݵ̊t#}@nj2'^ؖgކQPol0JaMjcQ_XQBP_q/jywU}z@\x7^CFy_"}؟CJZt7wDbwtX~"ݧ6|هsX&@Gdd7h vi$5^T1Jֶ^z؆GÙy+scrIW=󆏽Z/VmrIkޑϦr?9fhNc裮9Ա  K)yrgm@&2ISp-#oOFxVRs)`yr0!Y9b6_6^~|5ϏmA=G 6NW\Jwn'w;nC>`ZE:ҹ.Nd]§Iw(:^1[4똺e/0jGfJ:&3 G 5~1xZE/ߧsZd%h#H$SiczS=e,08ww?P%{&MOȩyCZ7od Z*^!D^lR[Ur96c #;ݱr ?(FNHdS8eۚ{Tg|DC2.k}Dj Qy3ᖣV?;M"7m dsk~i FHcC~Vh/;?Ⱥ\T59#\<{ AُG Emm=]w"gd$ݿ?G;kΝ;X4#7_^ N͹gٷ+B +.EI i BjR5cM}Fyq0{<gsX6  &~k: HHN;=tT!&}w +3׈J ޺$蛡39R\_`npCSMҤ_]_v-wp V.RР"|mʆU@/ڒDH068_7C*%bv{OɊKq%9 +GAE_eo;v+*ǛzeT΅HXCyoʣhtf4 q8NɈB +} ȑ1,p[STc&G&F"U[CVDgN +o2N"O<`vH%O[羿~WVƚHꮗ ൢXLJ5p$d,ɼfˢ_g߈l IBQg.Y} kG{@Q#H((g}47`HˇnTWKrE T(dy"EED5n̏@r]wW*׎V.$_E+9b[n|<8sgoZSG{j:8.+2z7q`SEĻaUnBJ!)|~yZp5_:a5:S|#=T-oN?kP6[nv vi9$:,7':;9O/z,>|D!X;,}PV"׹hIɝSAjN2u\ĊV? QNU84u84D ]CfD:NJUM 1,ƱCɋccVurbc4FB(Cb_Nj=@DvP=s\|k! xGZrP%={ő=AWҎ>qC[8"8ԭDt^Z06( +Z_v1p=WPZ7DHme;jh|!͊S0,즁 +ʊwo +#{P'lwZ<+I_/$ x# +O$i1}+[\?x;i>SELf-)ϯ\3(*SBo]yCqOz2 7oRcH8zi|{Ф];|<S8kz:Hf;]Z.0'$ +zKJTSQpZYT̥B@q~el$KRXQ33riO{N$7.q̷נ; +uG]S\{bENvAP {{#KxӗM6q!x1 7gQ6ʉjtI\Jw 7 (["5]6%g54UA289G$AZSa>[o,ZGgBVٟR#ehSS$܊!^-K\:%;SU`wL^t +TY'V;\ ?X(KZ'"KdBVE-Hii/BJ#T>b\i3bxĊ'szYyr!N2ƲQ?w%hdec;ߕ=3yaLB9e `iܡH9v˹g}l H +6hNowC)`wip*"N;nOYr=59QMSđcKyRkbڢeZr(G9?ṗz3rJMjhk? yL_U9֌$GakfӲ +yӚzZF.% +=[n@ s;fj,¹F?:;g>CɈr}'`aɴb+?NjjQO4_rYA(3:ZiJZW}?wFż䡓5gផT8hLvB?iyy_)ޑP|GQ|#آ?"PrCR7E +pqple#F`RZ،%! LkoO#?mKvlNET7`;_R֚J3"Xo־g/XSr8}H(M gxMbO' `>InE;oiq8kcRo4\V7J琚p6e^vĒ#̈́[ަt׏Vu$:@ޚnW~`4•yr od EJ|7(#_Osh~Ыh $röNlAŧTZBWY9_*p}:@ᐢp,z[{8D҃'jhwz+zmy,=ƽ=9ncBD;R j+PPYXz@4@u&+~nV%p}`~KKhgLR|f9-M_FEeeSfaO@5ūbx"yn1- -aS@4U kbNeoҵ{@YZR]$?uUfu6yQFDMjvg&i1Ʌk3'xBi gS玅R* UPv ݇=E$Hyϭ|Z3siX{bN(' +' `&'V +AGiP朣|E,|&3p v?Ppax7{K>aY+:ws QC>Cػ,%h9US-QCE3nm~+KW?+Wzz}WT?4Jۻ*P`8Z'"kle?u A"YdFZtGvxjuTөJq{igݎ+t2TG*~I#DC{lHV?uǁVs F@'FߩʎR'[ǯS|#[eg 1]b:GE%k!p_\A\L\|HFZwx UdgFU,riAZQfWנu(*HRq$̘_,VWd4vK4_Q]-Q?nO )5Jv|}_GVlF~G.łV1i +ՊRƩ5#ņO&)Uݔv%I{F(ԪG]C"GMN[x2r*64k/tAI.{|dz&$v\GXЪou[$~3)CJB=_=#LG,3H%MxG|G??3DH.^[ [cARzY{cCce_Nqٕm@qç,8BetzƟ;:.wE9e<yͯ~?\:09ff^\Q(  c]]mrZ +w\pj"<)6]5w]עIGd uw6Ե̲2kQf &˳~~WCul "FQH֥vaYPi~0hЁ4; +4v&(WWO>z#5vyaޑ#` n) +&>ZQ\${# `kMMpx1G+9qE;B(k,DDwr2?e=Th +ߠz<ŧZ3]gŪUZcƅ >imI0KWU#ȡ +]1զ >L% av}T:jq~!݊/u6bZY=n +sU]C.2>& +tQøЬLUZI~OO R +R)ݣ A/ )6%ZO5((j3;ʒ"`Amm6u90 Hu< :~ERK*'U Aݯ衅~n'Ç:Γ#RL]Ns%8!zGY9:J  ieF̚w5KK.Pqqj^ӭ]8{BX@*%) [WeRM?QAĦR#Dΐ-};$O,\؟^Y̷Or fr];C4 {mQtw4?t,z45GJ$R܉t@ HU>Qlr/7ңUͲ 3\ +V޾u~c[0$^$~kZgg>$˳5apiGU++^aps<v#$¢CS!GI~pȱPAPIn)=*)%/E(kVtZ i<`3H%T#J\g /GPI}W d +8J:#B*X{ iy1_?QB~Lt.DS⍮A=+Έ<#@);?Kn]yw᭔aC&Jr**߰W쑆Բ{DubVvN580 +wsP pSi͐'0" l~RJ[J3c;S)@M[+2 b&ͥFF޾-ML)O1Ky%Nݿf|'t\R1mPyykb gOnI"x8Wr3@-cFQV'4iIn|\J6L3@KE"aأ7h?o4҈ +M=9ץxFy@D\R(kOѰwwt[~0Rѕrw`- &?⠮Dqf5dOA*{ |@l>tw~"bߙ2hj`O31aPlsعȊɲ1v(]A 80qEpl.ʥ7h)%D9w~fH<0x}KۯEWf{JwO 2$V3LhCj9̐(Şʖ!*H߈3ey(I60[les?U+BRݧ)C<'c8P߃!xJ-Ri`5>+2z-Hc3(&)Q#)5K2 +GL۾&Lf:]64:)@%jPSES xcqyߵ16/Isb :~0y !atoQŦ!#k$ V T/.#38_WwILD#nNq +2)H*P.->;d#_lŏc]<ƨt, U䣖+ġ !'U8S3:EXO,0oS5ȶ8d!q5#Qރq{ޠG2Zpr!G [f~(~u()}*b*Y}ڏ& +s8c!?kGx%4P`UŁOW tf&Pk_8:0S.aL> hr)^(=Fޛ^ks)Ki-9 .Au1"{k>ǃ;sHwC3Py*H(ZuNemRazV/m~CQmJBsxkjWН*N&"Gqe8E+:37|tA䡱9B(JFZTVkN: ̶hUی`M/;39^#]y5rFrW>NӐ?K3 u6!6ms:}#/N'JkW'hy$Xc&=6氨f04ťxd:-<2~rGwl?k&yhd|_EZ0|3_Oj[KvÖ Mt>LސR;֝ iBPAg??@/_9=vOuLhe`q +>vxLEP0sLP9b7w׃cSlIزTuy:'KhݖLy(ķ'6YK=A0k↺dYI .uBXvGEbQkfڀ*Ho !י S_D4'و(sʹzBHW*d<_|*UH9#`}B!!_Lm¿@bGtkrܖs|"~`tT׮}Gh% +x+SuUjʋMb &t]E8h{ +iѢixZ(Ǧm~HzނO!ܣtpvjX(&g\qTLUtFO"KpE +!w_6R-c~`sP;bF_U}]3^Y'G N}jL1 `˕@Ycݿ ׍Fpe-TKʻ/AS =lwP]UpNw, +h#I7bX<1hY+L BA '($r>:0X)=%I̻RW4DPV.YKi{1x" ;gh8VʑaA% LFO;UxC.vKѭr1[V$5wdQQ޲: 6tbI@^l5/M3.EΛ@*A+rp_)q+XJ(ilRK.J ~O<͟H&a\>7#61!6R#wX"]Q(,ޘvԕ֞ DgJ:1R(:xV#7#>If9(N4`D~?A3;g,kXqw>r֥`4:_36VϤJ+N U2_ݷ6':'l@Ǽ0[}KةH6 QmYrZW%=g|zʜ>`EK}Gc[SV綏0!C{T?eN\T{9 Z MOk\Q͙253*kVh3 Wa|Cs7P ?[Y)_)ԎH};BU8 {+cv]ڭy3J65{@G+n[?χ;Rp6JQ&%!-2HކTcIvYJC.9UI P]C&hH)RaSaOg},U"|96D4Yj dZs@vcLTc.m_@'+5vT _3>?SӺ!T'?WLGDKWTrV/jk7P!S2':7z_ BNr>uzL*XTu#[W5b'ɪMؽyx>k 6K eew5s6 Pz;ٚFTUMv;Vf;!=K_ؗxtn R GT(&?k+Lwbh[mLDJ'F=GR:ܱ'+o +0> "͵ƨ- H4;eN^ĘCD,-u獐v\*)C' X=1b,`P]XID'BVk;YF9iHx]Q\sv[{ ic?Fv\?V(3ug ̚B0ZVE1(s9㲷Q#11RJe 8:v!HP[_,{NW oZ|CƾE`*ߒH q԰r `hȜ]˚U>d#! v"E+.+QN zo mƋ~wu'E0+]s|O"z!$R3Rt"A~{I2ɥPdZl J@[^UQCdx!}+BNCa$$rh++bZC?)%\z3O=62ӣXV~d{ 1\~~i揗jQ=wYﱿQ^{c_jMjz"ە`I3,sЯei7L^jxz QŹ7|GL <9?kAp^M +`+Sy ]=6y3OcOuRY"L:Lue}Z"!`i*tY"v2<ێ=2EY(4=ix! Fs{}!y5a.L22} +i  mԏi)N&uوkZV~06@# Wh&b)^[zddh{Ɣ*L鑿L0DH϶E35N—-.';P]`\ӺG45;ӣl=k6Dlz55e] L<=XADn*s07Ԭgg av ˽4Ng{82>>aLn=kUgI^m8lXt ]+{ Kg}p,ڰx\tslPt#:6TjDS,B (|6/.?D.< A9x'X-9z)FxD{o.c M!,nFgIj)̿N RTbf2A2k +2m}Fds4tRB3GUT>Xcey.|8nk UCSY1k^HK-/u DFQ,-кdiimǷL߿~~Zi]aؙ"+~$꽋GݱyCbG~%nK:v4gS$<_ nYm4v~(tg|eU~0'Cs _S~w}N/M]kG,_AqNL轣];>­8]Y2zy;poKFS 5["Y҂L+*BqEVe@iӏ+r5]\K[$~ 04lݢdJGěS ժRhA (Ti5D/ [9\2mV- +j +~tӏ*lRTKc8]WFz*$6fhԗI2G{mNW=Z Jϱ͸_G耹RGF#S;3sؐI:Zm,oxf-tԞA4e \8j;?11H麮AOwsG sNZx,o}`ǸK+^$BMCJ چ F:>~\K(re")qa2dz%_N!DԎE{)4ps~ס_30m4 P&Iʒ.3p튟aUN68*:,AP|z: Q_bS(3R H|vL[8HKF}=,C.馷">!gnT;~㩺bːexŧ1aĠ3''-¯3׫-=pȗVҪrr8ˊ+qn,tJpCb v}(ijH?<FʾK-ggbGTٙ+T˃6ӝ-7Έ2<䛷G ;^+'95VRHubUHh:IJnW6'(mv}\~u]S_HS5}'J7i$iT៌}-c͒^m/x7H.Pч]<ʤQH{"?*i`o3s&qi=>5)ҶtaQtn399nazQ셜TZ}XL.@uL#)c.pKM$9P9Ih+1oIR…s| a9pTvo+^ ۏDs!5>41ӈe|~}93sr3LE JsFq}ƹ~ZZJA9]*۬xR:[m2рVMtȀU +&tee]d__^-Vx8MbV4P.tvDAElLGdeyV0N '"!IB!zN©~eCC>ȹf!ۃT:ckg\y;H^i :3զ( Cɝ/üeS/S0diIM;4GOw|Kwp Ʉ`;Žz@㶷D]n +/rgGC]KFHwb䜒E+ & ()ytW3=\NHț눃i#U! ܸ^zmN[Mq4-h^Ly_\oh@DϡVD,/CPgʐa6ςJ K>Y)'; q9O,h-YATTkywV͓1!P̚Op,!6皗&՜"~[M}VhtpjIТ+EL :s~v;+ ٦jH;ofCa1C!\e}80ľqՈ`̽4[;m +O)`q8J5t.!h3D7,s YgՉvAK[5JFq +×O!|*bG ynH9I;`B89fU@N`L,pJAry_>D'f_*OdugE~ZTWj]4LX$LԘ M[@&L}I-B9H!Lk~7uw|kn endstream endobj 35 0 obj <>stream +КTßp(AGڜޑ8N̽՝a1E@sS y?A g+fSՑtq`, +/ 7 dmr*/AG4ht8#aV⩸|k\|D)Z!7  ʑpE_e?kW; 3=SxXZI,`ypjiøU)s&1-rl;TsT4<-'UӖBZ_?X[.c-@@Ul̏y+e$pI&o$v24uiIN~r6ĥڐl?NBao^'4.!*qM:羛2qW]+uD1ʌ8 +4)8s=j ֈxލ>ʆ~2:}<[g[%3h4]8a*{pS{C60t ,*l>Cl&xp VQT)⦅meཊoz_E1%fD߻,Xg#veڭ8i1b[o jF!j{#<:]i ZƤNsHpm+^5ul#:|1,C&gr]ʵR\2jT)13a (^ zR%ӱkYwnjuղuB3o%QKqi7v5-6nr?K#% r-sS?,/px'si _WRId[xVBE-pIPX+G#@K~JB_X=w4lzѫzlkA-(ЫLk12zѸ>-AF9u5Dg"^B(,uwho_q7w\3 Gفz}--U#6k +n+~ਠFM1+2Ez*tHF8?g@26">+@l8[$ gW&fdU5,5Fk}/nrA_=\Ȟ(p +-Gl;Iu*^PD4^z+I2I]*z\t6$;uW7&Sx?U@ pm~zYIyG'"~Cd0wX))bbSCNRBSlF~gG3OJ:F11 y׺ ֽ lʨKtVݢ c\*N&gݱйiQh!*-#DŷS,N9] BDכ J;Wv< H;x0r+(, 񔎏t +[HGb;=KLaw_oۨPp aq^QqdQ!(JKg(!/'#82\ +L@Xq + +,Qq4cH@dgl}CD +7?6%sS-d̀%\|[- +hK/"`L%?'߻H48AԎH w>YC9ȟ^,d{ Ϭ-z#F!prL`&c# +,5Lj )%0tn&Qv{] +~zg.3};Edf3؃_~n/97jmt%o^vәVfZХ3˾7*%Z O^1E+ktJ<`kZ39\oy""~so&p*j끪-5ɨRG$YέZ,&>V(D Z+| ϙD=]F X9=$S[C!&`FG)N^.fLv0iGM*PC3OC)} ۱Z2o𶶵hC8. O@Vq6\p9Ƈru4s%fݞt!Jā<>J8([zAd()SW.> d4 j7x74b_.OR9xݻX!ڌ!t*^{"ޝy%}_IzA+K죺u^xEC+E/T>8gaXMNJ(pm JttpE=T3 dz𵩑)/YSx Δ,L(}:Nj)hǞ`yr{\1:kQu}kF~ ;S!2-l +ʘ@4U(|D qA1%f+tgtO!g$V|pOE꽐|k<ӕl;N>cҍG8$cm?8  BT3\3Jz|OdLyC*> @:4.R})fkd\˅ /4pʯ/1Y-w l{"=J@,"ļt[ՙiߝBxY_m3Rb!m(N)_)JB+ꏙDD?:P讕R;^p)o>H:ҟ}u:Y:"sc)k1r,x|@ 茺& tcRlkS$+Lʑ߹K)hS~ɈW:#רj|r!A _c|d@!~b?_Yێv~E!̒zW\qNJeKՆTGK~*V~g +%`Eɑz8u;yTi^1nIt +}*R e$ZDx$9{'lNWJ<>; wq \I4ŏ3CVďV5)_MxDE=R$Z\+vq'#}NJ: UAeCOaR |9- a{ vWl/ @T'b#D Cx9C!#M> aq8٪ĚE3~^͡R\^]ʬڎ|ꁞt7/MSEb>glfZ-zMVcF%Qu;sW^jmnr#q}ħzjLiRr)g++> NEmoZ ȘYx0DXj,lmtH-_9\_s~e;Z?v7)G+{E`ɟWln G8&|+L*AB9cdv~#Xnf@bxrD{R|UCZ5y^R*Pr._%8tBGbz*j +Mt 9S' #'G^ $R.H*pnaWz=/;:Dz[ w,^#q +8| WycO۾~aQ g9Y]jf7)]fxF?8HLt[pة܎hL|73yBy$?CJ[~>tN̼?pޣzKw i[N;BҖ&)Sڼ +ԏ2 3`EWiQhS +h(lV؝0HϺL T5"%,d7s}Fu/1k ]u Gr`-\,k*G6/ţ ]]$/.XJ<'f ]CU1])eaGM1f.SUxm5g+q7UĒ:o~#Fv>g0Yã9*OB_ ͯ8'>e={Ԑcn['@>A(:åݺP\\xlhYWb̄g7.&MoW."O_uDzDQ{NP/z:?(r(ee$EkΦ0[2{W31'ro>hn$Zlq pbI3B2.pan P.:=R^!Q|IXMhOΈTQ d2z Ia1*eS(v8wV_Y<M(ZGHympPõNnr,u0wsx++z 锚'x(u*)n7i~ݶW2׌D| qVAmH[|(8#{WYNb+=^ LብvZuu'Ig"t|D-"ˤIf^ +/u |CSf$3rDB}-2? 7*?O(>| [kDvȘfOLV9f# -ߐz营\̽#yK-(z<8k#`S&3Ě㭓 aFӯ+=GI:0*E;2o6 Tb>}n˼;\Q'%&\O./OPwZFJvHv騅Ucמ`0+RkR\oGI51{o%aX=~pcDHTwbFMk/ܑ[6Ç,$`Y~UU9?+DYs0YkkjkI+dOK?kh ˢ2wɻ.q[ܥqA+qA=x d6~F?D-1N̑"ϑR)󅉼B{G8`غcibIh6}6!ir3l2g#Aؽ㶤,H֠s{Mj'9VKQA͸!_js)JZIpF`x8BAG1]#H5~-F ;nϽ^zş ʬE[UTEQF[[v͡?>=k*uRT`wքK!&gi u>uB7Mr!5B.R9:ȦZ¸?[Xf";EBQo>}NJ *NwdD'&Xy#hf$AtaSOp(MDRDK1N㛉qAH^5?Fr>\|mwdhnY `ebFId-H/bJQDDo +DmK[fJwȥ)W,Mk,)SrYI(zɓKzlqr3j j$l.|H +piUmRȊ%AeS3j˂ fAJ(3qr?&fӠ09@5$>(,x +l +Sϭ|d +!{)~!xsolPȲ:q]U4HϮ\#.ՖC,rSuUA8jgsgވ)Kq_w-*#x%v_|OIHvgHOOrg)$P3sxƀqInW0z ̰Tfv!׽B̠6BĘ];fC%X1Ϲ21&$k:ƒiw@.@뢯cB<7ĖL Y% M[oZ/+p[(oe`M=6Jx@h%wWF'E!!M d|ysnp/?Mko.}<)-~Q Eg{_x:jF2}d}6 ˢFl~c%Zqr0 +Y,;Rh*GP܉$N +6+D7餒ˉGY\fmI+XƁDWir䱂%s$ ڃe A@=VKy&(^. \i^"\Lt,T{z9{-OuQ +AfU[%{5Lj 0RRPDCCu cnը! `(CҌ'J*f#xֿS~#{яTjّAA~%vԒˉ޸P&I\. ExA8 ̔DWX9 N%z.41;wkcsKOqMYt+܋=)f>yg2 8 8q?!Kvg4ϣsO7Rfro&f1_ɐQڙL(AgH0u,"D1}Tm=ukɀ&5"mwTN, ԈKy1]:3ヹbƴGՎ/}8mFVQc4? e0N^[VzfѪC6ʥJ^CcAiaI`E0Dږ0i֗Z0="Yl&FGarvRuK5$5q,ڢFD>T~>;Gq&RgR`[k)o _S.NxGg8Gp|M,7TS>/$BJPYGQ$9!o(=֫{DsGNiO|#-+PTpd%Sn\V' H $@T#kgRϾLΝҢQL0ͭ0Vvл5AWObkȈ,QJSc؂h֢t2lcN?ЭިKjN;+<=' Q.uoYEq잢k jjqml_֜ѦesRLjz6"`˜N66sA0sH`y9s#khjTq9o2tlu`Q9R|Jg ==/BڜjZ[FzӮGAR0nRحkvpq⵾6 +9.ZGhEtT6{9@yZܡ ͯ E˭8`&"8k"xQ>́;cuO=zMzz?-Y1ق(OrWdu~Z0jXerՒe+`IY|8y \1E3i^e#ZȻv(lNS&DQY῅P ^7_qAz$L/9Xc=!sa.2`t?"+,a/p*],a殆|؇~ˈM +5\3>|(ciqR-oG;nOutPiK-r{S++n^MGQY:՞!͠G&//_C1533HOmƸΒPqH(=1V9|^Rx~yse?ZoCm^hDR>w]g$p#1"O1»**ʏ w[ʧFIsP@4o7>R%~RWnO1''RIN[ $/\KJ{7M&D3N >cQ$C_#IQ ez|N*ptt5 JRZ?N['A@r,$pY`P ]CkEr6ë9w#{=Q=~dVyV(nmBte?>0Uc%̃ze 780a2;iU>|S"Sc?qgO2ީuhq-#a>~^XAw +T,HՀ\df~n!!`w\?ǗFojĆx檐Ii?]|tt wϐS(ܔ2+ׯ'ZB}J!̕;)v0z-34UiR9!3$<.m | 'ACeUpO y±agV][#@uPIZp\0p%gg$B6QB)e^{Tz+U5 fȥJ%F%¥6}67>)BA=&)|&ntppZ~j( \G`5tk=c3rwn;k\ď;js= Y +7~LBfC>boV6jHc e0 +ݺ>`TCch|ʆeJ IR;MRbۢc zF G.='W'-o觩@V0Ote5ܺ1D', qN^~Hzf:2iW OTfJ6nd{D8bkb/p"7a#}mΰ2>PA"ψQQ! E +M#OiJW״ߛxiqH6 Q%} "oL w_W&6i6(=,P'(cʨ8 +#& +0G:vFFInJ<ψYHO]CS-/Xw5,j9r+Gi}e.3<2jH1{6ng~Y^Vc1rD&Sե~x{KÔ"ѝW_FgB+ko ({/:iKmtA@ūCpIɒA5Q >}i'8⼜q lZ\IG52N4(^Нbvpܮ|Hh !]q|U~vCTG[ 938:)Wf,4+izR>:6 ܍FܶI׉FFe}F!ԁwpe;RsGwϥ֓x#ՎMZ#Lxq)ž@[}&ΑcĦ V~tCMT硑_XՀBSʷTIĞ<%e A>w0T+降k0G\,;݊<;rΩNk]&"\l *9JhhjS3]BM~|hBqvl$QtB[Sr(>tޫʚ1r ѕ=ϯ0S PcsNّ<}bv>']=-;JN9$ lӆo@{HVTTF_'dv)RaXCa#qC(+zWK2gxjSIyƨTJLFQ]ǗJ=ѽ[ +YbKQ!s&O 9bKp 39wdiP!Fff޲ 3"7uh谎 Q~;rL_@Ҫ9ShlJΕ'&BEjJs 8Ő5J1h^$O[&4\y6 "$;d1zӞB}vD ^0H*)lJ(yO?ɩ:Uʘ/JA֕P~ՒTvSb'kʫV֩ kt RT5 #FMT#.('Ͽf裲cذخ$g%I3<9^KѠ@bQ$˸z* NF#j~s8مV8Eev wj[dܢ +/>TKT4-TstKg/έbZzЧ\Y%twzިBoS^L5K$MIOl3s0*B\::Wnv .$B`I'#_G]a +̊}a h 8_XcSMQ[TFZ~$YVeqѺ]?_ 0z3Rt˹9:;]2)瑈SA#V#SQgdjuP00S`&x+ BCN}%IЖਭ +SMJABģgSB_}ꌋk Dx@C{^+|NjDh+,J3ނG3O*Rd5 BJC(i/".xC˩PWJpQ>k8X {LwDWvHT,]7i.ZȄ"Oߍy{%/ZdKTark8m  ^mNmaoz=Ag P۾A! `+rsQ`oǻE'66aNE?0/ :-.9WY-c{zGAXTC3[.#*@DGqE +wWa_ 7+FژLV+s +dϖL"]4 +tFH֮%7amyj\8@+ZP]#k +|\bJգ142'wW3;Scki?1Ͼq婥kdcϵW:}V Njmg xzk |qoḴEtZ`+9H?#7S@7ŷRj +I:`t-IgziTxAj6P/VXxh*r#.|I; gE'寴zl[e^c?j oZ! kTt)Q*hu̚+Tj4qvT\fjgǻ u&tPWl:fή`<N&J+baYOsGΡٞP52.Jbs;]+ObCoeS+r.RFId]&ӱVtHgFrKBb05F~%= uԸ˼AAw偣wHf Q=Faߥv_3:ƕG+ ֈ}C\!?SukRVXe@=@vp>FgA.l8pZNFťaURS6JnLĆ)JE*SK?>&8#r >Oa6kH"+_BrTBC.3F ¡gD['j;'tX bU@K_ 7$3uFX%8rJ _DBK=6hb ?sՑBϴы@ a&S1^C6k'Uk SɃظfdQ$I .N`gG+1ђ%髹s1%야P4"^FG+&q\ǓDa63)n#*3Frj[;f'8ГwA´㛄WzWsi%!k. i9C`jCXIwۓO ^4ɕ wh͐PD:9WOpTYH{o'.8HKYyuPj?KB_s7H$,cugQCd麹!Qk ԹϜ4)?7jf|Ƀ5Qo&096豤FuQ-T#`O5ӯ]Q3\kaO+Gwax}G!9p!RqjZw?x. fF7k_z6 t"אT=6'!qr|O&:^%'fC9yj&3]1d1nb{z;f\CIHiSmR5$6{pm2' +wYǤ|´m1 +x{Te_FLQZw +VNs6b ]. Y=9FB޵|fT(+!$wAq6gȺ+U=xB]ToU#!mFMixuЋ #Dm{8ӞG1!-td1]_XS `p[5B V[,s=i +efH3רGq&`9j׉E$ۮ񉽥5o-h^a4~r+1.l_4NLۏ즿,{׎.+{+#[Px֝Cmn@}A#v>K6 +%Gb+ Zv.*靈7 A <ϵ`Dyez<2NhnW_y,q6dgD26h%m570Ȃ;H ^CF3$RQgM[*L^>zlZ̎_ьsmY_]|̙kh-Yxc3Js/sǡD]^ު}V[zJDBATͬϐ'`l\6Ty|1_I.E']!bFbv([=oUh\ v~5(ǁ"AT QAì/` t}Bt +#mL ɇ8&$I/?+g|B?U$FXƵ#G8=GcX"Qk%AK[iE3>طR ? OGr/uK{2p + + %V#I՞bY-h(R:*6qS/e? eHJ$8$`/G7<|L9&'9w2q26-c=GO??2ʙ:VSs66aN׻z[u{юxKmj%’̺;t_74|ψoo*Uj9o:p&zAﱶf:h <ĒI^+*܈WW"'EXɀ2#vG_Z~Ć#xٖNѦ9VP蕘 +9myɖgסfpN( 屰G(2F*| :vAo{HBۣ*"iP^9ڽ @u cioj d o@ھi!PDqA#~{~hFUٸ,ȳ>ޣ+TDjΪ]'ejj&$A=ΨrH~=$Zs'ob}zK+O,JdKga +5tN@R#K}[Eer ;tE'Lk+CyB=8Mg7g6GQArG^X&Wp}O^g)fAO^n]#7&O*6Bzz卆9=#ruͷ3F<+ I@fZV +ןDFKZA؆7&K`pNS~VVסۄNLULfBh,$vL)=`(̐5•/8_dt_j#LAh6$u~bCuPM + jSN8EGUp>(W{m^6=%7;/5D\44C⛦c Ӌ]&AL#\HzZQZzm$bA7uߓd=CEn,.&3RJjzo\|vc\si߃bJG*;Б SHMUKvJ˪>I8*xV0xEU_j&oߖk1m%[p}|3*8&#_qIdiقh$H`V #k뼾[{;A=ڊ t="RS hZ7dcDޥND^(Mq⼂9J"w|Cr]?[j}*F,'؞4~B?fJQX9܁9 /`1^:bUfNpm'6AEOʳDY:`IKQU9wTf-4kGU?:qUKJt&@hj8ou`_fIO +^w_!Jn\R (DItz>$Ib.Z̾9l{;K:!B2-k&B爕U~l\ }AtǑGܰct yT7>}GGl|WrR 49g#j[?6̯K$}oc*7 W]zd 8{ 2 }}\Lkn-zrks ‰FO|Nm +E͐@\Kt] qԲU%;vmsME.%KSlCl;ߝ }Tq $ $L:F:Ŷ&܉x1dR{wK[w_4'ܮ'$ +2./l0tFkBm KqBd*ij6)B=jY_ؔzۡ9wsY90@fцG>0G Ĉ6yGpt7ZBvrf xKISMMVt)نOcBR>9ܿ aِ OH ag ,})mlQ98 +MdԶ p[Z;ֆH1wJɰ_<"^DFb1U4pC1Ht F mX)wtS Z\KAÆ1~;k&s D 7gONVJSk&H@6(:$D1Q~k|!`q3e}Eɇi{ +aTA, +oւ!U Uxr78az6d2=iRI>E2o)ɨ}҅\.Վ'z(*EΆԊ9 pB? O0~CQm16J0xbC&xOdǞ;d +͙:C1 ^5tGOI|s=ƖZ9~&*_c/*T2wa6Rc糀>S68[Х'"ua*{ H_oV?&% dɉZT/0/|{+&8bϭp*[2r9{X, L{Q4dlK(X(̌N] \u\tSZ&#`OeaWbB9OZfQc6&ʚ!ҳD6iz"g"&*xPu LG +"ryT5-̉Pɞx?S+-6}"3B(Lc:iY^d<{Q2$/սz![|L[epyL<DjR<[vxq@R"00Ff:R8NU>*.^a+fʀZ39:Lrll5 RWQhEu[FǮ5݂\d^ =,Z =(8N;X㸊y@p@!5aA+ E+70?S ;"Z,'ZV?{l|3OT&g4.Gؘw&;/M,^RA>"6Sr|'{`v4.>,qoVEaXj|QJU3ב;S3iM&Vzt845f姊muO#n=Z:]'V#$hOh|;m_fDdrAi G@,]z稻"y+aѲ ZT䃥 9^v'-"rWW-T\J/U Q=-my4Ӏڣ RyO.W/q欄 줌{٫.V UbK +xG +?5b]MǑ@v77 +Ys ~Vl:s[ j xDgmV63T>"sL>`2(ZfgIi+q5$T+<p@,F&`HHSy!3TT.hN2D>ֽ?F1)_ؖH(i/Zڙқ +cW酯_ղh4#Ldrfb1j([-O{@zv^3韠 c˾U4WVh"zDK~WD+M Jp(PKbNtÆ&/>",o:Ux1_*8J[TVexVFБ\RhUYpgl!x71Xtw4Q-&zcDX QդTb2=F eA1fB-}(H;qh}Gx_y&>U()`ͧiΗż'Y>m9)&l+icnhŐYbe^ԨR2h|a"UhT( +S*(̸ꀦt S膶` +ݕK) )м]j8P8V'!ԢSAo!)B;]18xzF/ u tՂ|:&s)v$S 'sOlֶQ3s(,ɦ #@;`۫n q:Amϫr>T;==(vi6A= b+&_Rt`3Sq#Nܫr9xȉLt1u!^|sOYF[ CaD;G<'%\ZƧz^vMq=L;M +(Zeۘg0˯(X/wT ]0{Ug4`d]G m%X$b3jѕ-@H>cI`1gj Z#ՠه ,nBi$TQ6#f`vTֈvhey7bOax6pui-3CooUw;14L;}8GlJR*j_B꼣$w y ++sI(\p6rݾl>6ψb s{"w~eZ1GD!E`~R2``,-,K>QCQ17Ɣ;⭻~FUNR!4ѳTBK`^ ;c5eDX'unͦLWuP6ɚXpc#C""SnK.ZsƯΦ떏rt_Z_S`~g?شϕ"evl P2+Y->9jPyn!ug֐dy +De| d]+QIj MN%k[''ĝf R5eQCZy%;DkLr5*oɩw+RvI|Fw/B<:16.a 6!_5wŸwMwrBxMDscKVn接ܶ( : +xIYDs||7O鍍 u=Z( ?lFgT'3E%`Q_D!$,ڶ)%~tSHvʟsPRdW]7I/!lAq*ωDH;adlGICEB]-y'C7WVNHs}H]fcg&]\i0Y S~5L&v<QCzDyrH|Fm$gdsHb0\j EXk,YW®}1^$gjy$[`7C<)G078T4Xl8^~z.x8Av =V>hw#9\фپlr}udy#(4pS,ʅ\)`1&1@Z )9m)6q@̼<)pLmd!bTxMwļ@Wvҳ_}B`岹dcL0+);'EH'V]DhrW.>?53s7k?k@Q 3s؆>#=|Vw3[9Phw hW;K.ZxGH8$|Ui3y\B//|5/:G4[,x)Q9՟*i4ξ6"jC7{֚ f ^j\կP%0.rN/LoP!n)y̆cʳV9Qj(lXfLjrHؽlF7a҄Vdnl[&Mty.;"{˞/kgw2s$~ΨөCDHNQOWlr#ֿ8>C\ f(XɂP%Q cׄw B\O"ҧ|7#ɭUf[tmL]` +p$)(W]V`Lϥ6c>gӦinw2HE-c9t1ط$pemy=&=8"B3Be+tOf4 +5hW"_))=L;1ʃj<)2H# x7}BOjjo8Gkn}?5g\8HqH#ŊN˵C +.h.2{יM%v[o+ IOD T`~EmD+~iYϙ:w3R*aDrMljDawͱBڵ.%ELiDOG+Ss##7DڬFf잖4QaX#fіOafl7?%$z,NoW$xYQy?9yH>73C*1̫w m;a0k +z8yu8}6 2F Zz c!Jxg5,o458/gX)RaOl[tZhﰊg*~@5{:b>?-[6@8i}dv=בiDeQM6‡hv&_)1p8$;穛/H9ՏV|baTWcE4 x.nZ:]I^Zα?[ɩ c&G,P.yE |w znQ񚄻PVH` #P;Hз !uP|v%b>t/ϲ[+xQyWbv(^,ZYg/tg1w gLUDO`II.qDZ'ZA~XV7 IL0Зgj05d#Oe +MdU&on}R8<8vmZC-񯴢//\A=Bl&}KxtS/xVi\F_iz'Q%k䡥ՑFj_^#<'+f :d7+)QJ +my(_ADDb_^eab*4ט?֟[=FD@iUg'p\/tD_; +8eKrM Jwrm|! A?XzBa+8w LfrtK]^5JkAYB^@!TI c〲?HQC; +>ݞ`9ͬ!ܴhB[_K#aaKC/KH&hNhx'B8=uX\i_c}OVXyFǴee#eߡ0/̚(֖=etiiŨc!#Ą+ODRR>B0lGd;Y~)8we=jBJDq&+ߚULDpR X0k! +@;;釠zgMqLB~ `(B +R ؿ##97kdlB<]Q?.$BP8 61b ectPRݑT|CH@;+g9dT!!Jd1ZH@o}l"Ve;eG/u-k%5\zq֠(^gr`Q,(y@ЂQ idY[ +bVFд#1e.űs@R=jqa0儢H7A)`h#GϤh]Z'1oBB`qBs2L0 5PHmm酥x k=W*sgӮB%b[ eLW^/^~j,+Ź\u0絧keMېJYPOl43w &=?k ,stSkGl*n/<tU]WӺGQ@L̗OmeP(; WRSRm͑Z qfs@UZ j9_x;WP=dS2oZs)m;%֣,&dwQcc wyCgN*^)xv)̷+QPQ҃USR08|+vlx =8#5#X?bU-xcy[NBS V;P=%,(@e}ý'aT>b+2ǽV|~dA9ͳGsW<#SxnY#F7!ڹRbUi5TZVs ^mƌ=Kp] +&aLPg#3_paCwUI! ]+ +?X ZE;n$n! "[ ʈq[%a{g v#m"朓ZMe,TOx9oEK!k@S}y10[GaV윬IBrIQd/̚Aa*D掵@ +YϭQnD8̭Lu= '_W}߆PO09WU@Z1ĵ1q~%>mjҟ-9В|zu"L6&#k;"|f[PK+7稕݈\'@\DA!WA~`U>9#ԤѤ+HNҨ%.Ǯ5t +BWC*V{OغйW-#QP-us? +U2+yju!ҷg}Gz3,{/0*0 tWQݖbc/ۉܭtd|8ݪmt tsNJ%ɾ T"I衏 lC|m&'TCphU-L47gP;w#ks4mMPؗt v¦NFFzZŪ +\;#LW=;ŸvvFwc,CcY`vOVƒDc++N"fIsÏiAA$RƘí h aerS)$wȋc<7˒fPg0Xdt}y>E3oW nfNu:j?O)H"(Ui[-U:dTzF@O fc[RH'Rhk6*X5Z43XV4CW %ێ+: ,#zn^B *ɳdPf:2=Xv5#n])R+5?*xPҾ{clԝx$=#k G + &ć=R3;փĕ bТG+<8*/zG~GQ͗'< lU]gLא +׵K]i'ݲUºki + $&{ !%N+=VՐ~@p + ­I.5UkjL5HkXV-ɞ&Un2Z#0_R$?Wļ*'sV>[%}(oRƺRgjxG\R:=hcvPY]5?4|%-?F_Asw[[:Š$[¤' cH1. &L`fDž3eF +1[|J"xv| "g|ZF *z|md~Ѭ:* #bޞ kT%TUdl d=VC-WފDuMJEDs)!QQ_?'2([[y7$Z"WIv_U@"ڜ@;;^{ox c^W}RŐ] ` 6*{ ȯ :2#c/i,Vr搑œf>3g[''(}DR횱r#u@ձ-CAa&zD +=eA+VWMؗR0~g>NT&9N@6˖U_$&l'+e?q'(۶˝zzG`n{Y;|}uU 0֧UNuWMlW=OoA@k0FwdWFCd>8cC` d)[)e9Iekg҃eSZrmtʹDwX/C!0/m\Gl[nqʫ"WrI3+v +JI]oz{yYeOq )GQQD"'T =Tؒ%&ϥU8õc%GFI@OfkD;۽JB+YڞNybUAC*R-tBh#PV/D-vTS7h+ ݉rQ \u QV?vHQZNaQ5`?c\$BOEvQ$(D[yt%gɫggֈ[[kŵ܉V;`"QpO-^KU3,ڹ-g[rza~W.QʲÏO<^(35ԦשVW#$Ux 5_-tR?q@4/#R1璘>+6ݜfˋbCB8r/)~n(@O\UQ5ӂMk -fvW@,mUc^Z87ƶ#n4l˶oc[ K{ڰ[x/Gvj " 4U A捲CPo $sE}@^%-f[f5LᬌZ'3bJ-6sGK0(j:_Mjb Y|u^]NfZh3t;B o\TC\ȿoQ$$ +)W2rm[cRY=z74C0Ozt&eAѳCW +F{ew"ֶ'ZdM?bvzV*z?B_c &Nz?I~?ǔ3jCSZo;A tQ|!2;Z/n,yl!@ssD-b`مDgtD1WIl{^Q r)9"JP (d!w 0_ A]3+6JB+5=!F".&>h֯_ V(yKPW8Jߞ{x\g=10G#ac6/ȴFƯ:PL21YwFX ~،#ppy li̭j w=HVf3ح{IJo(y^C6 nV{Ze T؋ExҲ9 +sJs{/+;\_ YKsNp04na sHYT8Iޫ,ì:r1FAYF׫*e$N1&pr}J,4-[l[a/'L-r`ʈCSYgcu^viz E5Tp[aY9ROZ;`qvAʄoGY\ ڋLzU}D(=e/,2BUHU({Y5@=;ly ? =VOIJxY.2ۜ(G Jbp> hg@mٮN=ڮxO^M?_C@_;Mgp@;i}a٤)|U=I&=j٬4( +3{L|8+io~:*CfW +8g+ܬKި#)c!=qWqJ +uW]ꄋFﻗت1ZP2#nF0׈uSǺ5'kL\>`?/@[@3eZȘOfw1xE&wL$!|c5;ѥX^ֿC!:;} +\9/m?+~3dS~x_y0,t +m$C;o,ǟmQΥ%pEo61pPȟQߔ a_EYB$P%ӇuHK4VYvJ;%Zj.-yT9b-n˓uY\ +Ƚ.5Mj肠P㬧AF"z-"IQsnlɉ*ߐXl)k1=B'xsIToީ!ߤ:=}0s:z/s2 I<>^HV{-&) #n)OH !QC͛r V1heףE]{E&QwG7c[?s5KHv-| i4#V-ǾQr/GI^~-|nɒ٢jP(癚zB< +G6'2%={' A4݆o D'FAG͏y+*g~Jw'[GWǺ 3JnԐ-ƞm~x<I:36ɹ 4NܻQY=ΝW`ΌF؃/Q r=GڹC;Sz 1 ?6sX\ qIڳO(x.IXvvF%QEïO6Gi^3#yӤke?۩3/Qc0"{ ~ ܗ.ȵ}9ĞRr3)ʬaX$ܯMһ=80(_xj馝hnX!=:pngj8X7ƉrHv˨o@$LZ\v%tAzelI]9瑔܃϶槐KݛyC~3e39m8 뼼^r)!1,4e$Cמ!x`2oIry0#bPq@hέ1X#DA@dz'C͜E^Ӂb/r|׺eYi<\3etanQ"]h Ye/54jcҨGh%#JQSFНnONwaGBl"6wp~JJ^5?sS^ ]>I>iW=%!Clgu|l@2OǾ}gbA";\W(z*ߝ3jhsj(+*?}AK$iQ5Vcc㶟9jmƒ>G6G4Q #hޅT^+W'o.UQ%vHvȝ^g +sQS?Z170,X.f[ +>s۸vH3u-U%US,s!g+;Pwl]I4J,5+ -̧#NGHQ?*=.s +KId*N:+N/1vjag|uմFx놷@!vNwP/mmaV{?N=:|3$VVS"2t0:F淃CɌ=Irjݮ5lG-q83bO489nU9pCd"z^r[|WV +2fւ+ʑdn; Wcی2KϡAg'{9a])6CK A h)XVn9HnKt24XО>OОΈ8>q"SGGgÁBCg Lo/(V!b&;Sz  :#{/-0-0ha!xJ3&]uӣVp&K6H]a {(v̊3r`]ZAi֌ORֹ:Aa$=e87hY秚~̉UF)X~nO[[`Pk{/V u"zp@9I3GXC>@F^T%1W'qL '೹ Ӷ\[/O" `_@> RS[[d4jS !OEJyLhchI6-wʌO9bƩeXեEv{DoC~f-~ʘH}MDXgJ+v%aj޲ Ua!csBI 9f) u%;4v~*)՘RApc==*jZ4j[l_X?HTR"1 :To]5ʬT%YʢؒRE]$QdX &֫,&ϷKIuK^XU]z zPEm~3d@S6ևQg ,pSquM;LZ*}颤=C2N ΎO2Hnඐl*"O"3_@[,EiXۍg(5Aa|O/ŦL8 e8:z8mw +ܞ9ۯ=| +Q@ +VI%CUhN-x{_յF EZVFhQ +Z:1c)*tjaAW&3%&;7PoGO4K rKp Tt3g#CJ(GOGGjf2:W:v]]5E:W xʼn2%+s)rn:\TK^ʉ#62 #x۪WM[!VY, =+ W礲|gHU|)*s1:p\D IwMUjR_s~mtR9;ֈtDJN'p)C+BWx@TijuE#Z@cbqɤ4!<H ެ+yd5N"#=+|#oBQ=G6T +o9LQI9A܄$>ÿ6f~މi#WA*\Q WIM*v.m+!Xrhe%kA^f<%P|ǪCݫg=!54Bw^ϚHЫP8qrƎL;zP]?ZMe|PrBi24"rEZl(l/o^[ٝl&6BX-^Ýfe+Q"+:<0tTM"(7N1Dպ%d[~wc{^YH6Vcb0/7ⱗk}q`(c .bGN(g=lc>s1=R$_ZQE|EDž._i*Zczk7g(V$٣Ӷ7![#D5L( 5$V%uUf(Q禎̨4cu?SLEʘ|t{r,XC^uTGw54cW94h)la4{(k=~EfGV*SZE|˨W\ qWq\a!Ht7A#"S,?W~?"qҌ?rVʆPa")  +.X͈-Y@D)"lM=r3@C=7}aj/ɮAOÉfxh:Fru`2 m`:2 *sCJԀW ֠C+מ<PwL0q[iܫ.yw뚔]/LQS flUP6le srZӍ)4<8wRmGX`s~^Wp&2ЯRKyo:緲GxLric@bfHp=oke#]jXEAz1_z7LRm>Wq޲E ǐD|J `& / +3 += ͛ +db=1Q9nm&6ѵvtJު+|+FZ+]7@FlGQR)#rOT=ÏŚҠIh\^r JU3V-?]Eۃ3&^[KB&6`.#t2'G)goxш406en +RdF,'C@=ؚkHCh};OLΣP&Ji#XL%e>G'h} !,ymtZ7Ulu6x8jѮR _qL޴/++VRS ǾKv)-Fc)_N}4k[FR>+l0-0)"Tܾ&j(97J|Ct| #R)GuAKy\  .MB}Mq|}R";/O)$4/STK=X=)@o3lI8CƏ']wl/lX[..R\^gMx3+~–%?K3$?NbJ*PѯC[ӓiaoTL +:{{+Ih,-pɰ0@f0ҋ_e;'))Cq +kگA,l5++\nqћy'.ݒ1n[/֘H+s;r@9oqî(%z߮:e-Ti0ĎTtVQztEP*u3doJbPjJsl\ \BZK+O\T%eKy_~E|ƭ(g:.)xry:$6?`%XH}4,+|/k.`V3nFQV+$V ꉚ#rBR,{'A*¼.ZWR5S=|:QuR $Kafs؎ML5ks1sKQfii:Xl@J+ :yJi^k}+߰+5y5cކל*Z:-ɍd~MGOY}pEh!@U! o'=>[V}V&8} }kT%PHD+P ![Px3[4b-M/MR'_~~?CgC9K|Ah>XZ rh{ZZ^oԸU _gqzΖuK'b=l=ܝbE*fQeEJ8:RU#.ًQ?Ibhcwϗ5s9 o]MI}7.>X +T5w[ԭOWЍJ+ 'HOJt8]MN/pΦ[]DKZX~|! |9e}Wޖ-v;rң{nۅOP@;C&M#A "⡀\ PA[Yzx'DcUkb)j(jpl/ݬ1W=gT|,°@Uސ5\2ۡ i]Q#1a<&NrׂZлlekڎd/'*~,s0L0I^kSf'sk%m.v\{LgH `p/bHVv gWd=V~ v.uǼ1d8tо z7 pն植K;_9#{ou^ &KBeDjAD-$ 'pBԔE4]RE^IJzo~C-][ޫV% S@ж3GX + [i,sP_%J_z{; >W R7Vr0(([W>ucBYu>%̃ +uXBryhR;**iu<|k@Nj +4FA]2P r6[4mYZWVYF~mԑ檭{Nf># _͈Ψ9EUhv=V-%@L/d2u'|;R?߯=%8C&o;2v =%N[UR:l/){k_"OB8Gڿ]Kܡ݂ Cg{עpV \ѮQi%W +d!G-~ٵp +z@ -6 .m]TiC萵y\6(ڷ}& !mU;|Gq)o>6mVt}xD Rsaq; }($ׅk;I1|Nv"s Ogce2 <+ :,􃯈$i| gh-HDEGQ5_q5̘P+Yx(vp|v2( ϴnxW6so`XJ_ba6wL2_Exk9!`Z7/ޗiuL>}Zro'`Fж@8ȓթĎ(` \pݴ\R>xDUXw}:(Vť\MIcZG4.hL!=sybyy/8V/Mt@|m)a'кfC7Nk}_Fš$[T +7'n::V@=I t?+[ ?éc1bƧZ>>XFt?rh6r}d +>T1M'],}g_yα%`hrMM4Ƽ=CcfZВH!h#eBzж \}G|)/܊IиsRry76Y-^>lߝiD,jk fIR3 8ts!ݒ4?$+VxkzؐݠyD[l-D xx1*8qG.\ Ra ! +Ny:ֲ/*#I=)\,ӷ*4CiG]rĂRDI -{05m8VK0BR /F󜠆cɿOYBY뗳|J6T)1w-A"Zf:71@ 3o n0AJ) +h49{Ajjd^YؒcM"JlQ)"QUzɌ#5S\WI3\Z"GU>k+:GҀEߪV`+Pv0_G jT ]h>ʖ9s0D$?AJ?qW^ 0CUOPu3]Q(-lG5ӎUPS3un%v-@u & [_4j6zr;{Rpd.I:y*m|~@A(Cޞ%$q=Vm_jBtF6Y]aC]PXh$ a]"$Nȧk(Zjz**Es{d/gFꢪhC9#B|_{yo[Z5f,(\%!X5C`P .> ~ӺV.5F|o 6B `c|;3":x"n":!Q?B.s3KϑJw. ֳ_Peɏ%}A.P*ExL ;&q`d NT Y`pg')~,lO_x9pR%iD"]nՂߊ܉j,HϦ/0'-`qY1k#@TTh>>+jxOط[D"ѕ:[sU3;c9Ɩo[G,r^^-nODAү;TXYVaKWRXc*H`أc0xLD+17ȷE~!GyMqVN DZ#7/ҞxPB#_D^oW%W +@kK9Q-]W8ב}18nǗMIJ}Zq@QytCPkqC#$ +X>k%qɉfLڹ{Z&(]ֲ߽VoqbXtGhJyEP7 AYsVd񓳧͘Ϩrt 8?S~ޔnoAGa&yWG}Rn.G  4%o=[M3]{C +#QMZ\3p EO7f#zU:1+ao6,W'Djn+!C'DHøKIiŐΐGfD׊҄o,N# +7TwJl$TWAb1>0'c,wzE.HٞjLe!ᾴ+<:ڛC#i\oh<7S)-Ijfн#O8NP8W<[9F߳8,r+;(Վy&Id +f{9Sѧ1Cto,vOKZ0wm |FNܤEamlPCZUf`O]~1Qȩ]|┹HD=\X ׵ *XH$6E۵HO .:X-ń$ ;#A꽏<^ eDI8a1)IhߎHRS.ѳ/[n.)HOA.oAAcyUN8npaZG=zB7{Wu:*HweX5Q3ciX.[  +Gb;~RP,a=eH^m:P 9N չW^c?K6Ѝkgu@f-xY'QwP%,0';ko=4ZՕqUٸ_(7ɧI} +>bDĞD0BT[w/FK7cˑ.^H-d*Ԍ0ӗjWw=&OUJ|h0?Zu"Xgq!@W˩tg㾂OMaA]/>9I钚IS:N![unyXp$XUɩؗ[x?4ux=2%I*q01 eiˆJwc*)AOdB2 ,I2At\ʜ:PuX{֠3f–mӒg DuDK tAޜDodSQ!KXT! LpٷDz% Q)6۱|0T):z4>Ksf,% pUYlbD= |@|_JCi~\ܢG[ jȨy$'Wf +a_P]eM9 Om5Ha5s#dQϐ~ǥ.S +aaASz4T:.FoɁJpa.=m sr{#l<[!$`)lI?Eο~n|] +fKvzyX8%F3 +OrTz2A3[/a}Z.9Sr 7R\ +DIT2y+w(48š̈́olK0kOY7۳sIz JNo+%~CtwtW0Y,Vlq"z_ +3%_ϕV~"u@X(nGD8h Krd#zJ^=̷i9(;bm* #>O{K7WJxӔHej1 ?fqylu#7sglK\5Mւ<Ӟki'yQcMpr5I +i#{_g~Ι,hQ@[{jU<^fS! +o!i]`¼X~eAʉ j-mUDPg& !(X맔4kztX8%ɠPs_='|V{۫LZ~4EI!޿s|԰ym{EZ`-w^-A# ~Qob;ą?U qjsdn_VayN`,;^"TfH{S⹢*v}/Z{J2(1j ?qarthw$e?cSexf?qd4'm_sF10 1v:KkP#~*Go!! +e(N>-2UEh=.093E߯{pE[CXeR?tc}R(>JM%K '–rEwϷ_ѢBgm[7z5 + 0kG{mDķ_Я_Y]!kT6+QxbD_[7vZ!{@#:a%p_bH`^;*#;ڍyLg=ڱ@20ޢwl燩߅Ϊsp0}n[IKE`;8Tr^;Hx[^&G]j8*ep]W^WG5)tJrfkv/jPoİ&!X1'; +ʣ.t7 eSP(VwxK{ AԏÏ\dH7 +wx $\aB.I¤DcfD*Zn]Q0~i'SHL$u9rNzl83 ޏ/W u7=6^?XJ[mF3seGctDўkUFʙ{4eЛ]IG KeHU;0fq CO﹋l-IҼ_rZ.LiKsGdՎAajK5n  +m}Qƺ)W{WđR@7 [x Tj.}CZ`0o mv]23 +[v0+c0KBTFsyBq,( 54{-qp@a"zVi_ׅcyRBN|c..|mw%pTqUЂ=qE:>#~(N^Q~Vtev8v~8N.fM=hſ0R]jw +[գp] i2 qXA5'_3s\3dٟ_ L1śN#I7`3{.3!:FJ/$? Z龎W<7jx{uh0gr,ga:R$3QJ*U<}_Y)V WD׉Mˎ~fGhfL9G. G[&,ݾS:rQ#7Jf XK"x +Ej><({38*&ϧ=8=yTZYuO$ؚ)0l$zwiacњpM~6زɪ"g;ݫ 2| +w^CXLL0tZJ(" +ԧ5cP:p.qh A@*uWlgl\7][dwTf.8.`Y4Ν%+bA+E.jq`^R#6[s~T4pGp) $Ѷ[*UlqZK ? JI nϘ3Cc.nqʍvof.ь PjF!\{Qi`ID8tJv:g-a8;Ӭ Ҋ.$)dfiPS W8zvq;@yJdwRsUID^z;% +x&[p^W'(ׂ̏~Wpn1Gb)lGVPrxS3©g[/, +jz]+.'[sw'Oo$38]tg}"[JG!%iXp'o;P8:3$O$&W/../HUv"J=uY<RaV<۳,: k &E5ۣNm'׮pY%]D}F)1O2 +3_YE᳎-ZtĀ4sV +buJ3$^„öCt4`g7>zE?3=zPB(&{VwS{28Nk# ;U4dBcNwtDՍ)q&1dD$Վ\y#gA[<(su4S Q1讫 ɣ/Ȼ8tƙuZVNf!-۩nHlr/ЎHUIqdžW}@XD8Xv Y\n` dWxB\&gc fądm-7_GKi6*.N#zp{^]DO0Y%#Nb*|gFsx#{<#6'84}KD T]DRAźm˧Ġ&E9ǟʙ:'۱jL+v aEOys!ghOl:_$M>V"jя>u1#R/;R]ߊݦ豅P_Wxa::LGU;h66UtV}ˇ+d|bI\*[ҎJssk+mv^8}A +e[wc($uR=ܭ-YTvǐ9K+htSG (T)0Έڈȍ[0)Z/b9Yyī ff88&xGΕIKje@)^2T2̅2@sp: +GkX23ZFLa:Icn9B\!3eh#2k)`cPOqMC?pFw{~܀e2iHt hߴ + H~TtDbA:MJ +3V& _~ +h![bϏ#R5tg̜̎]AMDAN[Jp#"`P60s{3Z {7Jlz+>/͗s}xjc* l/ ^h>j6W ^u. +3ct,Oۉ2JYO~unϢnЬkP`vUC#11/77;SvBXx +-2UrOߛAݿR$5c;L5 +h&|Ƨ6k+dGuPz2E]H}Ӷ Q|υ@8$R©,HGs^ҋn[_p~E͌P3?ۋr߂NTr .d3ԑtd}It+M\!+7+-"@M x< @[wQ(kŔzs$˟;Chp(%鄷[]xG_ɋV|O MЯNp'pyBbuTG_%XC0@QmqAș[K37:!t+=wW '6;cã0i@T[s xέFLۑk\4w޸a/ 'B$(9Rp +L#?9C9rʎKi97XQ@i ԕ,?AuqxgBX`dXM)^=q8Ul|>p9 i!;xS;]h7֐yP):~:aGJ]Ty}J2c@<[?`R9'm}ss]:LQfW^ ^v kz?ZJ;V1vr F̽b!s7DYbsyl`"'[@xQI sxmIld=-]iʜR7:vB8VnHze"q\zt'둚rDy_UTNeGY{@0뵯:ݗ%1 VȔԪǕvl*WZ0? K+aL{$e_`5Q=Gqxky21z]պJn`޳=bF)֋% T7#lM2NuwY3q[D^񱋀M1t%`52DNWjj3l;E0q;kYU\I1{aZ7qط4`EP rhx5&{1fy}-@*Q +Pw.oLNדb'Į,zg_1L>H%Vۑe,)}JPnAN.m^},ҩi\9y13Mա?.ΐx:R4=4\EARn!=*ԥyTJY2Vs G&Qgz٧|B? : ui+`p3)9h<@/RHQm~7\+&i6Tyr?~iHÁG@EߓmNCWσcBs=aa1Mȋ?JǸSҕsy#)qM}?|Y%XpZڴx(H@R#u}cN,hq4sn4ZPl +Hva9Rz+мSɯc2HW2f3+ʏyP;h 8rBLA 0lKk_-((gbb 'K(R 1s +p5e9ttj5槰AD8051!TFcQXPy \7Cprgzu}b8 svr燭5s#DzD('2o2+93j#B)M˴;1?~+';9W~-Ƈ8mQFaʊ#;Эr}"5ԑ1Q)UNvX\' 4xԀveOxDf)<\%~߇!ћ, >26cacp?N=DUaeV/9BDϽ.05᠇!ScTW&}CzlSVYƚ۞ +g8C7>$qW8guܯ9'8V%v4aeQsLӆa:bgcb"ŽWIE|[s@Wzzo+\pgct4#Κ!@yOpayV|ss +AqA.y H EZU)^ȳCPsrr bC,iZJg}׷jȏ°{bFڿ>몧r0uSt)?~kLYǤt?_`~M[a [qŋ5QNdv>24RD#tH4@ 7?+Wv,\Yv[UIgq!PY +$ôu0=%ND!goɤC$T1O}Q㲊NO񿐊ڑ]vܾNhQhmyeJ':|ޓmuv4rjER la.&ɡr|?go_VP +Vڋ87X¾r?1nvJզ:_X +b:C-X:I)ǩ T";q\϶'ּZM[t4}ުɜ}S\sC-W(3(l>$>(#ZaH]`{|Cܕtc^/(Q4a$v6*N 56 ]-n?ۍJ?̩7eM6 3|9}w5@|'+}#FQx;ܕxǎI%ḎHu"H#ð !:i "4jQe}pIT|E^2F%qv'ڣ̡N AJ%D ZiߛE$ycc봩1)X֌/_wX'4~SqEY9\81F{uIL=N;H꺭JJC]j=KV":5+(Ea*CYx/xi&[AUG:aJp''|d~WD*_B3$p~ѭZ\+޶sq3ʅEgm;7C) h'H#p^jCk:mYVzKs*Ag<jN,-ZR|(T^h{yx^GWq쉝luoqWZzx0wԕ.5s7Se~ő{jV^"oڦm]Eʯvb{Z[0Ic+l{筁4뫴6 1E Gel(m_Jk5`W_:_&->(( r|KP^(Q%NX67;DZ8Ll"Q?PUJ<Ӱt͌y[Pd@A0OGOń߫ӯo~{ m@ Nc@A},pJuXF|N9Y=(؄ %r;~@jΪ +XCKFգZϪ~5σ^*=sͨ~V|(me8Gjs^jYIDSx ׀O\ljgՙ8iyB۪}\?^+SfZfTW12>iM͌T6Lman,Pwn ը!VHPɾUXv}Cbcu4n!c[`hEJF%Is?zhciDsg9_:G_S +pTCs>p~{tQ $(etwQEm4Yz2(%?J GbQ-?6 +Ko|H![RPMM?*|= +R{)c+:k4E'<($F OuODQ19lDQjc?Hw˜Ѵ(xQ1anEDCeˎַJoiC5%uW<jgZqCTt|X!,3h+^~{v5n[r!0ko8qa&)5{2*DRЙ]vR; r1ø0K&'ߜz; P0 .}i^ߥqdW˙fQ" XFq+CNs6Uj(1H79%44 p+s_$LHݯp-!v@r*G-OgnW Q^D,бW7y-@kӸj:aє'M"W`?/}\4wqUO'fѓ:gvtl0亐r%Y VeäHb׸K",tw԰3:T>I)nq1'{ NܱG)W9+C4$}h(8΃1$NHu5g]=۝ Jd" >)3'-eH +(? faaLewF[ÙXmʙ'ɠ{$RշGhA=Oa="r U!kh4r?b^w@>=jZɥ#4-0`:VQ+y an]P^ X +wFzCVg+O n67Èu-3\SU  ?2V0}4<< C,^[e&3DsqږlZLnz88R_1K_Ba;y}lo͠SȤ۟ZڰROcx܄#4zE~;vwOО=#8J)BCSI wǙF=eB8PU үKM8ܩ=]ġ`qgRːAfȰG"-鶴 kq#P%:Gr":Z ~~(8H>܁d&xGĴi\_C!vo.ǂ9S "lA\nsF/`Udɑ԰H[5Mx?̟:)z\0(^)24pkjoX{Şg u,|U"JgSvaiwjҍoF"䀻+:,r@7d"2c@- ߾NСH~R}*3)v˿ ePl*q_jP"+nBOb b Ǣ>c$1륂3/+Aщ> [^j%'wK/⾋82V-{^w=sOd=A h@d&-鳋2 ˎV;ؚ!ͼ'W?:^=Z@J w5v;qARɸoDU,}Y.##nkPvjjĜX7}Ca'SCC#҄}D bOizQױ.\=0{!['5Emgs9X϶¶9;xJ;xR˹%/__wzDTzRۮ[."2$zX* vǜ`v3Z6P=jV%@eO&W[8/z:9Ԏ%\'shSCh[JG*B@lV4u(0QJ3C#L2y kP(ogi (G+{גEʝK CM +ᕌQ/1 Otk-Ex&S͉CDVnP =U7Zv9[J$||^qu3?5s_]GB'g>U^Ń2 +.{˗ SKerUaaeoTZ + d?U>us3ݪ-Ԧ` m[ۿ$[>UjIش{E}rZ_&"WzQA#>sLgv:>l"Q; ¹=)FyTo}dh)SթPW:t-t|/I`XATE pp$ \T V-Kz4e B ('b+znWmԻ"=*Z8K6Ȇrqm wF<17;k0>aʾK/˲,H. T0φdG?JzLUkS87ޜ$ \[-ס㹚&RbZsjsmܡ7x6XQq!{".)4$_&ŗw,ޛ=+JV}_L?<_agɱ s!8rܕkq^39|E:Q+?e*i;sia?BRVsηrpכ|fA3I7f.mYPV5zݍԞR D8:GAQō=O.c"s>SfN c3ħe endstream endobj 36 0 obj <>stream + Z%b]-^zÐ:WX:-4ır>ĚyC)l#&"ntS쑄ATGN%#Fp.A5y`SPsw*BoCL_YU.cjNh8mbZ g׻6`I0CM=]U=R"pľ yK눴+J1T>jskmҊ{hn*4XcsFaTIϔa#1P&5KJ ۖ(U9̑1(Amh +CtRȀm9pK퐽+#{_;*<+jy6xp"@wja8V,olO;{hns%O=R+{Ix|{ +XӺ5Mr=#Zz@d+;ΩWRGnBJ|h-HW5uGV0 D(WS@# =)34[ޛ/qa'u2:ss>9w[$QCYiI>9Zm`n=AcXGjObmМd5'o'KMYh)Zw֖j$%o}qrES@\/."dɜU^z J"g1Vsi]=4k@uPŘf Xf/kuFk Rq{ Q4-cQe3 #|V\=7TaB'{nBK(HS U28[%t"Ei7ؿ }Irn4xPT&(1!ŏPӠ4%AyBΊvMAHEVAsahBȷ(n]<_N徭kv:{0E`[ wPX|uVE0NIRAStǤ4W${(EߩlB]j[s?JSm|҈uּª$zp4UGpGb IJ^Fp";5Y?'1!TRbRy0$ N6};"]Q'zl^Rֺ)ni7l*ēs s7$8\}cׂD5qQ:fA"~jjlA `D7ܿ񸅅!\sXeRy$ -݋^Rꧼ|5s`PT jStt՚Nj1phclO6^%zjK'RӞY%A@C`bjL:%@S+,'^?Q, bT+2D==`v,i6:u; 5K|K&6bs>":]-̨!e6kH:hqs,43h)P쟓#_cl1ltc}\.3+|](QOC=@̮4EW厎T Om0W`-TnM'l[4at%#1M'ڷ8C\FPPDI+4A*l3w䀷wAĕAOͦebXnnݹq=f@׍Ѱ! +F3!Fq~*, MTG]*cƥ a[**oy= +^cj" "mT116޷wq`}"{_,˰+ؾ/-J%ZaTc!?b+o<W-J_޳*%?Q[[>( +X.sC2LFd|`G<΃AeSX.?E\s!:ttϻN84SF!\+vzB=& [:]W/ia圲 |?ܳ:ئĈI|Rrdpٍ]B:JZdHlEn?|6 +6<)s7j^ #x,':?Bxi~Dy̝USt;1OhOmBFfY#&:W QDӯS9Lv=:DVLw=MhrnTY~0h LJ](VewE"+ sݚ[%uFRzyx:Q9R0Ufz4/V6/צXaebSlg=o-mDQnjri'OU-T-#z ĝB~rŕ!Iu&8xsmA:sʱ=ⴾrT&r6|Z}y_et#k*[߹⩗b528J'v_A&y}䃴!*2# ?.Ҝ]NXfj":N2ÇphH9*yn{ep8%nǹr_xQ'XeT#&?8^}*{Hoed{k'UGDiAo!MG*F(?[1 9R3'ޝTh\)ǧ(y#V]X&]BjӝW6$=} aMxoQ^zHayUL!§dW +&UFYFT([>]4y-6|L$SӴ};&o@@;iXFn4lkČe"8o*=-tXI Iznˣ2n]ɜ-,b2D('rc44Q{E) D^i* kIf92;蜄uDBy<ʔ#;^ߘ}ib+MXq0jBbi8 #֗?@bd/Ϸbr)l5))w5"6! 2[sR9}D5sK<tn+ҥW"J>IfSGϝs50FkΪe舮~\֫o^ 3znƷDvf悧HZL;\r+xۙ)ҕۑCN0ȘyKLUpD.扶vn}+\11=W2r]ΊVDږ!t .Ȯ9`BTWլ#2hkghu =BS#ޅFD<[oa١HD!"#@8Gt|a!h6zj +6:iluگrfq-!XTX=jV/?'ߺoݤ%)8J;TtB [C.CDB3mxNt/ +0= +|Asݯxֽ0,?j{[/m,@Mp'jest +.eŅj]pH [y,95Ceb}RBځ{}`c+pqMJ=5yosB +qɽ3sW<& (~nuU_2q 6yR]<| +*A^nX3w~rEhb!z~ÏDE̎ޙ +>=8W"%Ү.4)FI]Đb ;o!Lc1sk0/WV{IFކj!s|s°XHe9~-g!=qA06LZH KS|-Ձ!~v,v4G&9[tK =m]̀X[KZ'ہҒmǢ/i[E/K@'}Ű( OvG\Li5iK7kU|Rɿ *GiqL[r4G1#^`q}2PiN!Juֽ)yZ}Nt.<{ +LXV@s!]׳7;6]tI84+Y^ap?6VH`w<z,ش`L>X:eObsOgBC +xhkmzeh'=s^WY J}I5;3F a&t +l>.+?8(UFPخAN\iȭ-+\!DDKxRR"k= w`vb‘[QO]ꎈ0/R`1[uq1&AC_,\t#CjI kuR\aIdz1y_[ \ +#2=ZC2̸#@S^{ 'ؑ.Ef2[OTe: RchL"=u\=+g=ABZ`2,1s e]7H}<ٚw;E]ΑAI+/hq +WPI{4juA4YQ숗h8ER +aIMgo~! ![HsgHzG[WNG< 7j¥>u)Ґ,IfRCf7f?|} ҴԼ,ئQe;i#w#Go+Ɠ K~5$e/X+ +dJngu-)gs`yljX<㓪>!>Q{r%%M :Vh}C[^^pVAZWjhsj=َzd8ǽI`/ŅY2|Vb~EAﶓpnuM .eg~=ό_(-c:%d+]lkg>}qNҦ!7ut0cDGH.%9#^!GPⰊ M0!;U}왽:=bUӗ`Rr+gR)]4XL'ɤ`8h}|gzb?wU?Հ"3zROG@\^g]=X$ ϧg@EMu.h:ͷch9S@ 7Ep獜/*&1#Et'<畀VCLOw.qqL(6GҐUp`hk+0:HC+Ar ,gTǂ,&1(x9>m~2m^ ]?ک猙Jlߩ9wIv*43֋ҺL_wjD33ui~գk-ÓꪎT}1%VaRKލ/1Cnf, [uO7 +yJOJuWE.|ݛrmVI^-wE0j@"3k79xt?TTP=C,Oa1+x:l2ޗ:$,cv\AkN{] +]k +Hp]J/Ω9<ʕQ +pӑ@vP/ԅ!pSjg.Rt|,cƼ4kU~k;>dUȜ!~;<$Cx<;"{Rv>Kz(&iM<%f܄ +`;>~V4뙾O&} b;T6![_NZ_@~pBQNGBHg/^мOI͌CHBO-jP+]rԋQ{+Gޚ>w0#g5ȃ+xwP>Cw֎'9ATѠ @芌ّ9A\5 + il:+w +{]5c+.0G }9I0j`:&wz*S3'Pϣߣ*Dr5>^~l LbOū(PHG ;G#Xq.qVغe0ޱR$h=-Ocy@q*Kv ,aD,W%޺AkmZq XAtwD &HRV{GjI>O͢!<9{% +!,RD뒂*0HD_TVh4ۚM?0xO@jWbo? +!Zs&Ѥ" qiRdS]C'J3|a-"gq&ʔPzNHvn A _>3g2u 1Ʒ$SĚ!K)= B.vՓ.xt:9׶ '|5+mC؏t>SG֠TmS|SJ{r#$7Ar;^RN|ENG}騦b%0tULb~I_4bjyř" IՄߨe g?݁9.N ]y +QUڤeMVb 5o/ `lu/bօX۽BXd/C&^^0FiB84O܎!=> _` 9zگsI>~㏡\Umg'l;zE8g.uzޖ:{P=-.") ʽer{#(ԯo6ȮkЩ?"|dqad0$gtяLڴsKݍ[e+'l#&yx_tƽ;dRGKL~ <5&{m"h;Լ:ː(FoK)4Xf3y#فGuJj;2W4#7-9E Z7W"RV(kHY`n܃_q?Y}.-gi9+}u`@?O.6;G:/+&|[&e߳6_0kAaxi=TK\ֽk -iU-"ճSLm9_= p]kmEiWuZ۞xp^S:$TZB#VKqSC] 7vss@V8M@^{WK1kmCq Sb+KZ)KzqF CrF`pYk<6T(fR+XJ-G/\oǂz0H 0Z4Ł@ɷ\1B(Jtho;  .qDv/~Gm7SwE~.nQsE}IU}<_(_ `\RymٙV8Ű﷙tN"͋+jxZ̈́ϥdGckg:`LX6@W+W;XXnTo +8[OOw\7#M:GO\$O\# +Mf>~c{) \T=r'(9'¸b+I< w:(پk{+2kQ:y7[#m^!Xn;5 +OGhgۃƍAC>apnMɫٲ>&,wŦ4VVz]Ja9 +TpVq;ndU3Qz}i yp]:'^n=PKf`"~-bq| L6BƄSx-Ԡ* +?#Rwb`2V\g \/TuEaNprkm˹*p}D1 yTvH=+ޏDZŃ 73NI_vl[E X><?-2R̕!szqČv7gzc'+շscT! +r|({3W#*E:|c~u!0OnHK8AkDv5ׇͭJW#0j#*Y.>OK> +st[@*3i^5dnMX GK!::8nyGmdX3AwM 5@C +VT:;j@Q*LzBS;c(҂ +jQ0E:I@BY#j6d3dOt:!G+D 8:\6pa$j6߇+zjn.uF-uT26sC(?;\WJn(ZWN$ш56 GeH\[6V[Ƿd6bjFDcqrVP5S0]~G]sRگٜʅV GôoLi\=ƗG;Vʴɍ sM D/[/81hg1KGmϊ;wN˃ڟRou8ևbJ.JWR轠};l@- |f@82U*yT9%WHf`1~UG\19 9&:V5Bj-=!\xvr}T{XQ\fn􉃁H +H6Sy⫏c{eŘm?_.?kXiF$EUK ۋMEÿxB5r)>҂C4\)L:iBYKW(Kz/ 4ާOSט=H}# +ر-FM32ۊ!BCCHV|z03E&-$5ʹ˕f'$CKEFނ(#p(E>7ɔ23G鷅tUX'Z$Sry](p.#]+kͼ,A\ԗQ~b|jJ8p['zZ4@&㥔z#HH hƒ@fnv9'?YNUk2g?٤ +.ń ds+NgX%&[Q44oBN^RnZ9|Dة7eNA +%|S̎ z_JϙeLe_?U@ (4C$U|g+@YHW9/υ8N:KG2SXt;ͣ=Z=ȻIzRc, }3J{b}}?v‡>E.EY=RryO.)Ҟ^I{~7IW||625Z =n?zHIȤ;Sҏ8[y>bUDN!AEƌ:N@QI{'E{*ȘެVDeiS=-]63Ù]917)s_/@5ifpB*f_Hr%4˴zJ纋Ԍ8}X6乖G֌ꋧOx7ݒrvc!m9Zَ,d ܌V}V'Om򊇼d$ocrܬ)t%AŘa~/waAZ~gB!6.Sџ#g_Ύݽ,qČ-5pxƋ& HgږW˝{GֳU- +@M:Y-"[[;tTYF+'qI3$,¸7E=zP/|üպ5B0JylHYQ҆.罹 Җ;Ӧ(9GVr"zr`h|7UUފi\ef-|S{!*0[y3|:.'-{nE!=^>eBD"L;ˡi+[ +ޝ+}ytԑ'V? ל?C(85@($8$ݰ!&ϯWv7Gy-=+q3QQR2g*}^{clWr +\S+N"{41B0PXxOkڛ\ +C pwe#DiS# C:őx^ pٖ ^CWr;\5yWwz(i;d#/~O!<ByĂ-4?[1/V~[>}lCAd./=o<Իnuj5T~W"ɷbF; i&TI[*K7UoBN=kEyx])dN@6"E+uy +U #ωM'Hw>fLKʂZxgH 7,c؝875S42HaNulN!oOyF4^mt%M{YFXɋsz/(tQKzM_g/KxaT +h wdSF"cYr@(SWGB$sD'V@YWoQM(?B ^AGTi>/]5{>^u)}GitO3[!:MtҢ1΀GQ?u"C֪T6CT"UdHFU3&}OC!{bSZ0A׼}~AGĻB˻:@gnK]Iylq~@DbG]ם5rmLri_&MRe7U]?xq[psK뉊hDdӾ|fr-Ds("Ǒ`=5Er>\o>1>*!fܥ8#`$)ͥ @SU*OyL"pb~. eϸC +5&ZeZpS5roa{VRuۯsҟR-oDP1ehg{x^)DKv5k2/?N ?a}Wɳ J#ѻ}ƥ/ϨeG!"te]A wg!Ao6VˁK+ . E8#b!@2uƗc\+!s|w^c6;#~#Wʓюvl/BRZyuBgxU9I_/4)ZDc;Oͦ)x**Zc(la%ҙ)r>PY&Lv͟V-ۦ+}34 VnMYl)>3z $.H*s.:T $\B.iN('\۾,`[/ф]D5PΛS=Q\_Q (NdT8r(C#DbZ| Al!xDOӨOu¼>aXAV7.sq|.p]!X<7/EWEύRaYK]"ksʝX3}Ѡ9hGE݆|?p ;o +W}гA@b{5#7;{U pajO6V}omw+),*d gN<gX% JT jAr&yXY>ӈxapLe YS#g=**+T`hWdew? 1Іfe(U1@[; ^[PqԼH{U2ˠV։kcE^%$ؑ<3 1Տ<7w UW|s2֖Sxwy?HlC"{O ,b>hԎ| Yp^*˜jlbX#g*=8D#C?bb)lmnHL);5cw8<IAldl-ei`:\e`RX&bq拻Hڛ:SQS`T8,^s~̺/B9 :+rg> -BU/;Rmf|'A )/+| 9.g΀9Mo84{G?( +-m jl W$3Hۜ[=,HB.B:=g*+h7torNY7 ͧUVM-| 3[Rݲ#[s1m )" E\zWڽZw$cg0EhQ#20֨.I'IЬ)f82zyax4_.EN'ڨ,<I3k m7C:#s6Os:qԋ'tɜwJv⻑$-ITl<|:9˕HaJ[+A%M몓:tƒ=ly~R/G?[)KB4TD'VGʖ$ I\þ${.C#(J,DG9s($qk+"R6V: +,ReMH6/ ~շI{U qrҹ9*E/C)ΊniCX/A`8_|sx$[GڮL^Y +Lh5lW[ZEi~VuEnԠ3 +\"oIU#xl |n1Ν%2ܛbW^qg b=+>Cd DŬ3M!+A|D(] S%o&'ofnxZB}='NLgud +7T3 +mG˷a6NMQଓ<Gj)To+V**R \ԝWY ]xI/ibWG+:R׵NJ +Y]u^48L70jgxWc$ڡ%2{8.j2; A-E1Af•8z_|S-'IqT bRG; 줝ZE[ p0<'塡h;(`#Һ^Ρ~Wk1&V 8*J?BTԤ:Z\Q +%G*> mN~{sSdbׁ{-RcWypAw~Mp.obt3f(G,Ɣ#r%.:WHs&LHC1uq)*z.e5-Ǘ4+>+n]Z/`hBz@ݡײ9"Fo&`ŭ%0$W5cy + ˀu)97]m<(*<ҲېGNkIݶ0:xy+nG<`=ܪpጵ}[#`ޭuE*(.i=9G9'ɢ_+mUܦ73wta&-94@if& +?ЀZu8)PxM攜!3=$f`+r{ؽ㢍@>U;Mwd*ḒhF|uoT{-w[# 2yE0՞{=k +&*^ʴ#(sHzuʹR.oR&pc#*{% +S +uXAw:w[3࿷ _d5WH;:;p;wq8ۡr)NHٽHT|fH]N$Ď^0m1M] apYzu$jI\6ZG)qyWtQP3HqXޟ "4#S%+=Resv{QJ1t*ǚ~#Ł#\([;eA-u/R%xġbnEn_^Kꉆؒ.3Uv eKNZ2:})T謩P6fCYSJxS:Tr`QS2YUH`U3ž0?c{ҁw֙T)ղ_~6#T\ <Ũw~ < r-iRKA*sCb3bo)!ReQwJ\KW`+U Y$FDY%5#`O!HDZy?2]esąנ9QT܏PI)mϽt#=7Y7S0 mXQ`]ؓrVC+ ylͫsDNO/u36sl={G {VQ,*(՗i7\ ?KK\,lA$&}1VGMx_W܁긪_'0:vMh\ {Os+ӧI:CcP4z>k[!Dkb_{;_X-,s#4rʣ;1_D$zBcep7D^:kgNS9nתi#Rv8@DscL9;d#W ;`my|;<<1>kj>2Xto\N"]Zɀ!z-@ D{"1X?+@">̣*LV&DΞ)ض0Bb(#"WDzSs!+1a]XpA!jsƬ1UHO̬+HS(XގQ2JzNZWwQ17G⒫ǼKY w%ֈUͪ@݄QE;y\Sq <Br6 Lkp7vDBK*ѿ<TSt0[ Գݣ9 +8zfJiu*[X78hȊ +&~%X.uEB +Z JvJr CZ,ٻ_/ÆL5lj&]k#;:]Z+zAƤū=kq! Bb%l=jZ`|M [g_ 7$,{(/039~(@:2|_DwV+!Qqv@w*W;e 07&- s^*n[?!xTU풄3~vHIu.)o]nHKQgHfblQ l+N6݂hmTpL0 Xj+u;ge1}t$·籖d,mBG$Yu:LS!5=(ER̍{M[,0}JSIqix@=D +~~&%:W_CDs$=nJ+L#ES*3\![2{di8j9O~o# "XgQQGTgORBsƳ)kґ*_e2$fR0^M 3ՓSz|iOy,$WT7I/~..{3\0`[aH'A˾+zE ˼L0e)pSŶ pZ8ڈ9GGwӋׯڽm4I{͸+xa9ǎw,1STyt"'FVY.ݙ&7u!Zy#6I(i|ƚpR:tvԥ[bȹq{yjgn[T7+~hEщL[>ۉTx%7ttMWOռ㱞)<[DEC[H:t*P@qF3|dhj ,Q~ p@[zMmmfSU_H1(IRlWPA~*ka&P^m-=jEy/|c. BơtMy(a(_o%07F_sx0ZQ6]FU"{%ΘNVY&Cg:$aROfPSlg[Ez&V0~|YCVUti?^} 93ZĪ@ndQ,{\#{Ie=ˡF9bd|dilHeJ%RPcPZ$oEQ- &47 k+ NC>Cg.pxv⫼R-re)QM3յLL!^'p-8Ir6$DyKv~Cfq8@(?(F'? Fyja4Fڽ-)= u jttԙ N,Z{{+=ݙ[ӕlPkz# ^jՠ ["޲k '\ؽ-6*KJ`;CRוvP3eX< AG?#C,frob`#_( >2{|񽗸臮 +8 tpXąu/_>ֳg{<X!i/^$/=7è솤2dn"ljTqZ2T&=LVn?2KWS{tQj-ua%yCx@4; bkn!W#YCܵVđW3ajvvէ+J!hw("$/_//>}9;N;ҝg9 ) &j&ɜ$e}JsZ[C>壝dm*^Nڸ(z|+'sᢷSyv$aAeD4xW9ZWbww85z?e$c>'[Bp% [_~B%&Wqk>pSR;CZJiڣ5<}B`F;lLR Is ̆Rw<8\nzn߭d#n33Pq{w) 7 xK>{$Ds A3Q6|Ģ]=(ϯt@Gg' +)yg+֞F7m<NR^C0+|?z\dMYoFBb~h%6_Z=z(T\ ڣmcDcg>z >30da1+j=>"@&ųGǢ> /?td Q#gs%lȞ1r d'ONב'Bh4k="Sn3Heݮ4Mi8jUPjpP*2%/.\JSF@]E'-bi`p7͋8DE 7A?ǓMMT||Ԑ.U`0~Bé8d +ZGW͘](iyn ei/mw_UEF Û Qvc*os v +$IF ': +.Þx,] AO0pgYg9`_9qJ е5̎-\9z'IỔ+n5UQ0rffO):32h M!b"7nPq-)G]S#;|5x7*{lM7׺J(n!ybF/Q"/&E ,<^73C28^_ϭAG\5udJ@{ߒd2lvڢQ!'ǀG'oll`KN]^ǛV4yWoCh"f'+P$y:.l[Kd>9^5 ,‚ 0a!sv"*2H=2sbDuAY {9X3ËN.^9?0 2:{7N~#5v9#'Qp7G E﷟\?^Oh:{!xnh^E[GKKe=RɌHG5 }%&ʦeB] 40;q$$kqJjeOѲ>]&;x+:fOYonɺ +d#Ge G: +4;0 *#[&ޓƱ&xone?"F97.z;dE37C=KAMyμN&ɧʹ; =rT4s;OD|ܿ⹮y_yثN~_{>vQ:ȯ5KfǑ<=>>[5M?%Wŏ1t fv]18;54:͏ Qԯ'+[3BQNȜ+!;19J]n9fp/Ai'K(Y7Rz[A,FiFTE'<Sk L y! 9Ϋ^߂w=BǞNoȷ9V=.鸏pWH|bp6Gl}=;{7fLJ) 3co{E!m)y_ܓ!P̐&mrGld*G{Ӣ)`htH^rRObh$؝Ui|1e``Sp4TM!ΌC*w~CP49aD_-)wY{"f5 +lFݱ8%h2AQD̵A[Z7s*l@OZ [?8-+9n1p*_ixzfڑ-9P_"Vʳ41 +~W:Edh:fTSUۮ5\<#Xd=ލO3 ,zP +%A|(:fM%9ߦw!Z6M1@>+kTObnFP8*gm-TF#!pG3BAfT1O:ގmCkX 9ߜqvB{OTjS،xTo-T闇8o¬P2"/fĻz!)l g/{lh* N'.rk~ 85B]Eߥ\HPP|_,DZ;` +)淥eIf+g l5 k*B\TNң -Wi_>G MԐ,Ix\Be-Տ vË`ۮ iP1z>yw<x*+{EQqo͙{*_S8:=XS1! q^P""pּ,΀X׻Cwk@Gn1i Pҳ!?K`8帥uJ+wT?1TʹmfBG+; 1+HQ>*0U.ŃbhV5W !<y_k̽ s~3"nx}3R[TmNHGLݖ$xb8ғT~{DG[~Vgv &7;ؚEJ3;rF{#tkGùׅ[3k@ c̅;&iEbԐy5Զj ÿyWoI"Wgy1A;aOA~=F?CQJ%Enם@D5$ɜΫKC{ɀե O`OsHf>|k=TA}I?a fsvZ=cR^}s2`FeC-ꉼlӍ.X_3Oc-ԥbxTNQ|۫NRp6JG݊RUZ\9U4 +a6s%y8L$*- +Ύ΃ZKXw5aK2119!gCgzc|9,>; +Үxh+@W ]dEnXA`nV#ӉJ<rfA};3xؼ$B6,@dVZi2^U{4tA6g ;i_WCċ;uySU6+ -GI#Cno-doŭG2RVKx&@wcn$PE,`?'Bsg&) jc~F+ x'vsf5wG +-:I^Z{?̟c򡆸3+w5-}%5K9 +j#6`=jrĢpײԂ$<Lݣ*"_g[:" "{=!&G +jQJq)7Nd&Sz/[J<H((1֩yeOUl[k12W Ng@0GZYiH%_ODX*Q!FܸY(MlmK\˚|fNRi tlב h:zombeI%i3x>;LުFV#ް+%!cz`$><1Lje-e;̳f,!+a_ v] +"Zjh)\$|]FĐ56vhE^Q!F)/;;z H>AA5*A]=L QINi`0_(eNgn#A$i ՃS! ?/;ng< +TZ)>Ǧ~1AN2hziRY&ѷ>׾ ǺwK eu7{Ү qƛ9փ#ȓ2Ex?AyyUfʭ_hxcQ!P g#fhr2پ1*9ІAɐ^crfLU.B!BV>э~/H$[GĈZձA[۳fP-ksFic/$g/{/|d+n`£][f WWD8^*9ɶQgEL$,) u fHOH$BIR}&Lw?uDY +W+!&mzڧp@tAͷ"e*$} S3v+LʖG\hB8Ohs5d>j|Р_PU |#d&E׾Ewb:c.p~-f9MO+<ܭ17)7.{f&6NvOƇ4(J#䅨(GDJRҰ dGvN9>_Tz傖gDtcO;R8l0r= R6$fP*:ѕuWz{NLYj6ޟ[eH@k)n[:mW&"# r0?FV'N3@1r 5xe pE6aͨ <>VmnjmjBiĀ pxg!;ƌX3W@b~JHkbs(8bSL= +;Y.yB/UrGYM 1耑 ):MQڏ@-zOx9̗ m3 +vdȁ`ta:3TM:p;ۈf1³@0#M OS<#BkQ=Ţ:-&eJV\Qd^=9 J`U{xDj{{٠:VxQ uF@F +GEXz5p{[ kuDR-@(+cTI(oֻ`JQ\NBsNU^VfV$t~܋ W|4zNnu {v{a +>74joKdvd6ёM0&GȒ>LO&\l}<VΨĔ)ӹ&=uF]~]6@c#oA)YӉ~$KN?֣aѼesݦCDJy9`k!"JQDГCNA` 1ִuM;@ywo-噖R}>ο_~z]XQat{"W!tQCq85A,Řs'7Id7;p?]d<,̉}qD!ָ_棝1*ߡ*x}gAVZdƐ̖r&P4|B/a=zdhW^AH?`& 2YDvO/+ag!#HGmXt?I@!&֋c,X؆Qx:Hb^- +[Q}(U;FHG"&g: ݭQ {qKSWR#<2P\%SLZĚ4'/Ϗc\s l+j'pIXX izꁯ~ :N(O=J%N҃} f]ĹLy]Uh{ E`6WFD׊%qV>De0WF{%v6ÓK|'Ǟ(^Gd,1*w}/bW8[ſ#|0TqʇEGMK^=_td f4nGg:5I xnqs$ˁ+y%7E!NYXJ^#LOױ+͟3FVE[aGK!o2=8#0hSЊq2 h_<[4l`\e^IF1cCt f,a14X4xCdl Aر$"4+mאomDRU*:DX\g6ɿ_ Vz]ԥ8F9)ъf4f⸣Ǝ~n&N+ /q#TW\ Ij +dt2lH\2 ܨG QVBpSLK-чiGPU5/bOZD0}<k5B`Ңi+ |?ԩrQC'64/۝  %I'f&1#S;%*rsݨnh2Xݾv]#g3ukƇ0?fyU ?gn4{ riJ+;,@kz{Xߙ6GFOO^ +Q +b+*o:i @;U傸yeHΌ&c[)(٢(ѱzL9-iw;War땰&f>P;{(|%JOx\?SWeq8SC"LٵSiv|E$s3[{ֽ۫RXdUo J>㕧>Hy*XA`jZ∧vlH`Ԇ7gBn\.__Ͼk$#꨽#Zk\9"dRߠ9_?t/yMԃ|o)v2$Fbh:4q4{C֥wʐx +}0|o ߊNAs^UOD.]oG\srhXss4's)d]Rjء4;&Uf[ZL'0}GG-]"{LNbZ >?ǺX35Jg=l cg39 ؞EO<тmrpv=xD01M ).>Dr͕0(KL+'F^Y<_Y9(C*.8Ӟ]fs1Mƣ;5#?*'wgu6먞͉C:0x92}NCR\lƕ \xb +ww,7tiњK+J"q"ݳcޤB) f|FJpsŭg^@#Xz7\`DL&aK]cRFnyh ӎ%rCEg~`.K7PKw[X 1D@滿@>QK+azL\2U+ hY~)A 5+Wd!vx@Rл̳Hږ*QyU 0-@w%$ϟ=XwBuKrĜpo |@@tB_r闤M\J!.j0*傚b3ފ*OIݶ%&dMogX+EPԦZ0gl:J槄2QO &KqK][l 3HdO#@AyNj jXIH .Xw;ܙ 8{ +GݲA:EH%FnEZo1:2"p~cɢ \?fI^dmiݴ+2T>s|0+F +{?5ELTUb&7 QIl|)º +QˀZO :"xǸ%®i` JBSۄ%~E.?*S};4g +^*w# +DNq|\ s[CEb+u% ګ_$ѡ9CЫ-Q ra{ZLr09HH.Zwq>=YOI'np.]=MIYn{T=`ڵ48 w~?S쎟.\2bEP Ag@kaPop{"fvf{!3+Fz "I'YTO5I2ZK'a{q_|!A69I_!'E{K ,[WzUy*9<o}w]o`ɕVQ..=EV"c{dO@UH9#}>d/ a +Fhk?k+Iv[]=}^-֒:`3Ń#t*;=!Pk=WehJx^=÷X0>@vAmwlB$ȌCH<߁'\?vԌuyYBGC~jD̈ҽkPX>6D6m캚O J'丯`0K~$CG:j<[P~u3 לm{7"̀Iy#T?.b Z_~\lfoPp6ʕZzgOXT-mCq +mC\Q7((^ nd<7k1CwΜ;[$`p,q3X*\dn{??@=OMV^M*s}?֐P&H>L簱gUykzK|!ӽy D_kñ+\HCrmXR{˷d)I,.ƱY{ڽ(+[;Kj([ fcowK =K~7!$lik<Յx`kEK!%jE&Ʈce2xeSVH)eY#bm 8䩾驌UoіAEIJ9bW6UqU=g4+ vXB?%֓$0;;=`";']D_h`bb9&;wL+QyG| OզjMk X=I;3,\\Smi>= f|PNamĄ fA}E3ٟLMvܖC& +Qk.z߳<`j<-0pk,%(ߢ$], ڹzi!RݳxmEU[$(uA^2F `϶ՐۣÚ+Eh66z*fj:Pi[QxU -IE`X{5\,sLΔ@/#I[)]SY$IpMWCHMaOen<;PJ<ܰ9c6Ȃ6.LX <=.i#lf\+}dzh oPXITXԐb#& +N ٍ>>y[-9/uw\*iZ=X{rٵRj֐ DRC黈DPʘ{&Sր!=r(+%>K"Ԣ87B)-1@"N{`=sIOie=^SY֞63CRy#s DTO#qw;I{F~g1Mc ( Cnyϼcfm/p2G+ĂTgkD=tJh<.znb'>&M7"gV*Ck5X +kWR +>$fYx/2/!Ş@%9dpN Rx ,[f8K_|;4i7 -688PǁA6 XIh3U aFYt]FD_FHI8r3hb@my'+Au&5So+o +bksiq-g=? ^E-a8U9t W.2\9lކT{ekd[]TW)")mb=FjjjrGH $ %ӄzhQت(l賌ۘYѿH?z(8$Zt~`dTִ{x4|\nj0盏&y8ZߵBfSEܘ?4Nײ,w%@%gOP;^-[y<؈B]&b2cyI vN6[22&2\ Jб7Dyj-P:9e_I]t\^{ɶ/f u!\kgs1RetB^!W{^am)?,τ1v !ɛ=5籼*cڤճXMQy Z"-BL3 EY<}K: ྖj̳ǠjJM n@=TcB?pΪu/FloM5&vcAֲuRBk9#T܎D!FK=1}x^|1oQd4-XiQ_P^CEnPhk(F[5g~g""i FDꢾ͈t= ~q`]&#Pԙ_@ lIi*ZIgJ{ Ri$hOiRg;%:!Ӟ :mN#N t~g?Swt;BgE@ݞpR?;XWpGqVSTcPK*w5h6f9 +WB,:Hv@cmK5O?]\ulcd*vQ'6u:gJ'K̖ !a|Y _Y4z +d:Pk@1Ӛg +{1ݩڧhSB_wBcPK3( Gʵ +ZszWrrOدH !ey%?Wt+i&߶/Ƙ +D@G-CP^UK\!i,ڍJ?"Vtn.BFC^z%6$(u7=L{b+KȻfFO8J+-v5 -:B{-{+AnD0BN#W]߿nt?FYf)N e&6@kZsplLݷ+i#wPC F 6_ۨc6]Efi!Jݕ"B{dCp"AFA%' A>syޡ +뎘<_V)rsyvǂݟ !klH 8YLR'UHx'VYlՐ+L6} XĹ@`A +jXp+ +#Uy1)ceN?C]̏|Cd2LOQ 4m({CF"窾ܢooO}M +pZ0J[+bѱ9K2f"w+}z! u;"`o' 0;H|排FItNF^rd?E(Qfeî5ڏ;"`!5}򁰀BXYVA +c}E$A(!42VxprS8-']N"ν!4QI.Ek!G zl$|FS \8]z_ѣ:_CBv<]}T'v{<,%Mx+G  rj2=#t!yq`09⼖NbG:cѓlol%X, Oy,YY6|it#&j fѣd I,']p ȸ'tE ^r %?42FqfzħJ]sxP97 _QDY|L߯7zynKaK~֣󐽸:]㕺`3+\W`]5_}w¶2H8!ksiIt1dai 5N|I-CvC|+:?v)X\wgҎB#E] r#Q%hGαzTu^ATsRur/@[Ί}}DXbnDs\=JN a5={`uW:܃憵ʩrp=w, +#OȢwZ1: *PBT2tvw/c׮e!?Yl>%6C|?I7Z-Ҷ pHAxÊ^~8z)_Hu7L0Ȏ|#\k3HTv%+skOuͩ[TumІ]F+djgQwwi셜Y%Qpؐs6 6d9$yEj "s00D :CO% _.`]iqqĔᤣ$:Ѭ=ʖ'(1-:g^0\šQ\ 9eвiCwQ˿OҜkJ7gɼ+}֥ㄡzqF+!b@ceua?;f0QtCc&TP:z ='Ɯ_Ad9vs9w]{^!zȡx;o {Z6dNϞ(ʹCpfxE`+ YH=ͽ=Cp)e2+ܿ[lZz-HkdN9-3PǜWr)d(My=9mҖ]v^읺6y +Bx2ڗ!ToGXeⰇRۊyuvQ{VT੎V4=+ћG?4/)]%IF+ +.k?r' uCs)t_' g;{5]GvW虝R4 +_&;A20|BZF6~fFNm垉02bO>CrxׄzJSҸ惡 E6z-ZgM(稇" QޯP=lBy_MN Ŗz'2 nk5֮h9ӞÂ"بi}b0Pz*vgh +8hg& ryau+*λ:*vEfRw9c9ulQJxR)FxD:H̥=,#fخ-Ay/SIujqJ0^TT?n-|[UuSh#()#,x@5/gPgch&[خ]{C4Xၓxƿ}d:1äUoJey%|U=v(E|-^VU7FStd%R{&7*k\+Go}~;KaZ\i CՓ8F%YS#6(@`S4eo'B3 .X@2;wX} e!@M}!=Dg -}匓m7;lH=J- K>4 +S9<!bG<,Y| 8Uc(h+GO!i~$ݎ]}9ѹ̺)&kv n2i52~Hڕb*`^A$ [$绦6YE#NLo2^ ؅ B)U!륱EC";Uk쯙#K붻)7 +gvLk)te9n= !c칗vݽ'/.ŀ^6QC=]M5D(E5|߯@\{<NWdFyN<`"8q;Vﱯ@UD{FBhfY8('ywsˇFRL:|̾US/ޚV|).y\@f!tF y%ReL{Pmp ˜#<Վx|mNys^'fwOz -/{QuL;Ñ6#>M;B9LdIx}eoT}{֑Y;߿@duW7g)gZj +N:Hp G٨r7E ~ Vi]stTf7h؝>ݕgw=TLkes)yp:"Q-mHhRDy5NJbwb‰xTh%lcFԋ[Tė"c/N`iiZ[(m[E -fXz><+^Ks<,F i̐})8" g{|##'r3+jgT~PWuFzAul'O(Ź;4X?QYqD+upN9.x Qaϱ[Fp+}tc+tmp3iՖcJYx';w%6¨Ƕx/k#^@s ZFaa`hDcrg3kJX{ik@֪) {WX]1|;{rZmeېِFTR9x_rUǾ[3^x>r$^?Ri5r9` c=8L)F~wgxc rw#TÍe~{~EVO'6dGcͯ ZvD 9qj:JW(#}D'z1Le.L|؜1-4m^Tpy9` 9[s:#{'Z';ԈPcQdrxݎ I+ K5e({hʚv`JQg ͪ"9 +ժ\puMc#=C{5rd7#Ԋ9_o̽&@ҮҖ ~SMx5$l'%pe +1'501i[ S&Ы"yg۝sI7āJJn ]K p2 \ 6 ]N|W *\Pxx}V4۫S裧BaΡA5rd(ZG,z4&YF+LG'pݯ!fQj-xW}cgNjW>c=X\*,8h$߃H˨JCNYb>5`Im/cf_B#{YJ +3nWA4/>su+)w!NEFOljE>S!悴nUꪫ*|/C5|@e# oy8#5sۈ6|;#`Q"PsAjhёm&ϝ"nj'R%vG^4($G|f#b^gm$QmpKfGUv2Ny;^)L8Q>x+}Aw<viL=),ǻM->`2hnA]`KMt՝ ڳQh[Kv>ӨulU&/g:JGֶkү vvK|^>׺8ú %+XiDP::Zz+(YB}tl(a\f{YOT,` Y*yh+z b7jU 5s!D' j9tWqsfeQ]F1AZ=/'zZM&괅b ~.fjʃ^X1D5kRT\:/wж}q]nI!s)Z[4hR3k?M~@" R:`<@1b#O Y@+"[nF%N>@5({w׈G踏u5 ? SI5cUX; t̀e=܍@8ps'#j6Kx},0_#f)8f/1MX&١g">B dk=jHrr%a-.-\͋GBٷo WtmjIq9/J>徃ۢ* ZQ֟z%qЊ+T8pbBbkWGJ۾1f$cQڗCΜ,/ԠB =^z_H^[ ;unowFj{|?Fe8t{] 'ifB˕v~]{]'8U]T#*f[Z,~X3BAc'j,Py{UHx{cچ%V>Գ`݉U\iX\"ƒ~NY<Ȇ[8QyywGq ~RZzd߼q8Nv>8Mп4 + wSIo{k}eoxې*QMYْM]sDKY8C?=SnN./S/婃//pKcey)3f\w??)ۣ:JOW^ZCSa7o ,Pya Z?ElijEApd+ 341TXyKzs s;" 9a µ'-Q8R o㍏|ޮv(hu]_Р}u?[ߘH~q7a-( YJv@Qn\"i$?xSv;4&ē#4?1XC#Q@ApOMs%Qڳc[C(S̏bGK愃 nHua~\kѡk~1Yϵ=!Ğ "[D8FڐmwH&"nW{:GZ& I_eˆ;F>|"ܝC?ex._^ ?Ar{ +fOU1hh2| +ӶWdž&tgu];:ȒFA{cդkifO>xVJIcabY@:72 +f;37K,S o&j+?pwk?Va3u@3WۛpfNRar]v d{DQkAJ5S:W h{qjųLzAҢ@fD2:iComMmH[HP>V* |*~V밂dw7F(hI-gC,{]<[P!{+rH9(^<ȗJmK+ H,?P~q08D% +hk>T*k(gmMxA ?(/ Vv53g/ϳiM;a%vb`"jHCC~Frya#pY.2lZi|.nJ4E {ieAsG쑸bM3Ajy#/ 1֫mJ_Af R]F Tubgrb9sXU*X^rߎc#?>ogwPsw52)S̓Zڋ}E%쩚`L4[b-A :c{G6NjgMe驮,G&Υ 8'+($g :CޠCu'P2OJЮ/H|AZ俈4&5 +LzŇ&qGb"TQGlhmIGG]NyY#7\sQ~"n>n ѓjHQƐ͊HP/B4Qqh"t +Z|,/{6?܁*X0m k/)d[<%' +^B=JAXsUB.SKg+Kg䨏b,ICw{|$xVB硔U~`[3t3I7íW1[6`L_񄾮ո +kν b&3f9nI̧MHTzDa7Pǫ):Q[;.{igpoM1sciϩx&2Ì4@-0-G3p@lYybS3hs}O>!|M4dj%6͆_̼<#t8g}u_((4Z'P:d{QHYKpKeYt =RܣKc_AO^6/[gS=MPI}|;+&ak_g2ͯR eU+ޭN^M +72fgިᘓXWjuaՙIe-omcyxH#$ơ6c>|BՔT5^]E1 +$Jj5H#U4J2$|/6,$*h(gH;|jq6Kusy",xQ8Ql2$9 ,H͟9HTʞ:˕Do|HG;q/SCx֡c.&H;K)}+ӯ9R+P"~z]*<-@32JzJ+CZ'M`j_ܜw_^-`W1wNa>3_xJ{DGP%>3vڪVHjjv5>W,.]#O#ٶ2e-Zj^'q1IMx{ EE) ѥ<"Aa/z?A#b;d:W'lcJG{ru[[o[R{"=.^o/5N>5MXF|ejZ(=E& @Ĝpxzj!Kx]]uݵ"SNNI/mPLEwX1/һ +d2Н&SH-CzB)x} 3wf3S;-1Sװ ɾ*yTn]~Èy]ɓ-_ٽYtWLGg[QMEDžù%bY+2fKdwV_%/u'JR>{>,Eox5x2wIZF _-=FJHHH_S8Yv!KU1vJC#@1ǐ]3st`YϼuBV[3CW^[-J+TB.&qGk5  3[4b2[B(~G.:Sίww# )B7 N-F}Kv{}ufWUQق- ɮm$/=VM"OWUZDS +#m\uފmj_8 _3[!g̣wxh5:^{e @-FӄSjDf#ȫzndHl B>ԔLmR 0 +>jCnnk*]0"g`͌X8Rg ؑ!BEFI;53V[Du}DxD)2Vq TWKWP.@x..OmNbkX@gP sm[C|`Buo ϰʫw  +cmjKv斕Gsl6νfCqMӵvr :{Wd m 5n+j6;'+#ܢRR0[Zr˭`z4;ޭKw)zW>w7iOEaQ*{H;viPM +Gc/|VtN:޶EGMx#CQ6 ʳ{# +BK r.uFPYaT'H;վP/ +!ĝ>Fa?FB,duy~rnR7\::E +q_[~k'Ϧ0?xvUU~("U7w ]ҭ}lg]#6a{=E [ tvn)މJ`r"{m57lu%Tk }1ybrעq'b qD{^Ȟ.`|%Cp;0坮!dW5B'@!}4?2^IL t&V&Le~z` ; p(Ril~MO/D2t{g͎@x#\{!ۊ +Mk6A`{Qӹd8VFS xx<b9o@Iߺ.^EQۓ!ĎwO9b mPG3Fy+8~ю`|OW%|OUH<‹D]Xk[/7?Y&5}! +)UíHI*rhF9?/~^}1f+4qA, J̯cEmSt* rG|]ТU:Swx7<p`xxX%)8-Q _dEGH33)0)!#uN' &-k'B"gʖn@iBSN3Q"jmLE a{/.PyP? {D<] r}^ceaGI sEBdJ>'b5 +SΞ2@2]vXgsӪGjwlN$C90Y6ޒMk;%a&U : :7(3vz$\?{d[T䎧r~Yl" 7jzs̏AY@wQjUQA66$Γ sTBOϪ٣D En۱11~XO$'5B45Jo)^tKlFī zQ>\#HC\ɢXn{_#Lf|̐ ?{PtV#=wG~6j+3'`‘]E*|$JUCKl<3%҅UU*3/c_V~+DEyqDq~T(geVȕfVy]!a5klz-$~U5W#HunĿ| +h"oqT?4c +efp1G2Ծ)1;Z]#2|ʔ0sK]FvĂN& Qg3VOt{ֽapuwYa fA j#}&wɦy ]Q +>щ#{7Wv]@gqW1 &gpULK9m)KsnA~ˆ~O2#wkJF]@/x˓O[`CpġPHiѾxjgy.{[#V}!M|"&8'8W]Y&tKɁ- ӥax1l i[Ӷ |a vyAX-##G`=E'4|"N9f~膏Ngj+]A.˦;D[T/BQ56"3RoW]Y)&4֢i9c6t?WtV;2$3F%o +/k{emXw9I48iaηlܾi ))LC:q-ÁV~W4;OZ,S2d4D9嫕+ڝVJiT(Uh 9ӷXcN@ٰSW[t4 Fwq}ж& $xj5^}GCeM q<+RDeV A<!Y6 +lT.PI~lo=*g5𺃐Ho߯lTҕX|Bh0c:/M4U>AcɠVX豅=] +V@s?5x˅YjRA(dKUgWKl] +% ma/b.w {tB?*U.\J{~3с M|@}LpFրOU@&jRzlgBmEPT47gK,egR0:,.oD E YZάKA,V߱QAҺT1$j2[3}v98iy'UL^ 9N pTF >zS4uxl\D;QKfUJfcw7jO~GҊY (L ܷPըw$t(V4ۿMƁ +zR2_sO˄@)'Y+-E oܯ0FYr0S1V~:Rio~'S&rl6{﹘xWs_*{S.$o=og_[bW1wuS *);]2uUJ㸧@E&f:LҐԲ%e +ᬄ`tlO] 抔HVWf!{`0K#J&{d# :C6 &P=Qx6h]/e75Sf<bQpQ?ZM q:_=$;:Z: NJ,!W"VC]I#)SC 5~egЊޞ)!MQKxx!r/#gŽkx2ӈ#Xjv˟14RwNںc`+b-^slsjo!K磦H&iTUQĮ^6]Xc2U%V*8iWM%b_o)"p7ь(5nC:I8czRUNI ]q3-ڙZ o3Q:H T%_s!X)' sP޳F2 +72(#Z*!Bw 4x#Rԣ5{^}-d8G`ћ R|YpO2J_z-sLMu0AJOQU\Np Ea*&ES_4J8Mb(Јpg]j99bG6cQƙKvȌVBq{yw.&hwAYl/#oȩ"I^ωwE0JEDz#mRaŞ?'D8 9wU[I׊iumֺ{Z5y[8 s>j:IBKh7<⻐SR(!L18%G1#yEeߙF~>7u{?[(Нs4E*@ +/ƟERj?m,tX-t( +}dWz]4Xs>41j`Զ`#Q* lܞq$ +">LN6)0/87 +8pɔkN$pRs}(Vc#\_swujž(9|/{Cͧ:'KY<'\ۀuKT8uߡ"5D= Nk|MTy;4Ah'en~4le5u\Bm~(#* K 6 ٓy%$whUp{o;Ch۠x NV0_,|@SƂCO"I6$78\gfƎ@<\gL+gRq42U#R18QS%E9ZF/ -i;~#=Av +T2̂Ñ!Lf:74%e^G AZ]TrcNlDZ@@s /9^W>n7U_eGV*渇]ET{ ]S8Yſ8d1$ZP~lj1kՀ5NWd=>irwR3}H2G^]%3D5 ;-^ 2j]PNw3A:'tj@M3!H%7ޓ<3byxCA?0a/M!axԆ"*z-MG{A[Bb𽛰 +l2+>guh=;\OvL$ęX%a N$eʴI{Idm-7^g^DwɗA1c.[Fk^3pN;l`+d:(Wi{307RYI=x\Wk?zP. +sr);$r78 +3c^ad2yj~B朊(c&;dorZ3|z&iNYY6Z=ˡ$nj%/ ֗e 9L +(k5B"V rZ2HbC< urlT6Fkn_9CTde#n|YW̽m^>!JspIXH\~֥h !Te8.P;2@9s,O׶Z/Qg ̵3gTC{gTSV|(N7^b|INЪ8w u +8֬D,V:.ZMdSk]6Ø:ܩ R!snLΕ+hK tv +|rz؜NHYaA"]!DxR_)+Nd/+$@g@w7M"&yVS+%w @f&GD\d&J ͐0F:oP'Gvv֮s;2{jpXwh@޴74\ F +W!2l8@+!A1cf7r$}EG-õK®F 1*0qNvsT(:>Zܠìx8 1zb,|bD1K*6oU'A+16 +|N#:Ch;Ȳږc9j#|vE,O/oEXcLw~jALSC&;$맼bgo؀^_ 4Y}kWh]u+ǵ1 GE#Йxr<>.-0}G 6^mAlݭ;"**czRrRxJT|4TirX; Gk]/d~#Rc2`=Ax;#}=:%r\.dڴyVz+9=Bd63nǎ;,sbW-T_XawXoc[+=Fng*ḱwb~LlPUc7,3S]ClCngȝ.$R޽Xߓn>iO `KLW$i)gVvQ[!eQSk>K 2;μ5$bO @YP.C`׎R.#fѱN>ȤW$N7<ڽq]<1kC&CBt:Y77D>k21_!|NL`k NCm^v >`=!u4ۭt?$GM;ɀGGI6D2/gHȴ$ҴuŹ+B +7Do=-7gވb#%=-|C =7?h1Cbq)jwb|kF+%kެ"ɺ3$fװ_K6v٣D'O0_b@I[,r2Z:|HC9eH9M4Al&B[o}V|\"+.scZ@Z +F !Aא^évU~Mbl|=  ~JlCxԕ0DٟB ?yta4(pfєT|걕iEd8GJ %W +ۥdFmQ + ` ^AUx{]P{K`%Ac#:G0$Y hf_kɪ;@6,#iGd@~Yg9("NW][ +CR{P zP$`|}tC|7@6>{LØTFaΈBQWrN0׻{jȚqŮB:uH>ބb̴X P =Ũ˔e>f.늏Z% &vPΠ=Tok 3Fo)c>q3?}Nm]oUܣ bZ#TE)ikiG?]>Qv}GF=_Sv).uz@5 moHr6`?~ݻlm>5 +Le!i9TznUY[wkZ~5*atF&mgnzBIPSk[8T\#i}+߿B|l??˟9hƟ1 RAҁ]Cԭ9#GTw%ov7}@~^3:2sB\O5t]g4ίHNvF+ƩvuL޺E;t켏uzPA焴#G]&݊l5F$o4wb^E7F_*Qd?\Ѓܾ8"T__έ1$8x07U'_Q=GW>}N~bYS>b~z&.6 =s;^UEI _i4 +-P;2o9tȬ,ۏLE +bi+v/ +0nkWDX)oe/o^I-I@ ;J#qf[0)m,űwߙ\1y}g=!o+ 4{@-HuXbb()*5`5a"\ʝL\\C޾>[Y4nب]YSj'1$G#>z&dG,=LKL8 SRWEvFϨnN߯pD ^XK$QOIΒ XW v=,xA'`t/M&R{E=#OX#w KWbDkV֠"[z>,kPuB[z : A޿7.j Vy?YGL{%p]GTDT\vV!@{c#pm`kNkHes!CۻGh7`{ +w0374V\I[g6ѵc{h7}]K?H$ZPg/0oC1J7̔q߃h樛|/Ϸ1p=˔ %rVńuʯD(U=εIWBWOޚ BȼxG [RM0 +q!zgeUdyT!%{)S~fBKh<ēL ̊XxzhfZ|4i\ 9[$FO :D;h|D M{"x>a$\@GD{`'u;D pG GlR RƋ!svZ2=ٔ:`/T>f7U]Rzf Ά504y7ZwwClOI$Dh]=J 8A&Q혟Y}f6`&c,+ +E"Ҋy|`\HJNzqOHIBP + ;khwL"^1c0D(A tӮ +rphnnI@ endstream endobj 37 0 obj <>stream +%AI12_CompressedDataxr%Ǖ&w~ݺ#4mcBjITUҠKL ;XO?w{,q0eOnd?}o_w?ͻ7'/_ _L(t,㛷wY%o}~}#wo7.DW7k̙ug&oC}۷g^}}q73ůuhMl)ƺa 2>x?ь]?x7Ϗo^޿oov97u|ﻋ7x~(c/x~sn^b<__|P矄g׷/W7vqn?1ۗ/_'kgҩ?Eo1';5wˁCIǯoe`.׹7_ݼ+ňۡ]__F2C=%,eIqG6Z͟gQͻ/txw7G>u$p_H27o>Òl|#o\0RI.0Pw;4W~kaᄐ'oR*{3bl?qehmJcc~,:h{Sܬo0i?S;w/er|lK%eDgXŐlvżt ?=&?ۼ'h`؋ǟ(bC?".V_++.$q\&j/ּw_wx߿;E]/1}o6=;/_ޜnw<;Ђ٧Xw-r#<}㿟ey(ovgG}x8zjE۩̝Tߝk{-{({?צעhw~*믏o^s[s೿oƫyQz#8{~[oޝ8؝i߽8~uv~8˛tӣ8^_޽ =ޝz_?r7/:ۻ#uu9~;;7W7/W wm3)'Wt$p޾f߾y~<;8ϋ-s8{&_?1RiC딿n@AM}FgͽTvnKξEp޿_n(oD=,~?וqwСD\L>BjO~%%O3lNīoqlJ!tKξy͛^޼}׳7x 9/]=`bn|gԩ˗64Ey/}}|rWUkgzm576dj۝aV"߼+w.hgR +ԃw>?:3 w7/_~Jo_vߑ4a<]gp3c:y=bή/Zǝ= .|ŷrٙy\3,-|_w"J|=~O~AvL O^of._?X={M]i~~vI%h[)iߎ.o@@2x7/_NuoOYI|R}z}i ԟk|ߚ2O7ǹ +u\Ft}whOs~?/v7+ETRpw_"G ,w8Ѷz_@>޾}5Og<<~-8Wvu +dpzoǗKR=a,^ݼ:{ s,x|/Mog +y5?/y}^_L5'o~Qen;~pvsvxHxv]c."EW|.$]W9뮿lYErW) E;%-ҰH#yX)鯫zC'+?nHlZqBSlϥV_&gq=w 5bn5LT3,F7v2 +hLrlz9`/1 8&b80F6 +sqCe|1qX:=9qxSz-qzS*Z2L< R,WJrz;oY(9;r~}~Y0\/H.c9K )r'P/gE6!^=,QL|tE9srܕ'e4A &3abΗ.l%ϦUZldO(@)xJ\zp\[,YB+L~aQN.nSgJw5g.Y +(mfj +孑4Md BÄ oP(ΒKe;K#GxcHڍ{3^LoIKQ&R\\䋴_ʏ2+uͿ,DԦ[[ v+b"PT'.$JLv#n%m%tra1_vY+"i<&[ iOZlR7ZKI(>1m Fla> +˹hU.W9)Ueϲ\ zYQW?nҘӼIgЬ5¹=3yM2,Ti؎eC^u bG^L YoImϡڡÔ(mَ;Yi՝n{k/z!˧AVG +/2fN$ b#Dn1K{v~'CaWe H@D +A*9@" t)( DIRЅAABF6ǵHW ,DiȪ4$U.DR(B""2]E(X9D# +GA#/‘ JBK!#QJJ*BDdD5sqJs!4Ql*E''56A6e$uȹwK¿Ew1g*~5QKrb(uu mV9hR% +P'4v6sŹHdQO"kХcdYZԴTS-jq ERViv U*/UjadLu&WIWMjTX<+gVo&&MD'ӤV$H:*Vxbޭ4n$>9u'>>D?(nb0(͔S~9ehUІy۔(a '_]01)]l(t "M~-Pw>΂\4yƬJ&,݃4eE?%![ysӁhP/;WMYɺ:1L<9x.F _\QAVj!B[Hl!JV3E&R:B:g9ʙFδqI.hoq-ۚaT[Xmp>-=Z4|)*S=D5%OX?^2/K3< +2N&̇dkə. +# Vd7A/&/=rQxs"dO0d?"W_Ej+Vp \m~j+u:=qQqozfuQ'uɎ8i>d_ˠ^{MEOt-Z'>zCߓrfJarpDUᇂJAP)P)%K,^kXu.qW ֔ԻrXOjTw +7 A(f!QTS:":sTlU]Ux2_)NdAd"^$+j{S˲pWg]YbX]ihk Bh56d)؎ܨ76 XF!<ẓns\,,VYNZc\`W3$D=Pa@:a`T&$#p?*r 5* +2qVN h>ZcnRd- +JI6~0G#yp}zHo> d~78ޫGIEf&Ľ苦^T%^O^\-A܀gA(ڼޮ&zHĸ_j-~ܯ|'Wo_DKZ )K`Z%2q +s ra[K-pG圣EҴ,~vvyp'P$)U)FŐWx}+N”ӐӘS>'oM1nJaJO8)]̩felmp$c%?JPq#wI'iuۮYWݒ,n?DZJKKka㇢4h9[+eHtY㶚- _5")A+.ňۋ:@b^ރZ]NW ]q{#9l?̟-GMםXmՇ]svo\gK6h70/D!&ҫcvn@0w>,~~~N6}_ڣii\?ҁir]RypJC [ u>Zx-Y蔻K|4 +OqG:햴x$ .Aճk.E)ND. 73)!s<:z!LtU,(,Y^֓` z[ӕ$enC^kz-RB(MR5ngẈv +BB>Lg +ʇqJÔk>Ô,]Hf)/d9-eYn!^EΫ3tBNie4AkwNru!w8^"Y* _M*ϥ\):&6YŊ]oj)OܬCd_HPPUpm&,B֏1t9*EX PT粒QiU T4י/1+Y{z.kXĮDѥjưX!jNb09LA-WY.}yֹeK̒DRD)*CO1^\)jyf@O.ɯui OEm]!!G8VV +E"Xp9Ɂ!{/o<&}F#}htwwbu~3N5OA4 E5L` +.[B+ױ*& +ak천OK;-ē.]Lnj85<7\ q\s.:AQ~! ocJ=Ř^$t51%$ }1'=aQ|Ov{PХ)@dD'}43%',Zu6<Ӂv:t:prݹtrekմZ|۔ƾR3֞b*syCbZ8НO*,o[8飫f)k-z3 [̙8U.`*N^dP8XQEލ\ KcTE˃j= z5TFa>*ғmwA96yL(pуNȋކa$WG>$0hL^0+h+ڏ`P?9!bX\'\&n}!Zq}6M~+%fm^|S7^@^R]׻OW!.^ĩб0DF {̅11ߤ|٩b:st .j:0W9\mvP7#qOL52ɊD/Niд\NE.MWr}΃+_oﶧ_Wk@Yo rTs,% 0Sq %\좱?Luo|XNz&~W[ ~ hrhWN)c1XaT{=1N\9S0e<VM6șHk|ʫ$JUEUYdN\ʁu&a"uM|~7.Z~LÇ{k-q5GJL^B=YH/Zh0C*G۽\l6١zD5٥vyĢ +X\[.UCxDvmc\ zn=ߓLsXEeߝʋىDUB%h*o@kǮq ː⎾pL+/W{nnQNbHVqX,uyHqڷM]C'GiV;!.xoى 4gx1'^at;w/xѣˉ(Xtwly-]o5_ZA-[1 ks}(zv!RhPbsU*]M!̈VIbƖaΦzxq}JR,g'ў%-ί]8 1xRM;R[d<})=`dvn\eOA+}t-zf%|ްiQM` Gu{<&K;4I4zP?3_P(sɶuF.B28ͷ:Y&stI?GJ@7)6TOEw,-z섿XAݣ`f7S}?P4qWv!%kC>bhڗdmX^:?,D/_#A?T:=veQ.f5dﭰWr(LթS%7/^n;?1W[1Wr:fOy? .L1L` u<x?*wM|p|#Gls5ȇ< +| |kQ-`8;̂3/R-O,fQ{LThpԜ4[ $Lr0 #~bEs].:,fq[_2w\jaZ-- *W`g:ݦ,TS DG׈ fr֕)|CJNh%# &Q?-fЉEzfZ,q +u%!T}ȪlUdb{o@VG+1*|PNd`[2,uFf1̲Xf4_He!d,m VJdR(Ƅ]J~ ;t1$$y0l$х4{!64IP6aO+. ia?%:0Tzt88GT;{jj(nqɂԸO{s~үz?pig>~{ Ö0|7Vw~=bzy56KB[2bVT݊YVP̜ӭ;;Gge^yW򌋋CחR۝r:XpdAێ}N0sso,p\[ks.+zs,T)rR,YXw*6GcE#fG%㗈_">ǭ]h7.<`5t>i?bq.2:~oě/+׌ësG}AojnZ-SkM64[}͵8HX]e^{2;lv0ir௶giғno7!Yr` .!iÌ\->rjL-cwxy؂?uFN8| +FjqnNWeQN>/gpRA Oi) ĵr75S5j\e>Qt {;|Wy Y}hx5p|HQ “[ ^׫;pX*9"1l,n~wU`^ TQ.Yp +,KUag<Œ5}/KᗥRe)% ? 7wnor2k8ݻ;/}|e71:O+?`X8,ϝ5qϘ)~_X˷?ÿcLv_ }sS>]a +cR_Կl;wvww7oV'g/1x|vxqͳ`?~oxȯTw%O>h!ȒgUY;`B78&сP}h ]Ma/7$9c=E +akFH]iwgMcI]HV*x{ @~م7sV6=(UB/} mp{սM;?!*74BwAolm Oh&iBmKX{hM{C<!ph\DczFB`$vj0GVL4{ւe:l` CWoA]C$`uC98#8= +ẎA @vǢyÅ/=dX8el  UОM{ogyLZ1W#- ΒaQ. c0D&%+D9 +DiqXpݰҦd2`UaA`dX# (Yc+ ragR =˘A$ aJH zEaˑDD*4WuQy,ƀa3(&w![[hz- mXZv\=81=;EBz 2uԛbN!beHo( ,]67P\4=qVI(tk*Ǿ*tj{-qFl ffU9XR h^IVwԅuM`@u0֎6$$b-_ :4r/ |_˫L&qc`eh{ .xm&!yhBsh 'T#6ʋ F,V2qJ,,~o Tԛ] +ڈ&coHuu&H2s=SИ3L*dH|8<s>r0lHoYȳ)!_WM`k=t~`HЖ\Rgzpli[Si' t7X, 63C"ސȇ!' #{1"b׍q*ʩvx7dM dijLdsjR?ѶSS'iE;+x$'aٱ_0` +$GEKwP"ʓ~$1α=)=wIkqG,.%7#+ y#3}< NDEN N-~fuNGy;C\!Ǔ?G4*0ع%iVwb+4 # zW[yE5f9d8t{[ Fh8Cy4N40,z-Qgieu&m ++c۲Uݙߪ?}e{V_<DD< {x9ys#0|t A kn~ )xL(NՙiO V5gn039uv6jBb*qF Vk #0*f IȦ5lNtSrӖyR% 81]C'hw`y{*} ;gOQ,IS^Ny dzbb#kK9-1iyae#WHUI1u!b ;IS*nO@xKYYW\q$A&K]\ȿ֛G􊡩K3 +|65KT-Wgn>tg~Sö%Ԕ'$q+;d:cٌ¯2I8z,M86xb}'SVv+CpBH]xY]&d?ѹW#TN9`d)3AMx_}puB^C?1*!j!RЉ}]OV=68nq-uO/*k0]* Rz.Ou R=6M=,myt>>ġaxpvMoBo#ޡu+d5qNx_HA;ИmGCb)t[CPmpR(NkH~-Q4 QMr!D|HLYƿ+%tzkxKӦ{"1Qj޷~0Za*lgN9)Spp)j6fL%yɵ. .baB=۾V2Iɔq!G9pEs&1$+$ʆcIhjT4B!:⽶tܴX`[NKN*=ONZD2a!N#=XŹ`oB+gs-ܺ[6%6B7=7Ҕ0ÑT8˼j ` (~lԔ%ݨݦ)I7S(<̛4yb;:`GLY26ON߫ TrE bǦko]R-yԵ6.[({lvp&{;F'y +OqRɦy-E=rO5QnS1!RXgHSGW-`h YO! deыbՕb$MH~uKm U1ógz)T$|א|G^4< DTՐk}k}$s/xV :>:;fQJUo S=`eL[(+K2.s # _<=z*&A,@sWMhKT]/ե0rǺ-!iTx Hr=GqΊ 9t9 D^freW/C6|ڱh= sȌrRk[gvz +\ }WR>}j1 ${Sb[BPB xJOFSKN]amf-1wԘn#j0QC}JK.|oDdlϷ>mUW6}Ezqo['{: +豚n74Yxj$jT*9|#$\>OSCdT9[DŢ܁ m$TqP2,6ʯSUQ|Qu++% nhӛDZ J[M3MS1Ȉd( BtT9n鳝Ɇ=b0ARG8;u733 ruL#WGqxTD~u;PkBӅSDW WSYxwsF#0b%d,BȮT/[q1HOCeDF{ +5l,D $NDOSAQ`; ޻B%5&Sa[Keп6SK\>I\#yiFLaҲBJAL"o? GI`=+!Gz֘^mʦ:ZubH4jer4t9(@YНrX%XJ(闚Rt4{ڤڶX%Z TէDF44۴y+`KGQ ?>V]j37*;*Q@TrS$¾D`z[XJ+D]·b'DFc%a8x6zAģrzAKhՠ( pbVVH?%6drUY)mjҨB Yk:V4 ++%TuRT9=rF=+yBu}YM41HRɩ'P2j9'5 +S4Iޤ@ٳ1#_n!+P Oˠ1MP,Q}ϵFC:(+ %1J9]W@1݂UhS,wcz1WRGԾD/H\2c^38FW5TLdA'k.1Z=S2 )b:Z+ǂSe帒Q%ՌДĘF,Ad2( !nΕ U~ m 8g={^yQ^@7Д;Ru)zj OGlOVpQ]BU!:H6 xtF>FgDrԣTf&S|€# QfF8 + gA:IB&u +N\`py +AהPQZk 8jTiir%FS4Y] +ReBőh]j:Mba%qD[9QziD-22bڶ 1mK> s 50@ 2הPI| 2VSÁ4j\(Q̘TXW4M)\Ί345^tcv* +\Ω{A3T`% v, +\:VY.7Tr KGF 0DSMmEc[Eu˻ 5iR#4f ]6X)p‰ꮆm3Q v V(\ +2kҖ:/M 4}jI[Zfd<:C*~~Mx@D4w DD`5 (^oD40y HD׀D4y  ^@5 Sok!qhPphhm ok!$A[D4~ HDAk!&H-H ۯF(  D( Cl +&Cl +8`Q7ʡ6 ~¡l  6p(؂ zC2ؠ7h 5`P6:p(U'4~ B +XmS*- BݥMfd6iFnʣd%W~*L(X@%tCY8Un|)Qjж 0^7L +7R2f1tI(<ؔR TqO$%O @԰P7%,WX=jNFUjR1gҔ4ץ@o?jڒ10KXVflz.1_3jvM~Սӣ*b /h VZV*$%:S)kwC( m!B;Ӷ5|=8!#¸zch'B.?4[ڶOD1zM;Uw)Wn7-6_s{yZ#C>/\+X1W"2.9"C{ zQW1vH7ZS=`y{ _2WLU7JOU*AHM|k D 'xAJRgc:\(|&6m杦C97ǐloZsT^j3Ko$ck998/ +Dvݔ˘ -@nb!AynVVqB8M%dݡzwN CoELl/Un֥x`;(VяQU8;S!Q2$ܔ(G2X7p[hoAmҔTTR:O7ҠyU[,%7+CۧD⪱iiƷicss՚!=T֛R4J U uOW-uxں)C/t<'q>;{Ll߫ Reh1a~5D]{Ӂ~[O3M[FD#[2X)Y_2m1 XAAU$b}N=@_.Fƍ U=DdNۇz\G&4%{U^I⎃^7Դ%B0!eW @3e/5g<@5`Pb2iJnd"!%FS(&_2A/Fm*nQTVh?83|/ 'V;*y h3\Uq^xiJT])Vu)AцA}.mA Sؒ7Txm=6m=yX&>~0"÷ˈX 5`<@DҌB%Kbw-Yrdb@!Iq8ySHCFY5"S:&F/޺D :kÂ@m"m#V@ar-PXdXs $K#`T` >t Cס ]*rd&ѵq6@clq(KvrQ:B5gV0[XIeś6L+Wsf5ښ[5~k;'Qϟ>fnCp1nQ5!m kl$8nk -ngpj,*}Z+ LɥJ}.F"| ,ӷ0W6UYJd3~j--RݨqxsۨTw+3H>_aP-)\š +: $~W{4I '57P pF+߂qA7;1XPCpȅOD +J] T^miά610LqN]Hi?3gl@͙0 +Rij ܣ*s3'G 0ʊ)4gF4DtR {2Ҟh @dl&|7ʍGrr:єY ɡPۂ9 Vȣw 4gVB3hjhj)3B]ש)ǫxo͙n tPy<Ǎ轝w (9BSͭT5BScWNM5*i<#۴I^- 9s3}&LmZ L(ea:5<[^~'Q6B[0FE#bo)#( + ,0ɶ8+xGE)c b`ʬ +Sե@*D B;xճώo=o^;FisGmS|ߟ}~{ރE;ٸ/oq _2|/wߜ\ǯ=L\{Ğ\޿|X_޿=]s=C矸g]]~ݯo_wh._u8SwA =xj *4V$צҲ_ _wi#~7<;>^h#-33>ؚJ56k{vqw_2|yqN!1xY +ѽ/-*YG ă TR`'P1l^H=y:S`ːHFfҋDn-gGfբ +"ϧ+D: t q? +j.G%zX+!jiL<uFzH#$/@j|eJUzP^m4,2i*?22DI+ҙƸ~&g{96zpYEFA +]pv0PA}Ɇzo<@-TXuAx8q$ŨⰌcsUI@SfPK!Q MQž%Qc}'ΠX|ݦru5y}O2€zF)|8iި\pՆ$g4H꣕R?2#N7#A d(5cXZ &MbXTpaɧ@щA1'PuX&g}`RRIM%h8W1H0T4鈨 +*PDc)!FpĊҲ/C=nnQ#jvrЄD r %#XRLהJ)ZSX4qζD'_5JC8k쓈L#UQKX7KX!DAe ZV +?D'H*1 cc?Eenz +/)& cE2/%,U&+* D\b><d3>UCY;PF/tA/q†*A oH83eY̦LARd Ч8@HxŗzHId%)!D754Qq2̸| *JGˠ=rиh"H?hVC-CTeG^/ I08 0bϐG O,drKJ"@l~UG+^?-Y:D0z0;zfj<Бs@jT"eWj K+P v++}?MWm੨btnd2={aX;xnty0A]sCU& K;]qqY^PbPC(IT4O䦁/'7g㘤@1PJÉ 33II1_A.+[&joˢeT=یu bl|u f| (jvq%q=a'HA2xsyA4RF%ںVvfcLd 4+H~y%p[Ҵ&+apܟF ~N ^& )1aHr)OHc\|6sZyf#[(A ):%tի\1҈CxK+]Yr(ъ,$~8げEY#HGNE/k݆wr53 2S֟oSx$.$:HKڒd01Ӊ`Phh=5dI%y=XAKUlHbl#3[$3 60 w%`qc[/|սVY +L< _ ,2 P"n,SаyxWO,1#Ugzh3irN8|2u$Z흘Kh%OϳrƑnŞ +V%B +Az?;ʑh22h3ĈZ!9BkCh" $Υ#*u6$ @(93QGwQLŔ: +!L#9O'cҷFĮ6_C[nCeWQ CuG0| "bU aP*·۝sMdHqy9s72iӧ-lhe̴2U$.`G1DoVs &p-H+?3dۑB G#ɐ=z_Q`| |BSb2C zA 3C\J% f$aɎ!#>=VPS+q) ADmliz#`7RR+ˬ?1h[Da=L-GG> I@zAg +KU4<.X ?BdY"lFSRTEOX'y23IhPHc\BHd(zS0U'<ziGa\ƌA! yόZfVbʌ8Qo j$a.Ԭk`qx;`TGP-=qU nQ=+KIJDܱ*wJ Qb"(^w8RLbJ$[mhڝC*ĉaeL.>dnMoi^ϒ+23̞{NeC yO%\À<$1ü0S'3642N3JA=PJKX8^F=N(Ab38y:"a"w"qLSɇN)|&4wTc\Emlaʹ2SVg *p-#dE N=9!<mB"Q]6Pl2P x`zTXt}V':%ǺBL+YImgE#NNɠ_\ 3i3`UP2N<]r47X T=c)Xd +Tq`8 \(e=5dBސdQjq+_Nuy +.;Or/;#)ZF{Quqadŏ&}cyrM*1ۇ6+H6gtQ'B/SP`Z\"rTL Qy# 5Hp#~GRHT]HZ2mR!&-BKRA"gbxFmJɌ$PW*FK)%30 +aH٥228H%xuME|2Y +<QU|9+bG={E$I(Lzq +q +`q嗲B"/bsRY ivGi)PfUT@PnoJQ2֨BMȀϰ{.Vzi tg1#ɛSŮfY{i|^z@:]+LpG+wO}Эug¥FMF />i)̠B ER(#+7-?NޒAx2$ƻ4#$lOɋD&W'5!kNy2KePP'*`UVBs ǽMt[ ?4 #.ϋ~G f) + Ye0{BI;tPH؂ˆ)WUM9nlL[ q1c :OhAEA%A@xA32 -s%c:E!B4}d?Q,a9 1A^uzh#O!J-Ai3XEYJdޅVKX#YR%b,7=Jϰ+QY$+ZFJ2OgD$aq|nR ֒|% 4J _\Nb%O'5v*x7(䫓0հZ4)r3N9P^z+r4F P:OZ$Icי쮀pLo23aK #%P +YYUvZ 8oETVU~wHjsMJׅq|4u*7]Vbk3[8enSf0sm@wV[ß\瘀&RΝE~bHB1|$TǬ9г\uPDTA AaQ՛ x2 c~+ze&[$8n6N@n[ &qVu[Zriq_olop# +p fV+a0X e jV};)px;t$3]~̮;c]TQKx{\-h 1b3jz{5]qչWD"eUpS{8;&zT!ט(8Jc6uK7alC#S W6)=c)zh?E3 [J1;†+2 ץP>e:܆9$6(TkE|ƛb)[dOq;yR}[1-M3kZ g94u[מHmUOQs~KjU}/46c4SW-'1(ϣ eڐ݈Ź9:沃$։^ KEbߐi+vh8谬ğ%64mlX!m$@8M.VC\WU_Sh%oE 8֘s]K#dN.&VR۩(D-XseM-vƅId嶤F㜳 )Gf@C s, B=`jٞ$ ' tr6@#0}A|hxI-ri"H2G`jkΕ|+,ZC*ZNE5-u"r<שGp]kyjTs'FD93lS/s_,0`nfcӉ~ߨAP(v% "[L6"*j.m榌2ts|kv TԊ&^\͕9g#sEjwAꪽ>L +Nʠ-_w91pT}( JCƽzxCl%ϐw],TWY&!AP_l?j>A⺳7vFti#8$3 0nG} =}W\Pd4T]xhdX> +4zt0jм4h@c[(F4NeN i~9~q οb +u~eeH*͜Ymyh&4?G *`Z)²e۳tXFF3n1jd8J< + +DXB&z1;KzF!r̯rZmC<=?ฎmHHݘFdIԔ_RMx]Flj/snpw=x1[Qm5nxxdfqОVH\U|)'-˼)!=#bbv2MrA~`͒ 9?ppW6+F!S.dc0\+*ApA` Xl_gSԏH fM v#-nԹi$cRnw]$ 2b%Od/jK8̈́n(flLki$${5rABq]cQJdBIJ[dBu!ɛԄJ/5w?|"+|"V`pjIp^:񎀆i:R Mc3vDY:R5b{3F1JGup!!\t烶H@7ET8ȇs5 J"#r&m }ccTbDpSdhx@}Nv&seK +"i~Eȫ3{!N ڿs"l#xoc=0 frv'^˫-]1z'nR{wuesFP)_/:Zty4 s٣@!iLOiQ Dm:4 +W[r^QBpUM9闲eD?xcL[/Nb.uiBZzHBx2}7ѿ Dܛw:n'rdHg)ݶQl)Rk@xq|*9"o$\"!JF@-XʪS]48܉kk@{99l\O6J e^r\1)fv} J̙e&Bw֤A42'Cʙ뱉 ʞPr|Ly3Pa淼"݀k0g۵jsa1cG>: +142a48"VD{+ +qvD¥JD^%4i2_+?PZ3|HlHJqKT+K +ڣH5cb¿^>gY?QnOf-R:ؑ1V7N+;G&XcpfvW&׹QRR2 j=_,769 X<p\fcOhF]5LQ"DH+^v9Ϩ`CS6x聜-*9ookQPrl +0'bx RJPޞkw 7B򰉕pAw!X,jfcf%I4xi`6z_ aM֢SWsw$5e$^ӴЎaIp%l25Ǡ~"Uw1_ _yf ʲ !RGԳyYX_eh7x0ᥤ +f|t WiEέ1p b%aI qSyГ ".a}|°#41RL4F&|iT'՗DTR4LJ=nV꩓# f6k e3b5 +'npUT]# D~}~N仢o1jO!r.B+F̋3 wFq[XƮlSˀX59J\i $k %0%K.n]o繙\RO\lܪytљ ݥŸCϜK1ywP!5@=nP[.kyElA?bC!-jxW2VתYdhޥCP'pRZID̗>uKl$5;>rFmd|&` +c]1c~\܇5:qMw \@CT$u&x} ;D?IbB1Gt:*G%T46#QPIBա"/fԽGFr*˿nosW}܏xj6EV,Y*hsѼػk#k&Z^cĂ2uEnT2TKBGhk(׈-vWQ{^Ϟkd]KEPt(I/a@\toL_d1|@LU~!޻W髌)6jYY[3N_,iWi'zSo$)oǾn(EE6iB~nAقo<خU&'x.,#XAD0|}X!;X۬NoyĒ-GEbTl-t;;6NāƈPKo#r<244+则rM0#&)Y&5zC%E؁ў1£7>a3T3-) G3΃TW~6FCI]:x7lU_3K|PlLԋcl/sH_<ͧ&ƿO$;~fjjvF*bQ- +,WfvRuR]I-xG;ֵx6,P^ 8WMb^DTukePg]z_1@`F!W(\wCqEҫ3KZ,IKʦ##rawmT B,xQ񅬬n+ikXK[T">Jp9{>p” +enܺX(6Iǂy+L^G9N2G|$G#Ոץ 6P& PM3k)̷2TOŢ^3UaU}5~ZbSK8'm6-%fuľ!h B&e^UE4VH4*Ԏwq%l5T0 +XֈZLpOZ|kn +% C@Ȏ>դHXv]>KNRX$em(݉O wF; hgBՑ%.*j!!Er--mIRbӪ_19C no+⊊Dj_롆@1"i`zN>(o$G==ZL4u1X/)QU 6hh9!^6A68f)+(qNmr +=fvՐk]#Ps}(ݰ!6Q6xj+:d†˂Ԧ =XA`Hƪ>b"FU3/a`ںZT_pϳsQtKUtㅅc'%׵[2ectĀL h35$i&{aW4#$)gNB>#,kUhj2 +XJJtS+fΈR8xc=̾%!ͯH8 +Q-]566 Y n=ӻ< -&f-Tf0׵~W+t/V0u4_lihr6)b *S&9’TgӪsSet"ˋ۹zx#ȖO2j&[CED'nTK$@'hGN5si SwȽ{o,XVq=PkKŮQh j "͹4pJ i[WRie}u)$c5tbѮE*D.jf2*ow/(|d~$R^ERk}IwP ?Lz7Ok<-FԮm*6Ny%\1?ed3lq^M ǀ 9Ü |OuB8iTW6ӬRs]e)bpF|e_~9"E q|pȣ|*5ˮpǗZ$Lb!gQ[φrVh`fh6+k>Y:b,Lvj;8|?ŝ"YfHy=Q!*ttrTT5hl ?;b}m}7<>JcGc;Bm 8 ᗡ)onlKNtҿlbdqDv%v=ɣ3~{َ}zQJքcSY=3ea$:h'=܏!P]ԑj?͚A]>O(鿅G8ǨEx}  P0u' 8trP, no<ϝ.dZ$F| 3E%tV}&LO}-u8sTPl (gpf1/o43,o_OsX2$N<#Vd;c Ѐ-_>sݙs u:s3)vY.; m]C1s.;#vKgNZqLȝ$s<w-Y:]wvKeJb`漸Ya$Hyk^[<zH3 \a5'Ң3}L6 =*~/W 80+36 +@Ȣvq>2>s(>yrC6V#_/K#v]f6QCaSr5 Ë/L‹]+}؈>_bŘznpA~h/Xa="]$X{MkJy 90&k+:ʞV' G(M[?whF!|uΙS#,Q?GPvlg+6b=Z.w \:4qcpzW8zGSFE; hB\[w٠bKr"xtvW_z@~;0̻ۜx~Ɠ'2ε)8no  g}Q3֮HpV j3ﺎS=L!5sS[a2vB1BV,1^cu:Np.d@e8z9B3q}- S:z8xgk4_jx Stox@6j߁\~'ubsIu|* u|jo +jlJ,v:{wU6u܀Ӡh8աl*W8c߃U"0Sa3I!Ե^g>?CP;dX6U)e3xӱ,z q`3%m04tѵ3IbwL+@S\z0 8ܾ( I{@C9%Z'l9<E.ު#tu}kɺ"p˧.]e-Z_[hz]& TCwkU68)#iʻ7pO.fZ]:C F0ocpE%L4vZsEjKpt{O>W$;f/ANܰ WTIhXh4 t t" 8vjB@ Λ* jfI:U5+`AWKxC]GbX\{WuWua)/e zwc-L΃Oti%/ۜTuX.% Zb"TG _X|i $(^2UUd*ijTd!BSN+{i|Zt_ fBlY1G2ᜅ)3VM+yizN"RכnJ hp]fOrXagD9W 7Ӎ>Zu N\z(d +cT1,tunX3@zMO㪿'&cozХ oJd7oqNst&~C >P +g|C'&#A^<4OxuLxrOMd$yD7y׃,o{8]?C+m;vh7ݗ3='}P.b>)'}m?o:&HG,vl 7♍^O8ueKl:TYn:#Nq'Ai +LweT@mߘPy3]m Jr ni#}ZtXQV1> aAOޝzLݤBT|cp#1$6"'WXU#~c!1klw0!UF ]un7|", ]٧2TCqZwe$dcFKcJQb# XBpD6/R{y +xa[q oDTzƍa>fX>U't8:#E~UoasOӃl 8O,5^o5# FM3W p6{ 9ݪ 'hPښYN6Aӓ+2oqTV28iO4(٠GSFjC%VfQѰClfp~2PtPnJ B}gl +=Km<7'9`ņ:Hl3oaNJEIqŲwJ3| ge}I4y.Kubo2zOIoߨiWhMw2 AUftLz72 1S_aIfbnc) cM ̘y"ʮB@FTrJXcJEaĵܔcO}эۧvX'xWV}"mQýH~mCr7{xfOgI%7b6J'Zo0jXݤ7So,aN"wJPl|_;*Uj l7cf09; +Y:uV_FQO/&ʭ4Y_b Oo@cҋ{,*ǗN%ȇ j/['flѼ`[)oD^~*M S+a[]SAFٺ of=I EYz GU܋{eSr1n8.9`78[?&".{Lby|סU}2q3mD,*m#nez`*k ?9k Oۘ}d ZK0͕O?RiI8X76Q4^7޵v66%uH.+AxToc@7BkT4:-|Iϧ=Y4Z2v@* bIГ7>(}g/=}Al`+/T~]"0~H%U^$y%Tp҃G.V")$e[ :L} hyA |Z'ɠW"?h=[BPŢBP$7ַ(Z$rG~Ov֛җAXEK*lZ/-A^}N[;?Y(u{O,ZE4'V'>>˸|Z/Pjg$-_/-UAoDP=n ?h+ӿȍo-=T "KKFc4Ř"цhm@P)J@Kt3Z~& +NUpAz:PV EazPmրeoՠ 4XYr9olH:o =3ˇ\KI%xZA5" TS| P-ß,ًxY, T?jŞ7&MÞQM62g +,>FY#U`dgg<{sdzo7"'Km+b ߵ?Ŧr#bB\%:ݚcF.;# qev&!N]SzO[4WYj s5GmI=Bj!PL s 읟4n;h$Q Hi^ _o4WD-MtTWΒշMTWNŅsTQ,bCK(=T2R%eڨ걩LMuAg(AXWeTS-XW\u;ް+q`Ne]qaGu]%"i[.FMS^5MX,Ыr9e6i^qb<"z1kׯJ&×O+QC07Ku7QzD9»R$C1is^R|>#iJ^w0lk^ɢ4Gyɵl:|,Vf#1~c@JC. m=…' Z标Bf_A Å~e! F75l.̇(7u~Z:ϛKavT-n{"`: 1n, !X_E$37 +xpzYQj}|F o,X%R,%ҩ_0بj^Mد'}P *jN? \:>H7,UGeT??Êtn#`)R~B:Vý`&'' lf8T?`dfv ) +=2'V~ 8yr\s:VJG\7 +,H93rj!+酒˳h~MfldU[%uJ-tLWAwg7kjp٩?_7/x+x/ķװ7̳hw2bx"h䫍DgFQZp{_i +|c*;Ǎ|X"8ok4R4_R6աmΰ:i#W%)ZcWAlev;#^6נ#okgF"ŅoOQAm]]WL4n]ډ^-9ڍR=rѫ(A!\~iAUOWGI@P-wEN?K:;✍G= Vsv0ԓy&7<|>᧼hbKȺQ.?p:b 6U7䙊$ѳn|)^ߠt]醖72*}JDZD8 LJ>摚C]ȦCcxj&ۡ# ď㼩hMK7l=T-.QQI7GO +vs,+7lߴP4IZ$7E y#A{K6 :oOHf|:p'.6.> ٗe; CNdLJ;%lHN>WH6S:wS_BEsIͳi5EeGx4/hBʰ.~iQ21 G@4Rz01/ǯIpDݨFajhIcl Pk0Lᘯ]/Lca ` +6!ߎETם5Mo#Œ&SॖCpuſs2i.wtba/]xذzrzIC8$ r-pr,pr4T+3*R KAkҵ.!ֺL{i*AT`0w%4@?CE PTM4S2] + +8r ^R.];p/;s*`{ _M$ AxF4¾(izq(L\Unfdpȱ!x5*}z_iq'oCW#1OmaX 2k>MJw!x%7*O&r])6S{Vg'Oܐ{fb&٨n/;>g7sy ĴDCOUYMbzg 3NReI<:ŕn&G$AK]%3z/kAR47Ҙn_u +ӨV%&ls_Tj9YK?JU˱͉sti֦D5ݠK#5K_tiX_֋oIpZBRa, b Iͮ(?FCFLjZOHc+R"y#xvC9̢7v:9F^-$`"AO OBg&;J+JOnjs${M6=R|jH^cXZ3ߪzhHŬhn P 3-5 +*^&sw%O;C/. +~(VR&z\zKumuhl~I'euKZ/%#oLA}9[ڟ:x]T1ߡF߸>ߞR/<}RJ>&5K|7J;|usē'%0:b +| \m%x5*Վj>(}cFϹ?+!;K \ :Y +qۛ + +h i Q<0KMT^}L@m!QRܽ4WY*+Y +8=5+X$@۽ј;&s8`a-E/ +kl}gA=e=P6:F턖,eX<7HP! ](@pF hP3#,@ u&:ڟV)HƊV봛B5ܧ*o@DS ً;RHyS]}7;H ҏ HDUc(0 mZ t"6f>2VJ,2_wofbn󐵒y>B=u!AiSKp=Z”iY3^-jjyg+EB&;"uX0b͉ЋUbWy̛zwv2q,VPh +TG,:iVzrO(CXPޠ*r{\*^"T}shMO ¶~߼S,xPPaSX}ڌ)bԴm;1$rojRk&9#p)* /FKpta|8g zpE[>'rӞ.yCDv"ƬN뵽@_7,lSЄ ^gd&ͬ/k*d?QLM`kv,m;xM:Il_P%RWYJcY>JԗI$]Qu=W-\7'i(|QKɀpÐf !s!G|u]Θ]^# xt.s92d{oD$7AV7HK;ׅ ]j7<TtG@ѩ%T&@5 U pQK͚In`\7mHH6dp~c a\q)z" +yvgЩqr]{W (? rDɳ!?10'̞'>ܟsk@4an:tj*7Lq!`<.CX>\j/Ad?{ĉӣ/%|DO,Ϡ4FI|_?wٍǢ|vaZuS— q5DߗR~O8M'&M$E_qh_"АToΈL`'=Ĺk0iC;u&W"oMe=P ds5s'Mg ?"D V\#9j'_ s+XsoU9,,J8@(i a"\7t{r2ƚX3 +(tZ6ʒt9\vvptBPR-Ya.c.}Ez6#z(kqΙvzA),YLW/\tҶL:p5+8@R j+zP9{|}NC6{ifzϭ$M1MصIUWZjDbك=Ӑ;GiNRtu̬Re MF$!a#cԎuV)2ɲ,x a{n]b{ QbnuyAk|@gGJ݈zڥEƢ%yC974芰sjsԷ.)EGP&fIa`){IB1hV{6V2S 4Ѻ *YV:{ ^E%8yl(6b7y,ܟ#]6cm/rҌk/zw#Тyz$O$Êlm"^ZZDP2kЃZzT4=TgϰF\߳a* Ϧ+H~9Qz"Ƃf\(Ba o+Wd\ !n@Čۃ9n#mP=Rk4;]DU`q p#јRpWL:ٸinX4kO\ %sz rfj.K8eqǵ3NO = +y3+Tb!,.y^Q,qͲ@Ǻհف {XyASO,O)x Ԣx2qm+Jͽ +LAFo'StPj}R K=Civ_W yeuerk6Rہt[Ӑ^GM@gK/{AE>/G$ O#8pE5C%Α@S<WRJCp:.qs)-Yjb9zZz9d51~ʂ+I\SQaV-i9zF i8PBRV wUk3=tXbK+m﯁Ko]f9粯-2ƙ[Ddp=SnMIiڐ+ +SQ 'cjza,rgSbff}΍No:*l5?D܃I/,ߗ_Ruea$O4eWyҺ:>̛I-rzDZz(P+=\P19O/lƣӎuOңʱ:ߙA\}l~&|_=wU ˝m |~S͑;O:{_zr|pT/Cf C튂VYsՋPaĹe?srp1nM,h",Ғ#:zO}swX.Q=;e/n_|F~N7 #>6k!F-Wz; Paz'T#l.gW3WuN^`0Wf{QOacTuz?8!6Jfce>"V5da8E*X>s`8j% dyǶ$@"AACȄ|.|t6…D,e([/u. Qm'/ S}6\RgCܗ1$h8|Ɵ *`j~*%܄ W w<+)DvHVRW}xuVs̏yzY@$ഇtكJ\{t7Tv ,}+guzs5kR9*H535Ż +q)JR2ݒPN皲 f>,:6t=Ui&ԨL_p +i " |16󋝢*8/-I9jDŌqu_MK$Is&"VfsI6F(1sFWf^5dZ0Б 0WqGf#Dvg"I)W v-`7̶a(Բ%]أVe1"鏁΢i ׾Kn>lD+CwIJ +!RRdoò?xu\.TTVgVp"=$${9(޸?0|I |@۔ya?]usX`[sp>nbPݟ-ܦx #^R8A]"!9A}#Wmx5 ^ygι0^r`̜6!ydפ2]݁ˌ\R# wcÍ<髅1p1j$AؤۆKf=Lޱ=xUPv'i"]W͵X5M4:)FЎͿܓUmr7<0ŴfAƬN~ґWalT]t"TȯҥYcr=8X+JjO\Q#M:$dcGΫ;e"]>^ӊb56q1Nn1xU-seG`6pYaacI~X=C hv>7}You&Kj=z^^|kC_)>`.Dv*oǂ _ss/5wK$i5N:sg>[:Rj) fXfB$U#;ʾO' yґqPxr /IuA@*a8Xay-m +f>(_s}p^o+f$ +SlفߏTsnbJT}ZC3 +ra B=H6uA!`XYcNf-1FMe@JVeL **iݷˣ!!Fɏ=؁r$ +G͑SZ âSjivƒ^'G(-H c| B#ztbd^B +PPI 5(NZʦ,Q霳pw-Ҍ5RZ W\(%f"pXYYMCVWt-!;eOn:K55oC%TXhW= 3K +2Se~Sjk*8|gq'uAD峆F则4FC1"I̞k8::M2G<„hӈruX{1gGUZq+DGg6:wwm>\ 0 cCHMeVى).:.-JGؖq~ͷAdR\q-?$K(c][zqwssu%kE(X s.d(3pcUT [59Hɯs!PxC3_Xz()ETP,$y]r]%R/ÿl.A#O':`' ľ~dlB;7\r};T^B@\NJ)`:%UT%ȤT!8n\=Nռj]<=#,t't+Y zFn֩= qܓ\b)oqlG#֟fT*RTrD[`p$cR1W֕ qyIςh-V#fvv ;ԵS_$ыfEKU/b~!==_!~@.enNzsDƱ0#ന)X15pJd|k!Ʈ?CY@yVUb)J2cβQj_E2o6s?luK,<̅NVee,=Ϋe0Oؼ7y^͵1nL|OY 7% OyWc2Gl$H\qV:hl$N4Gͬm#z䰈hA_J #o{\w$IJec?qӵ b|(Y%{TF]UYFH!qhyZ/8ʖ;FIL<,Io5XmKT^mTܸ$RT@ +1R|i}Ws:.Um9hG:*QXsJ(? c1tBk*4RݲvD)RZmlT5F{Hʮlm6/i؆phօ}oiCtE$6;C7:H{,fP?0Zwv2[arYҰYI(jA|J֜[DۣGur1'BR籷m֒f|sؘ̬3zta e9zMYXؙ洊L1S yp++JX<6U%X tEFWX(蘀"jm[ +UiҐA.To6Bz[ +E )ΎpNkVuyد4RWrĭ V<-Wb;;űu +F*eg tÞ\>_}S[JOJ|в@;[(=iug8E\z!ҲL:9piXHGm@DįJ!)w$jwPBN\LZm3yox&4=-V/FkkCnq[@\br1qفI= [EO`U1="^PbXԋwg(uTY'~uҦP -+~7o 5b#dv,VE89{]WBKpP!J<5ZsdgVsϾn^^af?C57yT I}@z]&F.2QԊlj5ŋڠ-J.6SnQc37%-6@)6@8=(Gg ZV=L{@Xҏutׇ1U}8zRKu +0'KnPbs*2u}T=E>#h w  P kD8e"C[^:F!IY!O7Kh6fתBV *l.cN!֟H3,~R+ƹJŃWa4jU!5GLfJ(0Jƹf3`.JI4.2Dԩʰw8V5/͎y|?-z)o%gjP\'~U8&8|QeuW'zYiJy2>?#>Y{3K`╗AqD:%YB5&FMY2x`i }˴$.7< 6㝴)O = QYܢPzR~<-Qw{0/UeMA|3zoyJ`(x ayL.@D= ӷ&/`1}wa+`z:7"\ $$46I{yFQxK! +?њ$; FCEfW:GL(o98X . :RjʭM"( #yW ̻5`F p,OB5|ܶC/S?^+n@^8zPUf0b%>j]K'K'y)uXwHqZÍɻwl.n2,G+,y.(؋k)Ɗ/kmx  ǹ ZXVFd{jh̽0QTQP Ȝ! + ۊQ Lsq]`:M5TX]xGҡ#)c ɔr|A@z.,z%+jXyIſ$2/%`3[B:DTZOsc3P})N@֍UgQ7uDĽ i!0 ZƉ@ln lW)=Q@/V YEvE &|ٍKu9t;I4El2V,|חމ@HjZƁBzFO] +c8+ݷ1ā|9'Cxk  gED6X!wݮpmgS;JE'~f:f-ޗL2EFTVr ctEOT ݉J)9ԾQ/DC9۪qԟj E@|tҾI=tXd9|"iKA6׉Gj",w-~:nn0#e(k o نG5g[&~JlB|ZV8WԘk:Y9;}i8X=ǹ,Ǖy LA5 EVZQ#c4IYJYW#h^wX:%5Pi*([Mc.$PO[m%s^Z_V^`jN}{r-*I"pr XV@@@vHejJ1DZ:諂cMaUu)_K0<)QVw,pH*% py>)~3;kK4 Psco-l-Ŭ7fdFhuVau e|9eb + +T!V]\ uչ0[o5ioYmMoToLB,ڡcԢWj`&g2mI*VNJSTdɔ- +_GY{#F!&$& +yJ +<>cƹ(J%|LT-J#fRw9%QO|t"XXTw“KVs?k +b @uXΌ nD +S珲# k{z3]hI_C[cj--Z͚u#Z!Kj.%T SgXu_$AHqOUTuN5M.egȹ+ ț5Z %ϗ~1M3/m[;7;̦r::o+RsAlլCr=bMTA*#؈sB$EvV &sAAOPMθzp!F̸ՙE//·R^q1{vHB3j.Z[![.0H" 4lt/ 5A|Mj7Vhg;)?F'7|0]dr`L()?:?nޚYGdUl~N~%Y +(ֈ_HB1J $<4ԍ$h.3w}],A|Μ/lø?'&,05iD?"RUh("V .C2Co|>S'aJޟp)p1jt+ĜBj y1S[h.7f,8 $J!)s7zIOP,G#z#"Df?K)8!C5@@C\/;!L,ўfjMA)TPHd+H;5Ɣ-t2A +6OzAb};Z `E|4!!%Z`xP%7镞)/jq +h0A`ɛYJtgKwp(izG6fõyx+޻]Dd/TUFrg +k(W7 +K(0ai dv5Nޏ[4sL@t +@dh4e|j7K* kl\cKqoL)h) W24JV* f)dq'V5O%-דTz!(vX9)T7NY< K @le e5[wlo0(_!h{lioOp" 4a،Qr>u9~B h [ qR0g""]?&~Y乄bqn&F'鍺QC-IQq5.umd'鷲 jtVx2kOe&kh'#f䎬U;sM9qʟr.T{UYjI,_ DujLSa:$U3GbɕU` yXcOEe4cFKb@ZmoQpg@9vP{H+7Bg{Yd݀VR'ZJ^QϘꇘ2EZ^*BE\iQkr葓:zReƔK"?,狢 +lKUQ"K^,L%A{Ogs,0ZPFZDOSh.\t)9kbefD\M_*Q/18uB֒^.$sdaxY:<2 +PszJe"kXMk[oZ&doróXYNH7Ոe/{Ojk~NN cvYueVWڠ~ׯ|(T aoS.U$ +82F4-j9?٣yY#JA`&a/P odhՉv6X#W2x{ + dF:B[tGE{ohņg +"(Q< %rJ1A i)d\S&9f" +$zfр3!x00gs]^ dS5lz&'z51s )S^ 3٧%$Gi$< b>nj( N.͆X_v|@ zؓʘY)M^:ĵyB]<$#F2vÒ1YcQ0:9ṺXJYm&ۈw:iY;iجZ}B/S%EܭhЕ<-4͞Kk̯xse.I] vAb,%:x&]W08k$͊^&ÛGɉ/-N&N& 0!0:8+aPI Yk Z`iBv҈ѡmQ%bY_F0fN q@|GCG]Wy+߈X6+=p +G q_2 +PNqClpIƒV|м)c$@aB^T %3y%ıV=\eppW$g"2-p =j$CԲbϘ,&<Ԉ^7kYe6f׌CK՜W\e̍񎼺X%u4-gj-sX,V-Ї/[%X4Z6\:Kk_1&+.8@"+H]XSMX +}%Z@E o ѥGPW؊84~}{u`h A˪~R9HȄChKi4i~'24GJ?P%!_ +ص{z'`UnDy>{΅{꯫H Km>'(pkP}Yi%7bd/(#!,']}+a%p=ĸYٰxD%D)yPGLQ$7gM&Og`bj?,\xzuY"uo;!Z_i`Aݣ=aJ1#Kjݒ# PlkBY#2+DLє8qd;٦F'jHS@fBo79l`oFM,jCw2NrRݤ6sr쀢Ŵ3or(U$0tcaPc=5JھewĆFƝݷۇ%?IErwS{&ELV$p)BT +np 9;!_KeȔ d?%ZyhƉl&2_-hz_ؗ#9\V͉HZO<.֧tb4xX (S(ľid*&f3/ټj5̆>N.A;$Es dN$ +){ÿ?S|M>FQ|@{Vh"O %D AY(t +Y-QL݈?}"$aQ "sn=Eyly t[_!!q[ ET"iYM~`TXNqN!,j'gWeJs&*Aa߁֡{߃`_{z=!='ɶ+U{ԳkހkPO>5 jJ0YA,Y tK0vymO~'@vd!)x%=)Hh yTnOO=aXleb͌,$H+MYK=U|[3LA73G!׾7  ħVm6<ݏ́?D6r^ٽ&zAe/$cMB)Yۯ*w1EPI7" H*@TxOQŮ^w]V`cx`]EC@X扟Fe0 +s`(zv푑%]/b@mqX5~Y <8j!8F9Czj?PLb@Y< -ˑCuBzk3n*QoqG؛LEQƲ|ci--ӅF?${ؾ3*6Ka %!NW`y0xXsT%]VޘFSWU~!+a.TsXaՇJ79+A.Rzyf4?!R)S:GznL؏Bv3>6~6ؔbMސjcl@29I}~:pã>Pړp|wX)$aoP"UlwiBz-ԀN!dU/80 +D/%2MVm\<23F:Fb[0>To0T +ˁ& ^5{J|_i֕8Oi&X4YBчhfuD'HSS|NQ\*/#{vz0|:qƒIS(nWD lRX6ۚ-A>;#(4#̀a>)>qY6G"K3$Y- E ab|Y'HGAm)@*J9˽Mb4DjGE*G՝>Kz-4!q #;+>,PC^3ruŲ!HM,SZKZ^c )wpi"AfK~2 Ңn[\EidžȨ; 皉@'J)axB"3Xf A SG'w321{H-R@G +b~? B`H&{bmaπL^@U<9;j5`!MWU"ÓE>\l +_Ϩ0S0|'ytin)(;}V1;#U+~3l4$d%K&b؜^Cř ..?J&(~BdxBaBv,QY%VhʪӼmY5@IUx\jkH +ĆCZ&,;@N^e*& +cȱ0~jKrH`Lۈ)weccpN<%A3F1ӫjs +slIS8kWT%rQM񜡨@}Ev3C&Ap'u4=;e`Y9 ڴA%MlYkࢇK@_ÈJhZ!jބs l V-,w8E#"%gُaa +~Edߑoҍ{za, + {HSP2)pXzp=eY5A}= beU󋬗F.ZGVVvzڤ><ոCp>xdJ٧"49y9Q-*^`E/g<^  ;%mwi=GK{xOʊ|)HqHݒե}e3V/8 B0c!xqzR'X-zJ5&f1$8Ϝ'_?_o~7??~W|_w?_\^RzCt0/gE+BcH[1`ʀ1~BW00D.P:V.r +_ꂨ)SB鿒6) +q޻#tYCۉx + endstream endobj 38 0 obj <>stream +i5h;}u@*ڣ QE| .?FX*BRcG@~Uվא+ԓ]C=zw qڼDE @ݗ#B_`FElGኵf<0O2~nx3 lGBq:8ݩ\|磻W*a)YS|E !$dUioDzeIˍYUBH<nJJ./C cds'~= +V sXƴIi"$q8zWqaȅ;(CF[(V$ +1 Q +}~4mvOKBz!1_ȟe_HzȵOI +B>958lcc6WJ#+tog0kza/t@Kp {F읿Q lqڙ^/~"+aaQ/2$,Sܟia'Ҷ=P!U@e#<`y_ܠC}RN\fE/Y㽫>"[_H?|iQo')|Z eY=vJ}w +\PZ$,F\ `2y͸ +ڜ1"'ұY+uPrZutyHDPCUyaH9@4^-:`H;+,'L2r*m)w?%GN.1' +:[Lֹ*BCW_l@ ! ǿ`RQcUߥKfԷoĎ@wv$s3`tʀxaǀcO}:4Ôm/˹_Hz%6'$RA4BD] +;#(-|9 36bZ=a^8Ƚ7 É7=cZiI.`8*=rڢr;/>cP"Ur׻Ey meP͹wGh}j{I&U?C\=R0PR>Ҕ{rw86.\r? #-pzbƵЪG""Twd#%=톭͂`Ouƺ`\Q{V + +4!s?h6O C@Eŧ,f\C34uXjٽ\^2뭈2/2h{ >9l?ͬ7UK +`+dZQ'T^I_JGI5ۗ]zv)p0Qfh쓃 +R4 }M!L@ny + uuzO$6h:P; QM+j:W0ηE:h;?Z2T΍)tnu;ƞ*'<Ǚ"vj3+)Ȍ"֘e|a"F $̼gu֬%Q rG5hĺhQ館.5d[6N1ʏ|̉bc {]DXcQc;x^XZ[I19d[ s+ KpGhjiR_Lxd!Dv?ֵk3̯s9'љ2LˆX +j(*@Qyڵ)h J{a9#gw h:@b SB+qC#n;9g`!V(M' dĠ +Z[M(^V1>iOb0z(MtMNǺO*W6u'\q L^,nS={zz.0z²n? d/GT+m+P\ڷMze{mþN qITq ~mCZPФ:#OI߄5^ [HJz]w HY]u),ۛWK5;I -iu)&rw|YAk[3eMVK@"{mt?G!aQaH7'R &)WJt&Őle^T!n#& 9 c3qvbN (Ȑؙ-#ܱ|ߒqbͣR`av-,yXOTGF:a;}01ϭْpkz#ʽ>yW4g6ZI˼,4l}弎2WK\ `#}pDJIн^J5O^ͅ6S++W@J,iIV H[~GWӞd/+#0Ilf6&#aOm\ݑ<ܰ[JvqK; g??`q('}w~GzI=A[6`rW^ כLUa)a!rA>TU HŖ"xl'_糌17{FG+-RZ-֏jV֩~V  D[K;kW^" =@oVҘ#U*aCP{Ս%Sw RkU,RiK}s Y]+nxG#̕i؆:Rs.jT% + qqds88uK8Y]N +܀Nl^H%lJ_^0z6k(H' 0 +58)baғֹ{1}"i=3jNVFaZ !ZCnIqd15|5L2 #׷2*:jU4*َjkP L/ƾ tMwbIk2Ti:lw!)wŇhRb׌׌yH!wGFM_EJGVSg"Tt0d>APTOč Z<{LW+A"GGE" 7*3QȷkL2s/ -J dI5~r  %22_xPS G!x=uPr3*9Њ,SyN@ +B0r!Q+ɢ͊~4Z|n=Zi~蹡|s8;CTlU4h奍=xv5>Z~SS#6d 3gz3`RO;lg_0C^G9 Ʌnk=ڻI.BTѣɶu4jW &gA5Og-.oʐsDESdGwQ^C@Ϛ$RqĖ9@G,[ deebNJd$ A=^vO 3q+`Y֏'cCՆ7RcZAom1!nI!l)=JjD8$48$$\FIzF\b7tD 5lQ7Cg>ŖS3 + 8?tfȊ֠3>!Zsm(It,nYm d7Z%v|w*&d#s/{`:g0a>vϽ=NRvõs{&Nuc 8kKcd%'؍S + E"C؎1Bޘ8wN|1WbNlxU%IZ) 'f쁏$1gSΒ +DB:GV эF5 rd8m#Q*^EJ|J zK2RtVsГrp[њvXV1Jp+Ju3g\Y qܞSbOUPampʹb&r_LUl} jG) +Ԣ:@+b؞27D>y+|$fyODC |18K~3x,=W5eN-u*AsjCԦ51еumo]DPS>*ɿNtkjJ a.}N1C'9_o:V\N>9+qUHJ=m+c1Cv K֒hF=C2u<8\x\j%Q7Qi ߃ jʧixzWč Kd,k:S% [k\/?jQ ҥ>4 њgZEN P(3= gKS@|VחRiF(!vy;Ca%XKIPr-3ݺSOؓVj|gnF_~2 +xzO뛎LY>C {7yJ3P/R%zPΧ mi3aʂ_bgEl)\ ;1X g$ўG#aטqXvsPNӥYVն#%1`>Tptzwʝ+-P<p}҇mψ鹿%d9H-\gV5\]=Yj!7_3e֜1V?aVkNlTJ-nk^DfDW#?\"=dϧznǣDR!S{wjZ䟪<={ˌ[v%K +z'KޯDQ30 +|qΉϬ_xU;($sUip㿆 +=37]]ˈyON{έ&mwRd)diz0qFY4<4楑vg@ hE򟕘)!iiáWbhj_ĮEL0ٖ"JN#8'>n*B*74 W cFJy͊:1Ud-d<e-  ;dLf_-:`( + . RZ]S. ooބ9)eõ_iE\((D<Tδ.gU]"YK +:Lcϕ1j?!ftqPJ9]aYU%'2 Zo7"KUS'xkf$GUڱ]8+ѫBn_t\ү[))P3tP$);5'hXBk6@899rJV#%,oTN1x ϹW Id|?lTa=7FAfD~h H wN-uӱT$1V欽.|\:t`έd]];]oqBhOu)Ž4׌Nj+蛵ͯ{\u-i€گ__d]rEY*@/B(Ь~>CN,0 K@fZIHJ ,,d&D8wF#1C)++\YoƿKxī+ J4Lij>״BX}te2'DUp}vѾ%KG''0hiեq$IZNV?e-HgQ=[@23QD7J=dm{&rU~v+H OB'z5FG_[HvS숂”~^02ҒX!m19}b:ңgtV>[#Q4)o]LH]#zFWng76B`~w pkߛesrAoÉKH7[vKO$е 9TJt_P{$IerQo\;+dH/kI׶]ҟDcF;sN,ZsG; -3syHa!56{;"\/ҵɏHm3>a!i0fwLʝ$HԚjk#VƑ;ؙK&&@G大pXb̈4ͧFJ(c{ NM{00n7L+=7{pUvkrz3ݑurPni~:#J-W7 D>I]bgrU٘gfpO + xQ4o})Xihw3KT0o V`]l԰*d:=1\҈[ Z4az~ф1ktkSiזtǴ`kƟc PuַBbwPq}fDTQN{r4F\/-E-r=F̾{ enf\78gw@|*1Y)@)[]{u^gN'?֫fVi<5]Rq#tAMu- 0+:{ɎˢXqxY%Yp:~=%hY{7#gB@7*O{BoűuFV./f>>Yp K0_\ddLkdC*į:?^nRABt[~>rsK^Uyng yjM4|Ki͂=xI߹$*pNCH_v2~Xi˪-G9f8ϯ1ŗ'p_FDg(B>Uj%2 StM! H{{+{ +Ny#A$ʾEzPV^W y Ys9՘W/ ܗ*)W|EPl+*Ĵ T\{XlϜ9tWC:sq8chN!7Q#8N@ h{`.= gn +]|ixG {}~ZePZNǕ +Prrg{7,byt1"|*~r+y0cĚ@q:p>֜ـ| D\tՂKo?3(%GWPmu ';d$y} 6q:x/1MPrG:Z&6 + +>^ӻK rmg+՗\' +^y|Z7l~ _fH(m87٧TˈL3mqg}K$Ǥ.GF+?I+ItscFC05ȥ6G!v{S&<1=#xVl<)V:oI)i4^#o~W/Yը#i,/E-_9'3̈OdƃH(>uNRGjrc]iAp(izVAI8|^HDcf_t[s.Mcמy%IB%UU72k**YC4}_|!)w&Sk)0$ O"Z-:1 [CHGItJ_p}5_)-CרY\{W =bM8]CX'=?WyNcK jf_ g$22`oM> u l\Y7|q;*hwv[M%#ru#zR2dm`6w?]\;^֌SնE[is;*rPƲo {Ǟ}D$| rR@G-Cv Qk-hPJƩ&<賠 +D 6<[6/lVx12",T(q$I8," +$EH {# O+|N+::(1rQ|1Uʡ/==wOЗ%~ 2ٯQ :J;ވ_PwrK~+~b߯oPwb3JH!qQ6͊jȑ#oo˰~FEEmWRf+jXYRCZ6zR.2DgoFB&b 1P+ʐ1ZmH,nLSymB ,u48XL i: +Tݧ䟠/ϚAoVY}:y60IL$wcWQR!Au N3gȔ)F;$qnF-[oQ~V0b8 ϥ@?nZ_ްzjYQ {B^C[Aq{W{!qǸf^lA=C=S4|M pVX#Ɇ@҃]ihҵ}SO=QL(]WǥsOrN5lX&aM]u8!MaT\Ǜ 71^n*VŦ`Pg-]do̖\LRv7FB~`OeFiΔ/G1Id_[sGc}&%_ ܩ_?8v?=ysGXu- "ݼND5bK !5Yc_)^mͰ'h*'\<oN]=ݩ# [ +:~C8$ji(g6̵&P}CQ!}r/"+Ls_9im^- 5<~>0RQṠ1=k1GD9vZ?8o mu=w]v™l.e.t5) |Tv}8E`/I|%3#߹=(p ]GH+ΞfWFĽ*2hjy0 +ޡT1G,޽Vnw +Lwi;'CCyg9؁zg''m48C& HgzoD!F\)qXQ#}W/>2ENB~tġoMRۥ-i5 cJAkh ڌBq&<#BDC Uu|U>Ry(<{>!}xP +%tw'Zm0^??I^*vg (_P1(Yj +eϜ/L["Sv3$/n+kDkG1,`/9ʮ@')rK̴M!AVSX;}l~(lLx̒i33ݐg|t7=BzPg+RF$')-F&{iMXh- 8|Ո60f_y:DU`0h[GQɢ夣nKZ0`Gqo_| 7[oߵNJ !̴fģ:`\IcBR_& Yy33tN.ZYl\WAO8xدNZ??AQbIۺ2"RaX g) rV% \Ah(#f#݃3SH +'%s-@qD0-!?2[|ZH?Rm3`dM؜_ݬy0 V*tY^=4Tom[BDp;j,H 1j:RQWo%[ }o$(pt{A3Gxm^!/$ 9N{:V|Z kM"`2@}SP*{D?nL l ^hʂG)P1/yr9a# P芒)P#7Ml)\֕:EAn^ ;` +Weg a*W|`ބrgWOA=JENӞ PA8:Fy;.)k{H=|=8iR)㕶O׈WMo6;6ɼb,7a-K8fj w'ק1V~ [87U/svwKsT|%]Ȭ8@$RA@&xKcI&%], n+y==cRPXa%w&IF#?Q-ѫ|^aʎI@"&j e|E||?a==f( S-@VN|2d6 nѧ[oT)ZG7z7Q IwcK^M֐RT +44&> +}j>=K(Bh5(g/BGRk3J]]^^ͣdȈ~vlja@2 +ÿR[9t --:ZRzkҠf.iv -›L +YrGښBRuMrw7/VM~ok30=ܵ}*iKJ|->jH'tZ/k-^}>fz|J18\K8@%~Y}]AQ#bxF9¾"zNPg{f3@E0.eoѵ; K0H#̿.-4~jH &[7 B|` \=n/TT5}l ET]Hܓ%-x) +SY 'i{r8UԆ2I##x<`Cg_dka8^S5HI/wN{dmIeƈ}rgS8iȴiQ6[7 2.sZJe5{@3=!F'"b$uZ>ϐy5"cgtċ3* ka8>9;EtoMI$Z@ ;+S!nHu+owAMBs~NX޵[dqqWj/XPyDҚ c#$sK4[c!-B+ь Ɔ: :39ҰW<2!qs 衣Lp\BҼgHڻHXgeGqo@7(Jk&a=k>M+JVwՠۀ3rx єNq3IV09_fj;^c])($ W +=gʉD*5zcg +zy#fNmg2'u;y2]S8J^z}@ `n sqyN?}{dSY{ +.F@^ ;HaI{!k~_Gu]ݿ(~@RB;:;}bAZ[%U X:d(ƒ#ŝlu%~kg[&kpb~2v;ȵDI8wN.nV2,j{gH+]g˩ޮt=薥"@56 90%D4oxqq  Xh*80RI^ldcIKDBK+q5E4b.2D`.|3*nڂ1{u +zĀĻg  +r^;=Xz2v R+ K+n;l[߁'+j|Rn]'Zw.6]P>~T̗9vc1,)jjF< +fKdĊWݠ2uni"?f(VOD9% 7N!eP-s<%ZWZwjjϝ&&\ItVOea.PsPԄ^/E>bLQ_Vsڨ`7qd ++WiuTtL<n5;.is^JvD:G<|ݚڙEżDkb/񱳺*-l{q"[M&K1W¥9P`~<;tjSEqW.GCՊmds8}_)?D+ܠĆh!|$jȊbT#gB;s=ҙjz;U LlOxw4CT5i3~\ pgxYpܝC!)}}=&RoDZ5Eµc[F•BZ0Wַ}-Fͤ] ۿdJ?jqTo?ӊȉ:q>hc|ϫ,A?z8a26wcz;\.ԣxѝHkz*,LW40J|.T]k|,|D{$tu)<*zbJ" Q[Ohwtk+W~x0RF=wao H֐OjxI{ubNcBn^PK_驍qi@]~VekPf({Aq4Uh'1*os4f@n^k7I\ΓmsQYS8)pskH?2f^9*0$F`6W;ZYYGS4&,k=q_[8L~ n_)`o b~C_fdOf-x*XyD!\k ]oK>w|U{K!ޝ`ڙ'G!KѫWCv#ʕ31!'=u[`bDohD{Pn?|wTv 3lӚ`W?̸*٢` Wi>u!hsu^Շ +QUO#‹e^XYSԞs*c{ s~횀>(ʁktbe&Ě扽%ˮ |3R Zڎ7z)p6{jɃu£[|BF\D䬉%uE"mр +=/1G%pl]xes)礔ֈv1?+!D| +ZTJ(3;GOo ~loL6?zWER9jwhNer9sվ>|]%İ K9A.f< +X)oDtDž X%ed.z<ƬuM:I3F҄gPh%v<82xhaUC_%wV&/?=4ƈq`bi5?.t +|z9Eb\_>+O%H} dF/px Szko+%E 0 bk@9]v@r)ťĞ‹DAVn,1'&"$TA\( ++CP}nHT:ߴ>~ATl.b'4C]ܬ;P97yXϵwɋ ea*o8NA [&wwXJunbmSn- +p\KsGb^N6DYr=ªR5)._~A( ; u%GсOsZJvH`Blsf[ڐDGC?JHf`lyӋ|*0SC|F'9kw2E]yk%#M6iY=;Q/E󼵍dfw]IEI_iyXߎ9j2!-&_Gj5#NuS)A$h),3 mS]VmOy MQ¤{>z2$:t8[Dq"nHUh)ͅW@BNw9TSLCheyI1ؑXURZX1-\}i!@F/lURJΏEApGꊧ2I#{lV% +aZ;MI650]~r&(zhEGώuG?ڷȏ۰taT3p̴Kj.JI\x[s*`ȺHmow)uZA4y7' jcfM.:'`fQPGgHی&DΥ^żhB9`▞<+0R乱 Q, Ͽ؇Oh<{ y#D s.fWSH)e1]X~fCZ'bFHe Q7mjΘBYRovr ah"mg)i4"F'i5ۜrR;Ĕ&:Kt@s'|{@XBN9=;hYaH]w~1 gAQ+{~xd܅$QO) =%¸C"I/ E$ +0F<4=7S7!RYK|09ZAw\0nThG*Zƺ fFV QݍC3P9QǬJV.S׻R +AaJUke| *?wP_ g琙 tN`| 7C{ѯV50 h݌SP1+9_G\S嬔 +눅S\ +ׅ|8ku`Vq%_%8JɽO?/Dژc*KNؙ3#ڪpE5?#2p + 6"!"tC+`_̥L§^w}FnB6 ؼI{WC2ʶ"M HDU(=_qxen +UJpvtpEոP>bTụ kP03ޯ\"mH~,])Z8M6-BGobǂIP7aEyģÐz6YvևzЇ<}E*k;Gdl4bq#m*X g1HEX{J#'-z%Tpst$Hl"BC*D2j*p]eH"{$FL'mvL.wY9p4ZuXA&$j+,};>S%BbZsi]4VweBiEq$uhgy0 iۥs-Yl&E!a:^א{isWxq]+M{U^r8fɗ4B{1>c"5hD36qx٨]jW(r9if="Fds-6g . ~#mLe +"mZ:Foh! xѥp}ɩj>s3m#ɣ{c5h Mg^QxwC @yxԠL&k1t"GDɓL`+NxvGN N5xFV!SK8{jr’~[48L'Jo7)#>iY^ߐy?W43J1!sOX}{YoP˷Nn`jQ[|}~`#[~03qpEֺ\Coq>wM(RXDj^ښׇVWZW?%JOF䀍ei[Z.X;N\ʲB-;ҠoꃷFHBD[ A ;wcMgRO^XؕٗiccB֦YEL3'Gw)ɖ#*zq5)`W·|0߉ QUƼ5u|3Z/TJHFw\zΞ JdXʬX,_oME>B t~E xЊSWIsuQfǤ/ҫE s> ;8)n᪽Ɲp1{ŊؕhL?ɉ?7x2dtڬy"OР\].d}SOބ +QNKEtMj6y؄h2 +hc}O& O(nV+VY/{* HA{+ˑR;cr(hK`:Ѹg*τnCXH=1qZ-5 I]+V9t!$¦73jqjnb Q/Ӌ׺YnoF"<@!_8N_wň\ղZNEG9_,Wb2π͐OTP@^{W;{^3 _97uzz3h?!sz ?MG#Cɥz5-`qP (91~k|>ɤr<[BeUe cޣ̈V;[5L_$Is +b7! 1]4j\žzJJI{Jx~vhns]FuCBQu)WRek n;8VWLϙ^C;߮\5 (bץ{Cǽ`^4u89w5AG$Ϯ -ҐY15<?ߤJ,%QDBdr [;ATTj'"]\h8J0dh4z$9!t(Pq- +{M|B2K&nc!NԞn+G [0F_ ?[AD^ "q#<42W*X/`z߉5[ ѐ@"ut-GNGV ]G ѥPs|c^{$iBؙ *]5`\a~)IE3nf_` +[Xa;@"~{DP*sY4 |:]J&'4$L obSQ{@_$V:ޛ_T,@k+'Պǟ +Ϯ#֓ .>p_+E>5=6D/*iY pȞJW.24S +ݖ}Y !p lerW9MteG)/xOCޢv[~rFC +Ջ,XN=#S"(C #S2}.'3]p &g_> +w2aX<qG !X]˜n- +K-gxX{euC]>K@~Q-Odjq?-:$GyFJ*ZoJ1Sbh, /ч߅VH{.ub [,FQ.z+q)҈S&#ؠF \\ j7[g?J3 +ApS_Ctwz6Cʗ H!|\'zX$}tسvȐn~3)ԽhIm0wNuִnT!ZqtU:SV0Gb.6\6cnJO heV+0D̐Hn'[XamTfMk; +N{_m&{|>KsI?4{R%ssEx^z-}p¶K|DX&> גRAi/Kgn>x7RxdhFlޥ#B0/X$3a2P. 2z)*?\mH"' +&)o~Px5^kgfr  tQ1(*l՛)@C0`5OqwY__! &BnѪ0Ε2DNk +W;z-.RQт8m^C\z0߫͐*vm1qEkJ6Ɯ0 ctdՄTAirŇzwu!݈1 `rCTyg++Y7*gt:#{VLZnPYF{]t0/>ǫwZJ 9xl 1c@ +| !LVRv,& k G>BߑQY3>Hc5Lrbm\eĨ\k2QSY>fL2P?@ y.dcs~j ^;`>~np + KOgԢg#QZI [VPXu:ߺrO`qSGFF>Vwe=A^rY ԇ[aKҵ u!Pk1cc"m1DtKznbM=Hf6&ji+^$tI$뙾# R)үJ3)%Ya9f5?~WCl|/9뼢'm9"˙H,Q`CdրxBgrX[;3̲ &0.̼dƅrl}̉ mVD:$GV)o&XϼCo>'X]i@q/=w 3y{@5 m^ _.5UKhx?y!@IijM EץoX +mU? +r!^n&+͌9> +_]C(b .!vd 擂Cwz{هP98(!Q|rfG  +ngEa]w:2+=K8i@"Wa2܋{/yj\)[p)bu8y.{ainuncaNSS4q>K)'&WFW!Pkr丨]Z(؁IK"~޸=WCm2T~9}G@޹gza_D̐)ߪ3@.4}ߴ<]s2&eKfg9eW@k~[M[6r_yO[EU+6ǿ~h-]`2o']EvFH%wHoA(FebiڿAKAI,.1C׊i"J)Oa^Ytun!e Kdk7wESiP[ľwnxH+]?T˟#< 5g~ŷIܹ1]dBͅ3⧵ZmꃩP=9龺)DdhpL_jhVQbAGa:j +JLuQ1/Pzs(zI@:aoHcB)B6ksiFLI/Iv^ +L~Jw=6)͘",Eܑ1a;T8z`3G?l'X1u b {w(Cΰ.,Z: +1JV/<PS? }0oՈh2WuŤNElAĦ"\5 + eY`Fh7Ÿ,8n^=U;B:ں~}*/3˄NLE}#`$/ʖuE+"TVܑIv>WvWN \#b|E9*.3'8O'uKVFxE9#{*kBV]8i<d6-v@pp>ՙĹ?o"iIRn +ڡcy |LiSK~>W4g$8JԾf~sD؉Q8}<͵m. qxѤ4HG=6$ۓGBǯA~~FՒ~:@mx{2R7k ZȦ`tdޕhum?%(GondodH&9Plڸ֥npKw!EGAodZUQ8 `Z rW7%aT^r[ʅ%Nn*AU­srJnTL#bUz=v(T(HBMFx(4WZG$C10G40&+gvJ~x9Hf4إ3su|)0¬6H-_-#"L +CxPvp D5|fE5Ѕ q17e9Sf ujW s=:L@&mȎ!o2VqT23JeO%U([.XO"JGU\<.ވpBⶮjgUF bNi~[]YQ`e +GԞ-\Tu?:"yδji<} +X"gר\h@+=G(|M +@.õ6֜"k1ۭxS30g>kE {mVZ6Wo-9lG{ׯۣ*e::pEۨ!,"άSsG Յ ̳3:^hۋm7j5 zV"8PjT4B=i>5~2de#ԯGt*^9(_5uA=2?:6hgQT ?RLyJyOJGj B*۱{B pxՐ_H>k{l1=_czy91Vm-1=;)J|Ij>]QS Ch6J@{?[KuP+5 ەd_pxhkJgakKy߉3F̱ L8ԘK/t+z%S~.$,;S]9gp÷rkl/`|c_c;E|MD[|ι!QC;Zk Ei`??ZlW)H25*9D&be'xR%{+ahK#"S-SG9VZTqXLpXtDGG/~Z~дG;2wf ^zT?j( kΝ=]HC+$1\u`V* Hm5XL Qj)q6Hs ٬Pr)u?3r='bK˸\wפ!HvI:p̞d6U t{zNXd?r(Bl?-pV@ yq~i;e-7}#\k{ޟyЛؖѴAqK?=-n#W<@ҭ1g%8w',ϴH"iT_ξ/?_Q"AjkPϙtVw)h*rm3,լЪ#Ʉ- s4Lf|"#eGg2E٫S rfQָ=|5Zlbdܽ `b?k:ײ d?p%9[8ظ+ +S4:;OaK/_mHrX>JpW(}mu!Dmk3PкjyG`Iz'ӫWi⼨sPyDBX E" +3%<4q hɹ|k P6I\L VAF*gcNx:p!%_1/H ΟE6cH, *3#zo h59hiX#+N_\`<1D(W; r e]=ye5쐥GPmIGQ^ňLx8TMQTO$Z]2pA-DUԹkS@!s7y/Ҫ "k"x;:G3g x1DG1۹k8?Oz֐*^c\H1S4<-ɫ_0mOkVDA`vk e.T=hɀfB]/0sz*ufvIr'TSJeyե3x +͚|?E!ƨw>=lA+IpƩ PVb3!P)C*=Gie#1DCЦba:9YEx3v㫏L8X<чl=9$>{8@y8VU|"P{!ZB}R#ѳtrHx&Sɸ@&}|cq >4P:g8j rsFY3jg^_O2!/.m+8ANXI+<\%YRn*)W* &CD5.uGy/#woϵ Ş5DY{.h+^4/{k2U"^AwxTϵPP˜+"NAZjw XcPr\qMofaلᾖR aaG2횚Ja^ A8p-#h;:V+5bF鵞kCwx.e͡:ڶB=+@OrZS'L Gug$|=mN>2ܕw> +G_#K)-л>b^A[<| ^D're0 *-y&"?4DBJ\H5M6YH&QGmQIȃ? 1~byآ0rh ?uu4@ܢu(j:J^Ift:onHO7M^[ KnfL6&`^Q;>/\}y|rI>Uj T,VbDvjګV@>KzBds&n#Z.5 dGgr 3YAm `1)'5&Toϵs6cG5CQpq +*ń_! GQ j1hzkt^1d|+sSN`d]RpwFz\WX˾$yA.Y*NVdn|_70UYϾ'uDju>ꜤBbi}hqhZM 6oP=ߧx;D\6;:Y'dIVJzEjUKrV_ R5Yed9$j8 c"3 &/-Y3B|P >j| r13ݠ~41w#!E(qǢ2f=g)U9Dhj2,hv:R#PSpT֔43a8Z/ *xJc!5xYpr[;ڭ,X?ЯKNB´ca'+*)#;iD/#xjp5$Q\is|(ͤBcY,H+s)PZN~Ń/3:ZN[<3_cMF&DD~Xv,FR!sg$6>ȯdlQ o`/Kx¤A=YR͝}nG"# 92f,~ǔgkj!]Ks (;vk$Qedܓ D0zK,Ύ@3!@j 9jˊ-}˜K$EzvQÆ[a|秺Bhq{3~:b.M$c8`MwG"`39x [s=ҬɝTO@_NoZCZZKSFY<%"~="s)Z`flt_#L73kQLXS,Ă&]|! wC$3e6G>~'PCR '1K䍈ZOO$(#Tsν[?x!Ez}=Lড়m[o=gdD/0fgny/ x f+GˏBn 5gGk95kPh!OFnTHUL0{v=83i̍ 7-Y(".#TSJ@7B ,U)*3Nl*DY-X}kl0 d T_K|sSnD+rTw!E7T]&A|-S-a+% ԌSX+qE%R&I3e H!]G(1'JvE^t1d;¡e$dqI{dfYsRGԑKx /U[ kUVAxMɵמFzfCrdTd+F `Qz}eGU̖09j &ϒܣ~[On/`3R*s^S]񨸢 ?L{$b=zsfaf:w r(/劢9OR֍(E}.WGɥ~x?jŻl;#k[؃wzOI|xϢaOJ3!vSO/pzetVu [I'G,S+ce눧2| <|%Xx^_ 3 Դ2k=IP M] +}-;㝲kP4Z|~ߋJ fi,K6a~Ȏب*AOftٵ< q=yꄢz6y/9^3NC[w_Ts"}iNh0׶G@hn-p+m`Z%v=R<ؑ2ɵg^`l+Z91+&EaeWLC܈q_D˿-P"wĽ\V{l(aIi>`خ0^ɷ!r2;{(i~/,<5vM= 5Z/"/^9uuC^D?1Vuώg `aRWT`@ošH7uS_LZ~?Q΁g^0_ll_l7[&A' +킾ptFT A(duLHp.T"WGwd:+u=RA_#d)I+~(G_oa;4w>wrS^:d 3.}bs)BB4"K%K(߉kcPX5"n4 JV#FMs*[/һn]v\=#|3UCXOEvEERR>;ѷCX%4̥bU"UK]p2o1wYr`-u1 |s X)=.ڑ63.´ڈe5-eQ=yUJT +G, xotB*(Pb6mfg qBuHy]^QT1ˏmE˽?5I|琛=f~Rmj([z4' ~`gǵI?gM뿈@`Metr +3Mg$|u:z 32ܒ~`;7,h?wە{||ζҾkYrmVmz#cg50"xѢ2g:)'J ݫeVe DuHCrs{}ngd?QEj(uarYv=2 N)L _{p;FY:K|$V94=슑q=v `׉;يU')8F ymyF+lc|QݫCN *:)u|udp2Ըc=x~-P_7WD)yeMFEx^w-n@ޟK3gpo!-v&>wVԥ^~(&Wܨjn*ii3秫Fۉjޚ`!RP-G@*Kό- ]I=BG?їי䊀tc[f>L^Dc$Ίw̦87-%L9I>G}/ޗҀvh*IfE}h1Qq`dh\B"Qws +}&OgabT.]"_Rh {vHvkуtZR +鲤Ѻc.4J@Qpr3dR +A&Κ{đAϝ'#<:Tྪٗ҈!{B*Q*TGKK% nW~ʍ47P݃+*Wl 9תReba[,yl_~P ~G9pӏTzj bxK\Ni+L%(NJWbQc܃:;IS_lh3uQBa2L.4T ]WN])v  -eo*c;/[NJ;TE׆KSޖ-c9B gNBO rIG?N<F.57,UC..E8#viHթ)"9cMZY(z%Crne6^ze/Ba5-ݱPL/'ſ_^`O 6=ʽX| @opA!O~5c_!TFױm#c[‰%6oy~ UQEjWɂL5g5|Ş7-;(?×=D zlu)A>g9/!isSI͗ +ްέ{~B0( PB8$C6ǮjyN5h"E"Q:UY=f<.؄wBs#BYU>[YLܙ=UbXw-ŦL\^:Vxf~,6ѥ"9#'!QxC3pg?ސJ0 +i t%~1<.|g +'*lH4(S6̫(5+2hB?>S{/ߝZ|~ +sLWɃm "yQۏhبt"FqmCxYon9';gUhRMIJP "!/B} Bv둛Чe˧"w=WV D$]N„#jȪIVȵ:+cYSo޶(L%;Tۊ=V;f?kg)(DMP"fe!_W"ҥJ4BW"ZO=HoHokϡCФLu}'p7aE#,fZu Tr"dkĖ^1N(5ϠWePt f \~.zgFTvctO `N; +fg5 +擄zҒ%^0jubxhw)A,  緞3}K@(w%EYSPRwǏpF +1U8> gi!]h}%B<8ʘ `Ey944_DDn2JbiDURkTQΜR|P2\hJ+jiC_ TO/k>cϧI>$ġz0mH9zNvZSV8pp@JL #k3#SDFF@u lFd$r6!|ӆQq;`V)e4l"~TɕV(HGXL3)yڥ6QaK߷}5{=Ǣ({$Z4 5l﹇P~2щly@xXh59蜲-+BvW'CԔz&Ԗ4Kfر/笂'ܟw|M4i`̊3@+r*i ʹ]YcsAFLUjE+谿=ci&tQжawFZOԻ(][9< +5J)W%]NE3Q؁k-:סڕiF y?t[Y Fʽr}n(Avx ;HLWz^tҼ ^ׯ(pLԹOscTyx-ƒsjB 琈mi?cGgHb|3(NY"1l@)gwrkCv28jcPjqbbmX`^(ycdX]agI!FiR.h>3.][۝ ChOt=[iRTAw4p: &yg<9}ũ  yqQ>%j X2(.>vGEjxn8D{sNke3_zO++^!֟tNdsbK֮(X<{\@85bnm`ጣ>#;SmPb5O$R[5qrO:Bgw| 䯍-<!,\f8jhUSMy~ۏMI[.i =y%<ԏ( W HtU= e:YPQAsiadgiKA0sV:N9_]f]4pcGQd-U=5x̥p$gTiq#Fe 3,CikJ<a)12~ s,יl$-:vf'1+@a:ɥplTyX"X0;Ǖ]m}:F ĝlMtPwQXݢennɭk}kGFLj[C|bfwPG@-J"@m7kjWDX+*nKud"vܨ+kni522Gb`r@m`PB3Ai;G l}(י218}sAM2GLv=-wo \#9 ]f: ]uh橪"Ey5QMg܉wsƪgNRC"k#[33t/YxhMi#K}13 K)ӀDvv jYњsaE ѓ֐-zٲ.Z7^pH!S ĭD efd+b5PpQM *wVDoC' _*MnWqppX&M34jsw9&ݯ/8ao<1i6?b^货ZsTwZ NRJD@TX`D>&EOS`0QvZd e;5bF;+& ^5 gLKJW>HGtn݌*vU:)``Wqg`9K.뺰y}y)  6)L|^Z>+svĀ<9t֥X[A~ +}u33\N+Er} Z㍂B4ʱ ۗިL1d b9Mis>F'(hxQq[wx*)??7V|{F5}"g7<;'L 1 +#|4x᝝F!cL\2{8˹x@}CNy5`M!juX 2B%!+}OwSbh!kΉ YJMyoҗ%j]MǗQ+#H,P*T[ t $kriyl%eH T + ab@QFpԑ됉IL'.ґx&t։u97s婑uhI 붅0^$ʓ}vA(H4k)@2" + UCd0<3:q|=鱱tF!tQg2~CiIQ%jKbxHzRCN>nj':D{+?" +0UeәF +%3+.xK~GƢa-vbԷJI;RM`u3e?3^zr^BT,.p<ތD[6䤿po!63 +~MQ;8J'g!ND ́, iI@tJ AhI*[i5%=Z! +WOPojoGlOMJjLݯr2Mbʣ|+LUP`Z~X{:!O\tȕY!lv1Ӡ\zTu^zwbt_GTJ糚neLG3vEm- 20joQj h __FPAvb LMm.5n=]HhP;kF+3لL 0r ~,a@!*)ҫ6|Jz{+{Em;'yR.9##ɑW݂NEVyEplQDqj)\DySh]vR6tBÒxY1FvC-`j0jN&E3pX(I7fΪ w +^lJ`wOWĉUJ{laJM8^-wJ\ΰ@m,g.b5;]:iO6*DžsEA H2.3W9G-,'LQ{8OG}͗q#"{e +%yfwQLeD6#l{g_?l#.66VE}ABpAvIwíXNsV!W*lF]*- %eEݐ}kGyG\" +@^6=:5q`yocL LJ}UB@PSͱ"U?(/q=u˟#y=#C$YV 6SV|/p.fA:l3 +]YBڗs'yzH!1 +e!9JD}|S;&վ=vX_ݫF5gV"ajG0# kSW|sv QE~R'^Pkf dАW躴]J4ƻJhF:䭬g+s\׺TUk3fFlZ#dQA3_LYh1έK.x,My#U?C8g=62䊿x(RS=)DOޟR5m=M${]L/3F@B^, c>{׽ŤN) S2®D6!CƸG0P  XiIpƮ`R ~~[;3ڑaF$JE*=")ZsCJk +} !I,BCGZHb >|yAQ"Q'X+S>3(/7l-')*]9ʯCM8m練|epK{{=jiOd[Z+Z4u/4&w*1ZWw߈i \ 3=wŽeѣs~VGoy|qϺ(}Z'5{] HM/rYgKH`F +,*+'|ƑbU\z%^@s>uqIxL9k;Ӄ<ޮ! ,=`ig{R@ (K:(8H[k'R%ud#J`*_N9egr25ld!1h*%?`2=Eg8I,@cd!U$M B TeR4VRRUɋ"!@SEV1p4TTcc7_HrU5u*rS(U{f pM TzzP˱MP E7eE)Ed%Ļ +"")Ƽa8gd Y JØRwzNprpgS&clqƴ$JWnP$F.ǑYw3<$6~:<6DkXLMރNLOڙ +m8ԑH0WROa' P +r,NBUhD$zԀ6իPwU=+!(QR +1.*/STet@)JJt"f3ۮ(TD3{߽?1|Lq9`q;aUJVOMDVH +¦PA &=aϷqk3J0ȆDe_C=|O~J[do*JBj8wױC+@tB8Xz 4VOnaHŽ'FzR##=g +aK`źthMS_k8 njASfK;j1O@#p ,|Gd5 +F‚Wh\EdLۇ7ÝNٯ_}|-ƹOnZƞ}~f$]L +߱GI6@ \ MþqFLh]fQ;XwЏ +@wF)f!!\޼wZٸPg[<.NS84Cv^[kTasF|:YZ)E2#^3Quw%e/҅,a4u\UK$УBF̖-zT9_wXb}|+P0v '<XG[/JJ mjsxHdpM#XL;O,uG: oK>`X|qǺK c졲H7j9gLvb/~3 $-bkDxms?@h}qH +ߪ7'L-W7ro㻫hUP{5ОciOI^V0*6:\Gɍ OnyË=^=w@I~Utc#ϼ@u5#w5nl$:fv^f/Hw>KFs&ӣrp Vq̢Kh;v 6?=2CbyFLH9hȿkQ9fZ@*lk)rS \L??>ۧ9ZGAKj"LFBa ]s@ 1 2Ϫm (Ѹ +R,9'T4zxLuDhD3W+X 7{0=lI{ܮ 9HaKA TgIDp:bhTVEk/#T;g (^rR뿘B9EU MmOd +eA +^]ܦrNϧ+LygwtC_]]Fbq߬jÃvD5(PrI[ӫm-Bᄤ-,|G:`J*hã։,aW#0A+V༸9nCïה dr*6ҋ vIgiM9[>4AVwk}N"5b;ߘ/naZ4];E˫U$Vh(ءŴ\7`l!u |cNp7Nh 4Nsݔ.9+('I5dKdϴ0܈4fR2UUh1nfn;h٪I?v&t8W]eL5 R|.9W]UƨFjC~ľb4/ZЌK0?+Nॼ3=BP4NNA$@ҍ\D۝2bp!6{9o[o{ULdUR`sDS–Dg8I< QN "6#=WD\5i;:+`4 +lnڜ/ +@"uMPK\ɯ0y?p/l>\XZ n-?Vrs87Ix=WWX 0N0!RvT,H T8y^̫5_/ZRLX@%4Z.OdQoOb}?Ig-T0kœ0W-AJIAD @ݙ&VأiV(g kWrs-S3VHBeuYz!XQҍ:A2Q +Nsդ%,wJѽ@1~E$[0hX{0*#ڂ VU's_S%ۻG*~pOZp\|Vp ȘXC1ͫOB^A&>6 +#/-qGU#46N 9L"E=~;V+ڣB0.4gaEb)M#%Z-:HDZesy ` +6OaIP;ߪDռ.5i= }pE(=1m/&5&0k~9(Ȝ*22XQۜC?Z 3;`۬ BL_O|Ѩtb7>Dsȳ%HJ{X%.o! Ehqӗ/V9MV+yl *g$7>M@oJ$0:;RڣrL;̍,m7r[kU凞3e5ȅ\DI YFK*'Gڄ " .,'؁s4i\9o~e[G$=?K/Ϸ"c-ۢg2wɖ$ٵ`Gsx3%Fڿ[n `k wO=  kL!i):ӫHM hTQEPCW!al^Qém\OvS^mکqd<m-&B;0l"XYϿ+S3bG7:gzpZ(x}LٟH1ֻ%wLfj=ZνݴѹYBWQu6[Jذx0 ݃୳l.61,k .a2w`C??S09/Dc7h+ }}QǷ^ͣ&@, J8 +BGu.g9(TUH?*_KhDkyiePB/y[;eEev8s}mVp@ON| wMU=Bhg);m"5#h* Lb,<@\YU8 <*Љ|kɐ%X\3ٜ۲wk*]ia"LyK"RL60:{fyN.L!NxE-@9J˱Rю,@3l+$75Ѕ}Ai 8vv}q#_> +eueBk =9":|] Y>q.K=ד#(Y_-:wKrn@鸳)Yr嚣 =zõYN5XD7>%'7}uR: HhԱBf7\N#N9S71v(#Ay} +??9JWbv*2"e {nzJv\ztz^-+||vN>ਵ~ ^|3U}fh{ؘӉR~'>wFFEǰ$#:,mM,!#d`ǔH^/+$Om=3f)F$F1Q9@ (U` 3{ +i}Y:VM`Y)fpOh2 ln̹Vo|;5Hv35 ^t.RxX-ؒ6'}cWA^#B Fr֣K2`skObZըL]jEt ŠrhB7ܼdde_0Hr]Mw75rm ѳ{E$#g7(J SCyT ?c aǐmVUoFC,UV$fw6 iNxJ +#[=Gss%uπtBؘfd'by}Y0.v/A⏎C8#Zr $֩uԆϽ)BH ʦ&dT6_~j8OmZ(.hFde27vky_&}rr#آX3Ifaq J6RNrۦ#63r_qY2bHTc/X&pmcJ'CwYaH8O] pU­5(h9h90_!J (b?/͟$ +O%rO6+ +J͞;O}WK?S5@W[1vf{O#;0wU.γI].<~Y N +n׋V._֢O@jXzh*wBSPySfjuc֘AJgSM=)7R~Rb>3Td4pwW)[A#,( ϧt_P< ^#oE +v-u ,g-,YjF-?TylF'؃'vLIpu:YY l¸ig?RISvLT`V#p59=uZJւFn=~i+ +cs/DE8 g"ow[C*e]ֈ7dZTMd⽰׵P_˧s`oZH'v^^d֧]._Z>|a ,fD *=Ɖ*mz?J'*iMIPwsCP3~Q(R u rOJcy1ySd$ = ~=eJ_COh嘺Z=VZ#k\7yї\RJ gI &Ήr$ +~"ʏ1᯷c>4j ϴ$ sH/Woo4&>2$8|J?hwFNye\0үhwlLI劌jNCLnP]vt+׳5qIA,X]L;ح١I:ٯ4.пLO +Bz5">ji~rCDBBx!o$:o iYj=+V2e3<6ҩ&-I_ϚrAk0?R4դl!3~.M5UDw0jIG"l]}$=a-(@5{2ӂ +!FDv"CQcWX;#@YHc\ c¬ƢHDB [UFlĿy/W74~ +!b:R`+ W=,?brQL3>*p0{N+v/LbSw)#P+cF=o$kѷ|䭎ojCNL'RW'}D7^%U+H)t^L̕7~m:[fyk+BV:ipu(i)@Ao DZVoM=K]I nX!+P3\'2y{g)i5碖ɦg Dg<էPFGᜯb}lx$ oy2k`%]s#lܒ/C<"x[ +mF)ld@I䣕Qw*SbDAJpng\~N֯ྜqLP->[NLb 0b*|v_ N_C,Coy tgB(kN@HR󷢘;&ƄpD7F]D4"ڌG Y?3y7#We]3Ϛ”@a^4wkd9"9#)3n.-'IK8DaMɤ|(6ֱ@D$93;PކtlDӚk&e\hDkW2-n4-(6Ԇu%,!P5hPr`a߸1KјQ=ۆ35*)j9Iݐ{kap30!NdFs@?i%wƵϰ!GKn?;ջ%G66d +]5 >tG>EN]rpG$PzMWpɾ#7YRi*9a sdډ;!W)g hm3u+RkEC֕̀ !O0+z h:Rbjǖ{rl%ۖ!d\+roXvjڱ~7Zo?X%@~WF~~^1k{xOLk3-D~4Z^%h92' ]RS]& +[>u=5v|7oNU=8}Hu$@%L3A{cESbVЋ}'na: ?Y5@P79, qOɉ+Gg4';wkM^[0# +)pK:+ +F[!_!}ݷkJ/4Kn(,Zۍ!g>دn߂?ګ6e-Hb?ng"zHwcxE_$޹4#r`o'yaC4]֚FH8}!EVqXG?q|RT|>4rUZXѬ]!+=ׂkJR8}A.[soW,ڗ`#g*mKl[lmSNp!X_zw 5Mp0'=췪KC5 +lDZՒyP  %2AF5?ׄ55 fcN|(J> f7K`/LYhz[EFf4* mp^n *֌aǀ5yZ_6*CB}6*2g2;-g3PXaR!B&`.t³E[8~ɕE3$ Z7-Ll8E#`9#"r V-ehI==wIۜgeRԮد:jP(@9bRpM'HbBұq (NJł+˂elnű<{}m u.T[|kNƮpg +X lhjS|1:O }G'q86/nI 81zo~I`{cp|L<}-Ur>c?z1!W-mt%n'9><+.r)Z.<׶zL1:z}'Rj,/g㈻E]/N)!f +\GJ>i{WN ]}|L።߀/xϵeUA^2BeKP39@GS(VmGonai +lq{O#o43zRy׭шo)LB?\? aK^IJ']Qj ZQVkkc1~* (S^l(6g^2ŠyQ"$c/;sh%cihd^UiMkF#J6ջoe' +;xODduw޴˲[0f7HRQ^… O)R\gO?1A y$ J_|L vD +&e$xz7 TswPUOoͤ +yoXjBCNĮ6~R:Dx|haH_>3(lr#TyY8M hG|mv<+QtMFb#u5ehM:2oZ2l1^鍖SP6Ms{9ϧmcp 4uf|n&L77JVM&AfК00yqᙧ2< 1q8, %vD*}(,EKx~}I}'~4DU#(~glf]z3^5h>XeCS%Jp[Y Yʆ,)ʙ1.!uiB5a+ߊDm`A^.K)7I ([Is<eOL00W,~r| #`Lht;xh(}W{bJHծs2qyE)~C1(]C݈ %V +W 6w!7:<~D2X{,2`0'C] u-Fp &* 2Ts3/iv{"Vb/׳c:Ս;7G_veXoI2HkiL  rA*h t-?*@D"v. Dv0UOKϕ DX{^@"RYs1nUhKb>BfgQEɐ7iD Ctyuj'fU/L cĔ+ zl$5vF0>nzȚiۆ1 +|P#X^)4?o +1s]?{r # ʣ8J%k=GeI3p |RzI"hRЫ{ej;̆8΂ MD`Wzp7fg v,l\lD@~9lqOԖo Z%~[9H%ONGoX`Sk|qTCE_ͲBp'768r^mTFQ4\H7A QlUn'u^>!zH &!:3W‹wn#R;SʫᨫFAz(<:Rw^'5Q .Pg9 ߸xsܽjg5{y6,Q +uzE`Ti?H%í4J}0GaN)82!뎩*tx~}c~,MR7}+7Z4.hdr{s/? 1CJP(;w7|jV_%raӲB +;<3L?@m9Tr^󹎣kbV]q4Iƒ43/ptޒID+ 2YM {4݋) 25B@!ׁp?srpҬ,G'܊+Zxb~<|Ofݦ-Ť5]wr}G 'oG+Cq<@o5pɶ=br0kn@$g5mūgX (&,lmfJ~?X<9Jȵ=]uyl;ƓLVVHw}+d)X(IbDLm,*z Q=\;_9H:w\NV__Z{-F%a$rGT,{?AdN(9 9vLcQtYOP6Hz畎dp 8PdW0JVݖ|tE#[2If_0Ћ,Z֪}&^ +[x";4uZwL'51':F)A(r[Tta߰{5ߩsS:\øT + W6+ZOL7^vF}+n_GxskEoAAV#DD5"P+qm +mqUG8K6Ʃ +ͣ|oWL,a|뙒cb *1n +((p-jI.tVf btRQ=g.h?cv@>i?q]!zB.uv8<2,#]op'ȶk! 9Upq1L"*-< e+R|{gwR{: +py" FU,uMXqMY.;j6.$XT~\}7zu3Τ*zOp 7Hhk% +Z) +ʹm̉ 6JL}Gܛ( V,[>b?8YWf&mPJjR#T}{ U|O'\uyVbiw :Fzљ$W.w<׭R⃓>97FSi HIΐX>xv!H3Jr;&WV=HMC,߿Yܗ48CC"RZOwl!QWO@%{R]%%Z%j`Fh⤗#ʞY˃9 &93(kY +kb zfחA:sNA[vd3:,$FϹ ;ch >ǴwR }pt5|V '`@J'q JoH\CNVC~3?`Bw]J=Tz(QY{"U:O ۄG[;0b vqeBޒeiAuc9.AV 7-%950" v\ZoR|4Bt̜f[u?՛?.YVUGar9LɊݣ0q#pmY3LE klfl'6X 2/FN [-E'҆wA`V-Umk:֚[&pg'a|G Z|GKꅗ>ʇcE멈; f:1;J"*L`o1k%Y[^PJ/;tamHOQ:{n4([oyr-̔ed2QJg _ +Ȳ_C7έ ɓ-Ja!c(׈Hysw&_Q6$Cی9:Ÿ"3踂o>+w,ojou}+Z +L{ľ?T,wɱ14T w}R.tu\ԩ(à#nNː )xi%^lACB񾢴p芖[1,+#r J_f #{k[BG<&<ޗ(ec3ޕD+q,,ȳ>%nFeTkQ zj8ٴB+}˴#pF>JwV)MhƄ\rg?"wUgBywGJOt&4%]w!TMq@Yi&)JXe%îR Wx%x6۾3~i5oRlH}&C*"<s6\mbqm_ j=g3ÑKd$`MQ#hR Ks7'Zh 0shF{ XT7斟bk1%}\\9 .zikߩ_ϣU +h׆d9&UaE7CRV#bmvoRr$ٻ "Sr"Aێ+S(=2eeF0fPV ر ]u~a̙Z_ÕF[  j:JZlT5yiؼ@nz-ҿE cm ۳{qOȏ +ľz.c*jʛn@EI 3Bxr;%45, .ǵG()[e=J:z7}!"̓Cb>KUWh?uuMPv ;;JUG;F(l"Z+"jpGȂhArVA#Ȅ;Ͼ4IWFXaO+oH3'۩q,XBv͖>?Ao +PQϒPC1xW FKӂC+r9E˓#WĶgܭw endstream endobj 39 0 obj <>stream + e7c We&f"J!'+,U7~6ڗ!t`5v/|r[OSvd?+?e[ay젹pLɫP3'8c=v~9Ӊ|gPS!n 1giDVE+~jkY +h6@dR৿|[QMM"޺bkeqI5v-"{vrBcBpNHi<2nJ\IdTDbX o%,y nyMwf1n;]x tB_GnArD(+q,\EeN}CKv'pT!F,)2V598k3bORsC6M\ےND +וJb"o`h֜ŌfB^Z MigTQݷXÕ](QC +@G !,OV[J[6yK8tr12=Ҩͼ19d pH*]bCAUj}$jcRc'U=U#žz+I?"ڣnU ]&{@TxPPT`i1P UB+X3XW( UAM +k?a9΢q{D96Wzb!clVIYâp8K%kM%鵕BNk!:;E, M=Ny2Wm፵GFx,%dj0Sq%Ċ'Fi!W ]XFQ1eXdNH!%u!qƬ ~ƌ%e +)RH BSJ,I#wKDB8M>|~\CLk}7`0gԏFJ*Fo>4wqX7ȿlTxyϵ37_UN43W>88mvWeژoAb.N0+֊™ mwmY2G-If9tp9^$ԏIơ6s#Ys:;uiA?Zk!4@@"Ip[U,0-\}i֚ZSSѝG< +R|AS9+9uKԦc=$03$־6\OxJpm! %~"FiO=xV^Fly4H zO'ؗ1#" iQ>@OЈa'mmFPCNտ(Į}PƮ어&N`Y&UVmEW * DLi*IIxʮkULS#oU&?o3X߈RS<( + t@k9T3i*؈P&b +{2'ywyq o׷|:,BEL8=-vq"N93==nӧ1 &|U +&SѽӞ5 ɱ߁4](A\}Gˆ]Ŕƥ^ܾTyFUb) ~BI$ q蓍3r,t"L w`1cQG*TdVny^ I8{-ZFVmzYF)EAzhUfʚ"l5<+]f7NY/9SkrZדz!P~'uxuk;ǴI|}a|,I7nOM\dbsE)]`oSeŊoIcKc:qLǒ֍*F +`^k7_ +~˘-Q?/zsd ^YĴ^@HXuVE|^A>gܟu067+gv? . &99~v#R^v(1aDj0PW"?`!FսZ)+Md3W%13qlG9baޓ!+bP{Sp$H8@._/ {s4qFZ4KbYz{f&@݌(6pr*5+c!_~NCc%feȫ4xګrġެwC5Є 0Eȕ#u8|VyYݖop{-sƂRZ Uy ~^[yϨ41AoX؀FO݌<_o +swe"cO(zE;{[ !Rܓ)|:x RJz ڨC{q7MV(C(aRo)gjho͗c| +ڢxKD3z9"|Z4 Ă9T֣2-VY'ynDОߚR|I@$|E/E22A*pfN#Not5S'or2 H@&ȉ (Rlt6Y. Ҝ1 +8*EF`"ǶIiM넢 (- RmHRIK"@}qqΕSVQ.[b$A ]_QYxϤqzd>~Зuw' 0!&} gĈo[vPFhDo'L:D,YJ~r*+Sp]gJ}*si¨DOD`=d@0_M/"A(7ɘsoDxƙV/ *? gFS/b8D=˿b _I!g}<˵天!EXOV57V^% >P՞"i(l>U2$=V :ʣ=Rhlij7zv=x?kBYʥbL!D|D?{Psaw,z\}>(Խx (8x-=$BYJJ/*7kNJאho=4A8_WT9\<_Y^ +4I 9ujMȈ eGdM(@PWtw +*o1"I a PU׭TtP#Ә!e5"g}Pxz3"wfYb6)y:85Q{;,+|o, A`I_x{ϳp(^7- +AS\ +ν툜h8^DHk!\>Bⷋb{&Zwy{$zae6r5ANyIկػr-*?y7O4APp[;$(fNxHFou~]LdrUzŭ\:_ Olr'b:g;NH+*-4|sզinl +Ú_~uĒ6Y;nUtZ8"GS5= i'\)z(:7d] A"ř}щF \s{gكuZ("mr?|s9xqcB?b6pKųM[uqzx)]&ڡٙDŽt^fEqFDm)C &Iy>I[ 5 W, Gl#6( ?JRT+)AjmCAB/YgA*Ӳ`߲ꙢWDmot53WqղXo`C͆|۝\,](Mɷ;Lv +pqn +CGg\SSLfkAp@'z˴"S ƋB7?NkzA* p"N~'X2GZ 5` \@.i\_1K~pMzN {ФrEFV Zx׺1;2GP^gq5Ohz( r 0>P2ccGZ Uآyrb}8k>q-$x+<)p[_,!jH@p_z9AKkcL`נDGmgtT2g3d=f#jv@GY0uA3_YŞOIz#3"}0J+/Yq ŢmJ Y10X "kA"Y:fՠf4gHण }w=`Pq'Wr_aw, ;㫝n5;38D!3n#G_ _H%cоN7a+uPЫ4aYy/gϧU ~VC ܯ3{U39;h ePO3R7pL|K_jws';͇/; Z6)ƲV7zbq`5;y|R;Kzc^c˩S]/A~rԒuS:9bԏ 0pkfTّU%woIgsoU=۴n[u(bx@AmBbv] 璿 $E@'+q%yJAN(U+)49ۧ~΀ckzE:%|@Ӽ vX`Nxn޹F1삢LH g屳:Q4KH)\VgdAp5tx_{~i"|F׈d=<4w8jvez,C0*~F3ͳu[4WE&bmEv((M/+p4*5kJOKH82x(O8e}fȸ"~CBwʡO$}P ;d*,.'6$]/)) by?/Oji=?Ϯ0^63(pG%aE*ݲQ#WgV>A?m8Pqȵ}3=GW*"=?1ݿ@C7Rz YI.8t_qrlF|gI˦`Q#.Mzm +,.*{&o69~Q oX}Wੴe#ImcDgmPq5~EʤQkC[ P$R@ 38׌+-#Vݒ>FNu^L0f>kH1!:#;tLRF' +H)1?װI({wܧl3Ak?AJ9+cgb#p.IHg<>V.Rb5 8Ğc:à=A7ec{:$xcO|1 kE }# +k횤+ Aی^4/$cD$'Z'eA@Ы ¡OE# +Swv`;#MaK ;^k7%/'^ ژl~#|:2F.nNS.JZIͻlد~x,[W34S\,+J,g=MRP0E17XNQK0Z~X7˿HU'_JU ДS =J#% ZZdXC%| m$%󵵣 e[JL9(D)Į!=ݠ'n +S)Znp(eE٧~&.|4쏃f-G/w5%%dAώE)10f#mQE̸>޿u=lWiҢd<>`3hQхQV_5Vm+kO)m-=Jjs7h*Z}bLRvt\iHwdiw<͓c +W֫ίuh\{T;]9/~`X.bS{4vխmpWнUxOg G*)!tԙH 7W]i{ a/f^?硻J&+Cj#Evm'fd#v^_ P)򚃙zVwKqR[8~nٚ^88"}r{ϼq:z"?3;n{J 1B’ܡcz9\1Sx,N:@;('ƭrl$y"uK萝zU8 +Rm|;d&;#pq&:d% lS34"EoӲlH?j49>™jR2ˆڬgڸj*CqHE-:[G_s*KTuҁ7 [!]RdgABdnX.гb[{/-RN(d6857U0J`һ;k6(8K7* +u@EW|kޥeHcZˠ[Do%7kFSڝAK98猛i'ث9sO*,(fjqz>E処W ZP[BJFP:.{YN +Zn9gbf EsRemnm?mm@~_%J +d AF[ʈҸxh:~q;"1P{B2s<;cК=9tD$Z%#BfНm +z 3ղ/Խ#*Q'מ$r1yw0"ݫ>'kg(p>ꞈ8B8@wa~&BjYGtT噌?c}@h =g\6J>vUm+^].MzԌh@'L>xP'R#葦6P_[Ob:+"}Q%{:+;hf<) EZB?xFIG]$STwEV  c[Տ M +Y~-#["σ/Πb/ +Z + kJ+.:6~hŮZ_GDK C APⓀU_!J#yWm)grr CaO1ofh{Lmxɞy=Ea268)n&@Ɉr (4P\ =l\I(=C?8Et>a#36w O08BܺOnG\@;ZdSFmw8'tP50 npg?I=zt"]`[#sy>~Zmwd ? \.{a Jml~UX>p9',#<-gі*6>ݾXK{aթWͮ(E`Z2OVMBf;(`JJcjuQ YbWYT 'F:kb 9s^ЇM;dd OS%g\uhumXnu˞NK4Uᓔ>s%}[fdCB.*$w=8^(QjwnI|Gb; +J/UB3d&U59ֵA]Pz^E XQVFuj +7F0 elDr07}8 # 5blWR_ 9GesWʌBSĪKMĜ'Q7 H^x(OvI8^v?">GJ'MVt2_g!^iigFZ^R=ƁNlYD7;l{quy"3u|Aa ,w|VY6*&=¹c=(^+T̖> zcQ'*\ďM_4>~~3zyJ[i-o}HV_X63OA_;jpٻݔkli>?W Ð#՝ +BQ!m EgDMA,ē"! .9<ŷ#*STAET֒,ru7o8̱/SfHfCxD"*].=.v%[{ٺtHw 5; +Pd&Vf&|]ɷmRx={kW :RT77{fHs Hj_;Po7Ҁ9ަ*(\u%{pIo^b_m%Y3ChD?vl&N; $)P~ Ӗq'4iw]Pب] /`N݌5 X8ؗtќV&E!>RK=oE5GI_ӭؗ{]."g;#ob|<AIv.FMp.'B5$rB0jA*5!FZF\1pў>aK 0 A@xxquSNFdP-5#AaPdT\ʘmX1^w9wN%|lњ!ݐKM qA$:Tv L%3:0ϚZUÓFƾ`V:^^7gtUW:32Qgلu!*sM녝ɀCES@763ї[B^z9cr{HAk~ (ҙ"b8e mSGlmLy Sͣ;g=!/7LRK~ky1GMV-ޮ7ΙUw){ ,=0~t8V Œ72A%̆H?1}~{b:dSi 5v9̙5]c6Rފn.S2c餡BZ7̅i/w1(ZOL"xkMYޭG"5~LעqO:fbs#}9Z1X<IlW |‡K3@]ɭaw zfk4Ǜ~n@I!ba+ QCZ0 N]X/Q31 ;\z&2k;ёۦBd~ `跡IqЮnQ}UDH=UԱ1gM-Z'F١^[?cR}7#=[4}y9dz>_Ê?@#'7*\)Bc÷y{c-g C2PjCi]+0VD~BHdК+8b*1Hҵb +#n"pVGA[}';66<]2YqUShԟJH +xNoi+ +3j'_'65pM{Sy5bXnZmX!]uˊӜkbc +cN+ εeN* kF of l`_w㠩#V)Mai,1@w}#t\Be~^zlk҇z6E:}ڑDsBI 0w/4'Be`Z5ge{4֘Z?ʳ:a9C_8$1wbO69c#*jS5>fU0h!:#9#Hď%,BTNi죶`Bx2!2W;BeNG]#͖9S8WvY5#L-aGdjz&֙ێ!W 'dK:q1F1%g[iߍ?VLȃQnefP(|ˈ b:{Y @yO_?\EnkVd:eO`LĎI/a9񕀁:y9A-ܸ[ xi- ,΄ZJajU׌RMRªY:SجYUH{ zS3fg$Y!X"N% 3+Y jtXMGښ={*1YQAILr[D_C Qϒ/wFh"c wzɾ\Vk'喥:ZGnzG3@^Ce%z<Um?gU7|4 8~{r=̐!ivݺX+?r_Rx*!q3vI}&<{K~Ph\5ToJb3w'ْX`Gs _$@d; x7t>>T~Ip-Njp;aaK=B:2HRM^aZ'E٦Z+u⟧&{o=NߒyMyI%>L?H|28.n;EO"iI߲`(S![]&93 + ܏#v] +2=#PJ[57.Q=Ƨ0{Phqi@=sX[`&vf:5= x)W. AШВfpb+@=] +.&\-ʯ{_2MD>)DP)>_%oR?jP|qdgDUh2շ/dVVY1Nj3'oi}*x:ű5A_$׼Ր5bSyV'RfC>e9ќ$ik%:g9bR|eano=h!PGzY_IC>u =Pg µڔг6_d? UaIXYal9H_S6l]E3۶g~)-1GT-qT +K`$;^t[FKQݐ&:ntg?(XGiX l"geW +=ןFW6/FsFn[*6c!` +[c^Ӯޙ:wwF$;wlFWҰ2gMY`!QSH2, 9B!}A%&$fj~r.߉6Gи\ݱs` 2ڄ*_bptCmpY"zQr7(o +fv?iS)(r 5xŶ]~} Q[ @AG/ט|JK4Xyiܙ::;t%Tv޾2 ~v~$[By3\l+dC:?1WՈ UEnīx?T[07=@%sɷ)=t|eh%~^I9u .6?k3{ΩJl+mڂ ̳t5PvGy婩v+VDw +RxN+iک(L$:TFB@ c؆wWh}DHG,HmdtB{t/gi>W#ڼ^ms}qyOyLUs H Z $y^?L';dkV^CqK ;T>XxoS?fdI n*-"<eRCWx ?j| RNm-CG\7TIqH!W*kc0YZ{MF͋Cs^n)7b#b$6By:oh\4j}C$bK2 +[Xa +Gк4<9o1AaZK*f +}D+0G{d*=@\r?ooaTM^4+~:ŭ_KdPO)d(H1¼{txRD?~.H/!XG1xҮi?>KbGvIMޮF%.-*.[m*׿dvE[g/޾A?NfCIEB΁`w}C + C!Cy9RxJ4b o*Bh]-uOrUAw?@{I؀+Gd7u5^{<>u\sT-Zb !ztAʡ·/ERTJI Ōdh[sڑF7 c)(Hi9O%"(Q#"hեD5ZkgKSŪY\:ΠhM,D$2 f~re/֫q iYų kz,GQ1D1Okm 43b5[E ޙbaÁq;]Em6׼Nz 3>R69By(d\DZח]F~i~wRAq9adAh`?k8 P$Qؕfyk5+%KQ‹_A^4py,?8!6+RTGxF]|< q/ u3HNhRzZB<=Qh9YklCJF="{P+k'&P2/oR;rzҸlg<λx!pR!/DЩ8rR'2(>e񨺞5hg_qEB:sE_S?"AxRWUQ[λDG$ W/^QmUFp8. B'h#Or wl!$Гi`QD@Mޚ,U.6֫ a6:oGl+A(Tʴf FjrL̝7uZƙG F"K0CBeL>vJY쏾Үaik„FL㨤MC޸𓩏dPi_g@n <S&Jz;2)e^R]oyx:~~}0uVb$P61ն n2`ԹXUy2B@DoP$: ̴+NhM}oUn.`? JnK`nO$_ q~NYv≰5w5#haBZ^ dGz*Ez*J_kNNa&F_y7|;cD@BYlCuTihKbR+qʌeF+jTAgL/|Vޏ~"{r%Ui4EQ0(הdl̎3!T 0Q<hTU6tqm!XX2t)ht +FOU`^s<ܫ9w@ơ>]˸A-L4XSgGbЭ;p>@j×mh}N_>δZKOp5-D@!3Z"juYo^ɏ 85O]=e, (mh+ӷ;ߟ9tn$R/j6Fz1 r++qȼs6$pS|~5w).P?_IgE;c&!Nۯ=?wS.qp * TĎB:3{5zG"1;9OWRNq}%[r0#[, + ؉S1m"yhMb7!ӿ[E`A3K* 1gI`!!껿Sz+M`#8Bhnג |:qaH{f= o 8ߺN]-xD҆Z`-T܁b+ +V32*2[Ҡ;ҰvB9n^c'n0YPw|wAx}w8ID5EҝSJ'ۋr6u-a Zi 9xA7u`ꩻ4^fx!*3@CN: +뷇}J{`!mόyoPfEUmh"bGUt(>ٯGK&"yU˾G*d7SKI7 YuL1c +ѣ^=NX9Z1;`^YVU4CNIQo 5=Acj9ZfPrj%wSRjGy1| .w!I!x7YN@QL$5G!w{n4w̵@84iYqToq=. rض_ahN7x"pDv-a2و%߼=~q595_UUM>NE } );'<.QaS'd̩ 3ǐf^G%xר[, +@{Eq<ˀcM)'u Oּi-HsnO5{`t#C"aӆ8o"nړS IP3/ +6]D~hs}^ (撤Q){ !Pz(U<7W_:Jʠ\?h Qҫ!8;3G &:'n8,}ox=Kg^Z1ڸ,_Of`K>.ዣgJ5OB^ĤkQV=3G/2ڇ &?|w&1P> Ɠ+- W]Nj?C +y=H56LbIllMʷq!kv:jwϳA0<fZʋ!=klC8C7q05kKz.bVgΧ*%.O FO1eߪ|=G>X*WY}zƜ KPoN+j*ИWA=] +6&f l}~2oWIƞAP*ďG1#2C=*!;ֱg>Kv?sq7:ub3wQXhih[lȑ%zb{ǂNAs‡|'72trf>?<N[ +z7IU1=lS_Sw]8aWLOZG+fQ7'vܥ XQ7QqyتӯLöf5+;Τ=ZCs3g`isFXzb3|| P+,-[29'v/fPNgB~O% P<3Ij(c6 k=G=7/p! -dhfStba>̍q}WŴc%~']2h4?v6F +=g:Oqpm1|+Km3h'gNYws1-&iȖb]킓MG_l αPۮ$0QNRc /4H7]pǴs˾/'2[ .͛I/KWza"lC}UuwCD1Вծ?v@P\Q, 6 A$賃H4 ]k1VhDEN~|tc[k/g؀WM[A%?bթ٥/ݰCSp>S`vj.#p+ +"&IK1+^" +L,ߟ/{_(x۲&M[`]:-#v,)|u,AN3o?̄UG.ɫo; c]6q>/!֊!#u+gswx'Nq~)}b/! +tztM$G10#B`Sb. "G +ːKlùeN"a Уs;zX,%zK^rAe6tf c>C`!ǞJw &Ȍ0/!oFx#R}D  _VtIP@*Ji:q&"8JǦm钒:_g$>"1|3_# Wz q8lO\ ]J1pQ:R轫{* 3݈ ;H?=уxBR񧥩1aU}>wɍU8Z[rjlWJq\ tz|)*ev4WEٞ+5[QE?#-i@ZTjJ|3sA29{.tHg#͹{:0;1`+[q#{G|1ĔS6 S<1)^jz7t:M l(k\rN3qsBXeFk!-Mܹ9qL$>\y򹹏5y9<(1}u3Qjs#Qݸs~G]|^O u +SFb&5,VpRj97G.zي>nqkd2`[^f;<#HHM7WhI|HJ z{04rR1mwhܙD0w1Vcmϻ6x.' +1=L}ōoRAhV#H51=Ud }v__Y|H-%>jkz>ܢqbûמc9#,(j6l={U(5-2bu0Z3(F3j҃l7G5Dxfcp@He@DE@ug:̝h+b * +Qc}5瑮KeU;^7kڝ +XͲU^(Kg@r3I޿4!߫v'iQ̒9j<~U՗!=kX~]dk9'LkgO}.l m39H8 ~{f₾B+|Ŗv#bToː֋wx/|KD6Z[sc'?h{J=@e0ǼSHD1ޠ +uqS)n׮Ref^."Y *ErCMH9{DM/ة:j\ۯ(S_pd:o-xRE!5^vЈi{랱%)9mAr0G?ioRiCb#7_NC-&{ھ=c4g/Z: +A!8>I]=sʥͮ%y΂Vw>BIq_G*O^(#QɐSJ#9Eȧ)Uai9SKYNoܪ|֜E'3fӅA2S-p\[=1̠/e$F!avrWA뒙;2VWP&RC p߉?7^%3dphciDgb.ZgAr}*'WRWE΁F7V-\j|O> +Fez 8wI lY||~~ ['SJ+>TmI[Ȝ'Ѱy?T)}02;+M +^NR]sFY Nvg엄tG.4w{cq[u6 B}^c|@NagHwm<($vUtfR$s{4$뫨LoaF[G(jXeg?9U[< i+tyi:uM~-x}ޚGtuZ2r8gȭb^)fR8Uy_5"αؐ׸͔u#B_BJ~~( !E;,#rϠgF`A童!A>udhQ*/<y-.ϐ P<2dW,yK &US#n u1ؾ/UݞΦ_"1\V+,* ~X[PXω=l#f}Hhv/(w 6=xr%4CN-Ë9ssC1( k +NߎahIڣ d.ű&RcK[^APW-8#%|_<˴ v E(/xl)a k@AX(Mn^ˋa6{ˉ!M6Ԯ~8[NyA3]kqTpB6DWCQ.|p% CѷFѥ`4$2I݌Z-=OQ9uψ6@Cb׈{:9s~n -O@-tTPK!:ѐhq9Tg*gHWl% djnt IX~Av6*t0ޣUh%h#gٱ^QuGoK̠;:W/*iSFAB_ϙ^Cĉ4g$cR\;׹cSj+bVU$bA΃6e=h˾M@HqkyG`D匼.i.pMUj#t͹KIF<h3h )MD/G`WFE!Qka[eJd),oZ*Z1]6!WT>qh2 2[nM*fxAYJogp7ʗisV0x/Q^:[U9'SR$QA}uKlh`l=Lh~Σ8LL!yQqR%v&ݟ7:]h.}N!=!b|rRsTtuK2Ӎ4])&!Z$*$9DO0U|xefu;ZD$({ev#M?[okUP49'x ^~moRyߝ1*N]ۻ$9t>( 8̱!Ky5{+F\WOFq/D7#-g|;=\"=J8ylD%XJB_!hF|i8'U \>2 + "9v_!qQ" 'z3G @v!PF)rїiSt{5Ȏc480l郲݄,;ScJNo7xܸtUE-R^;wCYdOG\:Ln} 1;y!vB(+Μ&7%{kkxj'EH>[Bfw#Ƶb|ڇ[MP_ `㸯y((:>wDaP/Nx%kT}jD"_^ޱNgw1^I88(܃w&RYݞ;xpN+ mGhk%MW8s3ZSmG1汼1s8Ajϔ'T?I?Z*\z&]#҆`O톼x[D]- {ȫިk+*3k_dW^!z ,-eKzq.2zN!)1)blMZ$_jvM媹~5Uvߵ`XZd[4My~ԡ)d6+UFh?7K- ? $ad?OgV*x=J>>Ң54θxB_gY_AR5NjW4- z8ɿ# _0cA`XOp'̈5I9$w_-hh2;{X)9$3]+3 -|[@!WXg)[ dWV'\qCK'zouroqΕ$|!^Q&`#k7Do#V|<42JQACKs:@˩g=[DdHwb8P@sSH ~~)hfLy|W~9Ց L|w_񴎎| 9)[@׹ED79 q~gi͟:NVF0HYi@ߢ PDx"eHbg!qM<NK-sKK2NJ ʑub֤pRݔoAG"!Sw]r'?_naIO]L[,tQGA +_qkfm(VP[+2- t1`q^bGlprlrmbJ@3oc cZPuF:O(yW0 'duQA%95 /S+"13GvPrTZ[Mc!X2}sVVqX7 G +<~0EHe˜v ?ߴ2{W"jW2])kPYލ5, }DjJ.9#g`#?bD?B~F8wi <7~gLr|v|^F!t0՚^qѸe1Ɲ"0Uh?w@DX6'NIz,UZ4RORpuk &Z:kB}MS==oNp6hIHfO[M5r0kS  [D4oj3frl<]ӣ|B9MԊ(B}g>A< 4z +* <x!%b<([&'2k"r/q`c)tCZw׉_p8".ښÁXh{ϳz*+jMh:&=; Lo|}xTU\!KsM!#+lΐm6 t\߶WHM gl=&paL _h5$wgdZ(ޠ3q\ x&]z(agDzϞUHZOĽ\r53]ӫp +[ҺHb%0=1vqܿ\AgI{5fL~5rA-Ȇ'np~ ςiqq9"]xcJyUF WU?-wڧJۼyb6{PՈR-hq"CSU?$2"6ļ=0 B7t|l4`^ -j5VȵgQh]%,7$u= u$vIG-97~4aw'Foy߅R<CC:V\1^`!Ӗfxyi6$9 cî3a&rMXcDݰN8kH mL- yĄYBנRօ@0զ xТpdڝަ+~twq2ԭتeNf1OtI^M/^5_r(B35#h`%3AԻc +$Jn|\p%{ݵbkcXqbnȰ]g"9==*!SеӍj Hn*nr|n\g^ kjNq#x>T_)~{խۘҢ3olEEQÀFS/v/_xח.\j,НBQҟUJUĝrzԙC&waܟܪ UȠf9D!WSS~q.&mh䷏Oz059lcUIfƟт}A$^HdsrAYt幆F͕?b< XH\Ȟ'DM'8C~40Skr4'dO( gw ػL6G}&leɭe?W>pUχ +A55BY洂UZfo?ٻ{b+lh}Z̴5)dGZI2sS/ ]~"P֖P8&X9i+_<gc8 ;==xE-Cj.*|~{TN!$Ɍ8/ϜQ"IWPdgr42;HjpFPydg1UZ@FKFVpы_C{otHRo3C#|zָ(/KTT5([\;|\iO +>,,Glc(~>1H +I sB +2hٟHj = MɎlK~Aߊ ^lAxH=I%\j qPc\\U,wrw@Km,SE4*ՔFjhxXɒ]^Zlvrc`ȩ!Q%TCn8 }ST><>!IEY?s[15 \2\ X'BhVMVct :?#AZc+ $cu my |Q+O<ǏL#I.Q{Nv-fADŽx6f!dg4fhHn{od +'q`-K2U hTaϣF)r)%`=X0&4erNYRnJ~ȥ[?Rr)C]\ +Z'ء˪ch +3n 6Zl+j$u,|`t_C%!x!S8YCΈVtFweNEz0>S?byx)cLE%/Q F_QtEʱ mzd "4q O~.fdA}bĉk y[#, +-I^eusǃlOf"c2Gֆ(c=Gtg֯3J~~0٩)C(z^,oטQQxTo#TvF]" +Ze>mKüj@Wxe +KRfP#asA L+J;Kj +1rPc*a_tnbq]KFRZ7h,~ +l#[߹Mf!Y%14jsˌMuRsĬ1I:EB z|*?_@M5(SR8bR"}H8˖$uQ\c`^j3 %*3*D2Ju>يS&D{r>papi55@el%`=zWCr8t"Fw#rI0/ɏ$&Pi iQtΕJ)A(he[{GJ1g>7o3Sr3;JeJaw\E[[6ƲDRqs z{i\%7*nE줛3z{lԠ<I95+fvsu3G@.7fdݑT>Em䫆[R*~>f2Au&7^u3!_jtDB!v:EG +9|WtEr9bmT +A%=fOȁHӲ!F9xB>]B(kCq=bw1#p#פX9~{<|5{uօbSq,jkȜs@Ĵ_pm5Sfz`cʙ,ZFa C@`S\դR>ɣNUߍj"m=&7ȖR(9@l€wrƧ?1xKrM)63 ==n%H*%ùĎ,EE6 WaO,?ENA4 0FJJ$M__P#?s-C2dd:V;T҄@HR>';݂N:):l @0nK*$>K (t-;񇝅1AB#`R]LoR 딱-@>nŜqqi$)CįCF +pRR_E6{[!;fpF|6 Pj_ZGW[pJ5 r/q0i3^%NvG--Uo +9Rɥ=*i!9׌7Gu⼄m*À&+jeis&)&ONCY9Kd_H4qF!D1Mh[L%h"4i)J8RxWva^㝥 +zO&K4̚QOJ9-g'㟛6b8jrEش6i^VB˨c.D2l1 `l Cv~C%WC.8r*k=5D{zCC;4yw{q,r$T.OR(IZǩVR#hzGlOHǿcͶ_~F*4wjd[(ѣ>^8Q[9Dya(JqǧtH;D#d]OApT#`sBQdH[& ا1bJeX5{[C$4N{3 @sNgd&+D2gDFO| fMݪ[Z!ޚ䮞4_̧t.ު=㣿LUX:mqNAAmTRZs "`~A~FQ } sK/Ak$a:Hi1&T燿бzOX!#g٩60XʝQQI]<Μ@sRY^[iчRnΩw#4#?@z?Kx.D"\Wj7U8;hYx~7v Fq3: 21Wޒ H4FK{!E6d4p u ԰u$㩜#h3VKWإABY`Hr`g/AҀ! 2WĠq +w7#GQ]-Ez]l>ChTfX<Ѕqh(G-fnmS`Ad?w!<9qCzT uЪ(SHDԬsqO鳰 U3yZG`׷ؚ9F\"gt_p!noP>[KITM"5gxخϜ*PIIZ(-!XyE@f8QG:Bdy3M,FQRQh}Lɑ<):ڣg91/H-:}(=aɾ@J%ώX#S,- gpgƲhD1n{^37Ay )~;O(},+E0GGKl458KS#D9| abϦ0\>{EC]ɛ]C]邉UkĜϑgKq ;$#-PU^zWNQ߃yZ~P*Vfh1VNQxxw!#XUPb;ouآAi +"?m3t  Xʅ~!ҷABX"2y) D) N&huSGfB:*c;*\ K|z/%o_2zNG=K!@+XHĆZ jHw;m !y|߲^ꡐ8'Yf>]T߇**5E 4F[1)& +];8 +ǏCC)/iуlJ;SGoj-\ __~nVJأ#;AR]J\ :Wܴ+lIX|$,y5$*ZcǙT,:ߛ Ëՠy^T[D;t87%3PGn4)V9`XX#9t(4;oo_(/":}Y즣 *in,nKM|?&Hxv6bMftPWgS4W h]ih*2_Y6Q ]0Cbx'M6~sķ!\!5f!hGH҄4##5$+$O(%%IY~+Ư.ԣ:qY Ww{"J)C:\0$۾d`KV􍂣"7ƏĚ4\ OWvCapt7HWDon_Qi(YJwvy +0~H9$n-$RlH<AxI2'"7]~p; If8iBif]aTÇnIi[*EB;TNlf!f7 9{K75Zbͮ Kc+I0چiDhzj5AHRZmCekJqߠƉU~T}E[cQ~C#*^d`-Kwӊ?E Yæ~p=橨 +1yfp>TKjlƧSsd[$!QJ/s/¼2s6IlrgʻHrW麩1 1lek60KBTkEUu%uRnj%N4Ilp%u6@|I!8QX8?tH" 9H"8wp;T)kck_%ADtV9޿W׳yPmt lps<<= hI@c(|CY]QoQ܀ sGCZ9y2OQ_&.WG<ˎ5سRʾ~?C3) 뮖.Zۈ!'>KZCfcVo`KM9Ʊ!씈z+NaśJZR2!5=Ϩ=#ǽCCgz9y68Cs;}~ƎTvqT;T hL/fW}=W& `Gܲ"ķOm/E犵zFOcXW;>vk+R ^C >i +ɨ1oDŽ5#?*pU/ȧ!qTb~EjW~&f-*XEC 3=v!A<7wV+D kC$cn]rQp,\PiZ\zKP̨{ f_Aya:0 EL߾9Przڶ:θ%/y ٨.~}UˎjJIGogwTF}~ O Rn+;7hW'gLn` +3|Wpϟ2|}{y%20 @u2|U6pw{5e1 uU8ّY}~Z=89n}}̵_E$[0-,6,G]F 8Qy7妼Nܡz!:f´=(/*>-=ٝ⇟R/Ge)شhĝ=CF4K q8|?˕(xJ!\2T:ЍHp6.pOlA Y<̥FBۤ1I ⽷{a`JSD3#.%ڵ/ygGbjNKo)Ӊ,^KOw +w +՚[hb0pP1y21|0hKwv#iF?ty0zƽ{Wl{s)t3RoedBR?I~b8m!eơc[fl^mYq:н‡--PӀb9D!hZۓOM'}tS4D:]` z0$('l#l_gn E.˹[YS␪YIѴra4=oG{I*xS?{d?bo|H +`4L\w[%O=!?Qbyn}hl&BH[I_卧]:=iݓ j~m3H?~Mkr&'37TWHFLb&v ϙ6C"4: DvSbu;paPq$|T +R]|5C", <|K"U3WPqh1vsooiSXڤvV՝HJ5XĖ"q(zL))o}F,dѢU욥b HOq}/2xqCޫ^eD kI:y[LoF Cv z~n!x9=]hBfn6d龲vs)h%'I<浾BL!irx%8S^c-T,g:T~ hG|sbFTm䗖Yjy_4%o'/F"%t+'zvS$hmw +x(('sN˓Fˌ + x^8!5kDP\Ut1eO3nv3,̦:ų}ՏɆJp۔IA'kPjcӈV-C?lF0r]N})q8)8:u=".ѵmz(Em3{^Om 1rF d SLu5u7SvOFAZ*=m; C[CC >qgRסPٹ'|X-EۑXc*ip?y.Z-1/K6&y +s~8r ڔi˵-M=kDUbSpW{p_ဒ#.ًS&(ؒ(Cha<ܝWc٢@? ֍kzuJmF8ͪA2:4Y͋ MCK{6LY rĝ)tq ˜~\vP %XrU+’-"ixp)TX( CÉG}6WJqL ?՛Iȕ&L9 FS:3J^,Z3y!ܢ 2#%η `+ ~`@yԽAeKe3^!faW#|=7⣠ՏڴRg\\#Yvux-X҆Bq4IKtШ>@Lu9ר'J%FA-'60FL >L_94]_[ 0l{VCd \97nvƁ9\L$N@bmQeTG}9I]W(τa$. _bpE6oVO뢬B \pON{9N8ލx BY׶;Rec~f߈xl WVh)"9h>Xhc#F!p+7~P>uysZ1]!vSb >ҟmi·~"SjnjobBS53ǫ +JnTf6VoTː_?9GH jnfdjg`㺕KEIB_L:tˋ*Gy#<[8wu@ "_˩ J z_CA /$7ɫ +QAL䌐3 +h0 >q+LZ?mAUKglBT]CFb/ ]Ap.j9⬽~^51^b,xD\Z)q@`<`ō  灈C-/heO{WKw[\Ca";3A,Er6TnqIu=?)gm!sR@!tK4kJ >FqF'2q%DoId\( +1U0v͑-CwǞUrGYCc  )gEJw,>+2\zTg4EhςU3A`[7 ,_(ڪ:}+6zemm~ҧ)uՌf]d~a[?IC]l.$tSʗ 6eOjD>)x'>Lxg[d:ʉ9d?Z*u%YCG~f⳦Ε둠kFx>;ֽe#+_rYH[Etqn4 `E;uڮ7t<TWPj4-Q]²%3(4Dˆt?~7ývL1bVqKxk~XGeyv۵j )_%@TcO]ܥkw3E6y#7ѽtY rUS g>oсLQg}h#t)נK 6us;|y&5R Gm!ߢq>!GQΉ+jk!#T< 4~xxPFoMSq!+as~Ƣ|*4b_Q #~ϙ:C;=MLPAi@׫٨ ++`!J`[qu=c?io3(x|55kI[弘6 .v&ۋN}dđ =E o`b+Rٗ:buWy>{ɧDUtXUw5?果t?QagǮPZ톚[4^s%C&'³XĞjb:'k^s\"!*殺.RWѴ)|ۇY+hG3.0}":kFken 8&yƚdB!{D?fB ?Rxd&]Q$R!o__%Ij +M/%NbrHfWbd{9Ōo4 ۖ^6_MLo{h},?gYȂ$hUSIϟIDӨ!'#E4Q1@b _$ +Aw| ԢE5 ttYx0~xȈ;DXem!V3^.]V:G] Ga Y0NF]uvn#\2OaI:ѣ~O-yi5hvJ%QɟiQ1v@|@ef0GҪ3Б|'NBI 7.st4Ek$ձE(C$Ɏ1΢cS+ð!X7>}/Wv5xuFʨ_ͬfB◽lTypϧh4졶Z~sjfb+P Yt'*g')D 0\'8<&sw S)d=~3e=L;|#RHNFRRW*̟g:Ԑi6alFKM 5cmpicīWit18 ѺZN8/ +-F ;#_qk!6Ҳ /KJE$Q_&2{dAO /nFѕa F7BSiα4乤ne0g=э43簎R#,971ux +YlJSuM'28HϚ>匸șJnCECQHr VڏrpW f,@P~ߏVnjW7|WAt~gY t˾$}"|A;G j}E(kg=[Ԑy+Z8&K3:n~͈y!wz~qTG^ϓ$'*g+ݥ΍U59Ȅ: g|֥vޫGaG "iμe"QeTJknv.[!sԷ'Yn3sJGD +7kF>td׃g8ᶈa߃U Q$ư^Mۋd ,F\Ѥ"3-@UVe?yGm]oU^'؍w(Y9wфXlɜN1.MEÃ)^Q9{qRt.Y nR i39GD3Y {1re4LRv% D3κ<^o?xw@hߖzyz=VF>GtʧD}9lQ{_ y^<zAX@Ƞ#jAo\ uٶ ; %)7ky[N{ +zI ϥ[61O@z|h[LrM,BiQr IujdYi>|B&8{yv\bLh}&JE\Gjv:F=R]@N=JwP H+?Jj}x n!U?!H)=p{c\x>*}Al}>6.buy:{E1z0Bv3Y/ Þm0YqyaL32TG_cj;BItzFVȩ߂== +t3ǝ`n ^Lh a3(D^ޕIT%E~&=i6㊙C'_hS:GAXq +!e+x+TaJqLr5:asU089~a(4s];_:tP/UI,Ś0π{l^k,vC'_'u]<#W֏uP8g;K G/eDF 0a)#yx-X\T>F*AtA$l=S +QuYWZrw{3 c4ܝlj8vvDiu% B=E9ȾLFsƳ/o?❾Kjh <8Wў PBwfX 4!*s:3<jI'DH" (YsifȞx +=wpUgvO!-f`k _) +sa7`:45JH8Q|/tqW .xW +p:G *'a= +`fhu r|0LjNNVJ/6ۋqw|V7@(6CKShF7!+;4o'UF+:ީ Rj|s+d"Cft#@sr*?^%¨j{-hd9s)ۛiLjJ⪕! .͸G|t;4]GXr4bN5ϔdI㹐e]_r4ٞ#~x y_j=bYC_>`sL;$FUAY\.}^ +bPvE l|?*OU- #܉Ѥ7ӧ +1(C7R(>e.xw豪aH!:zg3BȓuXyNx2OgVIzp#Ƕ7["pLKV̈Kѡz>F ru+G|y:ؐ{^̢ԾhrWYNgQ!x5,d"30~5ܡT9,I&ATLEk=n}Ӕ^qhMѷ̊b3ƜI"(7%U#df|Q9 {/LJ~OZ%Fh 4U ІC7#GuSrd +;Oz;_f&* +Tgh6(  m A~xmP#G< ,kObuRYFwx7xdq-[ !~1 {sEBq֔]DܤW ] +X`$_t}rSxFe3U,/i@jpm#Lz(.B-Lf#! ?mrEUr>5U&59BHf[Ksc\^cx6ߘ֎1kX#b%gs+9 !=k̳ *:0ץ(ϑ9 +1¾+^FQf^I~|[_C=ZXB@+n. +F*[  Yx"n{(\0kwm)@h>Y|<>iΠaS-Ljv Ž/5xGšyt +Eu5ʕM?R"]~h)6< )kHCT, ,⓶2~^?vquFZ-;1Ty:}'X{U5 n??}ۧ"ނ1mBD]=HŁZFdS5e})"66Y7躝L owa_էKY?f~ ]hR3|:8SRϣlku]u%X#.4S!u;NxCK1,3?QS}B%KEkʜbҕ"ň㈟7wd2Dq]ǽ +,XϷDNwzF~`KݡJacb˽ھbl\Ы`Ƿϩ XJ5y~#g^T ۛU(PG8iB0"{TkibgPg.% է:W}N"QX1?a̳H|g[qr0 +kEU H.;W+eAӖT%N\ayo9_amhRTm]h ĸ<͕̜`1eX*LeFxCm"Cs +;?>CAvtPx@Nĺmx+@Ν D9 q"5Zb"E,}a܊D%]3#L:TNcUJH*V4Z [a(yb5'@_|%CUex.><頜-w?~E"()7f bR襎> ($Rjh(]GA|/?Tyc=mHRi?1 't\) }ш,PERY>w Hd"B-J&g43Bq'_>< ׫Bz+BџV E;U}T=?v}_}3+=:_dVqٜGݎړx~GGV: 3s}mlqًB1k?O=^rlR׋% ¹Qµx&jVꍠbJmT7͏^e.K~mSyAng~,t,X<{˅b<_xl#Je_tF?O,3({I(٩x&} #asGf*VݏXE ?H.K=,/D2wHr!C`q{&vF]Z>1K *-|l4CRn|R%K5U!Qk36bu/i,'|OsMh~Hu(Gk^gBC.\3o +'m9QkHf~鍝^C9-(,>]=(dGr\-aCnШ^)F>z|TwN4sR5n#R J& _C uQ怅MP ˭TǚuV.uGtP^ڛçk +;eQJ@uwu3 ٓ{M&Ͻ5Ayr.s]l<ŻUawضxIDpsܜ\'+pym{/O0S]2^s~DDXڃZE΅GsPKH:׵.e9kנUog1|?V|l uGL$YIP2)5ծGS.Ǥr'O"~W_c+2R13ռ'ʊ3 kq!W> ~N+]/I jZ]!FWD;CD*G>p;nN^M1m + gҎ4Vn$>e+`OƑ&7Ic;EoE4u-OΒ&В"ڋQ`ϭ܃V 3 ,ŏ%gu:ѷiNX;Jho +2 +Q*W6muWJac%ŀH\ǿn= +U Xy9\͎(1?jeaꩦ|@[p8Uqkc-~D5ՎW<9WR8ܶQ47gGW/٧@Fa-T8ؒ]c+!3dw=l/-h6 9X E\ zƋ"2Z-̟X *Wꖮru|P('}3Ms’K~ v;D@W$<dTE4.*܅iQB{rWKZ(qD#g*5z{-`bitVh}0QOˠt̠sEHo"Pz}n5 -VPm[a]7 m +!9J#͉A,¯MKz `HE' / <|&+>ݍ%iS!-&)ogX,Z7})_#AMwփIPS–Q?u$8=H#k3Uz/9?ʎe:C{ƙ S8R /] SMGNn VgBbY:A,J@=@&B\n)Z'2OUEoj/"OxQ;}F唬>ҞOJ*l7"u/3kd[S>Eܞ xQlikwzxv7%c9}468BVs}?|1 +ćsE.&0hBb F%E@=d(# W8<!W` EǦ|ȥ`+j+tGh`E!(= (λ=!1gRkI-T-!ݛ9BE=2$G*#XWK罞|FOuʣ39JhB/:oOMWsN[s#9LS0~Od*ֺ!cOE&É6RE/z sE*;RTc JBl 9;p&DvI{U7v rEH V*77g +3~U{_ $ on6F -2xGZ܄ׂo2aՂz0k!`R+4̸l|=0_3גh\ L.jP%caT m:aR8aUip]@h^Gr-HmD8'א sץ). >HHtVsKn(z$W<l`$m9hArU-+㖧ʼ(hאsq;:Q7uJ}LFa/lrwWLy(~2h?#uv/ջ@W9,^bs/k($B T\kۗ +>Gw-3p{pt$NeBͿ/ջ.n ۊ>Ewk!S4*]ҹFaP\@b?!&t'kroU-桖{nC{8HM!$9 ݺqk*(9|F|Πs{WWY%K˄(\1] t]gT?; -ga ks E'WJ=miQU"cZ vCAgLwqW3sH dsXޥtgzE&3ǣ:CTY?p'_zJ-rTJ3ˀi"bʾSe= 2Cǃ^U]cA3Ĉ<ɋRr:VcmHvVeBu_ԮfHT,21jsDU +3`4+8?UEsDX6JR*Ҭ;{G ߈`8V^6*XH҄Qf?tu%Pɶ衾Ⅼ'_}֜% J,(m3C-pk01 x/d%ƵgdA.*o%}+rlUla&P]&X +t/eTÈ(*ۻvj4S#(|O bmng7$ƪpS i=%+W q-T'Z+*ghrs$XWH -:ʍwT^11zmS6: +gN*XwL.{h2դܧbѪa?*QlO]2jqIRO;}M&27Vl&H+^¡# sCxtHkz筓y(ͭV.H`q\ІS;Tf5Tu1* +Io@oe6D: TBrgiC>ϟ/W;o-8dj=0齡q4m[W\Y=qRWh!_+(|k0 h\䖓}}uQ+g|=tmQnnwhcT;f}Ư|]ꌱV`Kgu5+jsxp?Ca)q9#Rz7_.Ք샆N-|-JNM!ʽv&?Z$fnjOearۚW7w?'8}jGԇA{ZB¾GfC*0[Eyˋayk*nrwjDhin(i) #9%@:Q8׽'ML4N]RBE5_QAQ&)FT(pľ ؇ Tj]ȼD0ފ-cMc)~G J dmLxOizoջ_6U%=ƚ^^r/(h2ۿSx^i 5|X)~9-H#;92>U,-WOtU-;y_5m6N^;M$kVĮ$T`.Μ; ɇ>z83#|Mx̊U_8Dp)IfR'=!Rh_rR>ls!kxN^!)i;u&o7EeV3CAroba3},E5\uhz#cK+1 $ zڬC^?[PR}%+>: cd"!uw@ +\J_N=9yf(qFK05QXFZ zK>~g>ģ=c$be gSclm@rwA+|KMo@cJW* uVG@XZmKz&L endstream endobj 40 0 obj <>stream +/R!8+oUKoʝϻe *wDhpƞj.e-J.#w??Mc6`3 ^˶_yR80wvg1go~Mc>(0Xq:HxF{f,+?C{M=>()$5*% O[LU>^;P]ңLuPpAs s;luyңؑr]NEEקQQ0ȡ}wE V(p]ގ}|9VNʸZFOzwVK=ݪ[60,FUDlN 8"?܎Z6K^ص7;1:0nSzW?ŕIq񞫕)酌VS*/tjDk$CH%Y7Ofe#ªM7~E@Dz}4 |xPzΒX|߰xaSȼwU1󃰪!*0j1!@~#=3me8k<사9TmW B $FҔ(?g/'H( r3=*U҅8zsm)NJ#Z8EkDÒ>[r{FeO+AT$8[IS#x;c$紌 Z8o>T˭VQϣ֖ScɁpVvAѕQJ ~bnqfDxOԖb5+˧@jmRӽ;bDѝԛ NB憥tRRdS"uFDz h!yЂ{IS j~=E|(ƻGS|.b޷E$D%vDF}3щAY:׷'j&Hm &XbU+oVm{w^d|eŪ1'SRXđ٫sGl"O`x~QRI1Hv'B|Id?SK9ΐ睩!8K3꣪켋H#,ЅNH*Xe^Ϗ-BdZٞ^H)]u7s +zݯ9IE/nwd47\L6wNQADtZm)OO#Ό✖rX%]Q n2t=fus6]3*J_^<~-{]q]ShoHS#&0svJYNoΊx^hI Q0hTg_1ׁч1߉/!U(/6]~*n>ۓc1JVo+bۓ:̚P9߿Opӱ"g|[y[clJ+nWk<Ա=_wf:=:yvxUf{cTw50*ᶸI0Z6pwj݈G̺ "TPЫ7쉎xB8~M->(:l-yl {1btIֱĝS~K%LA%,)aPKW>G V|HF.PX"'FZy\+׼}4Ƿ/rn܄}*%Cvo+Q\8 w[H|R" h ފj"Ѐ'e}m∋3"*d1"-zHia+:o,H*[Ve갲2b$ z: А粋Y>qngw`gFt&O-[(jG4B2ƺc*Ūoba^~x,o D"vsG P|KN%.&80⁙ie:*:uJ=B>4E!w7|Rj[i*WZyvvR5ɸP珲/0;E]+fW #1R\ у}XHt/Hd! A3I1vD?wIC%y!SC`.ohAZ̀){+xLiyfIg +Kg1Ə#NEo)*F" (VK o]{#(/:>֘$Sz|VYE"t㖰/R~ʽ2É,rnzdXRTOAN$dZ|Q^r)'g).X|qh% v ]R\Aޥ\QG ~.NciW$nR%2ЮzGǻšR;u+Gێ{!%q!O:5|FfNQXG[:e5B3-Gš2i?W+}VAe +a$|Ncװ)o 1j\?3&S"^1t` /!#,>B+A;-wpvP8reup΀rhobWOͲC<\`,TΫ? JnN d`'"EX2`AQ;\nd2vF7>CwASbC#X?Pt!7F Ak#Ź/) apQ"cEuXrϳ<#TYyغA 2ҥ80vH+<)?f{`ZV)nC?4Cg#;kg`skBk@Us*⫎e" }B"9N)'0o{ qb6q*Х~_B6a̦x-#E#H'yִ tTi#.$Ǣc֝y2$>Ңy]7yp}TKi 26C}q:pаA-sv1`6ԼMc27# lz8.vj;ك*TzGFcؗ'hri.-ӎ^ L!+8 +~Zf掎z: m:`bBҕa2#7WҼtz=QQp%fg&o)C>7L˛n@?T3yZRPQHv_!uRŦVW|$GD傏c,řz5L̾NmQ 1⎚6 +7-C,^!D9SgRY\XmV^%E =rgoZP"Xι;p9NPdĜȰI(Oe^W 2vx䟰3̋]F$oEs6LKL-JZJ'B"C'MP-}`P/ۜ#)~Ŝ-Nj(5iXzH[2̀vϵoT}T])a!BƸ|G=NyK5EhqWɹ(+M E[[ +ӻDVا1g-P8rE"]؞1BjBϵEm6H:2n,=XW:!cϧog5 Jn+0#}dAFtk!-f_.R8ns2F/ Fv㪇 +eLeͤ$p:Se^MsvfT5=}Wfn 7;S)>LZbM\ԋ EW$͖&h  .,< 7Z#!HM[c|;%<2гr 5Owyv +ueZj٩mù=>m6gC9]-%K2.`7C%-*Do V~RHKB<{GPØ~BlKk-->6vwO&֙;~Ջ ˞/P(bbSyX,aF{@ Dh7z4^ܜtKq)6[i>4aKEd9D4jX]. _g綟XnR +J(^ v$?%-p@[PXx0J"6q*X&O>%ŬdG$DE}JNM\ 8$Ħˊ>v8[ٗ wӼSHVVAG[GkWvVYʈ@#iG*l=*5D ɟodv?. i1qh wq #쎷sOk6{<ńHMh0[qb+?d\OCF ! +3I+׈9KFEE#Nr}KV;{Tkx^!t$߼x9X! D8 fz`kCسzCk X+&_ RƩ4fvȳ(ɫ +51uq|@|̋ |f?װb}0Z UsC!m]t Z5ĸ֚ oBg|CsUܯ!;ͷYKkqp&1w&ښ;9ܠ}^O=.i{-=jP}UY/u)nVXgܪu޾T)?Q(Jۨ2Q<ծkn8b}3f9q3\P9DҚ"r1'g=l q1C»]ioWh Υv܇נy +੢Trdtp-%]u_,HO`iKxc=#eS+{X Ӂq4sDY8n +Gz$"[z؃OtO9Pp's3(P7>y}y6dF}+3z@A;&&>OmJJ*w Q<˞WMKEzڷߏ Qut䈆UL26ۭZnH3nˢŔW-_yk>@ G{(zŤ,4Ht8s W&on󣿦s";[z:C0՚jU K8%?G$KsuhՃ9lIz:,yUAgBD_'WfIX +Y|󳒼mIB0>s熎dᅞl!G[)V}i6z!>z}$~;ǔ-n '00ApiE!BϤ:VCboeet'B=a~q#O4۶~Zx%G1z8Zj܍l֥O;CZ!i8QmZ S).V!̈[k{9`; +&N{KqQOh}}Xܪ#3bJU&+EK&)"Pt/-wlCG _X+ GkGX]ҍu@-]d +#X5*&o_J!'\jdrQK;];†cd/JE*'݌'U@,X01޼W=nce)n %OVqΈ4̗ö#X9mwAaBN8ERlxL[v;N/X +5iءHA1M-+|_&py$kjIJt y]hX +a#."i|P㽚4Wtڑ8"brDFتobxfwUގzjDnOT.^z+4ݢoJlUHvgOھ +sW(̓+T8,iNq֡#&?3Ӛ[oy8˜- +KdZPQ ݗs*!BB)B cnIsZi !py5n\k1`:I狕A~2x_57wl&,%=WN\,aOTg> <'lQq)ERaTcFHكz`q#"o:sQзyr<).ERF:g#9=lR syKu;Y|t0}C̤n~|{pZN!{|Mdm1?Z3{$GA/wagAb-?V-1{jq2O&0SV>o +>$D4@ħ#i/`->,I +0"lh5YPFrϞ9Dʯr@30 Xs=~ɑu$S +Qd(=zh1=G $CJg9=MB KQ%SP3zpI +!q'=Am5B +9\l{-GbSWV-nʼ#̇_)yTڪ:1YvgX6YÓ0è{.ɢqWLA:]X=Xg]JsVb֔,0%q #S+=t]ji0 Ds-v\8XDbhR 0sFb<+u0P2cQ$UM{Cԯ1Tt ԋK+] +tXز}tAܮȊWգof@`QȜk[xX\JpQ}lt)\vRG/t':}A:=t蓻`?QBYA2Mϧ=^[ޅ~fE-_N.R7E2{ziWL>  ww+`LZL64@4(+A5 H| ZaW qYoL͘t(L!;zzh u`ɳ"˜X<Vz,jdU?ۄ~-e }8Ɵ F,L'2ea:3͐3v90S8y_ď *,"!dtMƱ j)½FcI8݃udFq#YB7Irǎhk%$\⚎?6_igfS[7`%u0s`3*[.x_Yگ9(_<%*V}Fh*UEao7HfTDezZCZ2G$Oae[s 1}¹|i|&G)hHfOm;F"5$懒R'Ysœ<0^X\΍I/~L*;sc vr%=f, xψ17Q|Xai y\L;bt^3xXL:^yiy} #hw˛摑ŤCvbhY,[kmSv)Z*PÆcFG ')95۠$B_}Ǜ{0z iLc[R['cqA+CcRY*BW9L?Gzz:{J"CvPȧSE +g>n?>$ˮ|4knSI wQ|Ǻ}z%\D%%%CK1|b3KP`}ȴѵ%_[砨CM;::9T +vF#sI9,\5HU*G-=&w0oV!8McRkJb<G|2o,{|9F1@iAƌ`FpMĂd[j~J61 +eisdJ8}?&U4,|]Oy +XC:ÓP՞Y9 ܥNW#O,CkSԆZOCµaRUV+ȄR&M:.J2w[JdQB)m떑wV7gyö.;~nBy^cJ0s]$Q2mLGUUf>}V4u;U:.> +HW<{Ox `-43w_.m覤^*ye) N9CԫW0Rc5v`ׄb}$< 'cug>=jV=P8+NW J3@=ӽ~%j7\o*.D0 +6S2Kf{aBWIzTٞC7Q3L"'^U:]%<ٙVflipZ32o +UiGdc9kOˣuJ$3䄬"HqX:%g*G}[pC +nH^ju/9/j4hnY)rMĖ.(5`P8Wt (b}1 ګ^} + ;I9=4@ s81El:ouT 쇮vEJܽ$K[A'0 ]ӾPrg߿!eHmIʬtX80kV]߇l0ƭҘSb~)օ{!2Xwh^R-6u9LbԧNzSlyl W7(Q`PCBgkܱ \@i(E61=kQRkPe%LI%iS_;Ith+>T.*J-ɏ_tm5 F`A`?XZSJ5+=!o=H*Y +*]CU{p~o7ah$&<Ϟ?if/q2~PJ >hMH;xk]FC:3Up'2Հqǒg:ƙ6Dl( 0;o$z0T0Ia#З?aù̉bavVQjv}58K7I(yqTe J n:*Qi;b$,j` iyp' +XvA3~<G1m= +;~!;-l{>e~.ND'<bh ]os.ET~pnYL !FR)BB +`יb"kcҹ0ͽъf"ԃԃv$e' 8\[DO~alZJ: ?gd6/PR0Í3^߆ߴ^v m5zg`J$|!§x#sм I4OA|sR*?ZYL#]`vQȠBZ:,B~{[1eJoXY ,u$1y5lVsG1Dzb2Io@!hot]^;ߑA;XPq%TX qdPW +u2&Kg.,V ā?_b|Ͽa 9ss:/{CЧW34iY^xK#m}?FwubgjT7/zmB)s +݅ F0By1bDA M.۔0  ai6MԼ-IlZ|[MT6g +nULl?!B4)J+Ⱦې*~XylwqU*l) b汓;Yr ,7M^C `."P cx=YmyELVE0bݎVOc bo5*-uJRHCGO!˺j*s]>U޺a!tp~ϻ!v[\=xKm3O3DNٮ4/U[:$zc?CN^#ޞ=):@XF@NS4>{ȀhɐO֝Wm:`q,<-zC&d@|Sg[B9OK_=aj-MuYx\g$8euBWb5s e!> +0 \኿ntGqq"ChJW6̼+Nn4c0FF~z[C H$9[a_N5(]:?hc(??ߺSCrPaOYj]u'J9A*QZd׶g^9RWd]@ȴJ7^o`$:2[`3YŹ&G}KQpŠT?lGDtHx`? sӛRԛ4!*ߢ(Y:2/#WEk;%~f"HI0ʀ,O7e7Q*2$bLb[P CRQ^7.'upS'|Bq{ ~jG&W/dG=lntFz$OdyPf~,mSgS]r d:NȌ2TlAZw+FF63+N'q; +[TJ v;7mn}z*!N^}[ |iA`]\AX zbT,EpӦG ,aO&<W*l CT G| Zb)O~IrJ"qh?.#A=MXcl|b[@?9t@UVate: NaGNW1'!--s7N_Id%mR5P! }BN_>q97궯GXKYcuMN,QXm G{i<ő*bNeReoҏy%tE&}[ +1Cy2E-J? +qz>AS`G1xNI(~Ж}w1 GvOA^t7Z坫m~Dk!js'Z + T~%A{ 8wT1j0N[iSa/gL$ >'!3U$wϔTGeྪ)qⶣ~#9jH7:AHƝ ـyTظ^%u>p5WӉ̵ZFKRI>JII rHKD:4C%k{爼ٖXIgE Ql?_XX\r5Ǯ@C7[t~%v3`k_*6<(NZWNd4 + N. 3W\ >dVV t !AxAlW|>T=v"fi90"Eغz +<_|KRo'49΅gP?\M[8|+A5QH-];f&"^n@oG(e +m UN$A +u.h?(J[AJq7 i GjmU~=QHo-bS6T}~uz%fVodS#CwV1x-;JNTA$jܳ&_;v%>%b +Z3 s$6xIyk{H[SMTpN186F3^֩%G@QeGƀZj Pڎ3AJBhO֩ +pgnk( 8oe? +eKbF):xkŰj/ol"&޼e%ǹA (h_Yjh>)0ruh2EF6J\Zr߭pKۄ@ĽA19EUqdۃCwMϹ:^(1y Zq&zso7mWCo<3+VUfr">] ͉ed]@*g أv#U`aj2YGc P= L2CsDi-&\d|w'\oA=PbQ .u6ZGUA'TV՚Ƨ{$8GEۺhTpiY[d围}Wտq3jzb_ >S+tiPn/اъ@·ջqߐ  VhO|]a}; +Pܧ[|5hr ,0fIHw_R +Ngy-vU\K.|Nq 9עۨ4 ]Z ,L۲=ezP^D7Ǭ=YW}JNW⣞#WA2^t\īŹsm{eI=JV#r2b31Xx=l?;M7oe|k᠜t҄iw=3D3'.Q>QOc'霃z/O0io q/|ociݍ.Z+Il[L1wp\ףlꛩN5}UXqr~lQ)wC]tVk_ +vac\}{yȴ8TT1x,Gh=j8'P`;TvTИ}F r #[A^tй mmIqt剤#ݜdS=E0o¯γg71̟iKA}C:&)6gT>c딓 11y[ u8`.MIudIǺ'in{O|g ԡnM:zJ%roz3#^X 0b L-બ"/_lOLs8mlӭ_D5 J 딜 oۢO[mA ̈Kq$WfԺ?%bY:XM]#7_)_"wVn,t4RHgʰR[ @ phU93(xkT`㮕ͽZ# 7}G$mZ18nuCOzwNBt=]ωǼ o3<Ymxһ +L1[cSn)Sp@;kt-2#{O M(I^B߶:sݣ!}#0P#P9 Ҫŷ +PG +fS2 +拗V Fal4/<o P>GF Y!ִ3IR(C'0%F(R\p[9[C)C 6ZAE1^Gf{a(mHF0tB +{yl]DɓZpOrG&2?y9"\EF)jb> e>Fs>ykFƍJ'x򱿟*!J.&PUU{N9suJȣ o_ӯrAش u#_npǒl[9U Фe@ҕ5PC=H@lVW+wZӼ<-mx@y_@- Its@;nu1!q؏Z!6o'.dضPZ-[B՚CO=#X I$7N~v".롉+G 82CHBOWoqhult +۟J7=(LwzJ\-;P7mTRZS_vtDCrl}y c5+]t!Nq~ ӕ?W6Qic> ke$è* Fp CU1hs~iz̙ ᫨>;<Ϡb@-Bcx340Z'Cu]=$6z^`1K'|%vE_=OQlL9ߢ~dv0#VU sf$c99jF6/s6KX3u})b$-boeFhSWHHcn=A|p8(̃I( ; +CF[Gc_<03l5fB 08A70yԥcq3V,R Aw$NLsWiàCdK9臖T /V.GK n¢ r"#0 )wVӠGF!f8"O4x`q> +K酎% WLr)n<\][񁦉IqGFhɠYhs!jzjPAp4GTHOl(,y玾B:^Qԅ +HESu?0O3k!b *4n+qZ"$CaXH˜R('Gym z  +!,Ԟc) &Ԡ=]BV5}~韟|qwKQ UC"MjHw)qJ)yy WQ"Nt#tsUAw\(pz "o`,03_[9T64CJdDڨUH @hR戚G?-A02CӨ(jq|>; Fk8Dž},޸_߿ES>oU|e&72ϿTl2d+=3D/8(umѸݴX̹;v bدe$XOIİjur=9H3d_ʆg,YixJ'3-QW&fY|~DzZksv|2b$Z˫q3B oT)7$)|\+t~K]4Rj`zlo~Šr5\ me*Nln)<] \7)>I28EtczHsh6ݡE̠7ыn u)殫`HBX%|H 0>Il(z_DXjLIK B6!= n-Txr.=+ZRQ?E֡9)$7IϜ{ @UmBNmkn4%c-/x)UKǥgƚ ([򟰮Mt>3{J!{ +VΚpnnѳ_E"w Nct/렖 Hl"茊7wݩ + c #ޣ仝o# TG$GZXBϵ`lpܸ@"==Cpϳp +[gPCn 숫e!ži)m[Rͣ£Aƭ0sr$n醞@GAvJQ(r]޵LzR(kÕ~-,K[B(9725#1Vfh!;_Y8%i_L8JOZ5G-KL?IH6;>YyR뻜`U.@R4+HI pՐbgϐm;֧ e@joR8ˆ@UY4}@to9SkuUեh}R Z%nj3p̯i֏WRTl1Ψ]9΀@ \\X#JT,ub"|9 ]C k*4e%*Nu`X͹reiUde)%= A6ÃY_;-}-rè1C(tPXCzT~CnR*nN)M+~ _4@&Nbhݡ()xajDv;l'Yμbh@}m~ys1撒՛mDzQ\8]!TOʝO-S7>R,1U3)Ij&ɣeS-h|g ]# ȃ \nX86^%Ձ!Α,ZD}^s)grq#{wsYϙaю"0Ѽ[2$=ȥ:c(mR xAb8ߔf vGL,G1>+yUr}utyU_p܋t6+ρF ~#i.1xxXs_7@Ų6v5ۜ6^.awAwd}w@Fh%R7G̹8E֐ЂDHlzbK);IZsqđ2 B[0pgaes܏3ݳC>3s.?;&p +b촶x\q*)@_MduHx̄nbYoa&c.EϝHW魘;/ҍ=n6{'=Th侺E_3 mp LH(dF b+;/5Z*_}L''E^'@u^p^3nK>@f(ku=u +/9ڝP5J[DBe0]Y%doyvGLz/Krߏ'|сwG>iQŵ5YVAn%eN׫{H?GL؜X +$ADCy?LVg䶯!`t+/),9U'6%mv 1}>̢#"Um_]4{»G"[_A6 .ӡ'bÄz^%7Bx7IH`f;v%3Lz>RK* IfRIBL-K „7?[<2ﹽ%~* pZ%F1 gpVOD}a&5anZOgD 9_^1XKtp> +f_<^UG Q8ԶUVH'd5y\)Ít޴ 7 +e׊6ܹ}.0tX8[W#rH;nD>߬ B1)v/|# +άǦ +38ۯo-RYtaG(SgxBQ)&3Hh/%G·g N^`3)n'9S¬dF+FXV4v-u$Q\?=-ɿB`-wu2,T" }Ud/B[YیTsRoi^|>O=tLjʽ8@+,V!m_yjc{A[n0Oק:$*Ѥ|l]<؎@(vc/B6 ?*S6A-{Z 6- lH5BT(? +Lضh2TV#} <*Fh ¡$S+JiΖR%| H*UbQ֐AqKfPSZ*WUZ8j[T|m:}At4!,?mI3>AI|\{v8bP $]VC`u,HUzyJ@/t +7KJHk>{:pD9QF)|[ăw^<ж/h +^!>у?Δt |4}:x׬[f z#4eչFO xy#W!T Щ>9"_gъ(s?}I9I֏>o0M z$X#GFR +7w?aiZ%͹HЅ@~ | PAiOPRfn5&d4,eV4%k;z~[ K#࡟H#`==mmW (ڱMCOoG9JG155{1#|B=$@DtPz8rhC`A`}Iʼni o,ʆMz'l8VW9mAH0d$?&ElE?XMgM-sExha/A|f(0L_=XO35sHz3suTH׵<.׃Ujd8ENdfJqb@} U9K3o'fr~Ѧ`,3dǴ` /aunA0#tr6J$T2 o%//Cuif)/Kpuӝwޟ6[f@xR Wcg(^Qw +a:DȂ:/g  A׭G=)(^(OΣe(6OhwP=U&Tx 3hFyI qÎvM饠 +9dPzHRUxpz?NEqrE2 .Vhj`t\ g뀻?<.=wkߏcnE;\9Cpb\`;Y`(:8EywlG]QA,v~ԐZROo{)VAa;9x !rOZpߊ MHGa0K"yc:K$ UʸG4{Ik3?ؤSAJ3 M{GB{Gut<AG- 0z%:@z]d,|@3W37hdf$Z_& P{ G6G W& +Ps5ӥE=Z px3&B\ΧɄʶ;4vuZa[{LPy\۸U{W;D?٘\3iWI_^? +=`LLYMsNwUK|LM^[Pܽ-h焆ת 7"FBԔ?=IN##W0z‡Swzn15_ +Ea(JW^!?qʩW$n v_jD!XAR?s#&߷Lp|oGߌFa=R;W%O- 4B3N|-l$eyX\%KGo0!B4elI } A鬈oN{q醠0WxM+e{/?Hzx(wD{qF' _H$]y=WuNG@p_A[0)C=J\UCk3t#x*%XO>%bd;=1 udIr5=(nZK;V؈o45π {!M㎌+| dJ4i\D)yaJWL2iL>;XvM]&*-]񓥻ms$1=rajSFB'|E{b!kP^^ѡ;7ba%ѯhZeJu^j>%qeF ٓ WOk*xLs9`?z j4d#U)}sw#jy4=|PENGH-}u"8V9q{jӟ>p"8?C4yLGQC^0ϧ?kudC$M]p)AmEg#r3EsCރG5BϚ'v;J&:#d_yHʮ!!%T/>+g 5DWY:+=8o*Q?} 1V:dF1m)gOaD(~}CG;Q)1 ku%? Ey!UL)޽kI1`uʀfCRerH&_dУn?#&u_?7 +IGA/DӐhJSV& [8 i4?:Wx " 9]C6hw7Dd~>_TRo-yA +B=z**E$󸲛^bڵu^)Aͫz] 0B(k W'. 14,~g{i*ݵ~p֒+LHprW?q* +3Mb^=«d.npأU`"sTxz;¨Wܰs-tMGuR +߷< zOé-N*#xk$6 bWVjCzFo_8)OHNe񮙳`rT9ݠPdwxIzBxzÄH}Ḑt){GLĤ$3p+ޣb́5(\[w8Bf?\r!1g;SM #":A{ۃz9~t~o3zda MAm1^ȩy>9E1!)L9mey\_4(N&lTxIG9g)rɒgG+D8/4kl(g+_DQԄ垜.m]/0 L͂OI59ΨF)HGNkvyVE,?e՗8;>*$R' fLH:hݽ\q`O +5_4sY1$,8{-Đ\P,r x:J#qGMYeL;+;;kQe\;:BSS{]ȍs_ۀ$+ʳ)iLNȍ_8p}-"..u߃ɐatCvUq@KY$U;'` sɑ9$D>ړ Zhs_vq.MYOVK)@)MG٦K""[?\6zs5Cs$m +AW:'9ǰY*V>j&j?BA`8OFb BYqQ+ƛ&_S%Z? ˌ|<řuF'EwRx̠LhNr1&dZ@yn*]Nmn2;"JQE+yĤ̗j (M9$#/cNɺYY +ZMo>*XkȎEwlPdR +#'n9z}{/+ + +_Ī]Q2Ne\Fb yt1N$~_PWtU/yRa9l0*Y&<5i>?y +{cM0LFj$Quup+.w@0"|H[g7K<\j@O8M_LITa+Y Jg(:3;Γ0o w%0 ܟnГXrЧŁ4+fѳyC'=c$mF;XɈM>[`(at/1P̡rV])8/t@5[y>)mź_)ZlydĘߞxfO@z$RZ1yt,~i[62g5$!7{Շ] G򨭃 %Uߜ7KOќil\jg8wcSN|[wblv wW\j-Wo[as>Oqϯm!vQ< Fxf怏r.fwucC{LaMsғgý:Lw9 򡵶ȧP5UPfQZ4jKl*gTBt+R)IiB֓LNZEigs8tg?^>sdn:rfOx u9{uB!Mc P}+-ah(#9V=Y;~v p8uJ]'܇[APйE桤?3ք΢>h +4}?v@w̌}UrK x h-NVjýjOA֏RAܢ'HT#>'a5ԣ׳sBH'Vv6=Aicٳ,h:Ś!2o@*!%1"ò;"˨"T7Fz-!yߌƚ݁SW S{}NK8z̛$D5dIVԏ/ͯU㩲pL~lbt _]Zsn,"4_O3,I$j<`˘3IAaU4H^#kFS·`Gy˹d]wa z*)>+c3ǙoG`LBHʝЖ`/<)"f-!Þ ^*CxbQG3Y2%j˛ΣLY-r'%sa-|j*Bcvp?:||G]8GG4X^Y'LN BYUY'{ "Du^9}viGDyQKത Ff֩/ +b]dz {>'حe!dB-yu1Lգb49 +ZMW8~g pϐpV"~⡙O/8/⼹X?!%ʻկ> +OĂ^.F9l +eB[br)n@!v[31ĭQYdL3ݦ;|_)}%2d}o" iα{i@1Ny}('OA֭UU7u'%74fjO}D\@+~.M<NozdOC$+N0I]1JˣhnE8ʙ Dl&"3 z}N(2WC6޳>xDգA"d[8]uAEM1g+/j-=M+h^sQzΘ 0 rג,㣿DdI"|=xgw"ړ^&ܗHҏ^ڽb} I x3tT89 +3#U,iW3ӊ"*->/ǡ"|.ûwBi!m=Poʵ{._~|._nF{^_ә<b=yٍ\>,Ὤ4 }D%+:Oab9"sH8\q#Jd=j˘j_N\ߣ +֗6NބWi].N-^)>%#"  +Y J~-4bD~3S_a~SYMF,¢[xo:s>xOKEaQxmTZڢgUzqǹ+2HoVrF$&l + +XD)Bi9UUr~ ٘谩A\)Wa&=UwGkDeZ]h+iB7B*Ǔ.*Mм5Q<ОhVnŅ{_r I_rPt(r9yliZ0$[n{5GaRCMx۾jl&Es¾Ly &T:`b.^˪>U(LdOIىQ$ xw[@WE\=!/lCO&f/ce܉K `qw1Ck*8aiù#*0!%[G<39\fZq%̏r@SYŻZ~ 3vT1U@H3L*X܍ee#G`n%NN- v<'qsY&vHxĐ}/eđbZ2Eս@C><тxz=IN b+ij4Xj9t*b:n)_)MX_# Ћb"a|#75VѾe5?VbF_GƁtڶ53#?˷D__秀2JX/YuyBp'Dmߦ+ XC-4/]=ޟsn;8#z1[&x\|R sy'K 7H 'za*,/_%'#!8yY=8zɺO70&^6 x'FaGy/TMZZG{-HQ6aC8ik*zEGV%ـ}RefVnK9,<JZY +tSsL;4~@nF2w 9HwP5CE̦qFD#2emo3wFhO̿vI#2XI^G~=ϙ; gESq' njG<C )"y\Gj]w^$8sॢ:/u^]&BLMPU_]ߘ*HB^CC\}Q4uw!<}(HX+*P'ޯUQ9SV DI5#/pE0HNJp(0ɴ+Ȯ)Wm +piv?)Z PW?@# y^_ f :pԾһAc"x}G$#&-M|bw'Ӿ޳řN9>?>~1ĘAJ'!5%' {<`U7h3N Kc79-1Y@otoKIй' T .sh-!&>FWF0DqTY˺m^̚"Z 9poܕ1Йֲ9;/ϔ^ZCƂPEa[Trnͨ;>/8f^op:oK^ۜ4 06\TDi`0}̏j8e{]M*٭FHG\D v[̌e6 r^NcVA9ta{ /.? |jf{o6@> ++t݌1[* 'y=Ȁ奚08m><"VCp:F( i?@GIk*~/OxɈ +rW h9E0OcBes+]HWx$Ѯ<b#yUhB9=jOj]:h]_Y'aKo`"?GE9^ +sh1&67-U҂sj0 qE7B2~ 74+kKR2dLaɝf8A5Q*کV4" ֹ@ bj`{ 4)!i7/fD&eRP]Q9m[E7r;b;מ@$ aĖ[Ýj5YyEĐ(7$U8QJ0EJ{0FFh7`,˂EHȑ̸~E^a`W9WGDiWTo^?*DY=~8A p_'Ў՞T:zب3"3jH/;3Zg +5N#{Yqv.=  '5uRGY/J|#'Iȍ(ҫ$Լ*rnbJ Y7:-V\xHU{[Mj:tS? |o¼66y}> t{m+Tl:6gT4kóq޶(~?}uN%DcurdU*xay|蝾u>48-P+O,_s1+@.W496B>eR "~wͫ蓃$)%Ó4r9tߛsF}0u_/~Y8^Cέ:%ظA˲7&S U¤,'Y!x[D[usm]-`1y%  p o]lQ 5dwed{p}(Gr @lSo²Pր2j@IEg=7U#n#6"4%F +tWYDM+ULk$wFS$'H> +<Ϟe }FMn9nU/9Y/t.b+B&fVFǬu{sҘMrӳnPf(krROHN2DS"$@ /&!" FkȌ8pk>r%~ +bt=OEG#ߡ[ u=֎'Rp `}8PNhdЅ? +`jś[vYQc +%3^OւqeEhs, 0@d#fJፉ-ű!WѱmrN|/1m\GT:`S +4qlTP衴ҋZcj _}MMRbg\{(OMfvRWE)PCsJsHL4tUqS^ wND +ĴdptW 0y+;ʊ3UfC #w-a>Bn -,.)>0P)8QrGACDk\p]*g(3im\|C G|e"a!z0s {Y|vd]b|$kh<0l_^V8Cr_7&̤HQP4 B 9UbD%_M6iyG'<1e \5}-] X+8^u>0u) >7D;D>v۳nu`vI0O}}k097̑ŷڃlL e !4nJjX>@ظ晧ڿ(O+rDI )0WzM䖚tdUFLPނX_ "'D(D:[l0bdX`0S-Sw3'-xW'^HGĵ#gxTxBj4Γ;V"M{㦆0HĂ==L(^{ ^XybUZ*45r{UȅcĘt\zP`JBXv=9#r[P>V{P׹Ɨ[ΔXJF*.zLuJYws S=t7/7z=Qoi'J^~pjc@%f B8r+^%w*坙yua|xJP~㠎!y@ڳ{H)"s~;3Z\is#ld7]R+hxOJz42Hj' y{)=+EC +!u)ՀEDW?E Ix"O)ۖb]P5\oC.JJ2HDQY/DlKLwcXkϳiR;2BNZ {h|i^:;Q +a8(ؒo窋U%ALvfPCXbœ[ pb_o{|5(5j$j0ku#9ro˒TEAZ sA^a1 uVe=ױU $aW^H@W{3411FQ +U)^=첅Za~X) _K'}MBɼ]Q1MIe3$ B)G_A *v3Ax YZ)(Q] vE=@"az޹dYzHRnjˌ"u-b^2=eyeGoQ8dˏB–"L/|@ڥ0V?JoaiAGP|RVa֎]΄ofJPd:yڮӿ%vgrx +;esI,}5K +o; IJې m(uC+ŋ)2Q(l{b5H1lqݵ2Q=({D)ZR + 7;<0]})by]fւbƌ+jgSLmEc9ݲ5!©tfzz*";F'JH$G̾WKjUf|b+<~%FO!$Έ2]CtU&8@n6OvOsEj.&E zPJzG2{p{3^,V݈$_Ԋt"f|DpRP +tUk681IșL40gP':v:*K:9n/#6,="r0XoU&_PclU@n3xE_/#9KNF2,2飧*R|9V;9;`%EJt6Bk>~edNLY +WSpTw{Ui%}Oyp-YsH$Kc=gcin{/>0 `I9Ls3g]O + /yõ"6p+ͦ:mrF;V< o_%Q_V?ΦJeݣvMsrZxy"_L=-732 錼>G"8t'$7ϞUf-4Xc&EJoX9R٬jQQvk:/.빎Fe_YCjb +CtKXPUJvৎLx; PaW +% +_2\s  Ra_r گ4Ṡvu`~gIPyi,m/PiaVs%_{іDRo'a=!Cnr_sRLX#^gPWe甚Z#YMJ*<ΟhLk!s7^## +Gޟ YwȲ1=<, \#C-~V^If +3Dd֖uPPϤr-?Et9Z_%-Eނ%}v`x!(,pA- +p <;-!DVN!Tdq#l+$Rzak4/o0pr̃}LN\o:ibQ*;lIx C']+&"N?1bq>g*u{L (Dl9Gz(o||c-#(ے?R[YS_̧7?񓞨|BIQL9j!SgYxv۽I!l8G6[dyLJXuP]u;͌` `C1dĬv9w F oF'9SJ K0Qnu#bzC$wm_Zp0jqsPˉPW^䎹_J~=}N1o^͞xodCv{40Qrǝq|ȋ\jQ.%7|=UXD"ROt0ExBϠMYt&+_eőj.b +=1e*lW쏆RBžM I6?QCILO)lp`7(c& cq|rjH=l!FI1>~Lusz=:4b)Uf8aYb[YwDz{^ɣ$@{yTp)EY+ :8?[dʋ3~O!|Dk +ϜϰOG-UDx*:2S񗳉x/(#H:tTʯ.6Y^hYp3Hsi5gU_]gӣ"G?KD;qPX5Dc2Sh3Y1hgTBG5VFbb^?t_O,S{Κ `%LFq%NKwhFB'bÒh +3<{V*p Uu:̣̱b \ٙ2^\d┽{:\}d).on9 AR6v,'#n7ၪ1Γi;o #6b qZ6p 9b+m%)wči҄7r0ُ;#҇L@ Jl3.5[m=y)wMv-:;{TZ"l&?N,э"T0pF7VSy|)u?5BgրkG{ÚVd<n-{ -9#ens'c<5b(&59-5®xŕណhx2bU0`/$z}gܸs3 ~lă ?VJ= +Z:C';ڟiASui?ٚ^Tu17m›̉ +]3eCi)\&&;k'|ҝʌ]%ePV!A#"u׎tU+ 3٣|gȦNy r U?FMV b aC +nlV>焓;!ܢqKpn\ݭT_9 lm6V]lO}+JQ +FV<*HEGeWg_~lzhI)}Aʢ]IS..m +Ō|o۵ʢ_[P]*^#NC.bKGs(C\y +Ԙ~޼W8`"d&ؓ2@[OӄZ[lٴuH)AM$y)k@ Ie@sa'> ?e~[_R  l*?M|FpC+@l`+6HD/b#ׁ7LK6 -Icx7IUھ?[*+ +eöDAҨ9 Ch$x Df<'pE*NV4~]}= {M ^H۝ԩ# +>?/ڥYY%an nd#w$'9wէ Fo~_\NecD1o=r'8{v8yQƜg/CP vM0W\Π| U 5E!bW>j֌8\:‚7 etnԏ^i-HE|8vGI Y["jJz6~FfudH禢0p qz#G$H[$XoțEkLfJtNL`GFg6?Co_{ҦL#9O#.HOly,*"G`?K jmpg]]\w:i?Vi|A^b//ZA<fw=V*XW40dZ.R'Q/9{%̗s}aۚR3-ʢ= S78w8 Ϙ4n5wgAc@}ҫ7^w$Loxk,~fYm, P8#g'0uH"j:T{$.z|¶h`:Ex'Nr!{~<G|t50DDfr\jK1ÒT'A lZLs! tw>ޘ}B[YR4u|S( PJg':pJUUבVess>q)ӵfB)7pq.Z sD%rgڲhx@Y{YC\I#O"")o0W4èCqtnjU{5+ FS;NF}ݕn+sҘ/e[U?dyךƁnqd#jCF׏ Iwȫ֚tU~Jj3{=o>{QS5$šڼ' /.ڮHXe/Rܓ(u' L)D.pbWo )Vm6Δ5ˍ-3 hDǟ!EߏO7Y-S01C5C]ќFOvkބ\eKX yTnr-RJYWFkuQ1ڶ[ F01Xz Na %LVO9VyV Л%j rMԽkY\gf@%l#^wi]Y'HDIfY{&Pu2O~Ϳ8f.qlcT/ľGgXx!i& qfFk5BIf^YϿ1w1qn Eo\Um pcʌxA<7_dPLF_1T R"e9E "dO垃Lb#}-:8}/~[3{ AEwe +mQ[D^>Jp3e87ذ#&Wk.'hl.ٟxU]eV 1Awdmԟ5rKZS^(TĽqCO @R-@ V7fI,½x*EMy/I5ͩvB?h_>xP_㑶Sah`cYc&8)H8ȇ~l'gk`uR3v\LkjZl[vFy'+q3l +fmNw ­neToCezI$ZϺtw`Js(Fz^~c[6]\Qﯓ77s5[:G֚\ĵ5<--D +`?^eͤ_EG?t/4QoDM2oLbZ<:2itѥsmK;c|Dl&`$*љȳ!~ԑ=Y=%m/x8CZ EC׈! 99d3MJfcͬH"[:'Mg!*N5rRάs@ka3O)I)W\r| IҬh*z_ɖ$ۢ+H)c;{zG*t0/ޘ5V ycO9"WS̓8shY Hv͓ngy/-P9f)IaV)sybaCڃҼ#SӛQq}=?/spn#JC +.̟nG UCjcfO$[k*1x^<(ypEP]gUnu*\Xg BrC_!jAcD'~бנƒ[XЦ:*B {.5Ӣ૚i{} L)u*nrs'V3k! +{_ Vz| 5uayJ-1描q55 +]hȒY=1 +Yj^5@σOaxH39A3vJڝ_.RmIORn&5'nirI)"n} t/KOY/&<ի̀z?Fr |>oRJԓ+iL,u eϤ6__uU|GOl+FNO4&s n׊#{q?GO]z ;sDrۗ-qNAQ ~z~Bߌ_ƥ#G5kEߜOϝy|B1T`[7x(Z>=4ƜsD487yp #f~¡k)v҄@Qç#-L4F\;ܯ3tyJCr+~Ips\a 9/SW 3<-l eum7LbrL-p;Go"\!5?):~ (:?GߒT!C9Ox31)qsvt"ae`;F5GXqirGoq>MQ{ +A$\\welܢn(GϢN4guicewJ9ZGEY.F[*qY:&[}= 9 lV2Nq[XmTC CFB& qB!xeϘj*bgi /~[lnbcB󬛏,tljy-.g4ɭnM(}uS[ظ,|#zJ6C5Bb{-ipA4Iia.8qi-,VEzG##d> JJ; gS|*V(9h>-'#^ Q?0p"n!mBsgKCf +:dg5N3e_LOfGzin[3p&lwȮCh0y{٪ h}$Tu$\ +k_f N{ gu 5EsbR`0R(d {:Ѥ>jFLׄ1=82NV/bF?HQ{ˊ=;HmOE3LeK#ˉu\:|Ox QU(4eCO`)iciV v+jZE2|lT) _EtCà \Дp \ O Ռ8cbIo[(W ˜2O, ᮒN#^cwN6zw'`gDׇێvxP5hCeFhdLCs ✀\ hpW<[< ZQ #{zҚ=nI  w~=~(=0ed#K*SEO1Y _~SNyvR^J 6-"<]XQWxz지m 9y5qf]UN6H#X*Jy=O%zPG`kC@RbU٨La&ni1CxmhOJ/ Y~'xS\HtX q^ty +3:Z486:5" W,CKYKƁ W7UTO9vgѝ{qKG,ޣ"_flj`Uya`z&F}*IFKL_,3u`D(D_Eo\w + oR3bs&>OTz6:aXL![qp +-RRO+l /7<Y{EV %% C0 +5Qد| 9l57?cD}gGH{]a̓*WLKV! QlQ 9 ע+CZ +kJx~{4>k +ǫNNuFOd<6ǂsyQq;}{+5+'-ԞIycC!T{86Tсh&ˌ<ݏ`AxR{jAH2 +|H܁"ǩ9jUx[Zi>< Lw.s&َ'S5d\ߨ%H^HXG,J"wM@ix%o+}&bH ^Vc/=oD߶N=0ѰcL[I(<|60v(dcOETZZ +wޞӌJpRs? +a ah'>%5p Vfnhrwk)`fVQ$9o&AnmqqRQE[YJ.Tc鬑H's.BIX4DWh)cბXሒ4Uʥ `&T[X>)tCƉH5R-3`bd~>D.cNoW{Xvm>IY>A)ubm?STb혿˾!jCLM%c[CaҼM.BkC H:Sq䣄% !"=x3f},= [RQ= @]Xt`k:t#SYE%Wq__(9ԩKKF` 9SPs=qm=u[9fV:<u[ 7WT́0, +3gߪ>(k/xs[jF=q9f7 ]RŀZ@AfNn5g)N9R'=P>H|P({O$(?G;Wa7rI|QSc6YWB/QI +tB=R.^s +-PB|󅪪Mlaq s_o'!^r RК +^}ցEYSDnDl`b%n=K`d_;EpB8̣VB2{cp~n`|o ONW"4/x0UrSV71}1Qr*ECNK ZJ|&%9 ~ܥkujQAb^~U8UhLVﻪ}!+:q YOU-bcᰡyuvLmRlsڪ7'><`35tk.dDanl}fC@[J۝/!(1V*'Øac$'~~\3a}F }wd8G79& j苈pgj? +]GD1D|-a/|cYJ)̷bFw^mm +t\_y b v.$h;DjXʤ~/`d ƜM X5h54ʞP`SwQ0ծY-GvN1\rc#10H9Vg5?:0)*Eڼb=?1ACҁhEEX2}r^j+$D=g(րf5ih #6R&4vfP#Q[}~7FQ=lW m|dI4%R(_}N(~OAgѸya yqd1ϝr4ߑH4)K)ϒ zEs>8x.<#A2=EӖ +(tƵʟ>)V_\9vRɅ}t%Kdmy"F-Bgo`_Os<%m[ui ~թpFl!>QTX)]QotKT™peiOI'x| 9_dGom2Yw8{wVp(~#-G7 [sKĶ5ZxB8_ $: ݗ3R1~ԲBg +z +4c5Sx^El<@;{6W+ +*V/O֊L[W"d/fzŝژt5tRTrCH_(ԑlÒpw "ً%o2.لL1|Orx}V)R*.̪H\?UM*j%! LǩT@}фt=zzǫ)ow&%ܳUU2̭:ԂK ߲rV*H^Jж<,ŵ;Cy4ް#nX7&K, v(QY||8}/_':O&~pv"؎ @pw@m߃:I0˛.B&|+yP*.Z#Ax6l&|ĴD)zGJJa g6]6B^T˸r3fY٫6ٌbLJ.sPh;45=3/srjE>4^.³~OB< ݈~ſ8I6]6NQ1kCb[Xx;b'` Ay,Ɏ_> S] +"{c@R?ĕ0O)jyyKENGR8VDZ۪f2mMyw5^9;p$u9jW: +w 6 \e [j:<O4b1}')aӀ%-sn׃<\*ze!m 3)W@gZq¯#G(zBD SEZCp] +2)HMԫtz} +w|^9y +nz6!Dp#A>*)J16,JP<{Q(-G8\%+%5AN:P:1ߊJE ,yCQЭ@LϺL )ȶx-MU&$۶CNkZĤTn @UN-•XȦw3YWI6="{.BK(ANe+xcz9C@:J t†|Q*]C)j&D#`bxuGmV&2b׈9Tozдie/`)EH3Ȫ%=E6ƥ~CZ/y_-"E:X"}lT"t)^Oh5 |xf6gjA?ۈ-'u7)KF({)[8 9)xaV?j"3ڣZCsq1Ewrb@&{SN_ $ :˥``{gQ@\?7TcIAǎmiߐzI wq'۠+=l8-d|8q*+gI2&|/<ҧZܽ~ TMw-۞Ҭ!Ù +qr`2=nES(*i8ӡ:/5(y4"v%цE7^D+o(?SwvL=x%x|ILdu i_jĊr7q_*'H%$ 8"B6#2qru)UD ܧW0yܢϹaYc+`{v:RtC`[`ܣP):Ț[Rz=,I%~1N>2PQ>s _WjdR_1tEHz##%iy9^29 `(wth="ZDed +5IQR!=WAoX[swU:P2{UJ,Ei]E{Q O I9 +qm]]J޵_(_ +(+G <$Nٛ*VsoP Nmj|KZQ6Aj2{R}mЩ_CnZ;B9Xߣ^uF ߣIsW~DQUys5.0h GBŖ3 +Cط}~O:'0:3Zh!A7[x4:.FK{Ap DFoc씰5 z4 Mt=zKsCQ18GaX Ŭ#@EEԘSs9Rb}T _|Sؼl&WX_PCFE3v)L ~c*bvʗ%TުO E50;S[fi˯2Swң +PLEi SBZ 8>*лh()gkwvبRJmgy`۫M)L4(ZWwfvCVȱz9g(]$ +(u׵>rDW ^FQ6̭v[Ѹ^J99hPe_5d).6.=Mvh%E{p0Hzw:"N׿̷?rJkaTF`CZ*LI{T&)p|$,țc !"X?7%[O? iq]y^!5}>&KIT!i!H0 gS#aki!G +I;տ >>F܊ 4@fZ:N6GXAq;F +3we3I^_L?; lQ'0i$潎N(p8<\d"mt[[3|ocpc'RMg{ts$-͵ 0=Z#2{왞(J4C'3%;,!5䎤יy$8gH=vf|f)L`!-^~z 'l!9ߝ-wwݼo1]%3T~.$\<_xf T(6xD ?I4gr=^Ov IzFEp=E<踮"E bdz.sjh Os|{7h Xiv<vdRH8_4D_Z7;)*  +7mxbk vJkF-pǾҊek0{tqtIV_dVc mq|]D1aޕ"qS׻ɐ;!^ԂH;3]/sqTՐ HWb}OtWIfr_5Zg8IR{FAE)oTi&+HfhÂ3͋7~mDsS:y )P {;v8OLJiLRUs'`ImjV>VrJ~9"^@'HcG`\|bUM`73F"&\ExD-T5'wGUC%$ttq;ӺFEF{W0C6zHw%^s*dSz} $xu+"h4oGM+234 6;u+luŴod4C"B '( +%us }U~ `_qN;)Tz{QC6$JF3~zJ8Ugjs57:=qjdf)"38&5SIljk:[7gXHK)h4񞡱i~N]/aɧ17 yy? ^<SBO!l +Cg+fÓHKBCV2`0T, +\ +u~! ,4g]ZDG t9}>A?S,? g{ ]8^ڏM@MZ4~gd +ejՐ;8ܹÄzi%=n;]'' Թhxil#ʋ(iBٟuVc-x_jPA$J~K-pE'r iCz+;U ((9"섃,hB<8{+[HT88 +@LӊnjgYZŤ_)h^w:mO8wQ/ǡ??{l\p dhAo3l–2k! 6sc{{O-Aף痾-KȆ7ބVD9}{E)+|RQ0Rȶƾ-caIuN}sXm:b\9$س|n(O2}/_=hwDsvJ(݄lo|ΡvGrEnхOL3;͞'NeX " <!f'xQ5H1&A~^g%8ɝSyh 1ih}*RKXgLn熥<2.:_ Txd@لF9C :pzv ]8Q:zQVZ"5P|%+A~+w p1'(_%EGj]~ +Ym㥯y)1[4?bRKv +\t1v1 kGP)U 9nZˡ))7s^u#xb3Hv8䐉䙢P|w%&{kk9h~D-ҵNev0 Ov>stream +T?, \jCd~M8X;; +!OLYV?8)נhGLE 9 z.I* A.azV!>$ӲN¤|mvi/I3|hʁ_%M4y5sS 2VOE>WqaN^"ȦܦM<NU/zrtχ&B=Aw'ڇZ\}׈;I3F\X6pvy(ڜҍ?vm+'Ύ-(v_'nAg7li]j :gikK^Ge0TVJ_jmW\.3%hԞ[lIU oV>~i wq%rk.s>]9D@-4/ GAQ!*Da!OcntC%}賺.Qˆ-\IR]cƋsѹސ *ny =&<>skI>r- oX LbT{anuS*[zd# x,@8܉`G˟q|0Yo|5bEV"`4Q=UI8$~l_i;/&/ĄNY;J2YKZ/F8/)4ow T#lD8s)Sju,^v QbϴR"N1m\{&aa_F6*,4UG + QHǬ['W#}\vةÉwuPҋGxH#bbȁ>5O#nTN6/>⣂ް qud)e%!L3GD'tz t2m6I1R3 +WvCND՚W;GX,r>{KET^q}|~TF +:lΓdزͬ>'v0{6_6DL[^rźؐIp.tN{s^GјS,=1TL 89P"T)L,[V]\:#>9lÙ?ݺ8\ʎŊٯ;`nH:ZqD_07Tp̠FɍG@BvpHMC^ݳ*4Ţ]qfhF{|c G恇A~l$a|{ exTc$sH<fzdϻ&L7HfY(R,Z]Hڕ?cm\8Ǽf9ũrO*G +YtuNnH[nȤ IItO^p!H)Z + \jZʄҫ0Ic0t PC;#=c 1t G&0rA3x&~M[>yh +=N-Nʥ"|nlޫA">½ 0~Ĺ&KwNjP}8p +ԏ3V=qwjMMo Eo B dNe"5Ӷ$͖ >ƌ-Iϳf?bMiZwBs_ƼO>g3,{89PDdA"Gpk%tz?Dt &rcrE(֝yQT-Ct:hz3sFoLK.λ :46 s~v܁s>`~y8(7%^S9A(Pl2)(<_<x/ڋAST'hq4%bgbA#2:e1˴T[&ZD-bQ?o%)P&= gpGFpX/ną66Omh/Ҏ+[Զv3 wG^k31.Mp3dS!U9M0sxGssOjvm xx&-1)wZbG5"}znU{L1Yg {Tw>b0K [ n3 :FHUcYg_IgeuN}ܯ[FiOf" [V7{;N唊M'ÙηlE1vΫ/fߪ9S;+7 "=xu`G]!/_~S$upe<ׂЍg5drDr! ˏΥ19 *l#q}ƨOkҟQjk5X:٠!2g+7kErȍ:V5'PlQCr+ RGv]O.&F@H)G Z$X0q}7HQ&U `31ߢϚ's:udv z#m#'ĀyN NVO7ª 9sbM*`RBEk>[Sl8}6ڞ[e? `Fq1\0>s!B7rp2aejuF%$?Rs(@=ۆ~:qȝ4N>ב8/0hIxOIO`eE4WC"~-a<#i(ZG(Z%]8P۫992DX%x +C:1ܽ$[cp&PaAd[]"/a{f{O5U)MY'&zZZ.J%G;+Y,}h=cTI0OӖpVSY@KD땞 ķVj<3*e{ql{AK!!2dmنHZwbpwbBO~12u7u:"Lp DF%gCL]o4,\L5ipbQe~sd\zN`+(A앯uىiipjNY=z5r/ +mG *[ 5g+H)nai(6ԦY=yLn=|:*r&m ^ڽ +l@ uEϲ Mwg=Q=*#GL2-B!CɌ^„tMjKZb֮lCtqRCgOe쐃t.%Lj$LF1젷EUd8@]9E2ҵ.S`#$$Czd3iEE)'اy0 \;qV"[(s~z_d8?2Q չq.~bfY6=+s3~|3>;p$* u6g*c{ tчڽJ'=SIhy_'DNoXMɶsT. +GV%;"KKp!KK]aXNgN2_d//,8[r$n=h^m}5l ڔ{󜊍oh{ qo3oyDNĈibR +Tkzjww@fؕ;qHdsk_AcҸvũ~.]"\+Io8ң|hGs>͔lLnjѶ+W !umUftOiִNٶ;Nr@R]Rͣ^nC?<~v-5~Q.w _YJ>C|WtkhO Ď5>A?/F!jO+(+2E!I/b-:5tmSs[ݛxZl!IIegY.JrIJb<[6oftCTS#H]`kpvu{Lu&7.w*(S 4W~*mOUݬ\F +%Xw=όጽ^UcBsL_ +K_/2d仆Ph<f`s\TReOkz[ 'Fd\sޖa;iƯ'հ6KK7bmN84;/wJF4B(d`aaD؊A<,3!^x{dq#a os LM GNuF)͓!Ġfk̯!&*(P#@ip;4yP[hZ j=CL +ҕSEj.A |z+Ԇ &)/Rk YeOs^^_vPb.fV셐`Ku>5FAmS)oiz.*x(D~@^3/kN`;pw{})6kJzn+9jqAUNvt│i/OVIg~w=um}IȷhL*o.MU~",x;u eK1zg[}ƇaҡN|y( &PɆH].wvDsﻨL JGn2pJm\k8;: -'Bk[eE5sVW5Ih[90=y/XeZv愞; ,N R*Ia;,Y#Տ;kNƄQ}2ϣ.+"$Gf+O{7=le)0jT‚zKQ/Պ͛ҀUsJы]#NhJ䙬(Kmsdq +v<̴7 HhGm}fDL:¿;?v'bʚ)'iQ(nID`qdfI0oM~8U@J?.)wJԀF!ங>b9*(gSq#\<i b|gkrެd0R;szRv3P“.41C +Ҿ +u3Fd )"O?[P; rZ %Ol7UTA}Nn${ɹ|{CVwέObNs+kC0 Ð߿?b^={oQ5æt'`CcNGȵ`kꆱH4YpoEW9O1l 玦.+|?2X:W%(5nCaH#ڒm Grƺ ZD:'4'ȃ=@E F\*xxV@*Cl r)Uk}w}-PG|#rjGy R~wAO{5 \*TIeN3^y+efȧ_eM$gK5Uv +Zb!2}e9 ,1EQ<$m[$ęY c Ik!>8$yC;"8 :]Co{PNN1f +,=jrl2:U;<4kY0Iz`tTMWĺKij ^Ǩ!6@&hAZ}N6Pw#FmIm?6B*HLd-? "a*k'^-_)'ѧ vu'jO@KVګ LfΠ*o;A!w$!o(+grѹ# .DJyDB2Dw?נKk~ard/HM*3QA[s#yаzU&vD_o|CWЋ,}k7^/b֌z x/xgjmX3AO0@f X?0<+ ;;w ̓gՊuI-O8zAGXŽD< +D:6I8Se٢Y.C[|O7U Q +K(#ǛQҮ![{\J뻓ص7}79͏z{sIvxa4kNI8.OS1V;/DW Cu/wW@GA`pz&E| +067+^2^lf}S5p6aYMrrC`\}jE{Mt}W@AJ 9KܟUҖ'{UF`ƣm[~ݕ_/u֤-ѧU"49y+$mѧg3[{v"y itdْoWЊu'ɽz՜O2 >&wd A]"?mOwTI+%11-yYͰ|) H]W̍vOuZbzQ0c(\r^ !%BtV%aZ9$G=DZγx;5Du"tz٠1"laazf7x'yޏ&X׬ő5#1Bxp .zWOA{(˝ʳp9 +e˵4cדZ G0Ta<w #P:'nk* UG9X]c~=7|A9# OpkPTQF('iRmeF +pҕ$ay$| +xW*Xw9Vf{I|wQuBe2J]ѷ"EIVx[9綳k<grB2Xov<_;Upnjx{( K0>5]riP4(>N(6¤ؙ̛?ZI+P zo2aGfPiݥȜ;}U0X;\T{ug +=A@+Jz@OݲKP{y88cåg,!W5 [b{bV#nNıFgkAcM,[@3~;JXw~㈒Q˶&N"k&DNJv n aめ4}~Hnx +ޟs2LQj9v{?{yS= heҌ+-zdbQ@++Fwj؜ +|$Xx{RK#gzZ+P[ﰻ# Ɵ;h\ulUջHWa] +O,rۭOH5dgO6G(GAh+ i{?_EN^_𭧬cgBˎĐg@ZjDzw!ZH6)ٞ*#[U~z22 GF}N#ATXf2F-L #止}թ4븒TdNp퐏*̛57"Yr͸2|L9aUy跆~ʘ5%%o5vRm?Hgu%imZvH`~d8^NO24>Ոwcr(9 +G{>brs?6'19Yq̎C4}XAcK ^XJZ3vRJ)x7%zpuHoP3YwI=59_omPsBIZ>ۏ r.t+S5啛|C ּ5d-š|Ϧ!4GK"?ɒQf1 `Gѯ{#Br_"o#t{xQe;KoKSy/]Z}_}F{ znJ+ŁR4,9ۑよ, td넶0l%m3gM\  + 䓙"Kq+Yկy(=JF\0-nT$*J%"`ǃsԭz:x*WT o#nb)߯HrU}T,M0=oq8AV㈨ZwgdžМzg i' +ڃx\yY~17!]mK 6RYtTk#ܵ1qvV@h;eNwLzwVzR5U9۴zD]ϽT@+$lQg'=K~I%d/`0j q!DQϏ6Zl*c֠2 7ڼ^ 0׾|+kw{J* +xz-Fb)ajgsK *R닞_QFщo,0RÃ+)hHGBYHt$s|"/_ YeJϼHlJ^p"D2C(6(M[K`[@ +%ǞSd^[Dzn!]٘]s\-5CSޔКH; W/l$Hgko;i;]_Dv22@ći~2s[l٧ZSO!#-%d#Ȕwk>}Cݱi5Na=בu(wC7bX +郿)Qo25>#ԀC`yd;62MFu':(ϭJXwgZ[?!,%o5DT7 +:}! '^窟L)f~ubD/楴 DU)SmYLK{UQ$x&zͻ9R0XJ~dJ a:֐׳*J[K#zGvm!k#]s-ds&~YDꚟ{wZZl|3InufV't"h䗶)!Z~#Cai?b~nǰ6 Sȉw){ҰѵHм ,?A֋[#ETDwt H#8/^@j @:(g5CLQw$1-IL5_n j +8v/9aKUíIW0:;hV+-jBV+#ةwԧ>u9~pWh [ R0״R+ـ_fx>Dte׫1qXׇ\D9X8FM>Hér'b஺ 4'"S# c)~}?s= +J}B/;H~ycBӏY}\GTNu]>M")PmʁYW zGYan͊uU刀[Ac3ީ_Udu7ʔyn@jne6"a1%O`}\3 +;6V$loEC4,Ɨ4CsyʝE׉%]'Tv|uxkҹ^qngT_M +A_}*=٩OU./zÃ5hs'?4Ůx fU%RYkG" &C>dG3|T[wcoܖ<7IRYH_bj=23G5cGӖz{fN$xVKi3%&)Eiܢ͕;Ny',ǣ/:`I\&$1Vt-wѢhgy|DK+;Mc^n>,*AQ] LfqEfUtIZ] >R}JZAgw*!={cVg"j/y4gDP&F Q:~y=w]wGn /#QP؈vH1߳*53MUBtg&Ģ'E3,f#BjU>Y9vj0VKHβ8|D36Dɟ,ErLpG _Fܶz^'> " +K.֐|M _!ӡ4i X&Ei[Y<7uĸ%&DFPA +%Yq6摢$"J?g(hš7tnsdϘ#HU]!2P{T54hK$ NP+Cv>CDFNOn(_QWlWlsAYwDc]Yb},gj;N0z ].  s+ڀlΒ1~}L +\&"^gjV]RmUCvFԻNS8nm"Hz#q|e>E$])hu'I((YɧLR#Un/}<7,*ot[d %JUvǬ]Cs0Bh6uR˻Z* 슭dJ^4:WUA\?B3tyy5)#;>KOXžrnprA {plbTt^ +ohKqVS nx|Z{13zO93okgߚ%kb&]έu&̺B Ա,)Ÿ5D)iܨjY5Y]{G~Yy4KsYU.J1ZzHPSQ{ϝHY)gK4/.z;q64΢G鬪z#ӎE>zu7*ieg9&6Z 1 _K$אKdnɐPnE<*viČk[TX>A@AQ)j)]˳ʙ>!bZ3MQYaW|'>V?#/pJ\ԭEh{*ʝ8~IT1R9݄{Ku̎GHfoN4[ }P!<䁃*·Z:`4Ďmnlys#V"jZH tUC*a2>ZgkD|sw7#v= 3NS|K7~fD_Ub#oNHuT j؎؄\GJwt:{xI2w +b?)qh<?5ͩv] =3` +4z³@l"A^+خLS8}Rؤ K{k lb=OBHD6&K +h >,QȥQcƫ[$9 >>29g@yDI?AoT wjg6AX\SBwtAޔAΨ*yՎo Ţ7ϋpj4TVPq(x+3GO]X6sJoR֍A:9koxw ť&1 +4 c7c"~H1Rkt`sP\ Wr-w-$d%G){9R HgU؃#u;PВNU՟qd:f'>HI0 |HU|D:_|ui1j[˽Wu$əWrsQǡ+ H{c~ϹBgA:ǾKµF;g{]fΝ\Pf)F>Qquioy˰\u65ZiǝoIA W++u97wRrpq/:us(O䂑0Q eH.ZMоT(\[=u货!}>(-iy*X0DgȆ!]47 ֡BlU>LήԤž\[G8V-) xk=IS̚^íǚBmw0{7;^xY3cr+1U>Rވa&քSb +Imui{刻Uԩ[:}v \f- 1-LA-__R2ym~wW +^SJy(=+C tؼٳҚk'Ѡ{0gO;D%ҭ%W=Ki 5d$p;fxLTJ8`V꿡'7VMPGU pH8D0-0znMV_f{?#uUS> +8 ?8ړ^S8.ZNƋ%9?^ _x.kgH"+Em5GG!pk*ܼJXyHb:H;c}ԤpOQ|߻s~̳dq^Dw}<\RG|^s XBcc R)D.,q ytR`B'<5v_s"tKYl=LE&f E1;Ie@5$W+O"nJ7–f羕N9 0}O !xqlJ9yj:gZ;rdtA܌gO6)FɯxĮp^'襕k$s gt G kGA_b 8CI_!:}@=GSǮcKE'ŀkJxa5Ά>3ԝ%EP"!B^liaθ\aw`&U3HLL=B!&ΡN X&msvq i6O 0 k}/#*nWL N?%||aŇ,Ѣ0w$SW`fp&Z<`L8|Ou?&/D{y6Ab )D?"đ>SAp>`4pRVCQM]48 Ӥ9PiUS[7+E$̃拱Î+#xLn_ʱBI]EwAAˏؖRFY-RAs2"-Ԑ;u_ϧ;0_GYHA,Ps]tBP +^Uá36*׽gL%?"Lr~A481R[`&: +T,XSdqN 4J;'n3{yW7=Lwם9Ϡ~f9VdjTNdZǖc rbB&V,cۄy9Tܼ- %ַ ċhB]-p|3wYdT+V;8 +*5^wA&qTlAb[mkQ ߉f21I]e*X;k ul?Y=GѿomZgJ[,`uxy-dOࢰViS b@E*gkRr^۲g7c揺9-ɲ6 JU_7@Y~#& i檜 ^~`Nz W(I>*?U#q|VU!#O $C4( TnK4?a%'?S1ژs^yM[1ZO2JW:3 J)UF!<?r+dǶQY:{rk0` wƧ$(R=l/JXs;#8KFDF!W~ ~K>ݩ)t2-w+I/fQ2+R +w{ʡS| zT+n=], J¯A3dC_4hD[ぁ($UzS$]|P|z*=gISx:}Âa=:U SYP@'?ZTSűU; >Erm`2ٮ{$0e6Txt %請9!i[06&MewY+MTRx=WLnV%S2kqEÔA{J+G @lr,(R J֡1'BVA~ +Π}tP< +7:\Q  t N1b? Mau= `VYsT20ѥJݿ։30 gTS&^lM2h8= :<5>Rv}DQڝSjh/4#/cV:#3-"Z=F +^M A并܌l)-G'u +ЀzOJ=5I'RԗZ[5!]fc"8|3[ЮgXF qʐޓ0%k@y +y [|0wu>ZG+ezG~w~kRipy[т9DϓGRt.3ӈay+`'eG񗠃'+q,cH+g_l7jlɾL!2&@>v녌' q^CRRgCu6 KZRAL v D-bq{Z)Ht4PY44&%TY VB$6wf>>ȹ:~Kb/r%m鿱j? 0cU]`ß^J U j8Ed]~J+%'`]3 H4ËwKP(@,^{ݹB< >_P9BAΠP4jJLi`!HFSy : c:w+r8u*vO_q>^VH{^Jfɇ18^AfEbLp[˜Jt2kjG1\*H,IIߓ(IC;kqTAϸ% FaS¬">A +K/fqԡ^!cqRSP?GL+O}:4#-M!0NOAb-~,fZqcJR"Pk%̦q֪#aLh I<:+h %KEd~E̶7;.u{ +Ր8q2$5۸-T)JkrW͠;ÃJ½s#oSC{Ǎ(Agg:@d5TK2$"`D+hu%0йR >ѢN$wE0 ʑ| n(uHNJƟFcySJw}sm3ѯrC*T3 ܁ ݌ zL/3rU 2G8Ԏn zRCQUYV2TXUwB +3=ڄ'n?K׽8tk:rxx*hq\+IO]%Z#0*&c׏6 JƬb,JLVPځXD?A4=GVQQӅ)xGYBFQ`I~Q:_2)#CæVZ33YB5mB:.^>&X^Eyxi#ۂ%V "89 2!$Kxc [nɳ7/CfTo@4- +w24ܙ%>"ku>mk#H>zX|ϭ+#.5(2$Ug !HT'z$/ /ڮb=k=IeGyq )[IE1x(;ʬNk?x?7PxNьj~6nZbfj]tb`%:u]>>{irnccњM385rdx@U\z􏒖mAghӂZ_ӣP~!Q|URQ/u )hqq$29TiMWRT 6Ĭ먺r숅7Gt"3e;A+z46 {ĵ'kO1K$*cr>v܊ G"lkǖ'VXT(}'eѐwyW\ᮘϴkOwq2lw n̋AIVuGd# +ڙYnQR JerҿA3BѴ+l~}c~a8'%{uDG6.5n%Xo\^o%%tfb)j +Tuڝ,M:֛v4+CIy))wLK*!{Xsh'd&Mg׾W|7vB>T 1(L倘T[-KI.3k()qW-bO=j{^PKW? 鎗f-L:Y<7ԥ4nVm"(x >㧎w~2TM]UF6^9/>}q jJ]-pjӥwXeK9#FpP ߺJ-'t+m ʸ 7[ث#"5npFSsvYS!K;HkQVg,MK[d~R5۷3X7%Ms֘T|U3~̀2jX[ypy)֯Qfc1d Tj fMFr0^ׁ3tw7؝?b##ᬬJ'K=?ۃVC߃4z5׆ƌs v C+(GrR4|M4:GExTXM4p(dZ7y * xwonCuDIeDB/cu1zram Uzgf[(ć:<2UHKm+z[\W\FEztp0:/|L_'&vSVMAʀ`u%FTn¡%wfq @W`R]qE܋+َIPuԇ_^Wה"OicwΜr^bDjD{?I܍JSk FgZM }NQpR2W3v#=ˁ3r0m!8kyyb+5:Lc:~O4$^~]+ 5{{“7!FZs"Yb1/tF +9N'Ae/t٧@&AyL=[ƹb'gSM*;RYَpz(5 +Oz7dg u z?dѭbn}lF>aLԗ&0`HJdV^&d#3Jnsr_>kb, /uZu_4C`.fhA  l]O!e|L:KܞEhto+otkSNgXvR>`zQR|238 '1B &.i/i7A.Ow2}J$z#e`(^8l~Ԑ9`OR)CNn  vK jHr<ħ"LXI[)Svا 5%wθ?k*ww6Ur>qΰ@?0X|6kQljzIR|!h5I7͎o|Op T>&{W֡q j@(<>nC,;,Lt1fA1b8䎏YC0(l\/9 +A"XƐ3Jqf\Mڀ9lI?g%m31s$M9 +_ʁ~7%+ ,,B4'* Bn:(s &^\i" 6[/\?-ƮN3hAL(Qx$ѱ}ɻd[-Aq4K{-5}BeRby|A+hZD];֓Lqw}F3"cF8G r4CFU%aƯ +|47۵#+1 o t ']'wG;Gxr[ǁlZW#{虺(Hg7Cl)p}wI|Ah/젂Uq=E;niR @;th]ǁge\ ymOexᏽ S*'F T7]yޞmQJ!tֈ XXFi@:Kn YfLˡ-6ғ9qŐWlAkgzŨkmwiB`ŠA(Db6;~r8BH=E :_lNkcM +Ba4}*; ҥyJ +/!Me;J_t_ӨVh%dߕҵ]{gklxkّ7~A]=F;EbkM[/A..Nn9Hxi_1NƋKteix(&IN򖆜I7FNU s+,8)}Β:}cݍm<ʭtq%$s?/boTXŭg➷^qJ.Ӳk\SVQbVVEPwzMyoQ+K(3e!Q'>*_1<7 yFJɣƠ)O,0{*IW5BIf SVfp=_0CfZ2Z fPQ+9XFr3Uod~lwfcV`6[Z%6,R<ݧ^z7{jWt6 qޢL#o[hZF7xOs?@oJqda'Yit7KGtګsQYd;!W3DQz]Fν0j Qm ^jy;wv{n ԙ?" +,{AVd!L փ +sуԐ3V{GfZCf$L_bÛDPĢDR/H#C hU;,QPQx¾0iLzmJ]`@UW}-<<j)_/rBt/L_vqkRlZ@7У"usIBmUut\@gW5˞ 4[cMX|!j뤏T@"t55[Cbhx=S R} FI!>/_7Fcщ*WsFbebw_rLCީYtݡ,+nyLT+i#p2c(DY D<25׍3 4h)qq`_A]/Xľ@WWM{lc\Z`2 :4 v06$J:صUooп;W!Zd74L_' HLrNR/C~.pn(#+~}'i]PYK\C$|5UΝ1Mj@n!x0(hV'9c>}^{xRpP(J[Q9!E-1,AoŸm-"RV^,k& m*@+o?vDϢ"5mW\CПG8ȽYgUR9~ք# >D`нu;3RS?\Ȯҏ"1 zXJ 'e-D;׿R Эׇej:0lFT H#UxI}:/s)~,OqLB0,۳'?̧vE,##GD)UM]ńQ|8f|/EF, ْ?Mߦ5;,N_c $F5Y4O|ǢKEGǹB ]LGV(DM$KeeMUݴU[\@׎{Lv PG:kh 3 ,Fs+ [ sw}8xȡlZddN !'h༐s~lRgǮ8 mԉR) L B2!1{]~f8Qcsn3t@Rѕa˛3Ys 0"N,Y(]15Ĝ!dt݈8ARW<)ȁ3 W +FAUvO}7ZQD(6^^՞ks^[d +UdLb`n#h1ߧӖG;a} ħ޺g.+UOOfKMy]M kC1gK!IU<&NjqH&H+()'Y (~ePgo4[DH 纗9ZNxl')-6 C5sJ SزjJ‡I3B&L(>CD_Z ^﬏khhEω)(rn) $KJVkKLkv[~O Ij6`r=CFtQT]9bĬ"(Y/<dVW|LP[ xPۏb%~O0a:iMp h7!hZ k"CP'×gR2[[/ h%+͡;" kY%3S+e*S}ML_x!C#>c`/@c§J|w n +w կ蘫 O& MkTK;Jܝ|-::jҠkJ{%f:}[-&Yo$Oh*]#fz|{5E灾J&GΫA h~rםtQuGcG^*Q[oIN"T `оԎbM<@pzO~-$hܑUZ7aYg@G?dfF1 3p$G5$s/% łjc_ %).uk-I~Z9CLV^ϵh/|:iGCePBsI*Jӭ4*"!%YN?RajHBMT\%H BrBÈj}= 4@3o$_:t6m(ĉg 7; 8lu'|Pm}T4f0[,qb}DTVo4 {:us^ϨYO`"05t EC< +s<vǓ4SDzV}&Rfb옮epeN(&Nf豳eUC'Ğ?EۉpPKw医LjMQ=zOlgj+B9 .E. ygc+蠚&f]2NA:< ) 3˱70j1= YTO3^p$6*Iр0ﰤCz7ܠm3osei[~*ׁr0砹{ݻ.蛂=lg?bP$l=YTc "b +rEuo'eSAk|0cCZF&+=Жz**@d5X_$hˋsfϤC*ڣ 9-ӌNQ6cdAW"k_ď R+Z{ 1}k"2RsDGIL!*ч(w\-p96Tb,tHoi쩍5 G=unrأ,D{>#@Dt{&w+0GO*\w+]fèVb&#γ3PJIaCDŋJj{9m3+zrfk"G7K$jDJyeRÁ0]&"ݨФN)8ie%Ǥv)JQLG"{W~*z[`FDPJ NkpE/ͮ$GsPdg2RgD>aWWdi?_#^\O> >zc$>"=zq@&2 +>o"XCdysʬ֚\٤$V^Uv=R1\=s|$背W &ݙZ\-aabrNoQ<[qU閿1O9d83=ڂPA|WI\ +&Dnk8};bp荤{iiGKp1|j_|K;:] +DnbIvO^­]{ WRʯc(&xXr}]޻<@Ȳ-~!}.Y'$)ƒ 9Fx`t )I5H-|]QPaX綾c6F!::$8-A/epS +րCy:T Ν/Fqq[)Ғ V8UV˥;y +_{O])4Dr +nMBQ^~4ѹSƁz,wTrP&Isg:"]{_ћtKw| +]QML<8xS %1k3L#:sagLGM +"PChAx}'r׷jvʢ +x3",;6*|#i! :-H7;/9O59+\ YXטk$"]$t;v J̉AGO 13#5 q4|rӡ6ggʁI-/];]z%ՀhJ\4C{ 9-ܿ֒/x+?q`_GK@0*[sK_XenS⑘!VA`P/l"ȯ Ӎ\u}k`&~To D)w}c~`#TL5v?+~J,c?doִs1..dZl)LC1+cޒkױkіA;H$S.ESpJ,rcp1vc>ޟz#KfG`}Db)x8^rc3uО! єT!TT_HgjP+ +!!: .4DgwJjx4@*š<<:ȹ itk|~[$_ksGc3?VC2-w{gR@h\c?р6B#Ѽa3 dI=U+oᯑL*&ˑA:6q##OqW6:/-Z3>R8VϜ9wr LޏՏu^CmE +:J $ z_vY$DZUYiU부%{8<ERgx"Nz9~ikLKqwX +G*wzVwz5o]K|OI㯜JǙ, xZ +a.||' +xIm?`r^Aъ{"_IuY"Ae߰Nay@o +8 +u#<|ˏZö̱+oee2w [^a}[V-?h~$GZoE%RJ7XHwd+՞xM5]n(Q?2-RwzFDo楕\dFcbj ?t&e;lB³)iu\2 E>G:$%lOG9% "O])J/=On_# :SRpD‰SS3exP>3T8GsgMw Nh!7*bpdM+۟rEYqjj)^;l7wG 5@Wao?Tċ?x0&JA5}E{DDZw1sF:m% +eux DoWGn3-3,9o*_NJn%eCD0_*z'-"`%Dzru 5o^j|FLIl⹞=O蚒߯j?Uv'CHH>!@IW`)x(/j\l% `ozh8%gRoџlÁq[Bt{]% bf`#ek0m%"-7iKB #"@xbq%.w)Ԇ;ҴEپ>5m9v<K +l!ΫԼ SӹoJ3SÉ JZpg¸&kG +H"3&*H +U|M?χQX]mN<re#;mG@OJ`6fDZoQZwvNu_]VAɂvz75ue>˳J<'j>l64Hvt%U#JhYsCT5JRB7ݹǁ*)hG^QDTF4//6[ҫLE7nk-q}`= i*t( +4|IԆhF*S*lW+B-`XOvϚ&@> +]26Fw/y̼'(7jF.& +&L(x^]yE?X&|C;8]Puw=9+<=iI5"uI,/ے@byS}咜4JIW0NYܵՐDq >'jvPt^W(ϸoz%UGz$_з!f xjM@Q +pq;laFSٚ0A&G< ES^''Sz}9> q5z +Z2SĮ^9I2 + }]<ϚrQ$MKC*ʞVFk^jK6_0/1Db'+GUS"udGQ;r]R䏳4H 'x><9(u"Mzn=9-6/dK A; $Ӑ+h[xeh1.wThndzN%m%B{ͪ\t;umj-{ aa< Q*ytG*6nS.;FRObrÝ?wdzRgz_,ѪVx0o^qF st?V!w04*1ĞU}j߈?ORH7DCe[hj_L2)K7gb~ԂB빂 +K ԟ/&/z_Zy(\ޠ5IalGTa_KB^^JNE_9W*q01#5 +ΙE?l'b\*p-}J7`P77rc2bDb1faTf-_9;'xqHCISB(1Y_CRW])A-ҳ=ɾ>G8ǖN:yŐc7&߫","\U t`toO>T}Q~apşч|'P0[[tX'0jL[FRC+&0Mp̥'0}wwp{߶№׫-{2&m$d@Cqv]H%̣*r12$78r&= x_(]4$usCHGӑff~= oF6 DKM3u$4vLd + +0w~`(@EPa* r&sͭYk&L ++;ю޵R폖XaRv !k-\2( lD=B.D<\=H2.ݡel+c"GWVans{`vTV+KX$%TVKm +3SG=`}UX{4*SzŒ^圱BHb6/2S´)~s]G,B{pG$t竡o /#`'XMB91<s +!ł\xcBrOO!${5 ]-ųN} 71Y(穙qĨf6 + (5oU"~f՞1m4Qj׋EA*i=SW<s]Hh{_4זj"Fg'kǢbh}ScH&xz )ѹV7CCqt!|^:gCod.@d'mf^ę5  +ؽk#DFN:F9 +i[`yw_J::V +žb?:!;;Pɰ^_lS5xO Th .{*q%& sk/ τ:9GMdZCuwb$}kbR!XpGn%oDJ29dW爮V[RH9lU|PPr` +u{ 4 +RrGRZ#ѧ!4Q&߻b=;nkh1'ӰFv*6%@Z)KG[ ǣ҄YwBrYO{@jwM&TZvfU)Uh?#N4[x mCV5;L![ +8,ʮWZ=^v$l}-Vf*N>1r<; H3Yџ_ @71n`U /O53ZRObA{BysFHsxI}SaMҢB>u@ra2^$@gTOC4VH/]b>yg-`C؂Rޒ??S9$@NEt$$ Rr(\䨢G]\l%*9&~XQ2ޅmSJ݉$0rb%=$3hܷLrOƟlCYJrPGubv+n'7|;jH}sɢ]xhpd2pz졾>)^ 7v[O{{t\=1?d_byZAryWŸ;GQu]Prmo3]Rohb xcjQB Ti7@#5%'N +NAM  2-pSXה_E;-$kE; QzNUÏhNl%%߾L(Xo/킱X݁-l W4Zb1h[j<h!۶V'ޮvV'UGxϤaȭUS$Tu6PףOS .J[eqrF h8;dR!L;囒Ӟam@#y2;TvM! `gCmY/)&;"_l7a`!3j|g܅S!{#O +P+GԔ\EeYm4B3AL1eeKs{,LOW(NU(b&ީY_|K2X|ebn?OG8,ʉfB)b΋nZaHO|]fjpEWBf,E}1ߧK9[1@TMխ=tѣ&x 1͹,5#ƈY!X mh}?xbƂnOR, \ڀw#!lqf=+gTwxH["2s7OjO=Brzb?Scs`$Siv=ҵYtPu+ K>G \q8F&FF~ׯ%hHv={ wRU6g }m(gdE^weJ oF!ʹv,OϷ.In[w)/yJ}IkX9ăE<,焭[7{CBQ\U왺r?mhSa9߶ ԙ|(^kB:#t)ms 8a #3:;;KO0g`zEENB8 m3Koi%h@l㮰3xrLJrnH|X&i̭ŶJI}GH^JXW  @W3A&i ӻSo)!4c\wo^)Ě`!w:9+6#ۂ;Բfӽ2! hFY7z(9xRͭzIpJOwGn;Q#!o*Ҭ:T³얿MzM S}^G Mp"]k˦{q@`-x [0bVApD1jTLymKEOҫ .W%O?)"k_G. @q-zށU [G_bUH_OH;V/U"=Pv%-ב:~D;0+CT>ɐ%~F: mcA"j~(~DVT>YW.{V ST.Uf=+̈́3ؙ ̭p'rɯVH߮x ԐǫM0ONp}ܢxU?@ZYU7x~p~StN^? +r+RiK.":_UtァEAʕ_ғopGZ}ߺA[.CëE@rv.')\[$uX3[}>P42,:t#Xw60o4gj\pX <%NS('@\p}'_tv1@b:TL1"ieƬ V kۤdaFbuwx9s +[Y^}5\pB1d%Tr'{j=cGs◹<7gE4jE[Iv˟xf^gOBUwtg`F5/T[!.ǥq7rm{}qьՕynNXANY>ZA^=`IƲ~D̛IÝ9AF|\t"-&n=Ga s{ss^XӞ.sg)0bR|Lk1yX%;gUBzyp+2d43Zl$I#ޓ~qd]O,aV\> +5 \CF$B/=fUsqýP%}wQ,[!!2;f'fȖlJN띕f//7%Ե3b*X^~ j`NgŞa~jVi{|0P5";k\G#rҎ͗$ +(E*:ᓥ;C엱_5nĒ+ <'q=նg%,Z3?=*4Ӎ6L0j|!Ƙ 5NPw"Dg); 21)VaRNsENy +@+~+r/SIWGHRFĈ !Hz]!1;̙1 +)嗎}zyzia~#-8Td9~s^L>a-rZXZew@ 9xGf+ya5Bi3A"(^6.u#Q@h iULF"b"HuE?OzKd9+vH ?{ʙ" &H6i=Cu?B sj#^醨5KήxlqtЁQI !O2$~Z9H='mPՐd:i߸B џC|$Kǣ֛R>M |$9ffɰmRTz\k[3s a4V'CN \چwkJR9'`U*)b6hSДQz9zd/kwJ'r`(-%3vEraPOuDo͐`L/oxG{-3#\ `[!!ZGҐDTRgl)pQx㨉%  !:tUɴMAsԈ&ё^ ?}#5t4nכ3?_1EaJ >mƚW-S#g 2bmƭ0s};n5HFr.W1x7((_P+$iqLBգGFI}f1 ݓ ? _\!(CVĜgNCb;ҷK%Tё/o#"m*Qzpwi>k1<3GGI|/3uZ?ǶD `m^gq5x0{+"X$;ayRd*яygd}(kve)HmkRPV>5Zq穯=h큫oL$6U״RNi[G +uSۖχ)qWG8r^5 g{LI*3;i bY2y(iؕ #:W-| cuAP6qdq1£Nt(l{l *GZb*dM{BBq hW:dK3u?#}$[K4A`vhtDkQNsHQ*HqMƅnfœ^C.CP_gܦw{ `Ѿ1h&**BJkr!$$9Vp[˜z 51v [ЪD<[E_Lu8zFDEeSӗzQ1S! n97P?yF~kX|$=ؠ0(3~[*ԫsP%x[=&'&ڠ]DUHon{qd" D{{+8678J] +;m4!黔i/)wXپ՘Ϸ4OGdv Hyy/l:9ZY4yn!M#NF ՌSjO [ +pբH_@Bg*_eDH:8G=&ʹ5~@RgnH'KG`=VinPZ8 ]ڲ܉ՔH:~)'⎵k6> {d,l{̠[)wCe>P$mWpZ<&6tzU/>=3"ҤQ@/ĠOms_F7b7Y^nQ~s05 +N'+D6⡞=ůW~YSe?eR+7sEY6 NzzGX?%CPM"oȓ)^;Zcɀ.5HPuxe,~M(X!xMdu&*iodUB }=:E~4{D:VH5Dj3$q]I4tX}[:㟑3h'pbKZ` GLp/&ABU(Aзͱ_yX2S?bS(exb5ĮpnK3BfR ~PNƀ@'\ +%QfyK+@4!lnH;A's@5dP'cg/Um:}[7"[ |S"ޣ_Vtw<࢞R \MG2TiF +#}̝q.k,IK'?R'4HVgKU%|SU:chb2~=%?aSxvob.;S^vg>wDC3(;he AORhua±&AV +b]hoxRD'DFa/ vJО=mTC`H`VГ퐯AuL&͜~7DĆy;џm{ nCi +rg3':.aNЯ8k(Qf#W-U׌4QdT87˥E_4Len\ׄtCV=E/0Cto<[/J̠9?WN*K+>mL1{=29o9J/֒)Wo}_3H,W:cw)WliPK!E#9^;g:bP-;p["shѧ)*Udr8a%$fxKuO|-Tt~3@DO }y?ʧgw~P{!twhnrY88c9KbJ~YvM>Lp5i+3~]ڒ kJFN|<柱xbT#>iX7Z*0%w +@,%sXO1 N(-͇tphF߶~~q֝N|m~mG!ɸQqn*Bp9ĀXUjr5Ȁ*R޶+( ^ >9r\&;9$wAq=4! ʃ&F(4;s=P y;Sa{&Pp3Z"[7T. /2WQe*eC m[z;r 7Vl7oB_ǘۊs3O֬w/_nb2teTՓX38zͲC&}wl !v-CBFaRδx~D֑&ƭf><4 +:M157.AEl> =w37d >\#N1nkYe#rqof_z͍80$\+E{;gIؖ?3{:_P!I '|s3ٶ[͍عgRpSA|b 9 tH+FRL^o}TLE un/R@< ebtR oyi19XN=\dW[CRz:zO}z{-x  +J#,o0QyjY-BPͿTCWɐ2N#ՠ2\1;EsK "P: 5[RևlZ g@`Iv ITbNf+/ c'7Չ}ZG0a潋=A=fYjD 9}, OΤP[RX՛J̦nCv])g|!VݭS Jr##dF xmXy9[Ʈ!wG&qFV7VMdj6g uq~kz9F df ]>O]"ڂc~Sup.g>K&GQl-ds$W+Z +59.Xf݌+;<`Xt꽴Q@zR5=^ouRN,5KdH!康]1|d,\} :[<ӴT evR)KN'=i9SU4 VԷ%%(!t,mA!Sk?+29-$ȇA_Gu&z+<`u(rqa&%[Z='\%98ȼhPjbKqw^C vu57WOc +wA)timgQ1PΨKB-/BJ*㽒AHEs"iCǢN%J)@^4Ƹw^mpV*]OWXwcZJX!C 0L2ZI}P"QA kȾ_xul`u{eP')˕+Ր+2$|u\AiaKNqz>JWTJ$_;XCqOۣDD9PjϘpsC0r'9W͡oC01oQ{cV$SDBɶS#Nci:Χq/8Q~r7D%4WJP%h͐ !܊haFAI/~KE+3 *UpyW2>J`WciJgDL2orz!`J{]wT\e ,9Z9HymAzfzp]oY:YOxeo'0ϺE#c-ۍ)5l3Om +lG7Ym!*{xc{Ɯ#^*̺z%J*"WzW$JM +Xr uM o!X%X恪~][*R<@g}+6 ++-z1!|,c݄bά6j +Fy츫 >#]qIwV5#l|U/.(vfI +<xTɘheR<-PdZ7 MDA=j>3^`$pH 1# Do*ُ.kpl'2[G8@շ|煉&5DVR +Z'hXumXn!UE^UhQ>7Qe Cy LcHHF16v#Dn kϖQoGܟbYe3E%GYLcD >3aNGT6'*]& +[g0sP[볩&eȣ˞h0gD_4.s[$#nEP޲Q9eN)c=@tM1U,j";VH&wbw{a?,xJ\sV[Ww#ΞQq~{cUjRu `YR^>+GD6@ *㳖^z{XSE|sGQQ驛RU4Ζ%'wZ\NQCaj?SEg$IC"j@آ2L6be,>{:c< +᝜ߵ,`bVg-(kn,CxrhCJ Y0ނ0%4E+SeqϊwB^ T@$d (p?5$fiB!1={ A SN"b@e}ܖoUȿ\) +;DHz=V3}"&pw=p(<ߊ5 @~pPiW gS!pNVH %j541za%Q'<"V/3k24=l[ :d:($=jOmx֡y؏PqAx;A/*EbJs HQwng%E馄,N[ pl"}jTR5S93,js_E]&ڑUrw&3rL$jKn[øжӬ`5N9҂y}Dz)/-|VJ!r/K%߅{sDH))9 2=%6)y4Z_*5WTɜ,Hi{60pG4#:`3+ߝKUCrr=jg ] ᾜ@\;M'8{0$'F*֔tSi ;4oH&-}Y;XD]#vsGɳ-rCuy<ReB=FROJװjY.BxYΦx,'mjA@ի`J*1K[`"' XsVjK7W{ϕjyµ/qwm{l~_R/BFslhzzX\J ;Xl OL`Rw2E& m珞h3!kk'^1y7H1Sk3|pMN=9or}Tojt"2=#wUg;_k;}u+īdSSUVWCPxs98KK5w êFo!Mw_P5!xhG԰ +Sߩ5kKƒ6>w9CF͑ေ3b uwшyٜn-u2d[G<Ⱦ1p:HxŖt[DŽCpC$+O~^ަ4F +;s$ڋ]=91/QEI,nk##7pGI! +$Wt/p̒|1% ۓoqn6!.-/TleD+Kws'RtH"ؖJ ,?b`C$ %nĪ8Ն{ SΤlri +Rxe,7u2;tE}<"TB\Iz˞'NKGǂ^ e} Ñɑhi'9^WzO+6&*= +zi6Ee zQ_i#=@ߑGq@w +(&DwkفK!8 +q 80(-(YjljA$XLEcr"bdy[Ry.HFNz/A췅~r.#J u+EK(́G1?0DAw])':NpiLO-⸃uԾ8OU$bkX#KUq>0$F^+g][ ms̈^pcs=m?zeq+7-qZz'>'bb(KwGK=y9:M\,Uƞ(BqeʹQڄ.,ǫ ywk=- &קtnsJ *1|2 G,rpTi?*MjKz+;ʳo֑ =*fš1I EިhQȈփm,V 7-[|7Ҝ&IbUNM [ܚ^%; +zcйL}LXEN)IYe% ϛiiy1m/EdDJgXv/3BHl%{8AlhT'18)eV?[`X@SlFۋ ?@Je;VG[^eL8sƞ N|k@t[o8>[|X "TILɑꋊ^Uf噻m/@J 씔tUR +[.[D,mgE`G?? +&?@]jGvh5-HCڡ0WWi L+S6Pn--Vq1NOV}ҏE$ZU sDd""@k08 t0mF)tLE()I(τXB2psJ33oY扵)e鮌&B؎aܖ9Qs4 Gq?┡)a:r{L@dgB#Q2|ŵBvgO֣j=PH +=~i e~?wq׾eX?خW74Wʮ;IFI;zHD kQN"i A%Ŧ&ݭ|DWeG|RJN1/MvG: Y^COkudi-U`35 MkMj= RiDnCʶhAS5npӣI%Ui31 nj%RǎA\Rd[+i:T2`JS47RX;vc~(ߠx@f]X- rAhh7OWMc +*Ru󤜥P1tϐh]]!²Y{{*BVW?+k-x) +gw n״N{jq #f GQu0YKYX[*%9V`6PU Pvy,L:kƚ`,ۮ]2_ ubǒ3 E^xн"kP$ ?0l $v*`W==3H&]V$o:0m^g-Q΃H^9Ձ RˠE 1sʙƈVD(9a"\-w + Jfe {-K4tt|5c_lFtU]2̓PͅM3gP*rn*3GHc&\-}X3[TywQR wVh;ؤ#:WZNVm{A{twjaKЊBڞ3@x#!l^yQJl<ӆ؀^940aoU5;݉=D> .N=c;g[QGP{5l4wԽHI Q+^V@A\2Cv9?)V"p_x LfLJ܍&hӅܪe{ :_֎!= IODW"+ڗ -WIl=Y nۧ1˻p4M0gNywal5}MQ邥Dw&kdz #oO8YV+#\ E:* +ݕ#r5s;Ku[ܕykV}ߨ_ڍrKdtǑAAw(d!ECnz!"i" .J؜ڨNTp pVcBQѡ5ng>`Uʽ2©~?`51> ^^%,K@x-iB!85jyiE=O e(Wi|g1Gb2`7N}fC0SNNtT"T}` 5]?aYPiTD#DO&$'o@I&mojьQ= +K"x {AR3Z~gHka ?8ip=*TVfzjא/'UW<@yz/sbWfL_X٣|$^--%0==/d_Y1^hC>A` +#EYNRGlN *UwḴ E."V׉N=t;#b#`|Du$znL,:0 aGA$j %=˰|"trC?j,O"ܒ~|ꏨi$4=b;7S%p]cu)/nBO~.|vtclumi3MčR=AG4$I</W{*d>w<'m%}{M`43:'px= +Js|K n*+c**$ +o>\=4i<0N3Bm;?vps:hQa68nrB_[^) ]CG\G\|0VLz0p3O~rȼqlqVႦQtπ>U^ȹRpš{LeKEE8u4K驟~kR>(^~\ٓRR@`~()=.ZeY +X `"r:uG T; JKؓvTqh;8ϐ- { sHc@#X[*q*5ʹg&Q޸GCJSЦx \zU N֠$DholۊXi5nw(Ic䮂ŖU~ewW➩ARoWraǥP25%P4'bXLnw.xS%8yVc [YNS? ey$e1 N{^t8qF\ 5ƳCy=uGZcdb?zy&ww^Rz iW[#Q妒T0> F6>jMcLLoRZB& PxZlw|Jٷ6UXO̜̐;vN}9!'wooŇ 5"&v%Q2!l3!zС( $X@z!'J _r8eGz!rc:R6=iqkpY%sȜsb)^}zDo` 3MG!3nt+K}xgRnĎ_V0V$1(&O6)`G\g7I7&_)A +>`CsR6ڢ~z^|WX#3? ;i~L \qD&t' +Ć:ĝBVm7OQ IVAFT-_LaE5N1"ߘ:d=Y>ܦuonizu)40Xr@OA" Na&>tWc\ʠ:w:R3$2q.;xa!hQ#=`p/gLR kM# 7UGATM_e^+isgb\ZP9wpmY>Q40av0LO5>S  X}1)YtBNsd{v8:b!^ا +o^\L-3Uq.쑵\gt\oƚoT]v#3G-BML6ʫì#V3m-I;ђx*(;׍F QY(H}R c>6-C[Yprѐzխ Fk؁r4}ފJYz +Ja5>_5"H(D$\QkNTqzȠB"2vKatQfӗ3R# «0jsSMu/ +x^֘ ?a )KՙPہ(z _c"eXR~Ԁ8aX#.nO@5 헊0W%Fz5 ."^|m+_F*?,?$U.JC )F3"14LQ':F)dco|ᶕ 7 \ĮoN]Q d/#8^mM7CH(D$;hg\#aD%q4' x{飝X̰+H z$@:MQ#-L|1'#zF7 [dXpjOݠ aA% mpxڶtHms_ +VÛ =2+*֐ +/O8w1ytr7eJF>Y;J ix^ȴ+ ^[)_:lN1ץ21Zss;8Iv.C0j[2f?<:@55ּeDOm8"E8.O7'ص3mG%;{a:ȂNyM"r&QxooT_%W;].Ht^h{aB ڣjDF1 & V9R\AdGlʜbcQTvo=V 9uG + +֏skSe^w a߹pwI}Z /–t$#({7,'I>E+%6FLc2rKPd9ӂ;Q0b!G4fnoΡ*D^څՐcUx*2DGn[(_"!+"B^[.=(9%=G63*{X)ŵy+;nReHeQOXvn"r\R`ځnQ9l&(D9bs:r.+cJvU},:jz Pe yaM%^Ow)*ElԠR{#"UFiBF!2bd~:9-<3&ORO ATmX6х! }:d)ɚ "-&rE_s^85W%kTP\? +\iY~{zݗ#]G(Ω\~虵^QR_*RJa3CMe1H)vEqgF9|7+p(OS[S=Mq4ś9)[Fh)Ȅ yZQ?<̑Ė=a@y>pSڣz(t,Y: Z|,'uȏ5f*viqަl3H ,nϕ_PN@?}mٰ):xdF5#;W@K=zg"'ˎkp e$˙+;+ƜXT^2#g<2|輹iT +pRO<6o?>k91S<~楮ELxu6`C]oϭ< +:DㅽqC +0Ao\*:Gm6 ±dRjA*h]P2tXMrtJ*ӫW +2mPw{=ˏrXAq㡼7g3!z7dRu욅X##fℬ:-ȰA5Sr5hIJh;%jnBe̴u>Y~ 72,҅Eǀe7bsE`;L1w3wܟc IdWP(>;.ׄLiđKL4h)̫oIa~8 9Ec4~.Oyt֩-)Պ.Z>Ž\VхJ"U#e0Y\Eu:n4^> Gt{RdFtsؤj[Ju)m&u2=-1fi"{=E=9KM飘.ԓm .Y>pz$މbʡ2^@4%FTԸ $S֚>z Ѱx0pmʈ`ZX5DݷVbv V}S3DVpT-ۑ]IӞ= +URN8w\p)r{k7#-8o7=Fe+`㞆q(`{DDȣ38jGcuoܺF O \+xv? +sL82H6Wm"0ىv35nquzڴX#aEI ^f[Z[ ȄJ3PC+\(e\L-FRk&xRw󙷠ShB6+ +gLDg ݙ@,RH(x'opr'1p*,f^;PYZ G5w@cA@cKplXE߹!eYxHp}@~GyG{$Km󢽉KM *P ˄Bt5M[o `ٵ`K?y}zOn1=o5z|KRQJSR $qќJ]Z_/c6 qΖ n;3_GXw:, 6I*r^l9nrܽtD0EBETC4CFL9(}`4FTzeʥCPٶy4*tA{r`!UM7.. =@WP,u@*nPM"]k`X*[2݆=HnydEW'_wwz4!dc>Dg@']s9[/=KncƝt`R s=8u'?gBmwdHMjUgg47Qp^=LƶS%X[q\hw DSll$c|[ki)@ƚe~O_r ~7c>rnD޴/Z bo>W'7ǁvSR?CR(#=q&Q fr]V%!^b:M}n 4> 5״٪2=Y/t̛I&HLM_Kgz"0uF *;/aUC4Ȝ˖܁wcMط"˗FZ~WXDlm}zHݴ]+eV홳j:Z +%4#(Xw3%rz[K;nT>z&"([L &- kRq^ԝ +iJLm"[_W7d uxl17{-~4^re93F?7JMea)h`oP>Z@pW-]35Ξr lp7sEW+΂ʩvr ֕x;Yzr4ǯ()b5VJEAZ]|G EIR`67-0΢@bfSEa$#+dgoE xe Kv߭A + +{<ϒ=pabO&}Q8]*Fv?TJ+2c;M+{ endstream endobj 42 0 obj <>stream +?MsrB-phA=/Eegv]=5D1e{ff1!zIx#]?NtagAkG蘦^N.5a> g}Ng)ZwYeYY|A P=>ݴ`$aV=fx9 x 1/Jd˗BwXO|/XKM2^%Z!0 p+>j3]heӟ=r4 =! *;KL5C`c}tc" Z*{4[K~h~U˫ր뎇ϭ"^v^C谺WW*1lo6R\"rx228?h2h5DpQRq0Jo[J(-o٨~Y:_`kcZ>/CKw"kHkk^{ ew<d/Gfi0n +}pPP[ދ m,-":w\NO=sU7F:.?RJAf1!=p 2 b)K)߭E(Ae|Ǵqnf!i)g蚊mYry9Y4y`]P5jI!Hej52 ZcȥF&a z? ט-4}৵2㝵Qz XP7Li#ųS8=\p0.؋S|T9)euAxΊ4 :3#kdݩl#X!\ͬq (&}G/u4HGTO+;'G_wُT [6ޡNy>zI#SxsU ӏ`wXLelIv:6} Ŧ>MmRzr4mO_q 3 Ũ"@c!Tҵ[ + d!pK|0Y"%ZNE@.JhCۗ$ +TW0%Ůs]Axb$ĞO1,Pbx[sCZ8N N۲MԶ![~[2J +_%?O)"Y=:0>5PU%bPw*(^B[Cr}# p_{a4*:1>_C0wG0L]ַNF|oFS`֋9Н s.OB% \i-6GMG p6tH%h(bsپ "] K>5:^)XqH荗 + Hȟ2"u΀(_ \7&^[|o;~4 +wZ|\/Jcf̙V'Jڲ-Рhb>)[6uCi9ܗĻS9HXKB3qFj7rVF^wɣ`:t{eO/r6^'ǜM̺UZ[W%9nj {ZSZI݁HmgLɮ-B8"-Z*ɧ&,N}O'R tfk Y9lf` ..k̗Iƃz +þ辜Ld`$А"U +%qcik½qi `n `1-Y"@dHL8QQQ +ohJ 1WJƸ#^TO7i`$pa_gYTu;_3wQcRO[L-.UBn=*J)P<ǩvFZf і6UZ %.\E(J7AaGÆWhD5U%s*<ŋۈSV1UH6nV3rFݭ L9}ZK +A*[Z/*K*o݄B|u5N- ӄAgPyPt̾b2BiOcÊM9!a.%Fl=g@dWR|/>|Br``19e1@߶R/,Uf 9>3|$M:jAnL ӓu84pI]!gwhJ_wxTvd~,L[!|%JtZ +U]ς)Ȅ`?F2/ q +"JAnZ iq8=)l5kGaA[D&[l#'Y%'q@wApQ;8t\GV#PmՅ> 7Ks38f*h>".[7|U 㳾o0#:%_ E)4Z;櫔NW͘X8W9x58c(GwnN}@oVQ5-/aOTctsJU(B;Am[ՀhsQY<3h?OKa%iuoBCuVM"*Wx#zJݐw[h5;rVFlϝDDrF@5}(CaNЛ+PGMp9|N~ﱏ宰Y/%Vm-7J:Wld@G|ˣ$ d(- bNQ"'xYW7,o + %'+$]Q[-4]pV3VOCJ8VױuY92ꊭkUKP x9>=(!Z<̕^!%U0"ܲLEꎌ5r#1+>4^<_-cg rR,# Z+m*tQk^[A[5_܅D7e*#[teLb3!P=iQ'ʕ$0(ŏB!ւ,4(f~ca-P Y/C6_Aij٩vا-Ք iMwAOqt4kf.rqb+* 7Yl#*Dp}/T[>Kfc&p6wWrq%gckGH)A#diXU|ۑ4')י_RXf946mu-h V{ӿ;BxP K?~ FeJ2-TTqBFnV+ ;" 8S3E*ɐD, _a7yjm4%A@9 1q@ XvB>.M6%8 @08 +{ E)L|ptǣ( ז!ΌuOk I\|~}Xx;֥vE~ceY/<1.&VUӘ9xĂhGϹDCҒ,#x)G ALfH߶>edC '$"7]vdqQ[׆,oBg-d?jMKUnh 9cI w{ %i55uGxѺMRPŎtR>cW2˜< +85B@YߓYW5eUHÌe;Ր +w5!QK~8>4؇0 =ΤBA +zTl+ˆhfsBO5Ek8netnq4;Dǥ &"I#kɢǍi옃Xj^{qsblC1ƑK%V"ͦhHYH5+ (4=^"ƴOB "PZ@JH*NI lkg>yDEs?FI lg63kd'|MǏwwtq9))O{uV$ +[]7Gbc.G0*iCFoό:;YԢ2>w,L› 8 bTq}=;lƉ"?Mqχ.Azɱ_[Jq_͠;:&9HxkEhG%%{0g)ismHP\ 2#E3 m`\J{K %kVq{leTV1WZ【]sOl7/Y$>ELtO5J=8Ij)%b)zDb+#]`'&I2ͫ Yx{m¶hc2d'C/]Ziia ܩ\{ڮ2E۳qoâkR BxTuh rJ"f*Mqlڂ +DG-]W)WCX?5nCsryTlV:p|rk{~mD^ܔ 6F߯d7fܢPVkFrO=„c&a5y.%!y~K'ܲx?n3܏`[x;Ә# RKvokզ+^4mrX҇ x2ui/z͗_@ YFP#N֘s;76>Bzq{&.#*؍x AsN=.gSDszK-Jе/HMe D0W|Vr%9YW? +%B -G`8 y@v*AK8 ^/bc۱?G.eo#:߽sWrf?ᩌ/H"NT5B=*Khԫ?"T2-&ח>jgDmWC0m^psts'soOӏ&!+Ц ._hinM b*/yLDAJw$Pq!>\&J̩ਆ-E+IwŖV&Hg" <g{:#NOTw]jѵ/b,A[,yW}U)RڳIw2>!do#;rǠ%B Foh[}RۗqiadA^! 6]Ύɖs~KQ'hQʞMŧ,Hq%TlKKVd +?gr1Mnyg^W;#;SԋNͮJ|eWbVFo)A8bn [~,6+$}@ÎҎ4sG!\mrJ'ŵg4YGݱAJ>`8302(acUTm}&`?& E82ѝL9i9;$c?s>j)vu0Ǡ(n1gVw`-~oDO+X&@UlZ]GqO97Fr_=OߙsHGNdX'{ K8"8N!^GnR{,Ybxl.t<j]GEuZbjoa,!~~9Oȩ00iwŘ*8^R^ZWKY@t$ Bڅ]rj$//ҋWKJD]D2q.^圃_,},9 Pto"0=൘ETh1^C?> k}LTYCyLE?Ѻ=jReKXMC}WyHXs؂ +ƸOժ#iC7TWCߙljo#$?bZ@[K IAxPˇU&wՒ+~:& טk.jʷdgBQ{\D8L~ny%P|mU +o'v$Z{$_APi8LbҤVu7^cj,{RR$Iw)nߐZ!~E"XJRH][na{޶I/_b=ZG]wPrw6I }C4Blktꑺ7VL/dHҭ|8mNb +/#3Ysz]5ؤ _G'c?5i5fR'21ARъ2b^:b)jZjyF'IƷG~⽝/q\?V F=Cr"ѨuME _B'7j +"%>/^ܸ((3 ,8nJ {(sIϞK!X%&|R=47$fRW8O@(h2RH].oj=|Ä׹Tsv3A >1UQsi1)ID2Cv~;md0M*0QMjWj^?`;`<,a^#)i5gD[Gmr$(ȣ! L&kӈwW1Hr-.p2I2#pvxK tJoUdCVF4:ؙ}9y/&/!S(`%.7:e[5k9¸V"S ǡa'?LS YOGmv=⸽aswNAi\*' +i{{AjQ8Wۯcw~i}^w֛yNRmO,IZR :á@{fF:+H]hlk^r p{(G66-cB-+?s/U?o`HWvFp@;ĻjtNN7"[⃞:)fYK2DҡM}e`Y3jO@R RD2&rA q՗B_"[cWT vQ +S[`܏PKbIx!BrG_)w$ֺ +Xoߢ嘉dVqhډN{SNk#ze8M̯t8\/m%qR}5_ ~3:gJ(Q iA}'|zX[RիAfws +|5EP;Nh/ +@xec+#4"Y}2BPR ^6FnBJwmsKI'1j +yp%գCWY+iM28@-RtqJք 5HyBEE 2D0co٥Xcv' %?5Jm)<Ϧ"kďW"㈉|{*x$d$!d4PëI_``bYHm>AVhV-GSkFF@}pz۪ {eӜ!)f,c϶F)1 b!yXNLF̱r5DMs]vuŇv/ؑBQ<>FH/~{;MF8Ft ->'J9CƘ j\g +bXr&,Ll9v%v0HI;ZeqX,0Y:n&ܬ'%iС^&7 ٜ.^ ~![) hJ/4ͰE* 7%UXV>_6/R[/R?27:&CqD]*!V@sk_2r<;riL4`_$[xMoa=Jԋz]P90ׂbt_:1w7rWe31KZ%DdΓB?)Ɨq>SGZyL/\QIPRp<ӛ:9{=۲xx-pOx_@Cm jkø;jpk]ޔXݨD"c-]=ѱz +燚Iew/Ik]/_L~K;x~Km _YX:m&-)i󕕦A <=4g +V%21˫$aN;9Nm`'{ |TWYmxui94z&.%KMJMJ&c!TnÝz;KjE׷'[w;0, + +1!6})M +ߏe:BS3d%6IѠ9B=dw.@vGBK>+s~ +4ػl%b" ǔG%j*܌֑AptӝXL,= +Ķe{,AЮ>88 I/Z,g)GAĵ(KbjKJlT(L(U>?X2zJY +{ ȹbPHrUi NRU7!/v8f"7wFBE⋩El)?>ᅰk +i {-b% T37ƖoWJ];ZB8TDd4]D#x/{rLp[KJ5_bLZޔK(T;e G#X۝ȄZ$uOB_t >ǰ?V fu2'D}a;HX".\z^RH@􎷝O.BMn6X[- Դ欨>NϮaHKrO燚a&Y8i~ MRd 9r`i+)ldBS_>+z(a,}29 §i6 #qsR 1w,kCoydEK>KA_zš=AJBo'ҍjH#p1U-"1wRXo܋h9oqGo= 7^.ZFGp[i/oJG\AG#/>Y< +(E]?jðc9t5HEs:5@>UMǹ>#dpmSWW˳7dw/) P-!,fYh/'q=7ݓw]o8U| ,]r ;+8LOwP5IɜQQŝMJĴmmwd~332(HeO險f\&=" ؙlt vo^2smnePڂ ,fϪ;qZS?鏞TɯLV7<֧Dž2hO_t^론y;K(vPudb {0 >vQzp>l Π|Mbf|tQXGOJxyd&*81@@G`-Y6e5ZVb?N:, I#>FGl#Aue6#,=E 6sGN/G;D k)ToL".=〨$$uOLKxwQq6m˗Mh >^)+VoU4un)vMmUciuI.Ng0֟!0oG NJY2̨B (@{GтGXCpf`er,ƹsF2M ay#p dJ>Cl2>HH8̣<4Ҥ%ijئt4o8{<lS$7qLs:thRe*E.S`B(78&ܗij%N{sE|p :)]bFc5$ю>#sdFe+@3B`3Vk:$bNsp]B+6֪l 7Σo随#C]c{w ~:?պ.K+a0uTS#& ܃`̇Z~ǛvV޴9KgKEǼE)TT(yi٤s-GC8x,=hFG;'@ 3>~P?pKUn +hQ _Quݣl9ᶨv{ e3Ě `M^SQvSC90T=4c3ggw{!P )|6T<ڮޣ~JEI#czơGڥ˦)a7]RZ"@?5nxhM_̽ܔDEu$Q7C\\2#7}7+㉨x4-CT S|SUq Kerrz^i#w9T zEAPt$ːүrj WEǒ9!|Pt&A8\S+Mq.ZNZ'j -'16 8u0.E a1ޝ=U+}Wf >I,s5\{of8l/-5FL![A.0={_F;G1__AK.̙M˷Ӑ5@ϙ iӕKݢ mA˦ž" 2at"j87AruDw#&kH5viBE*"PG8#YCrmȾe|xt@;wlJ4WM2"<{8ćWZ8v+:kF2[ w@YJ:7HFh+]^JnFS?%^qPO~DsuK$.6-oK6G.R!BZJ-,\^h@:p?ɒ]? 2VI#(oaɉOCe,|M'Ӓ 9HCDc-6(@&7I-l p1 o:3T?5f,7J { ꏄ{5N;h5g0$\Q'(b.?<‡1t|_In)|MSOCꙴnVs$ּ=G|I^i+OJ B/:>< gϬaݸX|yEMM@Ϟx=+|{ !p$' ^y| +X$ݨ>d|!T״J5?ؖX(TCZ馐yE4v^a!w: @R9# ӵc?F0&o HG!^_А{R +ZݒAV{FLt"TE] CFܲ~>ߵ#Gc +Qyg Y9@0Y`k%#AlC᫳0[ȴ&|F[ s/h +s!/gj.]uUl;:V[]4,~4ԩ=+?ۦLIz\o!4})ߪ~s\Fڠͼs1\\  q\f'}Pn8Xc34g\G ^$$%4#j^8J"$>A1Yj^kzF<V4glho I]BA2vPT "nבy~o'zwZyaA=kup)zXr~K`7{8Vג!Zm/8n@Vbi}۵ 1Dp g]> +7\0ռ(o42$ +^LOy<\,|ےOl5lb#*(M}M8\V~)xeϠq㻷~䊈X=3UwX pv\${cAA)^`|F rڧbFr _ !V8֙>bASTZ՝c!o5$C쨩 , G#J9) Q/i_mSUZivl+f7Ao׈AIbi%cBxFFxKRܩ֭`L| adc=fGy|)z{>,_ٗ!jȼ,PwغQ&5Gӏ>-SH99u KnLc~ .E_g",OT5YU271I/Ds/+.Ts<02d|8eCP gKJEʕ~&O`" `&=DMë_n#';3[wv:9q^ALy%+]f-0YËnRПˬ#lgUL$2AٕZeߏe݅,ҏz5AdcEk&ƍ]*K ! OR) $U}+]y[w󒟫 +w6b.ۯPqM$ro(e>-@])g|1\ly[ܹ[6EzGEe)ÒH4h `mRtНu5ed[BhY55K5huK%B*IATR&ce*u78 n7 +)<%l.Z~y@aIpHۚ'#y-#\08z$k|.iP{Dز1^jݸ1yRg3h9#q ^ٞG7eeXц 70P F+lj!~e24#+%;+M*y!QI2' +v:6O2N{ʱ«(#bQWZttD;zIr"iFA>! %9aheKEt7u%W=JƔSw +Ck='XCf/Xi(oKNO)黉yZ<6m jYV#٣v8sy+Py^zi)FG(_- W YLL!dhk6eӯs+?:!byPqXK5EMM+FDAgگa҂PiRĦt*\iKw {?񜼪ldֺ-D.5*Y)Sb](XxjsWܰ8I+Wګl`͗TG\ ++}/p!=_?%- +)LQH;i:[ Y5XYqr 3?q2"$Wa+h4JvΑp/mHsv'RTZ޿FɧV a )Z]eDa%^GoxC0<֦-mf.6B0Wǔ Zg4SwS;3B8R&OgFeqUQͅetwOAv8{˘r<ꙉ5Pg45-h 3bO+W_x ф[O?Ͻ +72$:r+{GC^TU"'=f6 +y|܄̴WB3ce9UU- 6١PO﮽W蔜k[-k0QW4BO#s,=oZxM0;)W zD}+) LmpAdtbQ],$,L~l"W)H<¹$=W%+&[^[F7 +3k;5wBF7!#NuR6q#KeCgt<")VO=UKӆG5> 8H*nվ]4??j[u@nsoơ')b@ J(Q- h?F{' җ7D o?r8dTo +0)VA߭PR|qPFOs4Y>P6wDWaBWH0qL`JNd q^Nzz{1Vg+մ<;#\{i#GPx+ A5_>)KNkmk& #b\xZs7G-xEoTȫdݍ6 orZd*%Y茞|sbU5ZPh5y2+&X1BLƳ@>90PX~-Doc6 +'N2 t)W@@ەj~J7tj9祜7Ꮟn—6FBr\|.ldnC󠊇[e"!׮7>գk@a - +E;@PU'֣1$Y.nPRoN)-5Ma~l@ؐŦbE^ŕVg1o-E<_]LZ8ȣ.JD#$|^ޤt}9k /qRQ3??RjwF9plse>Z<(mU 848bf+,yoTsi`G3bM5h5-ݫ(VG`ƩJ]wjhɃ35!`֒)LPECS`|ŵN8w"#XQQܠ0P!J2VsXzwK0$7 6[twSU *ί,=zJ[(4Ë[%fv;wI<"|+~z9b'E%Dꐷsd9QeD\?.F0S!,N + Mm•2oBGh%Yj~d&àTw&ܙKxC8[FCɾ@w 6$٪MeʨD%Z08bzc?{'}ኚ8qx,aqFS:X-_幾>WcIvXv%AOiM:/o}1N?v?vo`Kny=u yn+.G u8)~h<{ve)(,eU1Fls1O}ŕ^V?_)}3682'r,Dcd/R2w%ZvHmMJѺ"b̧%ml'}E>1Nð?2&# >7|,V-pVlv*Zi댁})6O\"s;jǮz(`a i܆Α!6g7ڂ)$(R,KN*Sibnn/8v.C|z[wtFK6#~I%+iK1_jCV=LVgSg(֒7b'< +>qˌ };KK( +wet); :f?37ϸbC?{ |vp?(|!yq;}{ޫᓺߏ FTI^DQARv4pr7t#G ˡ9h<^i\Ѓz~F"T{""% +!UIո1R'G6{'kP7pC kO/7WuAފT1 u[h3$<$g9HamYcj ^+U=#E>fR"NsF0-ͮ>?=(O(W%9bf(4vaDO2[Fظ!3gðCߜS._lZˀnm-v4z2{F8 VRo,ګLwFS {1ɵ`c~iv |nB[9? ڳc}DuO~iG};[d jsz*(\TmGe;X؜V v=>4LA;G~=Vչ#87<9mJ4t[ңL g3(?)'~5Wϭpģ5dҩaqtYBZi0P. 6>5mX6Ͻ=`u[W +T_*02"z*M{ژYG+ uTY8{z'ĘIL=!zƣCӛ,gxl>*eH!{ՐRVsE>WBAl|2=Rm\aisv4_FQqMSmU(>:MgD*/rMR/&| 4ٹ:Ty88C"})wF]o؆d7/[31{β(ψ- +kn +>Y(ތyPxJZp@'˓^cˎP5\VsV9 *ioGXMGݐqq~_W0_߇e#%n1{z\A7 +_%nD}U?W&_"cpCJ}xm _c OΊPV:zG+XⰉg|.ZzL3F;Ԅ)x5~gW;Q7R&* z;! vO[z !zĮVןhHaː#[mr5O<2).`c(B 1ǫw,nrR*UWQé,Hsz b^x[MhXԲū픓,vrFﭓNK6ɒ qOpb(@>YdS rhΦEu /bG</RfkՏ$Nawdp.:&#ӗy1+eI[,b/_*h>DxgKpÄ9K³oxy9 ~|tzp,<* |U +dBR[4s*ڍ]*Go# hDK/ї+#L3|8-:C֪T_1$`g5C_h!b7UUF| S7yy?I!jԈrzdL(55Z()|-V2[L*%{d#Rs[fLj8WU_@'+ƙ '>Hz)er%G4AI|]8O ݷ|pkW ( !d8?X2vtyR0}}U-ƽ$ a/i62D/:U'!vx/{kMPLQlϻf? +m;!y0˨Z4`C~RUjvJ@:u)ιv@TϳøV) +x"*!s؜eH#L׃=&jHo> +Yc+*Cjp DSU't#{M_PS0 p&7C-RG_+*${ZbɆH_AhG +&,?ͯwTʒZx$[޿4҅~լ"0dl ZSB +ɢ_Hct8Ci_pelu[xnC fޒ&&`fv;MT[j9 +~\iu{r.:ݣf$S# z\Цz^tF'}USdǎuW܈GU++t7 :̍'XV̇G+7iYX|vXJhg"F9|T{S"E_e+=Tz ڝ!C"W6T'|= ufşp[5#w79㉁uwk1W(-d`"G}VwD@,]\vwo;Ƅ犳 q]Xc zrޱ7l];U(*Y`&;Љ # ki# 7p--u߲H>%!7"Kr‡ɼAS]տp~t +l@R͘<xſ>ۗa PWWoK`;(y_U|fYעd> +TEW[>UEp [4N'a'xHLɷ!b]ah^1w%'ϻHBjAE>̻aQՉOd~ g=6vvNؖX1JA*>J$%芖6嗿^Z_2yBۆ .gk`o]F=%[TcG+* /=H \'_vR׏'\{'AGyˍjf@nڒN5-T@i}-I} CR?,h ȇ1-ipP-W%GtWͭlKj϶؁/!ơ2[{}pyuMu=S=ޙ-Rߣ@ R2D`T&n%K;~M0~F@[Ҳ4"69m*.=bH}!j$X-TM0írhNT +'%{WucYsJDa՟s $(Z~{d7fjpK1#dW%c.4$,[=VA&P+T>" + + +R,3iv= y t CꈠGz7i$ɐpz, +_;1AgE^x/Cȑ9?U?K5T9,+h ^:m5Fm aa)m90 F3I:XP3Ү\Kʹ5aRh=#PAf|!a}{c4v~T3U@dQ7|@x;b98~mꟕ(qI^׷Zw09ءfJ|*_Q˥>AO{3HhK sҋx}YwS&(\J8Eo'PPbe3ϱWɂJH"_F[^!f003~1F5W(vfsVlFt`FYt*L4 +hdTO51xYȸ_iz0ޫ.}"n?Թn_rg?yC"؟^{&d2[.c4Π$ԺwʼnB3}MCfSL\1^_fk£Y:ޮ_^r&۹^eV3dzz^kTg-"on,N@`p]%pI"tf*SVi'ܑu;Y6|O Žm5dJX3h1Z3H0|Z0DGv.jA%~̧㋵I!r< + s['"|-Xu1Hj\UZ%&pHҟ [*=׹9IV,|1MTIXl+Tل9sr}蝿t"KIO !TVwZ>Uw8dTł +{0HΩܸZj}jms}W/jZڹ/BCdH| Aqy\8~xR7@h"+=[ihQr?9F?a)9"a+ +3}{IcG-vW-Rum6m=Exҝ2_([WMĠ#-ҤW 垲w+~϶ZA._Ħ:c'VJ'"VV⾹)&D^/c0^n5&0 Τl(%o]b<*NU3$V ;3VГs]T@vDԥwz/Gl|c:i>^3%|qRfo8?^4O1iQ'HurQ)Ml߯;LtA,W[xPctrIZZ૵j5tgɅn3c/K̾V+>?E^h,w +ݥs5~UAʄ# +;GHjƹ|.`>ّ!&62,<Զ+ʏǢӉZ ^-uo܆OFv-wN`q?֮m}?q=m{Fa5WBx5Q8]6S6{[[K f?h%?㲻UQ2!kLOj!)Wuo2!Gy[4FERMؔS+3hn 2ư ‹Gcv֭d 4[3[fy#צSV fe%9d;67"-̭ }P<۲,‰9 $B`?:2 +b'\^ Qth3(+{1?6 ˥nb, 2+#9o??=?mGل!vM]ͮ59^qS9^<=?CbSepg 9_L28`op P/-oY5.89䢁1Q}UG0Wzo:Ö-՘wB{¼v]%D2@>q怖syLwwKulaM{ŎΥtFy}#|E|/8xD ٿḩ^j)3|DL:#K(jl|3ƹGdOtP%eZL"ыvci!'h}@v Z!_;Z")!+T/Lrh]}A}b ؝b1(qPQdKp/Xh`;i_m#f$Ot!m!i3r-'Klx\b CPDޟ$Wrs635I_\`EUYf‡U/kz?f+)h#K_ B/j ~_2YUENj{> Aޞ^-~ x DgTk概!R9^vːy~hYK8o-3yY̅i!oR@7VYk?xYigS/hy{ft/.Thۈ+wꀀ)^`6 @ GJ%?L-W *s+ݮ|\EQ"쇾E_%9yְMߙK9{=Q!\CкGT<5cL$:$Ļ\:3^QL籿J otNYG}'^z:!/;\([oh!v6~5sMj7RPr+=Cfa&ElT-Aj@1:#jjQQTrvY_'Ux֯`&]L㐚b `rξPsF[=v.eSxNlZ'"3b4hҶ%oIyA&r-?/ eAQ'<s3QRRpkJ@` +Z?2WEHctD|OXz{lΒWR({TsLM;6 Gd c֥B/ c񲴙AlXKe|'KSjY' t5T^BD9yベ9.g+CX2{PdToZbln\Se)oʑJ6b//;/)zH sh ?mB^ŸyfݵL?&SOޮ=ER8ވSWZFr00SjT5?ajͫ1ՃT2h5HRZeCcvwsE({B)'=sPdNcmw-ݳQk,=~E\(@.}7onE|MQ'vfnYZ#(Č\P[<4?s,QUțS Ys>{I2b L/3{÷i)hN\!{VtaT:a_݂K ӟ/rV\t겜hLI^PH3.,ї0J +go2Bh:aodDD'*A aՊvߡޮ4Έ+YS +hdX{vPd3 +ĥj\U0sUkan>AhNOdN牞/4|0$ X M)aUզdD lO2&~->Q"S,SLI#E̜(Zy#q|˕WV^逮0;Dfț!mF{\AX` }.JIY#\qrgKu!sn8nL8GjmgR@[oԲv^=uC>MMO%7!I}|yG^e\gL}85-v̺=t}JΤ"?wSi 'L=VXX s-džԫJt[ bXO RI۲_`&R:WM&KVU0(u' +Bdғn:aq:Nj,nBĻSL"8$/uCxzVhOi-!oL7> Gs [xRuGZ5^oOׁt?#8PZ%=.mgbsGFd۷.з'm`7:: G@]ghɡ/3sN)s[Pb</[͙p~v"vZt}A-<{GC磍< 6(m?T'e:Ac +9_CFDZc n,ȅQT$0c++S(kg( k>U';ߙKχO}hjKW˙#-y)n&-൳2HE#*7| h{`Sn}UFbyM :٪5-Ty{x-b#)>O\uf E:V~2%y/8A`ec]]OIDǏ < +g>/ /7gOԓ>O=9m('g)nڝNM0@u1rk}N?/04g}?uT'8=a}N'|φ9Z\ĈS{fpe9]l#:W=.$Wb(MR@z}y?49D.$.Jd=sSjªV*+/-. j8G|7߃:+LbV9FN<İݮG0@h.P1 G~Ϡ=~gQ<ܘ[\ͦG>hjաʚ woodğj3Ig xibCC'Wmλ$:s-} }cGW\fʵKa:zJhk +ݽ3mMێ~` azH=&9X9Tg *IO|hwVrbURF7HI.WJLv0c:Ekv7'ñ!m?)U㴀h'b䓓2@T +~Ykg3^|zFsWJ/%ub(soihRm=ƏsHʠ'҄Bk:p]FŊkv!MAn]&ٕz .碨+ዑ(a"e2ʕm 5DxpD4Ny5#a +RR'~6 ,ߐF2 H1"=t>אyk,|߇ IA[ +I![ᘰ]Y1!LlIo:*Ci5lŏ#B ~LU `@In ![{d0om􌶣XmO C'\H*QJ&zTlsBEnLd8#':G_HQ: +4)\ާ@fFqZ(V?κXLK l_7sq 8-XǘxJCE3xoeG:8>/_>k^ BAQw0LP.;!TKw+1>8_+hvg-*\H;爠pfnSRq3120wX;S}}Po9F$|;wѱPfm`48㎡N|֟_[RoN` '=h@^2ODQN͙)=/'NYC2x'ũ~X2Cl`wfm8!|=ʷGw)zԈ Wʐ`;:\H}"+>BS%=Z `d 7Y!+}Gwɒ Z|Kl#@S.kě!*W1nyO[IyCSKŮא;U\F# v ^_b ؐx;4l >k` +q]^&'C8c / ;Ǚ<]:;N4 f(6 [dL:WӚ=go~peAٕVL?Pc>e3 e0Bi 6fhμ&h?{/iefhE zs5 <S. "[CJYMWwwA-h-ޥ!}ۯC Cdln ͞e/-b@.JecPUV! +ŠVZGy=LZC!R?R;fީr;gAxE_+`|y ۗjxfT5!MԮiy[JNZ-7(hے8pP+gmmJ#bBvU^*DU'$HȳD{ ?mv3e-H'믮ۗ1REDsG8JIM^(0Yj#ΥsjA23F=΂'S3k"8YU}xJ+A"Yğ3޵@^<JtH{]bY"kʶX_//F-RlX文UGCGci}AӂN%'{0qٺ3!/*%l,_RM>$ Hĥ)G6+cAAl0@rnGgt Zڝt㪳B f}5le&spޤ7E_!ܐg0}CC:,*9wx/;:p5Qs03\~$6;Wp=R R#tM΃C`.7 +o?M̚flMQZu(״`9L;4mCvxep~t +ho5&P t͐dw99`ҀY3*lF_TG,3R(qdSbU~ؚ6b +lknKVgicœ$o+CKyjxL`V{t$Fqy׶;`6?+xjsY^/ ot+0 sus}^,ʐJ9^ +]j]3눋7Aʀ=`J0ǖC̓v|8Z 隒ɿ qݸz;0P6jEϵm +ވ~E5ٲJ#k6E*Rɂ BFB|-oȺQD#>Z"\\R*Z aLFg ^U(wGDu?1(W )PźNRz,Ix\G;|yAG}sŝ 6rPOǠԐ b͆_ \ +~{(}pƽ{5u|ʗӖCHinWjG(9aȓKpx."s iQ& 5uhq/`<2I&s⇷-Êx. HSLgQ7XV(U>Q2r7PQœץ\{9eFd1rvFS^g/Nf[9ϙE/%jf!M̝ 2sokH/6}αzG}ZhQiq\GFy'!~ԉEVŎY*ų!DBjz^L"\q!Xx8a:lHr:"G{V7m\=d?x%N;ܦ~͙}l|yVEW$-L #֗=\f;!E&:v!b=RETRgi"6@_ӌ;ݵaRf{KMxwJC$VUg¬kͷ^IMʴEوQ{ aWRĸV4Y¨OCg5FUM|+ӫ Ç5߮B-{&DE6;. MɅZ03kCل w9h`Xb̥x?թj#猪_Ӏ%蝏 :w O4ǟcu/3am:͞b~F KeX)S$xl(Jp#ؕm1eo>56+&@wm9'!:.O-g)^1zG"8*OK3UAC2:o1b5ky CxN- t8ĒZ0y'ʧ3p35>ëV*8 ++NiQH  FJVWMۿ|pX W+x'85n5X:r1n8ʖsdg&0S>f4ķ pYkҝ!+"[#HXtOp96ne, 5D;2}L'=+NMF ^ +oT;[vyZBrVdz+zM-שA8dH/f|RE4H%'%ɔ r)˥t H+3ѵ`1{-CCmj ث$#nt<ꑀA^ u^ptxutS#qQC<+ws,M F/oQx,m|4Ro +#s4=S(il2 Q^&c<@8;-PĐ{2O! ɝk*ȡ1Lsh,{P0WѝΒ|S&{E8{P!]՗VNMw`mq# 5J_ +|z1yҥIx/dSֶ͝ /O8i^FF\rZ#%V8׭;]wRR9 CG[[Gtee w_ +t? H,^[f%{&q>XmJiF|/P=xm=X|: He͂W%ꊙM=Muf_qmS2{IǮ,i)5uN FD7LSW)b&Y9ks UxNʍI+C-.er9YY`^7ΆC_&:AI\~ClsNcwDыf>o"^n2q@ eV- +ҥΕzW/Vq\LOL5t%*I0mEޕ>re*nlz,Q%K < iat*mE$*Tkd9Q R$d7iULM-v7UR?HU3/:AԯklR%ӊ)5T +$M&[ +;(3\i+%"r%:d.gfB~G{};q7R}*hzdS6A>m掶MMK!k^K}>T&/A}@e 0ڶS{Hql +h¾g)eSpz=恗}kq@ e9ױ6 2wOe`Щh껢e"TfHD4zBfiB'Tzyu!C*t~q{-0(^]uH ]:aK 'x'!A毘yi$D ;5c{ +ZOS{>@-z(e0ZS?Yj>C؃"VC=cM2dVf-zϝd%&ޕ i&"}uJȼ\FMӄhsI4~{LWr&KX/UȔ5<㾗]O^ >p`w_3fz[)mW 3>QfO"!g"4Ѣlts[o@k|cn{>il.'1L ~Ib}& ΄Tw: a[jR|S! J=\v@r؟[_dN"_iwfG;xܾfkwM_ulڟz(F-$sL/\^APp(xoRZp) wsAy0"j4VSx92w^a#v4(,AHX$}/oʄ6WPY;5Yjx)6A^!cP`2**IO Q"~6pʚ^KsFH!~=Rn7D@&R=~^#&\PC"v>Lns$܋'8­m)JHD'X֨*XBx"$|qi`&A &U}:%t-<}<C +zImiBoh_dR:&j\ܶ6}Gud槂 +%(]hQDIVm8XI0qqOz;CC>H(WŦ(~[1KT?g>t랧FxNCD5G}Ae5rIGS iwΐۜ@`>e9BFv-'ɯV/1%Ϝ-t c#=(qdmu0aJq\P謝r#N)e OivUKԈ[QwnnufC1G$ýu̩yTph,o38wyh~wwHz .Q$}+ gD rH+!:?BsJ7haqXӅP8MAeԣ 8ЂAk;&կ83GMaZ&sj}Yy-7Eal5 qVj{ +]u@*߿P 6Ȟ0U/JN&-"1^r(T]gÙqa#LW0SSz\RdgR6!HR%T8>{ QP#=qziK޵#سcL3uQ$G+gG+yP:Ch[;\x$+Á C +ҜPOl.~L;Tn!!$tӇz!NJϸeWa0U1{yCT=W}c8B,HWrJ]B{lT2:b9_,#wܣlfξ `EX8zM>c6J#ؾ0*8D^ߨ'z]}0'罶\Bђ&˳ŋzqR;+®TxsgQJ]ipQjwGlWK?jKYiT_Pbu).V"KP4y'yFS*JIJa+PIv5TzQۮAn]FEZ`,s2¢7W\]D+cDsL9N9 1-d9MCtT"ʚEu0PgʴuvUi'fS~{Rso܋4ai[qh)N^F|P۶>ڏR|jF*1 9jVCOeԙ\fS0 jfV$PHqC^Oڡ~KvXIH? ȿZS(B1u_N=PƳb MŞYלE/k?B⎇B[q2ߌh5~ȟΚ;^+]b` K3dT)r4_Iz ,#IwU + hҘK :/";f͐mO\2鏘SK]wxRuQណ-iƆ&A2%ZcW4Wg#gS-=l39 a;yG G;BSW{鴍/~e_~)H"@ P7y+T,;MH0P@3ygM0.6jMzCl8✉Wzټc+cb#|JL;bI_LC0"@ +]7`W$ m() }ͻP%ᝐx֥H +_XY[+@)lɊIQ $e;b/Zt^o~ r8RgmWv4_LH/3} E"Bup; +Oi!ɇ,{T|`ŏVHX>ƜhYzj},GxkyAGg6F %-t̩er/Q />W#\Hu%bzjx CRE9+8c+ƫyƳ>w ~9 (E@~N8|N2|>:B떢/&Ff­Jy +!GJwH=65HGx +hc]3Gl=ֽY V&<9PVjFU + Nj$%ԏ{"*^!KZ3W%J!tc#/Hn#EѭpX`4b4 T9% +D;0^;U泉[xS8+Lb\/fP|GyT'9ThnTxJ+>1!7by]aMU j e/dns#*¡K)78[D;Uji(x hH_㸿:3"{j*\#FKtGx, > Rfi(+ECXIg@E'"5*IqFSJ~+1TƣuÒ=Z朸0QD, u:+Z}P]I)nk]>(reBUo$Mp4* tl0*X^g.eŘ1~R7甩6+epAĞũV.Oڽm(e:)3O.EYzv 2EU^$@MKq;,*OVCȞ;WY[Pc+U:єf^q0B#E*IJ\o-εGKKBa[6ʍ]$d;VCfH. U*k7'EF\c7_&iWBc=!R|~2 +dxb-r/VG94IC5*QE B7Nh5$ySXVGlD2z~^/g7k'Z:,N N{ I`ˈ|̉Օ6;> Yr!8a[j}]Ea&ve<?7󋺢lH<փ}:qY2gY>UHRfOЬnZ3:Wi- CT#*] ڝ^2E]Up礀t'ÇKUEATMGftTijg2+vԸY%;xtpVQU(s}sg|mgDV5]p.y~kEoJG'$¨qjysD"4bЏKP̃Z0~ Bv$O9&P^HJxx=HD~܏n-d{R qWg<_9iJ[@P] i~+Koo>O?GO^#t/و%ej(<.9g y]莪QkGkK`H#Ed^J5QSe%h?F$v>G#-3|Ng׼s30ȶ%A*\׉|qauP{Z 8"1sN"a nVS00\/_30sy +-U2 @S%#JSZcsɼRvix>~ ?D\@T ُș!nѢ'n~E ~{s@XL^"LU.V~pߧ- KTKy.֔n.3*G9BeW{tQ ᄃ 0C'@~!eC?#2'bQC >ADZ4O Cr')Wk/~E/x,UAvhmg7}HTk;g##! )jԒ}&ơnC:{wݷW6Bբp3:_Fe"d߸bT +P}VZsGc-.,s򘸃ңQ  W:zDص#;C@؛/;B<zujZ<׏8u˹9PGQ7++E + D+g[seR1S?L90.D[ᖜAJ bAPhXL[{(aƢ':(ne>^CKgqb:r#h1{ja3CL?GyXL#  xhwsX HGvφѩ$TzGfQXmO;3AzM/Jw} zWZXA)]op %@B5%tT(h]x"ɛ_!'o +O:C]qkMmTm!tե7Ja"=2 UO^FuW@ŪJ"y6i`Um߮s2!+kvUq`#E.pأ*ӆq}иMgϲV:u;;T}:VO69"}BEU]%t2$CKkA&=>A ˱| 8{dIðQ՟gqjjik;Yɔno?ˡ %6YE8S +OG%1i (<y4}W)7 چH/bzlKkHCA!S4CB!g2f_QӈrS9Bե:Oh{>1MBɈs=냹Z 4kF* 쓿XGLl Rޢ5)^jQ2!ș]EmSݺ8D/*[JHD#L!-go1AiqgH*ʦe*p:㭗{3 kŦ[g@3Rk"Ē8fDE'=f3Y;eCӠJC%RWE͎&iF;=ϟ/9F )˥z}xS؃:kXQh""+Qk-$m+fcGUdeTŴLtnYe{`F;xͿ &Yr?$ i*T$k} +st^Q}0O&ś/jQ5-Z"s`(I-X-^j?2R٘@ + AFkQ-Xi+kxF8zmD~OZI<,f VԿ)gQ1Az<ӇD<rvK'kyb|%/ΏdCOANDƂ~{oΆqTK ļKջL\!![_|{UǾ*AG&c`Wys"Z$|@ _'y3ܙ}yJVhԚq[qicn"f9S8cXEw|&ۘ.i :^,mmtG/}}=lL@au jY+pQʦ{d~j2/&=t8x$@nQavEA4 Ǩ(%NVaSÝ1H 5JAjY>fK%JdrN^ͮrϭP8]6b. OX80_V6 -w- t"O:  l]M #l3bF2ٙWV.:vtm|(j5@ ?ku%}*5h.LՓEjH"AZ:vrh* alh0X:}%py;%Qr@l8V䖖z&[|a NJ0Bcd#,+'H 4BpLl%o7{O03FMQ5-d-=G\3 +{;)) +3Uu;$LI !c{cL$o8i_QDNzz{ %| (O{+4}uVU +v鈶&*׿lK }`S*RfգU?6N~4Wܗc9vX^g}Q_9؞{B!$6k'{™٫F"ۼzVvw4,]+ƒV{L&$h78 MFoɦ2#fhC| x:g9bQ:'#K5DY=z#bP7xem\y8_V M՞8p(yݭ:|9d3{oٸ(9=vr 9ft#GΨ\ #q3Ml>Sg(o! GEF_w /Vn^VޥN_LfHѣ;sR+tr^ y͇GJՅg 6ёqn/yڟ#oIax@VHtȉ#_ Z/e7žKoR"+p18~D +Iu@|xy{!3/zOgP#CdK!u~ĞC7|ޓ5l7Qɴ_ {#G7NazDG;zt#(Z _VO}Pk{4`юR# +.wTډ$x\[N[?N%Z AF;~-ORs7M98 7Z$nCZJηsW?:ZN]hn9TQRƲKdW47A ySR"cCw:Q9&+Xq˞֏Fb@9.cUU ܙܥKHZ PD; 1TG3磿,H=e;HV[RyFY౒"dʆ>.`y{}cj8#n+e̳"aE}a>̠\I{~SaPSy(7-ݰׅu,9|7L롃XZbQЖ1*y3\:ThQ+#㣿$XSaYxTEkC@e旼9.EFa"!̥\|d /;rd<+USG9!`1lCvE !FB(1CTU84-t5>BP[AVU Dʙž~s3yi.h u|HB |Yd1i& b dRH'\.h@Y}}o3%fa*wxUM'x +̦+4;ݼy L}'WM3Buɚ0iH`q(wPt)7؜W=E9'Y6UͧZEӕ{2.euE<'ȡ=c!/J%S*.M^n-jF!y9ΜE62cdFyu.NKgu + ""/5) +Aj؈FM`oɰylS~_(^]$ +=#9DhEzw"(Ub",q=3o5NjpIx؛uM~1SKwz[ &/)ݴi~kls + $|}tj`9V-ڬn3~Zl.ӵȮc |9g@$n4b4Ⓝ'Ojk@c,Rrk +/i![w5:wZ)|+-@zccsZko-4#%dݺg+Ψ*&*4<]k oՖv5p +ĠNT?Ra0΢`hU͙#3Q3mUhAcfS.犁yb=ΈJ%O< z19kR]G{R$ALDbJ\`rēKWzKֵLGHuÝi/~2oT/aYRăSҧJxaSQRȹRWFR05ElXG`@qEՙv@X<k~fxBdQ@$.kVyLD#y!q_F;6HơDS3Qq}i@9KxD2ʷRizfA6>ḧ/1?}$- +3(iFh(/OV`{"Ž +) U Pb”̄k!>/l1#y( -0T@^ VG$tP PfѳRRJ'5\^X V}`)Uc{ (HkH~sFu0MbX(4K BH\/!<RM`SxN + +۟jEhѶυޜ0>CT0z G×q>3ljщcyb*<S")7̩q@~T]4DfaQ(Mww}ni;ɖ\Dz;L" ٍvN֊b}.D>5L"9TX)o'a `f?<_B$VwtPˣqƛ)5ڣۦD!=&B==] +7iT=Xՠ Gﲒʙ})UA"lގ|[آe:C!=9' \qljHJͧ `v_q8r{ikRH]?íL5[Ōdli.3:^ތuy/6ZvBx1U! ֍=$dcc;έՐnW]w +P9z&N k({gTA_OjU X?ι̕}~pz-*e+ߨ.G%>mp"k;GoyV;>La['X:gk)q-3'>6g˓J +U:k& `'CiX4t*h$ggPw;#F3ZHE<6ܩ^jkٔ% ,]O/[73Z%ؐu%)pvT¤+akxP՜a;9|чRxn+3;*`6^zޚ&P +2hFzhP`(F=s"Nͯn8i^\A~^.4w(E +\#RG̰fYsdi'a{ס$69 8]μxy!/ +^~0qC}&%x1lR{ŎS)!C9c\]g*4̈ qgUC2|tR'5#sW03~{'u9 =yLd¸eBuGPHT u3=32~mW/޽ tw>)mCxjйO>`C\5Ϛw~ɴSh{|z)3ߛkF-8{hZ%æIgHD+j#AR 4k1pstt8Isy 5 {Ѧ>5M$fJx7 ;lIEO'S5;QlݠRdWM " !Ql h$Xڈ@*2-=b9?sXO/ytQ5} f5"ULJY/*w.1ΥޑBC'~xi-ŶYRrhGx~n!ލ%յ%žv7 lЍHP FH/+|8Li -~C+YPꎌ )t/jJ\^^j$)dq(>ۇw< 7z}r|ϹдFBU r> "`Ф8km#6bmdbAdo:Kl`zR.LAl4CH%,n9 +9HQ1HEKk [ t+74~y=uhNu*:E6B4ZDut^=fC]: -d0 DD̬xk goqhhXpWow'xK(4'"k 7G!ę0eهwZaŷs~V[]Z T&~T h91NW,B`X`}08hG_wl|9=[򍜈;Jя|g_"Ixʝy99vCԙ:/S\q"lpP̔Ƞ[yi!8HA搻:D!mff*DKZ(p;qйFU/ݘ>SV]K"==:}=҂ZP󵷞Hr%XQ)Ly}V2YNib/|8(mO-ėܲXKV0l5'ޏ?RWq.7tG*^UZ yCƚ4) ZRbj`FE~󱿗dx{oT5mN`RH郩DYHHsjhIUZԖFI`%JiO=ەU*vAkX%:}]9]ֲEӋ G` +ѣbpLjyg,zau o϶~tZ7) acz}vͿspɘJ00WW"ŧ!`\v֕[VEtPCݾ- -qǀr쬀aޭ| efQ$#QL 'nd}ryyAqϬF;k^xi=FH)7Rs{@HTk(%WyrA)IoM bG`O}PdN8[oa%ʭh}T6,nDn9?;/ :tp /m%3j +ॷe;ctc(T5`J D`l_2Zgm+^z-_lPz?AF-G5İ@j9_#5noch\A{K2Ϯ8x#+gTj<]DkCPg~n1$6va"bβ㮓n:z!imDn4md.ScpI~у7 dfZt>(&/t<$wR"Kӥo8aAxtKum[F+J ~FM +nu +j$A1$9?7l +oG'rPpwͮ6_rSӇѺ.?5| nS#pե0)AM@Sԝd0I FBA"a](~kIB( +EXK +>{™]5h4*y|fG'Z59d^{)m:c0Zd/䌒 jLfJw}8Pi_1k#,G-?zB#G\T\!WY\O Attй`z}-6j%3on'iupQ7/$K߅fw]Cv%Ici>`7%Cs 6W zK)k E;]yVF;!0{oji$\u70#eѷ׽ +ɷ+fz>˳; =pFJlf-.jg *y۸ؼ{B弢Ej>n参-ty7ElVZK5EjaQj i)p=@rW*!)_̃št͊Ml,{랟Be M)5՞!’ ɥҊuȼjG~j m4"$#gaDELFT.1Rw[c nҎl*3j>sjZsG + 6О {=$Y̪=;h7yA;|GTW\Rw>kB"* P:hpߥm{aɤo;/uvTܹQCwD+9g/Y*˖. +q J4)C&3񿹳0Fn pXswb6ͣI2>m#0 Qo^>.10 mэUD +kxAAK8nhO<^+MQD'v~L1S"VK5ё+鸚y8_#ၩVבRN/׈p}gpDvۥC̯XϭAn<{.8[EpAmn{: cǻʫjc4b;H1`ǘ|0޺O$twXƩ=H]W !u >"E}UGm;s&U)e<$B;! +vYM"={jr9.խj|m|!`o:KWA*:fpX_0Dݏ4vnBv*e>}K0l V4C6,d*@&j<$+;@bXsnVv 5gFM++P{6]&ټZ +4R^Z%K't$aڵvD*:bN_p=*aWOZaybҰvQq~U_ xsV |X='B9"l'걓jq@~;)BV֧ +wuu=*l%0r!ZNT(r*ֶ2爘O0OGhܥL>Hl7'FaQ,Wv\h5TH#b,K6*Y=ϙ!bO.j-}pC(k4o[X0ԽdҀueOrM}XĽDK<$O$6<()gQ O8I +8KgGV +Wػ㣓#*Lՠ;"t'%4!μz4XZ }/'8ċ٘u\\'t<ڝ'BgxTU*U:A=2h#';PK> KWMkVO)X`:CŅD +B~s͜ +ELiGI#OεP'HAuW2cͅY>kfbOV3gN͕a[{kߓϪ8%VD <•t CPzvilW21O˞ krq *SC\;Uw| GjZPP=EC5O1F5K i.;ZywJb-nUqo+=&&t8l`Vfz~;YJ +яhM r#U.JaYgjJ_ +D):ܪ=ؔ?c$dVnڏV(aPxg\:/F:#3sAFZ@j};[K6O}7[ĹϪd6!Y +Ί4!h9'Gc*\ E~.+%xs?l0 zhΓz}0Ze +=Z15x +$J3OQW` +ﵻ//EuaT# T&FKFo6fs&JPDsV?ߜsxܤCGhCNY[Ǿ2fg׳ PU%*D$[J.6]A^?4pK:nbI*t 8Bq0wa!.H׹t6kA ]7v}͆ 1l1JjoJ +=]Lͨ?يPG_O Ǘb+|66W}%.:86Mki$k=$0{ۥ?œKtmVKJ?]ZWм[(/|ՅjvA,!q>SiCa=ڸ:hAѵF2)V~5N8V=KJَSyVE֮'v~o{d}m{w:~w6;9U%RFljAIȲ?#okuwaͬ\X)WL]s#%q7Mo:sekUSDc}fp+B=;f ዑ1Px׌۹9{o'?q5&U@?-3ˁ[={DE0~˽VQ[Q=3vΫ7ԗhj3QwFU~'w{1%]ȁYtP 8p$x^Ot"(:w?W^\Z܀6)O[DMKw~W9t@n ѽFe˪^@e~%0+E9C|uJU2V=˨/*䊭RH?OZy-HA<ZyF^;BQ׉5m_〼] $xLjQ'}*QNDpc>XRc`gD3Njr ;w{ Yf@?@)cfST?aZYH+ :Q(2%a,u +va7A&UU LfN4C߻I*2ذ|uRaqCTc+R>)$L +gazS'{L])d!79M]>L et;)%8Eӳ8"9GAˈ5Qٿ%\!m!S؊GtW4؂ds/’Yamh~v^Y"6;}hJ XwFb2N EUB*5W!qcx~o|cwLØh!fqGi[D +\5sR dS$2j:\&zL,=~'U|qs +]/R ![kB Y3&hP-ΑQ5YMN'lRM]rjO̶7-( @3]r (Le endstream endobj 43 0 obj <>stream +mQXNZ%jÄonl42B别q4-&8la=xB^M +pוp0!@[Y& EP >! r'@&_nJgkgbݝ4%tLY=tZ̓4Kx/<(,T^T傆iPg c8 ][:jBS"moqYYdOݤj_Klfd7Zqn5b  "vMWf'gE"b[ChZ5ڔ]cxkk"3֓y!/xNӉJrK~.$#&夕!k^Yc:@'D=4=L)wRCt^HI^sb +ZAg 8vgC)m/O'M#8ZD1|)%#*ԜW\ZR +RlZO (+n8 }1 + +),;E!;}k\]cZ%P-(L[c:dxF|G#_b;l!^7 3qT5Ŧu?׶ |PhM3*'}H^<Gs>W"-^ڠL?8ܗQI4͆p]\7cw=cN 5F߯W̃VӚXѯpN^6vRrRIp@PN_Z0Y 7Ƿ!Zy"}QCmnAcIk[i@+@ʆxjq'6زr17='[ؑd5ۙZ7nz̜*:݉r6lͳBv`YRQT%+`c=wJEb`z 9rbo@LQ(]ˏ ?r 8R^ֆsSp;⼕eT[鋟y" Դ@Vl :Ji:@ܱ6_ UR{ٝ/_bng,N7]cFQ .MY3ifgxڐ%n +!6{y&Z4CHƒy ]s\G:$kI6tW9w!-{OHhq N˵8dxƎArOpKbM^@?=?<'W;чՃčN܆Y/깱j# I_8h%EO>0(ۈ6$٦@79/7#^)~? l1$@;ֻ1y}Â>#óa$Έҕ>Zf?{1?B,_xBی@+mw *V b@ + HckL1nMp5lqG:>lwGRU v,*S+]]TNYrOIYTEn=;wBU|7xhjWDh>c4'$wWK(}*G=Ȍf/T[8i|9!.E ~R%X~Х~W1]hx[̨pCԏ2]hrs~ºEBD쬴9"eUեg6(a)i!7iL qh˱(~Fʦ-.Π\C"qq/j|&"gEУ֜׳_ m{ΪrA O"(m(;fW$ *H:zS`6gz+čâYB%)L#IljY{<xmʿU԰BWN6 -$bʩsѼEw2dOaxj6][@*Yi1sUZZhG +$pմ9lR9۽t npJ4P/)|jOuv&3HT H6!gQ٢.Aa^f7#xW6 +0ΑnPr۹4xoϷNΊ/n蔹mGm-K#k m0|W7 5J!1O({N1 o2+XS9K,fPR yrW&9 Yil[DUK: /+dMߑ\CNCO {d:\hdYerv<"O3Q6j' +{+5g{1>I(gQf,a?~&JQs)KȆT< +g BO͊4 % +!g2$] Zҡ3sShOvL$'犪ǦȆU2"Yڕ\J"o;=JWMCcЕTL67YUMbWܮ=6q^¡V=ODW9?pY|q+JP26Qf",q3T(\ +fvޡSkTtZ#X.P@Az3j/rABrdk/\q/#dg}y7gN{{{G=}] +sМٌI2P(x%.K1P-1<khE&(A3xoGRHv_k;CL*ϼW +6P3V|'2)N#3&&!8UHy"8}NTD,G5+Ȭ_ns3|:iy#hi<= APVֈ.`JO-sSJ(4kk!l2{j18ircR{ ڵU×?!G|^>(! ^>.wyty\~tIG"roc`6gǝm!Y #7ReH[D4nhAZt"Dcw1Q}O}oPx -+ՙ0 vΨo!q`G t!/m\xL| бn*}3ق0bY,p} 3iNױc(mLIͳ鴴sZQA;\L&`يN=փ (:̐]5}#^'Fa.plJW|vg`ؘ ‡aY2)LyPMejJOcPX@߱YǛWt_֐[xzGCnwu퍢Җ8 143`M=&5QzN:}g/Wu/ HOsg靫dGBQyl#^ vV2Ec9R"vhvTmlڢ/asM$MJ>pċ͎rBLwcPC~>YVo-3͙N-)~ܰ zNzldŽFϙMњjC[vSyۖiR '.vfTCt3+i<54SGGԣ'Pš$)5NOCW#z7Xd`B{s_R (_*Kf!,& +^r630ݲȕ2Kf~rH%r]y7nd ^#hKtfm ~o 3(r؈) :NNʏ "Q{|S;h7hEDPlN0*ڽ4(ת2g2$z^]륳;$ʻhշhig +@vItpd[@@{Ť*/7{&ETBgrS#ml#]{PxEY:<}D37h00c|U4f>E= +jl4 9;ZCNŽ@ b27oS`ݭP I pbaU!;n؊8{d͛ĉV¶\ء;ׯT(ocKf0NHt2*Qw ,`h3to[b^@m *Yo@T3U<*^]u!Zc`HSIqҥ00[b}9O.NϣtF<=lTzQ؁pg|ҥS=F\8`\WpZ(!l״(<Ԙ-{Z7(/[H;q''ʧM\Z,.8ijX@4%Й%v}0O%y ⅳ){B%ڬK28{$/bHR@˔--0D9:99as;S7 +!/ ]l/a2ץ.Ezx7Z&<)m<4>Po49>P="q3`.Cc/X`.1Ѕ{;qGwm82Aʨƛw 7ε+P#8rE- q-f¹LvlF +hAN/ƲU9Uss ^pLuqWf['ۮ'9AU o> ώ^9[s7$%T&u) Ml%| Db[V@'a\^[<ĝ{[8X.n:`C5"Sfj2V+|+W M,nfŚ jGRUtMZ$xA4mbY9 uR +\WTخmM>&}ѩBo+ +K43LNŠ0L9uѸ<]h.%멁Q k-4qDGQ ͇qTVy$6n "?Iwq+|c 4x6'o|A'? +֪-3 c@M;I`4a2 qF4V;&m7q"7C`(>geʸ{Zmɗ>ema^Q'2t?.vV9B;Xn~zIHK*bo8[gJB +,{a@]X?)W@Ωj1k9B B\:>=DנOKc֋юe>?u?KTB_^!Ldy +'4sn+2G4s+(:M7ji"/|MÇWM2\{vatFO)I"ݑ_5i]2{bN ]]swS,7 CS)W]]Pb:{duz8 ٽM:RAH{nf)m*⎃ihmMׁLfTЁgğ`h-ҧs".sJOGafK@2eP57iαx?)W{A6(g~՘6ܖA&gk1`z7Sj/5eAS07ͦzt~ԛ`I9]R#Wm%cbfg'eڂZ1ع%j/IOf|/ 1$K2&@;'&y5{W,̆"__PQ$1&sN?u۠;=86]J>Pthmanߣ:̇y*BX5v_| 㵎4y}-Rn6͹ϙ(sSjnjHT2[b ʨzS2o+X`i{76gє9_|w y!g]zs]~@boL: ?L?a{h7BAIcb[ 9ƨSOLb>8_dDn;ȳyb`d}s4 S= k9s#niQx w#Wk>_1E9cۦ-K|Lz G}$quK5n'@S'y=@RpKoJ {w#v&ۅrG7mg,,7 Dȳ܃  +7ZkځjI3z!A!xf AhlBu%'cLڷL_ץtXL{A'@AKPC/ac}`|u<2RK"mw:WyH9RӞu~kw5sbAlMANsgea#l*n&9 FKhɝ, IC,Quq21>Оby!-T@oe5;9K3bR{#Au)p$x̺^,xaIۜɜ\s&H!z yvX,_KR?֌@v{ɍ;i,TQK-^>2_N3~i>T"(wٮ>N'3:D( +Z$Rf"CF%8 :OckGV[ 4}xe/5Aa 9h7uy$a[t^ՠ#Ki{wUt DҐYd-0V'bOq ^4Rdb8w<Đ /Q(Vu˂btuIs鲨[y拟 зBwu?N疔^}-\ g7,kui +|jj^'̾U9'٧,"{0mӴ=[ܧ k;= +A%Ղ}B؈jaŹju'swv?d},>asR +o-c¤6_!byT~i2R̞e[)b<S^jdT*Aމ`$-+ )xc>|\,K׫ҏs9W1$G[%F#;G9iKMKVRӋ%z&IYy)E ۿԘs~WQN4CHWdrX2W +\۱ BCSQ(seCv^;B=ϨTqN ~0^LwgDB{↍5VFDT[vT^>k ?*&t7̣*Xk\ ُ3bmO;DZd L8jɄcDY9Q /8 H~5(س\6u y?8ڧ _2 -T lg [oa:Ψ\\|̽(~lTdiZԊdVWM@R-y$(!3Ѓ(<֞}]Yb)2!7B2g0>$x ` siy=efBRb3@%p~&pQ$Ͽt|n0/8BG4z/o2[Ri36".z5DqR85@JKN)5PP ~j/,>g_[]M#ڸG ̓ŮI[ NR\m2"R|QI(S!歭ꡞiCjo "60j)#dptF%x%c(8hxA21aR˨auYTWD~bCMvՐYmooPpOWL |7rhUGldd)U9m&g5դh6_Ni"եPR4O|s8᩷`m3|T W7q-M(f{ԤRIU\G`=F%kÉk 8[^k}fwGOhG +{4j]RTCq  q2'Թrי^@_}Az#żKJ qE+Y-޻n./hJī+1(ePu} jy7IBikBhnܫF{B Pp|Ͻtez9'E?ۋwsm\'3C6g(w0%?U{ieԢv0; +̷'UvfD=ڴ] 1 =:`#znQDz0R~gȷU=0}mt/Ñes12k2Sp$v xO. m} 3L DDo}>"-R Rݨ܉cg.Zy ϟs ӝ[As!2d|zO7@1{z$%d\Nb =n*̆|szը +JR_u0!qe'JF::كLu *+xJAr˰hx֐;ŽQR> ZtX|<׹N;K*Z~Wk%9A@<mMK}))URkHT$ȫ.GI4(<_&_ Qz^ H#/=:$_$uY3W _YU@Eq'z5£#<YjyD9w\57u%<'Y? ]_s])UnzGqׯ</K^1`NSu182iiXkPF( 1 HHCvL5䴦 +RRD6V'@n9rX$u:rIxLޓn A̿:e)m|G<)3O^ӑDh:E#RdqB"~ +(gh+ .>3 2d|r*+^1hwa1XDXp0?}u WJw\_ɐKm紐3tlO ; Y|q4[te\Ie m<=]?GU3*:xܫzIey REgN즘DH"9(|H(mZj69x?KB1M{AEp5[Jv!3u|CsI_y2_Kɳ[P :85wzf!/>x|tDc׀yEgA2/y+$?dCk60gy& Q= (Ck3}--QF$lhא;ա$然>C܅Czko!W<&RH8u-rE!}oe;`kPWc6-u552{6H0U !radCO +oL1JKF +ZTF;eA,DQV_3s=rb7^Xk6Wc$<Ë-ʦGryN :yJFʔU_%t2X]Af&7os +88{:loiژk/X"(eF=_!(L'>XOo2tU)( +_6+59%w#l, =?˥T6WU>X叶Rߕ,DG`p7BTdu[TnmssFܺUoA(:xҎBQDf5 +;.զ0B3?b;JQtDe=1 .b +ΕgNx c'v0cttV}H+V%8l=3Y|\g~ 0ٷF*\2] hk+WJ\(XUO)v(gyU1QBAW!L!Odaü85cc9p< ȱ,/*%YQood0出KbxE`F!Y0+{5Ա(nVs<(Q將 -}ʑE˂Xl䯸ʐ#_4rʳ)D%"#q-fv + +|)~BidҴP3Fu)~u,M7|$- +|7EJp#3݈{ ^4.U?.3;ke +HfZ=1>zDXⰴbR({Uo+{_ɐMу?E@q]˸ //eYgYtт?|!xcuIwX^cB=ajB&~!$7ʐ9G'l am)l{r)!0JH|yψa^t{apڜw #my^u$ .E`cgl.p͸wudg۲ s*w9q7ׄ)Rd ~QW"d0E?};RqM.G[,p EUU#>DPgx6Iz:mB`JF3#f m D-ҊFmjs3JKDMu! gxkxBf8bW,gʐ6l"[~#mK+YSp֥T 29.0Q(`XKJ>wn~w?8g(5/_E7;ᥥwjpG.;V"WQ^q:pO]j/*s+*`t!T|ђ=ۥbuv&\W$N!` A߮e "J9q(WUp|Y~vT0DtLų~E{B%[ydSo=m'P3?[W?t;͏ !r@@0ޅKc;JԷH`&L*V [3!a.mxԝ& 9\*tm$Kr)5'=CNޅ+!pt>KFsJk`u7>&[6-F)2#}d<֯̍t- جy_stH7P%鳃Ս?3~ j᳾ݰGdSV]bdh T5O+ܾݿV2DN]m +agwΦ29tD'jQcYvDz\/K!v?hj=!K'[DK' + * + +M^LN=Y|6,L]us|As*v`_O}n=A!17[:Р߷dNv#`k=dA녘Ї]Ȯ +taTt#}W^:O JT$ ®WxM.po,U_{םwb1@2xCŽ.v1%"K";^uxk+BDFDopOpmrSPgs|Gн8uOp,WQZ/Fdͮ[8 "=H9E4_|w^9suiۻzP=2>:41`LtGpVn<83=c.@!mk3{xg?rn{3X#A$Q.*ka~&.H2/-GNIa3sZg5`=϶m~5址1gjHLH dO*71 Da'T^-U8'pH,)y)AJ=*Mdk;0*)1A%UNQd;nVEU^eBIFD5j¼b # $ʍ6}K]VzE[6uim; (`?:O aX@8 V-:-RJɥ6Ro_bdu3uyĘ;6QE=b<< #N?C7FC!uZ"Yt@s={< RLz΃qlsd=]z5Hɶծ dW1[#C~nO2YU?yHɇ'am"zHjFЙ{Y||vnA_Vm5ip&y7d68uYKTx+^{ +Ufӳ͈/zg}3نxqXπG{,d.l=eӤ"Jl[LR6&>jPcyrLDSJUMqhx^1Pj!yoQ'Px +򨉫2{y$ֻyo<~^p*Qyi]σZFTѨnTD(A#az'^%Q=%`yeTe9J_ل&Ǡ^يcA RV 'ś?5yl:J}UDm3¸hŮ! @W3K_EYBM)b3T8bpU/74S[N[3jVzk9Zë}-^JۅU6W7 `e;{Vy\#7 @88?~԰5:qt8Ü3\|w@{oni@_j>)(W_C6#΃cn{ٺz!6^mdSˊw 4-g]Eqd b2g~KGdVxlOR + ^Ϥ]N8W\6cߥ:\*ϹDSbbG/xN `}0TNo|~U.Կ[E\W_`pvm:a($s3Yd|ԵTj gj: VHDyۑ:D[S2mwMu(E2uU[Yoilϖ~zjNwL)[GI("_$1̐{ A2s(ժ38Sgj3잘c 0!춧~Tˣ0&~秴oD4t۹ @5ZɳCO3Z{7}3Z3ՇsϜxhB؛!2:(?:ejV%wܯh%Xq9Z& }g98}_?n(LKWa8:' \#*/Sp +95H `9'Qv<Ɉ[xqu t_!̉Vdi4K~Ǝ)[P9تlTA=iVFfiTqzDA(n4EV.ܮx[dxLMk?l#t@fMpТQt2;*]S'MwdʼR3աՓ*R6[Mda;H/Q#b_i)tsF^?+83~-K+L91A9Ze7.Pn~3YX`%˿x)0hߪ l-滃qOȩn{RC̠FDA\| ( )TxG(^692ƿݖ xUʊ*ҢImݞw̅fR49pC?+2ha"& T( +cGDZh ŀ4H!qavs+G7o\Z\Ěg]'f$@<8Qįe6z! 4{2KXFÆ#t,`‘lF*U! + D^ @wnPҟ]b.خנ. z'ˀ=m ;ZU5 +昗4.FO~hHY<*lk\Q\U^v }94w^u~RF `^u{]~9qria!Uo/Bgt}bRM Kt|'ue6o,H̐C6hҟۇ6h6zS >?xm3[f0MZ@%TT0kPJ`p{nec+_J2I($u R-o/kMpa+ Nw{J]2}}63CxƧ_ <"W5 +i^ȭ(UQcQ_yKi)[0%Klܝ+ksP3e U;)ͶG]nΞm}BnsE㍇zx[aHM9bɓJ./EX4B01}/T<_nwi6|%LDzTu}B.^w q [X0|XѨ>3fˀd~[L?P% _I7zEZlW +_f/2ڏ^4#JD{Λs'B\%o 6G`3q[t2+l<`l80"j Gd{ \'P3I~=W 84iϣS6VᲇObk^1KV4Y=j0ɇ0=1gx060iJn +Ոt+M9=2 V#Z-62<~`aH\R[N,Ksge,XjpE2^2%jVx^?ݸyo=:"P^~zY'BE1'0ԁ"5Cs|cZ_(5GUy=?G1DLl>kfLUFmߩl(J s $ҟzͯ!qd{_ d8w(d#;]~c-"g oSczDy`Al 9106K[ :`^>4WKCk5l'wd#N@aun;xUGE&r#q^0{(R״4c/+yc +\8Ȟ)49T{T.NEy#7p<_x/*O\7ѭZ\&Pe>Q~&6+:r >Y4wKjIEQZd`E2(Z=R|N$ \@[,I ArZW +OqI!mSykM ($_BIg_M7! ^ԍCΙ?+&K0ѣl#"y{t\ aGM7Bω EAaB296u@ 'G\/Ū1BG{SW/[Jv! m!KEԑ➼* +\ &#kbnӔhawf_HHx[8;ç/W:;߃( J/N}ځDH4GЎ0w$+Ӱ}r#vHf!Qf|b_B n4 +!3eG_ +U_[2Ɋ"v@< ^SS [E}.];.@IP)Fʆ^,+BtZ .As, Bhˌ\'b"ѱP-39Yl)Po]: in/F˓'_/{,KhH;v׏@z)~I& ~% :]?~|GwC)LBed, rz E>G ̉S,x=60:3P)5ō\Pi_kxsy=B>aҼj!QJG$-#bJ9^#. Qr*s 0!: lU1&Uǟp~?-gyb~VC ]H?#0᜝ݲSQ϶Ey1 x5\ZJ~4Xܥ d](1*g?GF-Kȹ46>F34DZ}Y%(t<%Y[mgN4Ήk櫴PS;l1kgFܹ~l9҈g&!~QȨcf%X3E=5Yxݮ¼6kf3|rqqR#GKVe6ޑ-^@U:@+ٮȭ',k-DOVd8OV=%Aĵ=Ի,KlA{aYt^82GI~ڻ[g掱NjƐq"gb \aPbzcJ]<Pl{0Ԁnzg(\@ςo?ԚԅLS_ <4{l ZdzşբQ|J T1D4D'l ;IY<"jێiyRlfkHQ;:2GMwE'8W;w@UCItKk ^7̏sE'C8ۄFg>ae0W;uV7dr@^  +}0uL(:֒'ڊ)tUL%}[B~%!rxxXN@<+XMQw=Tr +])NmJS߾iP*l:O2*I‚!oUxOŔ}uFj|bS{̽>w={\<zu4nSPҿQ5՜ջ;9sb.r98n7Y :7NN<62爷*ϥ/Z9"cqVsSݚ7[/=3E7?zP[\P,cy]xk\ԷS}oߣX3*E3,®A^9"M|1Ǭ柀*7VX\"Id'ɐ5?Gƌ?bmqt0, t_M^ǻ(SL[Ԝ^+:\fL!wqbP!Y3>52&jʂMn5CFMn%z.RւkO!r~ag \vl[r61: TP +P,g\%٢1QAI[*zSی(-梛܉ (v/LYoנ&j:n=su$H ^>V'^Y2O:~_0+" r?EX7$S뽨h:NIe%Ǟ6ףyka{{cg=/VS;W O!W:vXVQ[ Ky0u!E+Ar}^UUC@вaǝll&r "j>zN;(0}^Z;?.1~͒Hce/ssZ5y_RdWJԇm"#b t/ig2R_HگW͊y^aѕge"'le1ME׭.\3]sqBE> +U룳!>cNَs;Oxi*de)j+tPҿ=g/'R ;tpݳi<}QIH!3I(T4ت@v' im0}Kuo|x !}>v7;]ރĴX)*YN>GL(8Oa@sD`A]8^<$G{} 䗨٫Ȉ9`N种kz,=ɒA6c* ֵ*]3L2M_::j8gNltW"'S 7bH*#)ի7b%,KK^<! ;s +tgDa?l {,}Γ*^ \K/rBgSC)4W1TŁ8[%Ed)g?W&a*,{"yVO5&SXHn5K՘UӅؽ2 ?~RlB=H%bSȑ>poOb"lsG3}C+C t_ԳRWyV7KIsQbMk*]{^\o ~Pw;5{wZ8¨׽ng O<ad+Q:Q/,XZxDR ޏ gLa<<ԅB&p|3v%TGUoNf ^S6]:S <%ظۧ#ר?2g uC&UF0 +*+/q.Zc lPĤ@&HS n#oN%g.sE.gP@!8-)wN5}rD$T#Rck@nsھVֹ[GHxw&ώ}AF1/}H'N{}ςx ;eHJ-,뢼 ۠ +iǢ5?%}/OUF*u H#?!$tϪ,QDiY8BKWE>Y(Oh:V*]wyht?%,;[X"[3F?EhQsRп}zX3"k滫 />Zm/#p+'!%rm8U~d ̿9U;Uם1nBE +kC ɼR?΂nx'rG!~1f]G.>84胟|e%lAAz124u#P0Q$:vUA̫@G5t-ӸV[(m|o|iߵh g?l6/7c}JxςLwȴVf7intݸ'"yS^5$jDw1^Fkys p+XQNQ tZMJ7_Ps>O3]$K7b-RQ" :ԫi|DPOE&׾nson2Ć +c*ѠgJuqviRc36}Ouŋ(w.Y _٥\Y] 5;q2WǨ[bI ѻz1#en)T\/(`Lߗbt4Xoo^X<WGf^-5rn:"j'BU ܃H)HUGz}:! +eHϩgish9& rO' d E)ii'?zu%R#ET(-_gzLZK pٲO*"UUaSĦ9[G9B#8^y}=g: =*ul!mk!PGP񠁄€顜L qQCdNzgXʁ1=&4Ff1jKs1hΈK/_]f̴YqaWUr֒[I~-, O1S1L#ͲtmoG"l-?sVZt 72p㍐jPv:UjrcnU1djr)=afPC5og uP^1=s'I"-n8Pܩ %4g8޽F m+CyRO o/jm_<S>.L + ڱK_gDzKwN* H-/1rY%K B_NJlB`b)B魋e^C +-^UO-D({J/9w"lҥlE]¾_(GY5¬wQ On^|g?&!c8AvZ#g6y; j=RwBfJVH7<\#;TʫQQ@Fi ao((ڷ:GY V~p8kDGN&kL'Q)U<\c.`B#CF[ ̯\ +p*_oרsC~=<{t šA&)i;k+MInN>Jяe0<$;?ymZC>%MPDׄ箢Y窵ڂt#}܄^z,MȞ}6_K*=3! f]dy!&GBt˾2~˻% FwS !Q$,@$ Ab̈́^2z D!p&tVk+V'8lnGž!?z3MܘU' wGijc.WX+9I$IzEDL (CzWᐋ^óZ;ހ/iHI8ɥI[yu+lJQ|̰Y3)0|oXfo\Ĺ+9AP[t)!}w'Ƕvr_i\/O2o& U9@YϵF{@(j;<+0 +lnF pr}+uGYtvü<Yc=/*9/-9XU57rũ-S-a%L 1n@ʲƽ1MW@{;Oge P8+F/*̜2b Q|;ң*R7&kzl^0o +dj. +*әX=a* +Ad#[*SR@Mu]A 2Q2@ƭ\G1N R;{MpQx]pnm +N Cy` SI$sLlOo)  %ݧbl~J{Vvx{2h4 x1pGSv6 dTNӜODǠ Y_nC\0 jNd?r,_@v0(+S+>EKgb5n[WVdŜØ-ZLZ;~G +Ï]m艚 ѰG,2 C[)cpf?9/='F%{GgCo?W:2y/BU*CG2oZܬO"=t!|( <{s%FCO=X +NŸG2iʖe.XmVw *>.&3ʜPFb0uZZ(#3S+ԯ#%T.x8˴U!.5w_Ի(]!)<'ZR!)їzdȺ |C\ r7,`RpG!VG­p{O|u-S@~mW4!.@4ԢFQzbǣ\`fb"i[I{_;8/1Ϟ9|3UՈr(d&<2|^rO! :)~,5舞N39IAaȝ~Gơ͏[c{xWlh`GJi"k#s|YjaVU(#V+M1urY:F`s$CFW,H=P,ج'잫7m5,g%਍o9i2 ;}k.T yj (g^my@RD)(fԅU̹t>)b֭1-ĦW"3yD/->_>iT\ޠZ,,c®Yl)?ykx1T\3\yiO_ +>ZJTOgw +Mc] Ϸn, tnP$oZyBbm7fQZ,;稝, مq;cm rd_lr;jO␛\X젡 C1.geb7"<^JA`DX&0]xY ŚH*1؏z;0rS2p e2rޜVxE~:Di\0=NxֆR0`7R +f>3hΝtgB"0?#{Nvx '%:/A=4پ;ᖋƊ>S:#[u7/=2!T%3[/]jB踔 +=;dN*Z Vwh'B9C[K=)0=hRcr9,jPAKzS5D&㇥?eX1dG}gH +Sp Rl7 -WE\>?7} O1Q A*O{kky??D{W_[:,}P[QG;WL wYn4?h +}rEI IBNCs?sx cYge!x>s}cjw "'-x޵ˇ&[\_YJ"[vL#0E,A i?\e؁G빟dQ3qX3шQ. +-E͜OYb1Z.DXldٕ>~nHF5h? +.{k틈΃;=S^ωccxδAЁ~SİBY̪*.LUzq|+JeBqV[h W|v%n{I^2ím{JL7K'k!\Pm$4 Pn4g:Mgl}~c>a׌X%(Sɖc4OM/qO93ڱSX%sL.=ҹqh@2Å*(Q?l~RϝӅ$CR:3W tsK|SlL惲WE;t.6]us&OU(QP)#Tv0_0#ܙ<JtWK-ɢ ">V{FqƐjثw5dF WOmQʭlQ$ʘ4u>uc4Yl>)M;` y .Bis٣bu r0 ̝ш2Xha^-%8 ;21Ty5u/.]Pf^u@F=N_nsĪz .0/,ȀRXDb1e@G但l.6b_Z,ƪ{D{{PFmLoftH{.}Fu<0&ӔC:7,3隬4O#a3#H7CL2L9avg,3cCYrm=1v+ЊUgD:JB̄uDv.Na>Fe}{z:䨑tMmH͉4ܱLviM-K0@(;B>;{/n<"+1(,4#C`kϖ)&Vbnbdu{D`SIGxM>RW_pF?_qLx 3|Q,O&l1\/kyN-@ KWbEdLKN`D9,;: :# vXQn4>r/k2rcmD-7LiITCJaՆ(ٱPl-'v:X[[ ]3:%Ix^yGz]@_r2'.'yR}#чff57H+rU2rW?#pDP6ۡe?g09mW`r#~ L. 5pUFIyq5Q6cH^[Kv*H f NO)tp>GJ4 Đ \sfV~$hc~ sٱ^ +bKyGW`\OYu\sш8oRErm {ɉNZKW7B,|LXPxq)ok<:-jH X s%ݏ ~3acn K|QǺј>5*(2]?fjۇZ,f@,  +"%`(p=8ap2@vO>&g9VLq,Ƨ0M */3HOt(-̌Nl!:p=i:}qދ+Ni]UFe4iZpXVdSUh+̭Yr@1vY0q{,x&{ȃY;oM +BL{yK< $̍@ + hœ,Va+$8ZW8(@{QUT~Br<սiv$L$8$SB7wRgZ<3EM#Z,x=c*:lqAEA,N%͡)P4@qc9R*^),$m+ɉ][A3BQ( IoRx3Q;E0[12Ev{&CFx2$!k[lHEd#`l߀ P1oAwW.-k{y: + -0_S{a!׉+XhRȆ\,8:A^ g{wD8(9蒉>nbR!O/o "֎r5 ~ Z/Pf!*o8eGfzq^u(Q[Hbܸ;mA"1y0|UR-j_gIb|cc)20I==}ik+t{[%pܑ猀-SGnLu7%Q^@){ˁu"GXe|p 1mzaZ1^jy.oM*CFxaQ =fÚR2Sdv5Lr<ު% z,s>zˬSzЭ#}^0[.x+RU–7n7]7(~' Q<|E@;|Q鎥*v;D { C|}*bTkx`Zq /vfD;WZ3؃OiNNsۧ2&,a,T(1;or+-]X kk&b:__wβ۰!Zڵ 4\z-M|lli&MMg B9]7蛗EU^`Mg#sF$ǻ6%%gxAC-(6eGl1K[Rk^" ( so3z"#,mD2Yx.= '-/jIdc~.AΆh T.vj*P6ٹm^6ѝ8 ߢFѾ E$ +e[ Qvm?ȋ8'|pce> 9;r/*\fRCܲLMCq)Pbyo!]F*/:L%}XXa _\ +R;Q.!M˲ggL4#mZoh}Ҟv`M3d]G rDuA Rn]Ua qaKt9~Mtz-LXڰP 0GNk޵O,?ޘ ||S,sؔo5Z<>!`8wtW ~M 0fGi&$2WtQA'n%1ٹ^L'ߣkU(A3Q$E.&0f*1(:9K{{Oi4M˖! ]Fj; f*Dep'*BZrQdÍdՁ2E~ +{ +XLJrzy,R&҅MxG17Kh.@v*|3{59|$dF0qW<+0(`<,4fO[JnQ͐vDηRͣ + $kzh8%K'yE֐ evE} +L s_-jzW&dTB)aNaSTJQ)\\QZ.z35uޯ]};H4R`jM>}+Ii/5B c\tR?{Z}ps|$:XYAeAZܫSC6cwv6s1!Ro +%#3R" ۣ@>:NGRnt\>z*5f:c\[_x5T%wf.DG!r[c С/q@L˅#&Jo>y'H\Sچ3E~ݷ˂p\qQJ(kṉ ÈymF <@ +S§ E$8''zh5#,x3q'`+D#t[gJv|h7^C}[L)|+=sESѥy]t9 @>C[㤭W+Hc%>t\[s%Q+ ZDJ:ywo>^xRKJ + +Q):w]5Iy?e7Du~1MC! =1S@ic&)(  +pDb +3Jڈv;e|AቶE^m2o]gE<~%k#\WޱVJB˫j|=AՒ$$~W<<u昀Lfscmj +0iCvD8%Bc,?YS#3S?}O_~,=:8:9yVfh =2 "]5|J ]v@uDi 92}mn;sCFz_k7̍vxRoC P7;.{jByٳ?,M+myr4:E²\#~4 L纓fu{(ЬPXQ?DW$Εpq1NXi8V +>SKaapGܕ_$.MD-NmE>{8e7P<sz(8e9/ӡ)«~؇l*'F/5+1Ta$,^5w3)X A7)mJ +?ǺRb4"{dvl@0d&TNVG+? =sF.{^[,Gc;z|@ W& q{ ; +SyXsN|̓7п!~7ISWlF +8y +{BPL u7Du#7/vd +=}J@WV>pMF `2m5P=7DֱN91̭e͹xf %} ' r=93v!u1`t: \Dݪ( "ܱCn<jȆlReVҳ,7_tIi*4x HѪLTE£k_& RGNԑԂS9bQ7r./[HrTsn 壕@YEnx*T1G=hd_M&2B +:qj?o6{42(qI6ٛju +:sZQwdL@nH +Gʏv$f"nU|oiE W,ѠD^K+] Y`adإG>}~-hV2a;"~Fp6kԾb3 8ˉf.[mT2! h|)_,\ 1f+j)mWyZWN,ięH4eeax/ EpݹB|AgjN O3L+"g$N+WI"40PA*,?zzIfp`LѭzI;? Q*OwqŠò8e u !یChF 8S9542*{n;˭C82xi1G]52M?3a;Bo^;{3 gF,m2" $ɚq 웞эz4wWcWه-( ʪs 3ַ+Do-1H5E'?1Ż>3ʀ&F? 0#-~xrA *+BM;u")=Fc=tɖ,'!^հ=rW"’Z`!2)@I0"B[dLzQp-4V4 EsmI ).L8ݴuXGtTɳW!v-KfKP$OQ7ON. T^ֆQ +ShdxȘtujB%ڀN46=-SSv<#-Y) +śUf('ҎWW;S\BRs; UrjPM8eOp`MܗZؖN>sb9H$ю'2B$JQW!]A(_$gu3VLY_O*1#?D&Nx.?5dןgEw +1t  U _4Fj| 8aI^zqrENA(*cwbv:-;C"\=Dg1> q$E5Ƞb x TB=n˷^UeWE2;S@+׬[4}PNJO!g?ofPXgЁ֐+xeǕ EY#Ksdܼ|_3H8T@QN mQ:1ЬT\\ Xb-nA:A~kv,` ߴk T<5J2mtЎiWFQOb_y+dD2A$Dj!BTwO +Sؼ4{|n}thꃃGFu(g$5o*l#L\+~U@8O˄/ޜvvWB8qNS*,ThIg/k,4"料uAM0ZYSV\:VkFPL+ dqAl|jQϛ_K?#dq=EJ [7qv_7L#e^B|yEUV`=$%Gs!a3[L'@Dm)_|_xG-@ʌ0 LsȌ lC?k됆\-VkutG\ ^ϐ, +6# ;D#k}1%K`֥ +we"DC #JEy=ϖ'Q密PA7 +'"PnuEgkbI#7L$h#Y=)8)$fwsm9GSe8#8{:H`"S/s g0Z BzXX +DRa~jY,RBO_᛺n}sb@l!FFJH%ƨ鷇5œxg)aagh)>ь8|鋩 CJ+1уH(.!̗~'l bs*$e +$KԷ׀ G)p G)ی<, Xi[Z +5D¥IQlOvéD6="OY'5fXۃ ݣ9s`o[XVtS5U;^C_o/ŃM"p]_\lwK T&)( YwTE1׆(GJ opD +z>DJKԷ #U΂.[́g6!ĕB\{\հ˕O&s&8Um&yTx1d]$=X[ Q鶘*(;JَYur]ςՔ0&6ص~ 0`r,D*^'m8|O1?:#o!fM,/?Ωyޮ@,*!F۴7@;!]u!X&㢝Ro !Bq܊1dd6g%Ub$5 +s2(Lt>;-"'$V'(?'%CF]r>!m? aezw8jsy+HΝq"boCwɐ%&Rj_Qzi˪ ;ϸsXlT,r4",r8F5K#Y2Tj уMWf6?{?Md^#,#3[R= +6TuyN۶ P@0F::qʮ |>bp+"#)%@[x6 s1}oP'N~5\gj@㫞QA%ݴ_7;a:&CVNq%|UL 0b[t[V +@pDGQ>*=`&D +/I =>ϺI\5)^7`kKfXo-}d"g 2k鵻48 .@GSx!dE-Dܰg.sO9xkQY(P[/8qC9spu'6R!Q p0cBF3=ɰYGޯ`)L]2gP=>OXPM^q iw \,;BcF{T9x(!ϗ|>.E<sb {9p+>n{a`J?z6EjSߔ~`5Y#TS_=`|-lbϾ)!sH1u@+^|j; xQt3G6VoH0EZ#\wB7CzpqLݮ=>RH>K(7b"w|x :s/R0ik)/Ɉx}&\W'دr1Yf)wLIH9Hҡft\ ҀݶRZD=8ϯȗ?3t1Z)@.CWڂj_x(e1 \JǓ%$kab%" ,mjk.H9bږXpd innKƶwP8y#;qDPBZ<Եe %ד W5s3kKBqRa +^ +EO0Uz3*{8br=!_G>Uc{*6RZdM?@9uY֨~a:z(}?_r] }m54XEaD^D\. +_ 20y%gea"ԎI:Ҍ$$be!f^n=(VJ4ԃ5]R+CM4^'>NSVGLlM7b!Kw"&G:YFqG*S&-ޣt=O;MrN5Z +x\I{<e}BA?c]:#b}W<&S8~ɶjٷ9h4NtXkDZÇo1Ebhـ΅QHGsuW#H҈B,=1h~n<2؛)5,e%b.W-c/QHF3=>20(*7f"6TV$E[s&yAJ(IYv[mEf߾t7 b䈭"fxk\c="3נ=1(Qh +cdu~} (wສEvy P]O۱(;g\J-Z^O צrZF> N7p7qB¾Kv>M f T&!!h{ ϧ1'|^ؾ^ z6b`!uotm~9cRGomdO0|Hd2@iH$?G'RsD2&s2M+ĆEZd̅um랢}>ᥳeh6 +v^70g$sc!eAu_>xPB.Y +*Qw^XG3S}{ )K +;,WK@f:˅K԰O?L{#R$W{v˝*TTb'W6*~@^D4xY#ߊn޳2c>h;QFヌ5?жks-q3X|\U慢:2U]6Tͮrrvʅš;X+oeZP줭w Q2™7+ޟ3DԿf}RV@{g_Wh{dFIfE|UҺft'hX/*V R_x6&<9[JJ~ң/乐5XX'cN%6W5UlpN#aȣ|{B/u\傇JJO==ɛ#C 5hcI"x-Z&܀ΒKp(GAy&_gh;ho, ϚNp$*7{z0s'(m\2OT+'^ ӵ\yV%H-$pF+Ч+ɴڂa\f=/ΧۂLGXj3B(KnC'*OP$7Rxɞܛ\M#Pq)v T)Ӏ^/ge!6Ŧ0)WtVT/O[dKDz|ߚ`bu-PB\A: +cM<1j 2/^(NWBUh=Tum[ Pٻ[^R]oџV9t +0܊Y*r$_ Jh0J%[|:Ͷ& +`E$#RUJ¬:y]ߨ1Vv}F2z:(>:pIbec΃j>3{ZpjP a\E%^yUQc*G+׹נ@ؔVA  Jzsj毿 6hsMm)þ՝Qj_T4E +p3x7$EB8.z3/|8G!@<=Rf'q[4 ;ENC!ݿ)w87.l>G*\5 ךB=EEuQ1rmL^c>wzOU{E+s-@H |k9 gv=W`\^)S +]VFʼn}@,H:#z&NI'.)CҲKѢV ǻrZ; AI:Tjm%z+<"6C*Z(r6Ok +dDҊȉܜ\Z V"L=ʪ +WsK-\_D8.i=?K1w?ߣI I;8HOG^Pv^ S388?9{+c!%;w{jזA#6h?[vQ# Iľ-8 0$fv*gklLZ0n9@YX -?$>݉Ѯ?[`şo_E+EW=j#MS>&2vHuA,r,!RvILG$bL15zo6h){-2B(f68k(=dv0yQߠ~'_%~e#1xG!i439֐r"M! |Qؖ~]̋߄{7C,d[dGoGg5s!C||@X#D=6 =jƳn+Kq3$a1l?eGE5eEEC` +8K Mhߙ+zboj W?L+VRƘ !ɠ`Kl1 ʼnS&ԌF)*["ٸ/]/Rh$C1e,$ޖcS5V\ښj> *nm4EX? 4b<(ǫ)CwFW?iewwo<-38x^ܩKϥ5;@SG%V򐳸WacCej3.pŊȭ\oF7|*_}Eޭnt:~jSw=Rl 5dZ9MV?/r=k˿N4Hn{I$#YsFf5|uzWv;WNrfcBmV{LdE"y^_UnX|m_>юVEB"+āK{x -pë>Wa|p~'.y ^426IKpjv +Ǎ뼜n^ľ!`wZJ{Hż; @yeҧ1ꐋbc~+؏3kwf2!HA "^uıi_ƭF)_}v.͵P@tiVi )U龲F(|KOˍ]qS + x's*ȹnl'Ƃ';Q+ĈQ?~YA7$ÍG9G<{$֮v5M,"K1= */q&NV"vwێǻ[tW tr)DGVQ~4-^B'zIhj|[!5{䍬{ B;VM] ɍWnnD <`{jmuM?|TM'ͺ;k|*T+0`pI=%;{_s{&9aBޡbrsVcvf=V*~ +"]&A{[bZ~x+/i42zlVj#ZQ +W@{\:z%l`x,ρY4}ԐTg[iE{^4XEV+֎Wΐh SaE[iVWiL +u&ޙdJF[H]nyeF6#S%w*G{lvDbH6G / +<*tfҿƂIrJz7Pv!NMC9%h +`Ne#W[{@2/sHPǜM\qL5h3um"v}dnqȉy\{$ ;uAhMXԕ7DŽv +t9Cռ|DC!q#0W >eW1O jyŽӱ-=>pfwf.^Ħ9+\0u^YԤ65b:s&Mzn8k}r'icjf|O=7h0pN:kIwsV^|]PYTNώgK2G$GZ/%wUj1?hQ3K*8}lo {,8BJ A<{݇q|J+AV,[IِTvf'\G$,n #LlXV <)f!GzCc1^7.G/ xR<9ͷ=$LKYv-%P2WfXLg$`3Ub0 W #<}b݊ VdO+t2MKsTx(w8UD3C2wv{m&a{(a8w%OrK0~J sBl7АcA풱Fܭpq'Z3 wCu{ċ8봄w4UI>{ڥx`3|+U}9eBxI@V^J]p`Ïr/Q}n/Y-f6H-)5Τ5sWQIGE˙II`d^687 +YTh(͟i&pV5.6mQk~s TOG +< G\ePc44o=̋σ`ƛdz \ܑ(up݃`5ة +s%&~FTX M>h,jݮ6*UhOs>;4Vmn=;>MXbZ@KITg~߷%F ق<\{hr=^, "dUt ->#Kw`C֞s$^f^90GS?IJopQ&X%*z[ce#/YTpa֝.[Qέ4ߢr<#sƣUPkjGR U +EӢvD6HK o]SasM~DuWZ(`NEԤ?8jÉ +x0h1Q|AZP`T\!3+dqw + ZzQТN~1;on1!ֿ"d.*gjZ>k:`B#葮HrCO]ZO{č(yƻecđ/*5]4wL6IjrB<` ua=d?R}"Hd@JwB<7OVWՕ^bY qI68N.t;!tDz9R$5 }C n! +,Ẻ^" .d.yVZ= d[ ճ>ýj_Έ:XwQTGZp/_4_{}3OPQhfsWivQPW}/6_o*}SLaQMɪ4-!;mO ) Ψ#QXrВG[Enb㙺ċ˻㝂]`h+6[tܨ5b\4cp1BXg2d"XljTY>ч[a-o كb Nw*8 hE_<ҍmV/oj?UT'sNݟӟBJ jOLY; ,(X_]$~:߬lIke O?>YZFmQH&ƚ)Ƹ䕞w;in,J\BϽőK'--[gp'>[ֳ߫;){t&ӈ~~xjѷW@h\0nRN0`ƑG^vR>ȌEl;M>7;~$m|u<9񉴮7ϝ!z_u5 g*9Chlh9Q΁' c]ynhpVN)U!,\Psze_7 s[jUѮ_OsD{!/VqWC B Y{ +] +GR5Oyd*)Y1V ={ImN,1M+ eɳ1W<2u.c[Q"D5q0NKb6aPN2_Iɐ\ӗD}x% >XTo#bʕmq!nfW(KB2-JK~jepc5ze +TkI-uּt`okWc6gBEA҇$ +*hS3'Q㇌Cr#֯ygu- {lOX5(HmֳشwzT>j DZO|y(o -Ĭ<,^ߵ~z8gH;/Yꁖbi z`hBER&`~KdS5!]{c,Tf3g 9%jk + M%^^sǹhe +$TH-1I՜ÃǦέ9(C9\&x#&+|F;ԍ[5v1BQjP(|E*Drb 3~5b>cipT`OHRPNHP6h'';ZFciԺF;?Sث_rCmQ0!bq+)( >*6Fڌ O$ór@ qԭ &Kwə)n^.m)`+E6! vMBYAJ3HН_FNgOe`I +u)JZoJV Ik&+%̗8*@-*x'/DKAdުK|l:@Ɖ^=8 ]in;0ytVn>{n $IFpo+x"/rzf <7=iNJľshؖC)>"ލǎͷ#sSy8٨d)JY?s͖2⾑Հ5dW2 kmHC}o͐(^CJ?6F$񵣌yVxIvqx͌R̓G\_̿"LH7mHܳTOo/ȷόa3*v<*DŽm?yh* -N+XՐČ,Hjp.jUJ4ѰGU8PP_tuz-@@ꌒ_LkWQ sPYyҟbڅ3-W@ +Pg9~-LYbA4`3`ЋQcg}3] * 7Ya#A+(CnS֕dW=a;/S9琩)ӃƵFTWTj =čY'r_wȜfVE[T/J/Y^ɣR"y*M辴!! m@kTiȕjyt9"j\OZv)4iAs/X4)^\M-k?k%`9wdJᜳvgj_PG1w~XyZ}BhLqSɋC"rm&ڦQ 8:{ | ­XUKJ}/f_G%Mj + i n璏1מ9_R% +zg?ԶQghin,^V9wעDVeƻ.|UxQz9giˉM4*D]AU@VcFҰA". +u*j{adiAeSwMHƒ)&3^^NJ]7T1;ZTy],bU!A@nŷ~J/'}v:)rdK|(Rvu\OB]DVb6ާz26H}-5gDG\@RעC:IfpnLبD瘿:b+S z[]>[=^G" 8ܲ1ky3FP[L/(5gf#ܖ/=˺d8֏7 & EIPZ^VO{o=;Ū<6?C\e2Sk}Z҄ o1ip?'jX_ht镏8/:aLc*OXtfq[k%L51JUYb~#Yz G#喎ߓ~{f KRT[R4pnCBcYτǧ^ \ˎ*/hAB7i 9Ѷ*8_Z^o\4CZ35;, +ߥ=K=(:  jJJIK~:ӝL /h_EƟ +xGf| +0ٗ"2٩f 7,翳OM)<βȤD[ig5 ,2JNlH<9 O(0dIud=y!SeHb~'?Aџ!Rj,: +(9 +#O?02EUۀI/3d+;4.uB3`Vhx]N-(YJU}[c[ϒэVY Xipٗ!ðOB)"u w$0n_|W`#оoK3l%e#s#U{.*(,zޢˠjͣJUPBSIya*TtaVjVq`h3(ĮLGL{z "Uq߂;X^iӊWأr/bVdƌNS~ʙ'FR=UvB֠UT"o]z,+sL#D4 +T7U!XHY|QeQA_\VY༞AVj>ɮq?R=xom=bBNܞ׽'WB-Jϝz^[=1rr7B5"eVsm2%9^rdqlKUع]Ac߶\Dd0{j郆t< dܵy)hm+" +?G0XlLIRE~}\>}qL4B=bưjtl&hl襣j(A/0=*h:" ިh쎸axոn8 l;]܅F,A:}!Ȉ*[XW#[)-)9;u+B5j:cm~@(JS--X%nnD 2gʹl6L=@Q7n_̽U ?EsU]MA fV:'ghGV\4:<6XC%PЭ URXHj+g\f,z&3tWG{L u8mr#t|dT@^cbGrᡜ~j#mk[-Qi~`^q6SG>N Wh캱w-.;iƺoCP.a~NQ|;-7HJKk+f$F3WґDE\87c2wܩnd?P:8T <ԑUZc}55ZgL4'׹O+Bwfa wecAgͨǠG Fk=2 wE75„; 3?9)oO$Mι]aׂUwޟ2 VѰnuF&#DZs| g\'0E~}0+^l5f|#vxD4 xٔ_|՟u_?8́w?i4uF9¼DC!_T >z_~EI5ڳx[_#V9jv̹laojfZQ4smnz "_R /sUhrFBkfjyH`=`ߌuwAhQkC͝5T^΁3(2f{ˣH=#LEDeMy['xQes϶[qOt=!EI@8NJC\6?En!=Ua,|}sVŅ֓^|]YuS[ARNj\ BȐxU>:hՐQXs &;gHK(u^qБS"aq #RXyWr쑱A5zǂߏ8ÔGyb[Rf=)[D9ꁺH8pX*(U_K"R:0 {V{G L*:Et7+'vDЅ|٨r 2m6)lR4AI}|oNRKuPON?L?Q͡zҪy@" S[a&\PUխ<gu[bxd?cuʨ +_FtD <# A̸so1Ds'X أ*?#t*&uXH줶Mw'R!do2(E9EΐPFt$ \zg6/D* +.N }Q&Wb ka\b;ӯ9H|AZXWHhxNBm }|8r筠-oHi=0gpA3ڄTp*jVԚy0:U<}Hg֭J0ڒ{.CX (e&tk;yH;[949AO h>C8)Ȑrj%q#:"d,Cn-G-JqQY$L}ȯEӑcW̕, |n(})Oٮ!{"AmѢD,U{xJWN8r=B!Q +@`i#Xa;'|rl seCHgH{[n+9cyź"vG)+Ks˥>w=FtήH <-4еLj2ے/ErQMu}0pw%GC{v>jOx6QZsw5b~%]8mɵ)fϬW-!Ly'xQS1}'.kܮTpg$+3Nߕ!/ޗQO>N ^r8rH1 +kv)a`iQk,xP}h֌ȝ +!EjoXaQXs-/;YFǤWVRZ#hHբA^^vEVx?4OguxrJ]vR,159?7CK]LX[[utHu`Sp[fZf'pNlʁS.f+$s_,ړ y#·l/wU3Y²DT˶9eGԍ[ +.d`cxp1RfEP_&ca0y{9jRD^[ӅH{yȤEl5)f™Q%~<2ꗑᜃ-~;Q_'[pF ] OU},scul?%(^=fuLE趗  m/kGO% m7CGM|s +}(I9$LH9xݙÿAQ>$lQDQժ&i8#SC݆*^uΈZL9pUS@3?yWo`fm{D +>:Cߺ5 kw 7B|JDb@xQzI{i_(l޴:`-j })x\Zd6ﴅ4$_,ߵE79s>1f갩ėXf/-s[-=ɛ U]Xވ@N9KF)zӦ (=gʥ@UENAƞIyZ\j͐p8W幗Rr9ē endstream endobj 44 0 obj <>stream +y*uȿ=% =fzGf>V1V Lҕ +ѵD1NRExuR좺m}=QD\Q}raE_ss\s&̭As$ŽuG![Bd"mĶ^ q\DZJT8[`{?/rdqաt$G_P% vR*s .OsSn4uboڙXa w15WF\fÖV: R I?DI:ʙG(Ъ@4RCؐ#eTlq?XNo~.uIg=ڢDGG*^~Шj>M-e$>vU7k{ދe3#Q_8 9K)a!Af!xO <6QaPXc( b &r$ +oFlb;!уEBc5#Ȏ"E #JN7vU, 9y+בbV%m/+[4wB2 AU!$;v?Ŀ Bx73*q-ndJ hjZK,}u:ϖiAEa%n߭ԜAhEV<&ܦ^BҢ2PUh^xk3B +a(ߊ2؊YKDkj\j[DdpGMda&Q'LGvp@]R.îoqvPU g٩3]Ez;G i^ 4!d"FD"!BhO|>CT;RVE;R/h2{z%}j*^cLm>&L&)܌M⌾mYnĒOsMK0P#KD +ƈ;;KrV Lhl~3,(vT0L0vgZ$LNm\sGBZKGz9U1j~T c|:2| gtK&׭Ms?9wKS_j+\]/t.bTa$L[g R4#b{R1"(0yI M'1]btŷw1U'?)4z(-GgK`XfH8`hd=\}ˇkIA,*<^\Xt +2R푸gUhVrIqInh}+ZGr\ )S~쉀 }Uw4px@l)1〗Pb9CXCp{O-ָHUO{^sJqbПbmEm<$vylqAD5àE-hz$lzt<:!+!_NY^9ٖ8P}802UdžvπA/SR?b+ +ZD7TS_/Bci܉b\myo)Wr s=U:H uJmg19j\ I2G 0ɻ0:pOt:+%ӷ)F6DDҷ{aIy7ᦺ,b'G{QfI`ROq>1'JkqwAu H!Eq%T]%_% +ٲ!uX㋖n|MKɼ3 cĆ$.PJWҢqψ +3Za ZF 1enoe$(h>b^A9L_8 ʧOI*)_mm^u- +Eni-鿞ak-!i'W krg\ 1]i )D%Zh#1=rcP8ʖ^pFPX{xc iM0L/ꎰS3"TF EpHFtΡ?mGE=j•A+WG{dfA"d3\ !\O=hK4L#tPuСeP~{5ꊢqcܷxՇCZQ^.Su܊d_ +VfҾ^2S6xqz5瑜Z7 ŸTEp&eG +F1 쒌[,丬"☮0S2>U ~|}0ϗ"m#5ڤIe98 RO=d3ƈ>IJUqvd5L~5:wT"?yG;ƠMRxX;P8g*Zx&v>";/D$Ԩ򍅭Ɗb;^ 3XPSĂ|vHXMMB!IWr~5VU4T-wﲆ((߇b +,ʯ\'X H\x>@F00:aTa!`<G2*Hq:՜sBJ+ +?)tHdnU-;*FmB`W8 !j{;p>Z(E:&+ZR@]q Ȏi4~Cl1q ښDwX +`"Cw-|kfIf.bZx-o|)3Tfҟϲr% ~8ڈJ@X:zmfpΰC wO薴 "x|jؾVe+!Bw@1J}Q,MC>!/#@:Wo|R)/j!N=,rDqx;?ת88WΘ)zۑ]Pjy'7wh"MCb2_ [耮mCԕ;YD\牍=ٱ'ϭZ9!FosrїYgcU=N^o9nvx[5$f{_IR#EJ'į=4R?ߛ +WšWý@ϧDӯ|f#PhGi*P,yfTsæ;fKzx[Uσ}oNc~>]ͬ"'w +8qX-|y"06@Yݹ~ѳdx|}=S ?}P팰oF!L0B'пsn}dbm=BXG5\nبΘ+uN zNf {$49a$֎cFjIQwPRVyBJR! J9alC=GD6C-YmAYV:c*me/ <sKU˼m_J4ԷH*&oHZtr#Cpz#S}P.[ 잿?"vhDSB|qt͡b!AْQ"e#P˜.Ah0[tcs6 ͂t+kWx{Ё:[mlgF|Tͨ^bg"c **Z|{g>=M;0][OF)x`vmjx}k șX1};C{_nc5-! {(sу#8r"fj8 6[(}} ~mdBq ZHy݀y_i7t-U; F!Ƞ8n 1CsLa 1riNU7Gu bđVNOB9%c޿2$:oGfh{?k<}]Ra(~[0O_nQ@dfe#-s'@=xo;kGyeS?77pWǑRN€D.!f lI6 N!}z> wq@.ªDua7=#:;:"oOoEm͉@=DCTPp3|\=R½triވbOA@C}Y%NnK+ak(Q9ϗhkYqm{jyI| "tNxBotn$siNū(fzyOs{;5DcqiGG~N%9V:ާN/nզ8N2衄S.%>1B0W<ٝ)?uzd;:m& !cZ=вyg")*0 +);Vz&)CrOf󄚳?%ʙdm=x^ +&Jƣaex_aCkt4q5(}oMOq~yPgVW)ȒQyF ۂ]{[ϡ, 3@i$t0eR2d8}SIe[bxq~W6vWXvёʭFw1ϓ+ 7 kM-tfe"{ynOW|wuj'][ )w'B;Oѐ-)USS;t5Qg%lTqE/_8I)Ny|a()e{>Ko+|$`iӦuݠ_F=6q1!~1f˙7InnQ!wݭʮnk/.kmb޽AK Ib/l9%ñYp`(ܪp +. +RE7Ax"Upn=J.`j&G +NIn/:kJ#emK +NLuNbMB䬷G]S7fҽ8>1GZ{u͍V*l+M/F[9~yLCF@i{9E5yl@ OdiX4*D6c뵤PvÆusS#@x&#aeR`xLxF[DzY^>qɫ3P4ScOe gQSapgM`G xtUK,urGvdqjϐx.1lDk {[\ ^37kBU +ۡ_vTyBl"qT0o<$870Mހr\E.ڜxdkt?ksUڊ D/h +w|?KsfӃ3B]i]WH?T4a1="5UEq} +*KI$ m!t:Ow +<"! |U3(%,gf\G9jn6 ۣ%'y[8L3A2s;`ȭ 8 n5}o4t}Gy[Pm0Ӛ4dpƉ<[AKWxU귒lz4^o3UŪH[$=П_ɟ49͇ȥú˫ +v{P豀yuSAqlfo)M[7;T|E3jT]3:jH2>Ɉ!ؐFSFF1tI|*M3_ܤ2ᝃv`Vim}Z:Mo~](^}46(cfPDRf"G::eF*T ) HxRgzFYs=W"Ih$):);+ON#6.E!?IIf'.b]we@ +Wݹbo.E'=3Ev'V/ acb9 b&g+I}i o@9yjd +oA\m E68C60njn{19 zTnV~\\4FWc~Usl}YBޔsd6yI +o:蹣zŞlzL՞3jV JyHE*Z@0VII(-S>o^)/s|jK9@LUH\ Q G Ոz{W.1{EsVEbN͡NŬwօTrza eUDC)ǮX@.1s + +xVhTb.yFtA$L<ԃ(S tWcc̠X"b@v bB1| 8lp@-hW>q!t9 R,^I뱴Cj֭[gԮ R̞}A- nD줍g%Z7ڛW+q4sSC;}/&Vדn~!'>l C{HDT~Hb\/ X:ZvCxCNyd"MTA5驉z1Dkv:8V_?uRYZAJ +YAYG)H +YJ}QB:XƟdLg4G3k{25 sâAڿ 'y+YMi&2m{ [7[dg~2ƹj/zkJjNha[ZJ+-^0Os=x XR1u\>yvGUqQMW +}$Č~)/3c %6ygrk%rmLG#:"han,<;`^AjibPE9 )cnuL+qH=f 5АA-V‘]s(9Ȼn딾K/;2+Y8wLvHZgnl;Cq{"i/,GxʹծfD8 , ݶc/z>xfG:"6cѫ>'>KVqpXyCWb1H yHUԖS>LIJmVɍ䴎.ScSFqsfߣ ="Fp9 F-Ȃ$&|]q\ -THb~$)ZZ"}MC0ixcůeZy3PCII5zSs//PPCER: 33Q1⮛;R|YrkQ ZRRg eo/kUޓbYwPf +{w0Ө 2 +i;Pr(Nsib3(Z{ŊuJ 1=]= 5$ZmFKTB@nAZ:)0l +㸴je=\/ 3m'O44`<<{^@ DdH*Ɂ[.ң0nN?(#\')O_r:#ʬfBR\%+)ަԉm+ܙzר*|ZXc@Ci +T"KaQK^es?Q2g-cHfrO/K3$>uF6:Wn:"Z2-R(DX&:MpX_Q쯩+#B-Ҡ-zĦܔkUv2SK! +Bnr23sWNzR'x-z;HǯM: +=g +^CeMx^ f6ӔIVi˝gpD~nPnIW `G[m +[}S4mho=ǴS`tA?j'&k7TgQg>"J6*-vB -bT=c~Q*o{T_͋RA29J[:yǓQ@pn\jREDO/A+ӺE*( +c{WW{eR8bJXwx-d {֞0U#Ě9 zl. o{^eڥTNܼ LDf֗US0Źy9+GeTUg$ORl\NAf6C/FEǑxI6lrP$b\sPey,G }|Ӻ~ڼ;Ulgj% M#|C3#gnZ1~2wtt޹# JBU ˃s8K.8K0i 5IOOc!:|}!H =_酲IxVf qb]wچ:" I,W(4HR/BM {w72gz9Lw|sw5PWۿo2daCB|;bP2A\"/9N'_Iqi69'N+2 @YFwQg`wb n+N4rP49GaF&'b~埇rjЃ#2~ȸjjc4EZu#KGYln`u DWp"!~-9jt(j8OHk^n4LiG9=ޜ@BM!)L6SNCzyTuPA'A"@MC7.`9g$~R#RMVk2"f5'\2tClVa cd&zļDX*z@SMoG}Ig0BEAuF_/YϜVa$@>˩v9*/KTcߏ~AǓrI3+;G!A lU"R)+^ZΛ-yuzDpyϜZZJzjcbqU~ZBC2 %Ϗ[2 -}/RE{p_v +|$HWy?bO⋹ ĥ>n誃+ϐhe$hԤ5$1`5DL1艒 Q4h0uTz>iCr֙!ߴŲ3 W_ :_orrƴ"sgK` ++2d(,&4d}>Ndps#9ls5~D$]0`2R7H n~:uO{™r(uaWͯ9eF;Chr~==ۋ*= Үd#-<1ĬȎd%}VQ5HAXiWoIן%<d_pH5܀H3FfEm{s N!Ǽxӫ1EzD> `34鉑ŠłO9UWn%4D\Nԝv@Vo(Y>R3ݐ9W[hxםCόG#"J:TYLSnΏ2w:TQ +hNUr)eP,*nG5vnW]JHHQ'=3\ه n 3w*;g#0a*ݙ5mO)Co/ט*cvƔP#i ѝfԨ!X5jaBVuϢv+۳$dB'lEڻ[mǡ_X+*sm?}QEhgVuP)mwGl(AnBR>.=)p_dͻUio +x.hB1QQ u5]wFUW ~6?ap*jn)_YC PGM:g; |V)N42-csnu"E:{`g埻Qlܩ@8uܽ$KcقA $lWk)Rg>=w3Z pM\%|]ʺI FRRYb}F~3eRP +m7EDJ#(K3t@|\Ѡ1 +A/mv9Fz|F-Qj#{6GmJ?x2}CJaVXxǟ++~< go[DmFQ[\K+hN3vNBD= (ګl.^QPc%g3h:Cd;q`%;Y#AgmxPN,>ct:jV#R_Oo&R_e"_7DP- ɯ_A 2;i@d\_ #Ph.k^0k̯_=jpϼaNgQ3ԶkT]\r?'2/=ƥm'{g.;52hGŷsn#ao"qJj:W!B}86'>xU+57pVkvs(:VN=%AhD?DG#p BpG[t=keλU\CcQ~\E,\[^OȀxanە^B\(᱅QwHj=m$'U*_x_s=!GH3,iT((9v +adz\~1^@YwPа `qp ׾Dn#:Y x9!{> +zWt[<kMl7} Wco ZOu%IH',EO`緄C#Z3"14ĂZg$-㕽dU `G݀-fZEvk4< |gt9eA{1 PxD,sc֗-qiZ!V?5D9 YxmyG=4}`Q/j鍻AuBMQ$Aq/s3GҼ(ͱG`Puo1&T)3z%2I8ms(ۑH\j\18n VV+4OQ"; ;*ƕdGItn(n< cr>$gާnGMnHۍY9w|qO!SCo5bT織" +aFEc\Б``٭9GjLᏖ_w It+#U ʡlTO[0!|cTüFRT8 ^!?5 IjA9q [fyL ΁#YH!-5| e<Vg(iјoH&CP<ʂxȡifUΊ)E1ڜs`d1;X!PtB)IWjÆE9%GҵM*@a/0'>0W7g.E%X\&R'kӃ9rL\ l,r7?ڍy*m5Há=}iu蔽kGI!8mV_tVJK@ _ +Ly`Ķp%tq 33[DHrߧT=}djAz= +Q |:ߓ)SKD)%ϱHsURv,u<~=ZD2ڞrThLi'*ha +ickCOҵLE}J!A{L3{T2m%|㘧'~*8r>ro e(ID(CK_f Hz/G6oXvPMtmA1$cd$QJ7Wv{YCt)tqJJtta `'ylW#Ba}h}P1NL4 \>Xsts% (f:߯AZgj֫`ʽ,Z6;&^f.!_D^WX=CCIX0 8Q-Quk߰sg &-!ݴ=M^ܦ){FMߙ +Ϝ~Ô[Ɯsp00q|J1kO' ^ {'U-xǣvѾg+u]:i ܊̶ gh*5${ Uj=S7y:ɭ` ʛ~6s p0j[@x d,TaW0'.#]7 >C2gA3'ϰ@]q:DžYjW8}a]AGyYC*wIR` w=Xν3wӂ=1e\0e|ד{`,r$7n1SQWR +-"߉",/70$rZ 2 )X0 GdT@^k1*d]}+U|6}j3C=[i&?5B"]υ⡍Z̺a ّzԁ*ſcĪ5LFWɪ[3PM NPB;\ףe9+*=|t&4mltՈ] MtM̔zj,imo?Yc=z #"W4-~ gԸ/qۊ {WOD#t"?xU|tbhY'{?YQAGr:Z4Ljm##dLi-#-0rtKG8[߳35S7E]a+'Boδ?T г] @&T="opsЧ2x>b~|rW|\. eWqJZdiʜ6nj=fkL ׳BʶZ-ڡSBV艞2UO=T>UwU*r?F$n+붬In^َ̔($t<^k:qJU.%IUV mz!o|@7y˪PWTS8:˞r{1E\S-!}>3OlX.9$;[}eXe%շT4`//`D侯!7%Jt(P1FJ=T 'ߓ R3y>"/Kh1 iJvA֙9-6.Nq //75hw]W*GUtΐ<޸hnyկҖuAlJ+33З%m_ʾ79ϊ-%w3-:nƝ4 [FY uIBAgu~eHw!$]L +erj$uσŦ k.Ȃ\]D9gFbEܘ么u/&oa6B=Si9Bww rսK6-= &aǴ Y/v/\/aϖ#b*u]g5;G"Ν~՞4!/-סt :6k eaW8ʥ[ة|-&bvg 9z~@ @=uS}*xtUmc1rQDyV9/MR L 79c+bKUgr.U8`m$ȝ'}=fH+=z{1BLJAG6UkDM9ݥ$R +$yHy'$6"'9r)Rio2I}K5-U * ̲{lV3YYikcª[(3 +ٱ5dzβRJxnT9F n֯rGzS'|tȻ"%4bV%}kԖT'+#ILPQPݖZ}9750;8 +E[js~Է[*̊~ +]ehB>I5:8lxHjZbNH8~;ߨwpnWLva|{7 qቚt:g<8 6FO5;T\~EM%e환( + D,i5Ո*]sQ+8!c)t*5|j|v{L20 ̣ !s᫵? EHۜs\wi3^߱*b;eHw %"R!D>xezxҳb'IO`xpg>w/yc\6/ԘHld\/\5u9!E3۔s{GJbwjS})S=qS?4'6wS4֒X8>P+MzfaSEw[NCX7ȴEiHXaSb6N(5CTd@gHa*Eܰ [8VI ֐?Sfn5_Y]b?6̈́x3G~0%b׶Ao2eP' ZJqv9ZRkƣd 73Pi7*2e 1X I1,ܫpQZyKwl~ YVލAj!EX&+~ޭ0 +Q9'+:"vܸomnoaP?21Y_`D31%>4JZ;b/\K[E/Pq(c J >4\M/ύ31{#s~c p~/C-?U[JPq%>A$Ίj:j ~wqpM5!vPHhN|-}.m*rg\BW㊘5W|0E{ԥB(oZyQ ~!4xpDA|0Pz^=_Ͼ-" Rp} +k\]E$?JX8}I2kƟ_Y:G:|H +5MGG]Q qF5za=Zj!6{Qz57TiZa08IUբwq>0]C`./Cf~Ts5w҈8 m|E:dO[W1UPh#]g̊ao B͋uܜrOA)dV6dK)tlR;E'7sCk1:)˞53A|=Ib]Rf(c_Ll|B{$9cW0 AL("6.d^nE4A|W8'/=w.b!Gt3"h%.YK>~>|b%)2˩BS= BUJp]Q9ef1*ZU( M?z64fhEX4qpWN_xnq<!⿿&PL˅ju*wuٔ{os<<"9'B\*U⃕Kmbyw5x82Z:qt ++yiU11z^B/Mwl^fAζ)-T{6˜K#$#m$ Z(|m' Dž`cIzfI49ðOC0MGgKF.ߴ~ڣ^=9_A'- 7pz]qߧ1_A*_w"5CN#n a1G +Z3,deN_"M8l@JcI]'+@šzd]?|o4 Y)t'h;ZsK h_ +D/ѥhYsKrT,=-a.@Χ~ E?%Qu൘/mןm~7=Dz~_hlasR $ ζ %Lyq y*-ݔOힽ Cbj(iov.81 4,K*N(t]c.16zn/5uZUUިŲBGU_'!z1b?nR'fD`pP-N*/=ž̊B=q\* +G|!q|O' H[[4ؿ8շ;2z;[(~8:K%WkH,*e$WNDm-O+ FYzWk_E9nT4)85qWi=#Rh+ӽ9-X[u R`->?=:08;:qNJQoɺc%JՌ0A43·p ؍P+v[īљ+ K995Lc&z:tVw G Sf˓ɤ<@~B3eRH`wA>Xi2t.E/NՐGy~ n %b[h_i qUh4bÞhəkqQ<@Y=<-CizHZ^@g\2+V s!*{1냭_ =S +}yd5wՍ>xF-v"h~G~]Xw;h4O)RaQf$NgWi^\`-NՄE,ϡuR;`KWм(Ќg3L ]L9`|1bj!Ļ6 3VQt?i믯v>r]aBrc!vH?f3@åҍOrZ=`^ ՟j%Ջz +D&Wi%!U?Dl3R#grt(>roP8]1:ϴ Kߣ^BsBah9XJ#B .2)NȖ́Gk *fуmsi:pa&x5980hX5)f]0M ^n[lTMan=v!; QqFt),Y<>֦wX#di_c3mN.ȃRcJiV{VT}Jp p*9Ex^%98z>n{#d)`Xi~鈯 V7%W:Vߌ־$Doȏh&P&$xK\UxF.]oǶ׺ژS@emQ~SQ_V"O3 ]N^%S(yʠ@ڃ5O; M⳶֘Ex@| F!{u +'oد@=Dy +w0ѷTJ׆j}5Bآ(lt]~tS=Rסr [S*Z3n1qra@cLI7ޯ$]/,l> 0O\vid21#D|#nq#xL1N07ei5U7}||Yd;n{:M</8iGԂVLڥ8NnR:ɻnR9ܴB'Zm O;d@F~#y39墖sOIs*(vRkxUTYi^ljV$0YGxmoosGr'Fn+QF*oXį A ߖ"l1eD-)W }ӿ${Dm o7\QN"!/l7F^3ӭ %Q4cy+.gR~ƒQ$;U\x&5%hM=N{E w,~d+<^?32A Ucйei +bƫ'}ƒ[~<ӇX*S5Udvy֨TŸKMr㈁h1)dINlG3 +q>˪-0GmSJk jXWx]>e¼`/N-uaiod햫 +>S#I!6~ڈql[O3X(/Έn!"-!izL_CZwmDn{"TZ~eǻK<*g'gŊ+m4Ɯ^^)+3S98y04n+??dw)P#CK(J=6j׺*C4WjVe_־,+3IuYp1Av)&5dcrAH~R~KgĴ@8*a {NTr9=jԣ紲g>3%B[ |> dkFl()(Qta 1G:_CNbX +Un؏ssBZE޷ιisW7Y=S~J}9g2ηp]E?KRM%yGIj_)5:R#X|FOJN*D5Iްkj&[Š'A)A(:!2h=^KKLx+})ߑk|MRv2(Z "#&O\\7REY3b\ ` +̦f<8SPȋK;j KN&}6D#ZgTVUԿ(G*_B'3E'76bqv"p}¶lXGo0dh~IJ+%Qb +9S=wՒMS0!]C`=K 3h|ӛ^쌪KPskJ_(GY>P1I,ED3VP@yF#o# [ D2Qk>" +5ޗK2f҆lG8Q>Z7u"YImRO,]o~2[vGroxtu;(j*ރ9ێP=lK|+h[8 }c[_r0z>\|{x MLW{VmIo('Nn^Cz` _uuA~unowI:,gku '!92#'!nǕsK ?T$jGa^Щ.gzz*RL۩\:raF2VfXvv_/IJ~{d4jpZTD|QvXp﫿߀UQ(8V0un&VcǏo[ ' }E0\) W47T MTd_={JDEwzAH/^P0U)wU E]<ʻaxRkJd6;pug]2'}3, gٹ=,p8I0`N6º$Jd̐ 6߹?WMpZʯf9/5Sc/_my%W v.E8\WfGLs/\wM'hDNy ]\HskZ5cq1-plBp@wJ=dߺsf\ym5+Q0>'EBSeX&ݸADJŽlAXVUEh)-۲Uæ$]BMdX +jlI[aedEW)Uا|zXUOҜ&ETbq RVUqS;C`WSu^:B$ ,H|-m@#lKmvM?SݐnO 6Z,e|Ire9Gpw@ƞ~y"ĥѢѦb|krr]1}vE=x~,緋 y|Xf(hd3D^~{Vmr2.]Bqfd8^wF`R4y'qZ_pshD0}j洇==Dx4( ΘJ_X[w"a'GS$h?vT-v2R a[ٔ/_]$8!jħg<}A!~|M^>5-6+(\Tk!yhm ]mM;HdV.l~.ԾL*̇؊O]8y7.uJ\r&C*wU%#(mHz=]m|U{#^eucXyQm&D" Pwخ bgAEԻϬ?~-:ޕ0W'm-{/|,<9rT|)%҈UWFeaD;)vuUa- AjehF.1gvfxtuEUVsWYlS!R:A|ƈYv( t rM:2p?z/A_87 7[ +or֐fZ Ȑԣ*LD,YŹEG:y_",>WL&9 ;٣O TSkawl.bmX!`) 'Ξ?$$^ꏌn(4Юr{c`wG(BgKن ^O;-m^}@/LF-'.&Nj`b-b/-i0f'ɴ{TJ\/PN}/[`7Gį>:к_s|<ʇ^f\ -U㬆!?׏X|Qz,‘0W4oqvi6 9ngݱq"{V{QȒDP0jv%ZfGJϰUYOU<"bϣvD4j!&zl)0IȀ߽|>b='sA5uG9̃."rl/KdRŴ{j\vΫLM3tV #̏hP+5haUJn;^ a#bXԺl˝W P.qGHdj/dbU0SUC0ԣ$iinQs~'Ա{sg- my(T8)6fsXz爖bS -$:$s T̷=aIq';qGLl JՑP 7WLv Q`.WO V@=G>,,ۓ*'#r^{a:Z aM}.- K$z$#6;{! +yO8O6`D_-$5qNJ>,}4MιEeM+O+FttȠb ,kQl<0A΅ZjT!rN'q9CQJdPU+ˆtW[(D+mHhBPt2g7*#t(o]3Sv}0TӌB\b)d`ր8Y_6_ֿYo@Q3Hz3,NK@0yr }PAm BŪ3i=p˞QwQ׍#JuCn>z%R8Ts) +coQ)xDf3%(zɱѹ,۸۫g~J~f})oh-UxP3nRI9 aTITM=JWRy9 %gHv?w b~sۍYITi eS9>TGS^̬ZYYPAc`U(9vHPgZ0dk0^@&ɖ3/=/=>{1Q.-d޼bĨd.#_b'2`Zbv璉uU8a*6ͷ TO#CgQUij_Ϊ9–9$,ZD.rJ&zQ{pc՘n[QwU6[A:KIt<)Y:)c+n +;lUTWw O> %uQo姻Xgvw:Ypn zakqX-Q 2e `H[Oc,9 UzF1*w35rϤaowe +QC5Yt,pδ5jZ#&a=X&\#*㟿3N֭b2LiDW``!5^U*"w|6WeO=6NEA~i~Mli@sT~OubŬW"@Kf$+gdNO#6 +3kE cF;$PgMȾ#!ʤ:bFeBٛ^@ПqC_ ݡRJ-X/iIuH쟺TbZ=dJȨiCdCN)4I򁲘S s̆is#~*NXw[WNP!/:mQCH|f*QnBRg Z%U- W[C \$5kobҗljC oa*.#*mOݎaq ->GEgi49QI_N+TˊnʳUc_>35J!Z4#A ږQw[NՠVYy\ GO"̫k m5c[ZPZR&f/Io K+{уrAAdM#T< _!a3W3&I[Xk8gSZ>'x&9s+ٍ .r5`Vu_cF)*UшѐÀC 1/+7b<27*|д/0c9,ID :bpq] 9J ߹C8\)a*ޅ +HV2 S +qNs0ſJ~"`K)]Azp{$ydvG zɼ% 'qWːcn)IpJ Rl^l5{r&XbTW+Eq|~?ds\OvSasŒ"m5/}X&֖ x %^? 6UC(-l46")Y4YRQ=k)UD-n^X![(R$$5&=mQR-襖 +.GI.Y2~5~V&1/TH)4>~Z{ Z +Uʲa٣j,SƨSK<5?MUrr_IH/OR&ZƲr=^E5Υ:z?;e|y4@Qn' ߤleW':<"aW遅 +tWE|eE (Br[e|S/' & +- ^uH+ f@*5Wƚ^.:`oxv/oq$U/{Tq!/,RIzWHVW4UKͤW&}Zז:LB +C: +-ijJvg@2NiSZu&3PmɗA¶$,2V'3["~ +rʋ% $@ҽaF^Xֿ>o!7]N|U;$5Ȣ3RN0yw:ʽg'khmejyhLk~{8 ֜k(7B+ĈlMCѣA<"m_8+xHʔcբv<ɪlS58TF_:֜"R!R/M;Y#"Vf]SD;}{̕^ڱ/ Eut'ӔXܽdRKgUdd!T>ȔaP2x_.fN ]k6'a~7"Č %9/`'Sj_̊5{\c//"KE2sK 鐐/̿A>a8DgI).0 +qe[-6 +ϣ8#B0ž5b URfyfƈZ^Kbc;Kr0CD.xtzu)TV=\pB`?NJ8HVPB&j.?B'.%et4,IsI2'] +(W_PSC)x҄'n +bO%S{K" EUHؾb ;yqh~loZt%+0fF%>l%EHDInv=ߍb ~? +7"N :|ڼaR;J<?I4^H >!ɻvp$J7H@ox*o!cxөԲXYn˳qپͺ!D`Ԟ0>sq"i=5BbZGB8XQcg g) V̎!wʦu3E$[tN_bD8r73gKx:#WQɒ)oޟZ\6g#Iޞm=Bc\Х~ +CF1';Ev#Nڊ}1v +fW3U8`uiZG( J:I0AbG [޺b7Ņ]x|WV(Sk7 B1'ϟ]yHy S?N\62P?e0+:mQD% Bg:[-)ӣr?0_cu'bӝ?[=ѺcZfo~:ϻϨNbDTeΏ*+ WOVYģ&nfyasi`pTo;au'd䮚OQ}0ϸ~xZnh0z]dYtg,YHG̽St+2g3b\jgt=x^#@U +nCJZ۹--`8 +n!~;OA=|€)(E*rU{nR/sFC&( 8!#\nhzXC*~5dOhe17U 6DАn 6 mm} %(]-Po*?:2H@`-Y7vJ4 !.{\*l< @ m;JWBr}~S?EqWO8nNЍ-\FN|*

4l:>Q^{OGY^3Td8ᦸn&=S6ݐPiH}Ibg^o \ GHmOl?$\Z9ͮx&Jh繯Au̪}Z8!K;"!kÌ/[On}`gCn<D̔!;3wD.J@=9,][3;XVWt[pq;GdžݘL /V~/p~^jrd,h-ZR#; -8CN`Ж7^!U"d Ũq?=7B.B7XMٱ?|?-+3T=H^k6lznWdvx PhP| +h/yPχùi_ChuEX@WW@ZM84U9YKsG-+|Da-jHǻ9CYHgN&K7D~OϬ->%vT[)0~ wApgby[rdJ'&] M}R%(ivV7Q} UpKp8ޗ0oL˳.Da hUY +d9̗uהH+B}meU?⪥ngMc'G+uժu1߫BOR䕕6gz'&qU'N /._52}+|͞] = al^ZzmmAJ;qS EݬTdy _XE\gGAc?g#x|x#h>XL^ i~GwK׳dwj:PyHwx`@'TRк Ggg­u5~Qձwޑ=zHh=ɺ"lR:ɽGiEV𘶸 2oO@`s$>ϝ+I*g`HC^qh5oڲ{5%`u2ސdZ +sF+rŕ +DMP( 3-!eygGOA2W1°] ٫ALMg ~63׏Z")/WP$7 JX'C_w1 \ыLVӤȂҕ?A|3^5h<E.]2GIَ@Y+{;xbO*~m?Loy}+Sc"=]~{XkX<\Pqؽ=q0Y!#Z%'||6kUoG#1CgnCVk9kLK!w+ͩ'XQVo^[TX[}qc[{ +F^ +nWO>K-J&9Ԑ;u %"#i=8GV x<6#sֈܶ-5-M_W \8 m`U|ϴy}z 6Ik0Jf(;64fzsZ); AI0(WTNC R oz=x5wEzg$oKk ^e~H F7jVOy$`1pgkxۭv[R~ω9PK4ϝrJ !cpآ<- CU) RrBn̯g|U"q/{3j*ܛbjZe=Dž$/o 0 bUr5Шi|}0)^OB~Uٹ"PE',DK;T9T{HRc$}$yN^(:*0X^"k*Yn 1;Tbb*L_OQu +M;w05^k{5L}e+7($`2C:y9 ;#6vK)V@zţ ęDjۅm;تƾ,ۇs7GI"bO^ I'zF+]l@}*-NO-XO8ϓ4F͡"V0 +UL,5?g4~vѶtfCacSPU9xӅۢe-`1VWz 'a dl{nܡ6¸ԳT |̎h~ptj@ F +pK#7G9Ȳ84cePpX>{ + 'Ѻ7 rl8olA+O&AzyYq3ldS+4}_?a/=ymi[3i9Vx$sװV@pBGPPSQXLo_Z$0Mf1JJ1|njJjOpuG 1&j-Iԍ]<Jw{>Sɰxxx,V&J?DtTyUϕo1bF3t *_-y~^z_[>xSkF+c( a'vhc>-WJ? ~_TGijۇ#`%v_qNVU&I[O4Dz6K{Q]hotyT&TPHa͕_2d-q2ț'K: +t3No5wn-rgF@dUUWWwW +H 6qb- ++%}y<` k\wxڊ;͐ e)b9C Q!nBg+iAs=gX#@Q~X9EYË_>B|6܊ E @uu;}VE@寤;~R%Wd11}czAPԀ9GdFx+jKyPX)ޒ7aFdu¾26y`$cHC$f2_-^šK"+xFږoiH=aAǚiPˑ%{ jgl87s9)25 O:/[l.wevbʔp@ Aѓ(sA'Y|C-4!`w xBԌqa%XBԌz\ewF&t3튡ₒrQ1oA./lRN++ɭT>T/m8pPg -qݯVUKО)MM1]!린$J%;21֝@Ϧ!ϳk|+Vc'Wr Sq5=b@D>/UMaiaN̙ +!,3&-e%0 0Z9$ $t2pajgI_0/eZ#4ZEU[ h-8P &?$ Ha-~s*{Ţ +o6(v?dm".h +Gfhį]:]UԾ5>"+"ur٤$ VL=HގU1|17ILNU \Xp*N"ntMkyPeeL+EtaP@NmUc1 ¨kvш$^p͛|%k4Wݏy^|3QȵōN{F FNk_wM'OGRRL]^_E[BE-POב&fu8/2mBN-uSqjXӹTٍs&wbEnb9Y)4bdX(,q#[- \iJœ٩:(nDH>Ix ׹,Zap b9& @a`F +Rrhq.}+m +4b/~Yq8*9]vH aHzC؏,R-rX2Z9x|H#YxOξ[izcMolD’,Lb"'_\$]1@\fێt' +KնpFνm=,+E\t!Vi*@A*Y $l"-w FeCEio1TUAHfJN#H2)a|9w›oBEO . B ^_L̾9yt)OTXv Ni%06JDՋ=SWy|XV/ִK<ݻO9WR6HtQ}{o[%`)鹭NcAYl#%jzMZɃD_Ҋd+M(z %I7QaXuUoAg}9 ,@`σy|>S'N.iF7)+YꙀswE1R>}֗)M>K2wPa!?z#e)1\[jƣh‥jtWZqT=/BӻªX5y2m $% F;qυ,z1c=kh;ظ#$?p$JZ[ +E*@Nmw`M?+WMba85q" "e<4( dsC*E.@2vYdANdTRNc-gZX?nl}"em9tt)֞dľz^< zft)CY߰Mv4{$}`: ]9.bQH[PV D+2ޠlQf;StSB0a -!{ŝ.ٳyQ:k !ܴU5hpE$jƗW~lrJA +m4Dxj̱h>gsrMYƣ]nKrs'IJ,˃A͖#U^A83w#+] aSyTRDB=hmyZ+sXT]!9sN!rypǓ#Q]b4d)b*t}b. x}h'V0@smR@46m{W&+k:6 x:0 c3KP2P[#^Ve֙k<}yH-l+jO^SPr5>"}*BP"*2 f0ѡxX1~z+2t6,.[CۺWFVpGR&*xD9IIS_|x Gn^Fܢ>Nb5W 7aUFDPqꅤV8TghK=BRgZUM6D'bXIcxZy]g8SsL6$KL{컈[/Mx&e5ȗɎ:{e>q9(.I$5aX9  IPֵ͠`vdv3]jƹPb)5,Ċ&v7W!ӳWt#0 ׭.t}{݋o}o^]yur|O{ӗ/.wd\2OzO^O<˛_}ssoNww^?7oƯ9+'O>W endstream endobj 45 0 obj <>stream +='dHs-K* $ް,ĐC5&῵ ABb-Nv@-H%ْ'XpiaA!V9 NwHH" y=]ڛCtF 4b54R΃x T[ޅ_,_ E`݄4k!Xiޔɯ렌dżV}ꃥr V3;{*vQ"q_UQl"f=3dј}: 9`+ބ;w|/1:I2;8@ARD"Vu(ǛT!eju( +8`0 fYH,*U%"$`3j?^TWqu鑲YA[ oo…^rqQB`?FGF$ H3d-=Ǚ|4Kߧ`;}!ַ윥%RVMdrv])r#_IC ~DS[Ezsx#U1tuEg,:ԁ{Eb; 0rX.]|`7/caⲔ$x®BNrZ`g%Vo v]s z ^~3$ӂ9y^MӖMboMh +v9ٜ\ڒ)U?GJ.RƩA}V2:bNǑQ}\CeFq5ו,A}mi_TFsL:b<= R`Qܬ8[J׋*BR~}#zV"0P¼KUOWBMn?-GQX'xCuaxQj0% [%l&f;G6lI{~hb &;m՞"`{q c:>|*PK!tʧa!2O&tUwa7Cߣ n &QU/ +3w$[߯Xlg{ 捴՝o jQs84;PJضē[Cx{Hm\sڗR݁d]7p u׻=wC/@A&nJ3ВX!lj[V$$$>eI}2 Rp*7h4C:\2nD/M@HI$p( +;G2#J{sy|.tUY-b[9h̠\j] +Tu /2tXChsѰ9G}be''ep3?UᏩez[(!Qёeȴj&$ڣbI=8" {,qm"ؠ(xvVdٓyt"^xvȔ/\-,R_әSЯKy)YX> Gn|} XZJp*5 |: +ҷYHS_QfLs5V +lq,8e|,Fgqwv(=ǔm;ACn%n&܏A@Ra8;6qSu Bo6 _ʤp5}9D2I>iN C`֪ikN@열/MR;4;==?fJüqHp#Ҿ|]m؆-CۋPU]RjQrB&c 69n >A"ۇHۄa/$+=.ҋrmS9Wٳ«|yDL^)Rw\vጿ{.e>?] Cd<Wdg(?ՇSfՄ*y~jho5ה!=$k2ih{+%~t=v)G 5uuk _X-[M!C%d!?C O/d[f323[-%}[]nK/S!:d5UEgXقrvN$k;KfrkTGH#mhr*%|1Z{Ox(4Q3 +,%+OY;njGDGfa"Òjw(~L2b\1HMj^`UH`-O@c,{5OZJ6PףI" N_tL9d;HJIKMÇbL% ^rngRgYj48= \Z!oYK/#ч4^@7Wܱgasd@ae{PFOAl|pEX)46ڀM.ڳp(+=إ^÷a9` EV0-hAjR#Yڽ:9% iRرhVhiF%|/b2jۅQ=ip!Ѓ2ɻdINvL"#WA sjAD$82Ml)&a6µ ?*cEgx ~<8`p$FB{năuYiȈ +?'* v~XL WC %W"P!9\+4 +̱))< ph}E'&OZLә0Iԕ$* e1iY^ :uHʪ*SsvTYXCafbrw +h+?@MX3e s}YhLҞS5*,ѫhU #b,ˣf'$ϰ$'=?xt!5`v# +'crហCaDߑm;Dz)h=*LŪT_OضXjAGB6M FܡXHR4AƼ7<ԋx^5\Lt~@HiCH&0dD~BJpNwrSFZV =<gH XK!&<CWʄ9j;V*2DFH #S,#%C;n/IV:RmkzVQֺMR5ϥG#c+G8BNJTKXl{,0~/<Z({L^˽$NqV JRC}%@[v2)S0ovEIXHvZ$x&yobvJ=gaI,n,'X,3z|ohxLr1'qҦ[h 6(ű °'[[HiI%ޯeEP(-/AI<\ٰ +k^/<.PQ丷AByxA2ˆ/9#m7G BnDgV '&( >U20vj(ˠr.zڦ sQӾOwādŒ~_$(ۈ,DIk3;Y1硦I1S@v`BbRn%Z@0'эHCr@|c1E(~z(j~27 121X$3 O*zQBHrPI,T!3B76cPrmEsAzm4/z}IUW(aZ{Ҁ(RNg'?a!MP w(9,Y !qkCu*B84DFͬCB#AavizLӣ˲mA<0y' +,C;wt}LYl0uȄg`irEߩZ i'Vp(= }((&ؙf-3X5rn;9w産b5-4@/Dq&]R{9n%:xZOŮ|ZSeJ%XA0M)oڽ/7<ƞ:P8(G(5NJ9u@?~i{6pzww]V50C$,(>p –AZ6ނ3hMI{\$movxh: +B|QkwpdvSd' `:/LceV!ýzdم45N賥΁~pTyh6\ns@Bf!|h'A)b W`֠pA1Kt=54k=%]bP]ԅ]4DE1>JlcɬXէE|d9  [~>`lPuItCfi;w Yx G[3 ݡ3z^(7SZ풰G n@;3(c@ gvuYZJ˖ CvxT@w"7(I ;g~X][j4HQ{ W]_ R=!:NJ׈!oG?lh٧z+&BH]Xdm .L ߟ2XPOuA'(=s6l% ` G.1d;&] Vyj = kgT^G b{|Ll +/ek]*#ļ5+A$#)3el OA+Amm$!F +OZ#0՛{tJTs 5zrN{#d1,B2Bq\3mϝ5}P;!)Ջ<}^%;d]>b+$M:O`Oi [^H곀v~4zRe$<T&RE+B s VIPלk _P ӆZ;blA=2Jg9U|)J& d@}C_&BK`c_лN|e 3g캦\ƌYznLa8<^}1IF|VXHVmWl] 0fTKHFD/{ /X+HyH,ȂK82F@-nltr +B 5@P +8Yp@ '@paVk7|{tbxp|ϖ #?1Cn#6 /|̹y(3oi;k>> +F 9u$~"H#먰p:5KDoK<0HEFQQ}IJG_ Oc +\猀>0YIRI[ۣڋzOW!W`aS{ +qĞ;*kFd3D#) .X==P4)$2d(PdO|ǝ>Z#'X^}NW濰wۗbjr`hG*lBwٗr^-6w1) g 䎄҉ܫD9M ؽ(I)3XKuZ{"6ڧfF$N\lL}KLHI]Z[Î;8=:9Dmc_%%c&=;`]߻dxڊRi/换r A癈`*dRMa#}EY썘w VF wѦ5.:Iv #!o{2{: ePO}?HR1caYBJQj;&\Sz$S3*U*A{a|qnZ;2AmZ<,*avE' 0t!O5'E}ht'NK2#6&4r3őR D%PY},:5bu@ZsM + +欇\z-.^CuyTI yF*#i:B<δ̨"@^݋W/M\F̆p$ryAn;5+ + M&_|9$2g :p$>-Ha=W Z8J;3i\`\#+!Az> %*rx'tD,{L(퀕p`]a6}39WyM/5*>gؒ<ļ tœ}W͜%YH?){1hf|Vc]N8?R38 V)C܈~VJCns9f\nz 0gfr%a*hFOI1E9d*a V{Ii2]bhGJH+Ah`MF›5tSe7I +U9]bx-{I)˰~K<ʨq*6X4BV4dA 4=B/f 4=9ٰZt@ H-y%Ż"yO=97"nI)3 gĜ~ӄ͝Ūl +XDvE܈ƴ"HR Tj{nBlrDx/px?TX5Asm>(mE!jB4bl?ȧPk='ݴDֳB(ͼ x<,ev1,P(Eۀ69ʷPȰCݺ5l`-> :w">=H2f~to#Wj&KxƷHi&4om-: y y9r(>{xصGRؤ֥>_*[@G \}5^~6 d鬓ՎF츢K=9 ?v#LF%{;aA Qtb '+^z)>јvK^m'2tlO_0|WVHu߳H7PB8,V KzR[ W/屆Җ7/},)=$mH9W~ĻLLM6=%el^/ytGRw) |xW%^^pUi!ʥM&x+N޾Hя*~F;3[)y1s=tωE;ԣP^RsE32}{}tyHAYBKt,&] P{{@]>َyeC yĚ +t TG?3C6W8)6rxqY?GV}vt 1' ˂6L'٨(}\=i;]F*{@AQ忒&u^S :$1D d +f@YO3 ViȒ%wzW|[Ifd>tBQb;rvhYЬ-U1e +@.BWq]m+{:q;ń⬪d{Gs2 2ZJi~:ODxk;2;XLqZaD=N\CLreK28S8F\@[23TQ1 ^IP; 鼥fXgAc΋`B!+zzx;g9/ +)a&YvD<+i2SX(E \_*"ibȗk^5dvt{ "*>?S5%0 Ob^ -$R)lkIIwѐ\)QAfHI(E_+09=3sn>b!Krl`+/2뾔'`c gO]jytbLQNJrSlVY@%8ϸ`D&ZaW&ã(wgvaI( W[Z\_|12fiJ34.b?K͔Ks]o;Pj\w:ud;X(^r 6:aQuF-İc)m Pq-ēY.d2zXok/E-]ۇOg#0e91*<`ڒÄO^ǡ@/S6;6xO2kFʽ/?AkͲ1t7v3t ֥Vy^J&{gղ%d5-{9PW9p0b>~|XY]y29"Zw"LED mF~Y'IRZ!K1VlX!第;XAXh$=e:;d7u-Npw m.TIM˺kY|=bG#2|:t}ԉ%PUuL%(]Z^Ic{om~٧zXn?T&/Z6&c_U,I@GV3}1G88bêT8b[8ڎMF;> +ET^ Wюs]g'P~˜H6DX(8)![TP (aU }Q.' Nd%DG4{"fXepz| 2cJEyH|䊺"?e ~3Tl PMER<5Rl\D-)c㍨ bσz91 +8#{,)e+!aT#y2} kVP`%RPE% cQ"%<"@ ZZF2FI,cmڡA!E-AٚC&tڍޣo"~+gfxx{d LvhwhuQn BO ?(>8֝%@}q9ibKtH{sa v Kx( I308YkCfIcuS.ۗRK Ucku5bA @{RrZrqYSiO0>Mi?_£tBJ KHi-Hc4{fMM38N{]ص]iGVc.¨]VF-:a)7 +D46) @rK2 kD0$(TRd}hRlE!Onɳ<}] }țS+І xZ!TXWIgR`48 )i7¶:`dqs?sfغXxeRmqG͟p"?ʼn~<Wwͯ~wo'{߽3~rjN~w?+_/7o_]?ܵ__o89_~~'?|?;wN}y~=~?_귿ߝ~1=~K7u;㷿m_ ɸ?K]%?_߼Wz5w%H/(!6~?);'{߾;ԅ\~Bjר(JtM"&1ySC(A4jxak7va1P3yhDW }7'dfظ||~suܸ+'\L͍O<UuChd#Nuchz:HJ$lGc!J(7h"UryJX%H )8,*eZ~.'` ^ |WSdP}@\h~4sz姧Ү[1mqǁgSFo }g#9Q ׋~?ӈN4!ӈ-st:[xT#| +g #gb:ׯτ"8:ţwps iXΧW{~P|b&[ٍNn|_nՎ{ioz5raOċqZ,B[G8w?iNŪ|@4$LM@h.-7"&Fc.,@=Exh)fCqa4Lh>O>ob7X8y +ǁ\FE[#*M&7"Np皟4EI^7ؑط|}jw`t:W?8ws>㷧9)o_=8ק;mFVϖX?pM(;7wYM7~N -8J:A_KR<Y@>o/ ir4xyL4/ tȧTL)ŵZ_E"<'RYKxݿ#O*h;O9x?5^i&f:vc5TnÏE)ss3xs7cjkQLj;?Qʯ%Jd{zfLsiqΐH\g}U\D˪^{*'>;mBBŒW z}d?[219|l9畣ys 0r@IP=h%!/rWV:i j8ʼnT84NM=W٥!_58[9[r߁ٵHp,ڂϼQZJ/^F 4@Ӷyx?wt#iN:Zx|Jl(3j9+Uw6Yҩ|vCwK@2+ τdi9occp9>^Ld8ㆽ$jjl=SDXE^n| 'zX}wc_''@";ѷ|3'VDu2 elz=͔Xd}80I{DL]N$9]YNsaH7z8 z"yk8@c:\Sc:kC1=QFZh~͢g3FJ1Ս}hp;vFq# |_ζ#`gr?KFmy{Q~WNH>)pz39c'R}4u->fjpYG?4W$l}.{s~%xgg8+?Tg`SEt\M4B>jNN蛬+^XkAO wՊ k:Y;xΜ߿*I:G9EL9u?R'i_nU-.Fpﴬ$m4ޝ<=A_gdhƖl 0}uyV?bŅ!z gÃ; y2թj,X%˖\@:a}'ExN_DDp7~ /PYS<su$/"$z}Jx"'LZ'2lVJ$5{ֿNǙݜ DuN]Kq <(Smć~/鹜v}FqҔ:NZoxW9k^7g-$v#gA~9>Rt7q}{>WՐw:{q&5.~$w9}_HkIjfOMUGEKxޕ"}wwcN4Qg?I5&m/Uy}F7xV圀I=|ћgtTDFJMfncU=>TZ=1e#rav>O찛sםDn@0k[+_ghp Q×&(|HwW?zm-IXj H)xAJ+hsh:' r҅zF9e2ʿ,o f5ٞtQ~Qf8V+υ=w|}zƖƻD5jwsO*nN7=5NcE$@M9 +yhu__}ƽMJuXN(|:FvUP|qO)Ht8:qcQOc|0Ώ|ᴳ+7Qۣ^\7QT=ۻV4֛3x'hwmwW*ݜRm 1oy 4=̓Lo-s'lNpOwmǒW,brUsv:PϭdFu$fgR8A=t8ox2M,z54BЊs;<W4]Z *3(>g3*4"AU;dF=NJRi| }+<IW ֿ uri'z~PX]}JO7 n툪,4P +[գo=ތG4(]d׃og9S,F$)x4y4y缯/~gwT<.s20@jsf ȁ3H^ٲ>Ǟz<[\'B)"Nُ0Q{gF{^2 8>Œ%=\z]W''.H1W$}*b^:Fsu6K9SkkI`CNս-:ӺL d}uCelgzDEmmri ' +W#>vu*''}btg)ܬn>ᇑ'upIf{yvCz?ZP~uuVj]C4t[#S܀%h^;"B8ٻuOo=\>u*{]m5{|diIٞ&*CM| 0z͟pnA5j"ڮ{d1.ܠ("K$EV@"\#^4 +*`/4M 5Ь!Pb]6Z ڞ<=h8F@yI7Zi{R3Z;_שЪ 7/yvu +HQb% +󕮴WJAb$J4]t06ebC]8Cj$M#G[;Y1nQ:0ՑmI{ RY {e+R3Q5\&7CWSTqs+,jZ 8_KlĄ#Od6F %#FN5Nd kY&5!{smhVq_CnG3-{u:Z=+YZYmTQ_:Z; Q,F*.H~ZrhTJU)@j"#CȚPBke ^ŋt`l$Tmi$`,5)Ko"$dB;%^IUF654*=2Wd[ 1"]ԭbސ$Ыe3 +;9X|xe#5߷F{(5meWHp.+Qz;4RW"j%UncɶґljҫԁCR+pF#J^iW fAqf;hek]x-0 w92D2몉FvAa*A^ 7JIz25XT`Ŧ#@FRwACx2Yc[9MVV܎@dAl 'N}8":B]A$RddlF lWT*u K F1J +-151Qy5xsS딲PҐ(1)s;Tm(!RƁ() +Uw@OVVjy3SvXH}o>ڲG[>6Ej(oҀ.ߟhmIEJt@$αV`uZ Ӳ h4 JviBTՖ6(e7lWjjm%./,5,y4bەc.*ճ:G8Z\x:,p|% [KW%NN0(.nԑ#^,Yj9Q#F"s05$NC0>C^ŸIpAWD^Zu5d͡& ++ +k2T)Z5R2-C⨉Ԩ%4vO7HK[Z@H4{t`[ۡ׮!OUjɚր"R܈F#z,5ZeFJg )C4\aH{_F;E+{5eQIhJB#ˍJl)r#lRYQdu #F{;ŐJQT Ĉ)"U;4.$<&D\LDwT!QOTJ^F+#d"5h) +]nNЫede?jk,tjU1d+ +#4]A٘*2IqDew\CRjQĀM[,CQx,EzRV̠NJLtNr'Vk {RN"yG4!TA+4L+I$(J#wjLFʳY&7uD(6D!O+hS2Z5J="Cv u"W|Vaz Ɩp{&j$큷P*~!AKʍĬ!)͠iH:=!Ζ6lõTɾ9>%ʻ[~!DnGZ&IJ#3IZLB+ 1+]u}7/-$D#O%RQfZ9ԯAɪX6 Jndg34*vQKAA='DHUAqH*t`Y8b.mõh]Zj:[), 4+DREgHfHsڇ6>X˦]-zٗfNLTÆf}tuoo#lȃizW:K4Oi:Hq Z=!ģLqIҁZWh i%,sDo!}3SCKJͶjH*roKEry +ү2D)S%@2AEa5ClTuĩ-_(8 K#)a)aUBЬ[+(km2DN"td+IrstBl˰Z=Vz@c f\EVd FVkPj2e͐ד NE:H䭀F qO?bD@DJ%X{rM\JQjh!M<{[ uJ]T/Pj$Ven#ubperŕӎpG ^oTmQʷh$IlZ[CA7Rwɴ뺤,(d4Jh߈|]H&h╳:@Fm+5u|KWٴ\Qڒ#o;)KRD#Urfd]Y +$[Jʅ:Q]F֠([5v2{f3fhY޲_/#lERuNTYm,UoyK3Ы=y/>;KM RNG#mi YvtC'[h(l;i;ܨV:q (&B}fMhT$Opܠ=y/@O#';EXtO$vCP*femḞHR@ÿw`)aUKUe8-fpJZ=Uzrɞ-*m;P:A: %$\k00 +u +.'5x l\]f"- &;T:PB-_E\Ķ|bHBh%Td5j[Kq72.ZR!׉68K +>lcSےk J@%gTXd-⦔Y&V,Jś$g ڕ-Ud5]mt)D_Ԑ4R +Ju)-E1`G5r2e|>Y?-Y:ŠQ+Gl4J JҁZY(5ZvH8Q2Y:K!zdl7fXD' Ub523t)Ét`S:Wjdzb6ʦOC ]jWI!pREfw)e+ڒT*Ut(-ʓ=ZϗE+REŠ6`%m$umOfU\k}ۥ{=&D=YlZV"%/G'ClIsiݗekI;y4(#\kCK*k/W+-x 7x^I(o ٰ WVH8^²p7{[I#[i:2n$S쫗:͔4_Sm `pȲ'O^\Q7*Z9]QeoFHF|v +F#!Lz*Sx`vZKNBԖWwMN}}$O6@q;?NoM\aB +H6 Oٍm)'i*Ъl.r_L9ߊNsXMY.7*Ԝ G-¥j}u㯪^`9M {'j"QnOJE!Qv>+b_=n$ Rrᯋ#X*˹JJۮYCc-7rh jJ&Ք:V2H:^ N!è&':QrNyC! Rig'<#Rg#2Uk9҉TG$ކ?\KuICoI؛|e!vPji@*l1QJmi ZRp{mI#`Jt؟AH!/Ku[Ej97֐z .1,vrz9꼘)ϡTBP?m5JKzrCR/תN!NXrܟz-U[xh) pi-H $(sZ[r?TIj= yzt`y8PR3`۵0=9OF$i0?[:ZLj(ĖwDs]/ڴAb0h0.${ן D{DrVw3Rn&~rM#7:'ԋ z2W[9Xݯ;\4[5 Ey=CzRhl5}AFWO+[7ne7fuX/f z~Blh!g41qD7Ӎ^ړO=sd1| AxgCsuСB^2,A{%qvwv& `+Fqqcw|xP<&%cw!^BP`>0q度;źY9VdQP98yvstvQPQº.hM 2^|ZQz[ݍ}UhL9]Vv3$}|2WFtUFowg%+\T.pӪKsT˝8G fC2iLgd]x0\H}km#6@1T*-/;S g +Y)Bd(~]/uȦ2O>[C6b ?J"@xǡg@.aX3 K®G҆I!ox>~|P.n@z0 O_>.R0~4 |Lh.d4<7*3 2yD : +xa\-YZYE02HT˗UF}$jUpOiWDoFwFڊE$eBrýfdˑQn=Y|D(. =s`/ m?MG{D₲!jE*gd܂zy+`ADC:t#ӗG2vMŭAٛq|B8!fX>oD!x_3Μy} +̛S5VM |0A@:B8Y1&!kTEbx0 1=A:l' sGrr%#Q6>Akb62YLA_Q6 +U6 kIH81{:[vž.ʆO](x~&@L\r&(i.,YEgئ {D{9aefץ6=b4z-~_l_Seyw7c]} A{Gvaݜin ($kHveZQ}8>ҋ& + ,oփwHof ?`cF1GxÄ eQ&L=鵙={e#U!=YKat\oh(ɇf<;d܂zRGwXO.` A_ +~ + {td&֌dsz_^1 +4먨|D߀`Ř1Bq%bh!,g8` Q#A XXCqc<Ĕ&ka߰^a]rn4׿'$׿ cGW96M2wLj3l8"157)jw*v Y[&I'p.Y[&HomSX E'8u [>>]9=.0y 5@0ٰa7q[H@o\z}/Ct]4HHE^7 |@1j^mf"94-<10 F dbbJGrhiﳁ9@ҾȾ{r0eYЛܲh|hľ.nݍ=AW}'Ƴ3 +[9#ea̕==up:SvVCן3]5u^: G^4>)8W3Dd"F0 kK 4x#Q}MDkjS|ROZAFcAvk0'D< p_;^@Hvs Y`JGnva ka,l'+#thLbsz5KoyOE-utS`ׇ`^>v\ڦ$kιWu\!8#F_ҡuc˚\0׃ƏE6A$z Z(d|Ёh>8Pnvgz!l:bA'|td9{F 9 Es1t?C}Mg`le(x ihMkSSn=k;Ыq3vgD<#{|⸩helL7x3@15_i{ĸ \6t};XG={8B7 ]->idž 3 >. ?.(~ṀKt5"R3 `Ma6HhMb8–Q#^DxOTe#E6 ٷ#l>`5g}TzBʹ>z߉to{c150五Lx0Gkd3Cۺx! ~KНBH0FށlѰJiz-?aˏkcΈ8.]=rf+ο/.19/ˊMoj7LH`Ejto0nyLj-̉f1_-!<.m0H@-:Xe<ÄĆwmS1M 4L >C!p ZB\X6O5];gs?dY̨}!]qFCm?j}4׸=[|Iܚ`k)&|Tht:{5[3!ӇOdM\0P-wxK.4k0BLEp <4j خ$Ս^TGjK up1>F>GlљLYF2@./5J|)wOs"{Tx?paCuB`.]SYi%ԮW7- (,p=^HO`|b<){4OwĐ!a|Bzq _ +so\ E6ct >W'1B|ډ9`ПGQL>[&'~~.}D`#  +U&<}%6w46e"OlѡLJ5`A/lG_0WLJ`LaSƺZ6c{LX0ټPs7X:̡n|F58O(ÕɔK^.`{-pfx.-W_;p;X*t$^hnk X GJ|=,g1" @xtt\΁\x(37k] >06BX(˸2X9$H߂̈8<+/ĄĬ2_+Dh{yClrCM(CU?\cڞ/bZ1z&f=Z8h-1!6% 9²q_SZ þ䆉<(exo7(d>qi7ǣ!ތ} @?1D)vd:u||zvMiDU֚/ҁ_wĭ`1D6]]wJO!^l6 dSz"]:Q:o57 0#ZHo5 ٮLI-0 sFUҘ6@l|ޏ8*` zI@|=m(y9qM8IH;ȾG~猶0_A\\ h+ ܮbcc +|5)!; $c +#a><uOBn&$W1_/|2u!:a,ꄚ.>2|\ RjALKĺj c9a:>[&.bI5Mgt.[c7dB\݃z`|1Ez8x4Z}ځ]Ĕ走bBDcl(7 q\ޏA2-Ruc>Wg|)k^7nyVLc}9Z L ؆/>`T1Mg3u?| +:'la{ĉfM֠ N06tշj,9;> }Oa +&[S >j*$oy~ 7b >hp-~6? tt;EσGl(4Qh΅z6ɚO{G<"wT3ҽ3SR9E\ +3@`g`=m`";d ɰ8N7'COfl?]_h1f)Ň Ј&?A kRН`# ^nFF7zKTc  σ^󶈌a\F2l?"F-sU +s{01}~}57f^W1?.]SSkS%ˆ8PnU=k Z X>^b0Z þlb^6 >,=7G"w3߂0jBO+|]bf1c %h iY"@R g`[ }T`}[:l=7L{@ngH>!=6p}1S3|)+!ݱ̀2`${14i0́X:-=±˄ q 7Hā0!y"pG:;ƿi;>r3W| [[Lmz2)4O~F=ʬ 9<1p =\N?r(o+GՊe*ρa~TP ЋN՗hO~jrFVâjF1HF Wd3_y[þ@Guf&OB SzzoqhMi(S9,vPьk7ЬZa>.[B-TwlsD(?5VzA: Oya=EWϰ^ NٲvL -L6'H7U_a}\GM8*a-Ѡ!bD<~Õ/w\M MWtjDX'l)}k/ KWVVLbYLCp!srך +?t Lx Etx_+w!&<]{SE rO=9gth!}ׇLgK_O>0n{Z yCaqK/༈ґ\/[zψ>b@ ɥoDװƛ3è! ΂բ9/rR98 +n>jD<4b$\(dkza>Qxr@Fi^,BEL6{*ˑX=xƇ'̘Vd?+G ɕ~e'd ߚ ZO )Ռ! +pWDo9Dү^:*o4[&wtC>Uak.~6\8]5 +xXLpU_2{;:9GtTh:d)8o \Vpbܺ9!9 $kZxJׁn\(iN4uC!_n@<=gү$%֏cw1|aʿvN|sdNyO0Y?0 r:$L<5>}9Cњc{4>}8]6|tL@@>=ΦqFħ>a㒷XSL~gwO +~ Mm@\]Eh|]3o4`ɀsڜSƫ 7q ++CM-v8ћ// +|k.yx"msۦ];dK2{߇.h8[1\\8 ͘El޶LIL2Z#k\:rGhOqȖ7]!'e@>Yd(&aǃLJjS>-:4)wNˤ)9˘`kxQV8 6> +URq^986g'T)!q k@o`>quD_W+&WLrM^cfՍזpRևqc9*XpiX'Cz,)= w%f<^O)>8OY/Sҽ3MTAGgLGU6|_sZ?B=7ҝ #jNhLUm$?2 cOŜqNC"]s_.k㲬Ɉ3-p@kQ S=>8H?q 1?+}c/mo1Z>TH@ 춶;$gW/+/Im\95.nxlRx㰵1dA_Iߋ|Y;?|GtĦq%d@>ƒ|ށ0X=1!~ B?I/1S{q,|X=3؈̡XFX3h!d?tsG2^5#jt-8ɟX7pc.hiCl6s.pϹֻ9ia%# n |ҧ)=[ޮlop [c6 8i{Mm/I4Mh yV15\2p'l_ qn+65_jd{@c.kΗy8t)5l )JS5w.kT>q7L."&8P\)7c\X2 ~?㿯IQI(G3J9zo5;Mkz & y2)9iGW_Z0@XŐ bFc!/->n+AX!)LⲎҭ א'>楀>%-S܃3SZYE'#/% IM%Dru*ctt9TO#v8㝿q2 BL3NogT˥5hp* cY2 +|YOcJS?St)S~2U4uf!}ƴfC'{il;\ CcSgO1_3pﭠ7?jC33ZfiACc5-#-=4 @ߟnH5\< CԆ'sϾ`J/h`A?B+Szƞ4a3O0oXN5^a_pyA&k2ީ^퓄! ȶ9ɠ  qclILi*t_QLC1t։m?}Ixeۯ 7W䛕W,姥S_/`w=uvx +9X`Z-bk~5i??%p*3:o7I) 1oox|PL)?&)[&NreNtv}X٣,OA쩗~0vS+7o^xG^Wm?ћ{2p-n}ɴtGSWL/MNnnQ|Ӄ|љR&qEce sM<+q `3$o8GfCf+'=pc=\o{HQmR?5>Йav0u!M}:nϭ]uc6>\hli9S %BrA6Wܠ;pL덅/@k 1 ӆGMu|zM_/vYN~|{3~ܛ?d% wY8Թ=tO>q_tg=xLq +ƺ}C֊{-˙زjخ |D#rW Y Vn;6=ҿӪA utˁg܁faC^lO[n܆%|۳gKܯ>bF{?+igu^rNo|h1<__7sf;~krx#W7 WOM7~ :d^I_,xh}vo.r{rt4ݯ+|s'{7^_ydsA¾fS/ ?=q1|aj>c +͛w]pq |p xpÍ?~W<~߸L1G_O}c/,U|?yЧ_y11_2g~NӃ7W_8O̥⃋9K׏=ύtM>741z(;}wXRW@G/vc]OmsܱԎ_Y|`e6Q߼}/ܾNc5s^(#2;η7zRw~cn?f{Ami1]lhLeĺRr.>y<ӗ=:.\eL~[j<3c:vic Ko?.63k>}6O#|sO0Vb49vjMu7tƃw4mD +t'eeb U .ex?YE$eGףnW;蛏x2ɞ\ӆ_`/s/zs v\p3ӟ?wxjxZ۵3≛ɻǞwOV +o#y/~!H8u5Ykؕi}Dn;\72 WFUY$zbxJw乻sfNp8?tGxbc ^O7?X?򰻍7 n46_K͸Y^w. wJZngWI|-F|َ+ }+.Vϭ̿]qzCiWׯcwwÅcw|ؽ4Ӊs7ǷQO;wnj߹Y||=> #ot}μu\~B O5|p?lsUb=e=Y\xl}p9w>v׭~xЎ cVf\ϫ]T^x5|夊7Oތ,x7SWbJ/܈,z="Q.Ė_breŜ;j\_}[̿ +ӭr9֙\ݿ0^+-o_[̾h0EG!^Qf燹WJ^\`=s\Lo;g:Sr9vζM|]%8{꼞W^S|=jﵸ7JO݉,ndi䭨7J]-?U/%W5_Nʸ_c"o9 \{ _=]% vp.3wZ_;a)dOJq3=JN_c_K\o[~YyswSB6,҅'^+~]ف莪ƆI57S5o%Xykͯ.zv遚Φyr'(k{VLx];/y>;X.vJmƭ\ϱbo]KhXVR;fiϽ߯f]ɫp.ٽgS*2.w{>^μz~|^ >ԧ]/l{:vIFM8z&biO}y kڣ?$T|. W/|>gKWc:;6ΪMwjvDSqNLeQGF3`<]aK~Ŋw EP.Q?JkQ٨?W~:f-=2tAL[xBꗇ K7Ȫ]Ze٩ܪړ9UNU|_CݶZקWʼEbN3ϧTmErz-<ҍWN vnd%F|OȊ![&z}dܹjkȜhՔUTcUcUR zG5j|)*3UƫUӦViUs򛷟.3>PܙP~&ԙ3˾Sz)3j7\-nh:Q iJE{U}QJDDQ 0(&r DI3%GA ̎a1bΎifbf9=>\\mMw۵kk +}{zK*m~{OR7=|ϡ6w>W/3wos̷OߧYއ1`4 +MƏAfpF ŷa92E#k4!- pJ=)$I҃ޟڤxPMQ/6ީlm\}~ץzl_,n}\)N"w+b^u֊*D]3YLW&OFl{*u^3Qk5&FÑ! -I~p4R&~FZJ? 1SЬ9 +.Ww/L])?]QrBY}Ӆ -5\,nvS\ȫ;v6p?v6We˨_Oy|)S6yDKV"[ [<6+u +=|oC^ g Q3H[?FMG3#;fo|rP{ZlWE /n\ذjA1d T|{V_^ʩ;p5ZqC /*>ސxc띗k6>ͭUuk<7z?d?a(MxwҚ|jN<-ANp#L}Vp- vUسuQSﵴol>{DzxLB\ZxlQ㙋ZNӫYuﮕϋ{%pwBg!(DF_F932/B3s|4R}:[1Nοp08߇78P& Ks7d(Vx(xP?|ʾ+uU64<p£F ıWC_{)E_=-==Mדq/5=}J' +<즳l<~1qFߏj2a`Bgh}?'ۣqhS +lW[]Axw]ǩ}N+h|! r;N^o9r1bq} T5`^t>M>s;kO~.1RFhXajfLM>~,2њiN@6Hg:i慦.C+i9_fro/ล yW}8]dk=1 'nl ۰mx-Ժ=8L{Zr"a_l%<_<5V d>t"i82uD& df<BCf"c~ם̜ф9!h[`+~%DE>W{g[5fO7lƾž~}5բmv^{9Oogmi0Aee4acnmX4B Lt쐩4rgc?\4ۢ6R_Tƿ9rfC_^=rb^˭Koiz1ҥW.`{zjN# _ԛ,"s6OAfFvx-DƋbdjmm0mDgƢ)>e> .ׄA ztQ q?jxџs/txr%j\ K&N_"ό-ΐ2sTc,~n,2}8 Lؠq 3x|:1)B# hK6(*GS64@N/k)l*T[Reu{m;z=qײ_g7b ܦmuEF|{Tkmǣ)~{lF:ӛ=4[Ǝh4ii&桱ґZ4=JЬ ˞s:.LV/嬾|]WG׷9Y!gxe㶖8Oxx3C*;S_ha1_$ւ{/MA㱝G26Mn84Rö߆OEf kd=#&,EDuhOܧPr_o>a# OX}aߩqnϭ=q#gݬuIS_l0ؒXXk-Tc 26+hcИY,)"d9rF<`4ϹJ4'ƼM {eAo1{ח])mPڈmgYK1Noeu}onvȆzgzoGSg4 YuŶb2Hl3a Vӓ,&`#+cSw_"c!h 4~/\lt49M:iWvY{Ά:s.z\sfiy%7u?՟WP^աq,0p9&ع/#8U=5s&{<} +r> +)w fvısf1#A.4TOrqѾQ5KqN stBfcݑMvYfP%h:Uf*]U  +VN-s-,3>Ama.%yK˞8780'^_?&sxV~ Ol%7$)NHwgGN~ +Sj YK?{\}o9_QpTOi~}&p^;2g龻{~A"!0!DC<~n~ۯ^iT|usCu%;n*jr8na\CֆcV|"@FZߞ&Y{ H}T~? +_ ^FxFVi]|d7pQo $Fʎ +̫y/v.%q$ bkNV=oyiכ0es4Nu!ւ _:v +x-A"*D,(]7ow +̭k(ϻ[UxP|8#8xn +'BJLOwO/DAs wu~Ԟ:ZWcav͗?!'Sw,cods$`Y ,c E>C}q n?*Y"$"|a[p_D7/y(?1':%xI_IX6O&}!D6_RvZ</ˏlt|őJ+!/Tǿ :>l奙'lȓ377+;{߶KaVpΣqS}LtWa-{1'aW)s.]C~gɭ藻?^~x\(v oh0ǯ^--6k=qW$*7O?= $zmHyX,,YKE^v*Vx}Dqw|򎘹(sg9I7 wA5j{< K=!lQ3AW,`S︀.+Iݾ oeUP]#f.c$ˊ=+7rF"WR RB!LAlY˅9/7Gmz'ɾm-VtOՉ+nq-WuٮPWAe֙Ұ(4 CmgsO7o]/s䴚2@^sb*]i&WNc-sG<Mjv\NڑX` +FS-#̆u'+z̍#;~´[LW!kF݊'7Y^i)kK=Vr= _t_d_ aawjp'q(TPo{4}f-U +s'NdI!h}eKЂҩs+4“s#rLBW̨Iw\Z|c#_K<;_/v[Q}6WbW+\ywIfk;̩L&n"f?c`q#&u%{\= "_GwU| |(,A# 4^-:88 )lb>hhG"$MlV%-]u)랤.7g ٴ-ϖ0%۬V &BmJS(26Ň&(? WݾU ^`:.dJvg+NJ. +RlO='L9<#i!a;!Io af^A.\\.B v HBU^4.F1B{0 ~aZ˖DDT irpw_Z|T9MGKY4|9/vA"$Ha\5!w + +jFC#//\MftѶ1fS)M& +}ߜT&2efsgOlEwt>5`׏Zg~/q0e ;EmF֟G-J2Mk7TI5 D[d|3j@KORfZTtkotd۞:n6=OhPŴ ,=vvJRqFbEh|WjPg))ѱdltM4}h_mtp&Zn/ *kJK)?8Y3 fR'Qe_L_%~(2YJ(͝< ٙGfE 777'Bn^ط'C>"yyI5iƠ=jAkX:LbCE~\ӹ\73hvS yzL '"]l 2 +4#TgJfbjwOPF]ϯ˷6_g O򌭼C@;GY,- JG& Mդ6JWfj/fsAݔ!`D6r.[UUEͥ]:+QA[ko: Hdכ3V'=}<ص ^Rk\64hy WW-O ϶/'Ơ鲝GK{Ng>PU=Ab=Tt.w8ũ'+ݯŲu;lܺTI7Jpqo,ֈJ3/[F7\o88i/Om6qv!c'd2|},0Bh~o'LLL%MkO R4K㋚3Mg]e\^%ڔ+7XtF)h~m :kq>cmcٜf %^7С Ά/m}&#jyК@>hTVdhC4qL+A'Pj"YLq90_d}߹qv|pw>]­7 Ay +k=D7jXS<*gl{gf@w\j;jks. XW5fv=]1vw!ԏ?hGʔxjPk7ڀ\jyPOAϗG\҆ܺMҌK3s,* Z) f6RTN媿:I4ǂ(ƾBzqb<cٖ Eֻ>2';[/z7"AϮ޿\!OqG)2ٍ#ܵgvZ\zϯ^ wO-uXL7.oA_%/;A݈vZthE4ٴjSze۪/ntD e~I}rd؀=Scگf&5ؒ=6g!i?zhRkr,ccK`̃dzH-dX 8VHC1R +kL@TvO:Ü5;ͨJS1yå ,X֝ qw:1__A rAcJlJXbDC-tr0IP,aHcyY@JuQe>bGt*-<Ōϭ&sұ:N&q&fT13ȸhSāV~v9Wmۗn"yˉ=:hmS'=ynMĩ҂ mȺsa[ك/O> <:=AǕ lRLb0X3 G𓇩9-C '/@4ʔ);l 6 U2|ӉvaQXz+_ /\~^wh";'hIHHpgpڞRvppfVc lN:"Ft1 /ЄxĄFkx% g\ih)6#8fC^U}v*6*^tؖ =@poL\7xנۯ9ݷ]!6PmLGwV*=`֫ `ʤך2%z5Zk cK FL&,Nkfu4'ށ/s=SM@]YkCx3-D~V:dGoY~ZkByVҽDO +9O,by:8u*2Ms-p&C #wg Uh)5=J#Љ-UN)3VfUxb᳁>|h qw\Aئ,}yMGsms{cLB!8A癫 r%F"}Re +)q&s`:PJ$CA˛hDgs :T-)!vq ˙0$-c,d;-/.ʌZНAWc,28AnP'PI8%+6N ֗'Dd6Dѥ]ih1_ZqQ>4&b$SloجQlN%׫N6?s;_x::1[n-"Ҕ+ Z@ߘ"1dž#Sنkn| U]qpmb_Osyu*z ǦplÔl}WfLLv{_?[:)A3%]cOvzݮ>ǭӣk +jE Qs^/ZZ,wc?1o{8[:ܚz>ёf wRv[ Ugoĭ)7R$ll`-Svbk.+36-dw:g, e^HGM-V =@SګJ1 &+Ѓ_K߂.>aᥗyQ;Q g+( :*mP;{_8;xZ&̖QΒ ;K'v;Wȏ" ;&) Oti{ƀ3?Yʶnnd9.ecxlkޣVڪO+ +:wpC/jw>.ce;'!:}Ӹ;u?G2hz7p[P|peVR/@{Cd_)C ײL3QZ܄?:Htf :ہf-G#ص:(e| ;֊`g"x&)C݀~H;;k$^쬈̡Y[(OMRbFtWnWF(P` +KU^h2-*#|x?ߟ7.+Ь㱚ZjZ3)'dɛSwCw~՝`s4`h*6V/OՆ5 ʵC5Uせ%Bt8jl 3:GV[I +lj&ᰮ.FX)Ff+UJ Sn%E{-s{vSəD(ܥؿDREU-QE(" #Ӈ7SԍR5[du>\`}5!nUXہ{f[qXf{k%W33oƵqQʘ=7BXE`s5Qa}&qzZ,5L`<+v!7wvV ;+1_F0wA_b 1g"0k#waC{ ?(e~[ƃ6aAQnkP鮩J3}3a7΃td l<~;a i^~ۤAz&?p˟⑘P)TֳE2%&6m[}~6Ē||A$1O$ELr UFY`qAM‚-34aOo3s7,ԁ%g8; qO?|jM2`xPL^pT0m6<^K'Mz&jdfmKkKH3 Fy|1hs>L3H(qG\*n#p9ox%0~*:W/8"(vتvܒPl&A;ag~ITb[n8vVv 4|a.*չmVMwv +*dgΊ< ҠX[*q1/s7[77 &*:K`+㼃_RՀK1+O#7`jdPx%>\='Z3ߊf \ﵯ|E?kr W#yB욡tHIiGd ׈$k+Mao𹰆i*?YM\SWln_9#٨lk,v˗ĕo`Oas>0`3agFzLYkt ;k E {eRpHg@ N j, ug BU\5Gg lL&m7`|rݯzoȎ<'pOT'-cե>  ץړk󁝥{ȷ>wvqwrF5C֜OM!,`zV9ݸM*dg _c[-A[WXX}`@<4qK;]kxDey\k \Jsp~ ^[ߙNw/7Ϫ:< xvޓrf &2V3 4'NTۅs,kl84ooqXdM.$U| NUl0p %!,f 8 +|]6B0uc4SW2]VTkxl]~U>aǒb4Aqlx*!S6]u5Eqx`~fWG],h jRRsqW*|a0㾁8~h Eŝ,>$7n:n'3 ÚC@ uۅ ӔKyׯ&va| B}'v[wz.[FGgC<.owydlj8qއXvVQ8`q0Oƺpv@rAPWUc[81;MbfU;>JĶw +Q]w)ޡ/}~GcܕGg}iwe!G֡T4 %&> + LYm%O5+<5LG*vM*o5 Ɩd혤*=kXoWAX`=K32tD-EJ~!R{Pl{(f[f!Yl%\u;'90oqGb֊}S`!E^%0؆smO\.\o&1Ez' +vb)EZSF +ۭ5!l3c3gy"V +y%e2AqH|l[8o ǡ!a0iX|k1[v._v#ak[ bV\bcp_K=` ++`Sɵ {3Z'S֒I#mkcLJMo* Ɯdg E) + !fT$&#wSNVԝ3&Xu"؇)́U6G؆c\p  zRCEJ _ЍmTh·s&o9Wd8rwC[9u)c. ΄' [ [ pnavm a5ly56G>6p x)R+ '9\lG ֓*'P:k8&>bcRq {iKy[7 XtW26B:<}xha +D`~A_$F*} K[e<emǧGuϑ0ckyp¥_/Rv{0Ԑ|o2>||>>||>>||>>||>>||>>&Nt w I +1` &:{ !" Df:'&E%EDž$Y;f͘co=Scm;Jk"kqb8,,$&btx7GyY;ZE$ +b{ 6.7 + +.kV$O|%xږ &Z㧭M1eol +aͶE~4?b={B5:^/5sa޼ֱ` >ٳϏwOGTL௤5g)3""\9_kVd#|yOϝ`ϟ[/~[`mL[08|Gw0`uꏼ/ W0χ5Q@8R˗GnR@R +5YD6M#呙d-r-[셼=8$DEhu9 / M5j4bV@H+_Y+H!q()YX*2KQ\(`ϻ_0yMFGi6*bh 涎|%1yzPCMjeVA*^fTCWi*4Tb2tAklVuPfM |XH͓sЧ"Z'ziZ:\WdJ Ơq$dtm(Ӂj"L@ _5PE$j%n4:j6A=L!9ŏA[A*2QźTT<4U Š*A#!fWW瑋ClH6W9Khxu`>&ixyoh#YP&Jd"S+sT95%Ãa̍UdTu9n/Mʬ͖NkFPT#J+3ŤQ)ʵTEvheJ x 5 +D뤠j1^c*@J` +Dau!hP0!@te( &,NLF貉P8CEdm.ej)ˬX :_}zy&J 4$xl$A l4E3>6[҅quo%/ΌMTv%:Z -.Ԟ`VT/x IxBH$e|S%kJ 92I7)R_#/; 4)x·I@@ hHpfemh.ք[#S%j:PӬ ?+oV+F U,ғ+4dJlG5+Fˍz4m L4 v"]f̍ 48.am~ z?ʤ2ce$|u#H?e֏+̈́IVE>im/ԟJv٪6ZkhClʄ\}b6LqE\q_Cƌ*jaI=3ԪlBuMo.MIF\sYDg <Ԣ&z'\?XT˶-s`b9gPɡWb@TTr3Mb s}~߃7͖^f9欪1DޗfyEW|K\g. t]Ƴs>?퓾Pgᜉs,>|=c .NgXpE$ߠ +Zb^I KG80.裖#L@\iچhBiHBG[BY#L"Mόы ~&'#,0##.c@'6CGa[p +:s$xi_:=!<<t5I9!96V`l=>*ʽXWa!`n,h.xNlR8(A>@l(>xsT.s@2GFc= a̠;s)>{M 5 |2#~04DKDwV6|/,_ +Z| ଫ>`l6|`]3F|(slK,sh`-9ۆ ă]hD5\sȗ|GsSgKKĎ>[0k=z==t["?kAB`b?ph0lpy,RF3FNhr_,$lh؂<0בu3 S,q#Sƃwgr#gzn5Esxh:B(yxCUaR|dH] e8N^9{l\].pk>+8b|Tϝ 842EpF1>hz$?(7,At( +AW☡3=?]|CabKwaa+yӾߣt|&f2x!അ.BArg Κ{uy]:~_ڠqA GClǕŀU;1yT|Fm"NEirqϴ +|>=A .ĞrY`p/yXDGş9x?7EsBq9x/,4 /!_ +uh`=AgE1* X2#p4Tw@e@'zg&ՅWx:p6Z`nk,G|&j8LAwy4?Ɂ_Z"EDTՃϰ½Td7Q q q1<00r?OE0CD+U{R_|ѐX\Y>1F_@:t} hh`N3' |mhKlxNɳ[ t0 x1N$$GṰi8P(BK<2![Up}n Ήh|;A {t!>1x*8V&IY7h< 0(S>x CC`m͓nʝN h/ 1 8 `|fS`+~o>褓AM3w`(pm30ϰ67C5IyiHiCgA-+0Ƶ>ӎ'?T{r,s('QhI| >]٠EQB^Bxo' 9!zy`fANF\xJ_AS90 "~HX [ Y$qۑ .  8f>SzcPO}YɃ&$֠Eu:sq4ha!ЕqC5'#i>9KcAԴ/1ЙtV&?7U^RvQ3G 7`D̄odl +Ό\`nz9z( X98w3z0pjmđF>X/ n Ba +K>'Xb (g 3aֱXk;1зDpp| uտGXQ/`$ ո@]kסo nMFVlP$]@G +}>wi" m\ +薪^V"o$qM\Q>Jdj1V hzg*hbokq0O8x, E! +b!FY//"+`/ S$|3Q7[Ȍw_dLˍZ\ 9㎣*v^q :0t̩زdj^ +]sh!p%-+$6(uJ?@Dy-~.ԥX 40!/g! ]ń<]KGoˀ(`Na_TcMFB͉'Rs=yXNdQ:з+8~$tFM$֬Ude;xIЅg':6c;aM9H޸WNLj"[C PZ"ws2p=rcJEPMp&ykPٯL9πYA_4š0N`~Snҹ=$k+p? g(e@^ R@:Ao\lr*G{!"_Nto.X3.ㄠye;Gi AAآ'8 qաׂk:  &**2] E!ah q1> !S( 㝮@_B%&0mXćKL6Z"Zp5\pԖ]Xy,ui!O"=1Z! endstream endobj 46 0 obj <>stream +qCyɸLj0hSn/žesWd +Q:@Mb7ɋ|h)ybBjx/£c`MH^w+DXd-=BݎڌmoMx?=&HNm& ^4t`]FS&5{6t*-c (B}kS"X t?=R+^/DSoub]z}Xް&[<7{ Wߐ4c&J٠Հd\6:::?Ĵd1QX+}QF ɘSC]FYOk)T{,`]s OZtE4*Օ(g_d-SK%/Lj8#1GFyD+V::kb~kc"mɿqy$B*,Мu9޲ =D)Q;׀QMt>ѸnCh԰[ӡp_x:vo-x&A x:U@C:qw8Be, g-;d-2^¦V +[-:[f8I!-UT# <^N#Wπ~6a"w\!Cpq=D' E8 fC:)qx)Az siPBXH&U:2u$C<=ۓ#cB/θ^@je=AX A8B+q0wk/Ic! aB +r4!N3\2qk,$d_'{u}#IIl9b x^=O7񹾘 rm> +FpYsc)`'. nkPk @ y#,]> ־%'8vj۰D<:Q珋1!&E)Xϱ-q1S,k1a8zH'feB];葢zY KPJ6vց +.)3W +bkՐ~v7{T\3JAS<gܥМ/H'Q ~PA=?kпD- |{F%\nU$TॷK}ķu5<J v'& 8>oa(KǽX;u5k;? +zXB@=[_2akC.KŰCGc>xASx/Ki5V}3C{+)GAGbTp=(zDjaz_rN !g zLxyL="PHlQ t~4}H_pK#1@zXD^(S]*Tī o3})c6֟`0E*baY7N*cMuHZ<&Gju@= i8pHSZ=d^+8x*0` XW3_>xzP(x< Լ ^n;ٯ = 7?hZc M" h xy cGxu S#Wsp'Q_ӎȣ1anx0n9 _O7Ҳ[g")`%{qH4t&^GLPuT`٨ ۰"EXX^uH]ԣ.w c 4OKc+ہ=&P -t]i9YACam݂=&܇<&D1aDŽH<)9\t!lֶ=^VAb=՘ld6['0E䓼4O[+-Z0U6|8ҐM:6 KQrV@71AcMaVA]pYxcΣlG +NxcJh-Bu Bn{26j6fQU@AxcE%f0 +e_=SaF~7 +wKKg1F[t07u-$V+CCnz{.PLۅ)u-ZV +~KTl§ +N5pxSAh|YƢ (X/$ =ӂxZ%Ϝ 5BTq ߄3\N_lo ] A@Ib//它=C&eCcw͆z0@s$c;[ר +cK (+>Kg*:tT'$Vþ1<)OH-um<^uZMArv ⭠x=G/`]TjSEכH_s+}D kO{+JPN*iUUwg;k5HEѧ&5 Qׅ:mSGZz4\^Vc yr;{ p\J'%U Rɣi1>y *0E7|mB]!(Ul-2j3r:xmIS ^>zX3cQ.%hbWޓ=*aaH X xIj^> Xe#|@x.e ]x7: #` +`mzWA䙢?QCQF=2(B]uCfS^]:Opm%83jF&/w{Ny=GvB41CM ex>Գjػ;U |iC䞱V^WP򻱄{")o3"r +W-~o?g c_n6 xR}d[x5{`8]{k {a/k xoT3l_ +4sQ!FxO7uS%q{š}}cxos\Xڇ<^2: &_BB3s! )^=,`.^q<`b4z?tXJ[ßlFuP?1~>4-=2><31{Gv>|:VT@@oDx*H}|M轁|(vP0.d^(ue>㙡#H܀hn~=1 ֊{ pc5{X煞ǡ 7Sٽ)qWzv \4{G̈́^K:S>7/FLDWW܏도u q o{ԏ(GSozz8xFg,8O({[iл렟FGmVqWF\&N\r_W`N ?p{]ї2B֤ܺ PjvmTf!2i7Ї!L܅rVaXGEfy .#򿷊x@Z>X\#Wu&{?֫LGm{X7iϵETZ~[F85y~%XygUܮ/ḋ=dK$3T[=<ӖW ֢ͧG]"r:vѥ%KqЗ{W=I4O!3$WHJJIZ0x; .qNJ2v7g/6 2v7x q{&6^NWS:ú <\z> +#v#d"B0 2: 6e:̅:I2k@Ljſܭ\4dv4C=N'E1ڄ{":3㼒‡tF?~ G=!!.!ƨ+jvMXe;!6u%WHɍ*\F(1[ >A~ F6yyyևp!_|3>>,N>hf݆c+xb?X+˖ۈʫ_=o)j }Uz ďȼb6W̤}PcVb$w0btV&ީ@,\Rnr[y'yM;h }SO?Hآk6OB=g睦LA :Qך.|>Nq|͈ K~2+z~Rau# u=t\t}bvDZL{M ݮ%:fIU<`oԭZ{o6au7j +|anvNQIKGVLEY4]]u|1 А}B;Q?R-ftc c4R4o 8sy˞~wAE32XXx)WFԮ1lnX\TyqqBh5wuoZi +?o+/Naʠ$M`r愤Ψpiq~EI0s+}PQ(14'*r2_W}A7SﳫSih,.ǟ|D'ڳ[e>buݔ[Bg[*o wR]_3/(󚭌 +\$Nָ$DTȾnWۉjk +. K[m/뭄oDg]âFU2kQa5<#=;ym4UçYT#ׄ {V?Qos΁W7^ٟƂ_U_S>:MO5=cEZBwgz z*:2~+y<'|dn$J[uD|HrN Cm?}̬7OFwKOeֈ?]nѧsi Iطg^o$D$ fiOm`SchTHOTfvST~mxy*:۰Z۰rse]DYcrrr8X„u=O?yO8C?xoUUt}⾖͇~%P.TN `?wH>EUKn'yg‚R_0ou)A~iZu?>=s'hذZ/'iq]Damf"5.<.YcT.wi +wYJ̧:(V Tc#Vr*{=3e%M&%Q7D-|ѣ#B_~1{UJLڋl~m_'}Bæ9%ddb1%Qaf2YRb[\lgU[VCi~NgKpWc`\|FS$Ռw-GL[ʣϴdnN #3-t 3WS1~JkuoU`lz=bOyPe(Q/=Ǐ2].;$xՆ$-r,u-r~_w7Ҍ^1GQ/P)"aVY]`˅QΫ!0GW6;gsJKlq'M&` ȇ&ERއk=uB'?h|NnODl߯Il޹غc?q +~bVebV=by)Y[_:]]#2^n, A1w8ݜ7ꩊqb*na1?"M۳fۈ;m#M~:9J'* + b.1Jgf/!"G9ut\b1 ,b1Yn61s1wRBiZbĊ-V~^ptj8 3c|2z+~[9MsdACӷQɕ11>qAIao|bo1ƿYsTEݱ9"bXcۅP4?=48C9=|}:.w*i|Y- K  g."kAL 1X~R$J3Ӕu WI©N}x& d=->Πn^ Ox^%V)EY-{\"}]y#yukdasdZ,'|0c.A'R#u,q[xGd7lIJJ5 Uk9 +U/DL&&Qb8K}#~<=3]X[#O +.2Pmn_) =Kc +c^+ezG_)M-+;hn.q/v~SW|g?7 ~~-&U+cZ\cp{Ng-Gq8xoлWc3=#^rj7L F N$FMD? fNYMbFQ0@ycsAuDdv-.;+52gt[h1y^U!q&{WCl>*rUWmё_&45JwM{ 35$ztMQMEaOߛ⪽' 71뽏:Ĵyź8zYWˢ1+8,tX-%)J${]ϝ}4J94B?~LabB]bEb{$,94ekn'O]u+p)z*=:쭿Խ*<6?&WYJgrרr/WFUts{uk3&sr8"q$z}{Cp>!T@$XLQjެ~8z\׆{$3c>';C$Z0y#rĭj75^H۽317~8O7E{$41l,vKl)Hi)uMpy-ŧI,G5ȉ73&LsÅ_}59#h|'ʉS7 Ma{2y:9#M7}_ny{SXQZ^=A[tjԹ2*U!(Kc=f=49U6c {1qsʰ yĤa +h!Lr|bڈ%Ĥዉ)cV3+;Rc +\o|9!ڭ28G@&h&(։޽p|yK_B7)f/)1P{.@qR"bhtRor6Du}]12 a$bLbĬ)93KֈeO 71g$f.33Uٳ]'u#w/YU'> .{3#we.UNꝒ\jċ $&dC9kЫS9Ư$EؼPͿĬ ikܘHD,\gI'6ݕ3E9[۪;z?py^qhj˝;]`u;wԸv4:)wAāznϒUV䳩rBBjsNBi3ƯF׶Pŧ +I]bFcbV+b~gbn I Vӧr~@\PUPUW .(Ǖ8:ʝc{*:\c&Dv͚4=dfx2ġc#-szWc!CCJESRrgk1_X2ąǠt4c/ux@1EFqF1za.1g&8[P\F(7&Ds hqG~KXӄWY*U64o_PpI3(FcZk뜒R>78&VvU9C/I-Ro~LDׂ/*% tĬV#f" a&gh͝3{/1OQP"n'rnzNwFwRc(Ls9Ɂk>ڵMsTycdKS@kr]KKiNl!>+C̝XJXnM,'ej1t&7]|2|sވ->c8~[mG1f={ nrHnz=;8v jcue#ZG: _oa/|=[-ZK9j%b0dqM~[HWyTs[Ws;v5LzqqQA[(y!0Ӧ^F%Cb*cbѶR5;b7 vqbfXZX\X% +m?xP;q&/];oD?͖VmfJjfrN_ 8~iމyWn,ƴL35,'v/Ϛr0Ƀ *m7R6=1g ؔG< qIˉ[XbQi?rX;g+g;ՄVpRNG3pI +}җv3O/w3#T{7GְsabCԣA[Ab )o+u@H\QhX>6aґUuڤe"X7"pq\byԉ PXRXcGl;ys6o.4~x2m}EWR`zd hrzzъz ;k3R{"bNifۜ/EPHo{~hB&;#wW-L^)SzRTZ~ {`/7UAVGUɻdZ/8g-X1&`sޤ՜GNb;3Bx-Gy̓6詍A]s# 6unJmK1{ #?UqfǵZ<^qi]#.7]Cp!v茼HNc$wKG%4}ӕn )K?;6~u fm9ZӇdyfn"m%AÔeU^p4r{5$?;<L`E_ |;"x.<%o2Dd--Bc7L(߬ o^ Ԩ<~lKL;Q@R&|QV%)*3./?v؜vCAVAA\^Zzp_?`,& ~YyXeE춺9=n{B1ipvq:S mca`hL0cMS1e{kt m f;!vclBm6BYl Nsh 1HВeST5:"o#s@tkzl-gDZûU,O?g~Ɨz9U?\{2Ni6qH8|z 3K᜘/ԛ㢪*'ޣ_ ywMt))p f_J'YD҃} c;6?n#]0ji(88rKkE] &Wm1xB/wNM.wP7ߪ}۬VTjʉ9FN[-2("/bxp )m[⟾THO|(Q(7,>Bq^e/|" yaSIEOՕRuں򹾐z\3 ~q^ۭdUX e׸m1]|՜[!-#BUY]*q`>b]a }Ci'3Z{յ" !Ds+ՒPB9sBErlv1i;|ιs7ϾuU/]%Zݽ֘#9|;?It=/Lq9ED9^h`Ěj瓎8rGkK0>t扯'vAiWƮo}އ3_%/op7^_jo^}T b?zzWpΝ:7?M磥YSoܣos_y]'B!$XuO7C>j>8fGZnqeon'G\n| hf@X<omJֽVwサ`84 Lh3?Oy'&%0x\vN O~^2X2\7 /͍7_{y!0oWyio }Ӈƻ,sj/bbaL9?G7gxDV7t۳unϖm:oO_!5ZpZqq**6<࿘b%: 913, ]^zn]arZ>\u_zqX]~j%lUAs>\G齕BQ >~Xqf^}ʿ?s%g'q_<StgW^_%W;Ks0Wj8i0_MgTR|[g =#7u?f{['=O +iq|f1OiMgeM[;?J}]ՠ[*sc7Sj-Y&Xa~?7w֣Ȁ/>,4}z7Wxg?}|k?f-4OasnGd"3m4[)z}ذxl\Ro +DRԔr'bFEdQJO?S/=V?semZ'C-ݠk(Ռs' h7? VaăךoMu TtqpP{uX7Y%]Zߥ?JcN¡~5o5$۳ mأsr@>lԺus&9-;]ȯ +g+WVb&O;rcz{@ٙr`(DS5iT:V)j&VpVK/7_g7wOavpĪ1.Vpr]?^wiP10;qwxᅿ #A/B7(+>5]<ʗt/B>`$X? +(>,o|G%iB1{V+//Fq%^/|𸳐buזr]oz})Η?>۾G 0*̅^ޔh+fXsQ6?CSa=? +Gk_m}A3Ě1|B#oEgvXrafAnx}͘d/@~?DX3}/N~!OƓY7}}1f1&m?\ ] IMcE pwm6nhmUC,IZ}Wֻy-۬ۻK j1Ֆ,;}x5jç7Oo.t]MMjxȀQb2n1ܓSs;S=0*՜]=ArATo8vo"(qu|⤘r +idz0ŃļSHK2y"5+'џ)Кs0*á,[ICoOXys_<ڟ 1`\l5 p/{ ~u~ ivhC@f4֠=ğ4kXsw_x7^h%ЯחBwqxkKxsn<.w +j3{mGvAek`Y?c7r#=dVyy:)jT?ڔRq,v- Ɔ蜑dk}p|-`@H9Djxs uI}=JZ,V>Xk{-7Vo;nNe?wNttYTA}.] +'N8fqi==e۱qS=Za8;Ÿީk)' fk6YvjCsF 5c MypC3]8\]`8~Xhg0 +m3䣍Т@fueBP=M(bq"Bvqt}̙+`QqBJ0N-':, nq@XK1q[_]ovAC*I:V~|1 CbJho~};t^ N;fBG:@Bl=X' a)6n[`b%ZCKF$졹?+N/+5VJեܱWSd{7W\kGݸtP;Q{-]}*|}YE75`VZ%/E`lys ƎO7~nyk>tW?ݶt&̈́^kq~=} ։О +i}#Ul(pK/7L#SF nk}a:x(UХZ•E(<B )\%4WC-e"΢6?w\rXClEb uUN2&3Z'SmY 4J:$c8V=rbq8mZ4Fά]F|.pP-W[:\YsuniFN!>}__X6YKX8aFXfqozbc/2(Za#C)|JbHBb:yiI:E3#:` ENqx+= +}2?9ʒƐ,[!v5 ENܦIR8[u dv+Y;Lk_[Fڅ%{oo)O:;dׁ;z0Ynz"qX f7g6'm ]LU2&A Dfc)%cDڦG"rh` U][^\].0Ĉ8[ԉnk/o7ZM ^7|'[ľ{A3'ASX1|\|$mocTh +M2{cH XRJ16acX~A .2ݖXB%=.2G} Oj= K>-Jbx[#c0(蚡_XDzRs6__)ۯ=S/Zυm?sE= rujfCsl3_qz> }~DUjEWb-t[."Wb5uos.A h+>I8r~R;:R%pjv8}}t}*_q1BRσ=b&ߔ3s_-` ՎlmXz۟{wVOu{Z7)8l +ޱ? AKhyFt\h)>mlCJl:-׿v-%@L\X삎/]#VEcFFb%8s\ xMrБJYG%.Î}&]W3?=BVh2*,9BrX褋7]z),& tS~) `A O/oQ +#& a>4'+NWNUjd%<_Y>(U̅ҥ':O)ʊ6LT{0EW:xR>bR#XS˳GnރT<g %gf}Sa`챘1||O񇛙 v]=[P󂙅JgίCM}[lb5#tv脢'B׹\kw{`ʬ$GIZ? RP> fxe9ss{UbOw~_q.ͧq鹤ow慯kOV>FbX.b4j]V+2^ft$av-Hlj5VJHJvNujV$BF|BG0ΤY ]O(4 +gdg!XImϫv'AՔ{b`k-t񉅗Q6Nnpp@.by'9Mժ+ QfKK7kqKS޹az8hqz|xwCoōNVC p#wV,%"3xoMC^p>j!8u%JљJĸ,?4(8ze !l ZW;g|V^_(ZAW`5,4^GN:Kvm5T;xV\u!!qr`&! k]z@s:q̟dqrA;|opPvuPwTUWswi7߾ݺ{H{+3;ǂ9}:kz#@^pOy1Xh79 +aq6JV +0u&kY*tW7C=4<=,;P!8lYme,ǭŕ8j`T[4m Fy2H- 783X!q|#q L sSE97R9;ၟ+]}!f<̜"- ?0M>/`k>hbgM֩`gbg2ԇ(W2J@l<) O3 {Rۿ+w?Kvָ^42kQjȬ|Z)ꙅ$獒BG/=+G[ ⷀ^yj| ֞Y*5Zt=]]4';Y#yӝ|kς?=k/,!8%CF'+^[&g=zdkh,/G\rN &fiPC(#mC "<$[%MZh+CϼUʮKvV5Y,/pP OLfo,1^`F/X{VRp EHkXHR."|&hzד78[,N+ |,xu S#m]uz!WЗȑ%d)^0kqҙ:xuCш9 Ƈ)*4Qj\4C<A),<ta 0!5&7vdH%;+4{){p{3,#nWVTb(fUMR+h-}-Bv_b{7,1?Ь}qViGtxCuS߃ugOwCw^PTs:+iI#3GHuNfp-|@ #8jgʍoB+gLEۏɴ5zcvbM%cŞZ:N?9CKm0ڰ\Y"= .-ιQ ja~ggF1Ǥ٧g%I g"G)#䰌`q:yZpr1 +xJ'l铫,g~`rsrrbJ-9 wo&u>pENoFJfB߼B1\p5X̤`R^og9fxe`gYEfvֈ/rH-.4õNȁs3g2ŔS?ɈrpbouGvρ61kYzwO@%Jv3t{&l/|# v,x|;Z\\`'ZdjmPt~RjG} Ll %r SUl* :rb"_X`χ=f.5Y 8+)Xe$vq'bgc_`kՔ`4!bH']'˕@Cz!|W%ٔF>@Nmn#sEl!FB9; +:Ki3˷6Lעӛdjx6t =㡝v#~v>϶;+,+OE5RnbS9E, d,_Yq/Y)#+Ndv`ẁ7IRl=r8{~`LJXK?x~͵K:CTv-|{E$[>9?Z=O[|9ϾPQ< +rWtz9#3>~`:h)gN@~/zNs!,91E}m x`s{7<ݍ:bD*ǍSԪS򥼒m\V y+n੫<̋YHNe;+yYW( ֩Ic wWe5MV. (TD|kdzM&=!piHb7O3Yuf6>|;^P-%5Z8V^].:Gnys}@jx18eO!X-kKYTN?dˆ/͇ЇpE#__sJ= i \sKi]Y1qM.؍5f>[SEP㪽J;riW?XFXνzk.h,{[&I ]H{Sa\;j_sbY5 ZB,;)F_B δbQjY_J,wD,8+?A ?a☖9b$kEϓm8~_2?~3h,1 .7aā.%.=}!.RB[l;K;X0vxb|HHyӡe-Pbbg~V5>;Β^l}wb[YZgP>-ȇ s̖K{gktx|stMĵŞKR8SI oJlQ +AUԪ Rg{z#Lf3rC~^724FG`9Ͷr[݅O\:43&g*:=Wk''FF fKJť {lF>r0B[QJ-2+QE'YMs.'^PF 1P`fy6ѐyQiȮ=Ff7t24_{&n) ڨuؿ"FM~Tb|<,x4%6rÍǍ6~7eY/9du[dz쬒`IXS'oc]-k3aT C]sjpezg#R۔PϪbbÕ1g\|Sl}S/|u݂3vsWosc _n|vf|LC/vzH% WY7)õ~oY{I#RKQslVB{B̿wsX:yc ~1UX\!W։3, Gl݋,?guZz' +#qLzv @}/-Z@ݏw㬧zkAl|wzgcؽKp.zW;?rOP+ԽBh dNxƠ03OS {cBX;i|ӎ7__T[* 3*Eb19ޛVvz|{3J?+8!)X>qR>.K`?-NLFMȹA^^åG[ f{\V{d Ex{U"zx>OfJG//{^=R* z8Ty~TZO.>ntrg 餱ak7nFGp)m&pR$ma+V!N̪gqeO*OW [^Yb';w6b^α"-C, pmnfӰƤ~"-\,= +|L1&}R10p8CRxjbhL~|X +X,j6j{oJϰ;*5>:SLTo/8h&wqݑ'aUb6+f"c]3k&ȱv*y둯zg8ub%3jgoMOr3G`2"e4;L"' ױ>'}^)ˉgSycNHc6ҖvW} ڦpR:\unλ! b}Q-9RgY:=rh.Rb1y9l8'blۀ?kH󖆟{u +᩶A|pzXXLrGzߴǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫǫϏņ J +r8wu__t8)4a~JĤIqA.[طVXe>4(e']&FGIJoC,óEo/}E^A顉V +Xe;{gne_7nXyŚիֻl\fuk6nt7lXv h|{jv~o/CCfE]DakCZ쏈bP~0Wn+D<]De ]\~{ ^b&e55_Cߞ~~c:7b3W9̟Or˺6lt }{uނb'[[>hY[xYuuNoPShg, Ͱ} dN6Y{k{kwtv?:)c8H?/EXa鶐22j&BȔ6u%{4_Ø!{0xdIvPfKmS2G1}a0AZAUM9'1p)G'b :QVh yD?-2ڐaYx:H.0 EQQcedfה?0aBDQng3 V1$woѐB,p Qf#RJx)pAKk@#JT=d!DbT4@?c'3Q{%Fe9lFK-U?hIxjr87旵jR8%1~dpϳjitƼ JNZSKA<7{ubi+D䍂-^1>0X["Lj|q%$/Z'uqj;.0$b/@0_FeA"AI,Q12{F[mQ> ˃qZ)#bFG+y0[if|-kkMZˆ_xyRG-VXv20G>Z6pA"Y`:z߸9A4Y FkyY! Z:a Y%H/rj%.B2L(11/M29-q}X.!=%P)d߂l$Ki4u`&p2hb%I 0?)&ZY}eq~tjL=nr>2\柜N i-ȅ )B>*d!I<.$Kƪ)I|Df3 96F!?^uV\0Kclb]Bfc{X7_QHΡ}&'@!ht$)6~Qɐ,,6?0?3i30fJ pH%͘/gׂx~H(;=,Dn':`l lJI&{IE\dH!>Hb1iaq| L,^@/*my[+im76r+n\I5חb\6Y:vuRL~:JhtV./h8]I?dv_YbQ:1T48 +~G "Tva#qj⚰A),c1~JRQ_cq-</w:~1 +4"䗘{eCJ, ~QGOtBi_sdaJz\CWC  fװ55cu躟*|zI ބіz)!pBLf(/fHJUcH~BP5om Ixpr n [Y]nKFoB-P+VB%1Snƀx+H8d,ӥ쑐#$d` +X[3D:o?{t2`8gv30I6<h/@ -ﮓ[#%:q!6@k@N Ipn#z(>12TgVQys)Xr$[@vZKe%!#, U5M$_b%6 ϓ꯯Id I5Y'p@:Ÿ|N$_65.r==5.Nk)P[+Jpe<9Art i@*|4~@kR!k I>>dq}nXYIF)RʷߖOA32!C2ǝZi>PH3` d߇1 Ͳt$uHsJ LJeb[c>}KNVYS8_dȑQwX=9*HQ4YO2G@j\H0g_x~5pËo|XǨ| &˜7Ĥ1E|{mH o==xޓ)}rQoHz}ȭ 4C! \˜ UK(ʳkuBYe4esP\7b;RơIeʡBˇ{vޱMR54F#qEdpR쓓 +Y#!;.l3dI5M4Gn,,u<%w=#w| +d!+mQt$+@)w})UזJuV>cԡ4ufWXS`u?IGDdhm2G)Qfu dѷCd385$ A$%7Qn-׌_F0daqj}n-Qr⑱JHH&jKWfvߙ8Yh@rH$h t?ȝ>ơ_v a~j%FkEXOP~GOϱE_R2z}襰|!Bo\Kf1cf!2?I5Td.,n>Iܡ^˳W X<8Hw! c1~#_SKB6UWgL䦏X䞜3 T}}.MbLE9}*wIeŔ:'>żd`(m\ۥ6J,Aɫ,7\ZdH> mG W +\o{n%I\;` u:$˛)oOTzDZ ׂA\XLdʱcd>RrB+kKbX;a%{3e%tٵQSN8CxX+ߢGfd5<֋9 +{BZEoz=5+dL*K=Bn# ۔ Z%KۗH),or֨G{S*C2gQlH'z^$:ߒ^w@Fm3SY\vTǀr͵e|GwV5acyV?2FlcIl'Rxʈ9?lVFPuDLCZewri%YLpgQVW 4`D0RjJ=@ ǀ.洮Y\X:{$Gmf$3`@4e~m/u<&vMQZ"L;dVnJ)rQ g:x.B55Y+־ XD`nc +:`z)+qRk+8B y:*r sgyz_cvP?G?1 V -ˆQ%8UL85_O9|)'np3!!"5axsx+^M2Kڟ0MH^GN1$ W~Iח+Uǧ+TQWGCD=:>a`w_:TjuHP6]B-@vČ`9)l9S(!!AB.X鄠ߐeJ2YP3v7791fFB(]宧{ *Ov$WX]{z$!w6n "'5+9T;6ېD7]_>J2l~}D8.j:?pudbF:rq!I^.rJGcU \p"GBR ʰނLݵCÙRo_$5f*6^_ d ~RՍ%2]w@w|{fQZ)[g7KRt Ӟ cw;Wն$!v§8Pg14fO@Hl>"k8``kW`kFpfy&B}ҾhKSm)7W~]33W͆ۑwm7wBBYnGB7`W{)dx-B;/B6桔a)ARyC ?9Wcui H:~Yf8e$l٥WH3ֺB}D + ~ +8w&yZo!8\H{/݅^"!$+!j'"@W$D b!ݘY 9R MBA,Ԏ`wK#t*.{WO.TJ.p֯J~iiXlRF jj؀<8.@)j!O~ 9@=ڏOv_>􀛠ۻS{": l/fALҲ_YD:BpN XDNr.$9|/lMH_wȁX H: QC˓IG`F~ӾFp okDm\=B~r7+l#N nـ<2 7֘FN}v죒sMlzb3j'rCυ~]֝זfߑ!!FjjR8 l=8{rM-4 g7@Mi:`Z?qd`|OOYnzow#SԲ946J%(wP-Y<%BhMeu|C݅ZQt,ɍoS..<t{+mr=R=Bp{_x ˯-\F>@NUg K('M긿 5yuQ@.s{&7)Ԯ|& +'.{xk1Ÿ)jĺ_F/ +E?GXjWWJS>sjk#t~Go-g0 \*wbP3NP=92jęsN=zT{ǩܤ1Ǔ^>8z+Zۃ}Okv3q>z}j'p,>o?1ݼ`:^mࡏ<&B8HC#b9f2/ʝd)synW !!Kf'g*]|k !!2jSU3=/}!!JH} |kɬ(q!SIR}U}ꓞ3X;Mk> fp:g9K>[إ>q>Xۡ=!u4CV#)A7߉^?+ւpbz$HߣDDkǒ/)uػ=z^TcoBB4*yXC犕$#-ÙeH)W]_ ƞ_°a+^|Ԅ1?;JInfuyJ͵Z<0V\#fخQE}uML_^혆( HP0Fͪv& ӽ s#=:&±% +}fS~Sj~rbtx.!u4 =:1ā` J:g_zF`b;.ܞt| -_j3H ǫ\8q{=bVzj>rUi+T;Wk'ڝybks?bHg;0IRv$7W9`āgnRمp,97h>>i"z-8 #dhMb9(rE89a9̀6F?SbdY5yrS0$d>:9;3.2\[~Q` +{Bgn:W| +dyTZx!&iau ɔÇfWN4Ы +[fUg|#rfjO3 +ţqgT~o z&'ߎB:z +V{JVld +gWVp}vpm/d3f5A9 O8g./OY{5M;>2a&.\<8.bT\W[.|V3ƅY]z/6uw։I3,A N^4{bO Ϥ{,DӱGڇOPt7$ FHK G?YCN9ȁh+bUIKS^u+zRQ{m)K +q#8k,t}uW+זb{@/Cs/=S{i/=,<RZ8@$a6ZcO>WI\“_7\4^]:Ol x;N :'1.1ہ}R&galmSNU3Qf?M2Ũp> xa^nBŒYRq Yu|Ylmmsk/^1;> Ƽ΋߱L8Oe}^4pG8:ԏ·q|pDO{XrťEJv4Bd6OFHs#8P̿S}:{TYJ_tF=gQG3DYdBϘ͵3lD*!6F8FX]oi`>y1eGv¹kpl +b*gABhb#бT3 +9рT"ʑKh K\j`2& Y$P5"5(._2guIĘ|s zϧ qi,ʟX<{>!:>rJƷJH6*eiu,f1bi.bC0<)Ϣhz {RBQp6C}𝏷,fm>*ס答VTjzL}w|E824PUEP¥|:? Z!1sTvvne7o\jy"Fhb;U)87G럅3dg뮱#yݗ'5A?L s{*π'F55tHsiM8V3TȷI*H ;YKeۂz|n Ÿ +c5"CGg G,(:G~KWBvOUAzN&0n1^|M|1d"(H_t'u+k2r;xwݘݔz4@aUXlAN'eph:-*l@( #2uyݪVBRH^20NOHi̍drO~'~,Twɝ&-M[_ҝHWr;#Т\Ӻt+zɨ!U~϶_i-lAڐ.Z"?ń{ +3{Sײ|a_y0Dg'P Q7|*=N\[)kr& cB36&[MQ|(ELЀ0WyT^/Ar']NuEzS2W|KT +92NdƈZ[zz-?٦8 ߫㓷4:5 ,OVLh٣ĽAC,w"# Q" A္lc^/l wSD1s}1|1ž}"ybݗNoLՋNC2sXQ&A/QcZw*LuIH] 5).u FQV7.n%iz!#*cH3no"$ш}(܀qҿd}Wf>p9?6pk|@~"Fz&*B,JN܋ !5=D->Ļ/SŠk7my]"qnYa"'XgM^bW\t?Ch3bo7[kzHդ-&r8‚SSm6 'y11 qPXaF=lȬn},_?;[4?$ w =QE~߃\ E_!["Y9xD__ A&ՈÞ;Dq%u?k2V!h uҶ&odG_#ߍX9upCc ]d|9KQ|@Um; EZ{맯EZdȓߴ_yo>سq*/vd?7 yBWم7]5.0,2-rj:#z!d3mT0Qc0'/ttW jA|r~ NT?S ?S_FoSYsiѾAq5cOߏsv{$5DDs=O[݄5fŠM KWn5-q?:;|x4vӮ`au]ŴM}ߚx7`.| +FT\u^XR<[J܀G8ҦݜjrxqAOx&T<*l)j$U0VaUbͫ|ğh}l|+L] ɻ#,,N"LMuJf"~`a`F>R؋Aze-(n;'|rVi{Z ?8H̪rʯrJLA,:s?V4V" xM_x7m^aFgMDObh`!9 y/q'zś4D<7͇ʣD dE lދ!,>Cu_5}=A1x-hv2.Uuq;sFE>S6]Z؄: ^M!:SŊ>ׅ ?u? +vZ=؞;'li v,aA7|?Se{PJf 31aw0(GJ;ǔdz?h tۘU{\φx"+v`!8 L\uE A_Du]{bOsPwsPbrԷ9(UUqhd6V.h Kz3&3&Mq57Ft`T`+1Ï#aojrLM^$IPw;U Μ m;l:'O9U񚾟oXscm1>/ +7]ܗq}錸h!tij)$9fۖ/E#e~VC/cO s+26;:YR|5!mCX)|-jV|5ESdw5ɵѾ!jͦU1sA aiӪ +/Ӷ ފHqu֨0'|c%*m泅'. խQj]&uMl=E>;=/'龟d:R}aQhEKkPAĄfwilWmWfh2o8ٟ kvkp@G^sS8Noj$+ܤ/*\\%jS9Q]_qcu~qAP mtBsCRqu@WmԽ!LTXH v 蟂ke1e-Qоn&熏O YhC^߶iM m j αz jƯ o#5D׺G79E9ʋ`'GiFc>= qiߌ(r9ߚu5%TgD<\b1B"n:O~EGuԅsY bG^EY轉 z|m> bĸ>m9qktF]@lZ=ɽ2Oou`MǭHظ7:|| +T{޼xRwɛJȂRo=$߻I˽c`-@տ.@UR\}VUЉ(λ% 7.c||~6ز+a%mQ!ɂo5Ao(MJc鿤UCh0${+^c`!5{*ع(ض_ʘӳyآBk{\#܈;*?ZZV- +ssRȰ`]ĉ<)̋êRJ+=s`S#Ey&ܣ@!d|5X;iX@,jK +dŞrJGqr ,̒[.'˧n-[>-PYP}ذ-?ٚK{ǖwx_Q\%23&Wz>0ѳ>4!ofL=%qc#*oĘ<`\!FGǴ^lFI~v:zz؆`fi`:<Zn!X8` ˩\y9S5~bVLB_5{* Ox :|C'^oV-S%~1*'i]s +(%/K$J"J*]"Rk<|:;A۰=W~x?%7F)OQU]J``@켬O"='mXT0ȃISK>e_?Ōk2Il&|-Y\ghuD2 JR'))%-^u;*3U=[<87s+%ܢW;Eܬ?a|} cp}XL[ mtؼd3ѹMg%˴ x3@,f)2l0Yv6i!X5!q6F敻I>Q{2tR5]tdFhD[X=s!lT'3Dg6-7Ce Ӻm |p.| P^yh>A{-^Fʉ$:4GǾ} sYhh 78~SƌOݣ*\ck#*gmOSQ }UR2~6#٢)ڵF`p̣a q\?:wbgEż.)}#*OQR\{#:].{nIќ/e~M Tؾ,4,we_S'0s|7KjO.fCOr<"+teDzض-5eW|s'G>ϩt9{kC\!,Cj1Z PJؔK$xk[ W~{23aM9Q :?: N=׺Go V].ԧHd\[jTSgBe{L 2RJc}F?+N(֟EE`,fnW~i_c:>20& l-*`0QhmmvF+}b^/H)LpKzY!-,qJ)FFó.8 +wILrA^5vr9˘t ',[,}2W `Τ`:0oVxI(x'ZLF]a"ń(ڰ@g:nIz}e + #נ6%ΒΎ+`mJxXO ?|%x,PngK(% +`}````>z?}X4"U~%8`%Cof'4CY+K;}n{{5V>YP]횈.Zz7o?ڇ4^I9pQSv%I”@a6`nxX~Zn`7,*{8phgڂ?U?qEҚwimGBmMZZ./w*Mk4S?2s8*ʯK8-S / ,?m+ֱu\d=(. HM? v隥!{*8[JoF=-q,:[T{b.QͥnQeQ!(:nS0yәldX(,,,[| l&kZ4X,[1x ,TK0Ā#ͳ4hmQʢ=mmjP‹q*=`+sUƎ9ǎ59JǛ\PNg?3tsm_㏘ޏfoEe36%3C߼, οC`#@QXpiͦ`:!Xl{- S)7U>ǏdkoicGl+j稞jx4z]z\SzZWZ/\>gmh+`ųK|9X!yUU`69P,m7Xd?OUhF`nSb-X6-T<؂']ePET\/I./ WlJ0UWDTֺH{ꜥEn1#5ӇYo {)k`B{\?#.6Cn7eUk1niX6UW +k`#̉;Mn랣OK]/V$yEx|3Or;"hT3I/KK`ZĬC +>KoXdǿ'1acٌB? +ZB6s"ؾ5`u@a1'hmf`vkfXo md68^F}&gp-_r,[z PTײMX$جR=k +뜮^y$Rz }t4y.""u'-;hn7sKZ/JSݞV}LF⬵m~ĶiLvt&Bz)iќ`vY`X(Xb|ASXS6^ۍvB +x!;'R1ZYV3Y#ijxxpˁG6YԷ7InJvd' WD7G\ _3J m̴gE`r+A\  |Tѱ, ,Y)IGFCR~AoFSzӑ,Q( wȽf+OZHohU?sefOB.KP=jrk>Ek{r*J:J8s[ȿ;9XeƊS󶃵?mf*d:N~ni{Oq٤.L9s_}.S;MQ򮛥ަLQGS \Con[iA-0 }cl$YP`V"@P n/cy?X̷Yc̘!PËUU@q3lR;c`+v=n}$^O=O>yOf(C6w䏺W>ץeSz7OtM9'e^C4_'jATfvhf<6j?xPri,x͌zɌ_0"?m<),w2*Ur CL?u w9`Ϧ`݁3`U\ۏ?`f-u?e^z:еjijZhc պ^SuׯOhJ'׭zɃ[{ i6&8umL5zX|-H4;p*i)j۫!4Zw]pzĚv)6{y۩0c=Xl?ذֶ0opw$N:Q>OI;ac9EW9t8NF?o mI~6MuDNӶlPBqkhK?,>1=wJ/%M4b<j0'ii]N~aNḭT<6n<8`VlZAO4n+miF :h%zi;}js$-RZ)qo LDk(z{ujh-Vmp~m >0|LUsiiLjow o- w+\Gd/73?i^p3lf?ւ6`s9}|$IHpbF۰m>Vm4ӘQ T҆Vڂ7N;gX54eaKzVRr~"FݹhN} l՜}x'ōڅڊ%O7 +˅2'@3*iB;@ ;H7 V5M:``5v^ܟtF#\^O>JQ&u#EQxnx8o⍎\3*Y,(EֹX/Of6.\gPF}.g02wľc؝<UXoi#O/zI1/7]>VkrANߘ09)+䝯Fw&{na޹3\yXd/tR̕qmʨD-U}EoUoJy?zZ_GkX7U:H. gfǗ\AbvcYcZD(۸I^)OHIdҪe>WgB~N7U_-4Y;Es?)ui֗'8M |vmjJV.@G 9LeO;δHȏ:J>fd,J뗁#زhhm)y?ō)ޅ_ +3j}ea0dA-2n"uP]x"/ :.`i`oq%VDkԪUoV?t<]IGNdɫΙuMg +:Y_^l䋓#Z; i}ܼ#GTKeOG=?BKK>1Ϛº:g_8_~>selsY=-sو_K_̳p̵睼 {d>=oRb'9ѕ;9O7&%s/\&tݩ"al^2Xj˨4)j۴ǴZ^/EM784{q` +v潬a%n-cb.Vpp80KG]h98z0^q@(2d ϸeؒ]\innB^<^»}Xƈ:_*oƯ!=Wڢ獧L<,jTU/sLh}Sݍz^ n^rWna ,(LC7dU0M?xcqѤ]ZK;Y?)+D*@k` ifÎZg1WrLZ .Lm"oowYd6j%)d!ڃA߄MGyE9p1PQ`|OUg2n|iQg:^B}k~r~`<;OcA+9sUX@[xB0m+>t_oO_VS|d ]O[#}_8={xqw4¯%)bg mXθ'n'V~v!Χ5r2~;^bZI{6jOІzA9o7)kZ jsn,ljC?j=-'f"&A"K Saoq[k_FK"ys&|K:J^M-!ܣ/=c^5 ]]u /fv &>H9}Ds[tMU}^wC9,ՐDpOh(=rY5"QA91G%!&4=Kiqzg?"-j`\fXs^ܨ6fcҺS^Y"`$"DZ/G}V*[?hd5i,rfgz7ނ<×=TvY^ A~IA'؅(pCETy!.~fݵgR#e9/o;e/;ﻺ/q;Ye9dTs2e\q?HGf^rV`z/Í{N +k ya|+ ѝwIשh/bx»I2ă6a 07bsB_*Vx&CvVF_(d ؖ2銞2}!v!^>XAksϳV@ˀ&sO]llq~fue2W|y&8Jv)`6a6cf-btfɫ(s6+ c7n94ޢDF\CJFI돚lᛞ)>=a"-K>s˨{x)ua. ?7~/7.;sowE: +9HwKh9%&o0s=8w3q,zyۘxCcK7#JW +i,TX8'ݧBK7 +v"NBqV1J Ѿ"{\ۄq]Ix EU\3TsHT#Xx֐&~0hD<}#W89c*he=11QNm/>sx眧x 5s7b~7$;9iCJߔFf`Ϧ`ۂ@CC `~gL=#h-"# /h6b-"[b{0+YF?.~*COTEn}a$_'@KeܤK'(l aAqP=–qF{, N9`:m,柗YʰͯLlCgsN9M>رr#8O h+@H#7l%k bk!5ġCBO Y`0"#q`%"Q$W:&Zx؋m8EOgH2Md.+PzxY=XϽ 6"ح؍u_c'd9W'af#>϶^3SkzUq?1tI^Flb#B_W$.mܦcg"v$}/96^r!?Qa%M$~]w7l̋C yMD׈w/\$E<̯*15@Tf}VC,0 hM%e8-=kRcSIjGah`Wh!˳Lq:IHaX g[q]:M@Yʊ;L{%#bc._Z,ҤBfu}T͈' +6q7kDst8AdqTHXXOCB^h͵GRǎG< kGkyWbTU{Qt< _aክdO$OAT.$#XZQ\q;v|l;.܌e]yim HhE'I% S\*5v Ò?8 ?5?̼څÎ;MAy7h>bTqO8NFo/|iYqMg 5i/ܜcXƐ9K>PR7"6%啾 +"J3S&'q,!*OwxbaF=Qv3J&nkY:#Y QU n~ߛv +.ыK1@{q{.K4qp2$R]S +b\TK iķaY=F+)skE)tuM\ą!̏}"&:~gT1fXl4E4 3<}6ូq$du5ds]<.t>q9lL,`tCPYY!|ߊb=*Ay&da#-?Ő>EL1DyA ^qP*5sQhaL^O^XOnĞA 8q],gBu0g +`4H1ePM4HFC;2͈#B>A1y'NN=rV¹BdĶǴ1`v|#8@YH 본v0$OXC銛]X2$XǰT| ܟV&,.FSVn3fW+6]GǚzAKZmE=U7z^9*>h$Ѿ|΃5/7m(/&)lp@3 ,$}pSwz&qE ; ĊB%O:-OMAq[Vl4t'!&}۟uaOœ2N.LXh%r=:gQ]5?; 7f!F v9x.+3Hot1︽!< + N2 XG `qPs "7&ī1L#ZĺCE 3.h"/``j=(q/x䙞LmDe+y uI,1 }#a%CZ_b"]a0\K晱)Yᙺ7?/`ibxtXqih?- sdr<.uQH^veM$o5)*T] .6ىwx/]z^aCF݃(On/shǕ05acrBV;.B5BF8 T6+\.]u09$ &7O#~)bqYi 8o<O(]$o;# 8qں"S) tH:3 q1Jh_ a~z1;8\!=.mc0&m^UpaЃm˅4ȓ&#N)}tg7o:Sx\׍5 +%2^ff,CLAn@A ǕDsi`/n*bW#63xVehl"CM`&3ZB)90? X1>Lޅ/rhúґVE\9h'^Iu:-aF[ܨEJFvɇf9[tLRE KkW`?E,@2ĶCX'-Ĝ&uod/C禫 >3Sӌge1k, +<}ж@Sq9efgp{ yX|sYFO>SAGA1ubf|發%jČxQ(!16`624Aف우$`dL!2Bq(EL[rOC䝽1N\"dTj#&dAZF$ /GesH[# +a6-/ud|襈#O]ͬwz%}Jח;"; !q9p.⤓76!߀>+o\어Z`9&2G #X܈7[g77bG.+b2 +A/ebirr1afXr *R^ޢAmf!?v^ß5iW j|Fq5䵴$Ҍv!p1VABQ`Ny['2IYҠC+t UdX("EHK; yكq%TH^vʣpEm([SD$] WD TK +ăE܀q-LjFlcRver il\`9`q[lۺ[WU?XiZZc&C,0F5eJ2w# Ggc0/ ĦyGq\ {yT g5;PELCd[\,C/Lh' + +GYf2H +i5L0uR|ĸ= +a% mGut 1R4;//5apF?'Jj5I3B]yFzGX:gQ{]e'v"|m$*o3響ݦ"zVmmꕝy uVmsuҦ`.mJFv0Uܩ1Fm iͣ"k Q>-LYΉ::rEqMn-{GFKZ0-H3/k#1 1(g*^AR&RiڈH$+Øp;Χh +:-,J޸ 3ZXd-̺!yXۅg +opIu̇y9뢴st9b4ֲ\%1l'#]$FA[^,vs%xb 2: ,طkg ,ٺz,GX#{igg|fp`l8e)K\GeR m>c̚%9S>bz37قq&Ezu&NaK77#R퟽ _ 3Y(GFײ'3?)1Y{訲n{<8IЂ[BWbĈ@*!$5D]Z?y{}~cQ$T9{Z{9sN?0؏F{hTCQ{p?vVꃜ/T23SNݠ8$5Yv}poUH7Bֹ]D(sd_TP JK)B=\R0d\~Ծ/I<y!UcY f݉O*o0)m1ySu˟ϼ;,'ÕWދGb44ŐBrGIȝ{qC㡛eؤe֒CG-zx>w|PWSd,,Tuk@;X /#fi0McWCEK{-sS#lT;˒̟YߢFhj AI-9޽xa_c?Y>T[bhP~z!"Lէ g +)QͶ̎{Z)9! 7ZV(8]aƊV=NuAH̤1vVEJzR>kg! +ޙCa|P0A; zS;+J"kOpΧ'.8ŭiCsoYz*$s#;xۀ^T{7C_ߏ<^mD**~4!8M{Z '1;ެ^Gڱƣ'5!q$Wh%B@y+x)oml)8-䎨gX2hbsgYKzf62㍌lQ9HA)oj"Ղ;SU.|C;{%9+mhgN@;8@><.y }YvփF#oѓs . 5Z;YIO\ɭ*« QW]u`*psS}IJqLe +OpGDhΏ\#t9 jX +U\Y玲 +&y}rQ*]fJp'7/YT}$EYJBc(ks;ƀjS'S, ΊhgIrӋrQ$ nv|x{CZ`o<_C[#yBi]*`N Ds\;A$Rp Gɜ{A[.̻2H^=֎:_$j4OpٸAY(INn 11S,Ua#" E  +;B +FqiAFk2 r6Kq(1~ +|)$q?I >2~+EM +~GFtW>jg}Y{g YYmpZ3RL=*rն(oM񠼑RW@ti(= +6yH>0`]?Mk?)..>&ei,ė]_zXjdSsBT:b,5~ 1&{H_K19g* o-vCf$_f$4P7 QRhhhw!@sZg9~ڗ F5`- c=joc(Nf n.EMDoo8h "~1`44iKՅT/( jJ)é3P>mSJ{A7\'Oz\ơi&[ռCn˰~E5js_ysO bsϧy[dh>\qyYrrYb}h 8IGXvVL60 +Qu1zx߭@sAPOd.,&6=O1 YAݓ Sdi$jY&kQ*=5R7,;)g}},mpv8vvuUm9q%ɇVв1d,5{P={8ǰBܾI4g.TѷT9k3SS b嬓\nEB%cz㺘k Q d/KE&/]7{8{H +ŽlQT5;}t[vGObBdi[(fM? 杞ѱJQt}!wL{]cјw<3┐2&DBEٸ?YG>o)iCjB}^\LAni M;%-',8e{ἱ\ @%֥?1ي)}ȰٗP_̊K} K콄и+Sڴ^X}]{2P NkUJ?*&1= 7L+.1Tg~bg*jXfPM,pnRBEqr9ñ'u +Cǒ}'hٻC9GT?Z[І *<#lOrρsrSvk)!W%v  eyOҖ%Xqp}Xb S/!t jw +Gp-3tZ~N0!sLo\5]D={Fi0r7G(N_7Aa+ n04ؠLg#x?yJ(Fkecs[?qꋋN؆4圵I,1P__OkB=}9n7Pf(TQtu}ɺj&R}8!;OBZr5ć$9W@oJn 壪*M|$gyM,d +p4r仸IXor[Pۆ4gBT7o +"\=-س%nW8 ~ȝ῁#nwZ~HF<^OrL_1jv>йE60i(j>\ \q?)qT +Z9&{E'EAgr82gozb0<kAMy`>:Əm쥍#-q$hQ2%C:mz2LL~W&B1(F J +/ƥԃ3&8f.bӤʌSӕǧ~ÀaKOTcC:;$?ԄԘ2R8D!q`*t@ϛc_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9_/Ǘr|9>&N\n6=vZ-ln3ћh5{׶Nܷۜx,%O͙5ol {W)Wz99:m!OZڸO5wV,5jLYa+6[K'-Z4kŋ /\8gy ק͟`xz\?omc-12Հe܏p?Vzyolf\͓+ɵ(lq3 M՛c}`ѻo7q"=?Rs-Zn~#[X[i1xM6Z&ZƦZx2C#Af!߬ei rrv>fe?mXL.Z&*OmZCVzlZ3̤[umWG(hכwS%fE>;yV~>%*-` %&0vhDC4g$ +Nq2QHSEq@.ƹA#(a-Z&-D.#Ĥ]Sh6[o]Fk(x3;hxJLəBQr_1qm01EGXJR0npd<=ASDxWhBc݈ +9J+%g G3Q 65w222e DJ*7 +2c5k)(m=%?s24΂qp+ RrS*gzXnMlnchV3qƿ'%m -c0_κkC|\Mgy݆U  dKV>=ɣSt'Yf +AYH/'KA[&~-ETAMIJ4 ùQ:"kE%B֩Rh(#mf JUF 4d caV٣Tz_?/y( `lX7J+'v.%t zG*$4mQ>(=,x9Br[yX&ĈՈP2{_2m*dq~ eB{qrA39E 59Ft @st|фi&XނsL?c)?jCp'eRnD>hUsh^.86 $d|R2x/e-i*)IOp:N|hD9lr_AA|/aSSEJRQ3I +Fx4$L|&`Wᖌ 7z0ric5fjơi6z F2T?X +׃ߢ_4KwLbvN\ck&KӸX76TJFDHY;~<ţDG hVDKh(>0k8%61kh_*CIKƠ !xGa-*HpJK#π}xutE/`WkMe-d +eU29OO|KW.9hPBG^fޭL2sN8a/>1.1Eܣ9 dq}1ԡ RaV>1 KOS΀oL/|uХ id[N[{V L.lMg  %XIa uД)E[B/&v@ ! GdU&%4 +)ff h84pP1 FG3-뉦B;4j&tZ(Kh=b6Txk#0W-޻E"ׂBaAo3%< A<-Y#X'C8փQ S_Д LLxP"ןGIԭ X@alR-.d!JhgM#69i\rIدw>-@kS'YkO]d!'ׂuJJGq֏6ѧ>ҏP GS{ =~W, +FF0&8,}J63HIJmGEl C!yhs9HQߊQhL db^@ 7 (QE78OJmb Mgm) |h +AjK|Ϥ͢hZJl  Err/} -G>v =-xH=4NR"6:AmQz +ތ-} Y@$cSGOI㲵 %Vm!W.~}4ͭNϦst)'g1gfSchc{.x$xb[G.-m/gwzbj(#5&MbPWwX: wx) 6E%#m*%DkxF@A{B%!X> +4RϣD08O1>Xg m&#!Ec>b_Y ]ĘGf63ߔ!\PPu̅$ 6|4%@3n 1;ǁ8hV8xK*˥j֋cho=h +HOŘGT3h3}8m~[3N},! _XO3nxsA˂rS|% +7ܴ-H%5&sm±i  @dwIۗc}P>~D6tq!#!X?gVʐQya;D,I3ѳz#Pk҇d*{DHؖ9"79ᖰ~|`0JRQ޹N}hL$("S3@`> >$vV$'ĭOD: 1 4"8K O 0ĖJH>mR'!!(ɧ+\R,Bb'yJ(2aC{0_0o)=qa2mdoas0w@ s <$3!8 #9rF;%c@@}h( ++Oz2?q!TXΉ$&1.dTx 0'5uD1pєo&'Z $BcUۂs$y6 `Dgg #$ r[+y %^%qp!A֤ +' A5=P`DSZ{0C.l1e촑!}[y耜[C4X7<6$1)QlܮG]x [ >A==5p|'5D7[0 y& PnEq0wWP\R6h:!@fdD\mRK悈|_I\ذHq["`fBdrAs  )f$A2\AfsHoɫINcfi%3m(a&yLl>'H1^!xtJr=#>uKN=oѨF %I r f#c[Kž?6*s.`18SKh 혌3mvA +{4H-jO A&z/ѓ&!t.,6"C ؅\œ#$ξ7$QA=H*6i9Ȫ endstream endobj 47 0 obj <>stream +@yPl{_AMIi njsN}xA]ho&+CVjvi38{_CT<Ѝ`8Aze[@JPSbŞMo3Yaf6 ýOP|hZG )qTZO@v HyQK!x XBm\&110HW'iB}!$O@^c<%^dKU@D bx38&F}s*ܓ|h*i" &V~,BRMg*.gcjA\JF'(*.0['}ҙ]_#;>N /MuC-;M22O97D.'Mkf[|*vD:JKGCJr07q/@L1? ԓ=i'f +{|d8G|y|(xd/,0jS\R,,Ĝ/T5.hs$ 0Oi5BeO.]@ 񍹂ʦ +cI"NO&~8(s.~;TH%`b5FB(i]w1u 2j _piGh͕%(kAQf5!x-왤*3<6m(bly2` 9*tYn@IJ+0PL;k4\ɔD.L.@ +'mW1㈘d +]-hHzwډPFp8]"TsDD~Ħ P]*vPnE2.4/A%4GrQ`ss@3 ,=A +[~'m(]CnHLRL/ f JihMr#`+9K})|'?0UP;t-d `si=-<Lr%s`]Q1lÍ4~AB2jSXqyPI} \؅Cpo aR̞.\j$'B3nI5 "kLc!ƂׂؙK>>MQزX"1W5nױf%ŒM|&jd6t &|Me,m)1.C $OJNuOԣ|Oa5Om}+zA,eȜ(%IسqJ/ 'B<ȁSM?)fQarϘ;+j:W(J.8 K3Mmbr[*0oڕ$3(PgQ*%|hmkMWsO/԰ߠ@! ֕'ij]R|$J9\Cӛ2 ~Nإ7[SZ Bb=T/uΣP1Q)vQٲJ *YBڦCmuȠֈ]G*ݨI؛I c;RA2P7fjwIG ||ɾ8*w<_yyGa W~(g!u=<ȩ!2/+- kE.ܬmjf*3h&#y FLaF?AVK'Cu3aS\$f^A>*.Z rBlL`r }5]l sj!ľ!vaDVZxX2 AM. F#[Qi}r_o7]װݢK:x^ $@=ч!-rN..@"0'j9=Ϳ8S&< jH]buTJ@L?`;1VLU\e*r/jLΕyX os33J9<blItcxBѵ†y|} XGC<}'TgIH{;y&t&t5D2k=Yk-sbAX|ƓZ֋L 2 +\(%1♭Ƀ_BU|t? i( +,dsO΅؅P@k`ϑ*[)zzt6th0MZ{G'a0|0@-v ]O:8j+Rq֏@80M>5{*IסSqJ.}Tvw1NuF6 nLUY-մ;*/G)! "@.($)6b:S=V>}rk IgQ4J]2]h. hAI>Yi@HJɒ﹊\UTv  ا/}~~L, ):gP6V|>Hߋ U똬sq`;tMAXKEZ!ɑ}JSݥ-v^\`2bG&%5mec@Tu0=7sI{yj>zHzPvdZWGDUp=Kƃ-9 jQc93bO?SbluU[_cP.Clj;L;< L5e*ږqW  +uX0sFFǺ5ؿ](IJׅbBb)ݢ*\k c +?] N`?^LRlD`U&NbKo/˚ +ͫc> 0}V@0C"%O=Z}WP{c`jigf!_.5블ݓ8e̞qV S G{a޺X3I,+Q866ɹz4Gy +0;3]eL=:b +tWTXjث FW}DВKʸmEȐxÇ g#kڡV+ELjɻa/ qJ`fCk,{DCQuǒ\vĘ_{&=g Ar] /M*3͂U߲هf)[~RY$ Ǟ +Q_@EBF@m>|pBEnq,#S͔#3 t.%Fa8.b$쟨r5_Ƙ=20pYBD3̅@ &y _:*rJA#{ օOXXĚH>ƘqMV-r6:3z%GT3u|ƭ|HhݣzCqؖ~"^9[QKM7iGSQH'㟈V$"H1\!L-֬VOE1P%:t8Zš)t-5, /~0Z3l4*z:ֹbS5S #sic!p?5TTw,8%>us&8Gi>YzHnۅAm[Iy*ɹA>OιT; Tlkkjl̎\vBGQzw va.Kzȷ)M;kTx6=_|^_ XEMq eKmiNt$jX|L˾3#Q+"s`"] +ɦ}#t/{&ML9~w9ݣ{QPG[9kۺ@m]5cŖо +ǀH Q9ͺ<)sj$׶$>{s˂3!Z})\{7 +13 ŨCķ®O*b6˽bVఀANq$^RQ :?"c~I)>`V 䞚GPpSO5A a5ʱ?xSxn!]r7 ffX~eET0ee14֑XGbB@BH_l}_-^I1W"Q,kLڗem?P81EʲIZ?uT/@[91GL:<>>@!LYڸIGE,PrޢKiTV\fY=l_06l${\`ր"}w`eHH&rXab7&]#?O߬?lb嘝uW+k߭Ow8Jxڷ'lu\'܂.A{1K2O'gpT;(mMՍ + 2*1LZJ1JeٽP2Ǿ e jDsNWFl5lX7BLUw.c vZ񓢰֥?O!`  ؙo+g"}z6{JeuJcPa׶m8Gop楰صrav2duZ@=TaY'yɣײg^Z 7n=st:G> Zq:G +h5w9̺3Wus;1ǞsҙV=[`iW}A^hr%V㝛]"sj̖G~5DqgQ~_֊s|#o9M7HUNcsyb+)/%ZMoYsWYlr1qMZ]:!zi}=GJiC4^p/ !%̨o-9 dgZ(lb(T?T\eY +y%Ѣ̮u+,:]O|{?MQ={az҃XrEү/[Uڼ]@ͱn.\9qS<|OeR~sKձܾ +eѽ%fu?dqOnj=MPlsB|`?cODX wc)jN49¾UXf sf3ccF^e}o>k'J%ײ'fX}boF ?S^dYRU]Pڍn_|'H<*'xyC ;ȋ[ˏa-p44dIǶd7 71⻆ Gjƴ✻񅛟W{#"뗷⫮4IG +s/܅W*-η{X_:_|ydO]ņ`RpVFst%ԺJp[pC~C]x\zIɼ8[wr#<'ms:Cu^:f:bkuņ=XN=U)μ ]•6w-GN'+`I..XFee/gcNMbvt63'EVdܞe2K~7V%ONXnڢ7'=oIeo=s_heyf3/Gl#oyo"0&vO ͏miͽfu2Fpv-j*,m.NmM,n(YYHKStTGv/mm,q~+{lv=G8nt|:yc>+[҈]] +r!W" ܬ0+yVsoVd\i=[qBRgKT҄qVr}b崙O+h,V%uJ`ִ߽B +7lzu+|U0{OĚ3vӍ‘V˺RvON9?>RQ=]]w/%/aFnzs\nmc}3nf_m +ʾ|?}Npαچȼ"B3w)/NTX~d6;?w>gÿ1|3uG:+{4Fz{?Kؘż{$YNf5<-g*+ ۖLQu{~6_z}#/;WЕ[y/&PKmYd]u%8(wU͑FW4F5>^0mͲLٽ?N$~a+^s+ںF͓[vOyU7xⱽtGJb_lM)UU6nˏ\{nhYp|/kS=ʻѾr7/\`VLG;JGޏ.誫=tzv$#4aJQޖ<6$tn3 S59?ٕ'|JtĩWw {[ĮS "Zo61_E_궴hvLSݤqo<#U^ՔXLM}ncK=TeJѾG @Y'<.41"Pkp6쪶Ǘ 4{d?,~U1Жܢ’;y7b +<Vy=,pW^]˰}yF-jϔI)ˈ?mGCWG]{g}Ӄ^˳{u$QoQo-۾96bޑyz/ȇ;\7mdRBScMʇ3kAyEYE^S{nEݒћnm7kn/v{sr*~%̸[ܘ1H:ډ:$9w* WטPV{7(Jzٜ?l< 4GJ2j[rJ^?q=Ns@q)|ma{}; +otej Rm 舫~&ޜxh|v/7 njm?ZV,IhJټE+ds(hl"S2E֚SԵ*y֐BBK1y9W.'W_Qg]I-'*ES.*jH*WvT. +c,br Ŀi>?mƻ>.4WZe$Ȧ6/+ӗ }EijlJAA ^Y#4xk(Q2=} d#{F$3:K6nԩd sD7Qmݦw1k}!540˜ GxR[8&̖9^f?\p/y $?  Mj \urW{/[,z{.Sx"o-t_>Iuz0%z\^kjj柼t;$gǭןFol^|әҵrL+9ףoFsFhvLSzͫSYw3a'..+{{rX{FqS\ӿwο4zekMϿ'9C|Gi!;|?ZzZƮ/{\lykvO3y׽{*N9ݶlS׮|֢"/,KlcXt-!xbLtKO^kVnHV +{usfo]-p{R`2B_FOw~g&ƍ3Z')ey)(RL+.G^|fx+.D\y3IٔUѐTޘ_DPކ|_^^&nڳg-Hm=jsP;~ KI~ߋ=<Ԣ_?Ud#eu'ȾZ6{"j*~~jDڮ鷒]j{U!$+/| l+pzKwݎm$agݏ,}sTM/טQ#o}~~׫wIL_rxTt}4O6ml~ݵ{g_vwj^Hн*rT>Qq-vxiЂ# +}柺_z#.}4w$7έ/;pM}>iO7CȾ,M]+/iX=n} -8)һW*nYtjD^XuhcnQMy$ v/g6tM<:4P_T#k #Oi(I_?V6Twl` :e̐_/,Bt%Wgsܢ&1!/1@Аj|kGWV0_L֫Bԝ~{Iߙo{꧓7d#u?J9^`k{l-l2! eȆ-ol;WߟNl<;bzջ>vrhߟֲ# йؼR]{﨨mwAŀ#`"HUT\ "9%JVd1kZ6cMO>[s}{}7{{T@\3S6b/ Z[%-zm7o@v[l7O?y + 24G"?ؗLA4ɘyD/F֢v8,B>€o~R ߞ/<[gU]W/t޺Tymov^Tŝ_--+_\,p>?&]XGt~<Gsih'Kde,V#V,lb[z S},5/ \.ȿ:]r-_lj?q f[:{tb…-P3lUݙ#됲VTͰXorMkA&ez6"siO9ئ T T4QMž?Af"Pd3? +M]*B[T:du@ݱiOT.8W|y"sN9W1>ZNo^vs=u?FYkoߦ˲_l4KFsaWo(ۻE_)6*k4?C|.8Пl&6><\w+ Lt>3<׬,V"I &BֳX4mq (A~`1ť.jz|B5yo.{V:^Yqe󙆶*\*z|_D|үa/KV{Pvx`z^Md%uIz2d6nxƢ(43 p G3fKд)51o "4u.)h~`=v"ui,nZITkܿR~PEިbVD?hxk7GZ8΄g#B^u֭Ʒw{U1s&c@bxdNWA_jY +Gsddjh,!58?N>U{TS||r լ}pn{cS-}cW6r=5UUӕзnNykRϯ޼z~;Ev?=w/n>c:rOȌX"+h\?dl4+?i_}Y{qu%.<[Kk/~eY81p;w{Zf֥ṙn,*/*JL%H?23wB Ew;D!P}&0GD&2lg_R 8̦x s;1喌ӕh]ȚВ=  WלsW ߊGFb5j])~֪⛆_vlj1O +!J߷*3;q;bƷY=+?d O*}P#";^Q4L5!0ӌNŹb4F Aqhg6Tyih4ׁAy ;;O4oujSOqʹ6<}ިB~-r_ǫU_~ɢ~-<߾nĢ*VrGJ=y?}u$g[_":,uze{i,Ӱսi6 T x4ћ,-KbxH#'&tjj T ϟ7P-YVU=U~TI~U)VEp?.x +hߪՠI+rqў wűvE}3`_{O/~pŃ?,Lћ>{dsG4^܍1/ U{bMJLzK[GUUy~CRXCŶyVu\}K)z +V%Sϓr髍U.K#_O}|¶[$n%Oh6ٲ'{ Ŗ*63oYLokAe898;4}{W62G2_ĿQ%{}CxKE_W©ܠoTq[*>[^\~Z4#ٷKWeI^bwL\}FRfGo"<2 ٱrŭ+y/ ' {?Z8r_,=BS|{^tS-Z祋8 +4~526h? 9tx[~W'Y*GϩtF48E/B2N$~q5d@K2VPSW$Fkt?R qm4ˑB˕uj.[zZ5j_UJ귯_{\L+߽(g`.I!Ai"{9k|'ttO/qC胦:.,wdRR]|͕ÏX7Cn^P~ R.~)YKwX*7/]j=`,F縣y~Ihe1k?QNrWU*$?# |CIBOꇗX ǩtK'!O7GDw^p{=5/zqZ;?֟쉎-aóAKVC5Yv{s^ɮ ۊn8fm<dTՠ5]ۖR{tZBu};^s:!Om xޟS{glTêsV_UU]S3*?ͧlÓ45|}Ӝ)ͥ郆&pD/" KX2ۧ=Zs.T:{auY[,( gnDž8]ɜFNU&m4f*L|{A*^eJ;{i-TvK* ȋ|NVz4Hڥqtg.}#UOrWX+=Х逻8R*[Ւ͏jY#B^b +TdjH{*Gw@^.>eZvE#${z"וn#IE )5BSr CJ̆ Ei8#C]';\:;UՌ)1ޅjj"Mӧv<;%.9̡޴w]mSoEU?r=hkn.m,pg8"?8#_!a!*b"sr8Ä6h2 j.˜ ;j2S8z@KOQmZt>OȄ#sVTzCg- |A%׍mqh +CɞwůCd_tz6+{kA:r%G/зLYL~)]9Q;A\}p,QNe''15*}dC@4m Ɠy% +bF+b5'yxYd/ʄJCR +3W.?BI7OmI߹0K,Sʎ +z`vבn-fMv }]_<t׵AZ-=0MRZ=JԃqX7ۺS}l&VkvOW}u9~8)%`\s=#'tC} ]l􎧮swgOa{F&i3qE{yОw.ԑwbk%ksMh;o̩w2޿z|8$Y]?(N=Z|rzJ?U/OFy\)$&G[>G;( +zc#gvk/5d3댄Fsv葻ը+Jb}rf*m#Bl'̖"rmAPnd$q[ijVܑ7 +DA$Ra=5E LмVh? +.*k<zwWW-$jJ(qXME,ԊKj +KK:Hx +8 +lJ8.0ͧ{඿&sqTzͥA'/)[סŗ̉,t.ki\Ud@>x,8QLT}G:d ?.·vW}hWC836mG6KZ?wc~Jփmx~4g+\jY~N)adqTb[{x5ޕ t3麃3OfPmWJuu4+0-ٛNC k"@67Eޞ"+FD *@6h-DŽ{Huz~?l-34b=vtnϳ@;2]zؖ/Xއ#pރ1] +%ML[鶫7A©Wԟoq x,;E%}5qX&h(Y-<Xꆼc)Emx=vޫ[:tW((H",j^}jτ>R&mlY5l{O3{.˜Ћ2IO^A$ߴ~ +M^ȴVT.jwo鍇mA/CL%!ٚtl^p쟓@SO:{Ȭ+YrG}/+&)ujC&H ڑ\~[j'҃=w z+q V!}P(9]q} c?Лk/%vKS[SCV_\# &hQ)Z!GΛK$IA!Yn+yNh +ol(eTNE2DIՕDhhDdh1Y|yʌ3ytlQ Nn'` +:̙]'.ܮ=Afl){D덫dߏn̉g븽E]O\ö\QDjһ|[9uw\w)WO(fZO-Og|!Ӓi2+Lln18uXM\f\lÂhIfN`su2=ShDGyxFt]6I_slP|&L?P #g:T/쵄"t;/zn{*~す:+wX!cTÚڋ~zAq'^*O_ҝ\͟9TKo?PFem1ax +r􏃞?hGJxk9&6@ ZŴ]YBGpħ֌7MmJlh +f|f)\ci\Xip$KY\_pWymf|fS6݌.Qf#6јMk2J +`7p@ \V)C\W.M&\t^u l&ǁ KScjЌ!T׎'bО6$f{z/0g@ÌLJ>_m y ۵\~[Zj2`q;P(QOPwj}6[8)=m~Yp  OʶMqblR4*]ˏG\֠=Lh&hpD?{-RGf +C3sVJYtE)k/އt'cN =|=zC(XGKnl }u z@D~>}c>OvQ:*Yc]hDfr?TM?Q8 ]2=Њn hsYM1V7XtDw[z/a$ə8d l)[>[ȥ7# *BZ H5&bYc_Qc,dvD*:C+ @GLk؞ck4h 848Q:Nȩ6]F8/ࠂPw:chhFכ!h}PW_`zv ]d}*$F62Զ;˹"Yz}#EetZ0!-'gA!ՉF5Qb’_3Xρ:XU9ͦѹc|,)A+Ŝ1k@7Л·_D46bN`<>?y}wDTyÄ+/OjxqeCB/[fgŦԏ*& j*\e?ry @L-"I׮bKRCb*T 4AKA OՒ2L-!? | 61ז*={B'vayf~‰̩AU#3H>YnDz]X; }Dj{HE)yׄcz.98M$Jl_ % + RR\*h6'8’^U*.&QtNBo87,ԗlnk0=לf(fmL'2{eqL0M@k TLe[LJ=*"M X|z alaj,m*a7TJBp~h#<#7Si'tu hKc Ʊ-&aPk-oG`a|'vj1إp9qa9wA@z> _S`P=O]?z愴qdPm g'Obc+:Au:*Kk#qg.ENː+rC4fcvӾlză*gTgbh A|}39&=~ 16 u?\{1lzN~ˋB"+2 "'`2+CA8#ZD#:1OOԡCӴ<|snB ٨T-^qz/Z3WAQl)/ +Eyut]r/ab7TRaqs^ml\ro|ّ7~$=s%ZL{s1\l.J''+曎53XKm"nW^y 3+$zm/\W^gGlBe՛eS@t`?C$96kLj0W1x_2tW"~\/+횬(j$k>&tǶHr2۰;X7%T}~Ǐ u X'=hr` +)Oo3gݓzLz1Tg@qj֢+ 'HGؾ.Kk1Z7q$ :c¦|?rh$БS zg6wu^"FGw64E8rٴm<4 |AJ +.hR(XC ƄV@z7Ao AoL"Gfȶ_w^[ \V% x~ _o ZҞsKOiZX ´2r ~ՙzPm>6I/3 .XK,ԃ6 tSITT60p|wܖN؆lr{-v#w3Լ̂u1W1<l^޸n\>fؾ[k@'D\ǖyZ؇j-~;>(s Z0aIxt;pU>|O<|`;o>lZw|^[½dl?a+1zj ~\5bKv)ގ4i0HiO'mԇZ7׀]@Ok3I``pRw l/T]1O5%ՌuZp=ys͗乍D +]o(1;ފ"ZI,ȣ5$2UNVd6`5B% oA+ˎm<:owr2}|`|c#SpRdjqs#Ųswbg%/ +gFoz*vx@\YAЋAsbC: V8%E\g08ހ6|p:z d7K3~tTY񞩄qYg +YD͇5ے oy#>6_T bBXVlQRA_om{O^}r>=$c΅&kS -*z ]y=Y5E`*"L,q5g;(Nތ +?}:]q:LK c9鸒^kX>|'>U ?ysRC6*Q "f%+ &O-+.#_^`L4BS +@kSb\ZgLx85qtzM37ѽrf)N༦IM#vBwA{Ьh3M"}`GFY-ܮG~>g;K"dNw^!y|쬉"z"*&˚N;(KwϔxFtחnWn$P̭1yqŔn: >?4DlUFMC3{onH{ԝgԝLux) My6+Y*:S$9cc +DL=B + ۏі!j a;$ =H^ahtx;t-p`}Q`\G!oΒr5';k*AL*QǗ(uO챊\EL,2m=|4 u'.州uѧO1Us}N:6Cm]S`7x) 'Y`<NX`;?sm1s Dz +$"2$Ԡv@>krI! HY(A2ZC[k,68P)ĝvv@XUf=)rv 恝{%`u`gQ,X`gaDY>Zyz,2`4 +I\D,O`wa,TM"09pͺ滯,LY4` +pr*&|M=;[# +۬L|nʂ6+El^HVEpk÷)2b:vIw]$,3agt;K2ϖgTpq^;ǵ(_rvփd#σ`2TDDgj1cdg0_ic y~{yI%%]6|޹ՇRT{`ú׆k?xTvBBŦs䅍eMo<>t5rXMM6s\ a+Kzl[Br1 הbsu\:俠]oɡ I2V8'dW.,|>b!HKİȥ ) +z&š'`Km5zb!p Npe# dVo2G9T8B|o& +0xp.rMw,`/f0\o`Aµ](_r\;(ᚏ/ l<f +ݷ\>pN>/tc \:Q ojӇ1au"2`g)v?a/]m7z>]+вfɚلXϪY5 /FYBK!FV$~-[O9M +S kbxR~k \R+>_{U\lïpt.'y 6*^Kh74{3g.tek,Rk't]]-l yHe& k>Cct5| b’i2\PmVau i +]P E*vNT:bˁeb\ya`[ 3=Ā$>`CWaMDV18 9-QKZ4_ZJxAuȪ@xl q5cyQE =Lw~y!?X8tG u4)aL"/A_OŵYB궒N+"ǩ{NPSQ.3`ɪ`q)ީ6aNu&uy @jAP7::bs[ϐYՍ̆ lJl  SxrFʏ {|o2h4QN&y'!W=x,9kqpu *$5sYgYvT^95aX w6Nq1sC)J1.1RZFXdmo|fs,EqqZڂ)ktW\XˇЊdM(=tYYB{ +p`mzɄ3pϊ0a yVhnILE'`da x\]aZ||>@Rd`_UclcygaN]\_w__-߸ka|$g;0o~j ;yy)߅ͤ1\Ĥ}sC.%*7"yJ~$EY ';yv)~b\C=/ū0Up\!>" 8o?=Wώ9[@>!y(7>7=v> n-^O,|ށF3Yuc {/H܃+{O%C#5 F&j X NAK* e"nL8'7F 9 ]=&rd2XUbßO^ux62Dp,X= Xz~)|G-`nu6\dk:hVb-Q/',k.zjUl}vo2ɝtxؠp &ۈLO[U!/i$ڎ-z9}z6<_[įWXV݆Oc{(걄9W 0ޫte Ћґm>`IɮizCKY[́U(6G؇ 6k.}xSl=nC,D(>NJᚉyp/- ) ]'V,ClJ{'^!!OWRW ْW W9~,s!$cXMq3%qxا٭ft&ꏀ}}\+ekW ' h "G ٮϗ 5yC? g~S4b;m'2ugsϑs}/םa>Vtp%#_>**~4FghCxfǁd>{0ʐ|n2-P}<>x|<>x|<>x|<>x|<>x|̘645Ԁ64[!52 `FהԵ11 )Y6G /vY KG6Cixh\lyl.Yklc0xyl6v>ߎ +0w\t%8.]p2E6珗,xE6qeKۏ|  AN6vm8`kAt-"`Hφ 4ِ+gOϏFahrep??,qh9B-y(:8,Zрa{ `Q4BMF#@SѡjAj(5o?y _?PԤHEkSQyڒt-< +dbT/ֺ!oO13b$rVPh_KEfkIQ( 4_O௲:$P'i6%)JG2 +Q 冊i5ƠF&Ulm)t3J5ZJD*F$kA7!t )ڠ-yDi)IfSz oy:) R*DM*qЉ?1[W`.=[Q401jlx*s9C@G(\ϔMFDZVs*_5M4RϛL&S6hl"A[B}Ղ'j5F@@I;NuPij0|:(#ꠠ +̢(5$TO,G$>(9P )RYχ T0=#,EW6Bҥ2@[ʥPD)koA4((u_Oq@gRjqU5P)jDtMVFej) ei r`9uYf<T uHM~ &&C[5 __K(Rlu5tP[vL& $=C~Tk +|pv29@Ȭ1ư" Z< +0LpE!c u}To_!PCԁ-JYo*6h"D'hrZ Ķ^i}se+2̈́=.1_:@@H^Vp+۔O!W 7WB#_0&Kx,=Tly[18 T-!Otd+{iUcI tq#xQqZ|^%_'"=]|D7EDHҠX<4~H +F5 |V"PZJ\Da+A}^GLGjDAt+-&|tT},-E2>Ko&tf/ףIR9%z Zcy|<6KPf|8t9dkC+tZt%T <D'TZm ]DE/uB]Ѕ*'NE;̖3y _O| 'c ]2`W#=:feFеJ:t4!iFް_VךSd)Eൈ"yn=%ޛvP̀&>ԭF;mIgXya(KvڒEB} +Hs4tAGHA4(rw\.!nkRei%`~+UP(X=B`LEkA(B]zza[fPQ~>ILxRC3ǀoBk0'PFh1:h `cLHt܂(єX?s8/\_^gJCjzcPYhy :fudMP +8 GcuAAR6|#rtQ^g XlxeΙKZPlC#ρq|.j@Wbp\X_ ʲlp& J`Hc!EP&Px\G7R(M2ƹ&T,Ya;c_JB ~ӶAиHu*$("G@\nҵI@y|%bFr@8Ê3؃$fՂt !QOy\VQcJ+/O6!G6Gm:o נk"9\ףU|U>P$?!n65.prʷO{P#*@e4|n,1|.d| +,a)EM&xH.8BDOx餆Y&x~ EzyJTGxTk)Gƫ aX3lTAy YH2!yY<^$CϬ6: T r#@tbg5_I|n , Q98 (\IQH)7$\9i,_J&5w +plts)x EJwIՆSAރtm1oT٘")P%2WB'Y9dEIp,W\A|_|A@xl֍Xjp9&B\sAD/ pY-\I`{-@: `%WCLT<\)9RMXQ /A/POi0%*]ICb"_z}gPgOwOm@^ *o[`2Ɓ}BHH8fAy(8z88&9eB'o6#]8o!qD3 9$DQT?DxSD +|Ӏ@LYA} ;P's(E~*qP3r81@TzC,IG'hL811 2KZ`@ ]b +*L%A^ =N]G:d:(ᇬ@~}d Ȫρ_qD~XM:*IH$`C8:&KnAPQE4MX`4M1 Spݨ$ Po$jNcƂL1qAV{'+?Vy"wo%7֗WiϗNI['P)kU `sV" {N(8&r*9䬀#䜑 眳Z̹;y>xznFvUk~_sN:ۘȗkܩǝGB$|9 j(KG GJSj~pKesS+`1`P2i-A6d2(H# Q<>Z`X8f+eBrW + +(ZbN.ŜLA11\{`] \)[0C}CLp*l<8;\8Lc*)$Fa\ Dpk6I{A +|⼠k`E?l9=@MZ(q]CpL̈@AV1!sJa^m9EI<>v^{j$uPy$'AhE?${ X[B|o ${<FށsOH{g+-^!u#:t-8j>g*Spb7&R1o: +VXn)j:%jOrE!~J0C1.!rCAo _*EB]gH=f>h._Q)t*Ie[qP$>$ m1.jP3 mC*f=s0ǙtJ6 +ZIY~pbVQK'Ukʴ@_8UD*!/g!Y=ZF2`]:^WD=7$9137:cG1oErvim?89w*`k񱀱@MaMxXp=J~aJv; ʎCe?{Z4Cm;æH}o+y<1iϱjI1D+'*p=u}ド(Gq LzP 겠 8Qo}JjPCpe'O4P ,w +j)o` +DP9bJƯ8Nu/DQPw4(5٠^ڇLl19saW3cA8^NƄ?t'OX 矻I~FUl`=.yQ?J.'}{:h<}Sx>ĕk1 [\dzrև(KqtBD9 T@A8\pEⵢDMpfAY'1n_KنM" `-! Hci߱ƈc. P y\웳X|F19;^'cs񵑞 *]NsD8BL9<^aO;z Xd!5Bn{O nXbAG>Z\m3ܭә@/7e(c1ӉyK 4*ϡ7%됨W2OYJT8V=(k7ZbyoU?_ Qot!>5sE7;W@j _[ ^b"[ y ks,nQlJ{zGl::8ʰ0GCNڵٳĮj 9 ƼlPMI+;8HI*Kk%0p +Usq^cqyF?ocU`\(E>n\l_#:q8aDA[q轕$5{L̃瀪"`PPb'p$x |љ6̕@\m!}u.rp7tث:LOkxaXϖFn:8B%ug̹/s'F./$FPg"nY):3F%H kcqs$ǔi ~Sq 9#R3׃82ERw A#PC><b((2Cp5K W5=5ǩvc!X \& I 0&}Jj\GP= B/nι^ɜ\ٔB? |FIN8nc#`״Ӗ $ZFmCem??@ +*8qc%5Ww!{>X-L|GhXm:z6L{(P8A28A&:s6Z>}- wt疥FiQjW{#$k0,ee7z I;*ޠpp8C\$kxC^S* |? "_?x \` 3!7{2*5'ON }:~"5tXU\>ft f? s#+Q,;!F\ Qǯ?'Oz3@- z#Poװ>2|96?`{q4C[?X'}EPwGYsus +QG g8oahbL$G Eg +qpwı N f,+ӒJBOy&d3]ø .8B8}6mwˀNӂSt&]kKp> ?K \]$P?;A Mjsv1VOPe"ޮ/R}F;}2d,Ot)CzȔ;@;* Pra ip:>{Qfߩ0viJNsN$?8 +ijTrV.MA,5C O7Ce rPӗS>IAXp%{T|\H+ѐe ;83u lV/C풜%SEC/7Al_4dYzԥM\\x o"ż /ys8RqRK}pJ̘Ŕl*40_pn IrN6PvJvI>Z%0BS'GCBh}l9k9S ,x̆&jQ/25qTP{wt2EM1^KZWD >YL 'j^R « BD-c)SԈDM%Whq:4jA{ю?Th֡~LLj89g8S*zl$A -X RC- !HմGe+w k^`-gmT@&%9p,Ů3@EImֆn#,*9p=xjwىrXJwg΋bPQ]"s8 zB(ΊA|ycH0ɩ̣̕<\,aCTarv~uyBNp;'LN(8cq6Dd~{#-ٸC~xe+;vT"5{cHˤ&\EK.Z$ΜCto5:(҃7WJ 'DqQ>9:tn^#yM\=0WI 8rctlw<~W ͘&Ր&VJ*v05tcrn4vZh0^D>"?;!}u@1RNa]@<n^l, ^WgS4m5v +8 GM95zfbGI +BU[#sdPbxXϪ8~[F8^9g3p1 g Ao>ʮ3O= A*H=ȹrpnbu6hsrFC5DbpFx8M7#gH,,7f^iΧmϑpΨc|&\+ +x\=1n&8Y F pou㘭*v:t"+sX_MxJTh@]7p~'Ztw)~np684Tf6BYK39t58g*t8*R{&87Bg7!6چ=7_#e}^iy&`-ե/J(>8OJBMzװo.8&N',SV<r +#ٰ7E$J^?9;M~ph"gꭏڒ2Ԍ#évAJ8lhj&t=pü^Ic,b8K_<\\ +lU5 +9g8h9Y\c DZ5LCx)pXr!w /wQN0+0p .!88_.p"Ku]Πn$.D<\Mz(p瑚 5c?|Fncb#}> +Ɔ=[nč>@8E䬄L8Mr΁2qξ`Zp Hh)ؠ˘ CQRI1!P&LFN:~ YMN,یs:ab~js!@ΑY…[Aj0}ތN(\ȘM\Vu %c;ڥɆΰa0g&S8+s풋Np_pyz%5<{UܮΖτ3d9L2#$ym6R .v2u*l4qϞIE|\1^#aBfA}rOTtFwS(}ԸWe~إżlq.iK#ާ AWr) ˵UVdWZeZ"jgS @}Ft ˠsجlZܺ&"Қ4pMmFŔ4`O:Mb Ԥ L{}1AyLأ:uv:q;ѸSh*Ψ7G*n+G{*6:uMi{z\(aZ6Q|&*.BRgUa㦽V|?nU[p7Ek$9 -^EYIod}`r;8ꃲ*k2jx/l.HpրUn&ms7DllŦ~lrvQ w3Mw*v[ܪ\˄Z 'ˉZBi]x1w | +TJpx28"O5uC =wg>OS噔wlUfwx"77+&v xk勣y3S8WsCZ^,~m<0{ҧuj[ܮ_+udI;ٔ8lyC-eZ4-/ҒV]-=f˜Z33c8zψS8xB#{͔kr\N{ܶKvYJMg҉uE;4mv+9nR>ӷ̹[m sSoH4Ieջ-,WaɦRjPZ k y%1RbkYm6M䗫ir1{ZNmԄǥԨ= *~0r>sӘgD_hLh>sqe܋$$7o:^.^!WcQWgWӯbqI)iI7-9"U!g/7qyk!L%R{u9 v])Z+[ e+RvLrB-^oȲZh:Fd 22 8K͊p{YND~KVx(e^~%: -??!,߬-=y;Ur*(eJP[MѱU5cqf CfKU"{,UɁo2im:zf4RYaѭߍ_Ḍ^Uqq^qÉYsؼO{W{c/Yc֒ RG;+{AJnJ]{IVܝzj12ڵř-{a}lf̕vcVFzJՊՀјJ9mB~'M?3y_poq?o;N'}R6D?wމ="&1߸k>%h}.P$j yiݞ}ٽτ=x4I,ǸԫaUW-i^y,F Txu㛡gz~t m$Зg6SV+_Yz2AFDfNʥn=i s\z>ƄRK3DqEQE߬ϮOoşqt?q_{γ}=^Ҿ-YG>eqb_W}I{k6R9PԴ QTZY1˂R2w?NmesQQ鰤gYaٲ2m)c5Ǥy '6ˎ]gux=T1b&yՂ<ž:ygi2DpAtAsJX%U~GGz_٧P{1OAL.9{۔ +{ yQ|4$Om,?+=&)#Uo![imqҊ*S;9y$YÖmԭԽ>/FZ^(r7V\,8V+:y#6HJm=ۖ\SeϢ^O8֔w.1tMDCr,E_^ὗC{CGskL:sπk rLl}mC["j +?"Z\ *nerz?-01,iNu I7K+h%%9:|{ڲ6OgJF q5+ +bhX{nGY|}zb^cm$YYnQ]z*5Dәz"X㹂Ftw=Qbf|җO2A].\.z򍕷Tٴ?0ޢ2\\!M2-j?,*d!|q;i9ĖUvֺxT8gzjYfF;?4LxCA7gĦy${T_HN<ٔhy7F=%\HlV!ɹwe]i,2jEV.|FQOEyE(Teȷ+G(YMwv>JUX|(8ݐ]U\S*k.:Զ}^rBtxGtFkdVsr<ިe~~QgbweY=?&{UuF~?w6K>6 ?v׎󇛲O4]J>t9pdY{So2QT͌i.}$yrz"g +#[q-n;~F36n\eK7v=:z7TcZCCdOS]pDPODF[В3aaն!U!% +b爌ci1/B$}֝#? ?oB^Dr}G36嫎rƈKO˺>\;/YoMSY(CO[f]6w8 &$Շoz+C,{_GzT']H=ٔyiwwOד}v^k Rܮt, + }` +ͫ ͫ }\]j"]\6-faŋy->4wNU "O@㢬>mb4Q$ߺGᴴ_e5UV-⼖ uE%S )Z'YvXmS!Z+GaߨHLqL<֜,}µ׺oUWy)*"yz^5qVKdlWԁ(q[8VVZV{#^ +2hϲXؤ"2D㽛K^(h)7>':KTWRRh}\_Ʀ/ycQonRX}Eh^\Vzҳ&4 +"U9GEܮv( Kwu+=߼3O+#b*.{FezE9VDEK:Q})+RUuHٻ@YS/k7?eucIԉԣMY {݊4jmڱ]oA{o({G͛>eU9m^q٢6 $:3Mƛ:'M4In\e^p>q9636Cay=Wqoa2^l˳xk )[7қŸE5.0ѯ5[VUh\Uv>1#־!6UU kpj$\+#+bx*v1I,ܺ ڦ3'tSR_[uxsL{βpܘíYqbccy|/VWUPWn:ErW|_G]#xE90_7.ENaY8fW؅|h<? +qUOTSx6aCdNCTpO{i{EC"6|k8wLyIB)ڰݴ o܎Vkkeh)Ҡ8>E.ե$,"yŵ^Q~<_EZWdh~@96dƊZtkqQ|hoBkl˼c ~c^Xz9oln؆l ~_06r-QUCNF3d&i_f&2WLkE%a8G$yDGĞHIHzs̩Gdܛs1oG~Irvwln<R|&xGA=o3;78d7j_V}=~AL|M~4Wm=eR<aFhcO*h"RSNPC+֛maʚgԊ0C3Y> tw>xJ|]m]9W'*-++H2 +\"_u +TqW/ \ :z(}⚚NB%sp;^봽^N*j6Z0_ -]XW<3c~{y3X4 EC 4|RƟҟ?όģk0I<74jZZ6J;nK="cGEFFFE9[p^^u)+vG||/#ny-nk v_:_Ogc^4r&45zg'oZ8u!^6+%2 +tx?(o FF&)㖢v>}8o|]p#i +8|%6++ 5 'gccmk1/^`,$a +ǜ^d >w/rn/u~,Q;ϙy;&4ҟW;k6y<4{ Z{ms?:*N +Nx+QaGB׸o\Gz"=*o{yhLpotPyEF9]eΑJ\#J)ύZ6J+)JcFW&1!d{xn((44MWفn>ԭҔtx]sUB"?š(SˀWb勿WGEo͎ߺEa^Wm֏umCkc{r"1j 2ck??u=Uq&Wf8B{#j RsO͘cs͐t4i>R]ehs( +黧.Onyu!A[$f"H3{coj^F*tc4x0XiΚM8qr( 1'hd 4MMM [&C 6AA4}MM))4e)MI^[!F[\jFLz= +K LxU+gBQG/ #;V;8%V$&53_1=<{¯COEGCSG.F*ؼM ᅪh-He4q626ՅHu^q-2EkwV9N3_W7[{0g^U1uxJZK\aϵ8V9&.q\7L:ozm|6^i*W𞛨<?7s8?& &\ǶMק^&hjK4C3GxNB)m uF .:{ 9Ux߫{U{xs\I99)6kɪc~xj +R< i^p*RCN}非\{!>RGR||~N&?ov.H`D\6B 9Helg9 sd,k{}*-8E`ԍ^/uxlyᏊ"Uڅ~qmrH.rJL*0uz:g?~+caxF6TR̕ m 4Z-C?dqL1A iZHe.RYifn{<>xKy]HMjr~?4\[Ncg ntNvJuJ\U^ Hftچkޔ%>Ǐx,}\~B84ԙ8VhSpL^SM PDMь z?Z㎖RЊ}J=J +mӖӶ:yS&{>/ÃEyF?||e70E]\[y;Xߦypʨs0!J'( 2441Kt筲AumѼh"=s+r:mƔhRZ)AN&h9{:}4^^Cocxmv63zw݈ Y/&{r(Rr"=<8dZx91ST_kR[8K # ;ks؟' c!ӑhCL8ZHۡ6ahSe{;l+>٠u>7\[7ryC6y_ߢq4Y;_W"V^bȋgb"0vv1e̐vzb:4 sl֥3T0&A* Ђr-ΡPAգ\gh&iq0n=l%T7N&4@y;mYF? Ь%hChZ&FKwB˷GKhRp.Z.FT ' h_8|{gVq7oE}bv-L9o? yngK ;^f<#(D}G']֝mLM-/U8uxyV$؇xEk'}5[MXJ0 sˆhtu4o9ZwF m>8pcqź?xgɧGnԯ*;&tzyw^f~2-wg4ب7M`zeoδlfɆm7M̌ 6*iv>ꖚ}Z{*qE O>FyIe=LqyAFoJU۰3O61K7J5:]>R׈rHɇPC1kO9^vg)k[ :㢾?ߪ/5M,*iـ tuG M&*EU鑰qb9L> :y}!S&pfhkS,,Ds׉Z$-A L.a_c&=8I)OxkQ`2-9Nef^fvy& +ZF jї?d1ūЧKgjsIѪ.?@ÊK /_Z6Vd]%5w5bւmgx3-gcvTygQ/o ]7O#/۠7B&N2۽Sxsfmcl\ݴd|\nr}^obھ,|?vҏXNoԙ Bʆ"D)$wKof|3l8#/>#LYts۸F'Fn1>fM]Kr2s]Bcq^-X/B4]ӴKmrяwou~V}"6{15=gP/~Hk["*,)?9"i[Tf*xlÓQ#6q(R;ckЖcׇ/3_Aubj6qodvj qҮ#S拣r YЦx,tGTlZɋ̣vN>??1"g:cm8]Gҩˆ=[ͤvjK5rle)Ia *T؋e_ {Ӧnr^KB{ӕ7e_ Ctpά˘}pǵ*,h|0\ie]^mgm٢VMCOGzhבm'v:7 3Eyݺ_Z^$@x_uDw狜S 7?OۧOY;Z$b%aq3>VxR`iuĞnK]Vj|-@]aUto^.z^w2j!g014fAQS18)UUHg6$0J`2+%J.)X%_-L_K_#Jxfr619Wy!~<ܧ'{TuݺeEq,rhzIMft3&RCb>y•H˵`N#=_>zн +^U^%n>|-O{=ZL{q` s{$h +k u둉ɽBD;#kmįxƫR%먠܅߭ $M֙2.d< `/ڷ3[GEem}UߵD)Ӆ%kGX"_˾בz?N+#u9oh^sxCGjG]XS6uT@_m\<m_ z ב >"/"ur )u:-{qM/sWazdm'Rj6S[KjkTM1UcY١]O}Rѹ!isw*<fPҴ:9>^dY/>|q\ߢpαzgr_'ȟ| +-sNC iǤ7j7#1aVqvި)::L(GpO#QQp/{VZ܇뙪稀+FSv㙠p,=LeKJrd]\FߺUԶMg>%ojN +n+HkD<\, +Dȼʸc6~I;oX8n Sg&C&ʂQcu %hzmdS4ҵHo~/Q%]=e;1ABSIV'F[zε y=V#[$I?k'^~EDO ),${!'ԅ)i{ڵf~h_aWbMr0-M&3/ktͦޗ*&]c[֘9L'L g&3@S}#Z?o!ںR}Gw +bŸ;%w%5bV# dz{]$9e:mT|L؇+T~UQg7maG /ۙ{8~Sޣ)ĺrYSZwgc^Dѳ%We~dW294<>o"=$v$<;Ќ>I HXQfbhfn1@选Ҡki;MQZ6bNЀߨ sYjK+}&s.}WO_?ސ(Cc{.hRzMs1U_ ox]!t$:3$k}2;:?=ss6W*g?0SI(3nit\&6ItAψ jEW{u.&|3uAl҆iS$Ull8>r( +>MaR+fԋN9SF?RB^۩#9fǕ-:볾Q%̳nY?tys{Rhɨ*5b``R{O ܮM}cNfX(A=3\zxng}ta \o&Y/enuS>qmxaz&R2gƉBTZm76օW |Zrԫq7f3sjtLE',\)P0w/pϘfld8mnF[Ե֦X{g)83lehh hXUWnɳ$!W=y q[-|-ĈH XGt.&6QJf * +o}n?<>* (bigQPS)pJ HHCX ̂*qZ댕JȩIUYu>{g}oD_jYg=?o\񾍏]yܷs|/ƾFl$Ϛ'ǸkFV{>W2js[u7l]G.ƼQVb;Cm;ρpo v|owg7x'b' ߪC`_}Kv+:Wd+|i?V{Tʷ> ]o>[= e̷'w=kwv;˝kޗqr-/\8TucϹgGJkWHWM,p%SL)Q5dʭ`?mNپ[Uz[MIMcws1 |Gs-b?qt':#p)_cnOQ{W0c׿=cN3C~V|困ﹿV{Tϓ= ؉2 ޽"ou={oso3v[?ߤ9>ɵk^cm!5sn;vNV*x%Ŋ7Wt F$o|z|My0_adN0WǼ/͏]̷|5Ƹ5?gzi?.߹|-;y\Lͮa)཯L<ݷ+nپRy+=_2jST}0M nCsQSug?z}K^yS/ ~eԖwl:۲tO|i;ҿGp;T>'[ 7_&?lſq?k}&້gX*w-+l#[\Q +ד]RqM:4=]q9waf ;KLemlnBlz֥K C J1۹kt7ǜ>׵cy5[22hZ7sd݇`2z_䕞_;-' s lyz<<'. _պ[yU}л_&Bo}5̿Uƣc+w]ҵ9]~/apx:v9G~yWȵW?|^y.Lq<傿`{\ѡc8sҸbN`ڳKG><ƻ͉moؽyc?XߝG=bmx_h'+r b<~Q.%~M.MEv6h<]ϼeby,c*#=i̷۽kΘi2PٓX=*b%8_\OGL< +>_1O*1>og_:ߏ G]yngy<]jnyч<|;6ϝõ?+3oiMB4jf$|v`+17e`͑A2v:>1q +bvxpb`NЖc:r\:|9\`+\{]$߃=>|} +tc{z߼o\G~_P?x!]ߪ]\LcM-Ir /YubWr/z\EU fkC 淡럼wcلp1<*!ںWߛ|͝gcNt1$_E8ĕwl8ӷ%`g!-}~`}8߳ +E[\_sgxXWB8P~?=Gp >paqΞ$C|3޿ s=ywaB}gxtסo&fğI<#1x"e0&C '194<#g֡N9 :F8{Sܾ%kf4LizI՜`qG/<ȗX5 +10?uCy3%! 1{sz#aKOKTcW7u+e x)K"izO=/"[K|1Q@_~/u~ߕ\%?{cb\z{O]Kf@L9nޱ*#K6fcsj| +N[fB̡>ڻ`ѩh^]za` ÜJ=jIu/-S1_9vj0iX9=Jzt]sorɞ :Ϛ6(<Ʉ qpу){/E@곂wn:2"_Ձ +Wrd=~%C"hhc>܇?  Twͣxo3ܷNWp12JFWZnE6Ŀ|EEΪih! iWS 1ܾ`(X}h QfKsE\"B{8ޖe#q&p(CMKGڷ㛷TW`!) +`;/lz*oƼ>wwvRλ_.CN ?{O~}ǝ ~XO!wu<Mn.[pc'R[µ*҇8J%SoUR۴xVZ4o׸#C0'7ۘ6=z槇WԖ ,Wp󖎘>"ᔁ_lüW sBU% S0=۟b%GBXFymN{ebxe 1SKgpa~CpƳ5ޟ?J0g(f|̃zP-=߻=0bx@9S}KvnjXE; c˚!#KhAms #[ޱ<^}l,ͬs)>+q JG.so`1 ύ|RιՉN ?IMp\==s7~;s˛w92}_8=O49ӻ\m1e'oQF %z;Gx(3THW%2Gr9wHM$~cǹ{ʷh9'spǦswmGsϼzv>>Qan*13]gvyoo4Nb2Gb9p+8w(egZDڇSNyЇG޽n;m?ל͘+z]{7 Cݪq}zu3ǵޣkp{n|ᜯǮCAѿ;d|UN?}?t{`B`^7ʼ|qCpC53\KD'80OAwK_'=g|+̝8ʻh繞-KFT#CoRrKpOhgr\`_ނy|IFKіQS0ڷKش0Q#V\ŘG>^{&wyUgKNQ^߼OWoј:lskr6Io| y͉ ~~[` 杋za̪Y%C0#M̷1߰7zp?ظC? ˊ9G s?tp,t{G '| _x +a@~7БzugS|Z뼋֔bz͙u7ǵl } +/v791fyZ'!~/uG>t$y3 hn+ptznBe!bfOo<&B鱾}_TwC+1C[/4 ykNmC~Tpc_hzډ/agՆ'yfk|Tϡ?X8Z?pl,{W~ ew~u΁q׫`@P?DwyCnuQcVu[^a=~usB-s'7~t/mP.d3,bgy?Ԅ_r}`~7@+{[|euw\9P?4% ?'RO5JߝsXĜ}:#`;v +̘z[Wk>DÛk]}99"fݱJw0-.^uVp+A̱y ; ס}R9Gx 15E[/kϬ'SvnLj]uTUAύFnę?,X!lgn'jX'V*{_mzjϱo׿޼gyp6DizB\ܛQqwюsi]q+|Gĵh0uzϪW8n +7G1q#B-0n7szagjY"vwJw?t)挭[ +vO׍?xw74_j{Je8Z\u˹vD C|&MyHg 4kHn8m8|ϱ]?{k*#^oL1%7pEgs2YΑ:_ Ď +'Rv?L\ t vV}IM5t8]xah-OW.D,WagEVׁBW?uu_p۵tńthı[{ ү[5[3:<ów9q|0Vo8#W7ȣͦGzywj^y_dz߆Ц!fG/ :r{&^p穁EY=sT{oF5O ~np!.CjkC%0Nkڳc9خ᜿k}Dezjꭄ +}:vF6zv}쩵`_}.9nґuΑh%F,n§^Ǡ'D{Ox CoKC7~k|}׆w!,tsfۺhRXt߹>>*GC+FZ{ +\o!9ӘFXN`3FjY>V\KѰކڷ9th+C,KZ`gC{|ڗE شh]nz.@sV?|ݹ]:B%o|,1;p%&>5vYg 6l vWѿVcYĎEl<*|{~,yjw=5 qk'XR +`ٳ]o9d0tNMdVjvm~)uK=~W :mvgsq퀰`7<;sY=uF`<#vl<+hYn_ćY0Q?v xC{ /Z}vCs5ljoJ%~"v#W!"b9=MG޿]Cw_ 1*;|@ +ss>=m a$lcգqwo}PQ|u WsE |t|߹$z/N{={AY=DY v)3q.C+ ' ;;kUgJjYN5N! ߎ>/bǯ|t F I c] #8߾iR@?X~+?DsUaee('ߡe_}>d?><_Q~7xJՌPIb]=k<%Cwnh0~zq q=ME/_x=sd w X7?9|ɯD}t.2<}U|*Α.;k>)v#C툷{uKw^!)_}&^!ĥ\W@. ޹ |B; N{&wOkax>b6+sM?qIÏ2;bCǺa ڥ߿}IJ +#On +>c_3F3Ox'4 ; חs6\Ym}{-B;"mzzĮ .?ϊuUm^|ދ#x\[_aym.aa8=oJނkX}a8}4a8&ق .A.G$s/:z4ɜڹCwkp&ƷuK]'p](2U̿4Ԇ3z꒧Ը3?`{ЗB;V|ed#.yB܆pT/9:ַṷ/a:8uKVva^-?X1pp}SzC6o~a s' ߱@`7bԡ +|]'񈋂8?M2]|䷷ [q.|@".],OlJ8k\lpcwR?a@쬺|#++]}B\ϺYk?{~&Vu+!_>b?{M!pCW»#-ןStl|`=G^›_-y"_s}^kg>_DkG/1x}p{7|wIյoa35ͣ~Spw5%sB<4u7m_] b1`s{.E.+A̍'wLhi~~\~zgF00B[3=qwi1胅ֿx&Hh+tb \1%KsHxAKj]? fC;nC>ؠ_p0NUL/]k?׽a+ a` _Fʝ⼡ֲ ZO0agw=Ƣ?wwnXi *'}0/hYk_FSE5U=tz˓TcSYhj˱ XaíqC_tNoXG7}3=?gp=|kv:7ٹs"jAo6>'>; +ʱͨ@óny+N j=)dC}SGlc u7tjK|KNE=/"K@L=@ӯ0}1ȯo +o|4G[g<_: K*={~).7ӻ&쵨ї +u=n> ~y[r_ C/?BՈپ>.',}OB",Cƪ0nB n>dy{@\%X=eyɏ\{hӹt Цgփ_x q}t{߯e9Y 1x%xvx)>g/w +n8zs% +}Ą/vfA?xjӓwトvS`;QX03ll~گ } ;bn{vg{<{uxF">sdhcϐ`C[|* !6-}:s{o_o*ԺdmÙ-WRZ烘 (2B|ꙵ%s\16x~`KFFiYh,O00] ƃxf+X< +ہš3o# 7}t?h +:%dLxx^q^8F{tXy5Cq#>G8W0B 0 >A @oOXG }{u'?ⳅlG[ 6]r穫pn'!vmxUxq "dn.my0=q鉱߸λ ŻF?GṗG~= alk8&8Y5y#!N؁ 4{0Lj7W'OS?OS?OS?OS?OS?OS?OS?OS?OS?OS?I754%o-W>ClH[:*]:aby*=%֘%Tm;'&F#qxNdN@t5F 뱴Vg$Hg44 lmgL_['ÿ2;( +6Ie;'r"k%N4Hv87dF%K}9N 6cQ54Ѕ0;IXb^YB?n_W>-GNԿLQLMQ! .n'2hV*6=mCAB"]h7 MHҝ_) vm'k]P{b^fExR"wvwK"Bw@3tE=mH.Q3u +ܚn;~eUfUzyGe>:IG)=27yhleۯ|Q+K7oHnw,R2{sVYKT7#r\KZ-mk[S{:hdݘ5 5Έui \vj?7Z4&}}<4٘'S:jgzwshB[{9UOZ*[(H2hKGgYvغɕ˩[h4 +RA5| +ie,1V|ͥ[r\ W&KrOZeԥ*<nJ}S%X(RSE'Xo)]vlb !#ֵPbT,=%-\Hv'g66V+2̓$&p) +%M͋$ %Փ]rQ_ey!P 6YwQB7[څNv˙!RXu^+"'S<˖*–AIڭߦuW7 +E &so-Ɣ/㻃?Gs#`uG@4``]o3ֽ MlXBPnyr~$]x1H.`Z˻}e2`ϫ^βP[n/ 3wTM#іh"=+Zx6ie}e3Z:f=nhy!PtxK| BRn +DUij, |T{s-^mI]ykeche!g%:5,O.?Lv)9l~nwfɄ%Hcc{K{2xG<ܥ#r,rd9r7>Y6ru=9[QMMtlQI/[g\DSc2?M)PdIZrP'N8ȃ،Xh- +LcL,f[|WF@lSL38X+ +w#3 3LcLL@T_Ai,)3ֻ+@)-9à,,d9+vF1w׎m`J94Vdɫ(+6xX_-</y͒~s`oߊmP귩) |ݷ(`0jVnE=wv+nEnmܶ ٹ-]$aIId$*NIT[# KD&Z>bm5M%E+=ccE/c; ?޶VLcBJcqjzbšKX${KrRX kbhs,+貹ZRJ @H:C6MRss#/7L1dJ&Wnevm^[eꜴ]Jmr^v-[ 5t,.eWK@ +b-Ѵu\x'G7.-O_\-P*^fy>ĔGWxGŷsnq٢ߓg1s rRh;iqm#m1( +F9cFo1 +,F(AZλP> +'aX a`1 ,0𻇁di^*b}bc endstream endobj 48 0 obj <>stream +Iǁ⠋s{Ɯ]BIkyv̠~ݷach|ϰ1h&-d(V?ڬ1mѦXs2bmxqdBJFcI9,>;Sջ x.ݘ~95 w(iIsŕN ޹q# +0GDBu8W&Io=ZY5UyH^F~gc~/Yq~/)Mv-$[vS͑hnY拱A6hr1Ykia3HߙrXNwEN;jMe#$e *U$B&hrcrշfdHv8+S&>pi,8<uS=48>Mj_,%/~^X&E+< gz9r` !"b-' +Yvk;͓-ГEZ43Ҋ&tMh"|7*|֕ЂRuT2=3 +KN^; +oX Hj‹k"χ[%ah +9kcuMX2f]IjhEz?Tx%UYNS1w[V(z^eQq 7uAl ;Hc̲9EMR(W8p=3γ}sT(ꡀO35( EE֑ v/)U89K |\r(52fy+xLQf'k O fl1ܦ7* +C;I5[Gȟя,( ܃чk'}|#ߗMLJW+'t<?麡p¡)Bz d/VgLDe$) )XCVCP~ruMKǗ75٦E%SDJ[9whi񧺔;t:!(;+Ht8rH,,/qCbDYxs +,eI8ewr)5;;6H6Ͳ[6 6ˢ ZJXu2lgEVq]t XY$bAV;Xu*K 2#٪J$:(8/(켃; J*MT˜S(T ,؝2'r9d +α2lgYǖR(cY;<Mq[UlBxto</K="\əYg I2D!!RuG'`E Dodd|gD8uI<:Ε%g%!#Akq6Q'A2/(pVDF$ G 04X;ny5N C0B X +bhx+$Uf!@{Y s20ˌzAh< / t4e%8U&p,'ܗh9FhNӄ J:@cR +e:Q6:ZfQl e6d@f BELs8(SB{aR& +GPt$PN1ÛiRO))RmW:;U>*+(ٍBjҒ,,eImkV\rV]X_׸nBYQJ jl\CVeTW u6 + MT2Ԕ%QlHjUkVo]Y +n\C_TܸɄ9HǙD Ov0,``)nxxT5FCv.(N']ZI+WE( ;Ny'\6$( b.ʢ[U![1Y ,QF tG3A37&[4DrbtF=ATC,ZTD-.mp#jNJTaR8hMFo(Q)P!J5͌v*r h"gޓ&%[CSz `et2ѐ\Qb1ٵ )!p2&64 ðh{q#NL # IqlGM)57HE׌ViϤx'XmT#RNAcsL-Ggy +w<(kFnoJCM (a ڝp1jSVi}_ENE)rSQFUd0ڃWZ%JҊ8< lRX?:̉̚^bmJ11V9ȉ 4o*ސ[QV}~jE*0Tk= p7 ꗲn{0o~)S;&cXde4U*6㥦iWui)\r +›;>e8>O|&\Ϥ0)*L9&&â#FɕEXʪÕU)o=AU 6QflWߕ NSX`N`4p =<^AW&je06{?'j0"߅ qXd)CSF 9QP4C ]fH4.wi?GͧPFAY$0fQ7UuIM6DGaVOwZJUu&hE^s"Cp=CpDZ%HiTOz\3aftzZ8ʊ̕u$ѣXA d̋چʱC_ͰKǏ;4O0Sdp'c֢/͘N Y[y+5F Xq'ImA vX{ۢ{ʬ2xzn>naIP>>'' \y,++cJ6%;d286sib&Z&qR-Y*R DD@KD'zF,œʹ^ +&uQF3y<^8z&@G!N9qUdv&Bb$cq6߰撶hc2CsE/v%$8yeq7'S-Z-0hs 3 \W3m0mDj2 0jbYvیt$=A?C:44 .W*/ǡ'1CAnV+NR9kh RK>tqFrF +>NHnq6 s$C &ץy|*RA;X;b% |O&?N&jhF@T9T;hYM_"Z[cbѦ.I3FHQIiHr-!6 `tQt;ͥ:<<^~ IOl@F9-S"$#Tqzw{hx%0>{o}P-J٭fwHM6}ɒNV4u-5Sظi4UhqV{L)ϰ6EĖD3+M-65@Xנdʌ+oWf1} +3 |9yY!ྷq%$,. W9d1xʌ!dA]XPh /*q rvnT8; AJ +jQ&h՟C$eSPa(sWnP*;e"-4b;h~dfSA:jT`̮EM)Z ڏjzGM0uRS պdE<`3$h)L +`7 B@}$H"4R41NMM:ɐD,RoLtfjf&,Ip1LSl`Bl8L!*P$Z15j7S4 4CfWKBoYhM5k!CLkZDеLWi 9pTI#h.VOT L` ;/pE/,kR0`Oo!>dlVe%%{#Stf'B YR"zתȲ"W a&gIU2T:d$;KYbx:첈/ .13 9XmLo!0dpTkV%S$03cYt+ӓU0,!CbdެiEDP%ftIȚ^ƻlӴZhLB/kVE%*n 1%QVrv dӮMF2wF)I3U ڳME۱_AR =\#F1uF)s^@Ϯ̖LsOL"H:k3zG&i>Id\1^`N2Df")ik7LmIdz)s^AYWdn&i$5љ<"4$3ͻҋ^zmF1fPJuε5$0ηV l25TIQ1T$0K2 LsbWL"IL$7b{y6<1U6OLO.1{g8]B +g79mPɬm.T1XV Qӝzt74D47{2iKfotZ߮Y6j[6HgwI쳶s >l`-LM&k[ot#͆O&CsIY?D[?KsFV'27Xsb]2V7[DDyiϦem9lac 粅drzLm$ 6VL^0VR7:`L6Lno`drn3ۗ&l_6_7C}ي2};pg3ڇ-)!־lKu#+ m"Æɱk_6Lkdqeh3UA6WެYokfIEfAMffAUf8f9]-TsH3S sL3|P sP3R$sT˖Oϒ)I?/U\_־5gyՠ=X'@%qFn"]H&Ԟ{&!j㠼{SR +y:9<ySN;.>OS =3j× TTClh22*dL뿘 +Ȋ<UKQLOH;loEeVNOlX&$cV ʨQ]w3F=FAeH(\SsAq@0O;Bu/9LCuYT䞺S{ȠS/.qv^F3δzXp=Xfh̬9j)gPVQݴV7eUu6r:0K^#z^#V#_)y$[=$sgk)XQ.% 3yӿ(e;m&Q.Q$K\;~@NUJor"$\dԡV]Y=ՂD3(TKMʼn fqV$~U*l0 :e{RM[߁8XAԄ2b2)WeZF"XaS4+XK| eo ei=YV;8Kd~WL}etѩ6L2mhhC]zצ8 ^=N"9V)gӻ\kO4|Ki]EOSLK]Y#^i&Rf6=t{BBEC^;vqqr2'*CXY6jTx>Tnn|S~n6a،u5$8l 6S̉Yvڪg78쌈/ZIgy K! n<"}j+q8A)t& +/,gô蘂2ˡTj -Ɂ q{,UEnZUJumGYצYV3RfWY +,j"Y Th%PPgACNhLg)J3Rc`KfZf6?xY +^3+T *U`w#Љ@D#hj,IP (!P%ehR-=ZkJr2D*ό)ITc<7L<лp?Bt4I 2+FGQ_dhN`/2q/iDNt ^JG$e\:D9>nAwCbeZt$\tEHekXLo :T4d|*CH>a [2T(Vh88JȂu4|$bodžd62'‘d1GtUn'V<a$a  06:4u `IW6BYGC+P :$"XȉN MTrڊP9d$alN A)I .҉ziz,j4Ix0{{ӹ]͵Tv7);Z9ݥ-O'qLTN#MJ&DR\rzVNp(U0=U͉=QRV&V)p25 pPU/i_M.&)[7/b+Y=`s?SĪ ]o"8fhz8MR"&xOv΍SQM J]-٣/clm֨ǻuksD4IƠB0>q^+DN=b2?pSz.97.{G%)qaS'A%~ >ǙFd!urqfB[NY;bhVg?C+ҥVj̦%NQCȼ_\ B,PdT4Мllo6ZŝZ SexcpR=U SL8"˻70,+ˆKx@#n\TeUx$\!:J%<%qQ^W)贀͉1Cd⟘aE 'f't QVVj5P:_j Rm][EmmV$M٪SM7ce$ikTtkm5m&M`++Ye["fJU@iHY?Sm03VΏVM4MxUѥ@$}M|OkjMUQ \W42qͮtoF_>oRa4zv|h9<uLF`G6U6˛)fd[mtQ&豶ɥ5T*X +SǩAǕeHZ2c @p9O@Ȣ$j}o̐iN]MT75Y}F`eY"+[C%' tD! "])5 +}:E'&yvyVģ%Y,Yhg҉SG۱ R `gХ˦hzu@UUi ژ/DQ%7>>[H6l펥IWnOAli:r0%t*3R̒*r 'UӺ BPăf8 ",[Iɬb-qݬnDމ8ZZ1'QV p8a%Zvp[A2z͔7csK^vr--Ң-~|9PX4ESPpR@mT"j;A[{]*ҵqYOɰ01 c_WtZ%("zl6Յ[>u4#!:خ(3bx .~b16m?7uk 0ym_Zb_Q|Q"OOP^;>Lk²bwO_VP -gnB!Z8Hl'pxAgO 0:\B2`"`T&Gӿ :\s <$PZQc+4Dt%/sRbB3g$.&s:/.m& +F +Frb]§Y*OA={UA2t+iD')FٷQ/Qx3OkkLK(Lf\7Ҷ*v +橂Ph(>f;[u'5^ēGG:IتVoC{Cw]OݚRO?nr?[gћ- +\6ZQ5vDIV +u qSr=gz~:a?1?{U+SUyWߴVlѿI4gXkw7>dX-9w$ +VӕFUE?>]6Ζ=y@m;nC:/sQ~j#3g1yv(BUϧÁ8% +8gy|-@FC ˇèFB;2)X +<KP燘z8\Cd C4 >GS{IՕ^0FPBy&4 +KuT-(S p ɢkf9 +~x}.] 0'V6 3,|?¡MRtF8o V9(o@(8#Ux<11x%cB?'8Dž0LG0vPF +V \Q =haP0 fdȡ+, it%J?ׇF{ (P~diTnϷys@eh \FiX `3"E8]~B*Wh? + @=G̈́aar-eI^(lX󄤀gZa lMvb!^=b82EG< htt1 FiG!xiGat%jJq03h $' %||ӤTȡ‚P! ,0̄XBEd`G.L 4::ׂ;ZjJA29&{@S X|OB q@Aʙ=a!Loa1d)т4u0 ]كFAÌxTc  + )J0NC3XxfIpBd4O!(-a@p4 YP82j:TTA *Iq 'Q!A,ʅ#>FP k*$Pd%@qh}a !OP 4I ~9x`uجY'wd ;8؝4#VTA߅4-hxUTFhnD\R(4L"zeB4"Hx  +RXʢA . '°qF;HS2ƲaFz!JoV BsX!$b +V;$.i?DV8`X  <` ]y`20ĀiflÀ +3UA٢cQ *eѱHD h;4ꑾW~Kҗ,!! +,(Z 7H g8 Fw6xuO I +/AВ{uSy +@AXTQL"H]b pXK!_ȵB +IQ] bGIc^]#\)eiy4Fi +n@[NG@5 4vd;w4(wĆĿM^0vA5,4@3 =,BH1{S OVh'7slh?7<9>EѠ}aAB/b~T@ J2rX=4~ jmmTo£ <ⱘpC)gu#T + +6 pL`xUՃy u V*SN@;.ez4i / TrihG~0_A _، k8r2Ƨܼ@2`B7rHJl`{@+xi2n~`y€flX LP%01.zkLj2ZK8h +oAј䫨? N$6ߓB91ϥ@\fl0\qD?3d Y()Bh_1HT+]u_!\(HJf뀎8%.Cc/2|9CfAVxزR_D+hXw/ S @PG`LP>C2OtHQp~x-JQܑ@!q;b;!;l8PBxA]e> + +iEe~C 6°q I*CTn Pa2`T FgE(2\$}yтaay TK`0)F#g`Ռ8ខIr-v;rAgy:IIt658$˯<ںPs(IojG?0v\.^7Twv{_T׹jJ5x] gr3Vv07Rd7Д.X1`=OwxZpt H÷ws?kSu/樏+q=30puѭ&G+`0z|v σ5']BVEBh#%IOџwcyDe9-tiфfH6oS, ̖p`3RK_;d1&no$NO 1ajno64C'W?L:jS4rJF6]t!ybf X3.݌b6&,)́}JZZIub*23Sw5`Pԁ`!.|jV T'g&6gG\|t|m٩S#(ONOjO"כPo ><m fvMac[)T=!̇l8l1(ad/ RpPGL:t|\t!r=+ln&Uwi>O- XZ01jf5ĕQdf[LfvhVn\_ yFYpx.dEqԏd3vƀp-0wa137YoW00*7> ,LJy=,*Lb ν1/XWa~@s%xL`?qHbd] mrnn|eO{ b>_9 +PdR8;p^]d +՘J:YρmZѩ^&y<_߁7C2 )YK,KW u3f# +Mh$x>],n{.Sair8ϻmo]`hGálceBŬ fgIլ8&՛di/L|pmu?3of%o2y]9&% xxChb ZCNeycB\91Un|4P}xHԥ0)u8Gz h\.ef'k. V4T^122L6CvE~^1$lopQF*&cc8nAẐZeyPUVޠ7*)$ Bf˿1u9 +ΌE.Z&jk;V%$֕`xj9ͮXM]+VT:j*%HDD4!Eyo8lFS}*=Z4TĴ{1{Ji?>gp P`(٨AHjʼ“CGӎ(%?U<.\vf{HƠ 5TqZ<˱p0Cv;By/԰B7J kjȨ۞!_n{1.#;v;A ]r%;RzBG@SsUIf+'TģI8 ̖m\ɺdwS̹r.z阒bR% +ibͱ/ɓZ ThI~eOSi6Y( ޷h']4vI 9&4W{쁊ԧ"d" Iʭ3O;A84bfD@0{P4rҢ#U;R{QӪ#$:6Mso7{B7T 0%k,G ԠCRo {7f +j3ľxʢ[d6,nN5mefCx7 ߫H4Gi?6ni"WptBC($= UZ)n39VƞF$2PS扚;8BݼNf"OMCf3:#hBx5wrkQcH/Qm9H8G Knr=E(dĞ3vBQ|~& :h"3xmi'sm7τV\,UŔ-0_BMf<~Lف:JcrBa6d""%ji8(RBq"ߣd4GuY 7HB4q: +&dK^w[ w\LRY~ieQj#Dظvjj$5#l:L`j2֮KJ =j9j1e4{X]ica SSv]3k.S]NlׂY+ 7E2\Fƶ|e,E ~ذW>n}ڮq:@3zsœ& =`H1" @h!7.r%<4", {1s0QWٿ9P Z%:Q v'ϺB[f @c4 Ѝm$-Gn1EGLdu >\ɍ1{jZ-,F-% +`z/E55d#](C!Z:3ڽiG՟Jq}a),>3=6uEwxzrY c#M##.ї'q[m<'eZjTܦޯoˉKfHOb~&p*:!|</9_7Bc;Y.a.ک}fόbO qǧd3h<'.Dd{3>.Fgvͥf}docT͑啳-Cޤ__) +?qlm a l?tJeN;ߛ7vÝ'*~;E&< D˭'oKqٯi,7hD8lH.ι7VSlq͈N;hCM$f\m.̠S'҅K[;CX#ks5xP=36Ädžm xjf"ie|U2#g>=x&gvg-07?]%yڗ}::THYzc ^j*$BF~χX52%yŨvŰG?|x3`3:/Rl:O6mvѯ$ ;6xgQ$PL_³ZZ23jdmۢmaPg+mb.^H}W(YX<ϛZ/jT*'.ǻ'aaQWg7s232EC'&B?qLD[kYQZIޥ_[oMcIR]\;=fq k1Ţ#OILmR S n|KKKa,`Sc kfCipfQ >PH(^Vݮ|yoQa$Z,Gf"iQn[00'݈*X(}9FS{ٓ߾[1\9M6+DY3>ѱ7TFaw|?tH0̺`v<: cd}vA9>Ncqfpk9gT #3J:_:˽-c< !Tz*>z>UhPem` oIʿ=8E|VR9YEY1&$tk"ӆlΫDPF.c\@!@Q;ԣ0FxG!0Pm^0㘒ar;?#{[dis f'HG%0+K+H6[E.A3{ޞ$be~{6=(QxkI/d*P{Ivgv `5#ypXJ) Qכ\^;gHpYodJWJu/wƶ0zA,+ɩΗLVөaDj@ x +STflp]==A2t\Hڷ`'#Op-~e6j)ƾ瀬 w@qnIH2m>;CЙ] (MM\|hnCNoW,H4cCW@bgMwJpKP:co'cqxR"S @A6<( 4/l zN#.j3, Ki{{|;*TZO*'E63a/܃S%}4/KT˳ǖ.voտ]\\L0Ftw{eؗz5\>:@=La]'KϿJ RӊLiyIM` ""%o4`6dDޫVSR +آ컚l.5x +\;jHnovx[ ]47KDY +ͬѮlu; 2#rO FsKgurˑŐM{坙^ZQ4_bL6Nrl {߁s,˄(o9ф/,Tu gL>'"7ӽj"-$&$|: s#. !5O8h1qä;P]dv$XC^>ÉHs]+Ab ;;c}׸o[.ӷ}Xhf9ʌ FeJ͊kMPtWWb533{ַ쒡D1ۯ*sILۧ Py+ ޴r!?+d|L^#XEf(){@y˽FA5P*9E~$>Vdz-,1c͋OεݪGd\]+?5#T;'&C-ң7 & ?8s!;M63]25 $"^09"??d*z3yLMzl^MuYQpcЯRL-ʃ͹We)`q@h%6ZH2gi0&CWCKDt +l/RZME#js4`~ާǶcoufTctXrUw[T"Y!,}}!k[.|1Q95C0Z~6_癷&"t EEۿ5Y/jڠ+g ¡05un)c=1NS3C.Z+*mse5ޖsϰ l=Jd'J&P_ʨ69ǂHfct[[TrQј\m勌iɆ̒j$xk}Oͭ^}J\y/Jrj-Q +% F8cso;oT#uXePi/dSe-'5l`MͽhLf}\JOdd|w4SPFמO0 7Fc9va٥O[&sW$h ;K}|v)\9 ZF٭` w`e- V4c׶;_ +?'⸸ĹoFn^z 5G4F7d;1}pʌ+-mcY 2yjl<\@A^ M2N'ۥ`2lQt}b㾷fJzX0l1G+OA6ҫ:̾νnsTnƱQ #95P 8 |C5{1QId^K rJ\,^3;ނL/?sZblvKehe?I駻ӌgstv}$non&BXlau0%-XN&HV@6ݩi/(A5v!qCbTȝ D22i)>oD,K֗76]f=g!8izw^WoXFG1]^_s"Cԭk钽 wt|[e!xEQET}hš#(SanwKV8%\<+FsVD5}?R9'+`MewT3ϓM[}bU9_F߂[9|Ծd^[Hy{"hSWs-+.g.}4 +^<젿\&^hW2>ߥEIfߟ)DP Em7 +LlΑ/Holq6*kŠ{0kTG"ˣf %!'[ܿQNo\D`e*S +7|4z8U*;:dpɚB]c[ +uSQ +wN6Um!8YLgm8"*Vst`@B!?'BFae7NVTQ:L6p ׾RaCyeP_wԺ%6FKe/֣h$*񫠄Nj%D*f++YD A?: +9Vr+j6A!%) ]48< 4Sm*ZIy`?Dqf; V~ %a/X+ 5 fF T.  +*JhG)Ymqw0\@rǜohS*?#uK,L ٞ&lvfX瀖IZ-ͮ'YzOsؽe\=(KY&q@ִA`++& 5۠ScsJvF{r@ #OʠĈt[/r er;NB=N. d;\=Na2٩b`CO.٢(Df}\;E'UgD1=@?MRQKF+cjfT(/6v@vJ@鬻a¸'n5'Iz-vkjܤg\U۬@L ipP%=]z@2kC[GDV6SmeRDkct`ZS.LhPҴ){>N >>!ðb=dD2.s1ܜ2dԧiX^jiUB3DKE:8YI C\tg4|X# XHǝjaDFD}4^jN`4T[&^Df(5/SyUIEY'`f]eńY% ,h!Z"GDnutP*֦c<9ϲ*!&$% / .UZ,쎯SNױTcTŲns]OD-?U W;1w-cxVUár?b .k3=XTbqPN'}kMZ7&÷ +>d1O<睁>{P#k=owΦ'f} eu>I4?;՟}Ї.C)j[x.< ssO]&a]fv]Щl~nlC]EadJ; #F{|ofS]2EӱX'WZ3;-H\;VPSp] +U-QuěIFy K_Ko||1"gvn\.0ʥl42S-l%KVÛ󷏨ىrm3:5 {cdcg{SYjOk?*IwOt&UK*埘ܤbe?,L]-*lo?-4ZzFl92Lɶ3ob~G~DxXl̍,vX"oq]-lci֚Z/-)a nK}ڭpC m^Xk 9.g{pNTh c-;ˋ% +Yb6gNjUU+nI,H%d,G۫%X_Xrτھz+%-rsu[j`%'FX;Kka/}Ykl P8 ,Vaܯ.,Al=b:k<,l}ԵRUbulk|gcnJ,qgҚXw>qaͽk0XۅnjZ;Y ˫7ȣhNml]uޱ6kc}as0Wc{k Fҝ-䠽𼻱]EGD)Vm "g+1[閱5?b~[vڞ֭u׶{cnJgvۢ6z5pޗ>.By/, 鼸6v x +j݈bT)/oj-]nwߔޝNtvv/oq{5^)H;e6>&f_seepWFHL3#϶Jth:|1m> =:vNǦw{'z8'gdjc=ůk|ٍ+:«s;M\scW`߻Xb+޻* ]Ezz]욥ֵMngjwޚ+Mlܵt/ě9 <̧p<˿VrmHT*\JCɦwɒNO\6*W\8]suO~-}\`,^\QDն5^VKxf CB׺Ё:xn_=\~㶏xEʑzH_ w#Y HQ_4}_oDax6i`tXDBwNnn^ҷ)&J&lr]?=xX2S#*:b&B3;f>% +㫣O)x)ٙqkF$܄ԕggvpI; zݴOp2  ny=u5/Isړ[-2zcj}Y?:@coOϊ>0P/ +m >hk^Fb(>w6PHJ5͙Zssdn\YRA7m> [a;nW K49m٧iN!N q|(˵&FtL#,4s-r"]Mp|k` DZ-JhrE+jӛx^ǥ gº(N#3gQdV֕PX7ޥ;Wkzi׹1MJx>%Ҏ\DSޣ(h{׵b7fP +xW':MlެʫvU`y)*-RB9tq۩Ea,o @Ȓ_,6@_ݕg.,TzG`#觏̥-jYSksS F9/uc˙Ӛcz*Dr7lrQz:zU)Pryb}=}Ot 8!=`U<}1=}dZX`tg1237qz>8VWYtHzu~zQO=PDf|CbwCi|\|,DӭOoūopkfd:,3a6aŁ*+Z"usO.lN znXDkU]!V 9Տh']cPd,S͵S)"Bgv܇?ZwXPR&LCV'4^X8f\'!,sf4" +1:e}?i'wŨV?p 5a )#DHJ4|u#`UY.+I#!=C'\?k-fA^@shSh!aƎ|1Ulf!=嘏dzv񏯲<979xIAHC1QMTH^%c8 |>sG_DKSC4O6N +g[hqS+5?b1lOc":L?!va.ܗdy ʊTxko`ums(Fw=b +(OV-zgv +CV (;]ѼXT|{/Yy*imMqZ\[>mE55q]\v`jaFʗsQ{)gN)altS]&bDZ;(PB^#HK\"*AiOݮR|񻝩T~ך*.NZzx%@f">GxBC +vV^N0^W3TJAyg b GuU$47vl,`'*${ɯ |̉A>rjI&Z}=Q|ƫ+b0b2k=Ur[JjdyW`HCސCEP&]mAIgh׏kLY-aݙe= Yw:C]171F&t&e|*HIKg7cJƙ]ejbڱk>Wn.Nؑc55hPh<`v_ eܦwt"c\NNT@r4 m^CA]ib9lK*\50cZ6DD@/Q0Z)ćuu!/ jY7#.}}Wfż<'+my%`]LTyx_ +9ӴҾƼc(&ZL 5c[V#US,ڬRfo܊]?;aj Z?d`>d*"&o=*LohmRhnRUS_xiU92:e6N^3ACB3eXmT&-ďٸ0!]3|ui,ԛ+3? %Zk% + +Km3ZTaB'M/NW}aվ'юW$h}/ {DSlqmłaɯC+Ucsus |gF͚hAOHF]!{:n덋6{5-fOOfܞ3!;TiM +-1R TȢ=y26+{9DHp?S ٰbB3cSe )uRZ~E K 7V`fY/?ɧ8WO}1XcBkMh1Xb/ +]M-_boG.Tcֆmc]6kh{c]σ5 _AeyzZ;hd=ZgdrTRo- @{>iL&p7t65kJZaq3s,5& +%:OA0)BqKbmSo'M.~(Lr\ FtԈ4HٶЙٽyY*6&:{ʕ H(@WP%A`lhM˜gF#.k5m]^uk+օ5t: +:Nq q>Sj4+贪SCg\A'V N辂Y qT-: +:j/WVG5ttBk+,WIȦ*bܪs3J^wP_Rz})~ylʩd' +#ͨ3fT*ztKd4C:ZK٤b.{:l S} SC~#~)b7xH/*vBJL׎oe"1-nYuDrŮ(:S`ynOnR&Ln*Vd*oܣ> em8aL8d,G;V0{0h J$7Nd?QHץ;~䕹Ņ&d3ȁ?,B+eӨt2;? Sz ($Ag'꿆53_FZ1v(SS~OuLE?w_lKwf]yv.N$IdcӘnu\#o6ŮOωQJʝ9BAdҜ,q;ytfd &t=&)̯(Uڑz>ƾVyܾG&wtL+WVϿo=o&Mp|9_Uy&NX;Qu4.xnVY +!u:Vij=fTMg9s:aɩ{N&nnH۵LN7iEo;y툹&s E(tɼ_"Ǔ_y9SߨrL7T޴Gl0`}!`HUPtN]XjcdeKtZ֨]߯ӚT_ӪٯA5ǟTi};j[M|)תNdAR5J +_;j:Z:ݽoWj5:Qg~NK*f5ha5aHM):*UimXM~H/Uiij:L~T}Tic~c_ӪSF5N/WiuptZ-FGNN$Wiu 0TM^坏QM>`췪LBj:N9$um96T:P:ݣdw& /vԨpeO2lP iy]M8}_݉5i_ 1ƾk]TR1JX˖y'UGZ+ 8ܘc5uk\թZ6qmyݣFu=fUQ4U㚺_CiކuaGM5>=Nj5{)Lv^[hM?Td$.Ce} uдxI}}r-I%eK.\ r|*s^,Wʜ+=lE}S6qE]'ȥتPvzEx/@EwugT]gdwOziz8!KBmB~4ƞD۳+ٍ ]D٧VWv 扈.hzf<;=fZpǟh[VmC3՘EM\^vD>2:%ƅw}.r/պ3昫vb?le"eH!]6j߾xl:ۥM'O6}3Kq"ԸLGj_IAœSJ~"@;3_u=h0ׇ]y:S쐈,M*q-1f=[4=HgsMdMg ?cQn\|| էOk2^ +8&Sh{)౿SiOoN).e:&ͻۧ:^vW~_s~<؉&sy{\xwpyD*wwUm~kOޱ +pg[`7OGְg WVJZjǫlZ@Q![*_,]ы`~yo"|F|;G|HGe+m/(^(ӮHoJ~oo/w*bWjwwF~jwN1?yor߮ XA5!`UR[b/oo<Ә?Ļ~#[!{иnMrWc"TyO+ޯ틒Y~~yr߽Tzu(]=fw =m˿~AZѻ~x?od9{}WlRn*;w-]''߽lxoY^=?|{y˻~{}o߯u]W*T1twFֵCсQw@ki!=}=?dml)mt]_۞ٳbwVrc\Yw~ӵi]&YghցcԶjWPm*w;_xo\bol귽՟뷷ޏ[]ߪyUʃg뷷aey~5 ޯMZvw~i7;c]=jb~üwAUnKP7ƈr~5ut6!xF΀׷VGcXϧ? E0xy' fGfyxٽ] t ұk/gOd[?enMgO=o=}|⷟???J|Jubӱyq͍Y9Xl];rsΥWwoXyM5?(<Ʌt%딽-N}{til#եS>S~?$هN-.xX>mxͽw*;пάdwgG?ڵ;TxK߾?|f'^Z2'xv4'IL=]ۘ W?Y|qK$OjЉ-.ƱlY0-*.gչ|?wgk ?g6/^%1$Gw~ rO3XlΥ_u㯽?[p&y"ޗ^,liY˳# ]h8{7~1?+XųVTXhdK? sd7xcua/b9ԞsXԯn5?swJqv8U~9oA5K#MC/Xn8I<7zڹX8_z}}g~~詣v^}̖^Y|y+uգŵ/t C0 +h?ꛢWZ>fig'Vgl'Ylծ>܉Ë^xk!/ +o|bW~ݕ-XO%Ӄ,rqVz_z,勼ׅ'.E 0R?z62Ůty:ų|+q_Xu|7'uq݃~<򼵎؅K;Ŕ$P]v⮾RKԳ|1\cuQ*qe\2#,^JYh*WWNqLí[ɳ868ka2- ~ϿnY:C:do'$s)ڝs7Srva >$?_XbG«#5>>܋fȍ?wo_ǰp >HK?- ?ڥey7ɉq.ݸraRTa\觭Ǚv7l\XΧ_}_.ֺps۷٩6<>o>'?;b/?,?;z 7ֱ#M0;:» cX=v&7vL0ّ#+g6ӟ1| y2קdoKqKS_$M|k Z +g2'Ϟ~mML֏1q|th'˼~5[/uȋu8W3g?B "ƘfS^~NѩtR[S|AKøZ'v~|;].wj>Nf#X~{LN!<?Cx-?0r^?#0HܱC8(ά}KG}t އPh^_BIK]7 xx4y ΍;'qmxPZ]N01*vbqSl(f|}m,Mgx/,2~rm@'6͙ngOrUX)~dاu,c y(=<-h&`q |bc$2:mQXvXڝ]m]gìZli~V~yza4r3ˏ.|{{tnpx3>*W{M廦xzuʏqمoN01}鉵~ w m1FprR{W'46FY{GsN8a @јNB~~2v~xun5'< 8,];z%dLю| +݀pq9kg_h\Ώǻh{Uv3\r׏ ҄H0|@?ĐTƐnuhOƅrq/0VBK礛ǫ϶|:y4fvg~|rQ6WA=S׷g̬ o?|~_y^ݼ{_vs׃L7bw`/. 8t!p,:Nz?Ncٲa6,|Y}ۧkVqٓyq؛^m?J^Ej>Vd~B9W2}5Sݪ_>Ȳ7/j[%fggb|(^ap/͋%3 ?o|s!fHWYt69M{K)\H*}Mmw?Vv;0KJ13`9>O./w|rx9mfj+NOt7OɿEr \er2۷Mv;;n+٥Ϟ +/lsW3n쭞㳸1b]l7;C=p+W1X}x7AW=~ٓٽc>'ǂ/__{|[?Ul +=N,aHQ~U{W7Grrp+ÌCC@fh q#CGcp`am*vS:HpнGI{? ?A3c@X +O vRO~[]Iʭ$]Z}٣WǏJL/JLҚ?rَ#CQ%{^y]tNG.ܩvk`';Axs_Eo$6ir_]6uj~4$ߎҤkzs$`{XGYt!FQ7ǯ$Pt"ڏ ۯp8ܹ愽AN߉5;(y֮ik/m}qg?'Xpu(;#ZlA}aO۽LLmfy5>tYJ[4_M5ua6qwu_qwuN[91?⼖ޕ4pw%q !Q[8ܽPg1t9OK79ܫg}?6fŁtY-ƌR[uuӧEÍ^З71;r~l7{V镥ϊ"ϒn[ӟ^6fN7?vg:]S~.imvN>a࿁?zAnv*HM$q I=k{/~ DƇޘ7U $?b9\sc*Ϛ>?M*#>\/V: ,$$ݭ1-x⿣"ڹ+G\FgA9׵r/(kK>$%הBƌ}s> y% n.X@Vff߼tӾz8twp}~Pm+qZ~X<~=}a} __Ifz;NM4cQ-E=|<,z>q޸yH\\:w{Ŧ\{7 )Tdb΍w &zv[_=\^w˝x)ݽMcf?Z뷧f6/ׇtGԺefMJVK{3,dbz曧Eߩ72+_k25Qs;Σ=V՟kl|ɹj(+dCLLq L'Vĸ 5XP=rww3l6 "F(gXԏX0鎰&:;ۉA w1j9r3n _}_>u?u}G>|"MqGVYNWӵՕ's=m|pmL46OǫpoN/7CP_ׯ6۷SgnLpBo{+w. jP|t6Q8pp ?Ǧ<9+ngµA6Sot/ 00O1n^cƆR)X*=֠.ᶺp|?hU6!JRw/ -ګr0cwφU]twF@oI> !!4#H=v7U?}]>׾(! 4[9ޘCO7?4ů|:4th{0WI@G.%~lP>,!9b~Iom =1jG߾])|G~1 %G7s] +كa4R.k;>(ݧD}Q %'R0 %}XvC*Xgy{Z(C YJg4jֶYVWYdA:nHcYE^zh^nuƶѧԶhunQUhbS5Re<=R۪Pݐd +i%yB[+"0ʐ +&^e ՉDyEƪ-(JZb*}3'MEȫAyYVŲ +\rzJJ-Ɋ}DjQȠam\Q;ס'pA:Im`Mdۼ*jR16>؆U(:evBm/䙶^1月f1pE@ udlju]NuEGk4g7"Je,-&[m0m5jP(sx`&tǡEir[=D' sQO/](YWI_QsfykZTx8ӿ6ADǾ~;ޟIxlJHGђnZFsQGOu#a##tENDu9TE!Xjd9+E ,UJ+şMe {O{ bmhC'ig"oT+GDjs͝K0Huc⩺> )ʶy B'y%lJ'.vEҢE=$j-AQBANLI^βFV7NxTB,>x=HiHC*di[@AiȻ u(y)zyJJ35D+kOKfu+s#mi(n$!~S!-g⪲(5(E70DgP D9RFTDKdO&lD U\įK65.m0R, +DIB\ -bJ414t}EMjovג/7;ߒ~ G(/z7(89Tu"5t-eKQǩ twUTы=s8Q$ e̵PJ$6ޥ 4D:ڜeHIKI2SQGMwI2-V#J[!Ҩ"%k0/ +,$#vV/"CNLHKVBM_RqMi\$,w_S*e/ˮf)Z11K +fJ )>"w$k|,&jLYVrbK@Z1}"17NYM:)in@X!uYRm 4ĀfʈCӮfu[t3XU#[Jz@ڑ[Ѥ{"Bˁcg-TYH?A8dm3GK}Dة`@u x)%ٰ _=bo?#)15)6adthc(HUC25TGV,( +t?@`(G`bFMC}2uG +2rC +J62oY 'nNrI %sH x%NQVmw-}j,*֙Xn!E M)<Y2Ɛp`(EqLG-\ tʥ7qbAKg +B(hrpu$ KҾe6=$dt9F;K @XM/ľQQz@, +^n`V<$Ů%ɡM:ݔ +23/>TC2FgI +p-*$@~%M:%/Wbre:7݂$8 +i:LsԠUE&I\{YCĬ%ӤYy$tx'-i;r_}6`9 l MTtLa0J#EaOTz]%zY"ϯ#WdKJ A÷d9E7X"͊,T|'tKJRpkNWAeFe|'âis)n=_6>lJ*A]"* d_8:e}SZ`D+b ȟ*ɍgüXIҊ%\5@s +Pqgt-WۼSO; 怜Ha'[F# ?MZtR:RdYBECB!#J#'Ui&QI(b zƳ+'-_(>$>B3maAx-M}j B8iJ4 +θZp* ԌdHE*qOD2c0Ї8bgz}h$eT Ѥ3kIPb 6Rֆ`Hl*FФ]5$ti\}ndYݑh04Hy @i1W$4=ijs.d\$ƐE蒑`ts!2 #-Ǝ.p~P 6~JsM#t/r@ajA0"b`adʩIXAؾ-q#VW]WiS`ϼ6A܉dRE"7E;׏yFt +{ 9]DQWE;b"j/LHhpx L, pZwI;kؚIDHp nS&l +9"M@uNhRȐ/m:dhܓh"#)!&;Y[ JB^ۑV,ƹ:\nŤ**AtC` +! qȚ0U"ii`R\$ +.ʒt@c%i@~(,d6h-+/ g-b4`ё .W(}~Raڙ/@ *2GQ@&FN0NI`WF* +]-k0uBQDe!xZ/_Dl 4}M3LM\ F⧄|~hך7< ycBEO"0MRl5X|tCn\L1IXI&V14`F3D|S y +k"(#oYZ[`*pFM 4!v +"3PSD80״qi;W B޲d1,fy+I\Po(Ei @>|33gB{ jAR׀|1z<uH>$!._Bı*Äp#w`%Jt){o4ƾWC&n@5l!`k EOJvA3 ɜҡˡA&JW p9RX*`NC9쪫2zbDs7$ptLj9EBQQ=9m*aK<5`_Iy1/bV`C@z + 6ۤi 4UB:BC M@35Uvсrl,1`!2=Zy8Rt$_QYV݀1I`{1\S* Q- PlY 7Gg$UJ ׆wC*Mا( +.+sX&X -HI2hv9 @w5V57L.A⼘2D&2TD$=Ti~+.8`Tt"qCɧ9 +DY2i- pY`!SNALa 8*K!VtRZUj/Œh'-Z;/f@mtU=Px WR 7qkKH~kTlL@`"ayB0 +@b}[wgy`=R_:C;|I,QT⴦H]CpZi]d|w t9+JK&u&2QIO`{EEa$3``X.fT j@<Sd ]M.ܐcGv0Q0}JX3jRIk$='s(7pΚXh+hb*tIr7E;!r$ڄ:YK9'*~hY&gH,N.7dcx C<\d0Ddk66/ |6N`]*lSv +nh܆aMD;QB6hS>CpT)Z #Hp8\j3o'rz?DӔKܘ ڡ\I

S$wUJdF1'Zv~_P?/e.d,Rb%xưty1eMm-[iRR|y@"K# ##ַ늦Epl9 [Uأ%kԦD@Cf3,D-'INvJgs[M%KĤIirטn5٣ )QJR,3f615j Zh Й#0j7 gP:$<.%]N=SE,!hbt.s +ZN%qOu!*RpMkGtr\`=30l8?sYJ#`mU'{ItE=.hϥR?hKZ)vBc Z١+ ظ+X #3 ؃+bB+=j"5@i2%W>i:O1iN}))QVG? O5-av?G8;@HḰ8Qx\M6tN4 3@63kZa5ՍM'pHb`P]*,aEF\K2 vsʠD5 hсEꇦ?(,M<%W&; %P?RRrUm. iDWf"/Ӡ*Zl* Kq"θ6:@–ҁ26 1gSEn&nKfHN6iH4Y,$/"2\9j4'k/emt 0Ao(?f,CXKރx`@0 9? t-QfYBAivC8/=џ""򮙛.0i +r4tUkNw@A4t4gi 4)FA#Vhm^o7(`'$b%HbZz +Eg2p՜m${r]UB4քԈdaJ($z#-hBP';fNK;!99!#1NVyGnfW2s%&EbMUD߸ 2q<,AB!nȽf +^ +|-TK?#;J84WpsT:柂`'u%NKQ/ÆjnHDvӖ%AI:iDiRoD Iq氠9HG .Ghԁ9@9I:FX?"3gCUtп" E(:Ue0]&! T&ng PIԈbM'~Dh,|:A#eFji݉" \\'+!а !RW+2Pe%&6a%L)ɼ-~ Ԟk^08PKbzLP lRmjX#y"hΆ _ڄ5/IaNƶ˺4V߲L= E: 0$5/M6C +zLs+pvtȹ^I +\gxtBvQ wFcf49٬A8Qi]2 )"8Eã PIJ!Fٸ݂88Qd~3+cBJ dR +q9y@5ƫtt(R~Ew\.Fn liӦ@&[g -m!UA{u9}aH vB0BeZ{W]Ofg!,oKVڬm"ЎC(&66DJݡTEnse#'u8/ͻCah)A(R0*&aCHpA47 Њ5tJ xHyF8 XOB*ݺxQ˩6 X\ILelɱg,629>V@>A=~(@'Φ7 "Y2L6=aiL!l!:bAr<:x6iͥԑ]d޺,Hn&G (nԿ&͋QB&& +0})5U$GJ?k(`O3N=)4R¥H?RC.-n젭H{#4 Ȭ % ,Of io<GvjIPiīB .d>4ab5L*Ypݸ\wOh&E  +o0O NcҨMKis%nś]ΒЯӦ*):3- ̜$?ѩqD +ݒ7#h˞UݽP+55t]07\gblMPA!Bf~f9ڹ6EӠʨh2sJk'͔JCO@הMCxh`Ȉ)Yk?e +CuT n%㻬 '”0F>\A)pI]}nWk36oѹ,:(4$ČѕwpO^*.'h2?S])[wCGdc"n'GX~ dA(zD"/'e)QnZL.^R̘v.*j%P nzT1:nj EÚRWK<< p#%U^X²yi5ȋp;_Ծ☌x$o]/@P}%z~sH0dIo[Bqd|ɇP*6Y˫kh2G+ITyYvډZWwBR)R8ΓER<~FnNYj4t\jKEC ?sĠH;?@V4qg|66Hi~~M0&)`}5`Mt'g<>`K$C2G}:wZc?ܬHLCPԤԢ iƨͲ++hȹkZҌB4*vx2Bj <=QS) @%kmϷVއvE}p,/!4s {9n=T: /x +8[D .#R!zV*UDZNYx~b5(`]Ξl4̈fX mGJGA@e*[|) "ɾNLty,ŒcU{=wd6?'$EI'qێ("R `Dl)9f +X̓A5!U.Σ`rMtp=1}+` +~Cx0LګR"PݪZj]hyQߧylmRoh[<("ÔCftK҅S840V, q[Bn6.40 K*z.P6JXaDVN2A~gRC3Ddڃur'~G E47zBrH?UnSj>3#VC7]YNgôZ-&D0^]Bwx ]x7My A\1zq)R4"4C~f$ulBgK`N=XMnKE\ARZ ' ~|}"1O{#?(w=$ +:pNʘlja>:(ľ'?Pi݌eFjxڏtPDqVhzIqngߣ܇lkM$^>rFv=WOZJG3pty* ɓpOMY svDi +V~GO@PnE²[)4]  tOCqt1K}wa~78>!~5kQ 3wGm7 +eeX9abG 9%XCOz#K!{;Rn-EPZP@џs}.1G4:hb=P+QF8%VVzMzEנ/Kt.類N,=4@L2 DWК *&ɓU p-w29 yj +FtZ +sj &hG_ ]4~yԩG҂xAl#.(冴< rT+-IM65Z^hrob?cLrYN٦=jT_,\M +v6gDNA$m7dI_Bl~9nƐ& QVo@GBU?my@CZae^v_> "E1 + S654/-wLokH"'Mc&=&Ms ݙ5;VCs=ִ[x!zۼSZѣbE&5,znZw*ƴb?i@ceiBlXMi$ tSoZf3t @&rjTxB03)*imG /:~5P"/uZYY4߯U-Q 4H?.dR5:LV4) -BbC2;Go..a$#{m+k#A翵Y'E+UWaf7p>{)%Nm&D +bk83Bu)W])*@?@#ěP6[Qq{<߶| -,+ Q:|rLgjlkjO@gjϛG3 m7J$RhopcU3}@sTsuKKci +L;ڂ +*vcdJ?S;#@!J~ֵx4(t&+ Il8tq`!'/Go W +u!R2Bmb̚CO4E?/u/sfLW+#V@*W`Ezu5 (aUG:5Ԧ4{*"E,s0ٜWl@ц<;&JO2`_g +ϻDAU$[ 3xn#-aٯ3RA9W®X_]=l ͗/~2{Jp_ ʺCdX^5ə,CZPixHKU d3@?Y1*}P{֯o6Mq٫>@2#@,KtFRô\j18GApNM"yc2;_p0JheFǥjOMɹ3AU^Qr +G@T˿0 \qaK-_dA]2|!^!)ƃ~q)BxrwiXIhr+CxrO{zD+/^jIzg !B'e?KT9yŦn|2W?+t:OѣL nowiP8cYb׶W趡>M_Yُ;/jFi]=S'Gb5`h!qz +G\ȌxXJ# ־},hgd{yKxd*+iׯR>b-ەZnY,X_6AXHE9JO?C,yjj%XTM)H[{AbQա¾̏ύQPZ]  K42V`Y*#IhC"n5%P-ՠ]w&ω^$3zh=y3H i`rSQRIɺ]X&^guF07l ۭѾ'&aWZ(U^Ov)HMJ=o̴_Kj 0rj#R(?U +n49{F uh+ca}UG˲e9Һ=)8lچGk]i*Xݐ7-hȐ̕9X߃->QkY0ʽ*0v# +\ +QФXO;\RlV dq$ ዏRPM*{jհ}عRm!Igro/RÀ8g:uB(aFu;H嬞X`!O̟;D_G>Nh~v 'BCY3(|CWqJO/«pJ s5g悴#lW//׸@8μZdc;wufuR]O4W{9rf^ѾRoxG Jci]?)Qc(8w#E;MODK_5qhy !2=Wt_ZE#6{L'Y +OLeB_#Y;'%kW<4~B&TyZq&2iGf<`w$ʏa6g #wC2PPiKEt:}@Y{amAJi}3gګ>#ET^p~LMv¢+C endstream endobj 49 0 obj <>stream +hE$Ulo+p|2E=I&c ҔPmVo3`7A +{v0tWnCT&'Ԍ DXG[E07 ]QHţ'Q'^ PttKi5z%3 oq%A u͉97FX sɩ{kvO&q[Xg#NJwd\+zp@ ) ߬`BA57}RKW}^ +Ü|Zy7";i#fHfL20/-$O3{S6BOь4$=pL04LNs7̙֏jgQ>ׇz(l1z sp#փvc`' gk~q#kNLHu2BAW 45"O2o7&%"e2ZuKfDwVzU{4woo;G}9_ }NWU,;5Nnjy#Q!I߇EL7i\v!L YFt0i +'rZ@B;Ck? HnaYr]"3$̯ĭ=& +]6iK,SK(YW+E&<#:*o~3.v4)+reImvNR {擦od;{Vmg y+kK ՘#H 6+~q@PC[+ 9 Cti&5MY2屷 ze˄~,~4=)6&%O}J4 phs]CtEy_82VNF=dE7lDT^kKa}vK|]a$u?ߡ&x$guTd:ƒ/7p\'abIBGY(WBK6-fʺ҄CR̎]ߜ}҂`$ Z`l+*XhdG~}EzK-$M\RLkɫjfwT_̯F kΐck{kܽRd>$MDq/G8+ @]|a@qPK'>j-C*^(ZKE ݑKt\-rȋ3debα",)!KȚ~&^g~,5.Iqj4ܬ!* 2ʋ~"Q9z.0R9b3O$lN,o(3x\]8R_utKX[?,o:@3:`D+?ط4$[(?'o+{>hlջq ^÷q iQOі bBA.3H+BqGRH"V] ] Xk~C|;=O79뎣nbG''jl+m^R$Z`a0R4%Jc=>@ O iq~_:)g"+8}QEf^Jxґ~[GAm@A2҈7$+Xe CxSL[jNXe=B\3+>nSQOyKy&ԠIϐV64fK;߹yP/8NAM:SU:}']r +5ɘHF ''ΐ$sY*$ἳwUCQPr I$*c3{T@Y2PP)DIi*$8!B +rRHzΨ陵[@w|rX)5d5uG[XWJe{[aتrg w\"3AtHoQ<5B^orD w _xMcw+֧Nq*F0qIqD騃!>կ:9xbQ%s +ELH+4KOZds(;U&:TI7 +tRDd2UC)Mp!!Cݿ3%߀6 tTtdDaB/~O9ӜE2np0$)ctf>"]9fL%jL?P=_<0S;.+H+Q;>Rc )uu˱͈jQY2\!yOf_+ ՠl˲FcGί(4:7 qbfWuH.w٩2_!2~/)ي,Eo:{F\)sL)פt_/JީBc +TT:BĦsFHeGt;7PFeƭ ;+Po-u飿8knbuKLWudsxv+tWi]ŕj]i\嫅GN.#:ѓپ'գ<ң1\6}(p(^̮h$Cy2!&5FqR;Gۨs,qW6*l>AY-zrd7}hrG+5izL>N j`eLl)SV{1E׆wо'43'"E"?{1Q|dfX12]qTJKfLFʹ)BY]5og_:la)jGK_=\L:NO)&toȀȽ<Ħ1GʉȪ=ILGg[,>ޠ6%Ԅb+#$Ɲ?{źqhutX3?CJ:(@ͧaoZjS)r֧Oi} @/? QGPҼ⸣1yę'r: aK~x1 _e@#zdY ?{AkY*QhS E WVgQ FЂBU6b:u5,C +k6Qg]2`H+ZVvZ +ErX_ZNj=l|YN5A H hWE/C+ +>><_a;;%tid ;*NX2䯕{)k+S~Q(D/˓imv:UQ<0ǎE)sh/Y{7GmP? I?/}3z,?gk \I ٦} J{[RɠJD׎uZt;[# ro9P4^AcF5b4#+x,9:CLu .Qq s]ErJh_*;dieyr#ԭb3]]~|'V,r_pm<2\ +,tkBkXh "/rj3]mA"n[Qq"J5IBg&\Ń1;9PHBЪԐ%! &K7QAq?vE;d-a{ XFY{:ygv$jT=6$Nc q +!b+Bi 9bEWIro*X1bR{j{LJq:o15{qE6? +1/GmՉEMC\*T>}F=^Ac +Rz燠BC`?mL9H9z +Bm0ޯ iD7qRRla(p#cY .'b-tpR׬ 6Kz +2ʕ(dw1Mztňw\wm3k@sa &#DSlV*%FNd{pGi.'4>IÅ1qv>Ah{}?#vtC4WOh/W(H P;N@JLV$dԼ,~$Ւ}b6% S/cy] X`-|]ĊE^נu9ghiHNx`CNSW5C]owh5$!IљLHM9yRZy +1#6hӆUX b-OE(gx\@čӷU.̶x*&a9HѠ'*TdKe UZ3¤Z3Qʲ= W~̏HB+*h¯جPx +mp!r)NUΐx<#'1]#+/8a]֠!Pc#H9^-`KOpx5袓nWBLJޥJ@~'҃~(F /RQ6Cfb64[%*!|JE"S ~CV\,>~.eH%}k [Um٧J °rԼ@wc}9lҡZs pT랺EvlZ3MnxM `v;=BHvE4A|4jL<-^7Ok0tP1сFn{CL0c}:}) +;|SziԝZ&x2+gh(E$"ߕV-Mo礢y5E +x$Ш7n[e@%=usZR(DԾJ(6#ʴW2BL| +Ho#o/Ib+8ܕo=UVǽ"IbdEB%'O:LBxDQg`qI5JK" '󱺏fv +4ut(U*AscΪ}smA֥ +Bꊄ,%QYrη#TQ`f,z#}sW(\0׍ǛeRH.&YK)kB5%f [Tv3=6-!5Dњ%rb!^wQ]y9RϫK g%ڳ? ZRp鳠Єژ&t%6?QC|JN9"vlQo5iRtTB&|TYZbj;hT16쳒A./U1E\@֋<Sa)P83TJQ*ӃU)>T}3;~7_G "WD̘Y?$vfx;1RJ|LTpٰ! {Ur?N([,Z8&Yom忌PuI-PojG/v?wj{HzFY_]O?ǥ▅~ y^. wC8D6Ƒ<\j4l >slƱ1O#Q6|jAN +puېRޞate?(Ȥ={'}d@d!!. +qBO,$|WLaYa73ŽaI \| ;[eB 99ͶC#ƞ +f*| 䉌>ts<ifCEW`H-5 vD>(fx_A<}8zb%"-jmE,mcgWܢ> R-]Dꦙ4>C`$*޷optoa*~nM"pAGRdqFf`xU@j~_,s`Rn>F@@D7d?~k*N\IrÌ'GknΓ7Sa=MNJʁXPZŌ^mr3wjWClA "s[S$p.B@'U#U.c6U<e6U;:-iMEnш*rOn6ռ'ޚG)'ųG窾uR3|D@G 2 {(Mpř1KUɥO:moLg7M':\>^XFDћZ +e_= wG|%l׬-[ Iyp%G0AaqNZvoO[ uٷ[F"f(ꪀec钸FͨAj&v|kՔwH "ydyxSQ27vk4 DS%*;Onrhl{,Nae@-z +׋ػ;ڏ<ϞKXX"JYCIqJ4kkҦ_AHa59aF *5Y,cXʪS[6g<0K%3P U66 —lvnRF Lt &~gbڳ~x3 +k%^,=}e8#VAWT~8gt՟mrMbM}; 11mUjʀFlT1fS]rjbMsMFI87:xf[yzߥ0Uh7VTc|Y$.dbAE}~cGH:YIo@ 3 BI*KYThCn* d9:|nId"1e(}}sxN|Rayⴡ(SW' p>8hץz JBz{W'F!)\OėFt 5r=Fq4TOԐ#dRYlQI(ъ>I,P0hD˟w&ߑKP^y1꧎ NN# Ń2(w!"I^HspG;B-jSju"~Kݡ YeR=~gtC5BiPo*D Eq"8GPk"ք.5rO N .d 9wsPTvOh3VdIUK2oQɋ}2i$=T0nE cLA +ySKl!ŋHBH:& +g=CIP7W`ljzv2v;(A%x5 %M9L{X QL~Щua5gzg<7pK5m e3w#LH?cEb"E3W$S;NC*CyrB#̇ZաP(mv +7Gx ȚƮNgC UVÂTȼqRFǏ${| N +:4V#/g6L|E'n xse5顂]$U 3Gc +4gGrÔĩ2[]HWM̧VؾdHD}k4eˤ~Fw8FrmFgNpuheV v}lbu(/g 57 Zܟ_w!r0}lMW+ ySf ?pj +U*;|f7ʬSVmk_:EQNQjwqj)kEcEܱ!xvhJӠFW]`@{ϢZ,҂! +aWXMȥFZB0OPIYX,ho6u/A5ҙGe{F RZ"dg`_.sRXPP2 j+7\Q_*r֔3Ћ dp^aEӵ*yC%G0D[W} NL]2jcTJJ0tHb+@ Ɔ,]埐+0l_@Fz;[ԓ6(_I#ӟ { VRjCe6U8/qA +qaa-wu'5\чͬU~b9Xe8&I9#"`ycrD:>]M}߸د~m.~ӎ}.f%"O!~N랊zEA .J f DLAl]eDyiP*;tyn6]!-+nxv:>":)5%~aVvJ{0TWs8;ڦdés.?H.3 +?e 5U{Ց AHN +5rIf7|4>nc-հ=dL + ?oN Zz1(T -$\a +T)r`o?5a^Q4Gޗ+5;֢fޥ9#d/Ƚf.%vs[f(7AǼI2!#D ̀M:)7bjqZ%SG`SGtWh ! 3r=Iji0sKJ@mxա ՟a=%|8(ǑY,c$\E*#uNa27M0kNp&Je!8Q;8cY?g];u %ȹgfq}fSjAˍ9]7ZIW.)qR5nmN?+?q.A֦2$6L.dO|} P<^iskljyFۭV8R8Go &bByjQ@sCT1 +W0~K1'S_ሮ1fL1wQϣU=SS)>/!CٔTgkA D}hNiNf}_6Йhg19s9r))@?A-=H Vt\ (Y=%}R! ?e\hdeTePphׂ,2GX!+$Ny?b5Xq4<Ӿ_;Wo/{6vwF` FJT)mO|sK7 XKch8I|bȽ!X[ٺ^EĤ/Lw.Eu}+D$]sŜkQ1FZ:w-#hp%M ~m\&4 _:3͕)zOdLHG !{1!HpB7So֪*4@i$C1ڞ=5WҚ95'#IHK2o%[ϠQڷ {&[Untn-o1 TnÑyُJ}?HX n$G:hhOR9U ǝ~@Za0DHtX$#{zFOE8Dɮ@.)Vn2!kɣ"2'ؗ/9.=#Zިޖvy}}(͉ZRQ%KtٟqS=@73"gXpVA2yֈzufj"ŷ@]1zDV0򲪻@'GV}&4;?I*QQ)E"T}H4#ԣy20u5 ? ?S6nM "aɋVۭDwBOܐWxw8iVڭ1> mIszo^\'D^MlҒ^C z%:}ټW>ؼI?Y%|~¦>b<=$K0'#I,~ݍ`Q"A#px]ZԏvpFngIrg?0i^Mվҥ`V^زz` K#Y&w |z o\K:T3GoA(V~G4/ȯ+@FM?'IZJCZc^`8a[,[{%cZ( +!VB*9sgaѼV]$3hB";0 +,(/-yld:;6i;h^V>@ѽsߎA!ɪ]ˉvf5gZ<&%8g $=b<(i'Q˲3CQ>_kEQi9F6jKj﹵mŽmD.:24YRdqxb3X!ZF1 ^3ش @CԸ5~sr!k6?))p]v3sμN[9~c?ZF~VRpG8"t;j)Zt  Spo|"9hRj 1lF +}Bbi! C@ +@cX: Ed/^3LAUErUc3>abQ %n#SHv/c"YXIр9i=k/Eib8>1PIX3j",:̷h?(@DM Cz%\ɘRk_#I\]D[ QXL?f|Et~DE2TèAG =[P?RzzCfAwP87T—y z";VIq'V2`q 6sgtڟ!]GɔՅΩ yJT},x+T!gBt$thjԐP׷/Jmԗcz ͒HSWSm_r2؍Z?%2&\Q'T1JHG`*@N3x=3ǨE%8IhL 8HEQ&Gq$JTx@I!vVEa[GJrGeR½8Ûj1c CA7v9ՊrNLuo{{|_[@O!}rÿ'znò. BadaH.3">"*o2xO4R\/YA{ʫb>RrȤLSjBd(ܠxcK`Y-ȡH&F`#HDɜ>\Q-Hv򹽫<_v۽ҨI)*NbqN^Iz<#&O\VCl'C1".VObtC9~FYO njJzOIOi%vu^%6iR0Ud(u:lΨ>)1O=F33Z%U$KZu ; + 'Ƥ)|ܫ[EsZm/ɤp8-5H@[ SUUR..kɑRg;] +NO\ex wջ8rkOr Y*1`Oȁ8mdPJhkQCR:M`-IkM ?AX|iFPSb_`UQN1^ > &aT>Cs{W rϮ'ڀҝ"r>J_yWiJ@ +R"ftKcyChgPg ++9 "Glŷ*#QTʎAhk_jҞB?Nŀά5|0x !9ʆw@BiʟrL tć Yfp9cd ;Ӹ(\J?iUC-z)33x9@F8oI} ]|0-AĒ5 P~=jRpI|R Q l #@ڮ3SUH)#dyP Н݇7&q^Y<_z47z-:-A))=S:W=bchRCG1@2ݎJsB[ ߙHWRʱh/#Z#-.׎9ƅmԠ%Li1K>扻2$XkB ̅V Q&Fd;'ӯD~980>R鷺pGPSIYV>U lE4 :b] +QY8ː(Aڡň3zSZ)I$i85Ct:wۖrJV6.=6=x$JTMb + #@#K&{w)=/#0,FV~(:~cd#x>~e#3IʿUn ?eGU?i4fzەU6M#IH/DmEAg?Zr"7{8ԅ%mBi-i҉SrbN 1'ڟh@i9HG%42|┷ Bgj_*G5·U\AgT8I==jh.FErP w`D5&n B']vkARI?nx Lb蓭J{^S^qIgE+ʱ$tBt;W͊㍘zB~G06ǂhx?N]ҹiGҽjߵ1`#^rPi.wGQ*<+2Y6L3t"Mfي`="n@w8"aL +S8gt$L 5F8r{2ۋ`O#Fz t.uj(tt^ob8"U LT+/l'irʬ\J^U|=dDo_@Ϙj~4S}"^Ez,݈3L Z@p)SxҜTd+*Q,13oikks/;ЬO䘢UCOz ewxr zqRЏ? Q-wֻ4U +!+>+]WL*N}(6`D'|L64( W$w-.tyZy 4T7()5~ QXJLMKξ2⡨d 0JzAq΃3VM1{vV/A)@}pD8ѥgc´q!؛Ny S5Gs6{Pv_[%= 3)A0kQjSVY]0sA:|ۑL +I${yN~'=\wSC8x߫B4b9\޻#FvEHNZ~b$Oџ<U䰶P}B )()ٜ5B@!ml5FajBo{*5[JvLa4"h]cm:D{s`4cOb(&sHsy?!֕X!Pz:TǬCR +pv 9[D^J1 +LEb6Ha?:1Z" яmI-TJVOaq܉w>T'O.acjd: +b19oo!B!Mv8^D6%5x>6/ +毘wSSG$ +±hF (3*"fzIOf&А0`:L3w.RS+)ډg`H0qՆC-n.%)! 3d?^uAT l 1ԑ |X6=̧#JV>6"ڱN;uT9k{/l(Jh. T 渳_̻H4BX+L*(u葉HNHņpЬɻhMHCEiB`$B[(ȠJԩE`]Ge;KS \%唜ˡHes1 OxcJ뵔t)g<%  eI/}J-!J;GH2Tl$-sAjR59w/?? ?"9@"]z񳜁C;룥2(,f|yWU ;J]x* b L:uBZ)v"m#]*VTyS(gJ4C+.ΦG_(G*-lp"3҂$3۶ +-p$(-N,!㈱'oVH-J9d=+KHW"{T'uOo#G@SNSJ 6zrȋtK +%'rQ|bCn'Cz4eiB~/ *@vvd2d:㠚x繡XREf↔+ݢWҰG\̙W|B%Y0CH?rСoz!u AOvm'h{J1B>"UY:Rԉ͘%\/f +]0r`E(sT4KIm*]V9Vl\e/%әbqFؾGKyoT#Zt!=ֳ+;dO{gLqGaDɥFy@sJF/3)x \҆jS-)\> VY`6Ǧ-"8lp! Q%}N(C hRIxjbY΃hFңHc(H=xjOI>Y_:eڄ52W g`Art-N]ނkCdtU;䈡sgP!8v`,)IØ;c~ib +$\0潐4i3)jX-̖~)C3x/ةz'UYp':gQzTQ!*9GU1C-5G=LR#f= X.t' !$)n8N1CV<k+ $螤)*#qGl(!D1c#SѯB7]Cob VHm[= .+S \*yP]}FFE&5rZ=ve= Q_m^חWH2!G!uFG`WfiWw<'"zB;0жhd݁D@+ y lh+QqOyHLgp ^ f`3Z*0n)Ab X׬~JԈefrĬmښ"]WًiP"cT# |4kcDi@EwЙA'Y^D-.%Ag4҉qfsgPC8G(hZ A`h~p^òEƒR/F3}+Yh5O8 + `6"Tge59W7`(B˔ eM2̭e_G䋑 jMx^5+̩d %f&>ɜ̬}ǻC`W]K!z~ 2h hp,4̐.RK;짋v=bj61~DU]qAGXA![^P;jqmSýqjvxC}Ex'Xmdm[Hڲ8j7~'.̉H^@)4\<|0,*TDQikFx1Q;, `Cst&y_xH찐" J,T1POE)q)F: #_Gx%4ʌ]O7hq) hV` &awT^z!.܈2))|P}7G';uF-5M!<K'i2Y4j<ė Ų,W(fy\&Nkz̒K^.4r/l` %lv g$pwe !:C\):?\SGAWh.(/qtDE$K3Mh|pK1Mhub{ZRWL$vۣ5(_'ˊڀ\RS{SZNW2n1"Z%yc%pTaf!H%ib؇(A7f_k|<4< nhYasI-k=N&vJ?r M҈+;Zsg,!udբ /NGf⎽*rT;T>frܝaaFnz{\nn%_:*Lgx>´YJ=D~53am5'+:( /<*i̿gy<RQ'Mt̞!O{(w@g-FnUWpg`l\adN$:!אV(.$4 r/W0ߘhO^b#!QCt/`=CfKEDSCϥz;LM#@wMD (O;!/ݺPg &ժjo'9(~m8hkʠjI|uB.{ ,B֋=ODu{zo70$J -@c ],#s͏Dލy/$0QZ"ZS3Eb9b&ϔWp亣禹T]Wt~Bt!ݝϴʮsF:u]J3AH hbGi9@}iJ_Cu _#Cpj{d5SrwTi 㷦c)wCvotWQeޣ4SOEy'!Rj!h܃ 02VO Z+ZdOsѬh#B8QjօRZ`=O9ޖ}F`#b#JO!'rґ.;l UΎfУ}8_)(BqtO F5ҿj찁 ,\TLE\p؟Gg<Gi=\#|Mwہ ZOuX&OUz/"P#ɻPbCF 發(ibʐ Ӎ<5aUB}0 cO] +xH~!_*^ !"->Գxca/-ѡXV +C8@DŽ?Ԯ(3(S +W9qV~G )GߕBSΌ{}iHQg"|m@W<4a +@ԀoN:f'?V]vcW2EML%+xy__%R =0+_4uCDAk2Dyblkl}&"'oh:FU@8E{ȣ)8♈.L:xi8t)ChMjD9 2z5 5D1BuQ2FyZ +$f,"̳Te=uWB|oUbؾR' iTe9M/"mTec͘+cỔťuaPe4yi;zuE6|&!UZ/5!܇Bi.p +L`dYMVARSS'ѱY!x 4[NAHtOFl!Q;e Rz|j](]{w=tgNgD aq>^N -Wq5ɼe;rA]r.="_|[}tjgdċOJ^hmGu_oO3Ɯ1P1aA^R!6'OSJXQy@qt'+w<)EhCQS ~jgcϤ3/0(WWb[ɻH*Ȋ.mA1RԨ!CDS/'lGE0LBy~F4o"&BO&ΞS!uyުCXQAsYGéX_}n>.*;2#=EPTO{ +NJeET-f3V+Fa,}=.89.#>A͛gLV YBuI%.ze@gL5n$@7D}hc(}؇cZE$6SX6p3g __A`OA}?ċUQ$$6}] PԨdݧܯÊvţ_iib.;Hg {=e@j'љ~A]'Fb~* +Ќh.HlԐGH>aJ|ՠ>v/ǘtE}f@Sq'L//bIH囃t,Hľ{?jYX.UNa s%K $hΊi`_G ` +de4*X@(g +jYz!~nF72 1=O=Wm'}᳴b@7)_`O"m ] ?#ƤX8tq@jdnQ]M7芒 Ǎ  +QG2/)# +! ivzgrv.qyyt䇙ړJ׈"<ɼhPwWq7e;|U?BV8 M4#naEzWYF2 ]egJ%LgT]Uw|zj B /VWc]w)' RzF޷!f*Hwp" [$/Όy%;LjVZIAdL=M%Tns&s 3y-rD:\!:<.H F2'21:yIOW :dK$*FwDx#K}*g {B΂fP<="oXY +SSCRgoã;#+c6l91sy(1! v!|<s^v5J/ 5D &'g&@MtR ]*8-ݴz(U#ÓQ+!UƄgMi$S={߹#Hnf@ +4J<ӯr)#B]OI[ș@1l!+VN![W&ͯ}XʕK㊂Cw\Ef!_`;W`+5li-TOQVк &:ҡw=(ߢiI})NDeiL\K'0ũ2rVa O + Ԋk)bA*NO#C4Iߊb +D%׀=LJx p]SeYdpļ``?sd9]z53 #w/̌( [WeVf%<$؏:]C%J`s׮aL6oY`W!xr f\5OWM8mTraNR:q*!ȧVRw30=oC?x:mfh-=*-'YpAHO2j]~]-yq))vw  jiF8'vh|F=@i^Beƒ޽TVEMowR3&n{xZjg c/2o9PPz,P~oeM's%r7-ƄLHHǟb\Q{ _?/} ]Eq9HE%Xx>Yzd1-)]b"x#(/]LtUA`{ _~[5&:kNE9X-Bx2D6!T]7-#u+(=("53:HuS/l8ϴ*=C*U5.zՕf?-d9[kf uL]B52E~[Ol@>/єثJ{KՁpT8w}#Sp=D_ػxoSso ӑ-4ZSzb"mg7d +fEKZ,l9qwB?WJWX=9/fᤨ D 7c.LC&Q%e OF\QoW],{{383bMMEi'- ++Ho_lD +W4@G{pFB4ݎ,=@<նDShA2gh^` zn%_KsUbqM }ÔP~},̬J+9*mSYap ѹ@v!aS+~-L;5"tאuו8vsROY` hHMްT 1/:c5A?i*J%o1Pn3o?"wol!T"_eRvcֹ4d%`+i>;.Sqp2'8b6#xmٵ;{"}(٣&1l'aG-]BhO/w,>l{l,sN! &]1sG@-!![I;=BjPT ~іn 锗{l$V۷HE$v2s_f) ڑ!'˦;4{O#3( j!vߵBrђ6qqN? ֚2gdq 뤰U-Taq]n{>10yF1knU <L:*}%/GDg䨯 0c&zi̎QM)yor~/i1aqE1.ޡje)4fי? Z0aP #M+77{ ]6B_1/,\+dѿE6B <\9I7 ç6XW?Z+;q|ɴ D2HƨQv5mSlPA Dҿ~ΞT*=Gvm~` (/ AЦܠ]1 H'!6Z{8 9Y:ȭNz?i_$ZAWmH +/Ia=c%cHdqTbLW>4]17uHQQ]$bO?K]ܛ/힜D*h#_i7!X +Gr#P +ܷF'IzщEʨ~9饃 W + `*$`bX" ̞\ߊS1;>##y% C)Uȱv˥Q>c cTSHg 7b剛x p٩f[2c^ӋW?bt +4xVεY&%e))q +߃(o2'}Kz4ogP ܷbт0!;% ĉWnF4ɽlqugÏ2!;awp:}s+iUA7BjGI_1c*֮"1KO"n:@Yetԋaa{0vb;V]c)&H;C]FD4DBU.Q@P :sgE؊rV/)#@<~yq9%H<^ǡC^U`!fz˥㋸ҙCGs?T'7,wG1?Kb@|7Ӝx5\3t͎/y׭ĻdG \r?lS,kgu'O?}khe#fBhZQ}dwԭOu'JOP8#fud=,_;j.b= +[!1b'O-- 򞚖!q)V|>'`hA^8D)".|CMRK  PCZ + D;(4pM-T 92M@!VPQo(4>k7\|^5G]^eHkv8 DD)[@@lҔΕRm@+z-:c00e7% "ȼBĔJdoO )S X'F .w_KG8uQFZpO#хקLW@8I]yJ~E:2A[~p L+7[:1fL!xdSPB'fN<~5Y_P_]a8"{4TP#w~ DԠu;}~A?.{H&!9H9bEP h?m₍ $'YCKJ=c1q` v|L3D,}YSl)dӃ ggWܕw~#%Vr:5ܫ%1&`#PבCﯱHEʑcnj /[^sԓxN{ |R2Ld) E%bbg¥G . |T4cNLLZ ֮wԇ;Y9b5A%i4Zׂ|L1D4D~#%^cu jHOSD@.I(68101YB]Or)5N㌷'w) EfJ4 +EFŽo{<%Np9T@. w㝹7褜*_Cu(sJʾv)1FVwO5`=[78*lJDVv6O6ZI{3l(g}r?hrM(>ҺcǬԅ>Ңjg(+^ILK(4,3&]WL&]|,f[Akst'te+`phe73 HPDWy3zJU%QbILj'52+Dd]6m/2b??gyׂC^tEZ*aEjq_^ueѠ GIDvZzAL? / y~a% Z~Fx'`7͎`8wO7 ]UW3>_ -K͐H- l(I՗$:ŖGO ږ1z 4F T3B *eb[0FK9_AP\׉o~B`L%~cLN-]&j!xK^y+ݡygC3tW! jY\xp I7N~teK~cF|P[,CxOkbBG%nT"~Q O_#?ܢayz= S2=\ie3h]ȏƮkZ-NVL+C5\Q¾qPTB諈 7v 5@J 3?{4 p?yS]9R_: $'CD{2mYW@L [R"z>^-D&s')DA(h_l{i!Pҟ}j*qHG006V B} +>%Uv= + B]q&ܮwwtOY/9ƀOrv8L @;A9K{G174,t! ?|MRSr6~qXĢ)hļ㫽wnxpӘX|5JT䣤zK_bI BbN)C>NO;cp% dYy{;)4ŨZ-+QxE>ٮE&2M!1/9`F gJ(ɣv/R}9Պ6T%C땡==v+a?BJmAƠذ#dަE<+4Wn{ɘ`BGSf\TfDt_+]%{{\GݟEa';T-KKNu +wLm5( psx[Ut=B|+xW`}Wچ*B 5fxi Ef$+cWE,?c olЯ5%AZ6x} М1vK4b#[ۼz$e%?¨SaD@;{#=.-#N +z^;1 9xb Ŋv_,3jsDud""2whn: _FMs j12XE3hxŢu"Clx=Ȓ֠ ˚y81i)4~r~R4dO\M(18fj{ d#AsԭH? +!ť'\5zB Bھsa"`YY('C쌊u\f਒||‰P(V1F;+T"ii 8#YxpKKiGfgZ\~qc$b5$OBHޘ[ <ԣ&!4΢de폨t.sɏq<״Ȉ8Ia?gg4;X JCZ[+5?.Iy xXئ9cy S#Lwzp)KI1q !`_Y>!`Kl9ωIU߾GhsqkTWd{5iGOY O-GQrg5<ɀ!#c_n4 I$W$}@Q_;tqR^b +YGk2/fg _wFwB  콊{]o3i;uxx ZvxE-j4]b0}orGUN(G;ԞzP+zh> ͱXΠS@x#x8[3CgM'A&9Q4{7P}=^YKN, >!N +$5L/M5HŠDOI&"_#JN_ +ɛœ T4G?;L*G(cO!:GxΝg.JڨuuF Ph(; 8]zTϝVO1[8},pDEv 'ikQ̆D +Wma#G-I:y(M $>4%fM#s + +ۨHH)sqW 㫼{!l؁hg>c"xTlFC"5qvL#cIuM]ȮSC'SoA ]9΂^.Lix{;y-T81aI+00fhI:CwW JAWl-F}3J)VmwTG3TX he?d{œ^uyǒӴ7ΪAӫb';!\gH6gU#(caQS*,l:*6h?O8y;>BW f5.*HB5)rGUi0@Wm^hZ'h,+U\OOOОJN|5B6 i󖅒Cy Nw G׊Ǵ(gd* {_AT i0{HIVs>g@2_QO>aQCɡ&OEʒZxZ՜2 Q2RU!9ɰ$ +Jk +"2#Tmt5hzeiln=š Qh <[x>b_<[WM&UpQ\NJJc/lЋ$S;^͜U:F̯[0#,j_fHޱ-uw#C{@F$/+!?t@3ZSVj߻ 0:mԀa0Pfc0Džɠƽմ1#zj-2_2Q \?" ]ZԯAJOl`Ë|5LdQpɉgf(k+j [ 95TҧE-B1 +#eIU63YbHWI "Bȭ4LnSRsdI^^ɐp@H&y +=NU{JJC~M6_ +!t^kV)zR X  Ե,h9ާz܆ qYUظpgov4t} ?eL3u8iא81gNw-w>\(u^4j$4%msB2$i{׈IKBs"ݽzm:QȡzmZ#)έp׫!ؓ,έ(]հğR=@YjξrJ2E J| ~M|u#K0%;9#=_G;bloQJ%yDpI:u[^+x7YgdjT#jp|Ȱ' g=0 p7D- zǞ?8n +P$&Ԏƀ@t4Vʻ)h|l8aZe`ZV\{29O<ܿr6R.Сo) F$ZC"hSXWj#{bx(5HC!MH%($=cm1YX<u@Fq)0+"$}bq+-IH UQ&zךg+H9yW:|\|jh2u`?әea&롽 C\r`ꎠ=MIoϿK3P!>W@s>{8%=Ah쨍'2&#?!|3"OgGc[Q,"xD|$vi?z'[Y'|m26&`|ޖxU1*Gjg]cTy|W ](߇_"YVj X2O2tdo`z}ì¶8X` } BǤY${pv m#8;@5A2Qrc]AYIov/օ0rEI* 8R)xC%?^O< + +o~T<9ف2v@㌐9ʨXxzR7WLVt#?xU Y"*(*$裻&+\gpx}h*!ZNE֭s|cPQ|?fovlCTO!X +A=Bd6MgD(7vnՍk1+:AJpA;${DVp:cKg$ɹh*{.oP)`_?GW|yɆ"6 RDS8ߛ݂z\}ocv఑0N#6ύ߸kOi‹phwy9G;D1J"fi=apHo Nq l0O38jl +hxѽnBf$wƫdLt*t{zP1)QC5uտidڟyif\L*KG}A/1˯}fe%nx6֌U)`(͙: ڭ7g}N ^us-՛r˭^?GϨ+lGRZHC9z.a8?]swu'v/B8}AVA0#eP@N+/y#E @u8}PWncg'VGbI)gQs3˴*N ʾr*5_dŢij+kazDĊU힟!]j +-+5l v!2e$Oe#beR'+tcfHD ++N5usӯQꎀ)#:pK$ ^(C}mF/EPk4!Te"c!vpNp\=X,V`+}XwB֪!,`ņ0o7Bag)b;-AG*ʳY0{..:/+rj/Q3t^K( TgokT#AH:EV ҇2\#պ;ʙ@5z0K(S.<˫ȸE@ggXayAԓ@OvF0 Lo[K#A QO\ NU%f+i{pɛ:{KML4KI`Hښ]/7"):q<%zqv{"e%[]#c~˸;CT!zv;!ϗU1K`HV}z`xi}ycHk.qwG:jM O"",.B&+z7 b9)$#R.Nx2LMА"W0HoL?EQs*Ti-T}uǮ}K)b$DT珺ãS03j%D4$h|çTT?dyW,_~aӮ:Fkx/kI?i$7gݗF<ӑ ' _&#1LHo_[=W3 PpSVPZ=jD5_&R-&كāX{ 2%G51)SH_g b(&\;7 %\J4$ 7\ +L爖è.D'}"%KkԼc\xU4 @cH7`#{"ףT$Y4`At3pw܄ڸ@CJfY'~Bh`iFpGm1NcHNw0?+$Ѿ&˻ +S&_52iQ^iCKHv:IP˻tJm\Z|~bo.C4fpB`ej4!ӽC'HhfyTP΄ ~Π0J\G!o$iɑn{Q '} ޭ~O}EO)Cy=PpY:~ }GAMIQ o^K|yή|,ڤw:`(#Ɯ+=B{%c9#}N\t?|Ch]z +%;3]'uxlePW{K#Pxi[.L1S)0B2o1=4YpWǘe}q}YIN(fo{4$O+%+%zC: +7iN{,h_gb̳h3a6wMXVźy@ #ԧs:tBSMdg!DHV:9y_"~ihӔKoSl ɟ/?Lr@BMd 0ؿ3eԞNN=yh蝈`z5ah/_]eF󋎴[O|58$?gN/׽ЍRP>B޻ē:#КN!R(e^P'le"9 !s fQm,0w3$㘼E7pa(IH,#O=c|cAB… +^dqS_WEvD +]YGᾞh1C:'Fd&j-_dإ"^WOQ +@yW倫TNw&ĞZ&oc@<`mnjBjL 1Wz{Y_ɣݣ48;sHW&Qa?gR"6~;kM`2ZVc$oPS`ݡT\%#CH2ĕ="hŹW'Cmcx_ΤZ~( ziAX/G=2ߨYiWs \gn!}d'91ڛ[2NvSH=Ez +ui0r')d2+ Qn(o*qJܜTF}`3F"@qZ\Vl$;E^av~s'V@ea+")E!l3 ϝ +j$l5BMལR`rh9=Ep9""LOf^1/W3BRRFeMx[0HwHBFayGGMJL/P n&\B#za\(o*HAa۽QZJj"v <z-6B!!YbW!n8Š4ᅻ {l7m_[%7B;*y tM'܉vvÕj;iݬ`ope);xG5d>gt#N^?g/}'LKn._7sCXfs;ꉎt#e.V32(~{A:8DH]*lܪqzYSvŖpBgZ5k~|olG_p3n*#tT6ꑦc3s5- :_;Z$Ӣ)A.vQޙ9ȣvLph"$/vO28,[#J>Ҧh4b!wzM|f)<_Thzɢ`)o᧳4!M;wzAʟxݟDr7fr@} _⽏%|/56ypZ %3j~{K}]Lm7B!@`^T"!(ЈvD:Sf^Cl#A)Db[%quI +u8ސ jCMy-h)ER3Ǧ)Q7 $}Pp0U&V؁ \sN|CRzf`m `;'}0F*;-q>S!pd@j"J;8""~ 'LG.'eDiM|XS K{ *}/4?GF{|#h~$>9}` Qkӛ 9̈ k<^2qt}`>PE9δCN^eDl qIAU뗦Pq7;O.z[iOG! +O.2w5pUu.ͺ`[6"//'b<"}~-b9Y[gegߧ"4`)RHup q?#y2ɕK4DE2UP\-';D`zَ`)ݖaDEdhoGbEѼDRA!{BEwF1 gLyu#zCl #F  þ[*(=-tSȡtDV:|lD:2cEsFNTU~(ui##I@?a-שͻi +"u]t]oWpN] XkJ}d`v`mCRI z(@Ժ#jr}y_NÜg|ח`(jP-RxQ2n-Jgp\%<ⷼ?CYohxUeWYU0YZbѺ@oEYl,-6Zub[Q<㛖djHOfWRcС<TwBX8Idل0}:ދb`B{〲u8Dc,6EGW=b|4Ost,GY岽&UfߗHdBP6]'˫@Nο3I_ Оho57zƒ+az&fOPb]jDzsLᣧZ弩b_^LꚐ$e םvL1H:2W`91;| +2:U;BwѰYOt5Xg{o`wqM=@R=RºPaxc"qC+{,oI/.>"5y"&= =|D,*Q~_sb9:(?Q3}hU׋—IK?Ƭe({ dqnNl*0_ 1 1[?JNO ztLϰn/& { [$%Kˤl/1[H0G9"ƠjMO +q= uoWdƞ}<֮#**{ɔFՇ[A%KRK1呚 ~_pG<@δuGA/Xo_aGWſd!kK |봰]}Ta!=@mTbp?+w1||%ScaT@Og/ED--LsA,-UDž}ׁPݧC8#o>q6*~ /}CXt)=y*FOXR~BR1dGINLxRc? sa|h $`7B42(m-gUbNR+ɽ0I;d/hE;8f#Ы*wDC5+OS#w!/%}+hޣZ^qŽi-էj ܠ/)+rum"V_}mQs#ܙ m#&1}!2Йp%= E]}(# ; +4,Hvll3S+Z:(ݞ+@.| 2d'. ͎5^fUɰ +IFJԧ'梆чl\Q~~_ $aW[ݣ8 UDSg(Ei}3P&ȗ@&S6w._Y' |@I\" ]vwEH%wJgfBӁoMgSh3[*P +8=NNB#@#xacp|['ut䵐+#bF?:2SG(ULwl +F@َT<)@cy2! +8Rg.e݀GKi?ҨsF +9pϭ l>c/!%[00|y"RR8_3nHdM0K>q:2{ { mdzwzF#zkM7mgܔ>x<C<8 98ԻPʭ6UV&( U ,#86a0}Ň#yD۪VvVDp 鷓& Qp+WS}U6(_%m'|%ƉЀ{/N9T^ظlꠜz M1C@̧ ˠSĴHQ4j~ pX*FW5\+-{h Oʱb΍j/-2%~JGhbabkLM-T.;'E؊͛:fB%TŜ6g5 M6ky +7dZhLcqLVFAΣH)z/AKWD,33]#q%h0X;VI |GN%*UIcx/O'[iRxpIUwxp3V6м^y;W 3 +޵cVZzK׿o +>cgkPwk.Y ~T_0Z|h6]acpb4HX 8{EꞭr_"- +_3Q53ˁa7AobDI0Ij!}J?R{۾<?_AI8P%`-O<#إR;kM[xދ>o|7ɗlA%_\jpGzEF:Zq"WgYI;&bVP.w'~*oeh[ȱsޅ[Wigx/Q(}lx e;W$y/ߵz <rDEGɼJ` L=FAjW)NW4M'8ʒP1W[Y(Ǝ+̊vc}+.u8w7>{Zl@/Vw9JN !Z\%1Ph.ʻS"p+ML9)<Т"im^1{O]^7+9)WH~&JFn:X>/ya]̎Jr̖"=*АY"`6Cgq)_]~,&||}D +V9'@|cV,Wr8OI;"mE= qݙ뿥,W/X{>dxv +lHhƌ&O Rԙi 0E |E^+UkP3:W+(b~ v.XjjbUqG'&4 j 90; I۫AM7RH:ynf Z-Dwz5R_-AiIE6G5Z!30-?,(BZZHc:cdKlx8סJ Gl5Ɣx\CÁ|]ѫF'p )G EK7IcʑƑ'}+Upu*0ػ QPj *|regTD"Vq~d:Jo;e;g݄=G\א"[7X%<+aOέb#3*p\I*34ٽ?q(:ln, xd9KQGivzNN7zCJj*Cd9'ߌ#~/EkMr4#9jaR5y}xB<i'=&v+&b±ľfC+( ɑk!Qގ$v{+qR_r}M]P:h3KiQP/LOd[ԃ1ASv9/0B$( s4{ds$CPwXpGG0",6~ї▪a +mNUe'/!>5HMM*,^=Z3XES Y GpVs0'q0crS@z~N lyu ܔ}=gU" PɼFrh!x`^ +&ZZMJ3wqV7c|:m6^_izWއG)P*="S7/W(*Pɶ:@ƎwԷW2S/. ?'?DTUcft%S"ː"`V?t#*s 4 =DU&1nu}G'0Oamb)9/\zl?WB%NdX=>'?>ʶX읁hj;v~Q?9[nį&=g+6'ro 2XDhl_- +G`KRA:c ,83r)]ϫJ +7 !. +Q[= 9J#L,8y@ Cn93_ +y:~X>zoDq\M^:O[JTjoՖ>U;Gp,V ROpӹn14| ܠ~?XGmg"$W-J o=5D}tS|d/I#B|cRq;Ŧ XQ̡~fq!SS'GESXӅBO]qE\oj!5 +p*N| _ ȀWUĿM#n&FGbE*@8H8wdIǐJ H`zq9@&@IDI'j[Sa~wHHf#%t +fE8ZBu\jWC~:1trF<'6n[icD!#jAʑ,+ȓC`u|Che0IdJiGy^dPlE{R_5Җ@#Uzj96F9>x"x"?EpҪeqw+@/@^P R" RikFl v[C4,E_H|hJVpq [<λ뙿 MD + _TKvJVU_|_=,@ۤ/\9M8E@w(k|M\nU=o_NѪ}7':o9(+]㓝D}+^X?75+x] <@DrچY XVe1w*? +v +~g?lN!!Tc\":#QxlF[Et16 40t9_Y[zaWǻcgo >}ѱ?~;-! e2K]g&ֺ V5(*|"",rkU+\0!1EV}qDT@.4I4X(4"-.[Hp1V NR@8zowpdLwJY$ VC0`ڊİJN)$>FL[l#oZ!_w 0fɢµ +jnXm_@r?AZ:h䟪5drK(IKFұh#,k`w8gSVfwAvN8IۣaL +w Cd3Ou5=ReɳηҚ!< c[8 :,34p'h.Cn<%)s,GƤW2f"j(l1A3&Z%tM oQbڒ)3VXԠa$ [Y4DL;?X%/34Tm>GW iZ&<0[n'7-h/&Π z;]7lA\oD?m, QR^aZ| )UM#pX*O 4@; W.2nt#Қ;ɝv3YNLUIӹtf@u'OG EhF=uG*[EǠfWd*|C7jP%Z4de> &߀-v QP<PU*g-]1Ho["y2?ߘ_VdO_KAJ5q.G!(whfDu(RZj;Kz #y1G6L,ם\N{9(Rqa 0{@v@1#T=yFeP3k$g' g $**OP<;էSԱg7z=g} v|nQ`<\Nm?CF(ei iqÿ뙎Rpi#SG${rPuz 9#gܙ|̩1G`.u {d/)[E;DݭޱH%Ꙡ=qFuojwv|JrG 54uFՁM ~s֓CeL +W5gF+<;st_ p;աNq aG`г};ъm10K@ =lXM)8?`mG_!\)mL +yުu%3k a݈c=M u%2J*T;ޒM§x|훃a^3[Cp?|=ėeqc ľ.旞7_r _L?bGQlx!@&3{Gg4CS^)4hnPʉWq7joBbMym +@"p-΍|,#0dK2ȕŅ,:Ҷ2D}_ d/Rg/&Me y<99Y'`8Xa2Z;6JPAucS߈/ RmEa}ycO{,Gz5m] +}5;W9 kJsFV647{q])R'H_QKѣ>0t( ?uۅ㉲ʍsG|P.ɐqӛOJ:.HQG)Y#H9hd$W --`NJ$'I}d%ҝC ]ýv4`x$|`)Ҵ~8yd2/fz /A7ONTgg*T_h*efI`(]{ +Nx3(@=g%_gBRLKD$I1$bJckLAHBϴ4R=,uq'[60Mi3OHa(tQ%X .pg88;$f+h~ޭGR 9{i;~$sRLv'CY!ʛzKM9 G6nXHA6ىGӈX;zԗcFڰKe>0z̍ά/˝lGI<)Z%P-6; &%RsxGLݗ f}k'~nd:Yҗ gг"VBн +MNΏJ!nr]u<4/̌9+*887W]S⦆9;0OViy?U2kPR923*k ƞ,ɳETeBCja*m g ).Cj> hj(},{a/سG)V_bVuۀhmP!T+A- log=W&0d +-qAIJngDng%C9f(w"z"ήOԝU1S5AO|P@/N^~iG8c_Z4Gn+ +½Us!L,U:.48Dй+&,un7C<2\$l,Gg̝Jqw@F&-?O`n&b>7ddžP㳡mu~g葖cMR)^P!<ʰ7VZ+^'o^XV|}ޙ~G$kX=,ĴФ\Y __Š~#sz@rBkO6I?Y~- ##V4FӝD +WTysPXDSK]p x%h{RHsr&"(ͅD|SW֑w&0;ofz?eu_cJI6PAABH%9|ƝF<סéU1zH=Ϊa=_OeGK ʊ'bg 0><2CM{2 +Ć+Ar$qKԹEgGˌHJvf Hg Ζ(\y@Ãsilv/1СtrL窕Q9$Eq( CS}NX[ὐPs"1+@D nhd~cn-;!u- jWi%?~q'qI?K~T&Ț? +;I=W"Nv!j(?%NYRݏlo2:g)k~E~QI:nYRHdԉG?-jQo<qF. L3ʽQKJix`2}n|/=e^}zD312#HuD>{a`X-ֈI0N ѺmaP6k*>:֠#.a}風O;clXL ?Tj|AVjNBA5k$@)팄ci㵳VvkI ,~%?PAWJ{&+pOo|/!}T.CgM|޹<"/iuOgn r_Eh. +m)FøYsǠv?;}dƮ}#hvJ2s*/:-Uez䖤sBe>D|Sw~psEW<[qy#n=v-sc넍bM5lj}t"g"e|;kOqjRTP lwTkz+ƣqFYuac[CEZOb.Z۫F7ר3YhMNo)_gۈwY2}veG˹@Ju=es|>anM"Grr!AL;pU}20ӈ&SlOti,diImqD\ˀnN`gr2yYA@{)]kȅA!k"^un[I^x>`j w0uK߿v`~F0˾l`|RC4++h(3;D.&#Fxft+eL~H?Bb4y֐溈V 0!rJ*Aw}h^kt"J2#" ;bCIc<1Q˷^>G[_N[I[)r~ Gܳ:(ujߜZrGmh~nDa2ѡ[K =qZ\~cÛRNxG80{:X CB P؁ +Ϩ`%Y<Ȉ|^[7D\# +ULi6S/Cȋ=gxH=x࡬Yq]q4-wP=z ΖKh(Y.({n}v`3Zԩqp\ +r(qRR@ypLvk4J{F"u4Xpa>w]QLw:2f2rDVzSP i,l%)8"Un8XVj+WQ7^;MMEj>ub.z26d}M~ЯWkuŃ&o_ 3\V6'5J!w9j%tߊi[uEuAyrZ{aħ/yR +qoD'&kBg OlT8 +#9XUs0DÅzT}1} 7=g榞JO9J*G|8QH:e`,Ky}Fza_)0˯WEˆG8*M LqkKG=Fg\zSrDҒz٪w:I@ u h?g:*qLe}HؠœzE~g؃4VGϳ-.@vȪ)c:ˇf B HDqoYLĶA˓ +b[ǓUnN87WiVzz'>^?lN9JCO)VICUI!kdHgfL%u)t{4;5Q% +TvEsS`C=dž Gxlrpk 1cxYk9=r%<2k"Zr*D  &Dv;[wBK +`yaFi""SAr!n802{(YJàa$k:ƅCCw5cǒ)-l{adz!I]9 e)̈́[Ba/xkROV~GdW00'L!З@g센ǀpmR +>TZ zCpH,Y53+*cQpu/)!򟡦7S"J,g^0iG$YKoS!YVJ+{ʀC +x0Z;y8w0WCS e(YŻV}sZ]rTpce$oQ#4ǎyf2F9ú(r4]eBĀ&e,#KUbyPn(^|߳gMogֳ9.΍w+LAq_<&3dhySwzH+?YyƝdCaC`_Lg>M;bK)',iθ]K_?e򫲲'{h*[ kW{>]|Wl;QuM+X@^(d + _kN#J:xD¢1{H5W1å}_#D2g&8(y(xm81g%%ұo5 }i"c#϶VCGPlu*yҺ  {$ џ!,zL0R? +ANf:.ʹwUBۃ58D2Il/I}x +8} V{NuF(%9Y~+uG貤 + a%Ufԗ ֱ@/>(JE)W|6GxoAg%rb׬`?{+-4ry\C $[-o^Zi)W~1lI02MtII^eX1/rUP6Gk"xJ Ӯ&o'ɰ(Y0!ۡj_wzɘ50hݴq1a ՈwfHFT2 Q +GNve䮀'!Y?C$m#by!ZX>龧ݔYDq|2~&Gu|We>p{ƭ%wƍϭRnʩJ@{ w#x55w{^ry9q(hE!$)Q)ϭw l=;[ ]Gz-z;G ] 7"Fz˙X,n=gҟI0K^Z [s1vV> KT>e{`}*A7^:|׵}?E_ KzPu>#$8M9P8/AZbiW6 S?1:%)&1Z@&8J)IdnP\]:28Gn ٢֩(lTu*AfO G(pE63xGXv(]u&-Z3)aPВI9;J"L͠u0|a𙈖7KF +h{*MtI re +v5FYeP=C闶\wU=A]XC ۶IVU@_]%kvcxЊG_,wt䌼ʰ +$,z =yy]8=s ,׌VڲT3Tp>Xzץ[k?Ҧ~!7Y Ka)8H*գ]B 줅مGGK Ebhe՚<8bCT P\דZ`9v0cD*xNF~4ǎ(gH$?E튉#Qssd\3\w!w\rx8ENi'B=8a # S+VStvK*hֶA!-N*F33<15"8"3;O!><yf{]ol>N;/h%5'?İ"4u"yBYʎUZpvE#ujϐ@3o-T=p^י| u' drGđYhIkT,[͖m6kMrg4Qyr9%PPTPIc8mބ@}hiڮ*&/}nZH7HB:ւ| ãJY4[?G&p]|fVAJhd\)P7خvzt׎tP0puN%CmOz=RH7Y"b:QSEfSX+T|mxE'_" bΦ3zK(}da@8Hj$Ե ma(5ۨ{뤖6WKThGI^ig9hLWCQ|y}05>zhXQ[@Grj|^RM藬;S׏"5XSGCvZU=HZE`7hSjfiaδn#~kG *zœFPd A3EneÅZ i]$ZiBP: ~<LuBrb17_f-aH\}FD>S#ȫ#; L܄6s L8ݣ̟ε_eѾ+Q +BQbN@,8 +w@r5|/*vYI~(m!WBi+z :V^:y"tbPq(ι/s0:l7>BЯdԢ*ҐZߺdz˽Yѝe`Gho]E/H. iNW ўlp &>ՀK5_>9U"}B.O 6L%Dʒ-數y>7v W/&CSMSh(&(@=:А2A>&h^q!۵|D_4 z!w#:>Zhی Cg3rmhώ^;.݂TA0uhaF݈*gP ++]km:G;N3"a?SBTQ bn ziKE_%^DwoV{zWB "XՎsZEl*8S*%g;UN%ßb43sUdxf\(\w3poZ%,;Ao-˯{;IGԏ4ܣQsڇsBzuֵʱ3Iȍ#jK@gv&1"z$Ac}3˶VzAbYZKz p+|rE1$z֝xbtnjYCFqQYh,BtjS`Wrŗ UnPe@!3M>_!*4=ahRg_{n^g77!cl&Q7'غaԓ$=} >1;{?(R;g";'u؟6-h֋PU¥h"% YȣgNdSYy3r#5D4୆[1`^-eN5'ٽ"9sFwv=ʱ/bztFՄBʴeb6XAҔ_$7B#vB$êU=)sqq39jD aЈN$AHC&2﫿/ilaݑXS#}>E1kfYFr|Ru9QX yU ەo3=&9nSzRg2?փ G_ҍzB[C+2`qU$}'H\4-v#h$$|Fl'"@gq҆J"Mu٠41+"A5jS +GhW؎ 3̹dʇU))A?r:[=r*ٞĨ 0든PѨ!Wxm1TjbŁp A~;^U)I(^Dpn +P E! ToR%w//PSEƶE)& j4X&!n}T:b2)=hLkȁΎH^} +yg06lS+; +&5+I, a%zRDJuHm|ZʪHԙEU+ +)Rr>6O@PQ -(H+ڸk3L{5=Cf70Qѿ܏Sλ@dt\ %Ԏ=\rly*cKw]G %/ ˘4+:!qBam;KK͋H暷w!ՖA(.1H_4Ư9<Z~DM._N- oBFq#]j|cSh5+{gQ޼"P( ++)zoLi6};u~0#=&#+JYV.ї%"Nv)ax&IC.NPI$%SE˷w8@Ay}Sm BP³\S 6U9C3i^:S)me!+,Ĵou)mD]Af{Ra gc'?Dq#T`]7Z>ڝGhsnLFO.$ϊQw%_\˧uhtO~7}7ڮJ?0&M 10ّ-3eWzv%qle`+ljZu_ 9Á +N\b.Rc`.w4n* à+|HaWgD"=ܫ%tŞ$ %SqBt!r0?[8ոg$()אUAI\$L ֩eݮyFEξ;¹kJ|zw5sn< gz c'ބkO l. h9V@`OC'LV:)ݡӴQuG#iĠu9iSkrx&eA"Ra{p= GN|(ay#|Ό=Ǖ냹N3o'M_HO+O}l_wy̔&Lq + U +A;3Hr"Qbȩ֙q-v?>ID/Al`wf^ +!-2lirD &A!bp,O` `laqA&]jH}AbLoL+PW=rS4jS+l ^wV8ì tH&:bi^0^ t!gXTˁ/q`R ;u U$>~TUF(`Nwv\lP/)ίR})}3%/T\sEGX?1Ê2'I`(g]|stI4[3d˺_F"C~C%"p{&Sdz# XL&nvdN|!(rS{/S @hhEN= + @,Q2W)DA^/":k'{❵<7hB#@ZWֽ +:LڱEuǠYQ8Kl)]5Ĕɐ#k:!m +gNش:.RhABX[#25%jIOmIhSq=*,H񠲖H{=*j#Ie7[*@)- `}J cA#,.EXeM MY0obU q,EL?# Aq\! .m##@۔v<YxaY$y W(9hfyu1d9 )B [N5TEx?hZ Qb)g߿?UZ4 +RcϢqFf)H$n/諲eYԊNcjŊ#VsHwO ȟarڸkm%t@5qrh4R4 }tvk!׳& m}Vyʹi)󣃾7gN(UA٢J, +d s*߾X_ȪE;8*PUD endstream endobj 50 0 obj <>stream + ^U [9(Z@ڔ3iwd #>h%𶆘jY{!eJ^1I\߬}F+%˚^+a3E0//>y4 ;vT.&;c:#@Oz?Uh壭7sN)R\Pt\;C8j=C0g^Ρ| )(FbY(q\(әh t1HS[d`Z<qTtmCdܸh`(ou*o 2eMe)4\(NJ&zx%LLYyzD^̼i`rf}Rp>X{} c3dv y(93ZS/:|b '`37䊥8[ B#15[׍0=sg~*OJm?9Ubx'8rX_]LEh8LT(qEgM,B΄(XgAh9њCGz" %#i*QnԚ'Yשq"VˆXkWpΘ#?+dIljUfj0^AcYNjvWvJI8Ub@o6zاinjwgض)0\^vv9.W*Ql pE'sHFaZ,s>i%fϙފ_>Z]32 iǼplQUJo95OγmnDL,1$NNۯΏڵ :+VVv&&/,>e{)_[MW/h}L̓~пzTvWdX!;:S~he}+45HuI 5 }=hJzC~d?ð]7֢{E##:8JEQPǏ}T*<`zem1;%xOLz&N{VB +*X Q$bqEBX^[sTXJiŶeZZ^wmvxO<0Qr$uՐLֻ49o [(lqz2ZAU5(!c,a՝$SMYr+anSǝT+^" -ّ^zq۴MۀChP GFt)B"BZĬHU)W1$Y#&CF}@֊Eo//Q)(OkS-^ǷþR? ߎ"p.6Hd>k|t\I~D kH?IA,vfOG.SA䀼t"1K1a8xx:'۱?~pĸGQArt%*B{{jz/VC{heNfzɸ#rN-!dѰ@{y5Lk%Չ# 4]z1gH++yU*_#Rοd!kA%NSY֤o@)ms Q qc^O0z흎#G>Jʅ"L6k Ɔ_"pATOGHN%H8bt>vJX9ՁʹF^H h+3)^ȠhW>Ț^23V_΄@_.UdJ\llW7g5dG]M4*K}DO5^œ_\]qR`S_SKNȕ@\ޔ<5`⦪BZ^O("GD5 iO9Q%9_Lwt$`'}g ԣ~eĪ1 HkAaB Ur,ЉΔ/f{',?s}BwۻyvimA:HYP81XHI42dq$:(mQWI}5]m¬"2\omթoL[ө ½<۾RKŇ 5VIG&0"|ӭ1WTFAa qZ)= +KN6i}|S_odoھ;!3z uxmDkHoUC O{zGd~_?o;vv{}?!ǹALF7TF1*/C%Ŭ_hD[($j,C*EZe,;S!# !uI"+\u,(z2q+d뭊.wTteژOI +iJ:(,*%yak΋;SJ DԒvnd8jOjURѬ!f} ^qQ:SEGܤ(`LUHceTk brW!Yy+Ml4\he(tGE̻/zz~&5,C%W3&2؃)o}q::*Yku!\|b\%B_%, *4q@HHe&|M4\4e 3c49wzQ4xFŎbe;i]q3?jzEר*KS' +Pe-xzal)j^uK!@'ܥ,8X/%ץ>0 +-gQd~'KNk%w d<}OMv};ۍ K$D!t+p"_Wꂫ/ C)"2Jl'^'vOZqit>CZdc@RN%XQC.fW`&mBpڤq悌g0"w`io0,P-R8+POoR3Z-&rB`ͷ#=f&s[T l`Y~yb x>C6jV &( w3+ :uzѲtF{8r;$m 5kږva4zgBPj-/G(wK\>ٙFluk+hp¯y̒7GtEf6Q;R^J&{Ow=ފp[n,r@wQaŤahcG _">x~dީ%o7wfXX)5مX{4)V)_D!ZQ]x~ZT\)A^ٮ́4#;ݸ5aGApv}//F^I'=gEפ}ZJb7Wb\6E*bMN:Xd[%WCIJ@o CfK + )5Un3;nPJc&9lԐ 1U};)˓@jYLmskZo3IA~xhWoƿΨVuj3&mSʑ0f1ր!%WhZfvL Fd/\2zzoxsc(mKdNaP=i!~aQ~7ƬT^[_;_9Zh{jmrB8.]tw"!;K >ZScYpB^{ 5EUW)R.:Ӿ6~8ESٺUcACVxm.^ϖ3c/j45[9e?L m4ttF 5-J0~ Ճ1xy%)WN3Q޻W@ԄGlL+2/_ fgPRT'2(n5m>?R|Ƹs<"8>O QO$j*-AAkLy_H۰ͷj@DEs\}9͸,# +9僛gNAkmD2e= *>2$/Z;-:xv9ՎF/\%rxfCtQHnQ?b$ }>;s!O{/NuzzР#*&127f5a W2r<&Jqn `b:<$3xqչ_0=0ՠuWk> אt\nGn٭ ٵu95 ii#>V\Ib51i k՝{XDa ܸ,_ą&(ϧz_-i$)@H4hT CXezTG0m"*B$;Qd@It=3?i4DŽ^}0E z"^rҘgsG⻰1#7+&H;4qg.Ɉ[u-$q6#:2b(,QDy3~tRʓBs=˦`-4֘+*t\ͨ5[QQYjSɈl" hDTgk㊭Q 7OAMd/aNOiYlɍ~w{FLV\Ѵy(X !_>RJ`B/Wr S3ŷ/ dsU]c)f̲ka~E + n?F K°8a"b]nj)*3B8M>=|f}.{IF|jf!i{Vn]k- ʢ~#TCa䭜*wSgC=.?FZ٬O >:B)s[byزujh_fء©W_ U_}Mk_$8ˁNg!4Fb({:|19Zas3jp\"htL{G9<%zt=j_슄dCVVx`,_+k%sK@8BE|VqFDE9ԙҨgSW, +#(+`rIxmw=þ;qrɈ TeJ4h2禄;q +LH?Ň 4!-3Cxki$@sdfr5Rۡν\mCOjS#.f#t*Mj)8>3Ez| +jte+Y4C7~kD2NGfĺFPCsS<4^,-3w{ Tsl3Ҟ.ADR/68 +٩Ixw!#ɝw-2X#WiA4I1nnzxcYm +nE/C$5kol z_ϼwĶ"6Jj+:zz%Ŝ1KJb0Ū 7[>BtFt!wEG+[~-'"X5dpl.vA{Bp~s,&kfh:=}w i\vQָkR^p*@-H8r{;6DY_-RkYكS?>1ΦuՉ|dV|TCJ2C`PC'FV=h7?J!1d `o: u9Cgs%z7p]MV;B!π05vhYgPe"|o%.A$:D?tzR')Tv˜ խAk5BI#TYк!췶hTcCs{ܓGbPYU|ŗ$měaQfĠ !Z3d:f,(GRkzzT ~.Fs +AG BGp' eZPu5aᄊZ&KT +JЗYa(z0l3W>)4:Xy4  =nnB߳ӵx{)XPخZ$e۴{oY +7C+:g_%)w FS!ڶ爓DI_|{]܉UʌTjȃp3\Ft1elG\FMFbnj +ל&wh*rz,Zʳ.+yU)\Fir>l0d>,,80#} $٧xb\H[+Nw֡؛A;F9qS}ĕyz4ȑf2R'\~`k_, 0${PBFI@_䰂Dv)Ip~YET5GA@Xfᰞ!­U,o*w(8_7189]:˓[X );YK6j@HwAsyDpۃ<,HYry*Mq3ĶQ +t`\<@.|:W0[]OI"̯I,&#rẁlHF \ "+ VlWr^wM^(!0Tuzo=x#8+mW韅vL9G%m~ Qj n\^3d8HZ:Lw-0YQi0(< Vi. l<+mQ$qIKImEdz:'Elm0\x4OsڶYLm ZZ/nhl3;6 s-p{2,g?i&Ѹ ?~i|AS:&O#U8iTώ~ Cȓok,{f lpfӿI(\4pd0(TfL=h}9 "n lvzq͌!#,c AHhKJy+zT:]*sbO0<^n3*I~f.9o1n.S|H9AO[W ,V+N@%G5V 4Hm +V=HA5x`ܖfq)L=#JAh|Zyl7uBxS5h~'65W,3)E6e9WZ_NuXUt*}Ȃ,S!sWBZ(`#TuםSaN@Q YOG**FvnZIt' ˰ErR,GoaxX;;cL= o\ZG]8"rZ8yA~?)͈b1cWiB)j|Փ5<&!zGvr/aJMxd +DXba{!\6-V¶իβbY"lE?S0z!AF2δyg|;Pr-a{LPk9I+ԢGJ'x8Ca\a|*sd.):t.f:]ܰy6±^vU@~{PgjJLm#֕_bTDZDTը~N=MAcwG"ũ1f|rC( OX-!jT c|95+(86tIwfz@*lRԌxTיyrf B6ǎ;b%S ѪGEk~ Xs?Fo23?[ĺ@+>]z߬PŕK1$GW/.x=Cz6ѵ6CSD?M_kGtS:ygJ_A4m+ho{,tv?m"Cb)-XRDBA+okj)!Go9xi Ү1h ɡc]z+:L d-?TnQP $ˠgG2@ {ۈ&/gk6QRA驃 +tf7-^M!ImT7{]LBg)Oy48T=W+Vh][7vPO3GHzA {*i(o;5[G\R/]W#j#S6zZP% -Q߈`w5iڑ%ڔOGڌ +1K,T.@HiuO1w̓$^ҽk[NngzJpOfXt 6AE'N'ȜLpK0ٖ"GjR2z{JhWXbNU+b9꒩萀{};ld*AV),cr(U|]f_*Ǘ6iY@vpU%O;$lchڸSST5t/lleas!h! VIO1K+W4i#ͨf[vc H#47i>::.eh%B;I_(Pqhrϧ &d6)B ݐC:] a5y U$ Ĕz$,)z4N`;/CT9ďkHl_I*XzE'i&o_ +FDFG`.ƐU{Dc'mɅL#Z&yDn%#%$mmDtgTn~=bLF 2:1ި꟩>_SdvTNuV|t6֩9Uli~c7X3D+A7} XkkIL# WˊF|mXC =^6Sðh xz8Xtpk4\j߈iJ6`hЗFO׉D1)O;u]F/6ӏe]%C⦂&0VXM^,b"+`WOa}b,]0 +{knp}bO3'46} ǁ%l(sN2);QB Ph!9usbeDS/%Zh.xi=>J 6(!CnakC' +z~ſP;ӫ;hUwrf#oyCKA0`ueه:w݃| ن2$FY mPh{m;B?2iު{4vVxr\# J44ޞ!bL5!BȮ +Œ| ^\7A.~}!0z8JzcA8Wɔ(ݏ Pq?Wr +k G& .34F uJ'{WhFT[$TJ]^@~ZЯNCrlKK;g"S +Y7 a+[dpH +).:dݔFmZvQ5H[ԫTң85'EЍj?ᆪ{LW GlUvT(NZ`v#G,1lz"Y +@j{FzFgD Sލ^^p .}ik3:1tp*L,Eu,ֈh71?diZ==c]׊,IE̺PCј^iE COOqds=xʹVB+咣4͚gDPCh1 +(/ۃ8:ZeK8) 0H&Z6Z/^\3 0F +#/>/b3-^S>*!񠊐 9IvOT}GB[zAT0H{9@4ϴloL#5<9M+zPതKR-?=y@eRtMW $?`1m2vp#g" 4L>F9-# +:Q̻KLG;-eY\5stc͹o鵠}0%ޑ'Tel>h;\U_ke&̿O.9\1tVyt|;qتqT\I$Zeq5fl\b1Y hGZ5kE1)%2*)ܰkg|l6Qu w>a=jZ#G+E+0Fbح|+6uYw=:+*+-t_rggy`r1pZzW[l:_w)DF/cU֚7͡R^ Ub zsяN|=W4 BSXn#=t5lakͽ7]mQC!i7%g ;Y5RKϟN}XPu !ym{ @B%^S%ۼ2 ,!F[!'=j+\˭2$lLۨH*z83غ`,A$)nI;3ʖXbӴ]OḒS]e*[:`eFr=T>}hxMӱ/Ǭ1Zֲ:SKҌ^XqKF' ,` ]YK&b UsW#dBHݹ<^?ѫ<}~oo=F%c<?qu1&_t Yb_ =l,#Ӹ;F5xפm5>@qܓH\ AMktTvɗ; xQC%1|itD8D#<;L.Fi?C8::N=_dAl\>Q>b?w ~꼲m 4&P[pHv<\D<b`1KP(y޼ߟapQ[xakל(VT!iqh7R(8Lv4zfA$=kJu=2t !2ğ+0 ϜJsXb1+iC- N1pN Aꊧ멶 |PjaATxWw4@K |8 [(? &".93$F0h.Ag7H#QS<pR2H#⭚ny(;#y}~]f;R,ZYPIwlq) Wx}CE1ȄGE_+!8EF?QQϷ~dCg~I(P;<Ck:Kxun|n^lHǕtud܏l![p瘱'zdr.#Onץ4 "IsA+ujmאF'xN@RwD^B ~[z޽ +eedb?ޭJRp󀝱v3mH_mZ)R~[ A"n=&]qEna*[*?b[O2S8 Dtd=΢ +ҕ!WWkz[X F2%)X{q\lH&K*DV[hStYRC7^C.֚\Ϫ\Bbqx_qn_"Y8GybD»- ǰ|JF!${ɠO9R;M(xi cHP-k(QBzqWTJ e؍VpO~lh=^Gw ޺Gd1*!ΛX oogyFT}wśRf~{vNwzYZVNrYXX J|!k>o\f$,uU^KU3`*qr +YĈwo~WHݵ#CӲ rC{ _X83"۞6y 71g-.׍Hbeإ"I ( SHq|> A,\.bY m`8FPIMQjt`#JtǍ + 3bWɚ6!T=eA@m-}_1-24,]'4yv\ >YMQǖ>f#N +/d/+Tx@J ;V)~ɮuJP>ZB9&z-TA 9H]WCJcڽaՒRЫeC`ǷU~>Ǡf_b?K8PɽeiCHX\}ǷۧZ8|_iM٦sU>8:^)*RO/E'cv:^;A`]Je0hQPhfhIXU!,Gi^s* +rw=Hqtm! ֩·hM}ޭ] &ɲ Iq TkBp-eۯT5ǹSRA^]q8y@pSZ-N>CvEYf'(7+˛<_+bs*8z*.a%~Gu/"fcGvjԐ +X$PR1ϩڵ'NdGb]0b=BؕRۤ;i7+8"ť[ N" \#~ŔjW6_l!Ӥb -.#ؙ'D+oEDex_W_ ш#h8ɓI zB6*i3QIvZp6hg?s+RǙ?^C,}(4zL"? ;]0Xo%}C)&u%O '_{3D_EәzDcQ( |=_o[l];q޵0Rܪf{$qzl/D[6%X1Ff}/Ud]o9d},⒂Gc7N5*[-nvks'!!.bQqP؇lh  d@( +)xu5׻TVgH/ϔ%pn#.pEw{g3mA=t9>C`,}'~=A Ӱ?4F-٣׫t<Ok}ϙh1۱! 09brO15ߓ?>SI-AYVeqD(:!+3 !Ja#|K4Ȣ"K,_TDi16? Zߛ %ڹ#|ʒWIFS"V;zo ^.Gp4\ +7זܛߦ +4K=Y=J#u}#Ik[]N +#WN,c[D%O<æqlmI{ mJkYq_Q:Up7F~"d`s M5wg/mqii"F07vUB%\ZNw3)قM_I +dD#"o$d|vb<68ΊY1$-T%NokT|}] Wٰ4SX_.V&Q 4\Fe6Fg0s{ ^6{.Ө֫rOO79р5Q;ږFqy +' 8'7=f(:A!-HSw^!Q.b<א-~j)Y#X!~FOH  g)zOB܃@+xH_s"f9>#`(GPtWЯz] ӕt0X'L_纔{%<<I1 +Sc~3$@ 8SY1>xOٴsَHUzK7lI\x w7*m|yX* ctF10)ϳ$LI%IhXvɩ>J# ^t@#gZWDiV(9ᏬLqVc<`sP;R1Vn7xiafTjݏ́HKKw)_zT m MJs*a/#tKovQ\Ns}gh^]]ztz ˫ $zg}ZA!P2 ,;p;̿x(߷ʾJCi;E<|CACsiEzz4ĂOwryC@1'}D%:lh l8sHQWE;.8>MlwVU($uUFՠ8OcB]u 9" Yr 1dċ ąBT~Px*`"5}zyp)?Gz\/L a\+й3ě;B["@#ñNqi=Bxy?0pik? ͭWUf'Gi~كVثnZ\]e{K='ap&߿ء~̺LR\ؼ8/9Y;Z$lwI1!$@`ໄg|v +>)y}/DT\Ftq?(@*ϠhOdV, +d.^@h#6Ǎ p°V-Dփ vWg\W.HrTz< M,4c.H S_m!Cz@DEiu .JM2)AAY} Q +3CvWP-w2!8ι<.U`J⌑ ' "8=F1v-|⩒(Un~ GV77?؅۪Z#MJ"nPYߣJ1WClgHw\!"~?" } TG PB8|3E녺PuQu59<:Lu+j5+Dz_+ +t@O`jwRɀ!LIGA&vT;/H"}R-x+OaXYč'3NuCL[bAg)c] {oIC)[{P*DHmbP59g{udC :Z\,9{L +ϒM2;>=O~`דW @,:f@I#E.U +6H6y:Z2463헿_ 5=5L!ν0i[q=^}RF WnN-(F4B#:өFs(PhH.VYn{MHWE"r 59ABL-ci&&\KsG:t&ɷ@|H;}W+y3 +פ`*l@ +g}\@.Mj=ІMtzANP˪6[NvS)Xmw B8WCi$DkIPD3EA`28.2:6[U&mDUha6 >s1ևx"V0ÙCl ">0 pNU;9?-s?B#8nY-!EDsJ:cߚW_~i1kL*mĚD[J{ԩetd[LoAK2-ЯᢣyrtPK @J,\$e+>zc 4M XTX5d}wrBH+xar : yc6)aMA )(lkn<UT$sȑ3^.A{+$W ֓}x߹>70((: H`feMPa +b9w!kTt/%&_~i#7Xd> NmF,BW<\ѓ.M#keWnSC[E0+bixF2G BI/i{(t] +-j[/S@ .6d5AKכ[N!#)]5+wnӭAgΈ:ďqz#\!F]kƵ$fҎrBYҼ퀵 +Wc:Ƹ@nnZZ/Pa^ĔΠPE8\}Vp3Vy0XBS,Ѳj7iiT.۴W*%t/@cԩ: +KBy2DrV͊lJ}f.0\E{LA0`icwfX+L.ك_|HuLtL[ C($ߚ WOm*h^jFBTsnn=M.'w .9=j4{=Я߁pvz'RU h.ڲa]_L4<%9p%+uH s u{.%%!uDJ+}`rڝV2Ca c9ݟ!Zג% +AJzq"uzZ=4hҮ2[;vc7PwPߎ37??:64v]Nr20+h%2ZsW߾wqnp&:n*\R1@08u3YL%_ąG5Ihڲ'ה)Q܏D浮!%(͌^ي.v"C0`NݜndVC(m)7"9"d@zy_7F?t橉 +FvCewT8bjg.98>Ɲ?G6M7@nwPd*Kr] +^`C(,=s@1(f_cB$zF[:]1k[bLwBAT|VȺhJmlo|[Id\Y]yv>x:FFC[/V 7W-<PMS\_M3˄ŗPjC3vcʅ^eXR-朮ABl}efL>**kVv̼N~ԝ=E1qG*Y"H ̆b3?3a}Cm5R: `4kr" +bg<\AGZrE]8%$R634HiADB?KZ>ʌ{b.K%ֶmaz;ɈeTJ9 `NrJ: +juLf34h`\?PǖHKtRUbdB5q$s "яҊQA-hKJodOYU4ittSh=?MJ@;*-qbeeNn:13hN4qÉb"QUrhDQ;0Hg1 r`s8]]YQg4_1d a\5`} +/@ a+ R:2Wv@Zق; _:Ї8e畢c`!쁨~ho؟vB/\v3s<* ! I?$eHۏt FCJ+jkUkO#1 W Z[j):JQ@6LRf]]IǃFWڃ."B|T4:FbwQsMMD@r$ϼxzϝd.Zq9jRiDfW\C4فdO.h- kMڞōJ_eP[F8*/ ކE>f@d6&kMЅOd=ݟS]IeN +%Mg]hn~BȊul?y~hm@l"=zmd)YK "5d]Qlޔu®<i%9E8lZSݰב,O/#> DLo̞Ʃ`bqL0La|U{1y$fJDmFGOL=-~;p914J|ԾiQ۽4?BosN;8 61<#_ XCBBwZi}Sзr_ibl[r4?gxʷS[z#O-eoA$6+i#eԄ]ԑxXYZbyL +0Pi"ahK>z6D]k f2~+%^Zot>3> 9I(T갆PP?VWXCF3k'?S{T@uHK“K]~}Bp=р70-xWZל}_g i}VXdtb9g5OƶkV_J!l6e0.i0Gf̦wWUnuGGc=!z1GEbzN%l[[5 웰# 1@r3C=C02irN 1DÙMQ^f:ex@d>xOܪDXd8Q~?/:ħ9a|KR +NW)f2YI_ >$-2$UI $NGogp6ĂV¿r*K&mZtG*g\(&RG][$bbJmp+Nj@A)n>TmfR2C`~gUqLt^?RP8 H4<Ak|Xߍ!) [:Ht_; 3+%X:x=&Al}G]3$z ayg֩< `+#G~J^yc=-J5gkK# !76vK ~Gfѣ +E*n +ؔxϐ\~yZ.*JNllo^ޤ6S3L!q~k/FFZ#,.aLF"ekZw +wx}>DZUh R5aV]Y +9N*bQzpJmfKXBo"=r_$VB$zX>W'g+ߙ:1HwDO(WIx[9ו&e%[jpA]j_6^ v no6lգظjV>VhjQj5ͺ\ɗb D`(N(%F=b۵;OK jD;қ:·FCň'QGa[1}lJa+͚ҥG,' s51 +Pp[ +K#EW_mE&.]N¤{(M2c[_+"VU4Mbqf?ġW,S8v>(U!',3P1%fT/s7^2zx{T@ 7(*pO6t7HKudzg\N0*(l6%lCߦŃ̢'K!0vxYYawߟbX#IdɎE5  v-AYpi}6#-*mȐEQAN2NϜjnJ +b6_%!:w|SA()T*?茵lh"S0o7Us9Fuoyk]:9-£uSȋ;@О-ԧo P{ƟVK$Wu0fNHX6MQaV{g\NlҐQTBX0X$ ˸=#֢V{n-˳DTTN:Qץw.dJvod *ji/ł RƉPcHqEDv#>,g e_{5dG ֆRӊuhO DޟA}Wz9†Ws$$=zFL ,["ShmXo-b+᫲]ChӼDRȐ7Cܔ/Yn9w7}+eA[]KJJ7xlAxj 8FW]#=62HϵRCyMbT* |0!+Ne\]o(W,<8Uv$< m_3I;Λ=MakZ0#*Ru)2ʩ<Եz U_S@c\ڡ{JO%CmWݔŁ\ 5LGR3b]^J҂THxDTORCbp ,׌l!tW AX^W۴C1 ߆6V #i uMa)Փk9#mҨ5VL=I05[ @FꪐLq!Rr7M~ouG}Nbq>̙p\>||~qFQYBxODTrfD_+ p&ŪbgM¶^@e{_ B*)٘Ԡ+H fja05I(ï $V,Ѷ"ct1b#҈2P/9 +ǚ@d% W3o2D#RٳAݷ#p)4zҸ-_ X1 GABN,o=w}8ƠJHJ:sB l-Qc8\nq +:\?WǗ|dpb^_[$,YgjR Y t܃lNI*[ +aي/(f|7TjD NCi:;sZ{ 1 +zqqJ jOg t7$ŽHtʱX@T=n$9p`<>5ڞ_F +!ߞ_TKx A5+@F)"߫Bh 5^Lj. -SKe +[h}{t<}4T#wVYNe(H!u,N>MN%EpS}Q8YK_3bn+UBWM/ 믞]Kwf*^ \53)HA)9K2Vۂ$6%21[5*$V'Z/(n꯽璂A"jvJd 4*5{8K c@}+2=ǡ@8gob78z{DlWthV'Cjncx=">!; ^[I3Ѹ> dBdcDuEQ!^rW+=EXo~K?Oͤ(Lѝ'Gc:KtRe9[Ѫ,_ |N{3:}'. ̉8Bٟ鈣"lˀ9UNd)R5ZLk>B\"vԐ]Vrx7+l83̪&@9to (We] umW+ \YTvnV͠K.ur_XW?^LDA|w?={3$ =DwW*w3V'@ "tA ޷"':<__e͠7uP7텵9UDͺ#(zb<L4G(=e?kF%屘4ce,[KP.vL]ШH$\qkvit w@kOu51A<܉orA"0qsF֞`l)QGكK̺\T(\wĔ>uۘ`-۱/$p3=hk\+, XQ|#6W@,~]g}*ogW1VhycN~DP!߽fNiD~=.|t>& xTO9Yf$SL=Jy6hdgH-{G$6#MCkLӕ[`n%' +x3Aoe)s8%Y (E 0WBB@T5Jμ2ōY~$\H#6:MvN`׺5bT|p(R5/i/bࢷJlzCr 1bnPڒgKfxZS7AYaqr~K`<ִぱ*x=B'Ie{Ȝ:@gzYS돬Z~k=ӑ2+ +>%}s5XRsjd ƚi;c;qGdO'}-};O!nshk@T2bLABM J!Ag GGϋzF@h:}M<}q ?4nb,=ʺqȳ2D<nM4 ZW +I@V·uS# p34, +ޔ_@  5; QsQuOyrxDv.5*_c. US(8&Dnl{w^mU69@e 3=0W87Hͧ_xPfKu$.ґ SjgJ@q&0Rд+2}6qdmMC/?bcrc[:IGRTaJZn1fq&j[ (H=z%(-afEIfǝxk3bw{\AmeF^}-eZ÷Z8X˻)k=MPIhG QOh]C`VhgՑN+k? |^OcuvɕU%dž衁F/{yHO-xY [xYGQݽyjZQz!ܮ@ێaҕ>}ۈV8&ͭqJԨd!KJ<9ә<Ԙ d{M%/a^>x~oZ(}0qͷM4(rPTҒK}t8`)5 ؁/]۹9cСt׉'!rF n2 ~о5ƈcYmzY^Q%ެx^zZCߞW(R=-̲T0u&5DP<,PrVa y`Da+\Ng58pcJ|++=橾f-WF(W iw$>7ۙyK^A5"r>{IÜy쫄2!9dY{^u跔Y6#!7,.BuQ&hg߶ehUzmuOii7[rbGz2<54g +>E;6)*FCwBՒo?Ҥ/ҏ~N%#يO wȔH!=# !l:׍ emODi 75\9ݮ}lMDsKr-Z`+(#N \ CW =:շwWIrv~\uq"=ܣtOdd@dE 1AS[@ewCRgcIp{!H9I{EJTtTͷuƺ +zu=Qm8L6E7y9`. rRSS5C}$ +E(ݖXWŵRmE['7v>- +A,PH' \HUY#%_bzgKp}J3|/fRCŮ\՛KE7!rlS[:B  ? f-kP}y4KB`Txcѱd=54)bZ@/6r>|`aOLZW@UTU,Cu[w9<[j-ZLz%N h9w8}Bo ,IDȑκCӡ\B&fP1^aZAgQo<ߐC{8pCkd٨=C; s*rMzTmjla2"/:#h43TVؚ~ q')+,<= 2 YQIU.o4cBUqqcÕ#ZQ+N0:mҍDV')3^:9#Ԡnbl4M:!`U@o:6kTH ndkYM+ST>˄[w G6kGc?B}C󱱷|ѤK<=HY8 +'j9&z8x` JXݗP+P 9w{p?UW (WnF:X-Q{_,}dj7AT*OKnRK˚X ZlH{DX֐g fQGuǍ'|%Y B Tq1PPOl-;L(}XV`Am&Sql#CۙrLh>N˹]ex`4;"UQ4#-j^! h`~-fiKǂr3N}-E5ME))\w^xwkI!Ng"kxs]Wҗ՚x8iw D+Z1ӼGG, JcQ3xCNVF8]3O(URon%fC5`M ]&OKG.±+k!f) +=_pco |>@*?w#C/oθY…*du?PKPPUC~} g=ؐ`/.j +l6R (Q>թ*=۪"}QgXƣ60V.{M0NNjZ;J,Cyɱ%cXCS@oK;Q++~ L3+=[(iDY|KzoIj6&Ό`$kЎԖ{h]uhYw ٹRЉ[ Ĥϐ OWR7t;& uVP+'0ж&b}oSt֙Nɴ|[ ՠC#/ %n'!hstOśgLKIJ˻1nYǵL]b>!\&FqK'!iSٳ3](P]W =' -(YR}Os7XUC4N[IǠW „Twy#LprZ@M ;9beZC1E0rXX^R Q8LbA)飂v;p9M걁k*mG1 HP*1IgXM2 +)IS:i73>~ ؿ |K=7Jw]1jNNR +hC۵^FwkTx_.ZE!xy瓲E |OII<jfCɉFZDMӬG3dG[@E75-_sI$^֌h|^UxiL&np;$WLƩ8ڭ(hA3tܸC>5ĕW =tԚIBE#d!}.ԓ1.c\` rFRmòfD*\)]>.ڨOV"P_Y,o^u{$|uY҉ر=/aUC1T[?Bݿ(|]ȭX[5} +/ZOy:5JdV7d''lJ.$̈Uy M_jLdN\ZkVoLwU$g*kɤveEC#iIRT_6{{:J `)0846Pʗ$8H5])B*m63]Uq02-devsGY7\5>ft퍠(nQkLPjKoʍAg. +xQτjħ4 3$|jfA}%ćm)vbWGd< +M"g}bLao6O@;_U-Y 車 wrP0>k}cQ9lʽBȰS4V-^L c7"9/VajD+b?&Iw{D"'cF#ّR0dS[h/CƘzն tE+9թټԴcjA*] :ːp]Pkθj>n\Jpy^iKTRc j,KF D!%S1ZrġKI^WyH-Lg32! x@V2%򖟇sъŰS)5زr([Q Qv/pyN70\`e(:%b|?#M|/U M1!j}yԷZ8CrWBHbO >6KS'x?2wsK>j%c3QXڭRH@D rwHZnC NU&}2Gqc9brUu~u$Xݹ'ŀS(7{ D8e{N&h[< +O;É* mZbf# l<{OHћkAѡpgI?umTo8CHnwk}$>LkJisBu&j6Q};jfSĕ7Ao +6%~|CV$Y&A`'+MI$sB{3FVx(qpXB/Lu'Ay dϚQͤ}UD&R̾s@t/>z-^l*NowrQMfNĿH7²%Iy@Ól(Zzz4$,֎+ψ[|!fX:s#?X+yc̀cbѣ to~a8C8$|lt;@FN:}1A5w!s*a޽UW[LN7 +x-{7t#l}J.=¿TEo.i_ S_aZB)!%+gm0wJf*"0b"q$&mzt(Z^ՕӥQoGT%!z@E/,UHqb;^ w6\CYFw\ h~611Cu]#QY=Eb]ԇ]_{q JgЈi (maC؇1''ms{S C89Ü!w>5 ղQ!]@M|˺\viQ+ c](0 +xD[M69lcKY(!Np1Zp$fܫ![,◖N Mt2(`K,3#^99e}3C nbK[Τ@eFf&Iilc7фv !eA]asjM.p}H_h'RQ7⇒3D=^BlMA *,Pgq#,F=wy:65gCj8ɓRgP_оd֨OvjAq 0ƱT2n@u0㎥;z +}r8uzC@ _.YZR=ugJ8;Q!o WǝUCM~?׫4C1 Sl"y;]Zt+a'w>;i#gH5h' + ha;pjs*ԐHs% lF^NrQ2kuSƆ[L@4w 3}JIЉL6hN S`412VlӲ +um@^l4E&w`S쎓e/3xG2d2͏|6 +DK1ݬQ(zط]6ݭ׮3ACeZ8gy+u3R4ݼj~kcUfö*sFX+Ũʄ %:m@6=eL#bI@Y2cvdH癄?y,fOԟf=,/dDz%h>w2{i#ln#}NlLjwo졇Wf1)VÈ 8$ XRá|zKO|ͅ] +eiBSۦhIBj)@J6v21 7}ܓ ! +FOA8wώhqwX +[z70gwmY sVbӭM!6}[74UᜉZ'҅L[{oC75R͊s}իjzZ8xkk}& H. nFzU}TGeɫ X3S=ngHUqTHrD2pYYNAfbC3Aϻ[)vC}N +&WiPló鷱&Vw'L8^Ou}eWg*^e)A`vGcIПIx"3Z;ƥ3`F"U]}jѣʯ;gTV(6})w( 2%$ǪCU0J&s S\kU(.8Sw߱a/H7ÐXnĨ F]z-,~6bG-{ˢK3r B*è,zG8P[3dxp*ih3hMgm6eTpX#lEB RY + );sT3M)6S[1P :ثdQ@o]Gm~\U.PힺƆѨY!g ٜ<5'Юdo +7Ϛs3hBZy>qGwmM}\]]Tr壒wTo|$l+繪oQ: +H;$V'V + ]KJTghA3NU}nj@~ٿ6(~S bg8V3(Xqt३8>[]JAE(`?6Z" g3ƴ'0J{tgc>܊O&!ێ7j%TpeUꃦKG!nb)ԾRzX^wsABPwSdg_5\Va~$t@EyڪO"$P?tPڡ׮;ZQ5F: f<ƌ'93iUZE!Dh}<8?[VRj$sCJs*AN=VLX>h#rb!ZYŀ!RݣUHRS{egTAOgk9.3)'cju^h9n1iTD(W{fxC2Dθl>5 U5j4aPzy6UFDmbW׺q^~iN` b gJt&}~F ' +OKEⰕ(5I9dL8oTm +@N^Fz0֤VKC_[dÞFN 1 +M_9Җ܎x:>]IlEXcUУdB~UOj~ |S]k@P| +2 )gHNq='9TJM}Wcb[!ŀГm"g TEQ)" .8( yXW\BJ= PLD"Ke l, +IKeͲj/WM/7'43Uimk#?48Kb~JP<έfh\/ +n'DTC]w*?0 (ǯ0-R ˌ7l;Ker:h罔 $)GeF=L1Ѐ+bŠD *mjlrǟ ڕZHEѾQLo:'@' +Cn7)u\OIGpR; ~39IN 0 %@0|Ė|;0NGM鞥נn+S 5W[XjӻՈt.Db_ rq8@E2Q^hw +}쏻Ԥɴ^`jk&ɠk YU!kqGR~ V,LIJm;2g[A :\u5Xs +ҦU.iٽ-t\BI ^W;C(m׳X+YSiҘ̢nV>Rg +`s$K8YK:EaPW'rG ;Flx V߶va y!gZmjTtLM_,wNHiُZP(\eQfx/0OjmG j~#"-pеjLY>%`;xSqU %VwTOs VC]r(eghXK_p@GJ|0B^jxbi.hv sH|;}̬+Pb6秃i6#[?5Bq(; <&R;I{2ͭHǨI??G@ݘEA29ѱOĺmFţfKbC]`={mdR62/6}zrs` +kӈԦFА5k+>DQ— ^F6纫>#>Vv ֩8r|,=7>blpt,f+sETJA<00OahqӓYA[ .kQV!:SWݖeKnSut?KXz+nH!G[IP$rkPJ3@kqb9KȬ|($95NzxM޹-G51>#y2:Sg M#֞2e"l2\q`NK r+(A`3<«ЃXevFEe*^1P=iy!)mU4 z ZJ +P>*W5de:w4t}TN~كJjMlƜSbOФB&->]󉟜c3ȩ##)Rѡ{2z͸^mȐzzJdYehtt$lBDEs(Tn ]je458.<OQ` VNA*GgAO;^\fώɩK_biوX=3}g"A$FiI] E@!g %/'Lk4>DeHv+>U;ڥvEـۨLLXforOYC >GCnV1.CpoP\Jg^/qm%@h*]R^7zW}wXubeZ:<*'A5dR=-/r&j,V`JÂyC!*0 АX3V@P=FB:s'TsS'Oo]$2 8!~]gR5gڃiNaܼ2D+hI&_#ΕwE@FV}]!s '~G\ o9B J?!d;bc;J`k5aeO.%Yr7.n-%TllcV ߤ{EdgHrjW<#&ݷTJ x +:K#o^hrh(sn,ݎM 3 vndm=ND&[ſ΄M$ ƚb8k4Mj$H>p%b({$Y#LѵAZM݇iRt)èڭ+(kb}ԐuX\#="ԱG[Cֲ!F߉iY*nY9ixAEZ_ bhersS ih"⏠q)LBfCfzi-AfK0kǎز/<3~-෾5Y/gE &ʵ:b4J(QiGrDSekAڞuhG=9)-U[+:`ԸL?/xMּMsz=|Rn#;=3;|b^8A.2&ÔǙxyM +HG؝ļ3gsmgu5gCI/TixISi?fj@ k[C^$$x{qB)V(S"Kl-۹"1pCx[4[_tysLa֐=vڋP ,qŝ~SH[C6MT«Tpb\WgB4ٱlu~h= `!z-9h uV^o5SujкS񼚘G@aBʞ+}DXQI4h|%adsZ1UUJi:&r/)ܞom[;GviFi`7qPF]:j5gٙO8.T{3ONx'bgP}F q'*sLh]\yzdעۢJ%L.%Z!`Mhŀ%{d +z[=kPZe'Ռ8'Vf߸>(KZ Y )QVү +P!9yy!]]8ZB* A0>3Dpj?5iʐOrS_Ǩ&V> + +AޫLMWNʸPU|M!7.xF!<{{[.'?S3lI?6&JYP0vpoiHJIо[B}BxRwGvO)w3znK- =x?ϔ:끗{ً@@գ@ΰ)CzJ׿~"C"sPM;}۪(g Y'"fqI$n;\K~F)TV[q>3fq\u;j YM=:X"]]Osg@P\EeuS*| +&<ܝZ#Y IcDM Ok kn + iPI-=Cf7 9Lco@%Y?V,x`~|GV Aԧvf׸cZ!vt{ھ(f COem^yK` 61tw0Bxl3FmqLiSg} +4H&Ԙ@&g ‰ܥcS廨MEk:L8=KfI5}A#¨\P/o)aѸH)r:2naZp#)<2qlӮhsd$E)Vk:tBείgw7H1uGdvYf}r5)`%3O$h!M'58Th֗O;ڴY7}}n{'‘yAgvCG3#G\I@7mwFR3?8qKm1aX@Ԓ7W7Ixy O;cs:@Dq4_+HȕK6$v?7y[;VkrJ ΀! g5Onk22F=]!R>o]nC:M+ou6a3O4kR+F`|]ޢ03h- ir\s* y 1 hX*m @ G-R.Ha<|'yq`Rs˜}׼=ѣؽ.vN"@| +Uptqj 7,Evpe$|"o5 {B8ߡz08RP&݁CK_t"$ѥ}fH?-9`X @yO)cv-L8]f~}.yAd*'͙9,Xe+S8C+j[Tdg792)}VCјR0]a0Apb`UbT5Nֳ0ai֛9@qfa<ľ<$lڒZ1cJLrx--͓%/cfqs6C:dYÞ7dZWSݣ7{V8wubdX{Zf_4(;bI+m2>^h"cP>/J%x9Qo]BKb;މ[7kB"VȲ-&[_#o< *iĈ0 DIqlcS<w&?"`>E sƩ eI P)8#d9hn}l{]2+u6-aKBބQkB\Pu(+o259~!A"Ϲ+ܛytuiVEPAIϳOŝQ +D4nd)nomѣ}sI#ڑV9io:ĚpL[G㋇F^[ARB| ԑ=nM\~^13@r˰?HhAw蔗H́XHnɇ/%:=l6mfkr=vdN=!D57;<o֠6!f䍗䝇rRr%Xw.d(P+f!\3Fw|Ү++oyY2dCvpol>?Cf(X~חr2c 7KsW.`6afsc=e#cl(zxAAD5hVD[E;`K`(z96Kz1?JZ7ƎSJq#ce{pNFQE=Z J2[:ra2j(|ҟy +gQ{JQ` +4 +KmМ˹(䈣$9?;Moq}7.K~ywaA7|s2sp1{@]SwP)~)D?x\0GG0:D*^槧dp'}daqG2 EX9~`pߑ$p~S 𼪌5dt->IQ'X<CuH1>QG (Ѯ'~4b0O8]Fyzn<-虿A2䘨NÏ04R1G]併M_:17 gLIdɉL9M?~AW (9{mqrKXȯ2˴'o{]i;4rzcHz.Kxfd\\LH :UL׿O| r" @#gUP/Gc"mFxn +i艎.IT0ÒKdܭ}޷]1 L0ԙG*:p.؟\}A2n?y-_|p'X;ᓊj7F!sjڨ FFȾ/h秢ZDOxBۗqU=AXXiL[c6jx[ɘ/IjY>MŃ+|^GjJQoDn]8ٳHX+2_Qb>a\rioHOvDy<Ȍ*q:8O)bP69+.fr~ڥFKI +{oNLU3ͼ7O%h37gm:c㉨u_[_u&D&S״P5#y[* +<}#yQvUWanmS PbX?K| )S +ތy VATܾx F7rđP.AdDSܡaGo:UmuioPZdT=D@oS`t/&sEL!|sJ"^h+'voGjYgmQZ_zRy|똚OuYI=YPC3uE4%$! ;<~(S/65;m#=Jl s_ұIEr7rG\ڂ+Zdoex0BV"\Nlvh*%`K>5.qjDXZzU \IO ò` +piatEuՖaSOC,׶QH)-$*UG$4^oAe$]7ѩn?rLKr_\9%my*W?[ُPŏ}=wHN)c90(  bp; :o# +@ `X5 B}a +~~WDb7p[ꃮB暜d;dFz +aFݕJ:j)Ls[-YiLx=Vz0Χ&G<0E/S?;k[]D8Mf;fj̇G5z`;U~v}6Ppw4᡹Dֶ{zL@=p~գ)&`0yè[$ΗCe X\O_:9M|TZ?]Ci7]Z۩xؓV +yoBFn4V~Nk[ݶ?~7?)&iAE%b׽u>{#yY~=^1^;[P!y, 82_8Ru;]P~?֛8Ay.N$\>Bk8ҠFgEuIL7wl\CZ5+>KU;s'MX!Y|]OrO`Ek1Mms&\cIExX7Уi=x_%m] 񋿊XgKj=y:kyAqՉCôTĤ%m}Y :8&Nh_ܶI2Lc`M%Cd#'=8lw25HMNyZt[jJFoY_o<` d5ul`IOHlƈZ|CV-a-$V +WDResxc.]u+=Q +h:~|5fcpPV3 >ڶ6ͼҦ`0OS[DYVPeRI5aՏN=u,xbX9g '!ݑf']8z=(xֽ}IjUyE̷ ['}u2l>rql +]{}),:́ᾦc];cYyg6h95qgFچ9UC1q_ˑ:GDsF +[֒N2X ABDB{0UvKpMN/gl$0Ɲ:+="1z+BEYVOH[fFZU޼#]B7Uπ)tNLSF4 XKjbsք6v/~a%9,GykIQr[iozB7 . j%Mś̫ӊlSنeAt̖j+F ,~4{@l'^=sCZߞqS{^>c ʳ(TwV*'yN&͉շ.9c^[ s%DC +{{,#[%"xԟdΰƉWUޜmfmq*lo, b蒧9S Z?G~D6}E5ݒ̽%V%tʱA*yz'K&'r39up5dF?CAYclEp%1`a풓CfCbD1WYL|q6b|3.# +$ :2Bw%/(7{nR }%*%wIv4cFHc0Zt8w7ɤg'?CFJdjS{Dfe{g,㭮BlrĔ\z5qEzs ![/ fd1 +kxǴ: +fLu}ͭ%LIhNYZ;fy>3k]ivC {);lWgIL&@LN桳 +2On-sug汜26S>?xb=2e dї_gwdyHnzymy΍{%s)@<CxI#: +j𰛍=!btnV|SXZSN[:2=-OC:lQ:v6 \ӣ͇abG(*~R5sq%[>L%KyˣR 䎂:ӠIj}v}%UGu$H6= 'M*as>'} 6\h3C/m$(d;8GA/2 +YՀq?I︡_Wh*1]2 2(]Q6#hMC`H%LHKS2fK24]x%Y|k\X: #/Qq +y4dt|qЙf:Kx@2 ]ǯ `ej^VaVhR}α>a nwRC[7g-o,++uyŏ { uan3>t9[e|_n8 +6 +!k ZKHq/*0r&:D=ܖ7ize[G! (`u&ۄ3sc$L\54ǍKqW\X{ۇftkIM$J[\ȶ'}eD0p^Q _6ԎtR6vXUކ{c(ۦYMYs׷vIJG!?A@GG(CUv{,|#7ԛ33ViuZfٍĞ']P> p)'SƠmïHa/[&8p@%U=ӏŁ[?L3y_zTp6as8ڱ:B=GvhOR.<):͢zl:D4CVФDڍ[Qܮڝ<: FrpW;G?N]+ߏi&}|lO`=C#"Q +4nxt i3sMon@][|{Ǝ0mn<{?^X@O2 ``' ro"]2Lt:$Rˏ%uCn a"PVxxtENkъ 7(QwFwͣLFG nh&dx<*hS5>0iʧc"jnsNbK8 +qKL_ND*L"*6ma{h-O]u5%*;4$dkgyB[y#vjl9w=TN3RL OH:?&%n8td Iq2W{{(sNQҗ9qsDŽ8w_(ۭ !&iQcsLPdp g|؁PS<23 %%:us{N%n2=A8*Mk=\.{ 2OP0H>nU)#BB=m`h:hg&VNs4[[+ph0ZϬqm^]B&'zVgN& ZZdl\=,a2Qs`3VOykWK"XA}M73křɹ#`I̬G|jo+=VpWd;v;:|-+uZ<]_OwYûrSD:FYtY> +Ɩ}Rb$MmA]}T١5:S%SH?Tœc;2fa?'$Tt??_\@.I`û8^VEy ^8[&B>#0JFar^yUNV \(ƅ]h[{A؞C>6mq1ԹV1rg3,CVqmi`2Oajݒ{x.,Bj5.kRT*o̎iG?|97A'U!zzohސX2bFDsƟuctG'RctwTqֹ 6!Ov£k&bwl:gՇK>fI \E:$\ D%1_V*O,<0S^{|Mx,> z*G _B5c'4"*:,~Y4g l~zqTyb(y>xֻSxX2Hv67xBhvX$\  u3w#B3'o%R)1gL-8gSCWEicQ 8暦k#’N8Ol*g qgL\ONhFLRv{ b*ݛWg'Z͂!G)eqڟqcq/GYF;sy-emKWce| =7Lc\Ocǚڻ/1~t4Z+?NG/L+7\#Sp)H~$J"rYCOJ᳥.O,: +U皖kQ .S^;mݞIjbQ`uVg.^MUj\|K|+jc>׬E pjIbR&~~/8ն6)-\{*$4z^>wꘝr嘻͕aZh5eEJWyw~wқ 8hr&k$zGM+epy9Z:>]w7.Ciް+7tLO|R^cEObv_sak(ӛl0s.^邓F⧙$qq[^}txϱP3̟ЄA{g/C\C,V\}Hoڷe9:,8PÏ~G]A\jF}I:̟~`A{K8U11a,XKOp +QkkUIG&풠%zb&4sF>{ȋ$=|A߈\2#6B!_euVֲyxn1?~|q j.hvlvo9xE3WeKR۽Űwأq'tGa,ԔH?_ADSc~uv 9")xnL-5 It7}G$5и:6Zp*FY~q 7V"E/N|^FO h!x fJ+Bb˗s;zZ.˕&Shlg$. +uv,'ސέ%2`~)zɈ*+Ŕ"򬁴"DEQ70mX/@;$\qqi3'Ymϩ++az1xy\?T?Qج̾<4ő9_Q aG<\pJdU#3a./U9tR9[^.v''Ar0jEnR[Iޯ0VB?۬T U8C +߇Y\Z_j[ǑS.Qp1-I29zx_Ϙz88gyGF0O+*NW=PC"S4WZb_CuHle5GCB33@a_8uJ| +ejuj$fٲ[MS "=pK4w az[ʷ;}u'D U aGK%=W~):X$'c|_^jK\"Giyb!Bh6t}. cbJ0.:Pب(K&4=̫v5$-uz޷ܽD8ŤE#C.^gd1t´>sh2QߚߩFz7DyY90k؞*#:F /:.* >rqKbb^B: +5݊8U".6K +^g+MDi5\n^%LVrO#CqZAQBKD)y{9wC}~ K2NJaPcZQ8n[y_dj+yW->YAxq3}sKTڭSQxUWŧ]$|G*1)׎ۋL9zd\X!Q?uZ/!E!KVɖ p3Çx٣ u&3U&kh +Ⓒd!2t:bg}E z5 y2?O!Col"ruE\vVw3xY١튧 "/gTxc|uHQ +=kq9Fka%eRې_!l8ٞ1 ,FE,LQ̰zD„Yފs + .1ߠd.бYM/(sKS(}wB)2,q\ +H`㡄^H +]>5Glr~-+i#m*CQ}:D-3B39W+u<7OmpDØ QěNMHt xB~ +z7W-kN QuRuc[԰#E:9e')1UÞvөY.) ),ǫbtgԤþV Y1lˤW|L_b~P(爥WAg|ʂNI$@̸u,6|ĵ*yP\܇XRU&$83.4z54n*uνjfvqre쬗)LmۓZ"Ks鴗P.w!d譧0CT~iWyj"|K`k3$%5&S`ƍ - cfoU!O2x>Uf~Xxf6\ee2ؔ7n=Dn"ϣ"!X[:'zՏF'̀ĖnLDSK/ ^k Ƕw NVK;IP*Ulz`0Xn_lJ{p_\ծό %e0 4,Xk_B_02&0qFhƮg!!kIKfPPv14Y~gnڶ4v +ݵ(1 !ݢ;Fڢ!2w_lmwR$pFzf G6V=-N:'JAE'WNl>x6FH&M-?4c4 i1jYpMy3a*0X;njxw ]]ٕ&JPT֘:+3yp`zl&@%so)@(c|(Uq|yHPcbDZLŋ|[&(L1ƌ"|<( > R3Xv_{D/a +I% +)wNz[qRwx{{3 +<!'It))09ݢ%ccQt}Bzν5VKuo` ɝҒB=g&\*|F[VEW.&zt~!UVyf.8ra@͊zS{qMWBѕ+Ad ,y#f:Uq!!r徔Ҹ̽h˼8BDLUj,pOp:[aJ'$ JAR8h]aZX|"?WklJ1S29ވI} WMQbG4%KKjwcsrJcM̵tG}uolaJ().C:7L#e8>>Xo*5x}97"czeBW/,Fz SЎ`3)1aH*km_"&8$b +;Ȅ+%-kV&4E|53I/f9w9sUxc{$[ZtFD\cm=#*jn'a)VCk]gwizЇgΓx !WYI;)'9$gSR4y@ƪ1/} Thj ؉&"Yv5U +DtR"%'=7$(w GwI;6Rb&9 sBR=B!' RL 6⑸ +dʌOlؓ쨵P" nbZ({ +G#ܳ;n"kT@G`# =AU8/t%C|6dFĕq_b?:{g r~*N40F vS@#›Y֣{nwS7 +0^Z`f&#VX fGQr[jHL +Yng^ʹnA{zJ+,];ȄwO)B1\IHRKsyfZE0h&C,K|mrVjNZo(@=/ȮN芃I2bhH;ݎz7IAOPY*k׍ÇzƆ΀짞 + 5턜8YC[N[6 +TQRjP^Da)4dRFߛӒgA)(SϽk]зϰ 9|y(kDj)Mjڅ{%~H{>~trwח0MAlѣ־}kMt +6k68 endstream endobj 51 0 obj <>stream +G+,/:YvI6]~8Tbbi1A<$]ױ6@a\s~LI}<^z:(4_V¢Б$S܏:cIlHY VZS?]T ϸ& +KkQ|'/D>%=g( +LdLCW=0L^:[Ou, +4mXL%CY/$̓l=I#ǂ$$c',n +޴ƋMR"+IE~d>;j݈ []Wy#YF#um;=H#6 H"&7S:J^MhZNwꈴ4ړwa'H'[k6 ]N-2*X/klP'N\I3LO;QU_7-ɫ5ʎ%l[;*ՈWJuӁyTA2%6D :+f{({b;ϲ?ϧ<] i'@4<6h!]pf$ThqѤW}O5H/~od:^?LӜؗ]3IԞHeth *ZM(3ks[Ɓt/&kH$y4>~uw_FRCLKSKF@1Hxˊ4*5}C 'u 31IȗO_nKĄ_#AxXq"=Kк{̷2T7ܢw e=~8Bԯ^QpY\k(KK GXjw& *Y#c odN\rk6"Qszr J}KZ!:r⯇B-EJr4s[(g<'_<b4Ek8ƊvP,6oqJ@c֛Kk3aǭ]M!Nj NS+D[(ܑ*¦T@\_Jt=.wIgA׫lP'b?A ;q¤R~7n_BlvfkeqB w\@ 'dw$SMͲG`:XOLW0'@cѺC~WS[Jx+psG8Ӛ5^8N@80)V +5o8 Q4Mc(~(ksMx_@ q:ljC5fђ1=rsk?7BV>/"NBov"b["18?Cدr\T@afr>%_")&H[ +'oQ]K%?sDc0uv- ~w&py­/4Y,ouGn !?-8"-cCHP55_5vv3j}(xt#<ٛ2@uL;!Of1A|b|s t+6_[BJAE)̽A8MuBD_l"ѣ<2~*E'\d)'2`Rt cc>p2Э.9Xč5RhaUx `7*n;9CoZ Pдz4%P s#;,zzvz5$$_cq +›!<4h V(TjFMI22JJ(*B㯟p+`!u|d3YWa9,qsϛH!<@TX֙9-]7 +&K\O`z+swG*@:^[z~JMxJק)#qCC ; %3rI + J4'X0#ha1hfp3U3eHW0|Bx֔[]Am& F띔5.w<25Bft'7ʘ9]/7/šYt_XO;^Xt'K]WMF{+ cWHn|RiH]ksR/}"d 9d^B]3]sZ*ZۦPj{~7|n@a8F$p&uXՓf n8#&X+:aX4O#QGRXoJϔ &QgD'o~^GJ$5Z,u=u#wΊ,p7N޳2Pn鼭l]KעN78[=b7sAݒVesd5xLJc6FH#z:sÉ͉xΙ2KN+bȩvg$ݰ;] SݯivQlo Y~d>/ꭞ*+fѐL\;p|݄gt12i7,n*1_ay Q6l?#1wHSrLt,e0i-WsR&JMǤ+ +Lu,0I:F߄gP<]rسs5%^$ +llݭ*n{eu!23UT쨾@/USCJ:<߇ygb _=wE:&&IQCI.>ںKlQcI u G~Q4Y%: 6X{9Y;Q([(2`#NXVa8Ogڼ6i|>|WZ;* H v5}FV.JFWL'Ccl bU+8i  pxSiP]u ƌ%w+HْJ +g%MQ& +"ez^g?$L+ +>y ;h)&K'd*67b% q 4/! ~ujԝDPz#}뢊a]Sss^I/oo]1ޢi2vV/:$qN W9lx\ ϟM8٪<+E(ȅ.fPk&|U|~.9JCTo/?hOhIuWLj1D~6o|dd!_Ng>NUĪz ؛Y\*`nQtNj!(x`4I PzN=\r\,+ȣJ8e!MPzT-%$?ڤ`0{"Mw& > U*w6/sC*Pͯhsp3+hU["W~¹5oEi-VμgmG#6G ›aЍ lj NUV:g&>q+o*46dI7#ȧdݷqO?P0bޔt6n†0VhVAjl,G0*u!Yfm兡']% $4j/ɨ;G#˯hOjyY7cbXLNènߝx$@ĎG%fQ0530A)i9#b}+{g7rԗ8!޳A_<PO)Vh>@!>7 o=# .;arj@ۊ$*$SJ0 +6]9PvCnqԶV +H#А^>z.ز1t֒tDzLڪooz*Ydi$ z`f ΞD1LZډWQ\32fgwA`QD3WއC+sc#wo]'[`o1mW@OVrK?pR8޶y=.e3L_X{Rb,o>#mgp +$D4E\^C xUrͷOɄ2 ϑH< +T+9ToV{[y=|ʽQaL:PҒ*%Cx@;Vngi,|~DV^zcs/bVC PoeA ʄv]b$}(+U+m5_H6&Ɛ)~&F j: +<٤|gTU\G@rm{I(;e~'+IyY798}B.ۺʎ;+AP2N콒@D +;nv.u$N228:2zس $F|cϘ +#-lvlхq[&f pخ*ZM8HlWQ |8,ge + +Gz+<w ,`Vi{st5X=>(xu6bQO9YOLAxd9 FDn%@n[Jdt݀"edȒ`BVJ3P!nKnrt:H#v9 +t֮ڟ& f"7ϗZyIO*y'nLjNXsbdŐ<%L A'|_ NP5m3\%Bzfz3\5(< JG q:*D}n^J% > fO`173#@nі5QEBXch@l+%>0*e #9>R=eɖk.b3Ķi%F\N 3k+>bffxmUE43̊N5 :xW[ +lFҬϰha1a/5"c.{\m9UzJPE]Gvѹ$AMD(XT1EjO2OХ8pdǏq.Lؠѡf,% +LZ4# KEȂSrI5`b~WB),r8jIg\m]OV؉v(Myyz-fNL9^豈:Ӆ|#+4-ˎ X *.=EFA! GbT5˿=v/g#Ñ<ớވ :AXk%_J3Vߜ%(o0cW>n1%f U9m.IZ~*Q a$DIՒ6a^TjPŠMP:x3="l{ +-+%*V2%y2'v5E[ߟ#Zzq^n"̟>̑h ؁1|$f~LTh6?ߤH.d@P L: M )s\'"^9;5II=;5i9Q.zH^o1KF ˗}I]L@ܩsPH=[CŮ-AnDݢ_X{lߐUDU8kyVg -1DGf?ŁKh=D@ݟe4o5ujPkXOpWMi(4m%IM >Iio^;}R$ 0\%[XGZ5N&Y8!+m sdDfMFsDji;a~dZuE!ћ3@(x.$OtVxz&ycSxۦ 2o8Ɇ^_rrÞ[a5kd;FYΈg{tNjhP*y'CPdAS߉oο$JoCL1X)GD$ Zn5Wk6#yvM55cbo< $:gl. KWŽ%:~$vPt,\~Ԍ}@)zxOp.!#! +Vqq񣚉.0 IaFqm.3RD #q;v?$V_U-iOg9>lh?qm!t߶[Z5b >l +Z+8ěV%`R +n{VF49H5 R_l(L# fVy SIdCSˠiLuǫd"&fC! rT'PaN`V4j9OA<OLU<;JiCiVMBGDؿ^QIpjAE_I w3mǹCb:#pTE |τ rʶ1^'BN/FKǩ?1/e4^ig4Y{[0 /Ӿ݁';7O^4P`m^m2Hyѝ&袺 p>,қMarrdVG:@r?z[ī\϶rݻI +5 2v6{FnwB:B)@؊hk=.2oJ/P}x6tK?Lz7zK`ln]}~H/piՋ"Þq;SQpaUorzR'|lkOsMW xc>%:UAp@͏&,{8\W^Vk[o՟dއ]ve3Sn{LomE )4G*Sr'"Td?CO_c3} iYdrd^G ؽ/e@W]"Y 7PHQy8:vAbSw{@_n}>(㪡ǂj~q?J*̦Ih + T?˨4%U^n׷LjnɵmK1ZЖ 8?c6 +; 7:I +غh3Wo:glݼK^;(Ǫ'(AP*TOÊv(]{`}߻)cw{*޻]);YEUΡt0Nj'G_=DrՁOVߦo:]֡^tZkyE 7"{R|nj2w'ٶ׹[> "P[PlClly +X@$3Hx1%Oi&Ί}a$3'T'н \ +v3pgwUVB15Q#q7)9bk.m_^>*ŕd:)ܕ=I0(1B\GpʫJGGVK5]=ɤ(dg=}^zylwJnbџ ~^o$=zZOݩE#4ʧPrKϪDWKq,7vw +Zђ$.'7ע@3;)lMA걖2.P0EmH;iŚ`8 +W2E>CȦ1Oq}34SCU1ՌQʦcT룍$o)ZuˏԨp鴐NhOR2!Lډi;hQ螈KW>[/\d\oyLNDzR2?h+3VGUA<GI *rUJ;qɩXA&JBه_.wRg3r)8q~aZiN-uyf _H@2)ҡSrZ'X+d ]"UVZR4'[)o* ;l#\RC EbOSI-=PԩטNG NXu| !o~ΐ#޴_ .t%)%$AwP[Kv!j_6mC-Mej)b> gמ^ X;IuZSN*/h$*)'D8g|k rehô4Y^(e q8~zc{-0ΞQ\r~*=S4 ܿK˿ + O,]XaIڃceݽ5MmOζ&7/we-~N̰d0(H]WU)0)09ǝz"^0H.@$A#\˿Hf9/g,@='ӌ9IpQ=V3?P2eiw3Qט[ aQS!/i2XhWD]һv * 9W̅R|rDR1Ŀ=9>xeEZt%ڝWh#4WAګ4C31K_x/wsa>2֗ /ˮ9|AݯQaƼKxJ;%f`i MVZ;SJe7<%J rX%D/f6%Tҗn?%_oop3(19_r1wcU U4L{e -De{ ܲiU+(*h[AQv[- +I!C! + j>g${LsGZ?b`VCڛJgrs^,;nD1 8N[NQyAS˾j~ @0CHuI$p+L!qVT3jXNI" <ޮwnh% DT/G0{YM"A +#H9j>7zd4Ug8unE!lv2B3BGYR=Zw_B,2' J@|ثqSԧi Vs iiv IAxm`76s:p(7"I锪DTu BB׏ ++y[<Q`z޷7eHF=t3'cZ1B=䶵=үXN55CWzNJX` [>w'1iBӵh k3R4{FKT4Qux>WCjxߝ{99#ƻe#'B";y0:(~~B#2jCO|捵ώ,5 z{bWm$\V:No/Q\UN RM Z>olkGsnƑ\SQL]`_B{*"j{#T1ia;ũ# [5#:V zN_4 aUO}XTNVzxtPlb1p"^*!򼯏9eSn̚w/)`mv#a 1$’7{ʌݙLoG r%p՞̠;]D &QK 杻/E54mI2б1̀/qRjE.i Z)M$wuc-6ٗXI` +zEvi!ֻf;T S^~|nX?O7V(!#0gUCAfeג{(6 +.ऺ43JFz,H_4 Yn|ƽx%#\|!=11dx;j-#CRϧDvo$'Sґ$Y OFA7Ҟ @iN3k,r@|>{^n?IB[_Ml4~̓QB˂xS\s$փl 0A:!=SH['բM,Cq+Gu+Ww"J2 Y9VLUM'#I:>`We7!o GV(ZCzֽz"q[T9):U2" [.oBGN] +(HB&thؼt6/N#"rb]* oj-£H ල{B(ksM$T^Cy>9|?J{Sb:j*"xCоm`?-&!]ig|잒)8x/(:W$%IM6]09㬺H4-_%5plCBЁת\a\IEB8uOdSJCzݖrܕgKQG&@ )K\b H3=V[s7V?WUR7-JT59#a/ +1|tq^eϭnt]PgQ9q,~8jSDH[fA%3vLyY t''wlP1#ͬ6ܕ#/m}ho_r (YoC Ӳe6}di+ҮQeD0{3E ,O2ˌ@Ӂ xz "?ٍ.ɟQ%ȝ#xҋlr[?*yEtd6DKov L4H_ڂ[@nQ^U>rT+U՞=F5+aQ:jwY"&x&4SwbF}Gu,á_FKjP'4I,^k"C.r]'NF˧J6DHg$y䴌 ![阮#Ý㮀dpK(M"v|.OЋj! .23G%Ҡ"&<,{nyI,Ң| VY">&vřG`V^#8 m6V@KNnijޏLMRMwX8eH19G9IM>Du툎>88l2Ub(mY9UvEɭDtqKao̤.IkSDWh|t*gdTu|"4$!0\-Y PXO]!EśQ*" ,@8D/HWzk~FXe{υry! f?j\%v:-k1H&0DKCY>YrG|oXOyNøc>gklwgkuz!3se6D)[˾SDsBrY"?([~VʙJ6WjP?chUޫ P8{܁(AljW` +[pu]ؿ^mYԙ8 E!HᐏO1Xܙi6> Qtl>w_/C.JXO8iB!&/DaP8 TLk,v .7>;a::K>6o(HbRz#:Hvબ9i/f1wTmј-[ ԛ9A)Uh{Ta28:>GztI\RJO!jro(Ld?NNjAIEkc- GN'j9& PHEm%5W^-Epȸ D +%GԒXjKm7|?4 +3]l_IPn@TEZV6+ݠRdZdX@¼ٔ3{aX 1 + 6gfz &"2Vk!CvCCH1 hD{8P[nu!3H$lޘlA~~ۿKlNjF5vZ5dMh} G|fC =OZx;G!-1T,uT28͚ v,C :5RU9kq'"jBaWCJ{Ǹ="LC"o$h{AfQ`?jXi\~By[w~W?sdN!SYlHsWޑJ!fEon"ŋ[DbW>Z;4'/Bm`l89vǎ!DJH3*b4LwLx+30B.n4`/v0Ďͭ~7bfW쵫Oh#\b#\ܱOzJ3D Hxߥ[7NJ,*5Ԟrդf 7CH`倫#j2lrbE M(NϐgWr%=w$^$v +mc2H)?LN:y b2k[})Ir>za-eP^K=442TOb#%t6sƥ;_--~{$LQ:h\ML!&8 b':0jAќBݻ.gK # OFQ4j mйvL~.Fn.Q4~wxHAPN'ymխ@6hh8VmۓD" 4k#VgTTt'Q[<0m>({>s1]RIfL yW^hJ2S0J=L]j2'QnTy$9qɏ|5hkMA7u\Giב#P[jx~]C|W + -ibHK6ʦN h#I؉j  w,xb3DR=Jp=)h++$5nDRWˁWI.VCm(K%ţV!戊Aϴ*]ES>XRxOZ@~=:DটIh#~1~7u1([?rp+ ɊВ7 !?Cpֵ`0FLcbzgM;FmT$J9,<-AM#53q0  +1"պ#(>ЩL'# XrGwxU[ԍs͚%uۧ)}G#L"/! :S}bUo]S)IU&p/,j8b,~gHFMv)>;ly0| 9`oKUZO [ TiLQIB@уt;WJ_.aRS D5Q7.-p%-R0D> +ȰlRvX~5hcRaL-ŠTs@+mI4G'FRTTtVnjIH ZUW5[Pr+trZj]*v4X/IX +Y' 9i[V(o^^pOA'ɣt^6rŬ %H‘e UIJ{d8f;CU(] +ч0w4b]u%|5PGXiQIpi>hh=r~ɍ\,3;$A"QvXU} P3oO@Jj D : +3pjdJ Pprc*y}{#%K^ЫA4{wHqЭNTNG6Pa4V/*BmQEPWf0=Gu2d"fg[#=JM~ @ǜKY# r^{_'RLXxfHB4(S^"?CUȲ״Uj:Nר*[#|.6ΖG"Ȑp֝Hȃ?&r`gkHBB.ID+$ pTu6эSir#%M2,tc%z +l],Ш;r"dwLޟ$5F svIFi +?bN$#F ƜXUnB;s\o{!M|϶1TEb5٢@(K.5c#CXe?"lVaw?8p cq0_.7NwT +7U<#6Tt3$?*>.d>"`A5 B'8q·MDmM9^@u&V' +YCF!LHw#tΌsUpHTƽ%jKQSzOM%̳Џc%:srDV=iu],gh0u%-@Oq3GQT=aB +@^Q3Y֊6"@-*PL5_АNw>LtxccGn>QHh`>89BΙ")fWK~'Lbf9?d(V }K-nט~?]#A; A`3?]>paؙ# S'GX`\Sǡ=u?? +9Gqϫڦ VWK0R)7X +Q!gEEo[!!pHM9ҲP)ԒYjeg4꥛,"x +2м1;K+.6sE8=$xpey.4cVD^Z YrpkOEw0;yYQ}RܩU_N`4>Yb͙'Vy{&sSE3'<%&%R5UC9>UA#!B D/Z.b;Bxࢢ]i{$d^- {i>TʿN7*Jy,Ӊ+BZ/?e%5푋Zn.[t(ea{Ň5hAȐmEz"zطe?z~( V 2! )%J{3;LpA\&|񩏮e|`fOi{!Sw`}0Cu4BR(oR xot* .! IwTx{/BCdVo$gl(P5f  ;9Xήb٤'qF#"c4P: +e`\y !㉃ p[b̈wEaK08%\g8 ڠY=nFڃulٻS0"!-nyOX>J6 + -eS``AEW̌nosI")$M +"3qNOHpϻàԸ5DDeY{c?WW'+/aDa NOK;I +~Q. TL&gnT?Y@}zsfFYq<1!ͭlT_&C%G֜ͭ|O%GN^%}gԋ1Kuâ>V)'RC?)yw0#;l1l47t+ ",=K;0 MEB+3N:T3܁p ˋQkWo8L%}`cK>(ay`k`=B$|AT{xY,2bbN} =|SkZ&rR?3GބJ'44Wjq@^ERj pJqJiHثȰuE/qL@3K9T+ha[dQ~!5u /֩ J=<.W; +'&M7 מ:(^O{OL +r;.aP)S:k'in _bDd1=@X539TdObAn芢@-r ؊boA- :TF| 1 + -@ t ѯqnsD#?9![ +/ᦿP[F޳*S: h*F5t#e\$?$O$&#+!@!S+ 3\gUmz=Šg.xro%5Xf/;GTk(,A"!d^"7 =WE"_md{}_9\ݹf!c "CJhr-6m'jQ{2̸^ +6V;`K¶+ژ|KD؞@T~N i#Dl]u Oq#h<~O7dKqLA%]֕L!&9H ֻX훆rgR5cM}I}5=w TqܲUuU, 8*Z4?]oᲰ7F&vJDv$߷ +(Il1jM G *>BHU?+C-9 +ҘsN6lV nu8s]#'jA!N+rf>{8l$md-jD%ٌC:'S%Lcedgm&˗ GO׷P9V6&X@F#Ǜ 瓐rsLt Z}vdFG6~DocD א#Ǭ!Xq ނz +D2'EWSāň=$x~=>LV -VʘB_6AqmDY'ek~%L{F>?\!c*41|>H}|lJAYt0SPΜFtWABF>q#%ިcinrzV$EG+;{Tө +-&!([| BB@EO L"SM ,h4=M[֣stapQm BT7(("3d )+pSQ"ZD ⒕yP4DO+ )ψC-ቺa@y痜2ݜuw܋kBFIH(UUW[A*B rY}i +kLTRDq6JhmN_\Y/k_i(x85?{Ow+\`н 0A22>wk䋱(DPqjƇrx'PwS(In8'#O +WT 1⡵UzhIN`\ +=`DlI 78ROt-ְ߭p>SɔH9kߙ?+TPT: xpXhOpw㮯:>B:㧣m-첽hlE9b[6'Y_c_!~gY hPQ!TYMX- D$U;Jԓ^5-u;L|Y+1'8zpPcVXёAiVO]&۩ȩL>ҔőO*0)Z3}P>L^Nn_!`>JfDU4ڛޡb[R3uBJt{ʷ흟UV@Q "OR'y/G GNy: + + bPN/O-_!ߌ{mu$"ŃN3) ~_u8kG!9yh+]+yS +#N%o =3bV"u^~U"w$P=aB=XE6 d=_Z㨭Ɲ:GS[v~sFpHqy9Wdh)\#~*{;_'#`m=M'_*;FyRn_C7~BEy37oJC 2.7u!J]!7liD.Zbg6?ywGKM[ +=<€uBBO~ KzC'0 =y^_AE Q\paG0A-m"/̥x$PkܛA bL7֚u?byTu>9Qbs84 +)?wo(u됙7]1Sf`3x*Uk7Vbײ'? +Q1?0O'w0v$P4S3ޔ7>k?tڡL +~U =`{ \u'hjy^b2)`MSt+ʆ1ߙ:A2B"*Թ9[sb#&AҤ&A=8DEЏl}p`ql4'<Wr#S">:(('ЄG +%V #YVT!޴T>J<(+ K>jꎊ^Y#0baamwtf *Pa,7%^q|I[V + $Az͆fުٰQDO5Fyd-PD;(2+Q('U슦L2bEv[!կ,,"\-FYAyucbE jzEB6|Yt,ʝpôd+đקқK Ch}1_(,jULSs߬/H~iS0?C\_& aI.:d}o3]@, (*($?w^a-`F}jrD:ѲA7l(\K[ HwpEV}${pؤ1wt$g=@e vxQz uїV#$I]}=aES!2Q +ϴJAER9 +APt<;qhB1cu#H;\2WuF\Nљtǔ. RdxGu?;w/+;&d4X@psfwJ=<+,xJr$ +it/O+RGTE1W\㚫v_t~=PeGlF!Eu6m5mIfJC KKyȆ_6s0۪uBqU'41 + (PAcXg0J0Cu~{n%A F2h7+LQ6xUSƷ86׋wce@jB8xAVb{&5Xu wL# )_a&AQ?%H|ե{C]w4}H7iEİB =tʆ +G)d&+>tFk; +`+`"rw+4!pyͪ#ΣD/lm p um2+ugE2ȳcR w~g\´WܕӾ^3,Ǿ(%q>7fĤQm?+~/I,~Si@385Dyc-X{&KOmg:+@Y$鍆y2@ OH5FЫw:;&cقz,)v!DkɎ$5+ [i~8ִ.pKQ^_=N$PPAG!ۄB{b$1zB5ha64H +&D>ڤ(bWCC8;SSR m+&~^PܝzE] + jŗȸ'ZG Q(簡+kJ +TD!گ N yb: >|3DI_+C읅}Wz;2-;1]LZ6ƓªDIŀj7d#D1"K|Λ&AKL3YW=_mcջ0$O`k)LSz 3 $VNT~4/I,UgSЏyIz+(3j3ڿt-M_QUm3Pv%;WY#\KN?G\rftLWJY1\{ rA0 Hn 2(mG䱫8p,3Z߰1oڷK;M.j$qEver{`ⴶ]".{C7uNNjXmў=H1IJi<zVhcV!=wTzuuZ +#[=f!gGcM+V8zZ DP;[|k۫&pQnVyeUnCuimWiW3 NzĤ|VPB:hS늉*DZ,jP w:w<|_dTKˣ)׏w _iD*:Bg@OT`s3q{5E + ٔ>[Y鄆}EuHN1a8 J$d~!_҅}lX҃_lqNd3Rx4vjMgH~\ ?\OwW$g@<0d 2t/;*- f p;&%l(<R$hA.pD颇&XTBp"SD_77pt1l򃘇AGq@W+h/ .*hf{#M{óf4#P.5#x?(ؑvɝJfʒE,y7ڻ]1GfOf~gHrm88G q:C^А"{0崮F )pP +4D`hzDPWIav"C{yk!'Q*TrA}IIut>} >?gzUu +iЄ w,tG`NO_O̸g=]>ԧ<Ų HB;C_=)B%R VZ#6OO{f0ǫͷl#Yu!P"h=5ڹV? +42<\84G4({iWUZ Ey璘J+.]A.r2; kA"=Z zQE"eGWSqq8`\`;CsxɍH1A\`d;UuAW֞]K2;CTJ򘡙@/?/]=1 "JkjW4Z^%6 eoz /wNe#? V!WTiwww ক<ntu2>h-ʦpؗ j4F%&bXJWY`re 3L<%K* @b;1=BO?j̨ :ƐxC#fOf=ࠕZJg_wуVMyOEhE!`) 0sXVk +#:p]z5C"Ghݫ|x@f33p2d]\Ȉ%iMSEt]KFjP6ɹ>@0~7iy` +r#Ptr(2MjՐ-[ +=&'CVލ8B.J,WB9sDٙh36 (raKH {_ ĵAD|~`x}3 肯ȇA;H@MLʁRv#-]D]EJE.0r-ԆT#IL ~̩!=}wr'O.'PACohBz|`;)$ve5u̘^})eø"IDy]N:je>[_q%7^5o읣_qS,@{tΖ!.$?8PjcA++Ѫ(%F6{-| C8s*M5N>@QzxlEGB⛘㺊_?3zJ(b(PW#ce*|$G.s??5g͐VQ>U>wLƨM#5VW"NGg\6U ʞ!_Ax`awѝ?X sA*jp}J P^}tҕ@LjumgL YȄZ*KfVAsyvF?EfB0o7 +u+!X +œهc>o8oGy~iC5&٧X\ZI^_nqkίPɃE!O;65!L3@P})Cz)bgvfR܃vRۡF5Çaف-'hfjSKH}9G+VQlKf 98<&bDX@,VH73,p@ÞB@_5@{F6sX";Vq9XbUf@c~Γ&Bi;8U[CDq_Q]MIhb?7-= +"+ ܲ{xB5-# EԒnː +(l@ y[eh 8Ĥc2 +y҇Pu9MQ!U +2?A?p2aH!B&abq2C-V>掽(FUVv}rDl{֠6PļvTbwM[ Q¢a#-Wd{jJkiC/84XDEYWBSePEe0NLxaA¥nш4Oί"Y*&{^aNԠ>R8Y! ogı,r4/y}סNU )!UGA1Cѕrityv}_PBw92;VsRE]%ءv֯!+BW8W5ɡF2^Ӿf5\a"Zcir2w*XwrTb4!95'%Z +K@?KThuRn~R-KOto->A^* 7tnYij =[u {';đ4yb0g|r?d#Tc7 # LC}mQsoXqsq)mnUCe: r2ʣLZχڍL^5dW5X LB4[ԥ*$$MMMKKU3^)#u&y9?@E~Ϧ E.:);"+ءs0#j/mw:vP$~bwmŭz/!Č~8-DݓpK1HZ*/vbŗIKABI(p̞qC\fqD,vupBo_KM^v~sO$UsVuh3EUQL<-]6Slf6_v}G8o&qNTD;!,}AlvLUK:;< +J!+bxs$PCZyq& 'rhOPM-Xeg +CdMweL{Qa@G3,]8|m%Octjj$O {"D*ffIJ\==}zFH+Ɯ'Heʫ@zT@[ΊL!ب{_@GJG +%HǨUZ*B~)6ߒ~=]E)),;kϽZ6{du-_/slU(?Pe6?`Wp$&|\n&gnI[l|=>_=W1HBF%̇q'aF4"By+ob7ӹ'"g0O:thvL-t5yR2Fǵ>7s9_8@8>}&dX}%4 qv=_ޞdF^^Y0AN} + +W<MU0-wq; +7+rrfVzx hFqGU~MQdtE6!g1iٿ3{{q/ hg?ˈJÎxČLXxTuf=%'w +{ĈZ Ԫr-uVmdF8q2jD9τH,^qLn/hOWh[8qszjTsμo>R~Y']Sɀ3l@ *? QvVH+YڮE8>;ʀzVj^Wb޳8 ͫIJՑPC2Q" ޓFJ.Aرg=\Y*>`X(B40:b64PG:{P$av WPcE +)wQ^CSpDwǹLsd䨫"b;Xլ=Yw,k?&8-ag6 CG!F]g;0z,6d_8SJ}^cS~S_vz]cc^5{^cHΗ~G"wsu7! -Y(.d hb 98*4G QҠ\$XsmЄvy1 If]k.FlNv̢Lʍ9>vOQ&w$^ԐAqVbjꉏb‘L<L{IJU +`I!1H:iQ1Nn}Z'ᶟNYheb332aR79jNxEي둋:3^kG-SI, +un5p8p{N r3onL;=:juXR^P<&[d^@p_E؄[ :KCI@4QsSHY_b"sg.H=Q6=ұR~l(e#bE}e}ʨ]6;QktpsG% 펟0D%vScD3*w*SwX?i^vO2#0;;91@a",xYK>&>(@§*Q9ĉ zB>N֤ImR~‑zoY{;>*/ G}*(y#Z9JNsr +&f\oF'fl?swmyÝ@" jf/|1gfvȕ+:B1X2BTF۟*NB$ 뾎7QӺ[0Y4Zl YolsQ![sQ㹺mB!D+Ơ3`8[8@;^*aeHF)S:^R2 u9>o/~2LBIkʰ F}SfɈh +G,vSlPDqwXiPȔ>wnqe;fͤ3My։qF y-U_vf Y3h-`W[#濒FGmeF΂Rf $5LY}`'tcϚ1^$Gy9RD]eBMim;@4[I)ˍ=pgfNdBx!]WZ QH`:z*#k(BrGm{۸;$ s0\GgpB*ϵ&Yu_tVF'Վt8y1DqW9A"Bk!a6R[эRԙ_CH ]V:vJ"UūO]T+B#R#k-?*[&lzMc/]U"tٻՁAHI[$3"g3"A7ej$h7)GI&=[UʕXݾ?eBK7YA9>VI^xcVG1uB{ĨoP5Г]pT @{luRJ`M%/x]jBP]4G|-dݨ*> O;q2f8{3,71Þn%:դ2v@;ЈJ+ +QJdrw3QVǼjpmJRKlV%FQE9t$yw.n>kZݾfl|s ЦM {R{ "sYp<K;v#sJ)+ NNb3z .T|#R{}? ٿ~tig)bg6-f?#HnD$di4uJ_~?(L0@D)2)`D `!Yk#\Uȍ y{Hw +i^3h:Lq.%ɔ1V%n\c7 %,㹋6Н+\!C$ipw 1۽ Wࣃ;<;Cڗc@|.KB'V(8+;W+}Ji @1gI,\=TVJ0hGw mm'{O/XY4a֝*S/gs=ڒ,T5#0ʃC;u5/ΥȒ0; Aa"w9%;,#TB6WC8|}co C(fRWƐ\-hB\-±戣ݾ GE/c% 򏽫&{z FtHqR u.>F^7ņ%w}'5gj0QEK1'ñ}S*&Ze=W ~$Aw3yOAqvGZK.+qU5EDM6B[1}c>W@eαP|%0qDj+*NIzjQn &y\)_wInff1A` 'LKrCw"%O>o ek AZZ(odʈ^kG.vꏦk~k`_~`_+Hh:ɳNA-(WKx֐,Fk˸rH\(# BLH3Z{(%_i?dg#WҢR+=`zwB+\ץ{ih ^֛y P)r?ǙV @=;gITjrB%k oΖ5g@=.2Cb/0yi! `]W@m<˵~]w,$[V”]PD{lC%z։ )S֚])(bi9MXn|~. y&z&x~ +(PTk Ddy{PBSwDuAA9a#_]?:3u#GIDD;-]]tAxwN*|wo=aW qtU=S<Фs&^zH>q،8̓ɒ]਎M> ~4Bnh+ +C\FR8+~>)HcfC +(]k9sh5ECOJ_=|[9]luz%J|׏zۧzi\AL>DޑoAi5rr>RTB!`hڔn^7ؿ3c&@*!%:~JxNoBj`IސIowFۅS\,\B};ݯT +ڵgoFx* +zaz׮ o7 j0wU\9Դ +֏@(&+狤תZ/ <*JnDtqR΄Tr-V{>waSע$f/A6z;af#c OaT5'US {6~Sո0TK^kh?դf6o+|oȤ]AiZյ"2])"Zaj ȉB冔8?DE`iD97U9R&+Sdx$n~̮@&lZ.]"37)ekԚ͕:>CȩG^-]9>O?Bmͤ M N5 j#hDs1%[A蟦.)`ѠIж-0 6 E +k+{6̢r9ԸYGIA;An6߂b$L~&jNtg9kw\'ijP**[C%H]XjբB5PR\-HGNqnXFMogz?|!h}rA-)0( +9w0R+yjHGt>YW(_g:.ׂp +7y73<,$CSO2>~卮&\B%g;j:w;6mR#SAŎx.+@-=5|o%w9:, Z(y֘*.ʔ4~9U| ֑%A`x60n5Vvë#~]8IKW\QD-IBfE2` ӝDfйI! l& uJFC[R!µ6F=p~b'g|k]/C0<( nCP(ManKR'R~멄L2VݷrZRk+\o +ݘ.xg@p-zeܲat}ujqz y(#apɘϞ*%5#60NI'z5pYʡ}J]Ue5W +{FLCHY)w3)y"n 1X_|3 hW W#F6J)uyI b:n._5$&Kd\S=WL\;Hc,>a#jYet RwY;R|gY<JS Na8{aQl%c9HrEK#-K7qkeUsMyc Z <*Σr9}x=*<47ڇ3=e9C`% "M< N⵹'g 8Lv.cӡ'z9H-)4NcΣDf Ӣsa]>i8 0bBQ*jXїu+e_J/ KߌzW + +dآ<y~XH^g(07Ė;7uLba >{TyCL ^C?]oo#~ʬkSXDC5wqNԓ@ lT)\.",غLx4VH p=uTg|G.˺[ }TOvdq~ OFL` )骪52::zwPRY'-B.%:SR.[u xt8BL +4TD뀀xIW *w/,FPAN +x]ieUz@[Ћ~!f] +0`&ŷHڧAS{y ~M1:mN 5l(w#kFlŠj +"g3_Yn6O 3HqzES1CG5hP'yagN8KP&p(k>[`F R{ik"ABN%1nt)BdJL=_s<I)U[췅"2TP.e *a!V^oPPhL~"t9D|%g{yx'VG` e͡~7VcztӠ}鱷GڒA1zdBBGmNwbqmؒoڢL㵲b )Hottc𣰚蛷UqJ^ﹼ5ģev|1AՒcgnAҖT}@/ o0K=T_+ϒx!jfЛ[Đ2OQg_ҦM2zs ϧꯀrTrQ\V\'eMJJT!.2hI["z @&蘐㚐#rR+Yg[2 mF}&Vm}n;ڨgjpPP&r!=HRl@GxٚFjDU`Vm4#׉Ğ :hn33D#\5t\H=b_͠'.~-x?AfgUt6eR|x"yp[$BTVF s+@{z;!]~+; +gU1gIPZ,GIHtBZ`;:a#bqVoF~DFd@M<0F>z&=ÇUd~SIӫ˞ +$9`ʢ 3xDFLHҤItXZ#n%x$bSOH}ZyĚn`_%oRƇlrzH+QIBu\ci帰A{)E.49h>Qf04~wxyHD~ Zi +vXpFVzv1Z +څ+禌.0%~4* c `A#\*Ŭ8<̌6ȱmk -AmLn-;7T)ςJe )ScQ3`Y:+Od)%AtF`}YUcgp'd4##,Ai3Չ+qFEyymUTu@CD'*tjXrqn!vfnZH7UUVJ!:<} {čot>FюZwZ +&<.c o^xQGLqUԀ׀0g2|4 +B˸.m_6hR3bq)W+kS da`6c9=Bƒť)@_'# C +r~\e& _VN>o^J;_r~̶ΒzI\kmE '*'GC2n Q=slH4h-=©ٜ hGp&-0@=ʔx֛:`'HdchkI +1fԣQS }C)ZwSeT2@;KlBVB+ێTT@"#`U{/BDqQ4i +dBnUPUKl럧pM%Y(ȉAٍK}Tt~~xecz|ݼgJ6af6{lF9A +f"u =;*](kׅM~GݏXrUTT,w{F(Mx..!HFժcVe;ytr4,?w0]\ 4+X4U 9Z.+zВ[49~jccb)rњPC' ݛ6J`FKJ mIU"TH;fRVdXK0!^TDP:ByӇ-D޳떓OPʻ\ RYky"vL+gnDwrͱ,:QgېHxb:߹ޕH:=Ε鯪_`Ai:7+f1 V0^M \wc@'FG1 g.ԇ|Y-l{<ج0j5kBR5:>6t%P[#l|u!}-zn#z<?",+U'2q3 Ow] 98ҹyOv>=5{Gv5XU#=' +%?N|R%"2,32#O; {:(R(Se1:VfȷXIV6,K$^;Ֆ&'5Bnw[  ےcWZdB|Jh?& uj}xe*u0tS~4"8(8?KP6cEj rkyzFG^)%eʿQKm2(0$=Ļ0H;Nw„rIm2"M9=[Ժ +[q_528OAHi&mhGid{J|s`~< +ጫg#D;!] Tr~u@ɾ{F=K F ⳱U' #/,Z8~(DhX::ˮ')qNLOixw5PQ32;KʊkS1G>-{n?mETRbS.;3oyJk0  g\H=끁&r)QbNB$4ʯ9)H^(C &8;H75+rLTV$OgGe)5}=67T#to `wuh:oQjw+u`Gq*RѼ5fGJ!-SGŁlUcҟ!o峧w6|nEҰAI{5ַ\S~G}VbTh`vzҬuZ k| y,)diݛ6 /g)Bs%`t&U~Q_A!rɩQhB!☪;" +-%t?H:v k;U,O.FkH/Sn )A?O܅XJSp`eO*LA%Rۛ@ 1Mm?zk~кa0(1ga + #9ơz(9/LpJvlCG2D.AIKF;-:J8TL(r?S첁ʽ"ҪArI +Bϧx֯"Z?l{b-818t& Ж|?UΖ&@4g0oB( 15vt[zCdb)B)R^7h-~eVV.tH!/UDiizipaHAp>,Eڵ-VR.(/筆/7_\DWWcұkYBl1 4! ٠3ch__kf~=1w]5'dEEj+ v燼+ˆڦKP +W8 c뇼 zCt<l1-T f$ m^k !kK9M0gfNW1ùq~΍ȃoNFu>yG9;|+0HU#*xv%GŽ1HM{i" '8gQ.J1D̡$QJ$=65R 9t;Ew Rbni$h5>黇4)J0}|k9vIN!ODL4Xk%4Ԋhd7\cfq}Ăh{tZfxghL4bK3qcW(E@ 9͛s!5OiQ'5x +?%buX GFG~#cK^!: /h59WB,ͦ'h+ޓ1Ѯ+BZUt'bGCwZ Q1iFAn?E.}kϹ4՚rϬy;#1prRn_]rAL#ig Wz,sPi)Bm祒zkFe"{>F+jCHpSgK׹tCCpMB*ќw!U}Lv[WḴw dL DfF +F9>D|Qqq~.bxBDK}0HosFE$P+GOލU8 +֮"g.]lRU?.'2VqQkK≔}^ْn7*Jm:ʸR9Ίd +A E .grq[,zFϐDqx&ޡAsH(+v+!65џ ^qG+fOп[WGJU)%y3fkGnlA +*nP]7,NXw}[=/aX\UG1_y:{8(NWrThS}' +̈́3Z=nBaTfm")Vv'bF@9:; vS}Az5 Uh#Fz4L'>3U$4բ &GI-q&(g,ʞ?PMr7boXdm)Xt~y1FxO: 4(ƙ ,ߡW0}?dzL-tr~E7[ecC[V8G{MHeW9 6ѱN5c6cP#DO ;9(`O#A˱Lڹ⚪3WVNHW#EH0quV)d`&qQPKB 9<`dT#1}FM +RM ;ބĹ۳V\w~o2rw־7o].%+l(Z آqcj c|_jn(n')L՗Q^\3*dTCZ{Ǥ ,ʠF֍onܤb %y-Ƶw*b~5OUؖkz* FkxΖqJ"-a>:zqu8F h!;8)Ez<&"|7p%?2Tw'`WFe(iF;3#}fH1v +t2Rk`K<9"vt]&Ӭ/$mQ˳ÿ%1r:u0D%Id(=;\.9sHlvGL׮]0V"WxO&w8UN":; fWآCKIፇ§RsidS{@]?֣[B剓Afngo ãXa!'u],-\A(VbXl%x#BO)>UN j)멏rg" a +PC3#8Yx=^ׅTb s>р$EsKUs|7ȔJY9vj\(r<շCqbOC2ԅdqjA%KȘٓIuҥjV\qSλ+>|Щ%02tͼ? s%sSAdW;؞:AJl&}mGCW2w8NpgG=97B>r=]a?-F<4*?&pԝ% qbp-,vتglQ3Ur +~53/*[@3eͫTeIr&SC~Tsz.ŝ'}qN&hzdc +=HL>oΟ__4Ȱ0Ov% S>W +6qyTL \hH\?M(G>_XFm# e1w\iQ3Jykr4FГdF:IiiF)ʦ@l=%JJ; }vg/|G MBcuQz>E8F70Ow\yި6¯1$ޠR>W3![n^::HVTQ)D:zJ,5+DђGc:e3[8;[՗iѺ=r8/H yd+^ CMch؝Tl-l?53f͉2R"o>xbbTtg9}hѐ kԎؒ:J0C5`QWT%||dN"<W8Ə7$)Y]@,<~[;|I +PB>4ZO|T➈B̐ ej hӵ*aQ +7@aEDȐyV-FG$/~w/q)r=Z&A&AT#4~;MH +DKm9)CB<\A|:&vYtWGNtSR]坷:cU^_=>Jh][HO(d$`_}i`0c\.-/A]`EM5ThK$/X-ąa~HDm;VQch4UO `CvC:)e +;g&7œ!r.]h2}c@U~2jӼܼ;&;~} P=wfN-ABPW;_`oQxM?|1-UdUWM =eVw*m<͆+J&e`* JJ$^b`il _@OmwLؤ65m9Z-Z] Apt2Y4.dAlKPI,xbz +g:X4kD\֠NEKjK>\+VA!>K kgg$jFH?AQt+w g8e vxZ .89 +ާ1ud:h:Rx O$ UIfzЬ2BX(N|"6Zk/;yZ_`S +}O!poJ_Kc@P!-GX{82ۙ+8STt'o['A&)"J-5;FyYJ{(b|( f })DDŽg Mbu0>hRI$8.qv>xYk^ҺšW)ǤK+@8 s*P$}.cgiP뫯!R)Jkkn%u%FQ4и=w;~*-iAJ 7+D|bqSQ+`sr.KI(\Z_{(E99Cz +"U~:@St9`cPQ(# 1d]@?cg`k>i ""o]#B]{Ux0"vz{($xy)\4:߸ZM4?,T}ifҀ|Sq +\ .J]ǽ9RлX Հ#.->%3Rl(;5nɅai7(c"/y,|2e8)j[f #0|5po_#0*wAѱ0}ؓ*i!kc'+@ݩU :W +ߧ8dϙ&5v#o()r*|/iG\̮8H(D³A(iז*epBQZñCTaas#Ҷhmf2RkE;ʽkiƴ znr&>Y{96J GxM+TF{ 2c:"•d:W&^QOX$/jG 1jPz!Hl;2bJ |{=X_xWp 6O#  XT';*?7}A]pb<Ld:Wj }UV_l_9O"I13N&gB^Q!<x;}Flg̋Z4_Fo\3#֣SqVļ2+m'DOy ޏO b4*_K- ݶFe^C X'+43BGTF +7 ǖSCȐP"7,]k+4 .EWFYQ8@Otd`Jk%wRضeGtc[ܞo3? W`8~:JK"\] btܺܬ5is.`‹%R#r-Tmأe)/ͫ;1`VWn8^W"A~D NnC} ?D 2GJoj=F%bNB惝CyHy_K|DSQUr`fG%4# +^.DHidRsF$P=0"G H]N]00g IY=۹!#þ#`'2#S: YyC:](SSl*;مx*)X|Ņ- Z7>$BO̥!my3-R9̵XA9w ټ+Cҁ&i1}&n''*&8έDĢ=#_CͯȯZ4Uէ̄(KD[K%U}ZS'J>h9yL?r[ޥ o%UdڣϹ+q@Uxmdُ`%.o[ +a:vNj&f2\)h9So`9u&n#c~uRS;U&e還1(/㘟vfWf +D(ZǒWODwy(Ud׿W> =vBa)X]<;ڨaBX- (A!g +J0`fk*ji+%?WzQݼO|.MƑW+g;>[|Rΰ84}1&Ǫ E#;RB +T-Gc)kqh' +;GI.BO~JebJ8ȨVQ +e +Ӆij᧽udBV6RP&]zC~!_4^b]ZRxS=M zOxgvU%3)sM inl_JU8]\(ǍK,M;eBt g<,uqΈ*fw^7$Yt /\@ФG/ɼ;) 7,ƋD= K  yFё _ZPpԘe(/IϤbb~UAݿE`h6Ѡ vYp.Ow} mr4Z u}EbWcf:ahi( >; 3u(,:X~>̖\9J-Wa(AgTY8 YvA{ +55 tݐ>E9C'1R"yG4ѹ5 D +:_ʐj\NjC>TMebqTѥDoevlg]Cbh¼jc~'ha~!qWL^TPK*a'@5(5` I<[jNw%= FŢ:L`*Ɍk2Aj?|Y3ڜɰbϛGtz]F^;T< \Q$g Ե325Z`-̰gW=Q$X 6s|쎀8Qf1g A֎ܑ)7\lo,-N LYbxq.)e~5e%o9'։qS/`'1AГkG}:/afItNލ',y %L3^Nn3C'r-mgDq0C}I>JШgiƽ*:/ʺ|ddUQ"J]CRїS|`wX#-OoČ\O&dƖw pL#1ShJtUHj&~rxrixAe[5jqMlM8C*ԛs{CL!.YEG:dx:Xn1ru"`tPЂ +^& 50B{7J%\zf.Pr9cb9vAkSW#ЍY`t؍tQ1I +Fr F]M,&VR [f*$ϏE"J:;+D(31+!Ks #NfԥnBNT6]5yϣ5I|h#p{+]|KǶPDӊN=E~6yZM{GVF֮DMtW$B;l ];w@Ed'챚3' ףʳ١-~:C 4쬸r%Ǎ`*f)E|M ;S:XF܍Zn|!MTO -m_b|D@) f*f-7UAy_-qY{-PoǶ;@䆬pm=eԳZOmCDQed$P̘I<|ss-+6G|K G|1kB)UtubwJ./;1rtqQ+qOq0p 2vBȊ^SsI1}ɑdf;N!RZ |x{SKT6^%Bʯ {NdTDFVl u@Rҕ_9YlŌ.Fq-W:9Vtބ+VGx @ըAX-FӪTl=L{R$ 3wQs}kpk㢃nBQ)s,#L/@iAPQ_v@ oaeȕc;oe2 += +pXL.!6Y,{ ` ^C*; h9N/bDB+#苤V0ugj QϹ`HACq4nG6rq KSLaЗDjrz\d%27|ޱ,:y1C/5w~Qzr 0!Lq r+oM,F/N)ۻ'sʽԼVQw7aRS{it&>'jC +o(oE{Y rBԮr%2[HpE#]mpAVbΫ{tYjrBMy-}PXù b;+,EH!˯4zVOo8 +wƵ(;rpK0,sgМw3c4GC쭓%C+(ƦȖJ\KM.h) +E4l6;1u\3ͯd0Sx=3"Pgv3m8v}T´,HHCtz娦GK$QZm{RC>-ZM,!ZgƒF19095&Tu¿X9ѥ:/C(Lr+K)q{kW_|hn5FWDv3Tt,(]>+BY5ob>9Asmf>[m3#Mvaݼ9$;m53jftm,̺!y#_Mǹg^@Mb4VϮTi,GM|waYqXyV^yTD#Լ*;3"؆KdCz2W\/O|uRis"}+2*܉yT_Wh)gѫ;JW?y02A:kHeE~Z-X +"!}?OMBtl ]bdEԫuqU,M~ n!Џ:PJM +OJ uyie Fm%w*4#[d֚(^:#[憄訿#?“/\Lk@Q4&̟ U;{"quy:UA5yHfT u +w3Q3dgGn(O3{$!mDVZt$2YZ#?2,Ar&jfƬ!WdG& C9 k6\* #\ACoiJjz2׆ +VT YG`% QyTӹe,̇`}%Щ3uđ֦3f*Afw&TiKe~0`oLTc0U :X¢W)]2 ǃׂ7#yX??pJqXȴd=#M+JקI"E]S嘟!v*R޶;8Z6i 9'?@6¼#qM%'nu^:| 5 BX!p2odA3|8cR)# Pݡ;r6$\6;DxMv0W["(Ր7F8vLR~=*|(Ji(?QŽ7bA@Y\Nf y(GDǞAcmL4#2ǂ F";㉅p$gX"kUef*U5~~nowۓpʨAt/l>K +P#R)-`>]&go`A-ؒiAɗM<&z^80*0瘥+ 3^(%pȃsSQ?uSO~*t+ãSz +=d}6JIvH6l l!gֆ\tgfAyKӨ)]4{P ?gΆqҊ+q,1ʥH)O;QODjdr"SEUsNJq<]fA!՟ft hu:t1fGA\IJDZWLG!Yr>Tj\7ya+xwB!R "b(~*^&:{8 %M#w\VPtM(\Aam uFm~j3޴=* u27>TOd@n endstream endobj 8 0 obj [7 0 R] endobj 52 0 obj <> endobj xref 0 53 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000061431 00000 n +0000000000 00000 f +0000536092 00000 n +0000535728 00000 n +0000535542 00000 n +0002188030 00000 n +0000061482 00000 n +0000061895 00000 n +0000548368 00000 n +0000545685 00000 n +0000545572 00000 n +0000532323 00000 n +0000534981 00000 n +0000535029 00000 n +0000535612 00000 n +0000535643 00000 n +0000543744 00000 n +0000536334 00000 n +0000536591 00000 n +0000544065 00000 n +0000545720 00000 n +0000548442 00000 n +0000549162 00000 n +0000550518 00000 n +0000565801 00000 n +0000631389 00000 n +0000696977 00000 n +0000762565 00000 n +0000828153 00000 n +0000893741 00000 n +0000959329 00000 n +0001024917 00000 n +0001090505 00000 n +0001156093 00000 n +0001221681 00000 n +0001287269 00000 n +0001352857 00000 n +0001418445 00000 n +0001484033 00000 n +0001549621 00000 n +0001615209 00000 n +0001680797 00000 n +0001728914 00000 n +0001794502 00000 n +0001860090 00000 n +0001925678 00000 n +0001991266 00000 n +0002056854 00000 n +0002122442 00000 n +0002188053 00000 n +trailer <<86B2650A87464FF1BEA49FEBE523C26E>]>> startxref 2188267 %%EOF \ No newline at end of file diff --git a/test/fixtures/whole_applications/requests/ext/requests-logo.png b/test/fixtures/whole_applications/requests/ext/requests-logo.png new file mode 100644 index 0000000..cb4bc64 Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/requests-logo.png differ diff --git a/test/fixtures/whole_applications/requests/ext/requests-logo.svg b/test/fixtures/whole_applications/requests/ext/requests-logo.svg new file mode 100644 index 0000000..80406e7 --- /dev/null +++ b/test/fixtures/whole_applications/requests/ext/requests-logo.svg @@ -0,0 +1 @@ +requestsRequestshumanshttp for \ No newline at end of file diff --git a/test/fixtures/whole_applications/requests/ext/ss-compressed.png b/test/fixtures/whole_applications/requests/ext/ss-compressed.png new file mode 100644 index 0000000..149016d Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/ss-compressed.png differ diff --git a/test/fixtures/whole_applications/requests/ext/ss.png b/test/fixtures/whole_applications/requests/ext/ss.png new file mode 100644 index 0000000..149016d Binary files /dev/null and b/test/fixtures/whole_applications/requests/ext/ss.png differ diff --git a/test/fixtures/whole_applications/requests/pyproject.toml b/test/fixtures/whole_applications/requests/pyproject.toml new file mode 100644 index 0000000..d3ab7bd --- /dev/null +++ b/test/fixtures/whole_applications/requests/pyproject.toml @@ -0,0 +1,13 @@ +[tool.isort] +profile = "black" +src_paths = ["requests", "test"] +honor_noqa = true + +[tool.pytest.ini_options] +addopts = "--doctest-modules" +doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS" +minversion = "6.2" +testpaths = [ + "requests", + "tests", +] diff --git a/test/fixtures/whole_applications/requests/requests/__init__.py b/test/fixtures/whole_applications/requests/requests/__init__.py new file mode 100644 index 0000000..300a16c --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/__init__.py @@ -0,0 +1,180 @@ +# __ +# /__) _ _ _ _ _/ _ +# / ( (- (/ (/ (- _) / _) +# / + +""" +Requests HTTP Library +~~~~~~~~~~~~~~~~~~~~~ + +Requests is an HTTP library, written in Python, for human beings. +Basic GET usage: + + >>> import requests + >>> r = requests.get('https://www.python.org') + >>> r.status_code + 200 + >>> b'Python is a programming language' in r.content + True + +... or POST: + + >>> payload = dict(key1='value1', key2='value2') + >>> r = requests.post('https://httpbin.org/post', data=payload) + >>> print(r.text) + { + ... + "form": { + "key1": "value1", + "key2": "value2" + }, + ... + } + +The other HTTP methods are supported - see `requests.api`. Full documentation +is at . + +:copyright: (c) 2017 by Kenneth Reitz. +:license: Apache 2.0, see LICENSE for more details. +""" + +import warnings + +import urllib3 + +from .exceptions import RequestsDependencyWarning + +try: + from charset_normalizer import __version__ as charset_normalizer_version +except ImportError: + charset_normalizer_version = None + +try: + from chardet import __version__ as chardet_version +except ImportError: + chardet_version = None + + +def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version): + urllib3_version = urllib3_version.split(".") + assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git. + + # Sometimes, urllib3 only reports its version as 16.1. + if len(urllib3_version) == 2: + urllib3_version.append("0") + + # Check urllib3 for compatibility. + major, minor, patch = urllib3_version # noqa: F811 + major, minor, patch = int(major), int(minor), int(patch) + # urllib3 >= 1.21.1 + assert major >= 1 + if major == 1: + assert minor >= 21 + + # Check charset_normalizer for compatibility. + if chardet_version: + major, minor, patch = chardet_version.split(".")[:3] + major, minor, patch = int(major), int(minor), int(patch) + # chardet_version >= 3.0.2, < 6.0.0 + assert (3, 0, 2) <= (major, minor, patch) < (6, 0, 0) + elif charset_normalizer_version: + major, minor, patch = charset_normalizer_version.split(".")[:3] + major, minor, patch = int(major), int(minor), int(patch) + # charset_normalizer >= 2.0.0 < 4.0.0 + assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0) + else: + raise Exception("You need either charset_normalizer or chardet installed") + + +def _check_cryptography(cryptography_version): + # cryptography < 1.3.4 + try: + cryptography_version = list(map(int, cryptography_version.split("."))) + except ValueError: + return + + if cryptography_version < [1, 3, 4]: + warning = "Old version of cryptography ({}) may cause slowdown.".format( + cryptography_version + ) + warnings.warn(warning, RequestsDependencyWarning) + + +# Check imported dependencies for compatibility. +try: + check_compatibility( + urllib3.__version__, chardet_version, charset_normalizer_version + ) +except (AssertionError, ValueError): + warnings.warn( + "urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported " + "version!".format( + urllib3.__version__, chardet_version, charset_normalizer_version + ), + RequestsDependencyWarning, + ) + +# Attempt to enable urllib3's fallback for SNI support +# if the standard library doesn't support SNI or the +# 'ssl' library isn't available. +try: + try: + import ssl + except ImportError: + ssl = None + + if not getattr(ssl, "HAS_SNI", False): + from urllib3.contrib import pyopenssl + + pyopenssl.inject_into_urllib3() + + # Check cryptography version + from cryptography import __version__ as cryptography_version + + _check_cryptography(cryptography_version) +except ImportError: + pass + +# urllib3's DependencyWarnings should be silenced. +from urllib3.exceptions import DependencyWarning + +warnings.simplefilter("ignore", DependencyWarning) + +# Set default logging handler to avoid "No handler found" warnings. +import logging +from logging import NullHandler + +from . import packages, utils +from .__version__ import ( + __author__, + __author_email__, + __build__, + __cake__, + __copyright__, + __description__, + __license__, + __title__, + __url__, + __version__, +) +from .api import delete, get, head, options, patch, post, put, request +from .exceptions import ( + ConnectionError, + ConnectTimeout, + FileModeWarning, + HTTPError, + JSONDecodeError, + ReadTimeout, + RequestException, + Timeout, + TooManyRedirects, + URLRequired, +) +from .models import PreparedRequest, Request, Response +from .sessions import Session, session +from .status_codes import codes + +logging.getLogger(__name__).addHandler(NullHandler()) + +# FileModeWarnings go off per the default. +warnings.simplefilter("default", FileModeWarning, append=True) diff --git a/test/fixtures/whole_applications/requests/requests/__version__.py b/test/fixtures/whole_applications/requests/requests/__version__.py new file mode 100644 index 0000000..5063c3f --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/__version__.py @@ -0,0 +1,14 @@ +# .-. .-. .-. . . .-. .-. .-. .-. +# |( |- |.| | | |- `-. | `-. +# ' ' `-' `-`.`-' `-' `-' ' `-' + +__title__ = "requests" +__description__ = "Python HTTP for Humans." +__url__ = "https://requests.readthedocs.io" +__version__ = "2.31.0" +__build__ = 0x023100 +__author__ = "Kenneth Reitz" +__author_email__ = "me@kennethreitz.org" +__license__ = "Apache 2.0" +__copyright__ = "Copyright Kenneth Reitz" +__cake__ = "\u2728 \U0001f370 \u2728" diff --git a/test/fixtures/whole_applications/requests/requests/_internal_utils.py b/test/fixtures/whole_applications/requests/requests/_internal_utils.py new file mode 100644 index 0000000..f2cf635 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/_internal_utils.py @@ -0,0 +1,50 @@ +""" +requests._internal_utils +~~~~~~~~~~~~~~ + +Provides utility functions that are consumed internally by Requests +which depend on extremely few external helpers (such as compat) +""" +import re + +from .compat import builtin_str + +_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$") +_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$") +_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$") +_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$") + +_HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR) +_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE) +HEADER_VALIDATORS = { + bytes: _HEADER_VALIDATORS_BYTE, + str: _HEADER_VALIDATORS_STR, +} + + +def to_native_string(string, encoding="ascii"): + """Given a string object, regardless of type, returns a representation of + that string in the native string type, encoding and decoding where + necessary. This assumes ASCII unless told otherwise. + """ + if isinstance(string, builtin_str): + out = string + else: + out = string.decode(encoding) + + return out + + +def unicode_is_ascii(u_string): + """Determine if unicode string only contains ASCII characters. + + :param str u_string: unicode string to check. Must be unicode + and not Python 2 `str`. + :rtype: bool + """ + assert isinstance(u_string, str) + try: + u_string.encode("ascii") + return True + except UnicodeEncodeError: + return False diff --git a/test/fixtures/whole_applications/requests/requests/adapters.py b/test/fixtures/whole_applications/requests/requests/adapters.py new file mode 100644 index 0000000..78e3bb6 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/adapters.py @@ -0,0 +1,538 @@ +""" +requests.adapters +~~~~~~~~~~~~~~~~~ + +This module contains the transport adapters that Requests uses to define +and maintain connections. +""" + +import os.path +import socket # noqa: F401 + +from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError +from urllib3.exceptions import HTTPError as _HTTPError +from urllib3.exceptions import InvalidHeader as _InvalidHeader +from urllib3.exceptions import ( + LocationValueError, + MaxRetryError, + NewConnectionError, + ProtocolError, +) +from urllib3.exceptions import ProxyError as _ProxyError +from urllib3.exceptions import ReadTimeoutError, ResponseError +from urllib3.exceptions import SSLError as _SSLError +from urllib3.poolmanager import PoolManager, proxy_from_url +from urllib3.util import Timeout as TimeoutSauce +from urllib3.util import parse_url +from urllib3.util.retry import Retry + +from .auth import _basic_auth_str +from .compat import basestring, urlparse +from .cookies import extract_cookies_to_jar +from .exceptions import ( + ConnectionError, + ConnectTimeout, + InvalidHeader, + InvalidProxyURL, + InvalidSchema, + InvalidURL, + ProxyError, + ReadTimeout, + RetryError, + SSLError, +) +from .models import Response +from .structures import CaseInsensitiveDict +from .utils import ( + DEFAULT_CA_BUNDLE_PATH, + extract_zipped_paths, + get_auth_from_url, + get_encoding_from_headers, + prepend_scheme_if_needed, + select_proxy, + urldefragauth, +) + +try: + from urllib3.contrib.socks import SOCKSProxyManager +except ImportError: + + def SOCKSProxyManager(*args, **kwargs): + raise InvalidSchema("Missing dependencies for SOCKS support.") + + +DEFAULT_POOLBLOCK = False +DEFAULT_POOLSIZE = 10 +DEFAULT_RETRIES = 0 +DEFAULT_POOL_TIMEOUT = None + + +class BaseAdapter: + """The Base Transport Adapter""" + + def __init__(self): + super().__init__() + + def send( + self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None + ): + """Sends PreparedRequest object. Returns Response object. + + :param request: The :class:`PreparedRequest ` being sent. + :param stream: (optional) Whether to stream the request content. + :param timeout: (optional) How long to wait for the server to send + data before giving up, as a float, or a :ref:`(connect timeout, + read timeout) ` tuple. + :type timeout: float or tuple + :param verify: (optional) Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use + :param cert: (optional) Any user-provided SSL certificate to be trusted. + :param proxies: (optional) The proxies dictionary to apply to the request. + """ + raise NotImplementedError + + def close(self): + """Cleans up adapter specific items.""" + raise NotImplementedError + + +class HTTPAdapter(BaseAdapter): + """The built-in HTTP Adapter for urllib3. + + Provides a general-case interface for Requests sessions to contact HTTP and + HTTPS urls by implementing the Transport Adapter interface. This class will + usually be created by the :class:`Session ` class under the + covers. + + :param pool_connections: The number of urllib3 connection pools to cache. + :param pool_maxsize: The maximum number of connections to save in the pool. + :param max_retries: The maximum number of retries each connection + should attempt. Note, this applies only to failed DNS lookups, socket + connections and connection timeouts, never to requests where data has + made it to the server. By default, Requests does not retry failed + connections. If you need granular control over the conditions under + which we retry a request, import urllib3's ``Retry`` class and pass + that instead. + :param pool_block: Whether the connection pool should block for connections. + + Usage:: + + >>> import requests + >>> s = requests.Session() + >>> a = requests.adapters.HTTPAdapter(max_retries=3) + >>> s.mount('http://', a) + """ + + __attrs__ = [ + "max_retries", + "config", + "_pool_connections", + "_pool_maxsize", + "_pool_block", + ] + + def __init__( + self, + pool_connections=DEFAULT_POOLSIZE, + pool_maxsize=DEFAULT_POOLSIZE, + max_retries=DEFAULT_RETRIES, + pool_block=DEFAULT_POOLBLOCK, + ): + if max_retries == DEFAULT_RETRIES: + self.max_retries = Retry(0, read=False) + else: + self.max_retries = Retry.from_int(max_retries) + self.config = {} + self.proxy_manager = {} + + super().__init__() + + self._pool_connections = pool_connections + self._pool_maxsize = pool_maxsize + self._pool_block = pool_block + + self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block) + + def __getstate__(self): + return {attr: getattr(self, attr, None) for attr in self.__attrs__} + + def __setstate__(self, state): + # Can't handle by adding 'proxy_manager' to self.__attrs__ because + # self.poolmanager uses a lambda function, which isn't pickleable. + self.proxy_manager = {} + self.config = {} + + for attr, value in state.items(): + setattr(self, attr, value) + + self.init_poolmanager( + self._pool_connections, self._pool_maxsize, block=self._pool_block + ) + + def init_poolmanager( + self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs + ): + """Initializes a urllib3 PoolManager. + + This method should not be called from user code, and is only + exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param connections: The number of urllib3 connection pools to cache. + :param maxsize: The maximum number of connections to save in the pool. + :param block: Block when no free connections are available. + :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager. + """ + # save these values for pickling + self._pool_connections = connections + self._pool_maxsize = maxsize + self._pool_block = block + + self.poolmanager = PoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + **pool_kwargs, + ) + + def proxy_manager_for(self, proxy, **proxy_kwargs): + """Return urllib3 ProxyManager for the given proxy. + + This method should not be called from user code, and is only + exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param proxy: The proxy to return a urllib3 ProxyManager for. + :param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager. + :returns: ProxyManager + :rtype: urllib3.ProxyManager + """ + if proxy in self.proxy_manager: + manager = self.proxy_manager[proxy] + elif proxy.lower().startswith("socks"): + username, password = get_auth_from_url(proxy) + manager = self.proxy_manager[proxy] = SOCKSProxyManager( + proxy, + username=username, + password=password, + num_pools=self._pool_connections, + maxsize=self._pool_maxsize, + block=self._pool_block, + **proxy_kwargs, + ) + else: + proxy_headers = self.proxy_headers(proxy) + manager = self.proxy_manager[proxy] = proxy_from_url( + proxy, + proxy_headers=proxy_headers, + num_pools=self._pool_connections, + maxsize=self._pool_maxsize, + block=self._pool_block, + **proxy_kwargs, + ) + + return manager + + def cert_verify(self, conn, url, verify, cert): + """Verify a SSL certificate. This method should not be called from user + code, and is only exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param conn: The urllib3 connection object associated with the cert. + :param url: The requested URL. + :param verify: Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use + :param cert: The SSL certificate to verify. + """ + if url.lower().startswith("https") and verify: + + cert_loc = None + + # Allow self-specified cert location. + if verify is not True: + cert_loc = verify + + if not cert_loc: + cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH) + + if not cert_loc or not os.path.exists(cert_loc): + raise OSError( + f"Could not find a suitable TLS CA certificate bundle, " + f"invalid path: {cert_loc}" + ) + + conn.cert_reqs = "CERT_REQUIRED" + + if not os.path.isdir(cert_loc): + conn.ca_certs = cert_loc + else: + conn.ca_cert_dir = cert_loc + else: + conn.cert_reqs = "CERT_NONE" + conn.ca_certs = None + conn.ca_cert_dir = None + + if cert: + if not isinstance(cert, basestring): + conn.cert_file = cert[0] + conn.key_file = cert[1] + else: + conn.cert_file = cert + conn.key_file = None + if conn.cert_file and not os.path.exists(conn.cert_file): + raise OSError( + f"Could not find the TLS certificate file, " + f"invalid path: {conn.cert_file}" + ) + if conn.key_file and not os.path.exists(conn.key_file): + raise OSError( + f"Could not find the TLS key file, invalid path: {conn.key_file}" + ) + + def build_response(self, req, resp): + """Builds a :class:`Response ` object from a urllib3 + response. This should not be called from user code, and is only exposed + for use when subclassing the + :class:`HTTPAdapter ` + + :param req: The :class:`PreparedRequest ` used to generate the response. + :param resp: The urllib3 response object. + :rtype: requests.Response + """ + response = Response() + + # Fallback to None if there's no status_code, for whatever reason. + response.status_code = getattr(resp, "status", None) + + # Make headers case-insensitive. + response.headers = CaseInsensitiveDict(getattr(resp, "headers", {})) + + # Set encoding. + response.encoding = get_encoding_from_headers(response.headers) + response.raw = resp + response.reason = response.raw.reason + + if isinstance(req.url, bytes): + response.url = req.url.decode("utf-8") + else: + response.url = req.url + + # Add new cookies from the server. + extract_cookies_to_jar(response.cookies, req, resp) + + # Give the Response some context. + response.request = req + response.connection = self + + return response + + def get_connection(self, url, proxies=None): + """Returns a urllib3 connection for the given URL. This should not be + called from user code, and is only exposed for use when subclassing the + :class:`HTTPAdapter `. + + :param url: The URL to connect to. + :param proxies: (optional) A Requests-style dictionary of proxies used on this request. + :rtype: urllib3.ConnectionPool + """ + proxy = select_proxy(url, proxies) + + if proxy: + proxy = prepend_scheme_if_needed(proxy, "http") + proxy_url = parse_url(proxy) + if not proxy_url.host: + raise InvalidProxyURL( + "Please check proxy URL. It is malformed " + "and could be missing the host." + ) + proxy_manager = self.proxy_manager_for(proxy) + conn = proxy_manager.connection_from_url(url) + else: + # Only scheme should be lower case + parsed = urlparse(url) + url = parsed.geturl() + conn = self.poolmanager.connection_from_url(url) + + return conn + + def close(self): + """Disposes of any internal state. + + Currently, this closes the PoolManager and any active ProxyManager, + which closes any pooled connections. + """ + self.poolmanager.clear() + for proxy in self.proxy_manager.values(): + proxy.clear() + + def request_url(self, request, proxies): + """Obtain the url to use when making the final request. + + If the message is being sent through a HTTP proxy, the full URL has to + be used. Otherwise, we should only use the path portion of the URL. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter `. + + :param request: The :class:`PreparedRequest ` being sent. + :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs. + :rtype: str + """ + proxy = select_proxy(request.url, proxies) + scheme = urlparse(request.url).scheme + + is_proxied_http_request = proxy and scheme != "https" + using_socks_proxy = False + if proxy: + proxy_scheme = urlparse(proxy).scheme.lower() + using_socks_proxy = proxy_scheme.startswith("socks") + + url = request.path_url + if is_proxied_http_request and not using_socks_proxy: + url = urldefragauth(request.url) + + return url + + def add_headers(self, request, **kwargs): + """Add any headers needed by the connection. As of v2.0 this does + nothing by default, but is left for overriding by users that subclass + the :class:`HTTPAdapter `. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter `. + + :param request: The :class:`PreparedRequest ` to add headers to. + :param kwargs: The keyword arguments from the call to send(). + """ + pass + + def proxy_headers(self, proxy): + """Returns a dictionary of the headers to add to any request sent + through a proxy. This works with urllib3 magic to ensure that they are + correctly sent to the proxy, rather than in a tunnelled request if + CONNECT is being used. + + This should not be called from user code, and is only exposed for use + when subclassing the + :class:`HTTPAdapter `. + + :param proxy: The url of the proxy being used for this request. + :rtype: dict + """ + headers = {} + username, password = get_auth_from_url(proxy) + + if username: + headers["Proxy-Authorization"] = _basic_auth_str(username, password) + + return headers + + def send( + self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None + ): + """Sends PreparedRequest object. Returns Response object. + + :param request: The :class:`PreparedRequest ` being sent. + :param stream: (optional) Whether to stream the request content. + :param timeout: (optional) How long to wait for the server to send + data before giving up, as a float, or a :ref:`(connect timeout, + read timeout) ` tuple. + :type timeout: float or tuple or urllib3 Timeout object + :param verify: (optional) Either a boolean, in which case it controls whether + we verify the server's TLS certificate, or a string, in which case it + must be a path to a CA bundle to use + :param cert: (optional) Any user-provided SSL certificate to be trusted. + :param proxies: (optional) The proxies dictionary to apply to the request. + :rtype: requests.Response + """ + + try: + conn = self.get_connection(request.url, proxies) + except LocationValueError as e: + raise InvalidURL(e, request=request) + + self.cert_verify(conn, request.url, verify, cert) + url = self.request_url(request, proxies) + self.add_headers( + request, + stream=stream, + timeout=timeout, + verify=verify, + cert=cert, + proxies=proxies, + ) + + chunked = not (request.body is None or "Content-Length" in request.headers) + + if isinstance(timeout, tuple): + try: + connect, read = timeout + timeout = TimeoutSauce(connect=connect, read=read) + except ValueError: + raise ValueError( + f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, " + f"or a single float to set both timeouts to the same value." + ) + elif isinstance(timeout, TimeoutSauce): + pass + else: + timeout = TimeoutSauce(connect=timeout, read=timeout) + + try: + resp = conn.urlopen( + method=request.method, + url=url, + body=request.body, + headers=request.headers, + redirect=False, + assert_same_host=False, + preload_content=False, + decode_content=False, + retries=self.max_retries, + timeout=timeout, + chunked=chunked, + ) + + except (ProtocolError, OSError) as err: + raise ConnectionError(err, request=request) + + except MaxRetryError as e: + if isinstance(e.reason, ConnectTimeoutError): + # TODO: Remove this in 3.0.0: see #2811 + if not isinstance(e.reason, NewConnectionError): + raise ConnectTimeout(e, request=request) + + if isinstance(e.reason, ResponseError): + raise RetryError(e, request=request) + + if isinstance(e.reason, _ProxyError): + raise ProxyError(e, request=request) + + if isinstance(e.reason, _SSLError): + # This branch is for urllib3 v1.22 and later. + raise SSLError(e, request=request) + + raise ConnectionError(e, request=request) + + except ClosedPoolError as e: + raise ConnectionError(e, request=request) + + except _ProxyError as e: + raise ProxyError(e) + + except (_SSLError, _HTTPError) as e: + if isinstance(e, _SSLError): + # This branch is for urllib3 versions earlier than v1.22 + raise SSLError(e, request=request) + elif isinstance(e, ReadTimeoutError): + raise ReadTimeout(e, request=request) + elif isinstance(e, _InvalidHeader): + raise InvalidHeader(e, request=request) + else: + raise + + return self.build_response(request, resp) diff --git a/test/fixtures/whole_applications/requests/requests/api.py b/test/fixtures/whole_applications/requests/requests/api.py new file mode 100644 index 0000000..cd0b3ee --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/api.py @@ -0,0 +1,157 @@ +""" +requests.api +~~~~~~~~~~~~ + +This module implements the Requests API. + +:copyright: (c) 2012 by Kenneth Reitz. +:license: Apache2, see LICENSE for more details. +""" + +from . import sessions + + +def request(method, url, **kwargs): + """Constructs and sends a :class:`Request `. + + :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary, list of tuples or bytes to send + in the query string for the :class:`Request`. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload. + ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` + or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string + defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers + to add for the file. + :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) How many seconds to wait for the server to send data + before giving up, as a float, or a :ref:`(connect timeout, read + timeout) ` tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``. + :type allow_redirects: bool + :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. + :param verify: (optional) Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use. Defaults to ``True``. + :param stream: (optional) if ``False``, the response content will be immediately downloaded. + :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + :return: :class:`Response ` object + :rtype: requests.Response + + Usage:: + + >>> import requests + >>> req = requests.request('GET', 'https://httpbin.org/get') + >>> req + + """ + + # By using the 'with' statement we are sure the session is closed, thus we + # avoid leaving sockets open which can trigger a ResourceWarning in some + # cases, and look like a memory leak in others. + with sessions.Session() as session: + return session.request(method=method, url=url, **kwargs) + + +def get(url, params=None, **kwargs): + r"""Sends a GET request. + + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary, list of tuples or bytes to send + in the query string for the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("get", url, params=params, **kwargs) + + +def options(url, **kwargs): + r"""Sends an OPTIONS request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("options", url, **kwargs) + + +def head(url, **kwargs): + r"""Sends a HEAD request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. If + `allow_redirects` is not provided, it will be set to `False` (as + opposed to the default :meth:`request` behavior). + :return: :class:`Response ` object + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", False) + return request("head", url, **kwargs) + + +def post(url, data=None, json=None, **kwargs): + r"""Sends a POST request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("post", url, data=data, json=json, **kwargs) + + +def put(url, data=None, **kwargs): + r"""Sends a PUT request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("put", url, data=data, **kwargs) + + +def patch(url, data=None, **kwargs): + r"""Sends a PATCH request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("patch", url, data=data, **kwargs) + + +def delete(url, **kwargs): + r"""Sends a DELETE request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("delete", url, **kwargs) diff --git a/test/fixtures/whole_applications/requests/requests/auth.py b/test/fixtures/whole_applications/requests/requests/auth.py new file mode 100644 index 0000000..9733686 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/auth.py @@ -0,0 +1,315 @@ +""" +requests.auth +~~~~~~~~~~~~~ + +This module contains the authentication handlers for Requests. +""" + +import hashlib +import os +import re +import threading +import time +import warnings +from base64 import b64encode + +from ._internal_utils import to_native_string +from .compat import basestring, str, urlparse +from .cookies import extract_cookies_to_jar +from .utils import parse_dict_header + +CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded" +CONTENT_TYPE_MULTI_PART = "multipart/form-data" + + +def _basic_auth_str(username, password): + """Returns a Basic Auth string.""" + + # "I want us to put a big-ol' comment on top of it that + # says that this behaviour is dumb but we need to preserve + # it because people are relying on it." + # - Lukasa + # + # These are here solely to maintain backwards compatibility + # for things like ints. This will be removed in 3.0.0. + if not isinstance(username, basestring): + warnings.warn( + "Non-string usernames will no longer be supported in Requests " + "3.0.0. Please convert the object you've passed in ({!r}) to " + "a string or bytes object in the near future to avoid " + "problems.".format(username), + category=DeprecationWarning, + ) + username = str(username) + + if not isinstance(password, basestring): + warnings.warn( + "Non-string passwords will no longer be supported in Requests " + "3.0.0. Please convert the object you've passed in ({!r}) to " + "a string or bytes object in the near future to avoid " + "problems.".format(type(password)), + category=DeprecationWarning, + ) + password = str(password) + # -- End Removal -- + + if isinstance(username, str): + username = username.encode("latin1") + + if isinstance(password, str): + password = password.encode("latin1") + + authstr = "Basic " + to_native_string( + b64encode(b":".join((username, password))).strip() + ) + + return authstr + + +class AuthBase: + """Base class that all auth implementations derive from""" + + def __call__(self, r): + raise NotImplementedError("Auth hooks must be callable.") + + +class HTTPBasicAuth(AuthBase): + """Attaches HTTP Basic Authentication to the given Request object.""" + + def __init__(self, username, password): + self.username = username + self.password = password + + def __eq__(self, other): + return all( + [ + self.username == getattr(other, "username", None), + self.password == getattr(other, "password", None), + ] + ) + + def __ne__(self, other): + return not self == other + + def __call__(self, r): + r.headers["Authorization"] = _basic_auth_str(self.username, self.password) + return r + + +class HTTPProxyAuth(HTTPBasicAuth): + """Attaches HTTP Proxy Authentication to a given Request object.""" + + def __call__(self, r): + r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password) + return r + + +class HTTPDigestAuth(AuthBase): + """Attaches HTTP Digest Authentication to the given Request object.""" + + def __init__(self, username, password): + self.username = username + self.password = password + # Keep state in per-thread local storage + self._thread_local = threading.local() + + def init_per_thread_state(self): + # Ensure state is initialized just once per-thread + if not hasattr(self._thread_local, "init"): + self._thread_local.init = True + self._thread_local.last_nonce = "" + self._thread_local.nonce_count = 0 + self._thread_local.chal = {} + self._thread_local.pos = None + self._thread_local.num_401_calls = None + + def build_digest_header(self, method, url): + """ + :rtype: str + """ + + realm = self._thread_local.chal["realm"] + nonce = self._thread_local.chal["nonce"] + qop = self._thread_local.chal.get("qop") + algorithm = self._thread_local.chal.get("algorithm") + opaque = self._thread_local.chal.get("opaque") + hash_utf8 = None + + if algorithm is None: + _algorithm = "MD5" + else: + _algorithm = algorithm.upper() + # lambdas assume digest modules are imported at the top level + if _algorithm == "MD5" or _algorithm == "MD5-SESS": + + def md5_utf8(x): + if isinstance(x, str): + x = x.encode("utf-8") + return hashlib.md5(x).hexdigest() + + hash_utf8 = md5_utf8 + elif _algorithm == "SHA": + + def sha_utf8(x): + if isinstance(x, str): + x = x.encode("utf-8") + return hashlib.sha1(x).hexdigest() + + hash_utf8 = sha_utf8 + elif _algorithm == "SHA-256": + + def sha256_utf8(x): + if isinstance(x, str): + x = x.encode("utf-8") + return hashlib.sha256(x).hexdigest() + + hash_utf8 = sha256_utf8 + elif _algorithm == "SHA-512": + + def sha512_utf8(x): + if isinstance(x, str): + x = x.encode("utf-8") + return hashlib.sha512(x).hexdigest() + + hash_utf8 = sha512_utf8 + + KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731 + + if hash_utf8 is None: + return None + + # XXX not implemented yet + entdig = None + p_parsed = urlparse(url) + #: path is request-uri defined in RFC 2616 which should not be empty + path = p_parsed.path or "/" + if p_parsed.query: + path += f"?{p_parsed.query}" + + A1 = f"{self.username}:{realm}:{self.password}" + A2 = f"{method}:{path}" + + HA1 = hash_utf8(A1) + HA2 = hash_utf8(A2) + + if nonce == self._thread_local.last_nonce: + self._thread_local.nonce_count += 1 + else: + self._thread_local.nonce_count = 1 + ncvalue = f"{self._thread_local.nonce_count:08x}" + s = str(self._thread_local.nonce_count).encode("utf-8") + s += nonce.encode("utf-8") + s += time.ctime().encode("utf-8") + s += os.urandom(8) + + cnonce = hashlib.sha1(s).hexdigest()[:16] + if _algorithm == "MD5-SESS": + HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}") + + if not qop: + respdig = KD(HA1, f"{nonce}:{HA2}") + elif qop == "auth" or "auth" in qop.split(","): + noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}" + respdig = KD(HA1, noncebit) + else: + # XXX handle auth-int. + return None + + self._thread_local.last_nonce = nonce + + # XXX should the partial digests be encoded too? + base = ( + f'username="{self.username}", realm="{realm}", nonce="{nonce}", ' + f'uri="{path}", response="{respdig}"' + ) + if opaque: + base += f', opaque="{opaque}"' + if algorithm: + base += f', algorithm="{algorithm}"' + if entdig: + base += f', digest="{entdig}"' + if qop: + base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"' + + return f"Digest {base}" + + def handle_redirect(self, r, **kwargs): + """Reset num_401_calls counter on redirects.""" + if r.is_redirect: + self._thread_local.num_401_calls = 1 + + def handle_401(self, r, **kwargs): + """ + Takes the given response and tries digest-auth, if needed. + + :rtype: requests.Response + """ + + # If response is not 4xx, do not auth + # See https://github.com/psf/requests/issues/3772 + if not 400 <= r.status_code < 500: + self._thread_local.num_401_calls = 1 + return r + + if self._thread_local.pos is not None: + # Rewind the file position indicator of the body to where + # it was to resend the request. + r.request.body.seek(self._thread_local.pos) + s_auth = r.headers.get("www-authenticate", "") + + if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2: + + self._thread_local.num_401_calls += 1 + pat = re.compile(r"digest ", flags=re.IGNORECASE) + self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1)) + + # Consume content and release the original connection + # to allow our new request to reuse the same one. + r.content + r.close() + prep = r.request.copy() + extract_cookies_to_jar(prep._cookies, r.request, r.raw) + prep.prepare_cookies(prep._cookies) + + prep.headers["Authorization"] = self.build_digest_header( + prep.method, prep.url + ) + _r = r.connection.send(prep, **kwargs) + _r.history.append(r) + _r.request = prep + + return _r + + self._thread_local.num_401_calls = 1 + return r + + def __call__(self, r): + # Initialize per-thread state, if needed + self.init_per_thread_state() + # If we have a saved nonce, skip the 401 + if self._thread_local.last_nonce: + r.headers["Authorization"] = self.build_digest_header(r.method, r.url) + try: + self._thread_local.pos = r.body.tell() + except AttributeError: + # In the case of HTTPDigestAuth being reused and the body of + # the previous request was a file-like object, pos has the + # file position of the previous body. Ensure it's set to + # None. + self._thread_local.pos = None + r.register_hook("response", self.handle_401) + r.register_hook("response", self.handle_redirect) + self._thread_local.num_401_calls = 1 + + return r + + def __eq__(self, other): + return all( + [ + self.username == getattr(other, "username", None), + self.password == getattr(other, "password", None), + ] + ) + + def __ne__(self, other): + return not self == other diff --git a/test/fixtures/whole_applications/requests/requests/certs.py b/test/fixtures/whole_applications/requests/requests/certs.py new file mode 100644 index 0000000..be422c3 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/certs.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +""" +requests.certs +~~~~~~~~~~~~~~ + +This module returns the preferred default CA certificate bundle. There is +only one — the one from the certifi package. + +If you are packaging Requests, e.g., for a Linux distribution or a managed +environment, you can change the definition of where() to return a separately +packaged CA bundle. +""" +from certifi import where + +if __name__ == "__main__": + print(where()) diff --git a/test/fixtures/whole_applications/requests/requests/compat.py b/test/fixtures/whole_applications/requests/requests/compat.py new file mode 100644 index 0000000..6776163 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/compat.py @@ -0,0 +1,79 @@ +""" +requests.compat +~~~~~~~~~~~~~~~ + +This module previously handled import compatibility issues +between Python 2 and Python 3. It remains for backwards +compatibility until the next major version. +""" + +try: + import chardet +except ImportError: + import charset_normalizer as chardet + +import sys + +# ------- +# Pythons +# ------- + +# Syntax sugar. +_ver = sys.version_info + +#: Python 2.x? +is_py2 = _ver[0] == 2 + +#: Python 3.x? +is_py3 = _ver[0] == 3 + +# json/simplejson module import resolution +has_simplejson = False +try: + import simplejson as json + + has_simplejson = True +except ImportError: + import json + +if has_simplejson: + from simplejson import JSONDecodeError +else: + from json import JSONDecodeError + +# Keep OrderedDict for backwards compatibility. +from collections import OrderedDict +from collections.abc import Callable, Mapping, MutableMapping +from http import cookiejar as cookielib +from http.cookies import Morsel +from io import StringIO + +# -------------- +# Legacy Imports +# -------------- +from urllib.parse import ( + quote, + quote_plus, + unquote, + unquote_plus, + urldefrag, + urlencode, + urljoin, + urlparse, + urlsplit, + urlunparse, +) +from urllib.request import ( + getproxies, + getproxies_environment, + parse_http_list, + proxy_bypass, + proxy_bypass_environment, +) + +builtin_str = str +str = str +bytes = bytes +basestring = (str, bytes) +numeric_types = (int, float) +integer_types = (int,) diff --git a/test/fixtures/whole_applications/requests/requests/cookies.py b/test/fixtures/whole_applications/requests/requests/cookies.py new file mode 100644 index 0000000..bf54ab2 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/cookies.py @@ -0,0 +1,561 @@ +""" +requests.cookies +~~~~~~~~~~~~~~~~ + +Compatibility code to be able to use `cookielib.CookieJar` with requests. + +requests.utils imports from here, so be careful with imports. +""" + +import calendar +import copy +import time + +from ._internal_utils import to_native_string +from .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse + +try: + import threading +except ImportError: + import dummy_threading as threading + + +class MockRequest: + """Wraps a `requests.Request` to mimic a `urllib2.Request`. + + The code in `cookielib.CookieJar` expects this interface in order to correctly + manage cookie policies, i.e., determine whether a cookie can be set, given the + domains of the request and the cookie. + + The original request object is read-only. The client is responsible for collecting + the new headers via `get_new_headers()` and interpreting them appropriately. You + probably want `get_cookie_header`, defined below. + """ + + def __init__(self, request): + self._r = request + self._new_headers = {} + self.type = urlparse(self._r.url).scheme + + def get_type(self): + return self.type + + def get_host(self): + return urlparse(self._r.url).netloc + + def get_origin_req_host(self): + return self.get_host() + + def get_full_url(self): + # Only return the response's URL if the user hadn't set the Host + # header + if not self._r.headers.get("Host"): + return self._r.url + # If they did set it, retrieve it and reconstruct the expected domain + host = to_native_string(self._r.headers["Host"], encoding="utf-8") + parsed = urlparse(self._r.url) + # Reconstruct the URL as we expect it + return urlunparse( + [ + parsed.scheme, + host, + parsed.path, + parsed.params, + parsed.query, + parsed.fragment, + ] + ) + + def is_unverifiable(self): + return True + + def has_header(self, name): + return name in self._r.headers or name in self._new_headers + + def get_header(self, name, default=None): + return self._r.headers.get(name, self._new_headers.get(name, default)) + + def add_header(self, key, val): + """cookielib has no legitimate use for this method; add it back if you find one.""" + raise NotImplementedError( + "Cookie headers should be added with add_unredirected_header()" + ) + + def add_unredirected_header(self, name, value): + self._new_headers[name] = value + + def get_new_headers(self): + return self._new_headers + + @property + def unverifiable(self): + return self.is_unverifiable() + + @property + def origin_req_host(self): + return self.get_origin_req_host() + + @property + def host(self): + return self.get_host() + + +class MockResponse: + """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`. + + ...what? Basically, expose the parsed HTTP headers from the server response + the way `cookielib` expects to see them. + """ + + def __init__(self, headers): + """Make a MockResponse for `cookielib` to read. + + :param headers: a httplib.HTTPMessage or analogous carrying the headers + """ + self._headers = headers + + def info(self): + return self._headers + + def getheaders(self, name): + self._headers.getheaders(name) + + +def extract_cookies_to_jar(jar, request, response): + """Extract the cookies from the response into a CookieJar. + + :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar) + :param request: our own requests.Request object + :param response: urllib3.HTTPResponse object + """ + if not (hasattr(response, "_original_response") and response._original_response): + return + # the _original_response field is the wrapped httplib.HTTPResponse object, + req = MockRequest(request) + # pull out the HTTPMessage with the headers and put it in the mock: + res = MockResponse(response._original_response.msg) + jar.extract_cookies(res, req) + + +def get_cookie_header(jar, request): + """ + Produce an appropriate Cookie header string to be sent with `request`, or None. + + :rtype: str + """ + r = MockRequest(request) + jar.add_cookie_header(r) + return r.get_new_headers().get("Cookie") + + +def remove_cookie_by_name(cookiejar, name, domain=None, path=None): + """Unsets a cookie by name, by default over all domains and paths. + + Wraps CookieJar.clear(), is O(n). + """ + clearables = [] + for cookie in cookiejar: + if cookie.name != name: + continue + if domain is not None and domain != cookie.domain: + continue + if path is not None and path != cookie.path: + continue + clearables.append((cookie.domain, cookie.path, cookie.name)) + + for domain, path, name in clearables: + cookiejar.clear(domain, path, name) + + +class CookieConflictError(RuntimeError): + """There are two cookies that meet the criteria specified in the cookie jar. + Use .get and .set and include domain and path args in order to be more specific. + """ + + +class RequestsCookieJar(cookielib.CookieJar, MutableMapping): + """Compatibility class; is a cookielib.CookieJar, but exposes a dict + interface. + + This is the CookieJar we create by default for requests and sessions that + don't specify one, since some clients may expect response.cookies and + session.cookies to support dict operations. + + Requests does not use the dict interface internally; it's just for + compatibility with external client code. All requests code should work + out of the box with externally provided instances of ``CookieJar``, e.g. + ``LWPCookieJar`` and ``FileCookieJar``. + + Unlike a regular CookieJar, this class is pickleable. + + .. warning:: dictionary operations that are normally O(1) may be O(n). + """ + + def get(self, name, default=None, domain=None, path=None): + """Dict-like get() that also supports optional domain and path args in + order to resolve naming collisions from using one cookie jar over + multiple domains. + + .. warning:: operation is O(n), not O(1). + """ + try: + return self._find_no_duplicates(name, domain, path) + except KeyError: + return default + + def set(self, name, value, **kwargs): + """Dict-like set() that also supports optional domain and path args in + order to resolve naming collisions from using one cookie jar over + multiple domains. + """ + # support client code that unsets cookies by assignment of a None value: + if value is None: + remove_cookie_by_name( + self, name, domain=kwargs.get("domain"), path=kwargs.get("path") + ) + return + + if isinstance(value, Morsel): + c = morsel_to_cookie(value) + else: + c = create_cookie(name, value, **kwargs) + self.set_cookie(c) + return c + + def iterkeys(self): + """Dict-like iterkeys() that returns an iterator of names of cookies + from the jar. + + .. seealso:: itervalues() and iteritems(). + """ + for cookie in iter(self): + yield cookie.name + + def keys(self): + """Dict-like keys() that returns a list of names of cookies from the + jar. + + .. seealso:: values() and items(). + """ + return list(self.iterkeys()) + + def itervalues(self): + """Dict-like itervalues() that returns an iterator of values of cookies + from the jar. + + .. seealso:: iterkeys() and iteritems(). + """ + for cookie in iter(self): + yield cookie.value + + def values(self): + """Dict-like values() that returns a list of values of cookies from the + jar. + + .. seealso:: keys() and items(). + """ + return list(self.itervalues()) + + def iteritems(self): + """Dict-like iteritems() that returns an iterator of name-value tuples + from the jar. + + .. seealso:: iterkeys() and itervalues(). + """ + for cookie in iter(self): + yield cookie.name, cookie.value + + def items(self): + """Dict-like items() that returns a list of name-value tuples from the + jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a + vanilla python dict of key value pairs. + + .. seealso:: keys() and values(). + """ + return list(self.iteritems()) + + def list_domains(self): + """Utility method to list all the domains in the jar.""" + domains = [] + for cookie in iter(self): + if cookie.domain not in domains: + domains.append(cookie.domain) + return domains + + def list_paths(self): + """Utility method to list all the paths in the jar.""" + paths = [] + for cookie in iter(self): + if cookie.path not in paths: + paths.append(cookie.path) + return paths + + def multiple_domains(self): + """Returns True if there are multiple domains in the jar. + Returns False otherwise. + + :rtype: bool + """ + domains = [] + for cookie in iter(self): + if cookie.domain is not None and cookie.domain in domains: + return True + domains.append(cookie.domain) + return False # there is only one domain in jar + + def get_dict(self, domain=None, path=None): + """Takes as an argument an optional domain and path and returns a plain + old Python dict of name-value pairs of cookies that meet the + requirements. + + :rtype: dict + """ + dictionary = {} + for cookie in iter(self): + if (domain is None or cookie.domain == domain) and ( + path is None or cookie.path == path + ): + dictionary[cookie.name] = cookie.value + return dictionary + + def __contains__(self, name): + try: + return super().__contains__(name) + except CookieConflictError: + return True + + def __getitem__(self, name): + """Dict-like __getitem__() for compatibility with client code. Throws + exception if there are more than one cookie with name. In that case, + use the more explicit get() method instead. + + .. warning:: operation is O(n), not O(1). + """ + return self._find_no_duplicates(name) + + def __setitem__(self, name, value): + """Dict-like __setitem__ for compatibility with client code. Throws + exception if there is already a cookie of that name in the jar. In that + case, use the more explicit set() method instead. + """ + self.set(name, value) + + def __delitem__(self, name): + """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s + ``remove_cookie_by_name()``. + """ + remove_cookie_by_name(self, name) + + def set_cookie(self, cookie, *args, **kwargs): + if ( + hasattr(cookie.value, "startswith") + and cookie.value.startswith('"') + and cookie.value.endswith('"') + ): + cookie.value = cookie.value.replace('\\"', "") + return super().set_cookie(cookie, *args, **kwargs) + + def update(self, other): + """Updates this jar with cookies from another CookieJar or dict-like""" + if isinstance(other, cookielib.CookieJar): + for cookie in other: + self.set_cookie(copy.copy(cookie)) + else: + super().update(other) + + def _find(self, name, domain=None, path=None): + """Requests uses this method internally to get cookie values. + + If there are conflicting cookies, _find arbitrarily chooses one. + See _find_no_duplicates if you want an exception thrown if there are + conflicting cookies. + + :param name: a string containing name of cookie + :param domain: (optional) string containing domain of cookie + :param path: (optional) string containing path of cookie + :return: cookie.value + """ + for cookie in iter(self): + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + return cookie.value + + raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") + + def _find_no_duplicates(self, name, domain=None, path=None): + """Both ``__get_item__`` and ``get`` call this function: it's never + used elsewhere in Requests. + + :param name: a string containing name of cookie + :param domain: (optional) string containing domain of cookie + :param path: (optional) string containing path of cookie + :raises KeyError: if cookie is not found + :raises CookieConflictError: if there are multiple cookies + that match name and optionally domain and path + :return: cookie.value + """ + toReturn = None + for cookie in iter(self): + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + if toReturn is not None: + # if there are multiple cookies that meet passed in criteria + raise CookieConflictError( + f"There are multiple cookies with name, {name!r}" + ) + # we will eventually return this as long as no cookie conflict + toReturn = cookie.value + + if toReturn: + return toReturn + raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") + + def __getstate__(self): + """Unlike a normal CookieJar, this class is pickleable.""" + state = self.__dict__.copy() + # remove the unpickleable RLock object + state.pop("_cookies_lock") + return state + + def __setstate__(self, state): + """Unlike a normal CookieJar, this class is pickleable.""" + self.__dict__.update(state) + if "_cookies_lock" not in self.__dict__: + self._cookies_lock = threading.RLock() + + def copy(self): + """Return a copy of this RequestsCookieJar.""" + new_cj = RequestsCookieJar() + new_cj.set_policy(self.get_policy()) + new_cj.update(self) + return new_cj + + def get_policy(self): + """Return the CookiePolicy instance used.""" + return self._policy + + +def _copy_cookie_jar(jar): + if jar is None: + return None + + if hasattr(jar, "copy"): + # We're dealing with an instance of RequestsCookieJar + return jar.copy() + # We're dealing with a generic CookieJar instance + new_jar = copy.copy(jar) + new_jar.clear() + for cookie in jar: + new_jar.set_cookie(copy.copy(cookie)) + return new_jar + + +def create_cookie(name, value, **kwargs): + """Make a cookie from underspecified parameters. + + By default, the pair of `name` and `value` will be set for the domain '' + and sent on every request (this is sometimes called a "supercookie"). + """ + result = { + "version": 0, + "name": name, + "value": value, + "port": None, + "domain": "", + "path": "/", + "secure": False, + "expires": None, + "discard": True, + "comment": None, + "comment_url": None, + "rest": {"HttpOnly": None}, + "rfc2109": False, + } + + badargs = set(kwargs) - set(result) + if badargs: + raise TypeError( + f"create_cookie() got unexpected keyword arguments: {list(badargs)}" + ) + + result.update(kwargs) + result["port_specified"] = bool(result["port"]) + result["domain_specified"] = bool(result["domain"]) + result["domain_initial_dot"] = result["domain"].startswith(".") + result["path_specified"] = bool(result["path"]) + + return cookielib.Cookie(**result) + + +def morsel_to_cookie(morsel): + """Convert a Morsel object into a Cookie containing the one k/v pair.""" + + expires = None + if morsel["max-age"]: + try: + expires = int(time.time() + int(morsel["max-age"])) + except ValueError: + raise TypeError(f"max-age: {morsel['max-age']} must be integer") + elif morsel["expires"]: + time_template = "%a, %d-%b-%Y %H:%M:%S GMT" + expires = calendar.timegm(time.strptime(morsel["expires"], time_template)) + return create_cookie( + comment=morsel["comment"], + comment_url=bool(morsel["comment"]), + discard=False, + domain=morsel["domain"], + expires=expires, + name=morsel.key, + path=morsel["path"], + port=None, + rest={"HttpOnly": morsel["httponly"]}, + rfc2109=False, + secure=bool(morsel["secure"]), + value=morsel.value, + version=morsel["version"] or 0, + ) + + +def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True): + """Returns a CookieJar from a key/value dictionary. + + :param cookie_dict: Dict of key/values to insert into CookieJar. + :param cookiejar: (optional) A cookiejar to add the cookies to. + :param overwrite: (optional) If False, will not replace cookies + already in the jar with new ones. + :rtype: CookieJar + """ + if cookiejar is None: + cookiejar = RequestsCookieJar() + + if cookie_dict is not None: + names_from_jar = [cookie.name for cookie in cookiejar] + for name in cookie_dict: + if overwrite or (name not in names_from_jar): + cookiejar.set_cookie(create_cookie(name, cookie_dict[name])) + + return cookiejar + + +def merge_cookies(cookiejar, cookies): + """Add cookies to cookiejar and returns a merged CookieJar. + + :param cookiejar: CookieJar object to add the cookies to. + :param cookies: Dictionary or CookieJar object to be added. + :rtype: CookieJar + """ + if not isinstance(cookiejar, cookielib.CookieJar): + raise ValueError("You can only merge into CookieJar") + + if isinstance(cookies, dict): + cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False) + elif isinstance(cookies, cookielib.CookieJar): + try: + cookiejar.update(cookies) + except AttributeError: + for cookie_in_jar in cookies: + cookiejar.set_cookie(cookie_in_jar) + + return cookiejar diff --git a/test/fixtures/whole_applications/requests/requests/exceptions.py b/test/fixtures/whole_applications/requests/requests/exceptions.py new file mode 100644 index 0000000..e1cedf8 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/exceptions.py @@ -0,0 +1,141 @@ +""" +requests.exceptions +~~~~~~~~~~~~~~~~~~~ + +This module contains the set of Requests' exceptions. +""" +from urllib3.exceptions import HTTPError as BaseHTTPError + +from .compat import JSONDecodeError as CompatJSONDecodeError + + +class RequestException(IOError): + """There was an ambiguous exception that occurred while handling your + request. + """ + + def __init__(self, *args, **kwargs): + """Initialize RequestException with `request` and `response` objects.""" + response = kwargs.pop("response", None) + self.response = response + self.request = kwargs.pop("request", None) + if response is not None and not self.request and hasattr(response, "request"): + self.request = self.response.request + super().__init__(*args, **kwargs) + + +class InvalidJSONError(RequestException): + """A JSON error occurred.""" + + +class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError): + """Couldn't decode the text into json""" + + def __init__(self, *args, **kwargs): + """ + Construct the JSONDecodeError instance first with all + args. Then use it's args to construct the IOError so that + the json specific args aren't used as IOError specific args + and the error message from JSONDecodeError is preserved. + """ + CompatJSONDecodeError.__init__(self, *args) + InvalidJSONError.__init__(self, *self.args, **kwargs) + + +class HTTPError(RequestException): + """An HTTP error occurred.""" + + +class ConnectionError(RequestException): + """A Connection error occurred.""" + + +class ProxyError(ConnectionError): + """A proxy error occurred.""" + + +class SSLError(ConnectionError): + """An SSL error occurred.""" + + +class Timeout(RequestException): + """The request timed out. + + Catching this error will catch both + :exc:`~requests.exceptions.ConnectTimeout` and + :exc:`~requests.exceptions.ReadTimeout` errors. + """ + + +class ConnectTimeout(ConnectionError, Timeout): + """The request timed out while trying to connect to the remote server. + + Requests that produced this error are safe to retry. + """ + + +class ReadTimeout(Timeout): + """The server did not send any data in the allotted amount of time.""" + + +class URLRequired(RequestException): + """A valid URL is required to make a request.""" + + +class TooManyRedirects(RequestException): + """Too many redirects.""" + + +class MissingSchema(RequestException, ValueError): + """The URL scheme (e.g. http or https) is missing.""" + + +class InvalidSchema(RequestException, ValueError): + """The URL scheme provided is either invalid or unsupported.""" + + +class InvalidURL(RequestException, ValueError): + """The URL provided was somehow invalid.""" + + +class InvalidHeader(RequestException, ValueError): + """The header value provided was somehow invalid.""" + + +class InvalidProxyURL(InvalidURL): + """The proxy URL provided is invalid.""" + + +class ChunkedEncodingError(RequestException): + """The server declared chunked encoding but sent an invalid chunk.""" + + +class ContentDecodingError(RequestException, BaseHTTPError): + """Failed to decode response content.""" + + +class StreamConsumedError(RequestException, TypeError): + """The content for this response was already consumed.""" + + +class RetryError(RequestException): + """Custom retries logic failed""" + + +class UnrewindableBodyError(RequestException): + """Requests encountered an error when trying to rewind a body.""" + + +# Warnings + + +class RequestsWarning(Warning): + """Base warning for Requests.""" + + +class FileModeWarning(RequestsWarning, DeprecationWarning): + """A file was opened in text mode, but Requests determined its binary length.""" + + +class RequestsDependencyWarning(RequestsWarning): + """An imported dependency doesn't match the expected version range.""" diff --git a/test/fixtures/whole_applications/requests/requests/help.py b/test/fixtures/whole_applications/requests/requests/help.py new file mode 100644 index 0000000..8fbcd65 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/help.py @@ -0,0 +1,134 @@ +"""Module containing bug report helper(s).""" + +import json +import platform +import ssl +import sys + +import idna +import urllib3 + +from . import __version__ as requests_version + +try: + import charset_normalizer +except ImportError: + charset_normalizer = None + +try: + import chardet +except ImportError: + chardet = None + +try: + from urllib3.contrib import pyopenssl +except ImportError: + pyopenssl = None + OpenSSL = None + cryptography = None +else: + import cryptography + import OpenSSL + + +def _implementation(): + """Return a dict with the Python implementation and version. + + Provide both the name and the version of the Python implementation + currently running. For example, on CPython 3.10.3 it will return + {'name': 'CPython', 'version': '3.10.3'}. + + This function works best on CPython and PyPy: in particular, it probably + doesn't work for Jython or IronPython. Future investigation should be done + to work out the correct shape of the code for those platforms. + """ + implementation = platform.python_implementation() + + if implementation == "CPython": + implementation_version = platform.python_version() + elif implementation == "PyPy": + implementation_version = "{}.{}.{}".format( + sys.pypy_version_info.major, + sys.pypy_version_info.minor, + sys.pypy_version_info.micro, + ) + if sys.pypy_version_info.releaselevel != "final": + implementation_version = "".join( + [implementation_version, sys.pypy_version_info.releaselevel] + ) + elif implementation == "Jython": + implementation_version = platform.python_version() # Complete Guess + elif implementation == "IronPython": + implementation_version = platform.python_version() # Complete Guess + else: + implementation_version = "Unknown" + + return {"name": implementation, "version": implementation_version} + + +def info(): + """Generate information for a bug report.""" + try: + platform_info = { + "system": platform.system(), + "release": platform.release(), + } + except OSError: + platform_info = { + "system": "Unknown", + "release": "Unknown", + } + + implementation_info = _implementation() + urllib3_info = {"version": urllib3.__version__} + charset_normalizer_info = {"version": None} + chardet_info = {"version": None} + if charset_normalizer: + charset_normalizer_info = {"version": charset_normalizer.__version__} + if chardet: + chardet_info = {"version": chardet.__version__} + + pyopenssl_info = { + "version": None, + "openssl_version": "", + } + if OpenSSL: + pyopenssl_info = { + "version": OpenSSL.__version__, + "openssl_version": f"{OpenSSL.SSL.OPENSSL_VERSION_NUMBER:x}", + } + cryptography_info = { + "version": getattr(cryptography, "__version__", ""), + } + idna_info = { + "version": getattr(idna, "__version__", ""), + } + + system_ssl = ssl.OPENSSL_VERSION_NUMBER + system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""} + + return { + "platform": platform_info, + "implementation": implementation_info, + "system_ssl": system_ssl_info, + "using_pyopenssl": pyopenssl is not None, + "using_charset_normalizer": chardet is None, + "pyOpenSSL": pyopenssl_info, + "urllib3": urllib3_info, + "chardet": chardet_info, + "charset_normalizer": charset_normalizer_info, + "cryptography": cryptography_info, + "idna": idna_info, + "requests": { + "version": requests_version, + }, + } + + +def main(): + """Pretty-print the bug information as JSON.""" + print(json.dumps(info(), sort_keys=True, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/whole_applications/requests/requests/hooks.py b/test/fixtures/whole_applications/requests/requests/hooks.py new file mode 100644 index 0000000..d181ba2 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/hooks.py @@ -0,0 +1,33 @@ +""" +requests.hooks +~~~~~~~~~~~~~~ + +This module provides the capabilities for the Requests hooks system. + +Available hooks: + +``response``: + The response generated from a Request. +""" +HOOKS = ["response"] + + +def default_hooks(): + return {event: [] for event in HOOKS} + + +# TODO: response is the only one + + +def dispatch_hook(key, hooks, hook_data, **kwargs): + """Dispatches a hook dictionary on a given piece of data.""" + hooks = hooks or {} + hooks = hooks.get(key) + if hooks: + if hasattr(hooks, "__call__"): + hooks = [hooks] + for hook in hooks: + _hook_data = hook(hook_data, **kwargs) + if _hook_data is not None: + hook_data = _hook_data + return hook_data diff --git a/test/fixtures/whole_applications/requests/requests/models.py b/test/fixtures/whole_applications/requests/requests/models.py new file mode 100644 index 0000000..617a413 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/models.py @@ -0,0 +1,1034 @@ +""" +requests.models +~~~~~~~~~~~~~~~ + +This module contains the primary objects that power Requests. +""" + +import datetime + +# Import encoding now, to avoid implicit import later. +# Implicit import within threads may cause LookupError when standard library is in a ZIP, +# such as in Embedded Python. See https://github.com/psf/requests/issues/3578. +import encodings.idna # noqa: F401 +from io import UnsupportedOperation + +from urllib3.exceptions import ( + DecodeError, + LocationParseError, + ProtocolError, + ReadTimeoutError, + SSLError, +) +from urllib3.fields import RequestField +from urllib3.filepost import encode_multipart_formdata +from urllib3.util import parse_url + +from ._internal_utils import to_native_string, unicode_is_ascii +from .auth import HTTPBasicAuth +from .compat import ( + Callable, + JSONDecodeError, + Mapping, + basestring, + builtin_str, + chardet, + cookielib, +) +from .compat import json as complexjson +from .compat import urlencode, urlsplit, urlunparse +from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header +from .exceptions import ( + ChunkedEncodingError, + ConnectionError, + ContentDecodingError, + HTTPError, + InvalidJSONError, + InvalidURL, +) +from .exceptions import JSONDecodeError as RequestsJSONDecodeError +from .exceptions import MissingSchema +from .exceptions import SSLError as RequestsSSLError +from .exceptions import StreamConsumedError +from .hooks import default_hooks +from .status_codes import codes +from .structures import CaseInsensitiveDict +from .utils import ( + check_header_validity, + get_auth_from_url, + guess_filename, + guess_json_utf, + iter_slices, + parse_header_links, + requote_uri, + stream_decode_response_unicode, + super_len, + to_key_val_list, +) + +#: The set of HTTP status codes that indicate an automatically +#: processable redirect. +REDIRECT_STATI = ( + codes.moved, # 301 + codes.found, # 302 + codes.other, # 303 + codes.temporary_redirect, # 307 + codes.permanent_redirect, # 308 +) + +DEFAULT_REDIRECT_LIMIT = 30 +CONTENT_CHUNK_SIZE = 10 * 1024 +ITER_CHUNK_SIZE = 512 + + +class RequestEncodingMixin: + @property + def path_url(self): + """Build the path URL to use.""" + + url = [] + + p = urlsplit(self.url) + + path = p.path + if not path: + path = "/" + + url.append(path) + + query = p.query + if query: + url.append("?") + url.append(query) + + return "".join(url) + + @staticmethod + def _encode_params(data): + """Encode parameters in a piece of data. + + Will successfully encode parameters when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary + if parameters are supplied as a dict. + """ + + if isinstance(data, (str, bytes)): + return data + elif hasattr(data, "read"): + return data + elif hasattr(data, "__iter__"): + result = [] + for k, vs in to_key_val_list(data): + if isinstance(vs, basestring) or not hasattr(vs, "__iter__"): + vs = [vs] + for v in vs: + if v is not None: + result.append( + ( + k.encode("utf-8") if isinstance(k, str) else k, + v.encode("utf-8") if isinstance(v, str) else v, + ) + ) + return urlencode(result, doseq=True) + else: + return data + + @staticmethod + def _encode_files(files, data): + """Build the body for a multipart/form-data request. + + Will successfully encode files when passed as a dict or a list of + tuples. Order is retained if data is a list of tuples but arbitrary + if parameters are supplied as a dict. + The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype) + or 4-tuples (filename, fileobj, contentype, custom_headers). + """ + if not files: + raise ValueError("Files must be provided.") + elif isinstance(data, basestring): + raise ValueError("Data must not be a string.") + + new_fields = [] + fields = to_key_val_list(data or {}) + files = to_key_val_list(files or {}) + + for field, val in fields: + if isinstance(val, basestring) or not hasattr(val, "__iter__"): + val = [val] + for v in val: + if v is not None: + # Don't call str() on bytestrings: in Py3 it all goes wrong. + if not isinstance(v, bytes): + v = str(v) + + new_fields.append( + ( + field.decode("utf-8") + if isinstance(field, bytes) + else field, + v.encode("utf-8") if isinstance(v, str) else v, + ) + ) + + for (k, v) in files: + # support for explicit filename + ft = None + fh = None + if isinstance(v, (tuple, list)): + if len(v) == 2: + fn, fp = v + elif len(v) == 3: + fn, fp, ft = v + else: + fn, fp, ft, fh = v + else: + fn = guess_filename(v) or k + fp = v + + if isinstance(fp, (str, bytes, bytearray)): + fdata = fp + elif hasattr(fp, "read"): + fdata = fp.read() + elif fp is None: + continue + else: + fdata = fp + + rf = RequestField(name=k, data=fdata, filename=fn, headers=fh) + rf.make_multipart(content_type=ft) + new_fields.append(rf) + + body, content_type = encode_multipart_formdata(new_fields) + + return body, content_type + + +class RequestHooksMixin: + def register_hook(self, event, hook): + """Properly register a hook.""" + + if event not in self.hooks: + raise ValueError(f'Unsupported event specified, with event name "{event}"') + + if isinstance(hook, Callable): + self.hooks[event].append(hook) + elif hasattr(hook, "__iter__"): + self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) + + def deregister_hook(self, event, hook): + """Deregister a previously registered hook. + Returns True if the hook existed, False if not. + """ + + try: + self.hooks[event].remove(hook) + return True + except ValueError: + return False + + +class Request(RequestHooksMixin): + """A user-created :class:`Request ` object. + + Used to prepare a :class:`PreparedRequest `, which is sent to the server. + + :param method: HTTP method to use. + :param url: URL to send. + :param headers: dictionary of headers to send. + :param files: dictionary of {filename: fileobject} files to multipart upload. + :param data: the body to attach to the request. If a dictionary or + list of tuples ``[(key, value)]`` is provided, form-encoding will + take place. + :param json: json for the body to attach to the request (if files or data is not specified). + :param params: URL parameters to append to the URL. If a dictionary or + list of tuples ``[(key, value)]`` is provided, form-encoding will + take place. + :param auth: Auth handler or (user, pass) tuple. + :param cookies: dictionary or CookieJar of cookies to attach to this request. + :param hooks: dictionary of callback hooks, for internal usage. + + Usage:: + + >>> import requests + >>> req = requests.Request('GET', 'https://httpbin.org/get') + >>> req.prepare() + + """ + + def __init__( + self, + method=None, + url=None, + headers=None, + files=None, + data=None, + params=None, + auth=None, + cookies=None, + hooks=None, + json=None, + ): + + # Default empty dicts for dict params. + data = [] if data is None else data + files = [] if files is None else files + headers = {} if headers is None else headers + params = {} if params is None else params + hooks = {} if hooks is None else hooks + + self.hooks = default_hooks() + for (k, v) in list(hooks.items()): + self.register_hook(event=k, hook=v) + + self.method = method + self.url = url + self.headers = headers + self.files = files + self.data = data + self.json = json + self.params = params + self.auth = auth + self.cookies = cookies + + def __repr__(self): + return f"" + + def prepare(self): + """Constructs a :class:`PreparedRequest ` for transmission and returns it.""" + p = PreparedRequest() + p.prepare( + method=self.method, + url=self.url, + headers=self.headers, + files=self.files, + data=self.data, + json=self.json, + params=self.params, + auth=self.auth, + cookies=self.cookies, + hooks=self.hooks, + ) + return p + + +class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): + """The fully mutable :class:`PreparedRequest ` object, + containing the exact bytes that will be sent to the server. + + Instances are generated from a :class:`Request ` object, and + should not be instantiated manually; doing so may produce undesirable + effects. + + Usage:: + + >>> import requests + >>> req = requests.Request('GET', 'https://httpbin.org/get') + >>> r = req.prepare() + >>> r + + + >>> s = requests.Session() + >>> s.send(r) + + """ + + def __init__(self): + #: HTTP verb to send to the server. + self.method = None + #: HTTP URL to send the request to. + self.url = None + #: dictionary of HTTP headers. + self.headers = None + # The `CookieJar` used to create the Cookie header will be stored here + # after prepare_cookies is called + self._cookies = None + #: request body to send to the server. + self.body = None + #: dictionary of callback hooks, for internal usage. + self.hooks = default_hooks() + #: integer denoting starting position of a readable file-like body. + self._body_position = None + + def prepare( + self, + method=None, + url=None, + headers=None, + files=None, + data=None, + params=None, + auth=None, + cookies=None, + hooks=None, + json=None, + ): + """Prepares the entire request with the given parameters.""" + + self.prepare_method(method) + self.prepare_url(url, params) + self.prepare_headers(headers) + self.prepare_cookies(cookies) + self.prepare_body(data, files, json) + self.prepare_auth(auth, url) + + # Note that prepare_auth must be last to enable authentication schemes + # such as OAuth to work on a fully prepared request. + + # This MUST go after prepare_auth. Authenticators could add a hook + self.prepare_hooks(hooks) + + def __repr__(self): + return f"" + + def copy(self): + p = PreparedRequest() + p.method = self.method + p.url = self.url + p.headers = self.headers.copy() if self.headers is not None else None + p._cookies = _copy_cookie_jar(self._cookies) + p.body = self.body + p.hooks = self.hooks + p._body_position = self._body_position + return p + + def prepare_method(self, method): + """Prepares the given HTTP method.""" + self.method = method + if self.method is not None: + self.method = to_native_string(self.method.upper()) + + @staticmethod + def _get_idna_encoded_host(host): + import idna + + try: + host = idna.encode(host, uts46=True).decode("utf-8") + except idna.IDNAError: + raise UnicodeError + return host + + def prepare_url(self, url, params): + """Prepares the given HTTP URL.""" + #: Accept objects that have string representations. + #: We're unable to blindly call unicode/str functions + #: as this will include the bytestring indicator (b'') + #: on python 3.x. + #: https://github.com/psf/requests/pull/2238 + if isinstance(url, bytes): + url = url.decode("utf8") + else: + url = str(url) + + # Remove leading whitespaces from url + url = url.lstrip() + + # Don't do any URL preparation for non-HTTP schemes like `mailto`, + # `data` etc to work around exceptions from `url_parse`, which + # handles RFC 3986 only. + if ":" in url and not url.lower().startswith("http"): + self.url = url + return + + # Support for unicode domain names and paths. + try: + scheme, auth, host, port, path, query, fragment = parse_url(url) + except LocationParseError as e: + raise InvalidURL(*e.args) + + if not scheme: + raise MissingSchema( + f"Invalid URL {url!r}: No scheme supplied. " + f"Perhaps you meant https://{url}?" + ) + + if not host: + raise InvalidURL(f"Invalid URL {url!r}: No host supplied") + + # In general, we want to try IDNA encoding the hostname if the string contains + # non-ASCII characters. This allows users to automatically get the correct IDNA + # behaviour. For strings containing only ASCII characters, we need to also verify + # it doesn't start with a wildcard (*), before allowing the unencoded hostname. + if not unicode_is_ascii(host): + try: + host = self._get_idna_encoded_host(host) + except UnicodeError: + raise InvalidURL("URL has an invalid label.") + elif host.startswith(("*", ".")): + raise InvalidURL("URL has an invalid label.") + + # Carefully reconstruct the network location + netloc = auth or "" + if netloc: + netloc += "@" + netloc += host + if port: + netloc += f":{port}" + + # Bare domains aren't valid URLs. + if not path: + path = "/" + + if isinstance(params, (str, bytes)): + params = to_native_string(params) + + enc_params = self._encode_params(params) + if enc_params: + if query: + query = f"{query}&{enc_params}" + else: + query = enc_params + + url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment])) + self.url = url + + def prepare_headers(self, headers): + """Prepares the given HTTP headers.""" + + self.headers = CaseInsensitiveDict() + if headers: + for header in headers.items(): + # Raise exception on invalid header value. + check_header_validity(header) + name, value = header + self.headers[to_native_string(name)] = value + + def prepare_body(self, data, files, json=None): + """Prepares the given HTTP body data.""" + + # Check if file, fo, generator, iterator. + # If not, run through normal process. + + # Nottin' on you. + body = None + content_type = None + + if not data and json is not None: + # urllib3 requires a bytes-like body. Python 2's json.dumps + # provides this natively, but Python 3 gives a Unicode string. + content_type = "application/json" + + try: + body = complexjson.dumps(json, allow_nan=False) + except ValueError as ve: + raise InvalidJSONError(ve, request=self) + + if not isinstance(body, bytes): + body = body.encode("utf-8") + + is_stream = all( + [ + hasattr(data, "__iter__"), + not isinstance(data, (basestring, list, tuple, Mapping)), + ] + ) + + if is_stream: + try: + length = super_len(data) + except (TypeError, AttributeError, UnsupportedOperation): + length = None + + body = data + + if getattr(body, "tell", None) is not None: + # Record the current file position before reading. + # This will allow us to rewind a file in the event + # of a redirect. + try: + self._body_position = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body + self._body_position = object() + + if files: + raise NotImplementedError( + "Streamed bodies and files are mutually exclusive." + ) + + if length: + self.headers["Content-Length"] = builtin_str(length) + else: + self.headers["Transfer-Encoding"] = "chunked" + else: + # Multi-part file uploads. + if files: + (body, content_type) = self._encode_files(files, data) + else: + if data: + body = self._encode_params(data) + if isinstance(data, basestring) or hasattr(data, "read"): + content_type = None + else: + content_type = "application/x-www-form-urlencoded" + + self.prepare_content_length(body) + + # Add content-type if it wasn't explicitly provided. + if content_type and ("content-type" not in self.headers): + self.headers["Content-Type"] = content_type + + self.body = body + + def prepare_content_length(self, body): + """Prepare Content-Length header based on request method and body""" + if body is not None: + length = super_len(body) + if length: + # If length exists, set it. Otherwise, we fallback + # to Transfer-Encoding: chunked. + self.headers["Content-Length"] = builtin_str(length) + elif ( + self.method not in ("GET", "HEAD") + and self.headers.get("Content-Length") is None + ): + # Set Content-Length to 0 for methods that can have a body + # but don't provide one. (i.e. not GET or HEAD) + self.headers["Content-Length"] = "0" + + def prepare_auth(self, auth, url=""): + """Prepares the given HTTP auth data.""" + + # If no Auth is explicitly provided, extract it from the URL first. + if auth is None: + url_auth = get_auth_from_url(self.url) + auth = url_auth if any(url_auth) else None + + if auth: + if isinstance(auth, tuple) and len(auth) == 2: + # special-case basic HTTP auth + auth = HTTPBasicAuth(*auth) + + # Allow auth to make its changes. + r = auth(self) + + # Update self to reflect the auth changes. + self.__dict__.update(r.__dict__) + + # Recompute Content-Length + self.prepare_content_length(self.body) + + def prepare_cookies(self, cookies): + """Prepares the given HTTP cookie data. + + This function eventually generates a ``Cookie`` header from the + given cookies using cookielib. Due to cookielib's design, the header + will not be regenerated if it already exists, meaning this function + can only be called once for the life of the + :class:`PreparedRequest ` object. Any subsequent calls + to ``prepare_cookies`` will have no actual effect, unless the "Cookie" + header is removed beforehand. + """ + if isinstance(cookies, cookielib.CookieJar): + self._cookies = cookies + else: + self._cookies = cookiejar_from_dict(cookies) + + cookie_header = get_cookie_header(self._cookies, self) + if cookie_header is not None: + self.headers["Cookie"] = cookie_header + + def prepare_hooks(self, hooks): + """Prepares the given hooks.""" + # hooks can be passed as None to the prepare method and to this + # method. To prevent iterating over None, simply use an empty list + # if hooks is False-y + hooks = hooks or [] + for event in hooks: + self.register_hook(event, hooks[event]) + + +class Response: + """The :class:`Response ` object, which contains a + server's response to an HTTP request. + """ + + __attrs__ = [ + "_content", + "status_code", + "headers", + "url", + "history", + "encoding", + "reason", + "cookies", + "elapsed", + "request", + ] + + def __init__(self): + self._content = False + self._content_consumed = False + self._next = None + + #: Integer Code of responded HTTP Status, e.g. 404 or 200. + self.status_code = None + + #: Case-insensitive Dictionary of Response Headers. + #: For example, ``headers['content-encoding']`` will return the + #: value of a ``'Content-Encoding'`` response header. + self.headers = CaseInsensitiveDict() + + #: File-like object representation of response (for advanced usage). + #: Use of ``raw`` requires that ``stream=True`` be set on the request. + #: This requirement does not apply for use internally to Requests. + self.raw = None + + #: Final URL location of Response. + self.url = None + + #: Encoding to decode with when accessing r.text. + self.encoding = None + + #: A list of :class:`Response ` objects from + #: the history of the Request. Any redirect responses will end + #: up here. The list is sorted from the oldest to the most recent request. + self.history = [] + + #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". + self.reason = None + + #: A CookieJar of Cookies the server sent back. + self.cookies = cookiejar_from_dict({}) + + #: The amount of time elapsed between sending the request + #: and the arrival of the response (as a timedelta). + #: This property specifically measures the time taken between sending + #: the first byte of the request and finishing parsing the headers. It + #: is therefore unaffected by consuming the response content or the + #: value of the ``stream`` keyword argument. + self.elapsed = datetime.timedelta(0) + + #: The :class:`PreparedRequest ` object to which this + #: is a response. + self.request = None + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def __getstate__(self): + # Consume everything; accessing the content attribute makes + # sure the content has been fully read. + if not self._content_consumed: + self.content + + return {attr: getattr(self, attr, None) for attr in self.__attrs__} + + def __setstate__(self, state): + for name, value in state.items(): + setattr(self, name, value) + + # pickled objects do not have .raw + setattr(self, "_content_consumed", True) + setattr(self, "raw", None) + + def __repr__(self): + return f"" + + def __bool__(self): + """Returns True if :attr:`status_code` is less than 400. + + This attribute checks if the status code of the response is between + 400 and 600 to see if there was a client error or a server error. If + the status code, is between 200 and 400, this will return True. This + is **not** a check to see if the response code is ``200 OK``. + """ + return self.ok + + def __nonzero__(self): + """Returns True if :attr:`status_code` is less than 400. + + This attribute checks if the status code of the response is between + 400 and 600 to see if there was a client error or a server error. If + the status code, is between 200 and 400, this will return True. This + is **not** a check to see if the response code is ``200 OK``. + """ + return self.ok + + def __iter__(self): + """Allows you to use a response as an iterator.""" + return self.iter_content(128) + + @property + def ok(self): + """Returns True if :attr:`status_code` is less than 400, False if not. + + This attribute checks if the status code of the response is between + 400 and 600 to see if there was a client error or a server error. If + the status code is between 200 and 400, this will return True. This + is **not** a check to see if the response code is ``200 OK``. + """ + try: + self.raise_for_status() + except HTTPError: + return False + return True + + @property + def is_redirect(self): + """True if this Response is a well-formed HTTP redirect that could have + been processed automatically (by :meth:`Session.resolve_redirects`). + """ + return "location" in self.headers and self.status_code in REDIRECT_STATI + + @property + def is_permanent_redirect(self): + """True if this Response one of the permanent versions of redirect.""" + return "location" in self.headers and self.status_code in ( + codes.moved_permanently, + codes.permanent_redirect, + ) + + @property + def next(self): + """Returns a PreparedRequest for the next request in a redirect chain, if there is one.""" + return self._next + + @property + def apparent_encoding(self): + """The apparent encoding, provided by the charset_normalizer or chardet libraries.""" + return chardet.detect(self.content)["encoding"] + + def iter_content(self, chunk_size=1, decode_unicode=False): + """Iterates over the response data. When stream=True is set on the + request, this avoids reading the content at once into memory for + large responses. The chunk size is the number of bytes it should + read into memory. This is not necessarily the length of each item + returned as decoding can take place. + + chunk_size must be of type int or None. A value of None will + function differently depending on the value of `stream`. + stream=True will read data as it arrives in whatever size the + chunks are received. If stream=False, data is returned as + a single chunk. + + If decode_unicode is True, content will be decoded using the best + available encoding based on the response. + """ + + def generate(): + # Special case for urllib3. + if hasattr(self.raw, "stream"): + try: + yield from self.raw.stream(chunk_size, decode_content=True) + except ProtocolError as e: + raise ChunkedEncodingError(e) + except DecodeError as e: + raise ContentDecodingError(e) + except ReadTimeoutError as e: + raise ConnectionError(e) + except SSLError as e: + raise RequestsSSLError(e) + else: + # Standard file-like object. + while True: + chunk = self.raw.read(chunk_size) + if not chunk: + break + yield chunk + + self._content_consumed = True + + if self._content_consumed and isinstance(self._content, bool): + raise StreamConsumedError() + elif chunk_size is not None and not isinstance(chunk_size, int): + raise TypeError( + f"chunk_size must be an int, it is instead a {type(chunk_size)}." + ) + # simulate reading small chunks of the content + reused_chunks = iter_slices(self._content, chunk_size) + + stream_chunks = generate() + + chunks = reused_chunks if self._content_consumed else stream_chunks + + if decode_unicode: + chunks = stream_decode_response_unicode(chunks, self) + + return chunks + + def iter_lines( + self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None + ): + """Iterates over the response data, one line at a time. When + stream=True is set on the request, this avoids reading the + content at once into memory for large responses. + + .. note:: This method is not reentrant safe. + """ + + pending = None + + for chunk in self.iter_content( + chunk_size=chunk_size, decode_unicode=decode_unicode + ): + + if pending is not None: + chunk = pending + chunk + + if delimiter: + lines = chunk.split(delimiter) + else: + lines = chunk.splitlines() + + if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: + pending = lines.pop() + else: + pending = None + + yield from lines + + if pending is not None: + yield pending + + @property + def content(self): + """Content of the response, in bytes.""" + + if self._content is False: + # Read the contents. + if self._content_consumed: + raise RuntimeError("The content for this response was already consumed") + + if self.status_code == 0 or self.raw is None: + self._content = None + else: + self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + + self._content_consumed = True + # don't need to release the connection; that's been handled by urllib3 + # since we exhausted the data. + return self._content + + @property + def text(self): + """Content of the response, in unicode. + + If Response.encoding is None, encoding will be guessed using + ``charset_normalizer`` or ``chardet``. + + The encoding of the response content is determined based solely on HTTP + headers, following RFC 2616 to the letter. If you can take advantage of + non-HTTP knowledge to make a better guess at the encoding, you should + set ``r.encoding`` appropriately before accessing this property. + """ + + # Try charset from content-type + content = None + encoding = self.encoding + + if not self.content: + return "" + + # Fallback to auto-detected encoding. + if self.encoding is None: + encoding = self.apparent_encoding + + # Decode unicode from given encoding. + try: + content = str(self.content, encoding, errors="replace") + except (LookupError, TypeError): + # A LookupError is raised if the encoding was not found which could + # indicate a misspelling or similar mistake. + # + # A TypeError can be raised if encoding is None + # + # So we try blindly encoding. + content = str(self.content, errors="replace") + + return content + + def json(self, **kwargs): + r"""Returns the json-encoded content of a response, if any. + + :param \*\*kwargs: Optional arguments that ``json.loads`` takes. + :raises requests.exceptions.JSONDecodeError: If the response body does not + contain valid json. + """ + + if not self.encoding and self.content and len(self.content) > 3: + # No encoding set. JSON RFC 4627 section 3 states we should expect + # UTF-8, -16 or -32. Detect which one to use; If the detection or + # decoding fails, fall back to `self.text` (using charset_normalizer to make + # a best guess). + encoding = guess_json_utf(self.content) + if encoding is not None: + try: + return complexjson.loads(self.content.decode(encoding), **kwargs) + except UnicodeDecodeError: + # Wrong UTF codec detected; usually because it's not UTF-8 + # but some other 8-bit codec. This is an RFC violation, + # and the server didn't bother to tell us what codec *was* + # used. + pass + except JSONDecodeError as e: + raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) + + try: + return complexjson.loads(self.text, **kwargs) + except JSONDecodeError as e: + # Catch JSON-related errors and raise as requests.JSONDecodeError + # This aliases json.JSONDecodeError and simplejson.JSONDecodeError + raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) + + @property + def links(self): + """Returns the parsed header links of the response, if any.""" + + header = self.headers.get("link") + + resolved_links = {} + + if header: + links = parse_header_links(header) + + for link in links: + key = link.get("rel") or link.get("url") + resolved_links[key] = link + + return resolved_links + + def raise_for_status(self): + """Raises :class:`HTTPError`, if one occurred.""" + + http_error_msg = "" + if isinstance(self.reason, bytes): + # We attempt to decode utf-8 first because some servers + # choose to localize their reason strings. If the string + # isn't utf-8, we fall back to iso-8859-1 for all other + # encodings. (See PR #3538) + try: + reason = self.reason.decode("utf-8") + except UnicodeDecodeError: + reason = self.reason.decode("iso-8859-1") + else: + reason = self.reason + + if 400 <= self.status_code < 500: + http_error_msg = ( + f"{self.status_code} Client Error: {reason} for url: {self.url}" + ) + + elif 500 <= self.status_code < 600: + http_error_msg = ( + f"{self.status_code} Server Error: {reason} for url: {self.url}" + ) + + if http_error_msg: + raise HTTPError(http_error_msg, response=self) + + def close(self): + """Releases the connection back to the pool. Once this method has been + called the underlying ``raw`` object must not be accessed again. + + *Note: Should not normally need to be called explicitly.* + """ + if not self._content_consumed: + self.raw.close() + + release_conn = getattr(self.raw, "release_conn", None) + if release_conn is not None: + release_conn() diff --git a/test/fixtures/whole_applications/requests/requests/packages.py b/test/fixtures/whole_applications/requests/requests/packages.py new file mode 100644 index 0000000..77c45c9 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/packages.py @@ -0,0 +1,28 @@ +import sys + +try: + import chardet +except ImportError: + import warnings + + import charset_normalizer as chardet + + warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer") + +# This code exists for backwards compatibility reasons. +# I don't like it either. Just look the other way. :) + +for package in ("urllib3", "idna"): + locals()[package] = __import__(package) + # This traversal is apparently necessary such that the identities are + # preserved (requests.packages.urllib3.* is urllib3.*) + for mod in list(sys.modules): + if mod == package or mod.startswith(f"{package}."): + sys.modules[f"requests.packages.{mod}"] = sys.modules[mod] + +target = chardet.__name__ +for mod in list(sys.modules): + if mod == target or mod.startswith(f"{target}."): + target = target.replace(target, "chardet") + sys.modules[f"requests.packages.{target}"] = sys.modules[mod] +# Kinda cool, though, right? diff --git a/test/fixtures/whole_applications/requests/requests/sessions.py b/test/fixtures/whole_applications/requests/requests/sessions.py new file mode 100644 index 0000000..dbcf2a7 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/sessions.py @@ -0,0 +1,833 @@ +""" +requests.sessions +~~~~~~~~~~~~~~~~~ + +This module provides a Session object to manage and persist settings across +requests (cookies, auth, proxies). +""" +import os +import sys +import time +from collections import OrderedDict +from datetime import timedelta + +from ._internal_utils import to_native_string +from .adapters import HTTPAdapter +from .auth import _basic_auth_str +from .compat import Mapping, cookielib, urljoin, urlparse +from .cookies import ( + RequestsCookieJar, + cookiejar_from_dict, + extract_cookies_to_jar, + merge_cookies, +) +from .exceptions import ( + ChunkedEncodingError, + ContentDecodingError, + InvalidSchema, + TooManyRedirects, +) +from .hooks import default_hooks, dispatch_hook + +# formerly defined here, reexposed here for backward compatibility +from .models import ( # noqa: F401 + DEFAULT_REDIRECT_LIMIT, + REDIRECT_STATI, + PreparedRequest, + Request, +) +from .status_codes import codes +from .structures import CaseInsensitiveDict +from .utils import ( # noqa: F401 + DEFAULT_PORTS, + default_headers, + get_auth_from_url, + get_environ_proxies, + get_netrc_auth, + requote_uri, + resolve_proxies, + rewind_body, + should_bypass_proxies, + to_key_val_list, +) + +# Preferred clock, based on which one is more accurate on a given system. +if sys.platform == "win32": + preferred_clock = time.perf_counter +else: + preferred_clock = time.time + + +def merge_setting(request_setting, session_setting, dict_class=OrderedDict): + """Determines appropriate setting for a given request, taking into account + the explicit setting on that request, and the setting in the session. If a + setting is a dictionary, they will be merged together using `dict_class` + """ + + if session_setting is None: + return request_setting + + if request_setting is None: + return session_setting + + # Bypass if not a dictionary (e.g. verify) + if not ( + isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping) + ): + return request_setting + + merged_setting = dict_class(to_key_val_list(session_setting)) + merged_setting.update(to_key_val_list(request_setting)) + + # Remove keys that are set to None. Extract keys first to avoid altering + # the dictionary during iteration. + none_keys = [k for (k, v) in merged_setting.items() if v is None] + for key in none_keys: + del merged_setting[key] + + return merged_setting + + +def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict): + """Properly merges both requests and session hooks. + + This is necessary because when request_hooks == {'response': []}, the + merge breaks Session hooks entirely. + """ + if session_hooks is None or session_hooks.get("response") == []: + return request_hooks + + if request_hooks is None or request_hooks.get("response") == []: + return session_hooks + + return merge_setting(request_hooks, session_hooks, dict_class) + + +class SessionRedirectMixin: + def get_redirect_target(self, resp): + """Receives a Response. Returns a redirect URI or ``None``""" + # Due to the nature of how requests processes redirects this method will + # be called at least once upon the original response and at least twice + # on each subsequent redirect response (if any). + # If a custom mixin is used to handle this logic, it may be advantageous + # to cache the redirect location onto the response object as a private + # attribute. + if resp.is_redirect: + location = resp.headers["location"] + # Currently the underlying http module on py3 decode headers + # in latin1, but empirical evidence suggests that latin1 is very + # rarely used with non-ASCII characters in HTTP headers. + # It is more likely to get UTF8 header rather than latin1. + # This causes incorrect handling of UTF8 encoded location headers. + # To solve this, we re-encode the location in latin1. + location = location.encode("latin1") + return to_native_string(location, "utf8") + return None + + def should_strip_auth(self, old_url, new_url): + """Decide whether Authorization header should be removed when redirecting""" + old_parsed = urlparse(old_url) + new_parsed = urlparse(new_url) + if old_parsed.hostname != new_parsed.hostname: + return True + # Special case: allow http -> https redirect when using the standard + # ports. This isn't specified by RFC 7235, but is kept to avoid + # breaking backwards compatibility with older versions of requests + # that allowed any redirects on the same host. + if ( + old_parsed.scheme == "http" + and old_parsed.port in (80, None) + and new_parsed.scheme == "https" + and new_parsed.port in (443, None) + ): + return False + + # Handle default port usage corresponding to scheme. + changed_port = old_parsed.port != new_parsed.port + changed_scheme = old_parsed.scheme != new_parsed.scheme + default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None) + if ( + not changed_scheme + and old_parsed.port in default_port + and new_parsed.port in default_port + ): + return False + + # Standard case: root URI must match + return changed_port or changed_scheme + + def resolve_redirects( + self, + resp, + req, + stream=False, + timeout=None, + verify=True, + cert=None, + proxies=None, + yield_requests=False, + **adapter_kwargs, + ): + """Receives a Response. Returns a generator of Responses or Requests.""" + + hist = [] # keep track of history + + url = self.get_redirect_target(resp) + previous_fragment = urlparse(req.url).fragment + while url: + prepared_request = req.copy() + + # Update history and keep track of redirects. + # resp.history must ignore the original request in this loop + hist.append(resp) + resp.history = hist[1:] + + try: + resp.content # Consume socket so it can be released + except (ChunkedEncodingError, ContentDecodingError, RuntimeError): + resp.raw.read(decode_content=False) + + if len(resp.history) >= self.max_redirects: + raise TooManyRedirects( + f"Exceeded {self.max_redirects} redirects.", response=resp + ) + + # Release the connection back into the pool. + resp.close() + + # Handle redirection without scheme (see: RFC 1808 Section 4) + if url.startswith("//"): + parsed_rurl = urlparse(resp.url) + url = ":".join([to_native_string(parsed_rurl.scheme), url]) + + # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2) + parsed = urlparse(url) + if parsed.fragment == "" and previous_fragment: + parsed = parsed._replace(fragment=previous_fragment) + elif parsed.fragment: + previous_fragment = parsed.fragment + url = parsed.geturl() + + # Facilitate relative 'location' headers, as allowed by RFC 7231. + # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') + # Compliant with RFC3986, we percent encode the url. + if not parsed.netloc: + url = urljoin(resp.url, requote_uri(url)) + else: + url = requote_uri(url) + + prepared_request.url = to_native_string(url) + + self.rebuild_method(prepared_request, resp) + + # https://github.com/psf/requests/issues/1084 + if resp.status_code not in ( + codes.temporary_redirect, + codes.permanent_redirect, + ): + # https://github.com/psf/requests/issues/3490 + purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding") + for header in purged_headers: + prepared_request.headers.pop(header, None) + prepared_request.body = None + + headers = prepared_request.headers + headers.pop("Cookie", None) + + # Extract any cookies sent on the response to the cookiejar + # in the new request. Because we've mutated our copied prepared + # request, use the old one that we haven't yet touched. + extract_cookies_to_jar(prepared_request._cookies, req, resp.raw) + merge_cookies(prepared_request._cookies, self.cookies) + prepared_request.prepare_cookies(prepared_request._cookies) + + # Rebuild auth and proxy information. + proxies = self.rebuild_proxies(prepared_request, proxies) + self.rebuild_auth(prepared_request, resp) + + # A failed tell() sets `_body_position` to `object()`. This non-None + # value ensures `rewindable` will be True, allowing us to raise an + # UnrewindableBodyError, instead of hanging the connection. + rewindable = prepared_request._body_position is not None and ( + "Content-Length" in headers or "Transfer-Encoding" in headers + ) + + # Attempt to rewind consumed file-like object. + if rewindable: + rewind_body(prepared_request) + + # Override the original request. + req = prepared_request + + if yield_requests: + yield req + else: + + resp = self.send( + req, + stream=stream, + timeout=timeout, + verify=verify, + cert=cert, + proxies=proxies, + allow_redirects=False, + **adapter_kwargs, + ) + + extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) + + # extract redirect url, if any, for the next loop + url = self.get_redirect_target(resp) + yield resp + + def rebuild_auth(self, prepared_request, response): + """When being redirected we may want to strip authentication from the + request to avoid leaking credentials. This method intelligently removes + and reapplies authentication where possible to avoid credential loss. + """ + headers = prepared_request.headers + url = prepared_request.url + + if "Authorization" in headers and self.should_strip_auth( + response.request.url, url + ): + # If we get redirected to a new host, we should strip out any + # authentication headers. + del headers["Authorization"] + + # .netrc might have more auth for us on our new host. + new_auth = get_netrc_auth(url) if self.trust_env else None + if new_auth is not None: + prepared_request.prepare_auth(new_auth) + + def rebuild_proxies(self, prepared_request, proxies): + """This method re-evaluates the proxy configuration by considering the + environment variables. If we are redirected to a URL covered by + NO_PROXY, we strip the proxy configuration. Otherwise, we set missing + proxy keys for this URL (in case they were stripped by a previous + redirect). + + This method also replaces the Proxy-Authorization header where + necessary. + + :rtype: dict + """ + headers = prepared_request.headers + scheme = urlparse(prepared_request.url).scheme + new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env) + + if "Proxy-Authorization" in headers: + del headers["Proxy-Authorization"] + + try: + username, password = get_auth_from_url(new_proxies[scheme]) + except KeyError: + username, password = None, None + + # urllib3 handles proxy authorization for us in the standard adapter. + # Avoid appending this to TLS tunneled requests where it may be leaked. + if not scheme.startswith('https') and username and password: + headers["Proxy-Authorization"] = _basic_auth_str(username, password) + + return new_proxies + + def rebuild_method(self, prepared_request, response): + """When being redirected we may want to change the method of the request + based on certain specs or browser behavior. + """ + method = prepared_request.method + + # https://tools.ietf.org/html/rfc7231#section-6.4.4 + if response.status_code == codes.see_other and method != "HEAD": + method = "GET" + + # Do what the browsers do, despite standards... + # First, turn 302s into GETs. + if response.status_code == codes.found and method != "HEAD": + method = "GET" + + # Second, if a POST is responded to with a 301, turn it into a GET. + # This bizarre behaviour is explained in Issue 1704. + if response.status_code == codes.moved and method == "POST": + method = "GET" + + prepared_request.method = method + + +class Session(SessionRedirectMixin): + """A Requests session. + + Provides cookie persistence, connection-pooling, and configuration. + + Basic Usage:: + + >>> import requests + >>> s = requests.Session() + >>> s.get('https://httpbin.org/get') + + + Or as a context manager:: + + >>> with requests.Session() as s: + ... s.get('https://httpbin.org/get') + + """ + + __attrs__ = [ + "headers", + "cookies", + "auth", + "proxies", + "hooks", + "params", + "verify", + "cert", + "adapters", + "stream", + "trust_env", + "max_redirects", + ] + + def __init__(self): + + #: A case-insensitive dictionary of headers to be sent on each + #: :class:`Request ` sent from this + #: :class:`Session `. + self.headers = default_headers() + + #: Default Authentication tuple or object to attach to + #: :class:`Request `. + self.auth = None + + #: Dictionary mapping protocol or protocol and host to the URL of the proxy + #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to + #: be used on each :class:`Request `. + self.proxies = {} + + #: Event-handling hooks. + self.hooks = default_hooks() + + #: Dictionary of querystring data to attach to each + #: :class:`Request `. The dictionary values may be lists for + #: representing multivalued query parameters. + self.params = {} + + #: Stream response content default. + self.stream = False + + #: SSL Verification default. + #: Defaults to `True`, requiring requests to verify the TLS certificate at the + #: remote end. + #: If verify is set to `False`, requests will accept any TLS certificate + #: presented by the server, and will ignore hostname mismatches and/or + #: expired certificates, which will make your application vulnerable to + #: man-in-the-middle (MitM) attacks. + #: Only set this to `False` for testing. + self.verify = True + + #: SSL client certificate default, if String, path to ssl client + #: cert file (.pem). If Tuple, ('cert', 'key') pair. + self.cert = None + + #: Maximum number of redirects allowed. If the request exceeds this + #: limit, a :class:`TooManyRedirects` exception is raised. + #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is + #: 30. + self.max_redirects = DEFAULT_REDIRECT_LIMIT + + #: Trust environment settings for proxy configuration, default + #: authentication and similar. + self.trust_env = True + + #: A CookieJar containing all currently outstanding cookies set on this + #: session. By default it is a + #: :class:`RequestsCookieJar `, but + #: may be any other ``cookielib.CookieJar`` compatible object. + self.cookies = cookiejar_from_dict({}) + + # Default connection adapters. + self.adapters = OrderedDict() + self.mount("https://", HTTPAdapter()) + self.mount("http://", HTTPAdapter()) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def prepare_request(self, request): + """Constructs a :class:`PreparedRequest ` for + transmission and returns it. The :class:`PreparedRequest` has settings + merged from the :class:`Request ` instance and those of the + :class:`Session`. + + :param request: :class:`Request` instance to prepare with this + session's settings. + :rtype: requests.PreparedRequest + """ + cookies = request.cookies or {} + + # Bootstrap CookieJar. + if not isinstance(cookies, cookielib.CookieJar): + cookies = cookiejar_from_dict(cookies) + + # Merge with session cookies + merged_cookies = merge_cookies( + merge_cookies(RequestsCookieJar(), self.cookies), cookies + ) + + # Set environment's basic authentication if not explicitly set. + auth = request.auth + if self.trust_env and not auth and not self.auth: + auth = get_netrc_auth(request.url) + + p = PreparedRequest() + p.prepare( + method=request.method.upper(), + url=request.url, + files=request.files, + data=request.data, + json=request.json, + headers=merge_setting( + request.headers, self.headers, dict_class=CaseInsensitiveDict + ), + params=merge_setting(request.params, self.params), + auth=merge_setting(auth, self.auth), + cookies=merged_cookies, + hooks=merge_hooks(request.hooks, self.hooks), + ) + return p + + def request( + self, + method, + url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=None, + allow_redirects=True, + proxies=None, + hooks=None, + stream=None, + verify=None, + cert=None, + json=None, + ): + """Constructs a :class:`Request `, prepares it and sends it. + Returns :class:`Response ` object. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary or bytes to be sent in the query + string for the :class:`Request`. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) json to send in the body of the + :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the + :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the + :class:`Request`. + :param files: (optional) Dictionary of ``'filename': file-like-objects`` + for multipart encoding upload. + :param auth: (optional) Auth tuple or callable to enable + Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) How long to wait for the server to send + data before giving up, as a float, or a :ref:`(connect timeout, + read timeout) ` tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) Set to True by default. + :type allow_redirects: bool + :param proxies: (optional) Dictionary mapping protocol or protocol and + hostname to the URL of the proxy. + :param stream: (optional) whether to immediately download the response + content. Defaults to ``False``. + :param verify: (optional) Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use. Defaults to ``True``. When set to + ``False``, requests will accept any TLS certificate presented by + the server, and will ignore hostname mismatches and/or expired + certificates, which will make your application vulnerable to + man-in-the-middle (MitM) attacks. Setting verify to ``False`` + may be useful during local development or testing. + :param cert: (optional) if String, path to ssl client cert file (.pem). + If Tuple, ('cert', 'key') pair. + :rtype: requests.Response + """ + # Create the Request. + req = Request( + method=method.upper(), + url=url, + headers=headers, + files=files, + data=data or {}, + json=json, + params=params or {}, + auth=auth, + cookies=cookies, + hooks=hooks, + ) + prep = self.prepare_request(req) + + proxies = proxies or {} + + settings = self.merge_environment_settings( + prep.url, proxies, stream, verify, cert + ) + + # Send the request. + send_kwargs = { + "timeout": timeout, + "allow_redirects": allow_redirects, + } + send_kwargs.update(settings) + resp = self.send(prep, **send_kwargs) + + return resp + + def get(self, url, **kwargs): + r"""Sends a GET request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", True) + return self.request("GET", url, **kwargs) + + def options(self, url, **kwargs): + r"""Sends a OPTIONS request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", True) + return self.request("OPTIONS", url, **kwargs) + + def head(self, url, **kwargs): + r"""Sends a HEAD request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", False) + return self.request("HEAD", url, **kwargs) + + def post(self, url, data=None, json=None, **kwargs): + r"""Sends a POST request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param json: (optional) json to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + return self.request("POST", url, data=data, json=json, **kwargs) + + def put(self, url, data=None, **kwargs): + r"""Sends a PUT request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + return self.request("PUT", url, data=data, **kwargs) + + def patch(self, url, data=None, **kwargs): + r"""Sends a PATCH request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, list of tuples, bytes, or file-like + object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + return self.request("PATCH", url, data=data, **kwargs) + + def delete(self, url, **kwargs): + r"""Sends a DELETE request. Returns :class:`Response` object. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :rtype: requests.Response + """ + + return self.request("DELETE", url, **kwargs) + + def send(self, request, **kwargs): + """Send a given PreparedRequest. + + :rtype: requests.Response + """ + # Set defaults that the hooks can utilize to ensure they always have + # the correct parameters to reproduce the previous request. + kwargs.setdefault("stream", self.stream) + kwargs.setdefault("verify", self.verify) + kwargs.setdefault("cert", self.cert) + if "proxies" not in kwargs: + kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env) + + # It's possible that users might accidentally send a Request object. + # Guard against that specific failure case. + if isinstance(request, Request): + raise ValueError("You can only send PreparedRequests.") + + # Set up variables needed for resolve_redirects and dispatching of hooks + allow_redirects = kwargs.pop("allow_redirects", True) + stream = kwargs.get("stream") + hooks = request.hooks + + # Get the appropriate adapter to use + adapter = self.get_adapter(url=request.url) + + # Start time (approximately) of the request + start = preferred_clock() + + # Send the request + r = adapter.send(request, **kwargs) + + # Total elapsed time of the request (approximately) + elapsed = preferred_clock() - start + r.elapsed = timedelta(seconds=elapsed) + + # Response manipulation hooks + r = dispatch_hook("response", hooks, r, **kwargs) + + # Persist cookies + if r.history: + + # If the hooks create history then we want those cookies too + for resp in r.history: + extract_cookies_to_jar(self.cookies, resp.request, resp.raw) + + extract_cookies_to_jar(self.cookies, request, r.raw) + + # Resolve redirects if allowed. + if allow_redirects: + # Redirect resolving generator. + gen = self.resolve_redirects(r, request, **kwargs) + history = [resp for resp in gen] + else: + history = [] + + # Shuffle things around if there's history. + if history: + # Insert the first (original) request at the start + history.insert(0, r) + # Get the last request made + r = history.pop() + r.history = history + + # If redirects aren't being followed, store the response on the Request for Response.next(). + if not allow_redirects: + try: + r._next = next( + self.resolve_redirects(r, request, yield_requests=True, **kwargs) + ) + except StopIteration: + pass + + if not stream: + r.content + + return r + + def merge_environment_settings(self, url, proxies, stream, verify, cert): + """ + Check the environment and merge it with some settings. + + :rtype: dict + """ + # Gather clues from the surrounding environment. + if self.trust_env: + # Set environment's proxies. + no_proxy = proxies.get("no_proxy") if proxies is not None else None + env_proxies = get_environ_proxies(url, no_proxy=no_proxy) + for (k, v) in env_proxies.items(): + proxies.setdefault(k, v) + + # Look for requests environment configuration + # and be compatible with cURL. + if verify is True or verify is None: + verify = ( + os.environ.get("REQUESTS_CA_BUNDLE") + or os.environ.get("CURL_CA_BUNDLE") + or verify + ) + + # Merge all the kwargs. + proxies = merge_setting(proxies, self.proxies) + stream = merge_setting(stream, self.stream) + verify = merge_setting(verify, self.verify) + cert = merge_setting(cert, self.cert) + + return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert} + + def get_adapter(self, url): + """ + Returns the appropriate connection adapter for the given URL. + + :rtype: requests.adapters.BaseAdapter + """ + for (prefix, adapter) in self.adapters.items(): + + if url.lower().startswith(prefix.lower()): + return adapter + + # Nothing matches :-/ + raise InvalidSchema(f"No connection adapters were found for {url!r}") + + def close(self): + """Closes all adapters and as such the session""" + for v in self.adapters.values(): + v.close() + + def mount(self, prefix, adapter): + """Registers a connection adapter to a prefix. + + Adapters are sorted in descending order by prefix length. + """ + self.adapters[prefix] = adapter + keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] + + for key in keys_to_move: + self.adapters[key] = self.adapters.pop(key) + + def __getstate__(self): + state = {attr: getattr(self, attr, None) for attr in self.__attrs__} + return state + + def __setstate__(self, state): + for attr, value in state.items(): + setattr(self, attr, value) + + +def session(): + """ + Returns a :class:`Session` for context-management. + + .. deprecated:: 1.0.0 + + This method has been deprecated since version 1.0.0 and is only kept for + backwards compatibility. New code should use :class:`~requests.sessions.Session` + to create a session. This may be removed at a future date. + + :rtype: Session + """ + return Session() diff --git a/test/fixtures/whole_applications/requests/requests/status_codes.py b/test/fixtures/whole_applications/requests/requests/status_codes.py new file mode 100644 index 0000000..4bd072b --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/status_codes.py @@ -0,0 +1,128 @@ +r""" +The ``codes`` object defines a mapping from common names for HTTP statuses +to their numerical codes, accessible either as attributes or as dictionary +items. + +Example:: + + >>> import requests + >>> requests.codes['temporary_redirect'] + 307 + >>> requests.codes.teapot + 418 + >>> requests.codes['\o/'] + 200 + +Some codes have multiple names, and both upper- and lower-case versions of +the names are allowed. For example, ``codes.ok``, ``codes.OK``, and +``codes.okay`` all correspond to the HTTP status code 200. +""" + +from .structures import LookupDict + +_codes = { + # Informational. + 100: ("continue",), + 101: ("switching_protocols",), + 102: ("processing",), + 103: ("checkpoint",), + 122: ("uri_too_long", "request_uri_too_long"), + 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"), + 201: ("created",), + 202: ("accepted",), + 203: ("non_authoritative_info", "non_authoritative_information"), + 204: ("no_content",), + 205: ("reset_content", "reset"), + 206: ("partial_content", "partial"), + 207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"), + 208: ("already_reported",), + 226: ("im_used",), + # Redirection. + 300: ("multiple_choices",), + 301: ("moved_permanently", "moved", "\\o-"), + 302: ("found",), + 303: ("see_other", "other"), + 304: ("not_modified",), + 305: ("use_proxy",), + 306: ("switch_proxy",), + 307: ("temporary_redirect", "temporary_moved", "temporary"), + 308: ( + "permanent_redirect", + "resume_incomplete", + "resume", + ), # "resume" and "resume_incomplete" to be removed in 3.0 + # Client Error. + 400: ("bad_request", "bad"), + 401: ("unauthorized",), + 402: ("payment_required", "payment"), + 403: ("forbidden",), + 404: ("not_found", "-o-"), + 405: ("method_not_allowed", "not_allowed"), + 406: ("not_acceptable",), + 407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"), + 408: ("request_timeout", "timeout"), + 409: ("conflict",), + 410: ("gone",), + 411: ("length_required",), + 412: ("precondition_failed", "precondition"), + 413: ("request_entity_too_large",), + 414: ("request_uri_too_large",), + 415: ("unsupported_media_type", "unsupported_media", "media_type"), + 416: ( + "requested_range_not_satisfiable", + "requested_range", + "range_not_satisfiable", + ), + 417: ("expectation_failed",), + 418: ("im_a_teapot", "teapot", "i_am_a_teapot"), + 421: ("misdirected_request",), + 422: ("unprocessable_entity", "unprocessable"), + 423: ("locked",), + 424: ("failed_dependency", "dependency"), + 425: ("unordered_collection", "unordered"), + 426: ("upgrade_required", "upgrade"), + 428: ("precondition_required", "precondition"), + 429: ("too_many_requests", "too_many"), + 431: ("header_fields_too_large", "fields_too_large"), + 444: ("no_response", "none"), + 449: ("retry_with", "retry"), + 450: ("blocked_by_windows_parental_controls", "parental_controls"), + 451: ("unavailable_for_legal_reasons", "legal_reasons"), + 499: ("client_closed_request",), + # Server Error. + 500: ("internal_server_error", "server_error", "/o\\", "✗"), + 501: ("not_implemented",), + 502: ("bad_gateway",), + 503: ("service_unavailable", "unavailable"), + 504: ("gateway_timeout",), + 505: ("http_version_not_supported", "http_version"), + 506: ("variant_also_negotiates",), + 507: ("insufficient_storage",), + 509: ("bandwidth_limit_exceeded", "bandwidth"), + 510: ("not_extended",), + 511: ("network_authentication_required", "network_auth", "network_authentication"), +} + +codes = LookupDict(name="status_codes") + + +def _init(): + for code, titles in _codes.items(): + for title in titles: + setattr(codes, title, code) + if not title.startswith(("\\", "/")): + setattr(codes, title.upper(), code) + + def doc(code): + names = ", ".join(f"``{n}``" for n in _codes[code]) + return "* %d: %s" % (code, names) + + global __doc__ + __doc__ = ( + __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes)) + if __doc__ is not None + else None + ) + + +_init() diff --git a/test/fixtures/whole_applications/requests/requests/structures.py b/test/fixtures/whole_applications/requests/requests/structures.py new file mode 100644 index 0000000..188e13e --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/structures.py @@ -0,0 +1,99 @@ +""" +requests.structures +~~~~~~~~~~~~~~~~~~~ + +Data structures that power Requests. +""" + +from collections import OrderedDict + +from .compat import Mapping, MutableMapping + + +class CaseInsensitiveDict(MutableMapping): + """A case-insensitive ``dict``-like object. + + Implements all methods and operations of + ``MutableMapping`` as well as dict's ``copy``. Also + provides ``lower_items``. + + All keys are expected to be strings. The structure remembers the + case of the last key to be set, and ``iter(instance)``, + ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` + will contain case-sensitive keys. However, querying and contains + testing is case insensitive:: + + cid = CaseInsensitiveDict() + cid['Accept'] = 'application/json' + cid['aCCEPT'] == 'application/json' # True + list(cid) == ['Accept'] # True + + For example, ``headers['content-encoding']`` will return the + value of a ``'Content-Encoding'`` response header, regardless + of how the header name was originally stored. + + If the constructor, ``.update``, or equality comparison + operations are given keys that have equal ``.lower()``s, the + behavior is undefined. + """ + + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items()) + + def __eq__(self, other): + if isinstance(other, Mapping): + other = CaseInsensitiveDict(other) + else: + return NotImplemented + # Compare insensitively + return dict(self.lower_items()) == dict(other.lower_items()) + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) + + +class LookupDict(dict): + """Dictionary lookup object.""" + + def __init__(self, name=None): + self.name = name + super().__init__() + + def __repr__(self): + return f"" + + def __getitem__(self, key): + # We allow fall-through here, so values default to None + + return self.__dict__.get(key, None) + + def get(self, key, default=None): + return self.__dict__.get(key, default) diff --git a/test/fixtures/whole_applications/requests/requests/utils.py b/test/fixtures/whole_applications/requests/requests/utils.py new file mode 100644 index 0000000..a367417 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requests/utils.py @@ -0,0 +1,1094 @@ +""" +requests.utils +~~~~~~~~~~~~~~ + +This module provides utility functions that are used within Requests +that are also useful for external consumption. +""" + +import codecs +import contextlib +import io +import os +import re +import socket +import struct +import sys +import tempfile +import warnings +import zipfile +from collections import OrderedDict + +from urllib3.util import make_headers, parse_url + +from . import certs +from .__version__ import __version__ + +# to_native_string is unused here, but imported here for backwards compatibility +from ._internal_utils import ( # noqa: F401 + _HEADER_VALIDATORS_BYTE, + _HEADER_VALIDATORS_STR, + HEADER_VALIDATORS, + to_native_string, +) +from .compat import ( + Mapping, + basestring, + bytes, + getproxies, + getproxies_environment, + integer_types, +) +from .compat import parse_http_list as _parse_list_header +from .compat import ( + proxy_bypass, + proxy_bypass_environment, + quote, + str, + unquote, + urlparse, + urlunparse, +) +from .cookies import cookiejar_from_dict +from .exceptions import ( + FileModeWarning, + InvalidHeader, + InvalidURL, + UnrewindableBodyError, +) +from .structures import CaseInsensitiveDict + +NETRC_FILES = (".netrc", "_netrc") + +DEFAULT_CA_BUNDLE_PATH = certs.where() + +DEFAULT_PORTS = {"http": 80, "https": 443} + +# Ensure that ', ' is used to preserve previous delimiter behavior. +DEFAULT_ACCEPT_ENCODING = ", ".join( + re.split(r",\s*", make_headers(accept_encoding=True)["accept-encoding"]) +) + + +if sys.platform == "win32": + # provide a proxy_bypass version on Windows without DNS lookups + + def proxy_bypass_registry(host): + try: + import winreg + except ImportError: + return False + + try: + internetSettings = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", + ) + # ProxyEnable could be REG_SZ or REG_DWORD, normalizing it + proxyEnable = int(winreg.QueryValueEx(internetSettings, "ProxyEnable")[0]) + # ProxyOverride is almost always a string + proxyOverride = winreg.QueryValueEx(internetSettings, "ProxyOverride")[0] + except (OSError, ValueError): + return False + if not proxyEnable or not proxyOverride: + return False + + # make a check value list from the registry entry: replace the + # '' string by the localhost entry and the corresponding + # canonical entry. + proxyOverride = proxyOverride.split(";") + # now check if we match one of the registry values. + for test in proxyOverride: + if test == "": + if "." not in host: + return True + test = test.replace(".", r"\.") # mask dots + test = test.replace("*", r".*") # change glob sequence + test = test.replace("?", r".") # change glob char + if re.match(test, host, re.I): + return True + return False + + def proxy_bypass(host): # noqa + """Return True, if the host should be bypassed. + + Checks proxy settings gathered from the environment, if specified, + or the registry. + """ + if getproxies_environment(): + return proxy_bypass_environment(host) + else: + return proxy_bypass_registry(host) + + +def dict_to_sequence(d): + """Returns an internal sequence dictionary update.""" + + if hasattr(d, "items"): + d = d.items() + + return d + + +def super_len(o): + total_length = None + current_position = 0 + + if hasattr(o, "__len__"): + total_length = len(o) + + elif hasattr(o, "len"): + total_length = o.len + + elif hasattr(o, "fileno"): + try: + fileno = o.fileno() + except (io.UnsupportedOperation, AttributeError): + # AttributeError is a surprising exception, seeing as how we've just checked + # that `hasattr(o, 'fileno')`. It happens for objects obtained via + # `Tarfile.extractfile()`, per issue 5229. + pass + else: + total_length = os.fstat(fileno).st_size + + # Having used fstat to determine the file length, we need to + # confirm that this file was opened up in binary mode. + if "b" not in o.mode: + warnings.warn( + ( + "Requests has determined the content-length for this " + "request using the binary size of the file: however, the " + "file has been opened in text mode (i.e. without the 'b' " + "flag in the mode). This may lead to an incorrect " + "content-length. In Requests 3.0, support will be removed " + "for files in text mode." + ), + FileModeWarning, + ) + + if hasattr(o, "tell"): + try: + current_position = o.tell() + except OSError: + # This can happen in some weird situations, such as when the file + # is actually a special file descriptor like stdin. In this + # instance, we don't know what the length is, so set it to zero and + # let requests chunk it instead. + if total_length is not None: + current_position = total_length + else: + if hasattr(o, "seek") and total_length is None: + # StringIO and BytesIO have seek but no usable fileno + try: + # seek to end of file + o.seek(0, 2) + total_length = o.tell() + + # seek back to current position to support + # partially read file-like objects + o.seek(current_position or 0) + except OSError: + total_length = 0 + + if total_length is None: + total_length = 0 + + return max(0, total_length - current_position) + + +def get_netrc_auth(url, raise_errors=False): + """Returns the Requests tuple auth for a given url from netrc.""" + + netrc_file = os.environ.get("NETRC") + if netrc_file is not None: + netrc_locations = (netrc_file,) + else: + netrc_locations = (f"~/{f}" for f in NETRC_FILES) + + try: + from netrc import NetrcParseError, netrc + + netrc_path = None + + for f in netrc_locations: + try: + loc = os.path.expanduser(f) + except KeyError: + # os.path.expanduser can fail when $HOME is undefined and + # getpwuid fails. See https://bugs.python.org/issue20164 & + # https://github.com/psf/requests/issues/1846 + return + + if os.path.exists(loc): + netrc_path = loc + break + + # Abort early if there isn't one. + if netrc_path is None: + return + + ri = urlparse(url) + + # Strip port numbers from netloc. This weird `if...encode`` dance is + # used for Python 3.2, which doesn't support unicode literals. + splitstr = b":" + if isinstance(url, str): + splitstr = splitstr.decode("ascii") + host = ri.netloc.split(splitstr)[0] + + try: + _netrc = netrc(netrc_path).authenticators(host) + if _netrc: + # Return with login / password + login_i = 0 if _netrc[0] else 1 + return (_netrc[login_i], _netrc[2]) + except (NetrcParseError, OSError): + # If there was a parsing error or a permissions issue reading the file, + # we'll just skip netrc auth unless explicitly asked to raise errors. + if raise_errors: + raise + + # App Engine hackiness. + except (ImportError, AttributeError): + pass + + +def guess_filename(obj): + """Tries to guess the filename of the given object.""" + name = getattr(obj, "name", None) + if name and isinstance(name, basestring) and name[0] != "<" and name[-1] != ">": + return os.path.basename(name) + + +def extract_zipped_paths(path): + """Replace nonexistent paths that look like they refer to a member of a zip + archive with the location of an extracted copy of the target, or else + just return the provided path unchanged. + """ + if os.path.exists(path): + # this is already a valid path, no need to do anything further + return path + + # find the first valid part of the provided path and treat that as a zip archive + # assume the rest of the path is the name of a member in the archive + archive, member = os.path.split(path) + while archive and not os.path.exists(archive): + archive, prefix = os.path.split(archive) + if not prefix: + # If we don't check for an empty prefix after the split (in other words, archive remains unchanged after the split), + # we _can_ end up in an infinite loop on a rare corner case affecting a small number of users + break + member = "/".join([prefix, member]) + + if not zipfile.is_zipfile(archive): + return path + + zip_file = zipfile.ZipFile(archive) + if member not in zip_file.namelist(): + return path + + # we have a valid zip archive and a valid member of that archive + tmp = tempfile.gettempdir() + extracted_path = os.path.join(tmp, member.split("/")[-1]) + if not os.path.exists(extracted_path): + # use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition + with atomic_open(extracted_path) as file_handler: + file_handler.write(zip_file.read(member)) + return extracted_path + + +@contextlib.contextmanager +def atomic_open(filename): + """Write a file to the disk in an atomic fashion""" + tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename)) + try: + with os.fdopen(tmp_descriptor, "wb") as tmp_handler: + yield tmp_handler + os.replace(tmp_name, filename) + except BaseException: + os.remove(tmp_name) + raise + + +def from_key_val_list(value): + """Take an object and test to see if it can be represented as a + dictionary. Unless it can not be represented as such, return an + OrderedDict, e.g., + + :: + + >>> from_key_val_list([('key', 'val')]) + OrderedDict([('key', 'val')]) + >>> from_key_val_list('string') + Traceback (most recent call last): + ... + ValueError: cannot encode objects that are not 2-tuples + >>> from_key_val_list({'key': 'val'}) + OrderedDict([('key', 'val')]) + + :rtype: OrderedDict + """ + if value is None: + return None + + if isinstance(value, (str, bytes, bool, int)): + raise ValueError("cannot encode objects that are not 2-tuples") + + return OrderedDict(value) + + +def to_key_val_list(value): + """Take an object and test to see if it can be represented as a + dictionary. If it can be, return a list of tuples, e.g., + + :: + + >>> to_key_val_list([('key', 'val')]) + [('key', 'val')] + >>> to_key_val_list({'key': 'val'}) + [('key', 'val')] + >>> to_key_val_list('string') + Traceback (most recent call last): + ... + ValueError: cannot encode objects that are not 2-tuples + + :rtype: list + """ + if value is None: + return None + + if isinstance(value, (str, bytes, bool, int)): + raise ValueError("cannot encode objects that are not 2-tuples") + + if isinstance(value, Mapping): + value = value.items() + + return list(value) + + +# From mitsuhiko/werkzeug (used with permission). +def parse_list_header(value): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + It basically works like :func:`parse_set_header` just that items + may appear multiple times and case sensitivity is preserved. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + To create a header from the :class:`list` again, use the + :func:`dump_header` function. + + :param value: a string with a list header. + :return: :class:`list` + :rtype: list + """ + result = [] + for item in _parse_list_header(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +# From mitsuhiko/werkzeug (used with permission). +def parse_dict_header(value): + """Parse lists of key, value pairs as described by RFC 2068 Section 2 and + convert them into a python dict: + + >>> d = parse_dict_header('foo="is a fish", bar="as well"') + >>> type(d) is dict + True + >>> sorted(d.items()) + [('bar', 'as well'), ('foo', 'is a fish')] + + If there is no value for a key it will be `None`: + + >>> parse_dict_header('key_without_value') + {'key_without_value': None} + + To create a header from the :class:`dict` again, use the + :func:`dump_header` function. + + :param value: a string with a dict header. + :return: :class:`dict` + :rtype: dict + """ + result = {} + for item in _parse_list_header(value): + if "=" not in item: + result[item] = None + continue + name, value = item.split("=", 1) + if value[:1] == value[-1:] == '"': + value = unquote_header_value(value[1:-1]) + result[name] = value + return result + + +# From mitsuhiko/werkzeug (used with permission). +def unquote_header_value(value, is_filename=False): + r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). + This does not use the real unquoting but what browsers are actually + using for quoting. + + :param value: the header value to unquote. + :rtype: str + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + + # if this is a filename and the starting characters look like + # a UNC path, then just return the value without quotes. Using the + # replace sequence below on a UNC path has the effect of turning + # the leading double slash into a single slash and then + # _fix_ie_filename() doesn't work correctly. See #458. + if not is_filename or value[:2] != "\\\\": + return value.replace("\\\\", "\\").replace('\\"', '"') + return value + + +def dict_from_cookiejar(cj): + """Returns a key/value dictionary from a CookieJar. + + :param cj: CookieJar object to extract cookies from. + :rtype: dict + """ + + cookie_dict = {} + + for cookie in cj: + cookie_dict[cookie.name] = cookie.value + + return cookie_dict + + +def add_dict_to_cookiejar(cj, cookie_dict): + """Returns a CookieJar from a key/value dictionary. + + :param cj: CookieJar to insert cookies into. + :param cookie_dict: Dict of key/values to insert into CookieJar. + :rtype: CookieJar + """ + + return cookiejar_from_dict(cookie_dict, cj) + + +def get_encodings_from_content(content): + """Returns encodings from given content string. + + :param content: bytestring to extract encodings from. + """ + warnings.warn( + ( + "In requests 3.0, get_encodings_from_content will be removed. For " + "more information, please see the discussion on issue #2266. (This" + " warning should only appear once.)" + ), + DeprecationWarning, + ) + + charset_re = re.compile(r']', flags=re.I) + pragma_re = re.compile(r']', flags=re.I) + xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]') + + return ( + charset_re.findall(content) + + pragma_re.findall(content) + + xml_re.findall(content) + ) + + +def _parse_content_type_header(header): + """Returns content type and parameters from given header + + :param header: string + :return: tuple containing content type and dictionary of + parameters + """ + + tokens = header.split(";") + content_type, params = tokens[0].strip(), tokens[1:] + params_dict = {} + items_to_strip = "\"' " + + for param in params: + param = param.strip() + if param: + key, value = param, True + index_of_equals = param.find("=") + if index_of_equals != -1: + key = param[:index_of_equals].strip(items_to_strip) + value = param[index_of_equals + 1 :].strip(items_to_strip) + params_dict[key.lower()] = value + return content_type, params_dict + + +def get_encoding_from_headers(headers): + """Returns encodings from given HTTP Header Dict. + + :param headers: dictionary to extract encoding from. + :rtype: str + """ + + content_type = headers.get("content-type") + + if not content_type: + return None + + content_type, params = _parse_content_type_header(content_type) + + if "charset" in params: + return params["charset"].strip("'\"") + + if "text" in content_type: + return "ISO-8859-1" + + if "application/json" in content_type: + # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset + return "utf-8" + + +def stream_decode_response_unicode(iterator, r): + """Stream decodes an iterator.""" + + if r.encoding is None: + yield from iterator + return + + decoder = codecs.getincrementaldecoder(r.encoding)(errors="replace") + for chunk in iterator: + rv = decoder.decode(chunk) + if rv: + yield rv + rv = decoder.decode(b"", final=True) + if rv: + yield rv + + +def iter_slices(string, slice_length): + """Iterate over slices of a string.""" + pos = 0 + if slice_length is None or slice_length <= 0: + slice_length = len(string) + while pos < len(string): + yield string[pos : pos + slice_length] + pos += slice_length + + +def get_unicode_from_response(r): + """Returns the requested content back in unicode. + + :param r: Response object to get unicode content from. + + Tried: + + 1. charset from content-type + 2. fall back and replace all unicode characters + + :rtype: str + """ + warnings.warn( + ( + "In requests 3.0, get_unicode_from_response will be removed. For " + "more information, please see the discussion on issue #2266. (This" + " warning should only appear once.)" + ), + DeprecationWarning, + ) + + tried_encodings = [] + + # Try charset from content-type + encoding = get_encoding_from_headers(r.headers) + + if encoding: + try: + return str(r.content, encoding) + except UnicodeError: + tried_encodings.append(encoding) + + # Fall back: + try: + return str(r.content, encoding, errors="replace") + except TypeError: + return r.content + + +# The unreserved URI characters (RFC 3986) +UNRESERVED_SET = frozenset( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~" +) + + +def unquote_unreserved(uri): + """Un-escape any percent-escape sequences in a URI that are unreserved + characters. This leaves all reserved, illegal and non-ASCII bytes encoded. + + :rtype: str + """ + parts = uri.split("%") + for i in range(1, len(parts)): + h = parts[i][0:2] + if len(h) == 2 and h.isalnum(): + try: + c = chr(int(h, 16)) + except ValueError: + raise InvalidURL(f"Invalid percent-escape sequence: '{h}'") + + if c in UNRESERVED_SET: + parts[i] = c + parts[i][2:] + else: + parts[i] = f"%{parts[i]}" + else: + parts[i] = f"%{parts[i]}" + return "".join(parts) + + +def requote_uri(uri): + """Re-quote the given URI. + + This function passes the given URI through an unquote/quote cycle to + ensure that it is fully and consistently quoted. + + :rtype: str + """ + safe_with_percent = "!#$%&'()*+,/:;=?@[]~" + safe_without_percent = "!#$&'()*+,/:;=?@[]~" + try: + # Unquote only the unreserved characters + # Then quote only illegal characters (do not quote reserved, + # unreserved, or '%') + return quote(unquote_unreserved(uri), safe=safe_with_percent) + except InvalidURL: + # We couldn't unquote the given URI, so let's try quoting it, but + # there may be unquoted '%'s in the URI. We need to make sure they're + # properly quoted so they do not cause issues elsewhere. + return quote(uri, safe=safe_without_percent) + + +def address_in_network(ip, net): + """This function allows you to check if an IP belongs to a network subnet + + Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 + returns False if ip = 192.168.1.1 and net = 192.168.100.0/24 + + :rtype: bool + """ + ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0] + netaddr, bits = net.split("/") + netmask = struct.unpack("=L", socket.inet_aton(dotted_netmask(int(bits))))[0] + network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask + return (ipaddr & netmask) == (network & netmask) + + +def dotted_netmask(mask): + """Converts mask from /xx format to xxx.xxx.xxx.xxx + + Example: if mask is 24 function returns 255.255.255.0 + + :rtype: str + """ + bits = 0xFFFFFFFF ^ (1 << 32 - mask) - 1 + return socket.inet_ntoa(struct.pack(">I", bits)) + + +def is_ipv4_address(string_ip): + """ + :rtype: bool + """ + try: + socket.inet_aton(string_ip) + except OSError: + return False + return True + + +def is_valid_cidr(string_network): + """ + Very simple check of the cidr format in no_proxy variable. + + :rtype: bool + """ + if string_network.count("/") == 1: + try: + mask = int(string_network.split("/")[1]) + except ValueError: + return False + + if mask < 1 or mask > 32: + return False + + try: + socket.inet_aton(string_network.split("/")[0]) + except OSError: + return False + else: + return False + return True + + +@contextlib.contextmanager +def set_environ(env_name, value): + """Set the environment variable 'env_name' to 'value' + + Save previous value, yield, and then restore the previous value stored in + the environment variable 'env_name'. + + If 'value' is None, do nothing""" + value_changed = value is not None + if value_changed: + old_value = os.environ.get(env_name) + os.environ[env_name] = value + try: + yield + finally: + if value_changed: + if old_value is None: + del os.environ[env_name] + else: + os.environ[env_name] = old_value + + +def should_bypass_proxies(url, no_proxy): + """ + Returns whether we should bypass proxies or not. + + :rtype: bool + """ + # Prioritize lowercase environment variables over uppercase + # to keep a consistent behaviour with other http projects (curl, wget). + def get_proxy(key): + return os.environ.get(key) or os.environ.get(key.upper()) + + # First check whether no_proxy is defined. If it is, check that the URL + # we're getting isn't in the no_proxy list. + no_proxy_arg = no_proxy + if no_proxy is None: + no_proxy = get_proxy("no_proxy") + parsed = urlparse(url) + + if parsed.hostname is None: + # URLs don't always have hostnames, e.g. file:/// urls. + return True + + if no_proxy: + # We need to check whether we match here. We need to see if we match + # the end of the hostname, both with and without the port. + no_proxy = (host for host in no_proxy.replace(" ", "").split(",") if host) + + if is_ipv4_address(parsed.hostname): + for proxy_ip in no_proxy: + if is_valid_cidr(proxy_ip): + if address_in_network(parsed.hostname, proxy_ip): + return True + elif parsed.hostname == proxy_ip: + # If no_proxy ip was defined in plain IP notation instead of cidr notation & + # matches the IP of the index + return True + else: + host_with_port = parsed.hostname + if parsed.port: + host_with_port += f":{parsed.port}" + + for host in no_proxy: + if parsed.hostname.endswith(host) or host_with_port.endswith(host): + # The URL does match something in no_proxy, so we don't want + # to apply the proxies on this URL. + return True + + with set_environ("no_proxy", no_proxy_arg): + # parsed.hostname can be `None` in cases such as a file URI. + try: + bypass = proxy_bypass(parsed.hostname) + except (TypeError, socket.gaierror): + bypass = False + + if bypass: + return True + + return False + + +def get_environ_proxies(url, no_proxy=None): + """ + Return a dict of environment proxies. + + :rtype: dict + """ + if should_bypass_proxies(url, no_proxy=no_proxy): + return {} + else: + return getproxies() + + +def select_proxy(url, proxies): + """Select a proxy for the url, if applicable. + + :param url: The url being for the request + :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs + """ + proxies = proxies or {} + urlparts = urlparse(url) + if urlparts.hostname is None: + return proxies.get(urlparts.scheme, proxies.get("all")) + + proxy_keys = [ + urlparts.scheme + "://" + urlparts.hostname, + urlparts.scheme, + "all://" + urlparts.hostname, + "all", + ] + proxy = None + for proxy_key in proxy_keys: + if proxy_key in proxies: + proxy = proxies[proxy_key] + break + + return proxy + + +def resolve_proxies(request, proxies, trust_env=True): + """This method takes proxy information from a request and configuration + input to resolve a mapping of target proxies. This will consider settings + such a NO_PROXY to strip proxy configurations. + + :param request: Request or PreparedRequest + :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs + :param trust_env: Boolean declaring whether to trust environment configs + + :rtype: dict + """ + proxies = proxies if proxies is not None else {} + url = request.url + scheme = urlparse(url).scheme + no_proxy = proxies.get("no_proxy") + new_proxies = proxies.copy() + + if trust_env and not should_bypass_proxies(url, no_proxy=no_proxy): + environ_proxies = get_environ_proxies(url, no_proxy=no_proxy) + + proxy = environ_proxies.get(scheme, environ_proxies.get("all")) + + if proxy: + new_proxies.setdefault(scheme, proxy) + return new_proxies + + +def default_user_agent(name="python-requests"): + """ + Return a string representing the default user agent. + + :rtype: str + """ + return f"{name}/{__version__}" + + +def default_headers(): + """ + :rtype: requests.structures.CaseInsensitiveDict + """ + return CaseInsensitiveDict( + { + "User-Agent": default_user_agent(), + "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, + "Accept": "*/*", + "Connection": "keep-alive", + } + ) + + +def parse_header_links(value): + """Return a list of parsed link headers proxies. + + i.e. Link: ; rel=front; type="image/jpeg",; rel=back;type="image/jpeg" + + :rtype: list + """ + + links = [] + + replace_chars = " '\"" + + value = value.strip(replace_chars) + if not value: + return links + + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, "" + + link = {"url": url.strip("<> '\"")} + + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + + link[key.strip(replace_chars)] = value.strip(replace_chars) + + links.append(link) + + return links + + +# Null bytes; no need to recreate these on each call to guess_json_utf +_null = "\x00".encode("ascii") # encoding to ASCII for Python 3 +_null2 = _null * 2 +_null3 = _null * 3 + + +def guess_json_utf(data): + """ + :rtype: str + """ + # JSON always starts with two ASCII characters, so detection is as + # easy as counting the nulls and from their location and count + # determine the encoding. Also detect a BOM, if present. + sample = data[:4] + if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE): + return "utf-32" # BOM included + if sample[:3] == codecs.BOM_UTF8: + return "utf-8-sig" # BOM included, MS style (discouraged) + if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE): + return "utf-16" # BOM included + nullcount = sample.count(_null) + if nullcount == 0: + return "utf-8" + if nullcount == 2: + if sample[::2] == _null2: # 1st and 3rd are null + return "utf-16-be" + if sample[1::2] == _null2: # 2nd and 4th are null + return "utf-16-le" + # Did not detect 2 valid UTF-16 ascii-range characters + if nullcount == 3: + if sample[:3] == _null3: + return "utf-32-be" + if sample[1:] == _null3: + return "utf-32-le" + # Did not detect a valid UTF-32 ascii-range character + return None + + +def prepend_scheme_if_needed(url, new_scheme): + """Given a URL that may or may not have a scheme, prepend the given scheme. + Does not replace a present scheme with the one provided as an argument. + + :rtype: str + """ + parsed = parse_url(url) + scheme, auth, host, port, path, query, fragment = parsed + + # A defect in urlparse determines that there isn't a netloc present in some + # urls. We previously assumed parsing was overly cautious, and swapped the + # netloc and path. Due to a lack of tests on the original defect, this is + # maintained with parse_url for backwards compatibility. + netloc = parsed.netloc + if not netloc: + netloc, path = path, netloc + + if auth: + # parse_url doesn't provide the netloc with auth + # so we'll add it ourselves. + netloc = "@".join([auth, netloc]) + if scheme is None: + scheme = new_scheme + if path is None: + path = "" + + return urlunparse((scheme, netloc, path, "", query, fragment)) + + +def get_auth_from_url(url): + """Given a url with authentication components, extract them into a tuple of + username,password. + + :rtype: (str,str) + """ + parsed = urlparse(url) + + try: + auth = (unquote(parsed.username), unquote(parsed.password)) + except (AttributeError, TypeError): + auth = ("", "") + + return auth + + +def check_header_validity(header): + """Verifies that header parts don't contain leading whitespace + reserved characters, or return characters. + + :param header: tuple, in the format (name, value). + """ + name, value = header + _validate_header_part(header, name, 0) + _validate_header_part(header, value, 1) + + +def _validate_header_part(header, header_part, header_validator_index): + if isinstance(header_part, str): + validator = _HEADER_VALIDATORS_STR[header_validator_index] + elif isinstance(header_part, bytes): + validator = _HEADER_VALIDATORS_BYTE[header_validator_index] + else: + raise InvalidHeader( + f"Header part ({header_part!r}) from {header} " + f"must be of type str or bytes, not {type(header_part)}" + ) + + if not validator.match(header_part): + header_kind = "name" if header_validator_index == 0 else "value" + raise InvalidHeader( + f"Invalid leading whitespace, reserved character(s), or return" + f"character(s) in header {header_kind}: {header_part!r}" + ) + + +def urldefragauth(url): + """ + Given a url remove the fragment and the authentication part. + + :rtype: str + """ + scheme, netloc, path, params, query, fragment = urlparse(url) + + # see func:`prepend_scheme_if_needed` + if not netloc: + netloc, path = path, netloc + + netloc = netloc.rsplit("@", 1)[-1] + + return urlunparse((scheme, netloc, path, params, query, "")) + + +def rewind_body(prepared_request): + """Move file pointer back to its recorded starting position + so it can be read again on redirect. + """ + body_seek = getattr(prepared_request.body, "seek", None) + if body_seek is not None and isinstance( + prepared_request._body_position, integer_types + ): + try: + body_seek(prepared_request._body_position) + except OSError: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect." + ) + else: + raise UnrewindableBodyError("Unable to rewind request body for redirect.") diff --git a/test/fixtures/whole_applications/requests/requirements-dev.txt b/test/fixtures/whole_applications/requests/requirements-dev.txt new file mode 100644 index 0000000..d626373 --- /dev/null +++ b/test/fixtures/whole_applications/requests/requirements-dev.txt @@ -0,0 +1,13 @@ +-e .[socks] +pytest>=2.8.0,<=6.2.5 +pytest-cov +pytest-httpbin==2.0.0 +pytest-mock==2.0.0 +httpbin==0.7.0 +trustme +wheel +cryptography<40.0.0; python_version <= '3.7' and platform_python_implementation == 'PyPy' + +# Flask Stack +Flask>1.0,<2.0 +markupsafe<2.1 diff --git a/test/fixtures/whole_applications/requests/setup.cfg b/test/fixtures/whole_applications/requests/setup.cfg new file mode 100644 index 0000000..bf21c81 --- /dev/null +++ b/test/fixtures/whole_applications/requests/setup.cfg @@ -0,0 +1,17 @@ +[metadata] +license_file = LICENSE +provides-extra = + socks + use_chardet_on_py3 +requires-dist = + certifi>=2017.4.17 + charset_normalizer>=2,<4 + idna>=2.5,<4 + urllib3>=1.21.1,<1.27 + +[flake8] +ignore = E203, E501, W503 +per-file-ignores = + requests/__init__.py:E402, F401 + requests/compat.py:E402, F401 + tests/compat.py:F401 diff --git a/test/fixtures/whole_applications/requests/setup.py b/test/fixtures/whole_applications/requests/setup.py new file mode 100755 index 0000000..0123545 --- /dev/null +++ b/test/fixtures/whole_applications/requests/setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +import os +import sys +from codecs import open + +from setuptools import setup +from setuptools.command.test import test as TestCommand + +CURRENT_PYTHON = sys.version_info[:2] +REQUIRED_PYTHON = (3, 7) + +if CURRENT_PYTHON < REQUIRED_PYTHON: + sys.stderr.write( + """ +========================== +Unsupported Python version +========================== +This version of Requests requires at least Python {}.{}, but +you're trying to install it on Python {}.{}. To resolve this, +consider upgrading to a supported Python version. + +If you can't upgrade your Python version, you'll need to +pin to an older version of Requests (<2.28). +""".format( + *(REQUIRED_PYTHON + CURRENT_PYTHON) + ) + ) + sys.exit(1) + + +class PyTest(TestCommand): + user_options = [("pytest-args=", "a", "Arguments to pass into py.test")] + + def initialize_options(self): + TestCommand.initialize_options(self) + try: + from multiprocessing import cpu_count + + self.pytest_args = ["-n", str(cpu_count()), "--boxed"] + except (ImportError, NotImplementedError): + self.pytest_args = ["-n", "1", "--boxed"] + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + import pytest + + errno = pytest.main(self.pytest_args) + sys.exit(errno) + + +# 'setup.py publish' shortcut. +if sys.argv[-1] == "publish": + os.system("python setup.py sdist bdist_wheel") + os.system("twine upload dist/*") + sys.exit() + +requires = [ + "charset_normalizer>=2,<4", + "idna>=2.5,<4", + "urllib3>=1.21.1,<3", + "certifi>=2017.4.17", +] +test_requirements = [ + "pytest-httpbin==2.0.0", + "pytest-cov", + "pytest-mock", + "pytest-xdist", + "PySocks>=1.5.6, !=1.5.7", + "pytest>=3", +] + +about = {} +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, "requests", "__version__.py"), "r", "utf-8") as f: + exec(f.read(), about) + +with open("README.md", "r", "utf-8") as f: + readme = f.read() + +setup( + name=about["__title__"], + version=about["__version__"], + description=about["__description__"], + long_description=readme, + long_description_content_type="text/markdown", + author=about["__author__"], + author_email=about["__author_email__"], + url=about["__url__"], + packages=["requests"], + package_data={"": ["LICENSE", "NOTICE"]}, + package_dir={"requests": "requests"}, + include_package_data=True, + python_requires=">=3.7", + install_requires=requires, + license=about["__license__"], + zip_safe=False, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries", + ], + cmdclass={"test": PyTest}, + tests_require=test_requirements, + extras_require={ + "security": [], + "socks": ["PySocks>=1.5.6, !=1.5.7"], + "use_chardet_on_py3": ["chardet>=3.0.2,<6"], + }, + project_urls={ + "Documentation": "https://requests.readthedocs.io", + "Source": "https://github.com/psf/requests", + }, +) diff --git a/test/fixtures/whole_applications/requests/tests/__init__.py b/test/fixtures/whole_applications/requests/tests/__init__.py new file mode 100644 index 0000000..c8561a0 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/__init__.py @@ -0,0 +1,14 @@ +"""Requests test package initialisation.""" + +import warnings + +try: + from urllib3.exceptions import SNIMissingWarning + + # urllib3 1.x sets SNIMissingWarning to only go off once, + # while this test suite requires it to always fire + # so that it occurs during test_requests.test_https_warnings + warnings.simplefilter("always", SNIMissingWarning) +except ImportError: + # urllib3 2.0 removed that warning and errors out instead + SNIMissingWarning = None diff --git a/test/fixtures/whole_applications/requests/tests/compat.py b/test/fixtures/whole_applications/requests/tests/compat.py new file mode 100644 index 0000000..7618aa1 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/compat.py @@ -0,0 +1,23 @@ +import warnings + +try: + import StringIO +except ImportError: + import io as StringIO + +try: + from cStringIO import StringIO as cStringIO +except ImportError: + cStringIO = None + + +def u(s): + warnings.warn( + ( + "This helper function is no longer relevant in Python 3. " + "Usage of this alias should be discontinued as it will be " + "removed in a future release of Requests." + ), + DeprecationWarning, + ) + return s diff --git a/test/fixtures/whole_applications/requests/tests/conftest.py b/test/fixtures/whole_applications/requests/tests/conftest.py new file mode 100644 index 0000000..530a4c2 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/conftest.py @@ -0,0 +1,58 @@ +try: + from http.server import HTTPServer, SimpleHTTPRequestHandler +except ImportError: + from BaseHTTPServer import HTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler + +import ssl +import threading + +import pytest + +from requests.compat import urljoin + + +def prepare_url(value): + # Issue #1483: Make sure the URL always has a trailing slash + httpbin_url = value.url.rstrip("/") + "/" + + def inner(*suffix): + return urljoin(httpbin_url, "/".join(suffix)) + + return inner + + +@pytest.fixture +def httpbin(httpbin): + return prepare_url(httpbin) + + +@pytest.fixture +def httpbin_secure(httpbin_secure): + return prepare_url(httpbin_secure) + + +@pytest.fixture +def nosan_server(tmp_path_factory): + # delay importing until the fixture in order to make it possible + # to deselect the test via command-line when trustme is not available + import trustme + + tmpdir = tmp_path_factory.mktemp("certs") + ca = trustme.CA() + # only commonName, no subjectAltName + server_cert = ca.issue_cert(common_name="localhost") + ca_bundle = str(tmpdir / "ca.pem") + ca.cert_pem.write_to_path(ca_bundle) + + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + server_cert.configure_cert(context) + server = HTTPServer(("localhost", 0), SimpleHTTPRequestHandler) + server.socket = context.wrap_socket(server.socket, server_side=True) + server_thread = threading.Thread(target=server.serve_forever) + server_thread.start() + + yield "localhost", server.server_address[1], ca_bundle + + server.shutdown() + server_thread.join() diff --git a/test/fixtures/whole_applications/requests/tests/test_help.py b/test/fixtures/whole_applications/requests/tests/test_help.py new file mode 100644 index 0000000..fb4e967 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_help.py @@ -0,0 +1,25 @@ +from requests.help import info + + +def test_system_ssl(): + """Verify we're actually setting system_ssl when it should be available.""" + assert info()["system_ssl"]["version"] != "" + + +class VersionedPackage: + def __init__(self, version): + self.__version__ = version + + +def test_idna_without_version_attribute(mocker): + """Older versions of IDNA don't provide a __version__ attribute, verify + that if we have such a package, we don't blow up. + """ + mocker.patch("requests.help.idna", new=None) + assert info()["idna"] == {"version": ""} + + +def test_idna_with_version_attribute(mocker): + """Verify we're actually setting idna version when it should be available.""" + mocker.patch("requests.help.idna", new=VersionedPackage("2.6")) + assert info()["idna"] == {"version": "2.6"} diff --git a/test/fixtures/whole_applications/requests/tests/test_hooks.py b/test/fixtures/whole_applications/requests/tests/test_hooks.py new file mode 100644 index 0000000..7445525 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_hooks.py @@ -0,0 +1,22 @@ +import pytest + +from requests import hooks + + +def hook(value): + return value[1:] + + +@pytest.mark.parametrize( + "hooks_list, result", + ( + (hook, "ata"), + ([hook, lambda x: None, hook], "ta"), + ), +) +def test_hooks(hooks_list, result): + assert hooks.dispatch_hook("response", {"response": hooks_list}, "Data") == result + + +def test_default_hooks(): + assert hooks.default_hooks() == {"response": []} diff --git a/test/fixtures/whole_applications/requests/tests/test_lowlevel.py b/test/fixtures/whole_applications/requests/tests/test_lowlevel.py new file mode 100644 index 0000000..859d07e --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_lowlevel.py @@ -0,0 +1,428 @@ +import threading + +import pytest +from tests.testserver.server import Server, consume_socket_content + +import requests +from requests.compat import JSONDecodeError + +from .utils import override_environ + + +def echo_response_handler(sock): + """Simple handler that will take request and echo it back to requester.""" + request_content = consume_socket_content(sock, timeout=0.5) + + text_200 = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: %d\r\n\r\n" + b"%s" + ) % (len(request_content), request_content) + sock.send(text_200) + + +def test_chunked_upload(): + """can safely send generators""" + close_server = threading.Event() + server = Server.basic_response_server(wait_to_close_event=close_server) + data = iter([b"a", b"b", b"c"]) + + with server as (host, port): + url = f"http://{host}:{port}/" + r = requests.post(url, data=data, stream=True) + close_server.set() # release server block + + assert r.status_code == 200 + assert r.request.headers["Transfer-Encoding"] == "chunked" + + +def test_chunked_encoding_error(): + """get a ChunkedEncodingError if the server returns a bad response""" + + def incomplete_chunked_response_handler(sock): + request_content = consume_socket_content(sock, timeout=0.5) + + # The server never ends the request and doesn't provide any valid chunks + sock.send( + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n" + ) + + return request_content + + close_server = threading.Event() + server = Server(incomplete_chunked_response_handler) + + with server as (host, port): + url = f"http://{host}:{port}/" + with pytest.raises(requests.exceptions.ChunkedEncodingError): + requests.get(url) + close_server.set() # release server block + + +def test_chunked_upload_uses_only_specified_host_header(): + """Ensure we use only the specified Host header for chunked requests.""" + close_server = threading.Event() + server = Server(echo_response_handler, wait_to_close_event=close_server) + + data = iter([b"a", b"b", b"c"]) + custom_host = "sample-host" + + with server as (host, port): + url = f"http://{host}:{port}/" + r = requests.post(url, data=data, headers={"Host": custom_host}, stream=True) + close_server.set() # release server block + + expected_header = b"Host: %s\r\n" % custom_host.encode("utf-8") + assert expected_header in r.content + assert r.content.count(b"Host: ") == 1 + + +def test_chunked_upload_doesnt_skip_host_header(): + """Ensure we don't omit all Host headers with chunked requests.""" + close_server = threading.Event() + server = Server(echo_response_handler, wait_to_close_event=close_server) + + data = iter([b"a", b"b", b"c"]) + + with server as (host, port): + expected_host = f"{host}:{port}" + url = f"http://{host}:{port}/" + r = requests.post(url, data=data, stream=True) + close_server.set() # release server block + + expected_header = b"Host: %s\r\n" % expected_host.encode("utf-8") + assert expected_header in r.content + assert r.content.count(b"Host: ") == 1 + + +def test_conflicting_content_lengths(): + """Ensure we correctly throw an InvalidHeader error if multiple + conflicting Content-Length headers are returned. + """ + + def multiple_content_length_response_handler(sock): + request_content = consume_socket_content(sock, timeout=0.5) + response = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: text/plain\r\n" + b"Content-Length: 16\r\n" + b"Content-Length: 32\r\n\r\n" + b"-- Bad Actor -- Original Content\r\n" + ) + sock.send(response) + + return request_content + + close_server = threading.Event() + server = Server(multiple_content_length_response_handler) + + with server as (host, port): + url = f"http://{host}:{port}/" + with pytest.raises(requests.exceptions.InvalidHeader): + requests.get(url) + close_server.set() + + +def test_digestauth_401_count_reset_on_redirect(): + """Ensure we correctly reset num_401_calls after a successful digest auth, + followed by a 302 redirect to another digest auth prompt. + + See https://github.com/psf/requests/issues/1979. + """ + text_401 = (b'HTTP/1.1 401 UNAUTHORIZED\r\n' + b'Content-Length: 0\r\n' + b'WWW-Authenticate: Digest nonce="6bf5d6e4da1ce66918800195d6b9130d"' + b', opaque="372825293d1c26955496c80ed6426e9e", ' + b'realm="me@kennethreitz.com", qop=auth\r\n\r\n') + + text_302 = (b'HTTP/1.1 302 FOUND\r\n' + b'Content-Length: 0\r\n' + b'Location: /\r\n\r\n') + + text_200 = (b'HTTP/1.1 200 OK\r\n' + b'Content-Length: 0\r\n\r\n') + + expected_digest = (b'Authorization: Digest username="user", ' + b'realm="me@kennethreitz.com", ' + b'nonce="6bf5d6e4da1ce66918800195d6b9130d", uri="/"') + + auth = requests.auth.HTTPDigestAuth('user', 'pass') + + def digest_response_handler(sock): + # Respond to initial GET with a challenge. + request_content = consume_socket_content(sock, timeout=0.5) + assert request_content.startswith(b"GET / HTTP/1.1") + sock.send(text_401) + + # Verify we receive an Authorization header in response, then redirect. + request_content = consume_socket_content(sock, timeout=0.5) + assert expected_digest in request_content + sock.send(text_302) + + # Verify Authorization isn't sent to the redirected host, + # then send another challenge. + request_content = consume_socket_content(sock, timeout=0.5) + assert b'Authorization:' not in request_content + sock.send(text_401) + + # Verify Authorization is sent correctly again, and return 200 OK. + request_content = consume_socket_content(sock, timeout=0.5) + assert expected_digest in request_content + sock.send(text_200) + + return request_content + + close_server = threading.Event() + server = Server(digest_response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/' + r = requests.get(url, auth=auth) + # Verify server succeeded in authenticating. + assert r.status_code == 200 + # Verify Authorization was sent in final request. + assert 'Authorization' in r.request.headers + assert r.request.headers['Authorization'].startswith('Digest ') + # Verify redirect happened as we expected. + assert r.history[0].status_code == 302 + close_server.set() + + +def test_digestauth_401_only_sent_once(): + """Ensure we correctly respond to a 401 challenge once, and then + stop responding if challenged again. + """ + text_401 = (b'HTTP/1.1 401 UNAUTHORIZED\r\n' + b'Content-Length: 0\r\n' + b'WWW-Authenticate: Digest nonce="6bf5d6e4da1ce66918800195d6b9130d"' + b', opaque="372825293d1c26955496c80ed6426e9e", ' + b'realm="me@kennethreitz.com", qop=auth\r\n\r\n') + + expected_digest = (b'Authorization: Digest username="user", ' + b'realm="me@kennethreitz.com", ' + b'nonce="6bf5d6e4da1ce66918800195d6b9130d", uri="/"') + + auth = requests.auth.HTTPDigestAuth('user', 'pass') + + def digest_failed_response_handler(sock): + # Respond to initial GET with a challenge. + request_content = consume_socket_content(sock, timeout=0.5) + assert request_content.startswith(b"GET / HTTP/1.1") + sock.send(text_401) + + # Verify we receive an Authorization header in response, then + # challenge again. + request_content = consume_socket_content(sock, timeout=0.5) + assert expected_digest in request_content + sock.send(text_401) + + # Verify the client didn't respond to second challenge. + request_content = consume_socket_content(sock, timeout=0.5) + assert request_content == b'' + + return request_content + + close_server = threading.Event() + server = Server(digest_failed_response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/' + r = requests.get(url, auth=auth) + # Verify server didn't authenticate us. + assert r.status_code == 401 + assert r.history[0].status_code == 401 + close_server.set() + + +def test_digestauth_only_on_4xx(): + """Ensure we only send digestauth on 4xx challenges. + + See https://github.com/psf/requests/issues/3772. + """ + text_200_chal = (b'HTTP/1.1 200 OK\r\n' + b'Content-Length: 0\r\n' + b'WWW-Authenticate: Digest nonce="6bf5d6e4da1ce66918800195d6b9130d"' + b', opaque="372825293d1c26955496c80ed6426e9e", ' + b'realm="me@kennethreitz.com", qop=auth\r\n\r\n') + + auth = requests.auth.HTTPDigestAuth('user', 'pass') + + def digest_response_handler(sock): + # Respond to GET with a 200 containing www-authenticate header. + request_content = consume_socket_content(sock, timeout=0.5) + assert request_content.startswith(b"GET / HTTP/1.1") + sock.send(text_200_chal) + + # Verify the client didn't respond with auth. + request_content = consume_socket_content(sock, timeout=0.5) + assert request_content == b'' + + return request_content + + close_server = threading.Event() + server = Server(digest_response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/' + r = requests.get(url, auth=auth) + # Verify server didn't receive auth from us. + assert r.status_code == 200 + assert len(r.history) == 0 + close_server.set() + + +_schemes_by_var_prefix = [ + ('http', ['http']), + ('https', ['https']), + ('all', ['http', 'https']), +] + +_proxy_combos = [] +for prefix, schemes in _schemes_by_var_prefix: + for scheme in schemes: + _proxy_combos.append((f"{prefix}_proxy", scheme)) + +_proxy_combos += [(var.upper(), scheme) for var, scheme in _proxy_combos] + + +@pytest.mark.parametrize("var,scheme", _proxy_combos) +def test_use_proxy_from_environment(httpbin, var, scheme): + url = f"{scheme}://httpbin.org" + fake_proxy = Server() # do nothing with the requests; just close the socket + with fake_proxy as (host, port): + proxy_url = f"socks5://{host}:{port}" + kwargs = {var: proxy_url} + with override_environ(**kwargs): + # fake proxy's lack of response will cause a ConnectionError + with pytest.raises(requests.exceptions.ConnectionError): + requests.get(url) + + # the fake proxy received a request + assert len(fake_proxy.handler_results) == 1 + + # it had actual content (not checking for SOCKS protocol for now) + assert len(fake_proxy.handler_results[0]) > 0 + + +def test_redirect_rfc1808_to_non_ascii_location(): + path = 'š' + expected_path = b'%C5%A1' + redirect_request = [] # stores the second request to the server + + def redirect_resp_handler(sock): + consume_socket_content(sock, timeout=0.5) + location = f'//{host}:{port}/{path}' + sock.send( + ( + b'HTTP/1.1 301 Moved Permanently\r\n' + b'Content-Length: 0\r\n' + b'Location: %s\r\n' + b'\r\n' + ) % location.encode('utf8') + ) + redirect_request.append(consume_socket_content(sock, timeout=0.5)) + sock.send(b'HTTP/1.1 200 OK\r\n\r\n') + + close_server = threading.Event() + server = Server(redirect_resp_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}' + r = requests.get(url=url, allow_redirects=True) + assert r.status_code == 200 + assert len(r.history) == 1 + assert r.history[0].status_code == 301 + assert redirect_request[0].startswith(b'GET /' + expected_path + b' HTTP/1.1') + assert r.url == '{}/{}'.format(url, expected_path.decode('ascii')) + + close_server.set() + + +def test_fragment_not_sent_with_request(): + """Verify that the fragment portion of a URI isn't sent to the server.""" + close_server = threading.Event() + server = Server(echo_response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/path/to/thing/#view=edit&token=hunter2' + r = requests.get(url) + raw_request = r.content + + assert r.status_code == 200 + headers, body = raw_request.split(b'\r\n\r\n', 1) + status_line, headers = headers.split(b'\r\n', 1) + + assert status_line == b'GET /path/to/thing/ HTTP/1.1' + for frag in (b'view', b'edit', b'token', b'hunter2'): + assert frag not in headers + assert frag not in body + + close_server.set() + + +def test_fragment_update_on_redirect(): + """Verify we only append previous fragment if one doesn't exist on new + location. If a new fragment is encountered in a Location header, it should + be added to all subsequent requests. + """ + + def response_handler(sock): + consume_socket_content(sock, timeout=0.5) + sock.send( + b'HTTP/1.1 302 FOUND\r\n' + b'Content-Length: 0\r\n' + b'Location: /get#relevant-section\r\n\r\n' + ) + consume_socket_content(sock, timeout=0.5) + sock.send( + b'HTTP/1.1 302 FOUND\r\n' + b'Content-Length: 0\r\n' + b'Location: /final-url/\r\n\r\n' + ) + consume_socket_content(sock, timeout=0.5) + sock.send( + b'HTTP/1.1 200 OK\r\n\r\n' + ) + + close_server = threading.Event() + server = Server(response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/path/to/thing/#view=edit&token=hunter2' + r = requests.get(url) + + assert r.status_code == 200 + assert len(r.history) == 2 + assert r.history[0].request.url == url + + # Verify we haven't overwritten the location with our previous fragment. + assert r.history[1].request.url == f'http://{host}:{port}/get#relevant-section' + # Verify previous fragment is used and not the original. + assert r.url == f'http://{host}:{port}/final-url/#relevant-section' + + close_server.set() + + +def test_json_decode_compatibility_for_alt_utf_encodings(): + + def response_handler(sock): + consume_socket_content(sock, timeout=0.5) + sock.send( + b'HTTP/1.1 200 OK\r\n' + b'Content-Length: 18\r\n\r\n' + b'\xff\xfe{\x00"\x00K0"\x00=\x00"\x00\xab0"\x00\r\n' + ) + + close_server = threading.Event() + server = Server(response_handler, wait_to_close_event=close_server) + + with server as (host, port): + url = f'http://{host}:{port}/' + r = requests.get(url) + r.encoding = None + with pytest.raises(requests.exceptions.JSONDecodeError) as excinfo: + r.json() + assert isinstance(excinfo.value, requests.exceptions.RequestException) + assert isinstance(excinfo.value, JSONDecodeError) + assert r.text not in str(excinfo.value) diff --git a/test/fixtures/whole_applications/requests/tests/test_packages.py b/test/fixtures/whole_applications/requests/tests/test_packages.py new file mode 100644 index 0000000..b55cb68 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_packages.py @@ -0,0 +1,13 @@ +import requests + + +def test_can_access_urllib3_attribute(): + requests.packages.urllib3 + + +def test_can_access_idna_attribute(): + requests.packages.idna + + +def test_can_access_chardet_attribute(): + requests.packages.chardet diff --git a/test/fixtures/whole_applications/requests/tests/test_requests.py b/test/fixtures/whole_applications/requests/tests/test_requests.py new file mode 100644 index 0000000..b420c44 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_requests.py @@ -0,0 +1,2810 @@ +"""Tests for Requests.""" + +import collections +import contextlib +import io +import json +import os +import pickle +import re +import warnings + +import pytest +import urllib3 +from urllib3.util import Timeout as Urllib3Timeout + +import requests +from requests.adapters import HTTPAdapter +from requests.auth import HTTPDigestAuth, _basic_auth_str +from requests.compat import ( + JSONDecodeError, + Morsel, + MutableMapping, + builtin_str, + cookielib, + getproxies, + urlparse, +) +from requests.cookies import cookiejar_from_dict, morsel_to_cookie +from requests.exceptions import ( + ChunkedEncodingError, + ConnectionError, + ConnectTimeout, + ContentDecodingError, + InvalidHeader, + InvalidProxyURL, + InvalidSchema, + InvalidURL, + MissingSchema, + ProxyError, + ReadTimeout, + RequestException, + RetryError, +) +from requests.exceptions import SSLError as RequestsSSLError +from requests.exceptions import Timeout, TooManyRedirects, UnrewindableBodyError +from requests.hooks import default_hooks +from requests.models import PreparedRequest, urlencode +from requests.sessions import SessionRedirectMixin +from requests.structures import CaseInsensitiveDict + +from . import SNIMissingWarning +from .compat import StringIO +from .utils import override_environ + +# Requests to this URL should always fail with a connection timeout (nothing +# listening on that port) +TARPIT = "http://10.255.255.1" + +# This is to avoid waiting the timeout of using TARPIT +INVALID_PROXY = "http://localhost:1" + +try: + from ssl import SSLContext + + del SSLContext + HAS_MODERN_SSL = True +except ImportError: + HAS_MODERN_SSL = False + +try: + requests.pyopenssl + HAS_PYOPENSSL = True +except AttributeError: + HAS_PYOPENSSL = False + + +class TestRequests: + + digest_auth_algo = ("MD5", "SHA-256", "SHA-512") + + def test_entry_points(self): + + requests.session + requests.session().get + requests.session().head + requests.get + requests.head + requests.put + requests.patch + requests.post + # Not really an entry point, but people rely on it. + from requests.packages.urllib3.poolmanager import PoolManager # noqa:F401 + + @pytest.mark.parametrize( + "exception, url", + ( + (MissingSchema, "hiwpefhipowhefopw"), + (InvalidSchema, "localhost:3128"), + (InvalidSchema, "localhost.localdomain:3128/"), + (InvalidSchema, "10.122.1.1:3128/"), + (InvalidURL, "http://"), + (InvalidURL, "http://*example.com"), + (InvalidURL, "http://.example.com"), + ), + ) + def test_invalid_url(self, exception, url): + with pytest.raises(exception): + requests.get(url) + + def test_basic_building(self): + req = requests.Request() + req.url = "http://kennethreitz.org/" + req.data = {"life": "42"} + + pr = req.prepare() + assert pr.url == req.url + assert pr.body == "life=42" + + @pytest.mark.parametrize("method", ("GET", "HEAD")) + def test_no_content_length(self, httpbin, method): + req = requests.Request(method, httpbin(method.lower())).prepare() + assert "Content-Length" not in req.headers + + @pytest.mark.parametrize("method", ("POST", "PUT", "PATCH", "OPTIONS")) + def test_no_body_content_length(self, httpbin, method): + req = requests.Request(method, httpbin(method.lower())).prepare() + assert req.headers["Content-Length"] == "0" + + @pytest.mark.parametrize("method", ("POST", "PUT", "PATCH", "OPTIONS")) + def test_empty_content_length(self, httpbin, method): + req = requests.Request(method, httpbin(method.lower()), data="").prepare() + assert req.headers["Content-Length"] == "0" + + def test_override_content_length(self, httpbin): + headers = {"Content-Length": "not zero"} + r = requests.Request("POST", httpbin("post"), headers=headers).prepare() + assert "Content-Length" in r.headers + assert r.headers["Content-Length"] == "not zero" + + def test_path_is_not_double_encoded(self): + request = requests.Request("GET", "http://0.0.0.0/get/test case").prepare() + + assert request.path_url == "/get/test%20case" + + @pytest.mark.parametrize( + "url, expected", + ( + ( + "http://example.com/path#fragment", + "http://example.com/path?a=b#fragment", + ), + ( + "http://example.com/path?key=value#fragment", + "http://example.com/path?key=value&a=b#fragment", + ), + ), + ) + def test_params_are_added_before_fragment(self, url, expected): + request = requests.Request("GET", url, params={"a": "b"}).prepare() + assert request.url == expected + + def test_params_original_order_is_preserved_by_default(self): + param_ordered_dict = collections.OrderedDict( + (("z", 1), ("a", 1), ("k", 1), ("d", 1)) + ) + session = requests.Session() + request = requests.Request( + "GET", "http://example.com/", params=param_ordered_dict + ) + prep = session.prepare_request(request) + assert prep.url == "http://example.com/?z=1&a=1&k=1&d=1" + + def test_params_bytes_are_encoded(self): + request = requests.Request( + "GET", "http://example.com", params=b"test=foo" + ).prepare() + assert request.url == "http://example.com/?test=foo" + + def test_binary_put(self): + request = requests.Request( + "PUT", "http://example.com", data="ööö".encode() + ).prepare() + assert isinstance(request.body, bytes) + + def test_whitespaces_are_removed_from_url(self): + # Test for issue #3696 + request = requests.Request("GET", " http://example.com").prepare() + assert request.url == "http://example.com/" + + @pytest.mark.parametrize("scheme", ("http://", "HTTP://", "hTTp://", "HttP://")) + def test_mixed_case_scheme_acceptable(self, httpbin, scheme): + s = requests.Session() + s.proxies = getproxies() + parts = urlparse(httpbin("get")) + url = scheme + parts.netloc + parts.path + r = requests.Request("GET", url) + r = s.send(r.prepare()) + assert r.status_code == 200, f"failed for scheme {scheme}" + + def test_HTTP_200_OK_GET_ALTERNATIVE(self, httpbin): + r = requests.Request("GET", httpbin("get")) + s = requests.Session() + s.proxies = getproxies() + + r = s.send(r.prepare()) + + assert r.status_code == 200 + + def test_HTTP_302_ALLOW_REDIRECT_GET(self, httpbin): + r = requests.get(httpbin("redirect", "1")) + assert r.status_code == 200 + assert r.history[0].status_code == 302 + assert r.history[0].is_redirect + + def test_HTTP_307_ALLOW_REDIRECT_POST(self, httpbin): + r = requests.post( + httpbin("redirect-to"), + data="test", + params={"url": "post", "status_code": 307}, + ) + assert r.status_code == 200 + assert r.history[0].status_code == 307 + assert r.history[0].is_redirect + assert r.json()["data"] == "test" + + def test_HTTP_307_ALLOW_REDIRECT_POST_WITH_SEEKABLE(self, httpbin): + byte_str = b"test" + r = requests.post( + httpbin("redirect-to"), + data=io.BytesIO(byte_str), + params={"url": "post", "status_code": 307}, + ) + assert r.status_code == 200 + assert r.history[0].status_code == 307 + assert r.history[0].is_redirect + assert r.json()["data"] == byte_str.decode("utf-8") + + def test_HTTP_302_TOO_MANY_REDIRECTS(self, httpbin): + try: + requests.get(httpbin("relative-redirect", "50")) + except TooManyRedirects as e: + url = httpbin("relative-redirect", "20") + assert e.request.url == url + assert e.response.url == url + assert len(e.response.history) == 30 + else: + pytest.fail("Expected redirect to raise TooManyRedirects but it did not") + + def test_HTTP_302_TOO_MANY_REDIRECTS_WITH_PARAMS(self, httpbin): + s = requests.session() + s.max_redirects = 5 + try: + s.get(httpbin("relative-redirect", "50")) + except TooManyRedirects as e: + url = httpbin("relative-redirect", "45") + assert e.request.url == url + assert e.response.url == url + assert len(e.response.history) == 5 + else: + pytest.fail( + "Expected custom max number of redirects to be respected but was not" + ) + + def test_http_301_changes_post_to_get(self, httpbin): + r = requests.post(httpbin("status", "301")) + assert r.status_code == 200 + assert r.request.method == "GET" + assert r.history[0].status_code == 301 + assert r.history[0].is_redirect + + def test_http_301_doesnt_change_head_to_get(self, httpbin): + r = requests.head(httpbin("status", "301"), allow_redirects=True) + print(r.content) + assert r.status_code == 200 + assert r.request.method == "HEAD" + assert r.history[0].status_code == 301 + assert r.history[0].is_redirect + + def test_http_302_changes_post_to_get(self, httpbin): + r = requests.post(httpbin("status", "302")) + assert r.status_code == 200 + assert r.request.method == "GET" + assert r.history[0].status_code == 302 + assert r.history[0].is_redirect + + def test_http_302_doesnt_change_head_to_get(self, httpbin): + r = requests.head(httpbin("status", "302"), allow_redirects=True) + assert r.status_code == 200 + assert r.request.method == "HEAD" + assert r.history[0].status_code == 302 + assert r.history[0].is_redirect + + def test_http_303_changes_post_to_get(self, httpbin): + r = requests.post(httpbin("status", "303")) + assert r.status_code == 200 + assert r.request.method == "GET" + assert r.history[0].status_code == 303 + assert r.history[0].is_redirect + + def test_http_303_doesnt_change_head_to_get(self, httpbin): + r = requests.head(httpbin("status", "303"), allow_redirects=True) + assert r.status_code == 200 + assert r.request.method == "HEAD" + assert r.history[0].status_code == 303 + assert r.history[0].is_redirect + + def test_header_and_body_removal_on_redirect(self, httpbin): + purged_headers = ("Content-Length", "Content-Type") + ses = requests.Session() + req = requests.Request("POST", httpbin("post"), data={"test": "data"}) + prep = ses.prepare_request(req) + resp = ses.send(prep) + + # Mimic a redirect response + resp.status_code = 302 + resp.headers["location"] = "get" + + # Run request through resolve_redirects + next_resp = next(ses.resolve_redirects(resp, prep)) + assert next_resp.request.body is None + for header in purged_headers: + assert header not in next_resp.request.headers + + def test_transfer_enc_removal_on_redirect(self, httpbin): + purged_headers = ("Transfer-Encoding", "Content-Type") + ses = requests.Session() + req = requests.Request("POST", httpbin("post"), data=(b"x" for x in range(1))) + prep = ses.prepare_request(req) + assert "Transfer-Encoding" in prep.headers + + # Create Response to avoid https://github.com/kevin1024/pytest-httpbin/issues/33 + resp = requests.Response() + resp.raw = io.BytesIO(b"the content") + resp.request = prep + setattr(resp.raw, "release_conn", lambda *args: args) + + # Mimic a redirect response + resp.status_code = 302 + resp.headers["location"] = httpbin("get") + + # Run request through resolve_redirect + next_resp = next(ses.resolve_redirects(resp, prep)) + assert next_resp.request.body is None + for header in purged_headers: + assert header not in next_resp.request.headers + + def test_fragment_maintained_on_redirect(self, httpbin): + fragment = "#view=edit&token=hunter2" + r = requests.get(httpbin("redirect-to?url=get") + fragment) + + assert len(r.history) > 0 + assert r.history[0].request.url == httpbin("redirect-to?url=get") + fragment + assert r.url == httpbin("get") + fragment + + def test_HTTP_200_OK_GET_WITH_PARAMS(self, httpbin): + heads = {"User-agent": "Mozilla/5.0"} + + r = requests.get(httpbin("user-agent"), headers=heads) + + assert heads["User-agent"] in r.text + assert r.status_code == 200 + + def test_HTTP_200_OK_GET_WITH_MIXED_PARAMS(self, httpbin): + heads = {"User-agent": "Mozilla/5.0"} + + r = requests.get( + httpbin("get") + "?test=true", params={"q": "test"}, headers=heads + ) + assert r.status_code == 200 + + def test_set_cookie_on_301(self, httpbin): + s = requests.session() + url = httpbin("cookies/set?foo=bar") + s.get(url) + assert s.cookies["foo"] == "bar" + + def test_cookie_sent_on_redirect(self, httpbin): + s = requests.session() + s.get(httpbin("cookies/set?foo=bar")) + r = s.get(httpbin("redirect/1")) # redirects to httpbin('get') + assert "Cookie" in r.json()["headers"] + + def test_cookie_removed_on_expire(self, httpbin): + s = requests.session() + s.get(httpbin("cookies/set?foo=bar")) + assert s.cookies["foo"] == "bar" + s.get( + httpbin("response-headers"), + params={"Set-Cookie": "foo=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT"}, + ) + assert "foo" not in s.cookies + + def test_cookie_quote_wrapped(self, httpbin): + s = requests.session() + s.get(httpbin('cookies/set?foo="bar:baz"')) + assert s.cookies["foo"] == '"bar:baz"' + + def test_cookie_persists_via_api(self, httpbin): + s = requests.session() + r = s.get(httpbin("redirect/1"), cookies={"foo": "bar"}) + assert "foo" in r.request.headers["Cookie"] + assert "foo" in r.history[0].request.headers["Cookie"] + + def test_request_cookie_overrides_session_cookie(self, httpbin): + s = requests.session() + s.cookies["foo"] = "bar" + r = s.get(httpbin("cookies"), cookies={"foo": "baz"}) + assert r.json()["cookies"]["foo"] == "baz" + # Session cookie should not be modified + assert s.cookies["foo"] == "bar" + + def test_request_cookies_not_persisted(self, httpbin): + s = requests.session() + s.get(httpbin("cookies"), cookies={"foo": "baz"}) + # Sending a request with cookies should not add cookies to the session + assert not s.cookies + + def test_generic_cookiejar_works(self, httpbin): + cj = cookielib.CookieJar() + cookiejar_from_dict({"foo": "bar"}, cj) + s = requests.session() + s.cookies = cj + r = s.get(httpbin("cookies")) + # Make sure the cookie was sent + assert r.json()["cookies"]["foo"] == "bar" + # Make sure the session cj is still the custom one + assert s.cookies is cj + + def test_param_cookiejar_works(self, httpbin): + cj = cookielib.CookieJar() + cookiejar_from_dict({"foo": "bar"}, cj) + s = requests.session() + r = s.get(httpbin("cookies"), cookies=cj) + # Make sure the cookie was sent + assert r.json()["cookies"]["foo"] == "bar" + + def test_cookielib_cookiejar_on_redirect(self, httpbin): + """Tests resolve_redirect doesn't fail when merging cookies + with non-RequestsCookieJar cookiejar. + + See GH #3579 + """ + cj = cookiejar_from_dict({"foo": "bar"}, cookielib.CookieJar()) + s = requests.Session() + s.cookies = cookiejar_from_dict({"cookie": "tasty"}) + + # Prepare request without using Session + req = requests.Request("GET", httpbin("headers"), cookies=cj) + prep_req = req.prepare() + + # Send request and simulate redirect + resp = s.send(prep_req) + resp.status_code = 302 + resp.headers["location"] = httpbin("get") + redirects = s.resolve_redirects(resp, prep_req) + resp = next(redirects) + + # Verify CookieJar isn't being converted to RequestsCookieJar + assert isinstance(prep_req._cookies, cookielib.CookieJar) + assert isinstance(resp.request._cookies, cookielib.CookieJar) + assert not isinstance(resp.request._cookies, requests.cookies.RequestsCookieJar) + + cookies = {} + for c in resp.request._cookies: + cookies[c.name] = c.value + assert cookies["foo"] == "bar" + assert cookies["cookie"] == "tasty" + + def test_requests_in_history_are_not_overridden(self, httpbin): + resp = requests.get(httpbin("redirect/3")) + urls = [r.url for r in resp.history] + req_urls = [r.request.url for r in resp.history] + assert urls == req_urls + + def test_history_is_always_a_list(self, httpbin): + """Show that even with redirects, Response.history is always a list.""" + resp = requests.get(httpbin("get")) + assert isinstance(resp.history, list) + resp = requests.get(httpbin("redirect/1")) + assert isinstance(resp.history, list) + assert not isinstance(resp.history, tuple) + + def test_headers_on_session_with_None_are_not_sent(self, httpbin): + """Do not send headers in Session.headers with None values.""" + ses = requests.Session() + ses.headers["Accept-Encoding"] = None + req = requests.Request("GET", httpbin("get")) + prep = ses.prepare_request(req) + assert "Accept-Encoding" not in prep.headers + + def test_headers_preserve_order(self, httpbin): + """Preserve order when headers provided as OrderedDict.""" + ses = requests.Session() + ses.headers = collections.OrderedDict() + ses.headers["Accept-Encoding"] = "identity" + ses.headers["First"] = "1" + ses.headers["Second"] = "2" + headers = collections.OrderedDict([("Third", "3"), ("Fourth", "4")]) + headers["Fifth"] = "5" + headers["Second"] = "222" + req = requests.Request("GET", httpbin("get"), headers=headers) + prep = ses.prepare_request(req) + items = list(prep.headers.items()) + assert items[0] == ("Accept-Encoding", "identity") + assert items[1] == ("First", "1") + assert items[2] == ("Second", "222") + assert items[3] == ("Third", "3") + assert items[4] == ("Fourth", "4") + assert items[5] == ("Fifth", "5") + + @pytest.mark.parametrize("key", ("User-agent", "user-agent")) + def test_user_agent_transfers(self, httpbin, key): + + heads = {key: "Mozilla/5.0 (github.com/psf/requests)"} + + r = requests.get(httpbin("user-agent"), headers=heads) + assert heads[key] in r.text + + def test_HTTP_200_OK_HEAD(self, httpbin): + r = requests.head(httpbin("get")) + assert r.status_code == 200 + + def test_HTTP_200_OK_PUT(self, httpbin): + r = requests.put(httpbin("put")) + assert r.status_code == 200 + + def test_BASICAUTH_TUPLE_HTTP_200_OK_GET(self, httpbin): + auth = ("user", "pass") + url = httpbin("basic-auth", "user", "pass") + + r = requests.get(url, auth=auth) + assert r.status_code == 200 + + r = requests.get(url) + assert r.status_code == 401 + + s = requests.session() + s.auth = auth + r = s.get(url) + assert r.status_code == 200 + + @pytest.mark.parametrize( + "username, password", + ( + ("user", "pass"), + ("имя".encode(), "пароль".encode()), + (42, 42), + (None, None), + ), + ) + def test_set_basicauth(self, httpbin, username, password): + auth = (username, password) + url = httpbin("get") + + r = requests.Request("GET", url, auth=auth) + p = r.prepare() + + assert p.headers["Authorization"] == _basic_auth_str(username, password) + + def test_basicauth_encodes_byte_strings(self): + """Ensure b'test' formats as the byte string "test" rather + than the unicode string "b'test'" in Python 3. + """ + auth = (b"\xc5\xafsername", b"test\xc6\xb6") + r = requests.Request("GET", "http://localhost", auth=auth) + p = r.prepare() + + assert p.headers["Authorization"] == "Basic xa9zZXJuYW1lOnRlc3TGtg==" + + @pytest.mark.parametrize( + "url, exception", + ( + # Connecting to an unknown domain should raise a ConnectionError + ("http://doesnotexist.google.com", ConnectionError), + # Connecting to an invalid port should raise a ConnectionError + ("http://localhost:1", ConnectionError), + # Inputing a URL that cannot be parsed should raise an InvalidURL error + ("http://fe80::5054:ff:fe5a:fc0", InvalidURL), + ), + ) + def test_errors(self, url, exception): + with pytest.raises(exception): + requests.get(url, timeout=1) + + def test_proxy_error(self): + # any proxy related error (address resolution, no route to host, etc) should result in a ProxyError + with pytest.raises(ProxyError): + requests.get( + "http://localhost:1", proxies={"http": "non-resolvable-address"} + ) + + def test_proxy_error_on_bad_url(self, httpbin, httpbin_secure): + with pytest.raises(InvalidProxyURL): + requests.get(httpbin_secure(), proxies={"https": "http:/badproxyurl:3128"}) + + with pytest.raises(InvalidProxyURL): + requests.get(httpbin(), proxies={"http": "http://:8080"}) + + with pytest.raises(InvalidProxyURL): + requests.get(httpbin_secure(), proxies={"https": "https://"}) + + with pytest.raises(InvalidProxyURL): + requests.get(httpbin(), proxies={"http": "http:///example.com:8080"}) + + def test_respect_proxy_env_on_send_self_prepared_request(self, httpbin): + with override_environ(http_proxy=INVALID_PROXY): + with pytest.raises(ProxyError): + session = requests.Session() + request = requests.Request("GET", httpbin()) + session.send(request.prepare()) + + def test_respect_proxy_env_on_send_session_prepared_request(self, httpbin): + with override_environ(http_proxy=INVALID_PROXY): + with pytest.raises(ProxyError): + session = requests.Session() + request = requests.Request("GET", httpbin()) + prepared = session.prepare_request(request) + session.send(prepared) + + def test_respect_proxy_env_on_send_with_redirects(self, httpbin): + with override_environ(http_proxy=INVALID_PROXY): + with pytest.raises(ProxyError): + session = requests.Session() + url = httpbin("redirect/1") + print(url) + request = requests.Request("GET", url) + session.send(request.prepare()) + + def test_respect_proxy_env_on_get(self, httpbin): + with override_environ(http_proxy=INVALID_PROXY): + with pytest.raises(ProxyError): + session = requests.Session() + session.get(httpbin()) + + def test_respect_proxy_env_on_request(self, httpbin): + with override_environ(http_proxy=INVALID_PROXY): + with pytest.raises(ProxyError): + session = requests.Session() + session.request(method="GET", url=httpbin()) + + def test_proxy_authorization_preserved_on_request(self, httpbin): + proxy_auth_value = "Bearer XXX" + session = requests.Session() + session.headers.update({"Proxy-Authorization": proxy_auth_value}) + resp = session.request(method="GET", url=httpbin("get")) + sent_headers = resp.json().get("headers", {}) + + assert sent_headers.get("Proxy-Authorization") == proxy_auth_value + + + @pytest.mark.parametrize( + "url,has_proxy_auth", + ( + ('http://example.com', True), + ('https://example.com', False), + ), + ) + def test_proxy_authorization_not_appended_to_https_request(self, url, has_proxy_auth): + session = requests.Session() + proxies = { + 'http': 'http://test:pass@localhost:8080', + 'https': 'http://test:pass@localhost:8090', + } + req = requests.Request('GET', url) + prep = req.prepare() + session.rebuild_proxies(prep, proxies) + + assert ('Proxy-Authorization' in prep.headers) is has_proxy_auth + + def test_basicauth_with_netrc(self, httpbin): + auth = ("user", "pass") + wrong_auth = ("wronguser", "wrongpass") + url = httpbin("basic-auth", "user", "pass") + + old_auth = requests.sessions.get_netrc_auth + + try: + + def get_netrc_auth_mock(url): + return auth + + requests.sessions.get_netrc_auth = get_netrc_auth_mock + + # Should use netrc and work. + r = requests.get(url) + assert r.status_code == 200 + + # Given auth should override and fail. + r = requests.get(url, auth=wrong_auth) + assert r.status_code == 401 + + s = requests.session() + + # Should use netrc and work. + r = s.get(url) + assert r.status_code == 200 + + # Given auth should override and fail. + s.auth = wrong_auth + r = s.get(url) + assert r.status_code == 401 + finally: + requests.sessions.get_netrc_auth = old_auth + + def test_DIGEST_HTTP_200_OK_GET(self, httpbin): + + for authtype in self.digest_auth_algo: + auth = HTTPDigestAuth("user", "pass") + url = httpbin("digest-auth", "auth", "user", "pass", authtype, "never") + + r = requests.get(url, auth=auth) + assert r.status_code == 200 + + r = requests.get(url) + assert r.status_code == 401 + print(r.headers["WWW-Authenticate"]) + + s = requests.session() + s.auth = HTTPDigestAuth("user", "pass") + r = s.get(url) + assert r.status_code == 200 + + def test_DIGEST_AUTH_RETURNS_COOKIE(self, httpbin): + + for authtype in self.digest_auth_algo: + url = httpbin("digest-auth", "auth", "user", "pass", authtype) + auth = HTTPDigestAuth("user", "pass") + r = requests.get(url) + assert r.cookies["fake"] == "fake_value" + + r = requests.get(url, auth=auth) + assert r.status_code == 200 + + def test_DIGEST_AUTH_SETS_SESSION_COOKIES(self, httpbin): + + for authtype in self.digest_auth_algo: + url = httpbin("digest-auth", "auth", "user", "pass", authtype) + auth = HTTPDigestAuth("user", "pass") + s = requests.Session() + s.get(url, auth=auth) + assert s.cookies["fake"] == "fake_value" + + def test_DIGEST_STREAM(self, httpbin): + + for authtype in self.digest_auth_algo: + auth = HTTPDigestAuth("user", "pass") + url = httpbin("digest-auth", "auth", "user", "pass", authtype) + + r = requests.get(url, auth=auth, stream=True) + assert r.raw.read() != b"" + + r = requests.get(url, auth=auth, stream=False) + assert r.raw.read() == b"" + + def test_DIGESTAUTH_WRONG_HTTP_401_GET(self, httpbin): + + for authtype in self.digest_auth_algo: + auth = HTTPDigestAuth("user", "wrongpass") + url = httpbin("digest-auth", "auth", "user", "pass", authtype) + + r = requests.get(url, auth=auth) + assert r.status_code == 401 + + r = requests.get(url) + assert r.status_code == 401 + + s = requests.session() + s.auth = auth + r = s.get(url) + assert r.status_code == 401 + + def test_DIGESTAUTH_QUOTES_QOP_VALUE(self, httpbin): + + for authtype in self.digest_auth_algo: + auth = HTTPDigestAuth("user", "pass") + url = httpbin("digest-auth", "auth", "user", "pass", authtype) + + r = requests.get(url, auth=auth) + assert '"auth"' in r.request.headers["Authorization"] + + def test_POSTBIN_GET_POST_FILES(self, httpbin): + + url = httpbin("post") + requests.post(url).raise_for_status() + + post1 = requests.post(url, data={"some": "data"}) + assert post1.status_code == 200 + + with open("requirements-dev.txt") as f: + post2 = requests.post(url, files={"some": f}) + assert post2.status_code == 200 + + post4 = requests.post(url, data='[{"some": "json"}]') + assert post4.status_code == 200 + + with pytest.raises(ValueError): + requests.post(url, files=["bad file data"]) + + def test_invalid_files_input(self, httpbin): + + url = httpbin("post") + post = requests.post(url, files={"random-file-1": None, "random-file-2": 1}) + assert b'name="random-file-1"' not in post.request.body + assert b'name="random-file-2"' in post.request.body + + def test_POSTBIN_SEEKED_OBJECT_WITH_NO_ITER(self, httpbin): + class TestStream: + def __init__(self, data): + self.data = data.encode() + self.length = len(self.data) + self.index = 0 + + def __len__(self): + return self.length + + def read(self, size=None): + if size: + ret = self.data[self.index : self.index + size] + self.index += size + else: + ret = self.data[self.index :] + self.index = self.length + return ret + + def tell(self): + return self.index + + def seek(self, offset, where=0): + if where == 0: + self.index = offset + elif where == 1: + self.index += offset + elif where == 2: + self.index = self.length + offset + + test = TestStream("test") + post1 = requests.post(httpbin("post"), data=test) + assert post1.status_code == 200 + assert post1.json()["data"] == "test" + + test = TestStream("test") + test.seek(2) + post2 = requests.post(httpbin("post"), data=test) + assert post2.status_code == 200 + assert post2.json()["data"] == "st" + + def test_POSTBIN_GET_POST_FILES_WITH_DATA(self, httpbin): + + url = httpbin("post") + requests.post(url).raise_for_status() + + post1 = requests.post(url, data={"some": "data"}) + assert post1.status_code == 200 + + with open("requirements-dev.txt") as f: + post2 = requests.post(url, data={"some": "data"}, files={"some": f}) + assert post2.status_code == 200 + + post4 = requests.post(url, data='[{"some": "json"}]') + assert post4.status_code == 200 + + with pytest.raises(ValueError): + requests.post(url, files=["bad file data"]) + + def test_post_with_custom_mapping(self, httpbin): + class CustomMapping(MutableMapping): + def __init__(self, *args, **kwargs): + self.data = dict(*args, **kwargs) + + def __delitem__(self, key): + del self.data[key] + + def __getitem__(self, key): + return self.data[key] + + def __setitem__(self, key, value): + self.data[key] = value + + def __iter__(self): + return iter(self.data) + + def __len__(self): + return len(self.data) + + data = CustomMapping({"some": "data"}) + url = httpbin("post") + found_json = requests.post(url, data=data).json().get("form") + assert found_json == {"some": "data"} + + def test_conflicting_post_params(self, httpbin): + url = httpbin("post") + with open("requirements-dev.txt") as f: + with pytest.raises(ValueError): + requests.post(url, data='[{"some": "data"}]', files={"some": f}) + + def test_request_ok_set(self, httpbin): + r = requests.get(httpbin("status", "404")) + assert not r.ok + + def test_status_raising(self, httpbin): + r = requests.get(httpbin("status", "404")) + with pytest.raises(requests.exceptions.HTTPError): + r.raise_for_status() + + r = requests.get(httpbin("status", "500")) + assert not r.ok + + def test_decompress_gzip(self, httpbin): + r = requests.get(httpbin("gzip")) + r.content.decode("ascii") + + @pytest.mark.parametrize( + "url, params", + ( + ("/get", {"foo": "føø"}), + ("/get", {"føø": "føø"}), + ("/get", {"føø": "føø"}), + ("/get", {"foo": "foo"}), + ("ø", {"foo": "foo"}), + ), + ) + def test_unicode_get(self, httpbin, url, params): + requests.get(httpbin(url), params=params) + + def test_unicode_header_name(self, httpbin): + requests.put( + httpbin("put"), + headers={"Content-Type": "application/octet-stream"}, + data="\xff", + ) # compat.str is unicode. + + def test_pyopenssl_redirect(self, httpbin_secure, httpbin_ca_bundle): + requests.get(httpbin_secure("status", "301"), verify=httpbin_ca_bundle) + + def test_invalid_ca_certificate_path(self, httpbin_secure): + INVALID_PATH = "/garbage" + with pytest.raises(IOError) as e: + requests.get(httpbin_secure(), verify=INVALID_PATH) + assert str( + e.value + ) == "Could not find a suitable TLS CA certificate bundle, invalid path: {}".format( + INVALID_PATH + ) + + def test_invalid_ssl_certificate_files(self, httpbin_secure): + INVALID_PATH = "/garbage" + with pytest.raises(IOError) as e: + requests.get(httpbin_secure(), cert=INVALID_PATH) + assert str( + e.value + ) == "Could not find the TLS certificate file, invalid path: {}".format( + INVALID_PATH + ) + + with pytest.raises(IOError) as e: + requests.get(httpbin_secure(), cert=(".", INVALID_PATH)) + assert str(e.value) == ( + f"Could not find the TLS key file, invalid path: {INVALID_PATH}" + ) + + @pytest.mark.parametrize( + "env, expected", + ( + ({}, True), + ({"REQUESTS_CA_BUNDLE": "/some/path"}, "/some/path"), + ({"REQUESTS_CA_BUNDLE": ""}, True), + ({"CURL_CA_BUNDLE": "/some/path"}, "/some/path"), + ({"CURL_CA_BUNDLE": ""}, True), + ({"REQUESTS_CA_BUNDLE": "", "CURL_CA_BUNDLE": ""}, True), + ( + { + "REQUESTS_CA_BUNDLE": "/some/path", + "CURL_CA_BUNDLE": "/curl/path", + }, + "/some/path", + ), + ( + { + "REQUESTS_CA_BUNDLE": "", + "CURL_CA_BUNDLE": "/curl/path", + }, + "/curl/path", + ), + ), + ) + def test_env_cert_bundles(self, httpbin, mocker, env, expected): + s = requests.Session() + mocker.patch("os.environ", env) + settings = s.merge_environment_settings( + url=httpbin("get"), proxies={}, stream=False, verify=True, cert=None + ) + assert settings["verify"] == expected + + def test_http_with_certificate(self, httpbin): + r = requests.get(httpbin(), cert=".") + assert r.status_code == 200 + + @pytest.mark.skipif( + SNIMissingWarning is None, + reason="urllib3 2.0 removed that warning and errors out instead", + ) + def test_https_warnings(self, nosan_server): + """warnings are emitted with requests.get""" + host, port, ca_bundle = nosan_server + if HAS_MODERN_SSL or HAS_PYOPENSSL: + warnings_expected = ("SubjectAltNameWarning",) + else: + warnings_expected = ( + "SNIMissingWarning", + "InsecurePlatformWarning", + "SubjectAltNameWarning", + ) + + with pytest.warns(None) as warning_records: + warnings.simplefilter("always") + requests.get(f"https://localhost:{port}/", verify=ca_bundle) + + warning_records = [ + item + for item in warning_records + if item.category.__name__ != "ResourceWarning" + ] + + warnings_category = tuple(item.category.__name__ for item in warning_records) + assert warnings_category == warnings_expected + + def test_certificate_failure(self, httpbin_secure): + """ + When underlying SSL problems occur, an SSLError is raised. + """ + with pytest.raises(RequestsSSLError): + # Our local httpbin does not have a trusted CA, so this call will + # fail if we use our default trust bundle. + requests.get(httpbin_secure("status", "200")) + + def test_urlencoded_get_query_multivalued_param(self, httpbin): + + r = requests.get(httpbin("get"), params={"test": ["foo", "baz"]}) + assert r.status_code == 200 + assert r.url == httpbin("get?test=foo&test=baz") + + def test_form_encoded_post_query_multivalued_element(self, httpbin): + r = requests.Request( + method="POST", url=httpbin("post"), data=dict(test=["foo", "baz"]) + ) + prep = r.prepare() + assert prep.body == "test=foo&test=baz" + + def test_different_encodings_dont_break_post(self, httpbin): + with open(__file__, "rb") as f: + r = requests.post( + httpbin("post"), + data={"stuff": json.dumps({"a": 123})}, + params={"blah": "asdf1234"}, + files={"file": ("test_requests.py", f)}, + ) + assert r.status_code == 200 + + @pytest.mark.parametrize( + "data", + ( + {"stuff": "ëlïxr"}, + {"stuff": "ëlïxr".encode()}, + {"stuff": "elixr"}, + {"stuff": b"elixr"}, + ), + ) + def test_unicode_multipart_post(self, httpbin, data): + with open(__file__, "rb") as f: + r = requests.post( + httpbin("post"), + data=data, + files={"file": ("test_requests.py", f)}, + ) + assert r.status_code == 200 + + def test_unicode_multipart_post_fieldnames(self, httpbin): + filename = os.path.splitext(__file__)[0] + ".py" + with open(filename, "rb") as f: + r = requests.Request( + method="POST", + url=httpbin("post"), + data={b"stuff": "elixr"}, + files={"file": ("test_requests.py", f)}, + ) + prep = r.prepare() + + assert b'name="stuff"' in prep.body + assert b"name=\"b'stuff'\"" not in prep.body + + def test_unicode_method_name(self, httpbin): + with open(__file__, "rb") as f: + files = {"file": f} + r = requests.request( + method="POST", + url=httpbin("post"), + files=files, + ) + assert r.status_code == 200 + + def test_unicode_method_name_with_request_object(self, httpbin): + s = requests.Session() + with open(__file__, "rb") as f: + files = {"file": f} + req = requests.Request("POST", httpbin("post"), files=files) + prep = s.prepare_request(req) + assert isinstance(prep.method, builtin_str) + assert prep.method == "POST" + + resp = s.send(prep) + assert resp.status_code == 200 + + def test_non_prepared_request_error(self): + s = requests.Session() + req = requests.Request("POST", "/") + + with pytest.raises(ValueError) as e: + s.send(req) + assert str(e.value) == "You can only send PreparedRequests." + + def test_custom_content_type(self, httpbin): + with open(__file__, "rb") as f1: + with open(__file__, "rb") as f2: + data = {"stuff": json.dumps({"a": 123})} + files = { + "file1": ("test_requests.py", f1), + "file2": ("test_requests", f2, "text/py-content-type"), + } + r = requests.post(httpbin("post"), data=data, files=files) + assert r.status_code == 200 + assert b"text/py-content-type" in r.request.body + + def test_hook_receives_request_arguments(self, httpbin): + def hook(resp, **kwargs): + assert resp is not None + assert kwargs != {} + + s = requests.Session() + r = requests.Request("GET", httpbin(), hooks={"response": hook}) + prep = s.prepare_request(r) + s.send(prep) + + def test_session_hooks_are_used_with_no_request_hooks(self, httpbin): + def hook(*args, **kwargs): + pass + + s = requests.Session() + s.hooks["response"].append(hook) + r = requests.Request("GET", httpbin()) + prep = s.prepare_request(r) + assert prep.hooks["response"] != [] + assert prep.hooks["response"] == [hook] + + def test_session_hooks_are_overridden_by_request_hooks(self, httpbin): + def hook1(*args, **kwargs): + pass + + def hook2(*args, **kwargs): + pass + + assert hook1 is not hook2 + s = requests.Session() + s.hooks["response"].append(hook2) + r = requests.Request("GET", httpbin(), hooks={"response": [hook1]}) + prep = s.prepare_request(r) + assert prep.hooks["response"] == [hook1] + + def test_prepared_request_hook(self, httpbin): + def hook(resp, **kwargs): + resp.hook_working = True + return resp + + req = requests.Request("GET", httpbin(), hooks={"response": hook}) + prep = req.prepare() + + s = requests.Session() + s.proxies = getproxies() + resp = s.send(prep) + + assert hasattr(resp, "hook_working") + + def test_prepared_from_session(self, httpbin): + class DummyAuth(requests.auth.AuthBase): + def __call__(self, r): + r.headers["Dummy-Auth-Test"] = "dummy-auth-test-ok" + return r + + req = requests.Request("GET", httpbin("headers")) + assert not req.auth + + s = requests.Session() + s.auth = DummyAuth() + + prep = s.prepare_request(req) + resp = s.send(prep) + + assert resp.json()["headers"]["Dummy-Auth-Test"] == "dummy-auth-test-ok" + + def test_prepare_request_with_bytestring_url(self): + req = requests.Request("GET", b"https://httpbin.org/") + s = requests.Session() + prep = s.prepare_request(req) + assert prep.url == "https://httpbin.org/" + + def test_request_with_bytestring_host(self, httpbin): + s = requests.Session() + resp = s.request( + "GET", + httpbin("cookies/set?cookie=value"), + allow_redirects=False, + headers={"Host": b"httpbin.org"}, + ) + assert resp.cookies.get("cookie") == "value" + + def test_links(self): + r = requests.Response() + r.headers = { + "cache-control": "public, max-age=60, s-maxage=60", + "connection": "keep-alive", + "content-encoding": "gzip", + "content-type": "application/json; charset=utf-8", + "date": "Sat, 26 Jan 2013 16:47:56 GMT", + "etag": '"6ff6a73c0e446c1f61614769e3ceb778"', + "last-modified": "Sat, 26 Jan 2013 16:22:39 GMT", + "link": ( + "; rel="next", ; " + ' rel="last"' + ), + "server": "GitHub.com", + "status": "200 OK", + "vary": "Accept", + "x-content-type-options": "nosniff", + "x-github-media-type": "github.beta", + "x-ratelimit-limit": "60", + "x-ratelimit-remaining": "57", + } + assert r.links["next"]["rel"] == "next" + + def test_cookie_parameters(self): + key = "some_cookie" + value = "some_value" + secure = True + domain = "test.com" + rest = {"HttpOnly": True} + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value, secure=secure, domain=domain, rest=rest) + + assert len(jar) == 1 + assert "some_cookie" in jar + + cookie = list(jar)[0] + assert cookie.secure == secure + assert cookie.domain == domain + assert cookie._rest["HttpOnly"] == rest["HttpOnly"] + + def test_cookie_as_dict_keeps_len(self): + key = "some_cookie" + value = "some_value" + + key1 = "some_cookie1" + value1 = "some_value1" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value) + jar.set(key1, value1) + + d1 = dict(jar) + d2 = dict(jar.iteritems()) + d3 = dict(jar.items()) + + assert len(jar) == 2 + assert len(d1) == 2 + assert len(d2) == 2 + assert len(d3) == 2 + + def test_cookie_as_dict_keeps_items(self): + key = "some_cookie" + value = "some_value" + + key1 = "some_cookie1" + value1 = "some_value1" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value) + jar.set(key1, value1) + + d1 = dict(jar) + d2 = dict(jar.iteritems()) + d3 = dict(jar.items()) + + assert d1["some_cookie"] == "some_value" + assert d2["some_cookie"] == "some_value" + assert d3["some_cookie1"] == "some_value1" + + def test_cookie_as_dict_keys(self): + key = "some_cookie" + value = "some_value" + + key1 = "some_cookie1" + value1 = "some_value1" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value) + jar.set(key1, value1) + + keys = jar.keys() + assert keys == list(keys) + # make sure one can use keys multiple times + assert list(keys) == list(keys) + + def test_cookie_as_dict_values(self): + key = "some_cookie" + value = "some_value" + + key1 = "some_cookie1" + value1 = "some_value1" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value) + jar.set(key1, value1) + + values = jar.values() + assert values == list(values) + # make sure one can use values multiple times + assert list(values) == list(values) + + def test_cookie_as_dict_items(self): + key = "some_cookie" + value = "some_value" + + key1 = "some_cookie1" + value1 = "some_value1" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value) + jar.set(key1, value1) + + items = jar.items() + assert items == list(items) + # make sure one can use items multiple times + assert list(items) == list(items) + + def test_cookie_duplicate_names_different_domains(self): + key = "some_cookie" + value = "some_value" + domain1 = "test1.com" + domain2 = "test2.com" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value, domain=domain1) + jar.set(key, value, domain=domain2) + assert key in jar + items = jar.items() + assert len(items) == 2 + + # Verify that CookieConflictError is raised if domain is not specified + with pytest.raises(requests.cookies.CookieConflictError): + jar.get(key) + + # Verify that CookieConflictError is not raised if domain is specified + cookie = jar.get(key, domain=domain1) + assert cookie == value + + def test_cookie_duplicate_names_raises_cookie_conflict_error(self): + key = "some_cookie" + value = "some_value" + path = "some_path" + + jar = requests.cookies.RequestsCookieJar() + jar.set(key, value, path=path) + jar.set(key, value) + with pytest.raises(requests.cookies.CookieConflictError): + jar.get(key) + + def test_cookie_policy_copy(self): + class MyCookiePolicy(cookielib.DefaultCookiePolicy): + pass + + jar = requests.cookies.RequestsCookieJar() + jar.set_policy(MyCookiePolicy()) + assert isinstance(jar.copy().get_policy(), MyCookiePolicy) + + def test_time_elapsed_blank(self, httpbin): + r = requests.get(httpbin("get")) + td = r.elapsed + total_seconds = ( + td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6 + ) / 10**6 + assert total_seconds > 0.0 + + def test_empty_response_has_content_none(self): + r = requests.Response() + assert r.content is None + + def test_response_is_iterable(self): + r = requests.Response() + io = StringIO.StringIO("abc") + read_ = io.read + + def read_mock(amt, decode_content=None): + return read_(amt) + + setattr(io, "read", read_mock) + r.raw = io + assert next(iter(r)) + io.close() + + def test_response_decode_unicode(self): + """When called with decode_unicode, Response.iter_content should always + return unicode. + """ + r = requests.Response() + r._content_consumed = True + r._content = b"the content" + r.encoding = "ascii" + + chunks = r.iter_content(decode_unicode=True) + assert all(isinstance(chunk, str) for chunk in chunks) + + # also for streaming + r = requests.Response() + r.raw = io.BytesIO(b"the content") + r.encoding = "ascii" + chunks = r.iter_content(decode_unicode=True) + assert all(isinstance(chunk, str) for chunk in chunks) + + def test_response_reason_unicode(self): + # check for unicode HTTP status + r = requests.Response() + r.url = "unicode URL" + r.reason = "Komponenttia ei löydy".encode() + r.status_code = 404 + r.encoding = None + assert not r.ok # old behaviour - crashes here + + def test_response_reason_unicode_fallback(self): + # check raise_status falls back to ISO-8859-1 + r = requests.Response() + r.url = "some url" + reason = "Komponenttia ei löydy" + r.reason = reason.encode("latin-1") + r.status_code = 500 + r.encoding = None + with pytest.raises(requests.exceptions.HTTPError) as e: + r.raise_for_status() + assert reason in e.value.args[0] + + def test_response_chunk_size_type(self): + """Ensure that chunk_size is passed as None or an integer, otherwise + raise a TypeError. + """ + r = requests.Response() + r.raw = io.BytesIO(b"the content") + chunks = r.iter_content(1) + assert all(len(chunk) == 1 for chunk in chunks) + + r = requests.Response() + r.raw = io.BytesIO(b"the content") + chunks = r.iter_content(None) + assert list(chunks) == [b"the content"] + + r = requests.Response() + r.raw = io.BytesIO(b"the content") + with pytest.raises(TypeError): + chunks = r.iter_content("1024") + + @pytest.mark.parametrize( + "exception, args, expected", + ( + (urllib3.exceptions.ProtocolError, tuple(), ChunkedEncodingError), + (urllib3.exceptions.DecodeError, tuple(), ContentDecodingError), + (urllib3.exceptions.ReadTimeoutError, (None, "", ""), ConnectionError), + (urllib3.exceptions.SSLError, tuple(), RequestsSSLError), + ), + ) + def test_iter_content_wraps_exceptions( + self, httpbin, mocker, exception, args, expected + ): + r = requests.Response() + r.raw = mocker.Mock() + # ReadTimeoutError can't be initialized by mock + # so we'll manually create the instance with args + r.raw.stream.side_effect = exception(*args) + + with pytest.raises(expected): + next(r.iter_content(1024)) + + def test_request_and_response_are_pickleable(self, httpbin): + r = requests.get(httpbin("get")) + + # verify we can pickle the original request + assert pickle.loads(pickle.dumps(r.request)) + + # verify we can pickle the response and that we have access to + # the original request. + pr = pickle.loads(pickle.dumps(r)) + assert r.request.url == pr.request.url + assert r.request.headers == pr.request.headers + + def test_prepared_request_is_pickleable(self, httpbin): + p = requests.Request("GET", httpbin("get")).prepare() + + # Verify PreparedRequest can be pickled and unpickled + r = pickle.loads(pickle.dumps(p)) + assert r.url == p.url + assert r.headers == p.headers + assert r.body == p.body + + # Verify unpickled PreparedRequest sends properly + s = requests.Session() + resp = s.send(r) + assert resp.status_code == 200 + + def test_prepared_request_with_file_is_pickleable(self, httpbin): + with open(__file__, "rb") as f: + r = requests.Request("POST", httpbin("post"), files={"file": f}) + p = r.prepare() + + # Verify PreparedRequest can be pickled and unpickled + r = pickle.loads(pickle.dumps(p)) + assert r.url == p.url + assert r.headers == p.headers + assert r.body == p.body + + # Verify unpickled PreparedRequest sends properly + s = requests.Session() + resp = s.send(r) + assert resp.status_code == 200 + + def test_prepared_request_with_hook_is_pickleable(self, httpbin): + r = requests.Request("GET", httpbin("get"), hooks=default_hooks()) + p = r.prepare() + + # Verify PreparedRequest can be pickled + r = pickle.loads(pickle.dumps(p)) + assert r.url == p.url + assert r.headers == p.headers + assert r.body == p.body + assert r.hooks == p.hooks + + # Verify unpickled PreparedRequest sends properly + s = requests.Session() + resp = s.send(r) + assert resp.status_code == 200 + + def test_cannot_send_unprepared_requests(self, httpbin): + r = requests.Request(url=httpbin()) + with pytest.raises(ValueError): + requests.Session().send(r) + + def test_http_error(self): + error = requests.exceptions.HTTPError() + assert not error.response + response = requests.Response() + error = requests.exceptions.HTTPError(response=response) + assert error.response == response + error = requests.exceptions.HTTPError("message", response=response) + assert str(error) == "message" + assert error.response == response + + def test_session_pickling(self, httpbin): + r = requests.Request("GET", httpbin("get")) + s = requests.Session() + + s = pickle.loads(pickle.dumps(s)) + s.proxies = getproxies() + + r = s.send(r.prepare()) + assert r.status_code == 200 + + def test_fixes_1329(self, httpbin): + """Ensure that header updates are done case-insensitively.""" + s = requests.Session() + s.headers.update({"ACCEPT": "BOGUS"}) + s.headers.update({"accept": "application/json"}) + r = s.get(httpbin("get")) + headers = r.request.headers + assert headers["accept"] == "application/json" + assert headers["Accept"] == "application/json" + assert headers["ACCEPT"] == "application/json" + + def test_uppercase_scheme_redirect(self, httpbin): + parts = urlparse(httpbin("html")) + url = "HTTP://" + parts.netloc + parts.path + r = requests.get(httpbin("redirect-to"), params={"url": url}) + assert r.status_code == 200 + assert r.url.lower() == url.lower() + + def test_transport_adapter_ordering(self): + s = requests.Session() + order = ["https://", "http://"] + assert order == list(s.adapters) + s.mount("http://git", HTTPAdapter()) + s.mount("http://github", HTTPAdapter()) + s.mount("http://github.com", HTTPAdapter()) + s.mount("http://github.com/about/", HTTPAdapter()) + order = [ + "http://github.com/about/", + "http://github.com", + "http://github", + "http://git", + "https://", + "http://", + ] + assert order == list(s.adapters) + s.mount("http://gittip", HTTPAdapter()) + s.mount("http://gittip.com", HTTPAdapter()) + s.mount("http://gittip.com/about/", HTTPAdapter()) + order = [ + "http://github.com/about/", + "http://gittip.com/about/", + "http://github.com", + "http://gittip.com", + "http://github", + "http://gittip", + "http://git", + "https://", + "http://", + ] + assert order == list(s.adapters) + s2 = requests.Session() + s2.adapters = {"http://": HTTPAdapter()} + s2.mount("https://", HTTPAdapter()) + assert "http://" in s2.adapters + assert "https://" in s2.adapters + + def test_session_get_adapter_prefix_matching(self): + prefix = "https://example.com" + more_specific_prefix = prefix + "/some/path" + + url_matching_only_prefix = prefix + "/another/path" + url_matching_more_specific_prefix = more_specific_prefix + "/longer/path" + url_not_matching_prefix = "https://another.example.com/" + + s = requests.Session() + prefix_adapter = HTTPAdapter() + more_specific_prefix_adapter = HTTPAdapter() + s.mount(prefix, prefix_adapter) + s.mount(more_specific_prefix, more_specific_prefix_adapter) + + assert s.get_adapter(url_matching_only_prefix) is prefix_adapter + assert ( + s.get_adapter(url_matching_more_specific_prefix) + is more_specific_prefix_adapter + ) + assert s.get_adapter(url_not_matching_prefix) not in ( + prefix_adapter, + more_specific_prefix_adapter, + ) + + def test_session_get_adapter_prefix_matching_mixed_case(self): + mixed_case_prefix = "hTtPs://eXamPle.CoM/MixEd_CAse_PREfix" + url_matching_prefix = mixed_case_prefix + "/full_url" + + s = requests.Session() + my_adapter = HTTPAdapter() + s.mount(mixed_case_prefix, my_adapter) + + assert s.get_adapter(url_matching_prefix) is my_adapter + + def test_session_get_adapter_prefix_matching_is_case_insensitive(self): + mixed_case_prefix = "hTtPs://eXamPle.CoM/MixEd_CAse_PREfix" + url_matching_prefix_with_different_case = ( + "HtTpS://exaMPLe.cOm/MiXeD_caSE_preFIX/another_url" + ) + + s = requests.Session() + my_adapter = HTTPAdapter() + s.mount(mixed_case_prefix, my_adapter) + + assert s.get_adapter(url_matching_prefix_with_different_case) is my_adapter + + def test_header_remove_is_case_insensitive(self, httpbin): + # From issue #1321 + s = requests.Session() + s.headers["foo"] = "bar" + r = s.get(httpbin("get"), headers={"FOO": None}) + assert "foo" not in r.request.headers + + def test_params_are_merged_case_sensitive(self, httpbin): + s = requests.Session() + s.params["foo"] = "bar" + r = s.get(httpbin("get"), params={"FOO": "bar"}) + assert r.json()["args"] == {"foo": "bar", "FOO": "bar"} + + def test_long_authinfo_in_url(self): + url = "http://{}:{}@{}:9000/path?query#frag".format( + "E8A3BE87-9E3F-4620-8858-95478E385B5B", + "EA770032-DA4D-4D84-8CE9-29C6D910BF1E", + "exactly-------------sixty-----------three------------characters", + ) + r = requests.Request("GET", url).prepare() + assert r.url == url + + def test_header_keys_are_native(self, httpbin): + headers = {"unicode": "blah", b"byte": "blah"} + r = requests.Request("GET", httpbin("get"), headers=headers) + p = r.prepare() + + # This is testing that they are builtin strings. A bit weird, but there + # we go. + assert "unicode" in p.headers.keys() + assert "byte" in p.headers.keys() + + def test_header_validation(self, httpbin): + """Ensure prepare_headers regex isn't flagging valid header contents.""" + valid_headers = { + "foo": "bar baz qux", + "bar": b"fbbq", + "baz": "", + "qux": "1", + } + r = requests.get(httpbin("get"), headers=valid_headers) + for key in valid_headers.keys(): + valid_headers[key] == r.request.headers[key] + + @pytest.mark.parametrize( + "invalid_header, key", + ( + ({"foo": 3}, "foo"), + ({"bar": {"foo": "bar"}}, "bar"), + ({"baz": ["foo", "bar"]}, "baz"), + ), + ) + def test_header_value_not_str(self, httpbin, invalid_header, key): + """Ensure the header value is of type string or bytes as + per discussion in GH issue #3386 + """ + with pytest.raises(InvalidHeader) as excinfo: + requests.get(httpbin("get"), headers=invalid_header) + assert key in str(excinfo.value) + + @pytest.mark.parametrize( + "invalid_header", + ( + {"foo": "bar\r\nbaz: qux"}, + {"foo": "bar\n\rbaz: qux"}, + {"foo": "bar\nbaz: qux"}, + {"foo": "bar\rbaz: qux"}, + {"fo\ro": "bar"}, + {"fo\r\no": "bar"}, + {"fo\n\ro": "bar"}, + {"fo\no": "bar"}, + ), + ) + def test_header_no_return_chars(self, httpbin, invalid_header): + """Ensure that a header containing return character sequences raise an + exception. Otherwise, multiple headers are created from single string. + """ + with pytest.raises(InvalidHeader): + requests.get(httpbin("get"), headers=invalid_header) + + @pytest.mark.parametrize( + "invalid_header", + ( + {" foo": "bar"}, + {"\tfoo": "bar"}, + {" foo": "bar"}, + {"foo": " bar"}, + {"foo": " bar"}, + {"foo": "\tbar"}, + {" ": "bar"}, + ), + ) + def test_header_no_leading_space(self, httpbin, invalid_header): + """Ensure headers containing leading whitespace raise + InvalidHeader Error before sending. + """ + with pytest.raises(InvalidHeader): + requests.get(httpbin("get"), headers=invalid_header) + + def test_header_with_subclass_types(self, httpbin): + """If the subclasses does not behave *exactly* like + the base bytes/str classes, this is not supported. + This test is for backwards compatibility. + """ + + class MyString(str): + pass + + class MyBytes(bytes): + pass + + r_str = requests.get(httpbin("get"), headers={MyString("x-custom"): "myheader"}) + assert r_str.request.headers["x-custom"] == "myheader" + + r_bytes = requests.get( + httpbin("get"), headers={MyBytes(b"x-custom"): b"myheader"} + ) + assert r_bytes.request.headers["x-custom"] == b"myheader" + + r_mixed = requests.get( + httpbin("get"), headers={MyString("x-custom"): MyBytes(b"myheader")} + ) + assert r_mixed.request.headers["x-custom"] == b"myheader" + + @pytest.mark.parametrize("files", ("foo", b"foo", bytearray(b"foo"))) + def test_can_send_objects_with_files(self, httpbin, files): + data = {"a": "this is a string"} + files = {"b": files} + r = requests.Request("POST", httpbin("post"), data=data, files=files) + p = r.prepare() + assert "multipart/form-data" in p.headers["Content-Type"] + + def test_can_send_file_object_with_non_string_filename(self, httpbin): + f = io.BytesIO() + f.name = 2 + r = requests.Request("POST", httpbin("post"), files={"f": f}) + p = r.prepare() + + assert "multipart/form-data" in p.headers["Content-Type"] + + def test_autoset_header_values_are_native(self, httpbin): + data = "this is a string" + length = "16" + req = requests.Request("POST", httpbin("post"), data=data) + p = req.prepare() + + assert p.headers["Content-Length"] == length + + def test_nonhttp_schemes_dont_check_URLs(self): + test_urls = ( + "data:image/gif;base64,R0lGODlhAQABAHAAACH5BAUAAAAALAAAAAABAAEAAAICRAEAOw==", + "file:///etc/passwd", + "magnet:?xt=urn:btih:be08f00302bc2d1d3cfa3af02024fa647a271431", + ) + for test_url in test_urls: + req = requests.Request("GET", test_url) + preq = req.prepare() + assert test_url == preq.url + + def test_auth_is_stripped_on_http_downgrade( + self, httpbin, httpbin_secure, httpbin_ca_bundle + ): + r = requests.get( + httpbin_secure("redirect-to"), + params={"url": httpbin("get")}, + auth=("user", "pass"), + verify=httpbin_ca_bundle, + ) + assert r.history[0].request.headers["Authorization"] + assert "Authorization" not in r.request.headers + + def test_auth_is_retained_for_redirect_on_host(self, httpbin): + r = requests.get(httpbin("redirect/1"), auth=("user", "pass")) + h1 = r.history[0].request.headers["Authorization"] + h2 = r.request.headers["Authorization"] + + assert h1 == h2 + + def test_should_strip_auth_host_change(self): + s = requests.Session() + assert s.should_strip_auth( + "http://example.com/foo", "http://another.example.com/" + ) + + def test_should_strip_auth_http_downgrade(self): + s = requests.Session() + assert s.should_strip_auth("https://example.com/foo", "http://example.com/bar") + + def test_should_strip_auth_https_upgrade(self): + s = requests.Session() + assert not s.should_strip_auth( + "http://example.com/foo", "https://example.com/bar" + ) + assert not s.should_strip_auth( + "http://example.com:80/foo", "https://example.com/bar" + ) + assert not s.should_strip_auth( + "http://example.com/foo", "https://example.com:443/bar" + ) + # Non-standard ports should trigger stripping + assert s.should_strip_auth( + "http://example.com:8080/foo", "https://example.com/bar" + ) + assert s.should_strip_auth( + "http://example.com/foo", "https://example.com:8443/bar" + ) + + def test_should_strip_auth_port_change(self): + s = requests.Session() + assert s.should_strip_auth( + "http://example.com:1234/foo", "https://example.com:4321/bar" + ) + + @pytest.mark.parametrize( + "old_uri, new_uri", + ( + ("https://example.com:443/foo", "https://example.com/bar"), + ("http://example.com:80/foo", "http://example.com/bar"), + ("https://example.com/foo", "https://example.com:443/bar"), + ("http://example.com/foo", "http://example.com:80/bar"), + ), + ) + def test_should_strip_auth_default_port(self, old_uri, new_uri): + s = requests.Session() + assert not s.should_strip_auth(old_uri, new_uri) + + def test_manual_redirect_with_partial_body_read(self, httpbin): + s = requests.Session() + r1 = s.get(httpbin("redirect/2"), allow_redirects=False, stream=True) + assert r1.is_redirect + rg = s.resolve_redirects(r1, r1.request, stream=True) + + # read only the first eight bytes of the response body, + # then follow the redirect + r1.iter_content(8) + r2 = next(rg) + assert r2.is_redirect + + # read all of the response via iter_content, + # then follow the redirect + for _ in r2.iter_content(): + pass + r3 = next(rg) + assert not r3.is_redirect + + def test_prepare_body_position_non_stream(self): + data = b"the data" + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position is None + + def test_rewind_body(self): + data = io.BytesIO(b"the data") + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position == 0 + assert prep.body.read() == b"the data" + + # the data has all been read + assert prep.body.read() == b"" + + # rewind it back + requests.utils.rewind_body(prep) + assert prep.body.read() == b"the data" + + def test_rewind_partially_read_body(self): + data = io.BytesIO(b"the data") + data.read(4) # read some data + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position == 4 + assert prep.body.read() == b"data" + + # the data has all been read + assert prep.body.read() == b"" + + # rewind it back + requests.utils.rewind_body(prep) + assert prep.body.read() == b"data" + + def test_rewind_body_no_seek(self): + class BadFileObj: + def __init__(self, data): + self.data = data + + def tell(self): + return 0 + + def __iter__(self): + return + + data = BadFileObj("the data") + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position == 0 + + with pytest.raises(UnrewindableBodyError) as e: + requests.utils.rewind_body(prep) + + assert "Unable to rewind request body" in str(e) + + def test_rewind_body_failed_seek(self): + class BadFileObj: + def __init__(self, data): + self.data = data + + def tell(self): + return 0 + + def seek(self, pos, whence=0): + raise OSError() + + def __iter__(self): + return + + data = BadFileObj("the data") + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position == 0 + + with pytest.raises(UnrewindableBodyError) as e: + requests.utils.rewind_body(prep) + + assert "error occurred when rewinding request body" in str(e) + + def test_rewind_body_failed_tell(self): + class BadFileObj: + def __init__(self, data): + self.data = data + + def tell(self): + raise OSError() + + def __iter__(self): + return + + data = BadFileObj("the data") + prep = requests.Request("GET", "http://example.com", data=data).prepare() + assert prep._body_position is not None + + with pytest.raises(UnrewindableBodyError) as e: + requests.utils.rewind_body(prep) + + assert "Unable to rewind request body" in str(e) + + def _patch_adapter_gzipped_redirect(self, session, url): + adapter = session.get_adapter(url=url) + org_build_response = adapter.build_response + self._patched_response = False + + def build_response(*args, **kwargs): + resp = org_build_response(*args, **kwargs) + if not self._patched_response: + resp.raw.headers["content-encoding"] = "gzip" + self._patched_response = True + return resp + + adapter.build_response = build_response + + def test_redirect_with_wrong_gzipped_header(self, httpbin): + s = requests.Session() + url = httpbin("redirect/1") + self._patch_adapter_gzipped_redirect(s, url) + s.get(url) + + @pytest.mark.parametrize( + "username, password, auth_str", + ( + ("test", "test", "Basic dGVzdDp0ZXN0"), + ( + "имя".encode(), + "пароль".encode(), + "Basic 0LjQvNGPOtC/0LDRgNC+0LvRjA==", + ), + ), + ) + def test_basic_auth_str_is_always_native(self, username, password, auth_str): + s = _basic_auth_str(username, password) + assert isinstance(s, builtin_str) + assert s == auth_str + + def test_requests_history_is_saved(self, httpbin): + r = requests.get(httpbin("redirect/5")) + total = r.history[-1].history + i = 0 + for item in r.history: + assert item.history == total[0:i] + i += 1 + + def test_json_param_post_content_type_works(self, httpbin): + r = requests.post(httpbin("post"), json={"life": 42}) + assert r.status_code == 200 + assert "application/json" in r.request.headers["Content-Type"] + assert {"life": 42} == r.json()["json"] + + def test_json_param_post_should_not_override_data_param(self, httpbin): + r = requests.Request( + method="POST", + url=httpbin("post"), + data={"stuff": "elixr"}, + json={"music": "flute"}, + ) + prep = r.prepare() + assert "stuff=elixr" == prep.body + + def test_response_iter_lines(self, httpbin): + r = requests.get(httpbin("stream/4"), stream=True) + assert r.status_code == 200 + + it = r.iter_lines() + next(it) + assert len(list(it)) == 3 + + def test_response_context_manager(self, httpbin): + with requests.get(httpbin("stream/4"), stream=True) as response: + assert isinstance(response, requests.Response) + + assert response.raw.closed + + def test_unconsumed_session_response_closes_connection(self, httpbin): + s = requests.session() + + with contextlib.closing(s.get(httpbin("stream/4"), stream=True)) as response: + pass + + assert response._content_consumed is False + assert response.raw.closed + + @pytest.mark.xfail + def test_response_iter_lines_reentrant(self, httpbin): + """Response.iter_lines() is not reentrant safe""" + r = requests.get(httpbin("stream/4"), stream=True) + assert r.status_code == 200 + + next(r.iter_lines()) + assert len(list(r.iter_lines())) == 3 + + def test_session_close_proxy_clear(self, mocker): + proxies = { + "one": mocker.Mock(), + "two": mocker.Mock(), + } + session = requests.Session() + mocker.patch.dict(session.adapters["http://"].proxy_manager, proxies) + session.close() + proxies["one"].clear.assert_called_once_with() + proxies["two"].clear.assert_called_once_with() + + def test_proxy_auth(self): + adapter = HTTPAdapter() + headers = adapter.proxy_headers("http://user:pass@httpbin.org") + assert headers == {"Proxy-Authorization": "Basic dXNlcjpwYXNz"} + + def test_proxy_auth_empty_pass(self): + adapter = HTTPAdapter() + headers = adapter.proxy_headers("http://user:@httpbin.org") + assert headers == {"Proxy-Authorization": "Basic dXNlcjo="} + + def test_response_json_when_content_is_None(self, httpbin): + r = requests.get(httpbin("/status/204")) + # Make sure r.content is None + r.status_code = 0 + r._content = False + r._content_consumed = False + + assert r.content is None + with pytest.raises(ValueError): + r.json() + + def test_response_without_release_conn(self): + """Test `close` call for non-urllib3-like raw objects. + Should work when `release_conn` attr doesn't exist on `response.raw`. + """ + resp = requests.Response() + resp.raw = StringIO.StringIO("test") + assert not resp.raw.closed + resp.close() + assert resp.raw.closed + + def test_empty_stream_with_auth_does_not_set_content_length_header(self, httpbin): + """Ensure that a byte stream with size 0 will not set both a Content-Length + and Transfer-Encoding header. + """ + auth = ("user", "pass") + url = httpbin("post") + file_obj = io.BytesIO(b"") + r = requests.Request("POST", url, auth=auth, data=file_obj) + prepared_request = r.prepare() + assert "Transfer-Encoding" in prepared_request.headers + assert "Content-Length" not in prepared_request.headers + + def test_stream_with_auth_does_not_set_transfer_encoding_header(self, httpbin): + """Ensure that a byte stream with size > 0 will not set both a Content-Length + and Transfer-Encoding header. + """ + auth = ("user", "pass") + url = httpbin("post") + file_obj = io.BytesIO(b"test data") + r = requests.Request("POST", url, auth=auth, data=file_obj) + prepared_request = r.prepare() + assert "Transfer-Encoding" not in prepared_request.headers + assert "Content-Length" in prepared_request.headers + + def test_chunked_upload_does_not_set_content_length_header(self, httpbin): + """Ensure that requests with a generator body stream using + Transfer-Encoding: chunked, not a Content-Length header. + """ + data = (i for i in [b"a", b"b", b"c"]) + url = httpbin("post") + r = requests.Request("POST", url, data=data) + prepared_request = r.prepare() + assert "Transfer-Encoding" in prepared_request.headers + assert "Content-Length" not in prepared_request.headers + + def test_custom_redirect_mixin(self, httpbin): + """Tests a custom mixin to overwrite ``get_redirect_target``. + + Ensures a subclassed ``requests.Session`` can handle a certain type of + malformed redirect responses. + + 1. original request receives a proper response: 302 redirect + 2. following the redirect, a malformed response is given: + status code = HTTP 200 + location = alternate url + 3. the custom session catches the edge case and follows the redirect + """ + url_final = httpbin("html") + querystring_malformed = urlencode({"location": url_final}) + url_redirect_malformed = httpbin("response-headers?%s" % querystring_malformed) + querystring_redirect = urlencode({"url": url_redirect_malformed}) + url_redirect = httpbin("redirect-to?%s" % querystring_redirect) + urls_test = [ + url_redirect, + url_redirect_malformed, + url_final, + ] + + class CustomRedirectSession(requests.Session): + def get_redirect_target(self, resp): + # default behavior + if resp.is_redirect: + return resp.headers["location"] + # edge case - check to see if 'location' is in headers anyways + location = resp.headers.get("location") + if location and (location != resp.url): + return location + return None + + session = CustomRedirectSession() + r = session.get(urls_test[0]) + assert len(r.history) == 2 + assert r.status_code == 200 + assert r.history[0].status_code == 302 + assert r.history[0].is_redirect + assert r.history[1].status_code == 200 + assert not r.history[1].is_redirect + assert r.url == urls_test[2] + + +class TestCaseInsensitiveDict: + @pytest.mark.parametrize( + "cid", + ( + CaseInsensitiveDict({"Foo": "foo", "BAr": "bar"}), + CaseInsensitiveDict([("Foo", "foo"), ("BAr", "bar")]), + CaseInsensitiveDict(FOO="foo", BAr="bar"), + ), + ) + def test_init(self, cid): + assert len(cid) == 2 + assert "foo" in cid + assert "bar" in cid + + def test_docstring_example(self): + cid = CaseInsensitiveDict() + cid["Accept"] = "application/json" + assert cid["aCCEPT"] == "application/json" + assert list(cid) == ["Accept"] + + def test_len(self): + cid = CaseInsensitiveDict({"a": "a", "b": "b"}) + cid["A"] = "a" + assert len(cid) == 2 + + def test_getitem(self): + cid = CaseInsensitiveDict({"Spam": "blueval"}) + assert cid["spam"] == "blueval" + assert cid["SPAM"] == "blueval" + + def test_fixes_649(self): + """__setitem__ should behave case-insensitively.""" + cid = CaseInsensitiveDict() + cid["spam"] = "oneval" + cid["Spam"] = "twoval" + cid["sPAM"] = "redval" + cid["SPAM"] = "blueval" + assert cid["spam"] == "blueval" + assert cid["SPAM"] == "blueval" + assert list(cid.keys()) == ["SPAM"] + + def test_delitem(self): + cid = CaseInsensitiveDict() + cid["Spam"] = "someval" + del cid["sPam"] + assert "spam" not in cid + assert len(cid) == 0 + + def test_contains(self): + cid = CaseInsensitiveDict() + cid["Spam"] = "someval" + assert "Spam" in cid + assert "spam" in cid + assert "SPAM" in cid + assert "sPam" in cid + assert "notspam" not in cid + + def test_get(self): + cid = CaseInsensitiveDict() + cid["spam"] = "oneval" + cid["SPAM"] = "blueval" + assert cid.get("spam") == "blueval" + assert cid.get("SPAM") == "blueval" + assert cid.get("sPam") == "blueval" + assert cid.get("notspam", "default") == "default" + + def test_update(self): + cid = CaseInsensitiveDict() + cid["spam"] = "blueval" + cid.update({"sPam": "notblueval"}) + assert cid["spam"] == "notblueval" + cid = CaseInsensitiveDict({"Foo": "foo", "BAr": "bar"}) + cid.update({"fOO": "anotherfoo", "bAR": "anotherbar"}) + assert len(cid) == 2 + assert cid["foo"] == "anotherfoo" + assert cid["bar"] == "anotherbar" + + def test_update_retains_unchanged(self): + cid = CaseInsensitiveDict({"foo": "foo", "bar": "bar"}) + cid.update({"foo": "newfoo"}) + assert cid["bar"] == "bar" + + def test_iter(self): + cid = CaseInsensitiveDict({"Spam": "spam", "Eggs": "eggs"}) + keys = frozenset(["Spam", "Eggs"]) + assert frozenset(iter(cid)) == keys + + def test_equality(self): + cid = CaseInsensitiveDict({"SPAM": "blueval", "Eggs": "redval"}) + othercid = CaseInsensitiveDict({"spam": "blueval", "eggs": "redval"}) + assert cid == othercid + del othercid["spam"] + assert cid != othercid + assert cid == {"spam": "blueval", "eggs": "redval"} + assert cid != object() + + def test_setdefault(self): + cid = CaseInsensitiveDict({"Spam": "blueval"}) + assert cid.setdefault("spam", "notblueval") == "blueval" + assert cid.setdefault("notspam", "notblueval") == "notblueval" + + def test_lower_items(self): + cid = CaseInsensitiveDict( + { + "Accept": "application/json", + "user-Agent": "requests", + } + ) + keyset = frozenset(lowerkey for lowerkey, v in cid.lower_items()) + lowerkeyset = frozenset(["accept", "user-agent"]) + assert keyset == lowerkeyset + + def test_preserve_key_case(self): + cid = CaseInsensitiveDict( + { + "Accept": "application/json", + "user-Agent": "requests", + } + ) + keyset = frozenset(["Accept", "user-Agent"]) + assert frozenset(i[0] for i in cid.items()) == keyset + assert frozenset(cid.keys()) == keyset + assert frozenset(cid) == keyset + + def test_preserve_last_key_case(self): + cid = CaseInsensitiveDict( + { + "Accept": "application/json", + "user-Agent": "requests", + } + ) + cid.update({"ACCEPT": "application/json"}) + cid["USER-AGENT"] = "requests" + keyset = frozenset(["ACCEPT", "USER-AGENT"]) + assert frozenset(i[0] for i in cid.items()) == keyset + assert frozenset(cid.keys()) == keyset + assert frozenset(cid) == keyset + + def test_copy(self): + cid = CaseInsensitiveDict( + { + "Accept": "application/json", + "user-Agent": "requests", + } + ) + cid_copy = cid.copy() + assert cid == cid_copy + cid["changed"] = True + assert cid != cid_copy + + +class TestMorselToCookieExpires: + """Tests for morsel_to_cookie when morsel contains expires.""" + + def test_expires_valid_str(self): + """Test case where we convert expires from string time.""" + + morsel = Morsel() + morsel["expires"] = "Thu, 01-Jan-1970 00:00:01 GMT" + cookie = morsel_to_cookie(morsel) + assert cookie.expires == 1 + + @pytest.mark.parametrize( + "value, exception", + ( + (100, TypeError), + ("woops", ValueError), + ), + ) + def test_expires_invalid_int(self, value, exception): + """Test case where an invalid type is passed for expires.""" + morsel = Morsel() + morsel["expires"] = value + with pytest.raises(exception): + morsel_to_cookie(morsel) + + def test_expires_none(self): + """Test case where expires is None.""" + + morsel = Morsel() + morsel["expires"] = None + cookie = morsel_to_cookie(morsel) + assert cookie.expires is None + + +class TestMorselToCookieMaxAge: + + """Tests for morsel_to_cookie when morsel contains max-age.""" + + def test_max_age_valid_int(self): + """Test case where a valid max age in seconds is passed.""" + + morsel = Morsel() + morsel["max-age"] = 60 + cookie = morsel_to_cookie(morsel) + assert isinstance(cookie.expires, int) + + def test_max_age_invalid_str(self): + """Test case where a invalid max age is passed.""" + + morsel = Morsel() + morsel["max-age"] = "woops" + with pytest.raises(TypeError): + morsel_to_cookie(morsel) + + +class TestTimeout: + def test_stream_timeout(self, httpbin): + try: + requests.get(httpbin("delay/10"), timeout=2.0) + except requests.exceptions.Timeout as e: + assert "Read timed out" in e.args[0].args[0] + + @pytest.mark.parametrize( + "timeout, error_text", + ( + ((3, 4, 5), "(connect, read)"), + ("foo", "must be an int, float or None"), + ), + ) + def test_invalid_timeout(self, httpbin, timeout, error_text): + with pytest.raises(ValueError) as e: + requests.get(httpbin("get"), timeout=timeout) + assert error_text in str(e) + + @pytest.mark.parametrize("timeout", (None, Urllib3Timeout(connect=None, read=None))) + def test_none_timeout(self, httpbin, timeout): + """Check that you can set None as a valid timeout value. + + To actually test this behavior, we'd want to check that setting the + timeout to None actually lets the request block past the system default + timeout. However, this would make the test suite unbearably slow. + Instead we verify that setting the timeout to None does not prevent the + request from succeeding. + """ + r = requests.get(httpbin("get"), timeout=timeout) + assert r.status_code == 200 + + @pytest.mark.parametrize( + "timeout", ((None, 0.1), Urllib3Timeout(connect=None, read=0.1)) + ) + def test_read_timeout(self, httpbin, timeout): + try: + requests.get(httpbin("delay/10"), timeout=timeout) + pytest.fail("The recv() request should time out.") + except ReadTimeout: + pass + + @pytest.mark.parametrize( + "timeout", ((0.1, None), Urllib3Timeout(connect=0.1, read=None)) + ) + def test_connect_timeout(self, timeout): + try: + requests.get(TARPIT, timeout=timeout) + pytest.fail("The connect() request should time out.") + except ConnectTimeout as e: + assert isinstance(e, ConnectionError) + assert isinstance(e, Timeout) + + @pytest.mark.parametrize( + "timeout", ((0.1, 0.1), Urllib3Timeout(connect=0.1, read=0.1)) + ) + def test_total_timeout_connect(self, timeout): + try: + requests.get(TARPIT, timeout=timeout) + pytest.fail("The connect() request should time out.") + except ConnectTimeout: + pass + + def test_encoded_methods(self, httpbin): + """See: https://github.com/psf/requests/issues/2316""" + r = requests.request(b"GET", httpbin("get")) + assert r.ok + + +SendCall = collections.namedtuple("SendCall", ("args", "kwargs")) + + +class RedirectSession(SessionRedirectMixin): + def __init__(self, order_of_redirects): + self.redirects = order_of_redirects + self.calls = [] + self.max_redirects = 30 + self.cookies = {} + self.trust_env = False + + def send(self, *args, **kwargs): + self.calls.append(SendCall(args, kwargs)) + return self.build_response() + + def build_response(self): + request = self.calls[-1].args[0] + r = requests.Response() + + try: + r.status_code = int(self.redirects.pop(0)) + except IndexError: + r.status_code = 200 + + r.headers = CaseInsensitiveDict({"Location": "/"}) + r.raw = self._build_raw() + r.request = request + return r + + def _build_raw(self): + string = StringIO.StringIO("") + setattr(string, "release_conn", lambda *args: args) + return string + + +def test_json_encodes_as_bytes(): + # urllib3 expects bodies as bytes-like objects + body = {"key": "value"} + p = PreparedRequest() + p.prepare(method="GET", url="https://www.example.com/", json=body) + assert isinstance(p.body, bytes) + + +def test_requests_are_updated_each_time(httpbin): + session = RedirectSession([303, 307]) + prep = requests.Request("POST", httpbin("post")).prepare() + r0 = session.send(prep) + assert r0.request.method == "POST" + assert session.calls[-1] == SendCall((r0.request,), {}) + redirect_generator = session.resolve_redirects(r0, prep) + default_keyword_args = { + "stream": False, + "verify": True, + "cert": None, + "timeout": None, + "allow_redirects": False, + "proxies": {}, + } + for response in redirect_generator: + assert response.request.method == "GET" + send_call = SendCall((response.request,), default_keyword_args) + assert session.calls[-1] == send_call + + +@pytest.mark.parametrize( + "var,url,proxy", + [ + ("http_proxy", "http://example.com", "socks5://proxy.com:9876"), + ("https_proxy", "https://example.com", "socks5://proxy.com:9876"), + ("all_proxy", "http://example.com", "socks5://proxy.com:9876"), + ("all_proxy", "https://example.com", "socks5://proxy.com:9876"), + ], +) +def test_proxy_env_vars_override_default(var, url, proxy): + session = requests.Session() + prep = PreparedRequest() + prep.prepare(method="GET", url=url) + + kwargs = {var: proxy} + scheme = urlparse(url).scheme + with override_environ(**kwargs): + proxies = session.rebuild_proxies(prep, {}) + assert scheme in proxies + assert proxies[scheme] == proxy + + +@pytest.mark.parametrize( + "data", + ( + (("a", "b"), ("c", "d")), + (("c", "d"), ("a", "b")), + (("a", "b"), ("c", "d"), ("e", "f")), + ), +) +def test_data_argument_accepts_tuples(data): + """Ensure that the data argument will accept tuples of strings + and properly encode them. + """ + p = PreparedRequest() + p.prepare( + method="GET", url="http://www.example.com", data=data, hooks=default_hooks() + ) + assert p.body == urlencode(data) + + +@pytest.mark.parametrize( + "kwargs", + ( + None, + { + "method": "GET", + "url": "http://www.example.com", + "data": "foo=bar", + "hooks": default_hooks(), + }, + { + "method": "GET", + "url": "http://www.example.com", + "data": "foo=bar", + "hooks": default_hooks(), + "cookies": {"foo": "bar"}, + }, + {"method": "GET", "url": "http://www.example.com/üniçø∂é"}, + ), +) +def test_prepared_copy(kwargs): + p = PreparedRequest() + if kwargs: + p.prepare(**kwargs) + copy = p.copy() + for attr in ("method", "url", "headers", "_cookies", "body", "hooks"): + assert getattr(p, attr) == getattr(copy, attr) + + +def test_urllib3_retries(httpbin): + from urllib3.util import Retry + + s = requests.Session() + s.mount("http://", HTTPAdapter(max_retries=Retry(total=2, status_forcelist=[500]))) + + with pytest.raises(RetryError): + s.get(httpbin("status/500")) + + +def test_urllib3_pool_connection_closed(httpbin): + s = requests.Session() + s.mount("http://", HTTPAdapter(pool_connections=0, pool_maxsize=0)) + + try: + s.get(httpbin("status/200")) + except ConnectionError as e: + assert "Pool is closed." in str(e) + + +class TestPreparingURLs: + @pytest.mark.parametrize( + "url,expected", + ( + ("http://google.com", "http://google.com/"), + ("http://ジェーピーニック.jp", "http://xn--hckqz9bzb1cyrb.jp/"), + ("http://xn--n3h.net/", "http://xn--n3h.net/"), + ("http://ジェーピーニック.jp".encode(), "http://xn--hckqz9bzb1cyrb.jp/"), + ("http://straße.de/straße", "http://xn--strae-oqa.de/stra%C3%9Fe"), + ( + "http://straße.de/straße".encode(), + "http://xn--strae-oqa.de/stra%C3%9Fe", + ), + ( + "http://Königsgäßchen.de/straße", + "http://xn--knigsgchen-b4a3dun.de/stra%C3%9Fe", + ), + ( + "http://Königsgäßchen.de/straße".encode(), + "http://xn--knigsgchen-b4a3dun.de/stra%C3%9Fe", + ), + (b"http://xn--n3h.net/", "http://xn--n3h.net/"), + ( + b"http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/", + "http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/", + ), + ( + "http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/", + "http://[1200:0000:ab00:1234:0000:2552:7777:1313]:12345/", + ), + ), + ) + def test_preparing_url(self, url, expected): + def normalize_percent_encode(x): + # Helper function that normalizes equivalent + # percent-encoded bytes before comparisons + for c in re.findall(r"%[a-fA-F0-9]{2}", x): + x = x.replace(c, c.upper()) + return x + + r = requests.Request("GET", url=url) + p = r.prepare() + assert normalize_percent_encode(p.url) == expected + + @pytest.mark.parametrize( + "url", + ( + b"http://*.google.com", + b"http://*", + "http://*.google.com", + "http://*", + "http://☃.net/", + ), + ) + def test_preparing_bad_url(self, url): + r = requests.Request("GET", url=url) + with pytest.raises(requests.exceptions.InvalidURL): + r.prepare() + + @pytest.mark.parametrize("url, exception", (("http://localhost:-1", InvalidURL),)) + def test_redirecting_to_bad_url(self, httpbin, url, exception): + with pytest.raises(exception): + requests.get(httpbin("redirect-to"), params={"url": url}) + + @pytest.mark.parametrize( + "input, expected", + ( + ( + b"http+unix://%2Fvar%2Frun%2Fsocket/path%7E", + "http+unix://%2Fvar%2Frun%2Fsocket/path~", + ), + ( + "http+unix://%2Fvar%2Frun%2Fsocket/path%7E", + "http+unix://%2Fvar%2Frun%2Fsocket/path~", + ), + ( + b"mailto:user@example.org", + "mailto:user@example.org", + ), + ( + "mailto:user@example.org", + "mailto:user@example.org", + ), + ( + b"data:SSDimaUgUHl0aG9uIQ==", + "data:SSDimaUgUHl0aG9uIQ==", + ), + ), + ) + def test_url_mutation(self, input, expected): + """ + This test validates that we correctly exclude some URLs from + preparation, and that we handle others. Specifically, it tests that + any URL whose scheme doesn't begin with "http" is left alone, and + those whose scheme *does* begin with "http" are mutated. + """ + r = requests.Request("GET", url=input) + p = r.prepare() + assert p.url == expected + + @pytest.mark.parametrize( + "input, params, expected", + ( + ( + b"http+unix://%2Fvar%2Frun%2Fsocket/path", + {"key": "value"}, + "http+unix://%2Fvar%2Frun%2Fsocket/path?key=value", + ), + ( + "http+unix://%2Fvar%2Frun%2Fsocket/path", + {"key": "value"}, + "http+unix://%2Fvar%2Frun%2Fsocket/path?key=value", + ), + ( + b"mailto:user@example.org", + {"key": "value"}, + "mailto:user@example.org", + ), + ( + "mailto:user@example.org", + {"key": "value"}, + "mailto:user@example.org", + ), + ), + ) + def test_parameters_for_nonstandard_schemes(self, input, params, expected): + """ + Setting parameters for nonstandard schemes is allowed if those schemes + begin with "http", and is forbidden otherwise. + """ + r = requests.Request("GET", url=input, params=params) + p = r.prepare() + assert p.url == expected + + def test_post_json_nan(self, httpbin): + data = {"foo": float("nan")} + with pytest.raises(requests.exceptions.InvalidJSONError): + requests.post(httpbin("post"), json=data) + + def test_json_decode_compatibility(self, httpbin): + r = requests.get(httpbin("bytes/20")) + with pytest.raises(requests.exceptions.JSONDecodeError) as excinfo: + r.json() + assert isinstance(excinfo.value, RequestException) + assert isinstance(excinfo.value, JSONDecodeError) + assert r.text not in str(excinfo.value) + + def test_json_decode_persists_doc_attr(self, httpbin): + r = requests.get(httpbin("bytes/20")) + with pytest.raises(requests.exceptions.JSONDecodeError) as excinfo: + r.json() + assert excinfo.value.doc == r.text diff --git a/test/fixtures/whole_applications/requests/tests/test_structures.py b/test/fixtures/whole_applications/requests/tests/test_structures.py new file mode 100644 index 0000000..e2fd5ba --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_structures.py @@ -0,0 +1,78 @@ +import pytest + +from requests.structures import CaseInsensitiveDict, LookupDict + + +class TestCaseInsensitiveDict: + @pytest.fixture(autouse=True) + def setup(self): + """CaseInsensitiveDict instance with "Accept" header.""" + self.case_insensitive_dict = CaseInsensitiveDict() + self.case_insensitive_dict["Accept"] = "application/json" + + def test_list(self): + assert list(self.case_insensitive_dict) == ["Accept"] + + possible_keys = pytest.mark.parametrize( + "key", ("accept", "ACCEPT", "aCcEpT", "Accept") + ) + + @possible_keys + def test_getitem(self, key): + assert self.case_insensitive_dict[key] == "application/json" + + @possible_keys + def test_delitem(self, key): + del self.case_insensitive_dict[key] + assert key not in self.case_insensitive_dict + + def test_lower_items(self): + assert list(self.case_insensitive_dict.lower_items()) == [ + ("accept", "application/json") + ] + + def test_repr(self): + assert repr(self.case_insensitive_dict) == "{'Accept': 'application/json'}" + + def test_copy(self): + copy = self.case_insensitive_dict.copy() + assert copy is not self.case_insensitive_dict + assert copy == self.case_insensitive_dict + + @pytest.mark.parametrize( + "other, result", + ( + ({"AccePT": "application/json"}, True), + ({}, False), + (None, False), + ), + ) + def test_instance_equality(self, other, result): + assert (self.case_insensitive_dict == other) is result + + +class TestLookupDict: + @pytest.fixture(autouse=True) + def setup(self): + """LookupDict instance with "bad_gateway" attribute.""" + self.lookup_dict = LookupDict("test") + self.lookup_dict.bad_gateway = 502 + + def test_repr(self): + assert repr(self.lookup_dict) == "" + + get_item_parameters = pytest.mark.parametrize( + "key, value", + ( + ("bad_gateway", 502), + ("not_a_key", None), + ), + ) + + @get_item_parameters + def test_getitem(self, key, value): + assert self.lookup_dict[key] == value + + @get_item_parameters + def test_get(self, key, value): + assert self.lookup_dict.get(key) == value diff --git a/test/fixtures/whole_applications/requests/tests/test_testserver.py b/test/fixtures/whole_applications/requests/tests/test_testserver.py new file mode 100644 index 0000000..c73a3f1 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_testserver.py @@ -0,0 +1,165 @@ +import socket +import threading +import time + +import pytest +from tests.testserver.server import Server + +import requests + + +class TestTestServer: + def test_basic(self): + """messages are sent and received properly""" + question = b"success?" + answer = b"yeah, success" + + def handler(sock): + text = sock.recv(1000) + assert text == question + sock.sendall(answer) + + with Server(handler) as (host, port): + sock = socket.socket() + sock.connect((host, port)) + sock.sendall(question) + text = sock.recv(1000) + assert text == answer + sock.close() + + def test_server_closes(self): + """the server closes when leaving the context manager""" + with Server.basic_response_server() as (host, port): + sock = socket.socket() + sock.connect((host, port)) + + sock.close() + + with pytest.raises(socket.error): + new_sock = socket.socket() + new_sock.connect((host, port)) + + def test_text_response(self): + """the text_response_server sends the given text""" + server = Server.text_response_server( + "HTTP/1.1 200 OK\r\n" "Content-Length: 6\r\n" "\r\nroflol" + ) + + with server as (host, port): + r = requests.get(f"http://{host}:{port}") + + assert r.status_code == 200 + assert r.text == "roflol" + assert r.headers["Content-Length"] == "6" + + def test_basic_response(self): + """the basic response server returns an empty http response""" + with Server.basic_response_server() as (host, port): + r = requests.get(f"http://{host}:{port}") + assert r.status_code == 200 + assert r.text == "" + assert r.headers["Content-Length"] == "0" + + def test_basic_waiting_server(self): + """the server waits for the block_server event to be set before closing""" + block_server = threading.Event() + + with Server.basic_response_server(wait_to_close_event=block_server) as ( + host, + port, + ): + sock = socket.socket() + sock.connect((host, port)) + sock.sendall(b"send something") + time.sleep(2.5) + sock.sendall(b"still alive") + block_server.set() # release server block + + def test_multiple_requests(self): + """multiple requests can be served""" + requests_to_handle = 5 + + server = Server.basic_response_server(requests_to_handle=requests_to_handle) + + with server as (host, port): + server_url = f"http://{host}:{port}" + for _ in range(requests_to_handle): + r = requests.get(server_url) + assert r.status_code == 200 + + # the (n+1)th request fails + with pytest.raises(requests.exceptions.ConnectionError): + r = requests.get(server_url) + + @pytest.mark.skip(reason="this fails non-deterministically under pytest-xdist") + def test_request_recovery(self): + """can check the requests content""" + # TODO: figure out why this sometimes fails when using pytest-xdist. + server = Server.basic_response_server(requests_to_handle=2) + first_request = b"put your hands up in the air" + second_request = b"put your hand down in the floor" + + with server as address: + sock1 = socket.socket() + sock2 = socket.socket() + + sock1.connect(address) + sock1.sendall(first_request) + sock1.close() + + sock2.connect(address) + sock2.sendall(second_request) + sock2.close() + + assert server.handler_results[0] == first_request + assert server.handler_results[1] == second_request + + def test_requests_after_timeout_are_not_received(self): + """the basic response handler times out when receiving requests""" + server = Server.basic_response_server(request_timeout=1) + + with server as address: + sock = socket.socket() + sock.connect(address) + time.sleep(1.5) + sock.sendall(b"hehehe, not received") + sock.close() + + assert server.handler_results[0] == b"" + + def test_request_recovery_with_bigger_timeout(self): + """a biggest timeout can be specified""" + server = Server.basic_response_server(request_timeout=3) + data = b"bananadine" + + with server as address: + sock = socket.socket() + sock.connect(address) + time.sleep(1.5) + sock.sendall(data) + sock.close() + + assert server.handler_results[0] == data + + def test_server_finishes_on_error(self): + """the server thread exits even if an exception exits the context manager""" + server = Server.basic_response_server() + with pytest.raises(Exception): + with server: + raise Exception() + + assert len(server.handler_results) == 0 + + # if the server thread fails to finish, the test suite will hang + # and get killed by the jenkins timeout. + + def test_server_finishes_when_no_connections(self): + """the server thread exits even if there are no connections""" + server = Server.basic_response_server() + with server: + pass + + assert len(server.handler_results) == 0 + + # if the server thread fails to finish, the test suite will hang + # and get killed by the jenkins timeout. diff --git a/test/fixtures/whole_applications/requests/tests/test_utils.py b/test/fixtures/whole_applications/requests/tests/test_utils.py new file mode 100644 index 0000000..112bbd1 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/test_utils.py @@ -0,0 +1,925 @@ +import copy +import filecmp +import os +import tarfile +import zipfile +from collections import deque +from io import BytesIO + +import pytest + +from requests import compat +from requests._internal_utils import unicode_is_ascii +from requests.cookies import RequestsCookieJar +from requests.structures import CaseInsensitiveDict +from requests.utils import ( + _parse_content_type_header, + add_dict_to_cookiejar, + address_in_network, + dotted_netmask, + extract_zipped_paths, + get_auth_from_url, + get_encoding_from_headers, + get_encodings_from_content, + get_environ_proxies, + guess_filename, + guess_json_utf, + is_ipv4_address, + is_valid_cidr, + iter_slices, + parse_dict_header, + parse_header_links, + prepend_scheme_if_needed, + requote_uri, + select_proxy, + set_environ, + should_bypass_proxies, + super_len, + to_key_val_list, + to_native_string, + unquote_header_value, + unquote_unreserved, + urldefragauth, +) + +from .compat import StringIO, cStringIO + + +class TestSuperLen: + @pytest.mark.parametrize( + "stream, value", + ( + (StringIO.StringIO, "Test"), + (BytesIO, b"Test"), + pytest.param( + cStringIO, "Test", marks=pytest.mark.skipif("cStringIO is None") + ), + ), + ) + def test_io_streams(self, stream, value): + """Ensures that we properly deal with different kinds of IO streams.""" + assert super_len(stream()) == 0 + assert super_len(stream(value)) == 4 + + def test_super_len_correctly_calculates_len_of_partially_read_file(self): + """Ensure that we handle partially consumed file like objects.""" + s = StringIO.StringIO() + s.write("foobarbogus") + assert super_len(s) == 0 + + @pytest.mark.parametrize("error", [IOError, OSError]) + def test_super_len_handles_files_raising_weird_errors_in_tell(self, error): + """If tell() raises errors, assume the cursor is at position zero.""" + + class BoomFile: + def __len__(self): + return 5 + + def tell(self): + raise error() + + assert super_len(BoomFile()) == 0 + + @pytest.mark.parametrize("error", [IOError, OSError]) + def test_super_len_tell_ioerror(self, error): + """Ensure that if tell gives an IOError super_len doesn't fail""" + + class NoLenBoomFile: + def tell(self): + raise error() + + def seek(self, offset, whence): + pass + + assert super_len(NoLenBoomFile()) == 0 + + def test_string(self): + assert super_len("Test") == 4 + + @pytest.mark.parametrize( + "mode, warnings_num", + ( + ("r", 1), + ("rb", 0), + ), + ) + def test_file(self, tmpdir, mode, warnings_num, recwarn): + file_obj = tmpdir.join("test.txt") + file_obj.write("Test") + with file_obj.open(mode) as fd: + assert super_len(fd) == 4 + assert len(recwarn) == warnings_num + + def test_tarfile_member(self, tmpdir): + file_obj = tmpdir.join("test.txt") + file_obj.write("Test") + + tar_obj = str(tmpdir.join("test.tar")) + with tarfile.open(tar_obj, "w") as tar: + tar.add(str(file_obj), arcname="test.txt") + + with tarfile.open(tar_obj) as tar: + member = tar.extractfile("test.txt") + assert super_len(member) == 4 + + def test_super_len_with__len__(self): + foo = [1, 2, 3, 4] + len_foo = super_len(foo) + assert len_foo == 4 + + def test_super_len_with_no__len__(self): + class LenFile: + def __init__(self): + self.len = 5 + + assert super_len(LenFile()) == 5 + + def test_super_len_with_tell(self): + foo = StringIO.StringIO("12345") + assert super_len(foo) == 5 + foo.read(2) + assert super_len(foo) == 3 + + def test_super_len_with_fileno(self): + with open(__file__, "rb") as f: + length = super_len(f) + file_data = f.read() + assert length == len(file_data) + + def test_super_len_with_no_matches(self): + """Ensure that objects without any length methods default to 0""" + assert super_len(object()) == 0 + + +class TestToKeyValList: + @pytest.mark.parametrize( + "value, expected", + ( + ([("key", "val")], [("key", "val")]), + ((("key", "val"),), [("key", "val")]), + ({"key": "val"}, [("key", "val")]), + (None, None), + ), + ) + def test_valid(self, value, expected): + assert to_key_val_list(value) == expected + + def test_invalid(self): + with pytest.raises(ValueError): + to_key_val_list("string") + + +class TestUnquoteHeaderValue: + @pytest.mark.parametrize( + "value, expected", + ( + (None, None), + ("Test", "Test"), + ('"Test"', "Test"), + ('"Test\\\\"', "Test\\"), + ('"\\\\Comp\\Res"', "\\Comp\\Res"), + ), + ) + def test_valid(self, value, expected): + assert unquote_header_value(value) == expected + + def test_is_filename(self): + assert unquote_header_value('"\\\\Comp\\Res"', True) == "\\\\Comp\\Res" + + +class TestGetEnvironProxies: + """Ensures that IP addresses are correctly matches with ranges + in no_proxy variable. + """ + + @pytest.fixture(autouse=True, params=["no_proxy", "NO_PROXY"]) + def no_proxy(self, request, monkeypatch): + monkeypatch.setenv( + request.param, "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1" + ) + + @pytest.mark.parametrize( + "url", + ( + "http://192.168.0.1:5000/", + "http://192.168.0.1/", + "http://172.16.1.1/", + "http://172.16.1.1:5000/", + "http://localhost.localdomain:5000/v1.0/", + ), + ) + def test_bypass(self, url): + assert get_environ_proxies(url, no_proxy=None) == {} + + @pytest.mark.parametrize( + "url", + ( + "http://192.168.1.1:5000/", + "http://192.168.1.1/", + "http://www.requests.com/", + ), + ) + def test_not_bypass(self, url): + assert get_environ_proxies(url, no_proxy=None) != {} + + @pytest.mark.parametrize( + "url", + ( + "http://192.168.1.1:5000/", + "http://192.168.1.1/", + "http://www.requests.com/", + ), + ) + def test_bypass_no_proxy_keyword(self, url): + no_proxy = "192.168.1.1,requests.com" + assert get_environ_proxies(url, no_proxy=no_proxy) == {} + + @pytest.mark.parametrize( + "url", + ( + "http://192.168.0.1:5000/", + "http://192.168.0.1/", + "http://172.16.1.1/", + "http://172.16.1.1:5000/", + "http://localhost.localdomain:5000/v1.0/", + ), + ) + def test_not_bypass_no_proxy_keyword(self, url, monkeypatch): + # This is testing that the 'no_proxy' argument overrides the + # environment variable 'no_proxy' + monkeypatch.setenv("http_proxy", "http://proxy.example.com:3128/") + no_proxy = "192.168.1.1,requests.com" + assert get_environ_proxies(url, no_proxy=no_proxy) != {} + + +class TestIsIPv4Address: + def test_valid(self): + assert is_ipv4_address("8.8.8.8") + + @pytest.mark.parametrize("value", ("8.8.8.8.8", "localhost.localdomain")) + def test_invalid(self, value): + assert not is_ipv4_address(value) + + +class TestIsValidCIDR: + def test_valid(self): + assert is_valid_cidr("192.168.1.0/24") + + @pytest.mark.parametrize( + "value", + ( + "8.8.8.8", + "192.168.1.0/a", + "192.168.1.0/128", + "192.168.1.0/-1", + "192.168.1.999/24", + ), + ) + def test_invalid(self, value): + assert not is_valid_cidr(value) + + +class TestAddressInNetwork: + def test_valid(self): + assert address_in_network("192.168.1.1", "192.168.1.0/24") + + def test_invalid(self): + assert not address_in_network("172.16.0.1", "192.168.1.0/24") + + +class TestGuessFilename: + @pytest.mark.parametrize( + "value", + (1, type("Fake", (object,), {"name": 1})()), + ) + def test_guess_filename_invalid(self, value): + assert guess_filename(value) is None + + @pytest.mark.parametrize( + "value, expected_type", + ( + (b"value", compat.bytes), + (b"value".decode("utf-8"), compat.str), + ), + ) + def test_guess_filename_valid(self, value, expected_type): + obj = type("Fake", (object,), {"name": value})() + result = guess_filename(obj) + assert result == value + assert isinstance(result, expected_type) + + +class TestExtractZippedPaths: + @pytest.mark.parametrize( + "path", + ( + "/", + __file__, + pytest.__file__, + "/etc/invalid/location", + ), + ) + def test_unzipped_paths_unchanged(self, path): + assert path == extract_zipped_paths(path) + + def test_zipped_paths_extracted(self, tmpdir): + zipped_py = tmpdir.join("test.zip") + with zipfile.ZipFile(zipped_py.strpath, "w") as f: + f.write(__file__) + + _, name = os.path.splitdrive(__file__) + zipped_path = os.path.join(zipped_py.strpath, name.lstrip(r"\/")) + extracted_path = extract_zipped_paths(zipped_path) + + assert extracted_path != zipped_path + assert os.path.exists(extracted_path) + assert filecmp.cmp(extracted_path, __file__) + + def test_invalid_unc_path(self): + path = r"\\localhost\invalid\location" + assert extract_zipped_paths(path) == path + + +class TestContentEncodingDetection: + def test_none(self): + encodings = get_encodings_from_content("") + assert not len(encodings) + + @pytest.mark.parametrize( + "content", + ( + # HTML5 meta charset attribute + '', + # HTML4 pragma directive + '', + # XHTML 1.x served with text/html MIME type + '', + # XHTML 1.x served as XML + '', + ), + ) + def test_pragmas(self, content): + encodings = get_encodings_from_content(content) + assert len(encodings) == 1 + assert encodings[0] == "UTF-8" + + def test_precedence(self): + content = """ + + + + """.strip() + assert get_encodings_from_content(content) == ["HTML5", "HTML4", "XML"] + + +class TestGuessJSONUTF: + @pytest.mark.parametrize( + "encoding", + ( + "utf-32", + "utf-8-sig", + "utf-16", + "utf-8", + "utf-16-be", + "utf-16-le", + "utf-32-be", + "utf-32-le", + ), + ) + def test_encoded(self, encoding): + data = "{}".encode(encoding) + assert guess_json_utf(data) == encoding + + def test_bad_utf_like_encoding(self): + assert guess_json_utf(b"\x00\x00\x00\x00") is None + + @pytest.mark.parametrize( + ("encoding", "expected"), + ( + ("utf-16-be", "utf-16"), + ("utf-16-le", "utf-16"), + ("utf-32-be", "utf-32"), + ("utf-32-le", "utf-32"), + ), + ) + def test_guess_by_bom(self, encoding, expected): + data = "\ufeff{}".encode(encoding) + assert guess_json_utf(data) == expected + + +USER = PASSWORD = "%!*'();:@&=+$,/?#[] " +ENCODED_USER = compat.quote(USER, "") +ENCODED_PASSWORD = compat.quote(PASSWORD, "") + + +@pytest.mark.parametrize( + "url, auth", + ( + ( + f"http://{ENCODED_USER}:{ENCODED_PASSWORD}@request.com/url.html#test", + (USER, PASSWORD), + ), + ("http://user:pass@complex.url.com/path?query=yes", ("user", "pass")), + ( + "http://user:pass%20pass@complex.url.com/path?query=yes", + ("user", "pass pass"), + ), + ("http://user:pass pass@complex.url.com/path?query=yes", ("user", "pass pass")), + ( + "http://user%25user:pass@complex.url.com/path?query=yes", + ("user%user", "pass"), + ), + ( + "http://user:pass%23pass@complex.url.com/path?query=yes", + ("user", "pass#pass"), + ), + ("http://complex.url.com/path?query=yes", ("", "")), + ), +) +def test_get_auth_from_url(url, auth): + assert get_auth_from_url(url) == auth + + +@pytest.mark.parametrize( + "uri, expected", + ( + ( + # Ensure requoting doesn't break expectations + "http://example.com/fiz?buz=%25ppicture", + "http://example.com/fiz?buz=%25ppicture", + ), + ( + # Ensure we handle unquoted percent signs in redirects + "http://example.com/fiz?buz=%ppicture", + "http://example.com/fiz?buz=%25ppicture", + ), + ), +) +def test_requote_uri_with_unquoted_percents(uri, expected): + """See: https://github.com/psf/requests/issues/2356""" + assert requote_uri(uri) == expected + + +@pytest.mark.parametrize( + "uri, expected", + ( + ( + # Illegal bytes + "http://example.com/?a=%--", + "http://example.com/?a=%--", + ), + ( + # Reserved characters + "http://example.com/?a=%300", + "http://example.com/?a=00", + ), + ), +) +def test_unquote_unreserved(uri, expected): + assert unquote_unreserved(uri) == expected + + +@pytest.mark.parametrize( + "mask, expected", + ( + (8, "255.0.0.0"), + (24, "255.255.255.0"), + (25, "255.255.255.128"), + ), +) +def test_dotted_netmask(mask, expected): + assert dotted_netmask(mask) == expected + + +http_proxies = { + "http": "http://http.proxy", + "http://some.host": "http://some.host.proxy", +} +all_proxies = { + "all": "socks5://http.proxy", + "all://some.host": "socks5://some.host.proxy", +} +mixed_proxies = { + "http": "http://http.proxy", + "http://some.host": "http://some.host.proxy", + "all": "socks5://http.proxy", +} + + +@pytest.mark.parametrize( + "url, expected, proxies", + ( + ("hTTp://u:p@Some.Host/path", "http://some.host.proxy", http_proxies), + ("hTTp://u:p@Other.Host/path", "http://http.proxy", http_proxies), + ("hTTp:///path", "http://http.proxy", http_proxies), + ("hTTps://Other.Host", None, http_proxies), + ("file:///etc/motd", None, http_proxies), + ("hTTp://u:p@Some.Host/path", "socks5://some.host.proxy", all_proxies), + ("hTTp://u:p@Other.Host/path", "socks5://http.proxy", all_proxies), + ("hTTp:///path", "socks5://http.proxy", all_proxies), + ("hTTps://Other.Host", "socks5://http.proxy", all_proxies), + ("http://u:p@other.host/path", "http://http.proxy", mixed_proxies), + ("http://u:p@some.host/path", "http://some.host.proxy", mixed_proxies), + ("https://u:p@other.host/path", "socks5://http.proxy", mixed_proxies), + ("https://u:p@some.host/path", "socks5://http.proxy", mixed_proxies), + ("https://", "socks5://http.proxy", mixed_proxies), + # XXX: unsure whether this is reasonable behavior + ("file:///etc/motd", "socks5://http.proxy", all_proxies), + ), +) +def test_select_proxies(url, expected, proxies): + """Make sure we can select per-host proxies correctly.""" + assert select_proxy(url, proxies) == expected + + +@pytest.mark.parametrize( + "value, expected", + ( + ('foo="is a fish", bar="as well"', {"foo": "is a fish", "bar": "as well"}), + ("key_without_value", {"key_without_value": None}), + ), +) +def test_parse_dict_header(value, expected): + assert parse_dict_header(value) == expected + + +@pytest.mark.parametrize( + "value, expected", + ( + ("application/xml", ("application/xml", {})), + ( + "application/json ; charset=utf-8", + ("application/json", {"charset": "utf-8"}), + ), + ( + "application/json ; Charset=utf-8", + ("application/json", {"charset": "utf-8"}), + ), + ("text/plain", ("text/plain", {})), + ( + "multipart/form-data; boundary = something ; boundary2='something_else' ; no_equals ", + ( + "multipart/form-data", + { + "boundary": "something", + "boundary2": "something_else", + "no_equals": True, + }, + ), + ), + ( + 'multipart/form-data; boundary = something ; boundary2="something_else" ; no_equals ', + ( + "multipart/form-data", + { + "boundary": "something", + "boundary2": "something_else", + "no_equals": True, + }, + ), + ), + ( + "multipart/form-data; boundary = something ; 'boundary2=something_else' ; no_equals ", + ( + "multipart/form-data", + { + "boundary": "something", + "boundary2": "something_else", + "no_equals": True, + }, + ), + ), + ( + 'multipart/form-data; boundary = something ; "boundary2=something_else" ; no_equals ', + ( + "multipart/form-data", + { + "boundary": "something", + "boundary2": "something_else", + "no_equals": True, + }, + ), + ), + ("application/json ; ; ", ("application/json", {})), + ), +) +def test__parse_content_type_header(value, expected): + assert _parse_content_type_header(value) == expected + + +@pytest.mark.parametrize( + "value, expected", + ( + (CaseInsensitiveDict(), None), + ( + CaseInsensitiveDict({"content-type": "application/json; charset=utf-8"}), + "utf-8", + ), + (CaseInsensitiveDict({"content-type": "text/plain"}), "ISO-8859-1"), + ), +) +def test_get_encoding_from_headers(value, expected): + assert get_encoding_from_headers(value) == expected + + +@pytest.mark.parametrize( + "value, length", + ( + ("", 0), + ("T", 1), + ("Test", 4), + ("Cont", 0), + ("Other", -5), + ("Content", None), + ), +) +def test_iter_slices(value, length): + if length is None or (length <= 0 and len(value) > 0): + # Reads all content at once + assert len(list(iter_slices(value, length))) == 1 + else: + assert len(list(iter_slices(value, 1))) == length + + +@pytest.mark.parametrize( + "value, expected", + ( + ( + '; rel=front; type="image/jpeg"', + [{"url": "http:/.../front.jpeg", "rel": "front", "type": "image/jpeg"}], + ), + ("", [{"url": "http:/.../front.jpeg"}]), + (";", [{"url": "http:/.../front.jpeg"}]), + ( + '; type="image/jpeg",;', + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ], + ), + ("", []), + ), +) +def test_parse_header_links(value, expected): + assert parse_header_links(value) == expected + + +@pytest.mark.parametrize( + "value, expected", + ( + ("example.com/path", "http://example.com/path"), + ("//example.com/path", "http://example.com/path"), + ("example.com:80", "http://example.com:80"), + ( + "http://user:pass@example.com/path?query", + "http://user:pass@example.com/path?query", + ), + ("http://user@example.com/path?query", "http://user@example.com/path?query"), + ), +) +def test_prepend_scheme_if_needed(value, expected): + assert prepend_scheme_if_needed(value, "http") == expected + + +@pytest.mark.parametrize( + "value, expected", + ( + ("T", "T"), + (b"T", "T"), + ("T", "T"), + ), +) +def test_to_native_string(value, expected): + assert to_native_string(value) == expected + + +@pytest.mark.parametrize( + "url, expected", + ( + ("http://u:p@example.com/path?a=1#test", "http://example.com/path?a=1"), + ("http://example.com/path", "http://example.com/path"), + ("//u:p@example.com/path", "//example.com/path"), + ("//example.com/path", "//example.com/path"), + ("example.com/path", "//example.com/path"), + ("scheme:u:p@example.com/path", "scheme://example.com/path"), + ), +) +def test_urldefragauth(url, expected): + assert urldefragauth(url) == expected + + +@pytest.mark.parametrize( + "url, expected", + ( + ("http://192.168.0.1:5000/", True), + ("http://192.168.0.1/", True), + ("http://172.16.1.1/", True), + ("http://172.16.1.1:5000/", True), + ("http://localhost.localdomain:5000/v1.0/", True), + ("http://google.com:6000/", True), + ("http://172.16.1.12/", False), + ("http://172.16.1.12:5000/", False), + ("http://google.com:5000/v1.0/", False), + ("file:///some/path/on/disk", True), + ), +) +def test_should_bypass_proxies(url, expected, monkeypatch): + """Tests for function should_bypass_proxies to check if proxy + can be bypassed or not + """ + monkeypatch.setenv( + "no_proxy", + "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000", + ) + monkeypatch.setenv( + "NO_PROXY", + "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000", + ) + assert should_bypass_proxies(url, no_proxy=None) == expected + + +@pytest.mark.parametrize( + "url, expected", + ( + ("http://172.16.1.1/", "172.16.1.1"), + ("http://172.16.1.1:5000/", "172.16.1.1"), + ("http://user:pass@172.16.1.1", "172.16.1.1"), + ("http://user:pass@172.16.1.1:5000", "172.16.1.1"), + ("http://hostname/", "hostname"), + ("http://hostname:5000/", "hostname"), + ("http://user:pass@hostname", "hostname"), + ("http://user:pass@hostname:5000", "hostname"), + ), +) +def test_should_bypass_proxies_pass_only_hostname(url, expected, mocker): + """The proxy_bypass function should be called with a hostname or IP without + a port number or auth credentials. + """ + proxy_bypass = mocker.patch("requests.utils.proxy_bypass") + should_bypass_proxies(url, no_proxy=None) + proxy_bypass.assert_called_once_with(expected) + + +@pytest.mark.parametrize( + "cookiejar", + ( + compat.cookielib.CookieJar(), + RequestsCookieJar(), + ), +) +def test_add_dict_to_cookiejar(cookiejar): + """Ensure add_dict_to_cookiejar works for + non-RequestsCookieJar CookieJars + """ + cookiedict = {"test": "cookies", "good": "cookies"} + cj = add_dict_to_cookiejar(cookiejar, cookiedict) + cookies = {cookie.name: cookie.value for cookie in cj} + assert cookiedict == cookies + + +@pytest.mark.parametrize( + "value, expected", + ( + ("test", True), + ("æíöû", False), + ("ジェーピーニック", False), + ), +) +def test_unicode_is_ascii(value, expected): + assert unicode_is_ascii(value) is expected + + +@pytest.mark.parametrize( + "url, expected", + ( + ("http://192.168.0.1:5000/", True), + ("http://192.168.0.1/", True), + ("http://172.16.1.1/", True), + ("http://172.16.1.1:5000/", True), + ("http://localhost.localdomain:5000/v1.0/", True), + ("http://172.16.1.12/", False), + ("http://172.16.1.12:5000/", False), + ("http://google.com:5000/v1.0/", False), + ), +) +def test_should_bypass_proxies_no_proxy(url, expected, monkeypatch): + """Tests for function should_bypass_proxies to check if proxy + can be bypassed or not using the 'no_proxy' argument + """ + no_proxy = "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1" + # Test 'no_proxy' argument + assert should_bypass_proxies(url, no_proxy=no_proxy) == expected + + +@pytest.mark.skipif(os.name != "nt", reason="Test only on Windows") +@pytest.mark.parametrize( + "url, expected, override", + ( + ("http://192.168.0.1:5000/", True, None), + ("http://192.168.0.1/", True, None), + ("http://172.16.1.1/", True, None), + ("http://172.16.1.1:5000/", True, None), + ("http://localhost.localdomain:5000/v1.0/", True, None), + ("http://172.16.1.22/", False, None), + ("http://172.16.1.22:5000/", False, None), + ("http://google.com:5000/v1.0/", False, None), + ("http://mylocalhostname:5000/v1.0/", True, ""), + ("http://192.168.0.1/", False, ""), + ), +) +def test_should_bypass_proxies_win_registry(url, expected, override, monkeypatch): + """Tests for function should_bypass_proxies to check if proxy + can be bypassed or not with Windows registry settings + """ + if override is None: + override = "192.168.*;127.0.0.1;localhost.localdomain;172.16.1.1" + import winreg + + class RegHandle: + def Close(self): + pass + + ie_settings = RegHandle() + proxyEnableValues = deque([1, "1"]) + + def OpenKey(key, subkey): + return ie_settings + + def QueryValueEx(key, value_name): + if key is ie_settings: + if value_name == "ProxyEnable": + # this could be a string (REG_SZ) or a 32-bit number (REG_DWORD) + proxyEnableValues.rotate() + return [proxyEnableValues[0]] + elif value_name == "ProxyOverride": + return [override] + + monkeypatch.setenv("http_proxy", "") + monkeypatch.setenv("https_proxy", "") + monkeypatch.setenv("ftp_proxy", "") + monkeypatch.setenv("no_proxy", "") + monkeypatch.setenv("NO_PROXY", "") + monkeypatch.setattr(winreg, "OpenKey", OpenKey) + monkeypatch.setattr(winreg, "QueryValueEx", QueryValueEx) + assert should_bypass_proxies(url, None) == expected + + +@pytest.mark.skipif(os.name != "nt", reason="Test only on Windows") +def test_should_bypass_proxies_win_registry_bad_values(monkeypatch): + """Tests for function should_bypass_proxies to check if proxy + can be bypassed or not with Windows invalid registry settings. + """ + import winreg + + class RegHandle: + def Close(self): + pass + + ie_settings = RegHandle() + + def OpenKey(key, subkey): + return ie_settings + + def QueryValueEx(key, value_name): + if key is ie_settings: + if value_name == "ProxyEnable": + # Invalid response; Should be an int or int-y value + return [""] + elif value_name == "ProxyOverride": + return ["192.168.*;127.0.0.1;localhost.localdomain;172.16.1.1"] + + monkeypatch.setenv("http_proxy", "") + monkeypatch.setenv("https_proxy", "") + monkeypatch.setenv("no_proxy", "") + monkeypatch.setenv("NO_PROXY", "") + monkeypatch.setattr(winreg, "OpenKey", OpenKey) + monkeypatch.setattr(winreg, "QueryValueEx", QueryValueEx) + assert should_bypass_proxies("http://172.16.1.1/", None) is False + + +@pytest.mark.parametrize( + "env_name, value", + ( + ("no_proxy", "192.168.0.0/24,127.0.0.1,localhost.localdomain"), + ("no_proxy", None), + ("a_new_key", "192.168.0.0/24,127.0.0.1,localhost.localdomain"), + ("a_new_key", None), + ), +) +def test_set_environ(env_name, value): + """Tests set_environ will set environ values and will restore the environ.""" + environ_copy = copy.deepcopy(os.environ) + with set_environ(env_name, value): + assert os.environ.get(env_name) == value + + assert os.environ == environ_copy + + +def test_set_environ_raises_exception(): + """Tests set_environ will raise exceptions in context when the + value parameter is None.""" + with pytest.raises(Exception) as exception: + with set_environ("test1", None): + raise Exception("Expected exception") + + assert "Expected exception" in str(exception.value) diff --git a/test/fixtures/whole_applications/requests/tests/testserver/__init__.py b/test/fixtures/whole_applications/requests/tests/testserver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/whole_applications/requests/tests/testserver/server.py b/test/fixtures/whole_applications/requests/tests/testserver/server.py new file mode 100644 index 0000000..5936abd --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/testserver/server.py @@ -0,0 +1,134 @@ +import select +import socket +import threading + + +def consume_socket_content(sock, timeout=0.5): + chunks = 65536 + content = b"" + + while True: + more_to_read = select.select([sock], [], [], timeout)[0] + if not more_to_read: + break + + new_content = sock.recv(chunks) + if not new_content: + break + + content += new_content + + return content + + +class Server(threading.Thread): + """Dummy server using for unit testing""" + + WAIT_EVENT_TIMEOUT = 5 + + def __init__( + self, + handler=None, + host="localhost", + port=0, + requests_to_handle=1, + wait_to_close_event=None, + ): + super().__init__() + + self.handler = handler or consume_socket_content + self.handler_results = [] + + self.host = host + self.port = port + self.requests_to_handle = requests_to_handle + + self.wait_to_close_event = wait_to_close_event + self.ready_event = threading.Event() + self.stop_event = threading.Event() + + @classmethod + def text_response_server(cls, text, request_timeout=0.5, **kwargs): + def text_response_handler(sock): + request_content = consume_socket_content(sock, timeout=request_timeout) + sock.send(text.encode("utf-8")) + + return request_content + + return Server(text_response_handler, **kwargs) + + @classmethod + def basic_response_server(cls, **kwargs): + return cls.text_response_server( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n\r\n", **kwargs + ) + + def run(self): + try: + self.server_sock = self._create_socket_and_bind() + # in case self.port = 0 + self.port = self.server_sock.getsockname()[1] + self.ready_event.set() + self._handle_requests() + + if self.wait_to_close_event: + self.wait_to_close_event.wait(self.WAIT_EVENT_TIMEOUT) + finally: + self.ready_event.set() # just in case of exception + self._close_server_sock_ignore_errors() + self.stop_event.set() + + def _create_socket_and_bind(self): + sock = socket.socket() + sock.bind((self.host, self.port)) + sock.listen() + return sock + + def _close_server_sock_ignore_errors(self): + try: + self.server_sock.close() + except OSError: + pass + + def _handle_requests(self): + for _ in range(self.requests_to_handle): + sock = self._accept_connection() + if not sock: + break + + handler_result = self.handler(sock) + + self.handler_results.append(handler_result) + sock.close() + + def _accept_connection(self): + try: + ready, _, _ = select.select( + [self.server_sock], [], [], self.WAIT_EVENT_TIMEOUT + ) + if not ready: + return None + + return self.server_sock.accept()[0] + except OSError: + return None + + def __enter__(self): + self.start() + if not self.ready_event.wait(self.WAIT_EVENT_TIMEOUT): + raise RuntimeError("Timeout waiting for server to be ready.") + return self.host, self.port + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None: + self.stop_event.wait(self.WAIT_EVENT_TIMEOUT) + else: + if self.wait_to_close_event: + # avoid server from waiting for event timeouts + # if an exception is found in the main thread + self.wait_to_close_event.set() + + # ensure server thread doesn't get stuck waiting for connections + self._close_server_sock_ignore_errors() + self.join() + return False # allow exceptions to propagate diff --git a/test/fixtures/whole_applications/requests/tests/utils.py b/test/fixtures/whole_applications/requests/tests/utils.py new file mode 100644 index 0000000..6cb75bf --- /dev/null +++ b/test/fixtures/whole_applications/requests/tests/utils.py @@ -0,0 +1,17 @@ +import contextlib +import os + + +@contextlib.contextmanager +def override_environ(**kwargs): + save_env = dict(os.environ) + for key, value in kwargs.items(): + if value is None: + del os.environ[key] + else: + os.environ[key] = value + try: + yield + finally: + os.environ.clear() + os.environ.update(save_env) diff --git a/test/fixtures/whole_applications/requests/tox.ini b/test/fixtures/whole_applications/requests/tox.ini new file mode 100644 index 0000000..546c737 --- /dev/null +++ b/test/fixtures/whole_applications/requests/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = py{37,38,39,310,311}-{default, use_chardet_on_py3} + +[testenv] +deps = -rrequirements-dev.txt +extras = + security + socks +commands = + pytest tests + +[testenv:default] + +[testenv:use_chardet_on_py3] +extras = + security + socks + use_chardet_on_py3 diff --git a/test/sample_graph_app.py b/test/sample_graph_app.py index b4232b9..c9a98c5 100644 --- a/test/sample_graph_app.py +++ b/test/sample_graph_app.py @@ -143,7 +143,7 @@ def make_sample_app() -> PyApplication: source="src.service.helper", target="requests.get", weight=2, - provenance=["jedi", "codeql"], + provenance=["jedi", "pycg"], ), ] diff --git a/test/test_cli.py b/test/test_cli.py index 11a5490..c0bbb42 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -21,7 +21,7 @@ def test_cli_call_symbol_table_with_json(cli_runner, whole_applications__xarray) "--output", str(output_dir), "--ray", - "--no-codeql", + "--analysis-level", "1", "--cache-dir", str(whole_applications__xarray.joinpath("test", ".cache")), "--clear-cache", @@ -50,7 +50,7 @@ def test_no_venv_skips_virtualenv( "--input", str(single_functionalities__stuff_nested_in_functions), "--output", str(out), "--cache-dir", str(cache), - "--no-venv", "--no-codeql", "--no-ray", + "--no-venv", "--no-ray", ], env={"NO_COLOR": "1", "TERM": "dumb"}, ) @@ -93,4 +93,246 @@ def test_single_file(cli_runner, single_functionalities__stuff_nested_in_functio json_obj = json.loads(Path(output_dir).joinpath("analysis.json").read_text()) assert json_obj is not None, "JSON output should not be None" assert isinstance(json_obj, dict), "JSON output should be a dictionary" - assert "symbol_table" in json_obj.keys(), "Symbol table should be present in the output" \ No newline at end of file + assert "symbol_table" in json_obj.keys(), "Symbol table should be present in the output" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _run_analysis(cli_runner, fixture_dir, analysis_level=1, file_name=None, extra_args=None): + """Invoke the CLI on *fixture_dir* and return the parsed JSON output.""" + output_dir = fixture_dir.joinpath(".output") + output_dir.mkdir(parents=True, exist_ok=True) + args = [ + "--input", str(fixture_dir), + "--output", str(output_dir), + "--no-ray", + "--clear-cache", + "--analysis-level", str(analysis_level), + "--skip-tests", + "--format=json", + ] + if file_name: + args += ["--file-name", str(file_name)] + if extra_args: + args += extra_args + result = cli_runner.invoke( + app, args, env={"NO_COLOR": "1", "TERM": "dumb"} + ) + assert result.exit_code == 0, f"CLI failed (level {analysis_level}): {result.output}" + out = fixture_dir.joinpath(".output", "analysis.json") + assert out.exists() + return json.loads(out.read_text()) + + +# --------------------------------------------------------------------------- +# Targeted single-functionality fixtures — Level 1 +# --------------------------------------------------------------------------- + +def test_decorators_hof_level1(cli_runner, single_functionalities__decorators_and_hof): + """Level 1 on decorators_and_hof: symbol table populated, call_graph has Jedi edges.""" + main_py = single_functionalities__decorators_and_hof / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__decorators_and_hof, + analysis_level=1, file_name=main_py) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) > 0, "Level 1 must populate call_graph with Jedi edges" + sigs = {c["signature"] for mod in obj["symbol_table"].values() + for c in _all_callables(mod)} + assert any("main" in s for s in sigs), "Expected 'main' callable in symbol table" + + +def test_decorators_hof_level2(cli_runner, single_functionalities__decorators_and_hof): + """Level 2 on decorators_and_hof: call_graph non-empty with PyCG edges. + + Key assertions: + - At least 20 total edges (observed ~34) + - PyCG resolves HOF points-to: apply->triple (missed by Jedi's single call-site inference) + - PyCG finds closure call: log_call.wrapper->greet + """ + main_py = single_functionalities__decorators_and_hof / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__decorators_and_hof, + analysis_level=2, file_name=main_py) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) >= 20, \ + f"Expected >=20 edges for decorators_and_hof, got {len(obj['call_graph'])}" + + pycg_edges = [(e["source"], e["target"]) for e in obj["call_graph"] + if "pycg" in e["provenance"]] + assert len(pycg_edges) >= 10, \ + f"Expected >=10 PyCG edges, got {len(pycg_edges)}" + + pycg_targets_from_apply = {t for s, t in pycg_edges if "apply" in s} + assert any("triple" in t for t in pycg_targets_from_apply), \ + "PyCG must resolve apply->triple via points-to (Jedi misses the second call site)" + + pycg_targets_from_wrapper = {t for s, t in pycg_edges if "wrapper" in s} + assert any("greet" in t for t in pycg_targets_from_wrapper), \ + "PyCG must resolve log_call.wrapper->greet (closure call)" + + +def test_class_hierarchy_level1(cli_runner, single_functionalities__class_hierarchy): + """Level 1 on class_hierarchy: symbol table has classes and methods.""" + main_py = single_functionalities__class_hierarchy / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__class_hierarchy, + analysis_level=1, file_name=main_py) + assert len(obj["call_graph"]) > 0, "Level 1 must populate call_graph with Jedi edges" + classes = {cls for mod in obj["symbol_table"].values() + for cls in mod.get("classes", {}).keys()} + assert any("Animal" in c for c in classes) + assert any("Dog" in c for c in classes) + assert any("Cat" in c for c in classes) + + +def test_class_hierarchy_level2(cli_runner, single_functionalities__class_hierarchy): + """Level 2 on class_hierarchy: PyCG resolves virtual dispatch and super() calls. + + Key assertions: + - At least 30 total edges (observed ~51) + - PyCG finds virtual dispatch: Animal.describe->PoliceDog.speak + - PyCG finds super().__init__ chains (present as super edges) + - __init__ edges present from constructor calls + """ + main_py = single_functionalities__class_hierarchy / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__class_hierarchy, + analysis_level=2, file_name=main_py) + assert len(obj["call_graph"]) >= 30, \ + f"Expected >=30 edges for class_hierarchy, got {len(obj['call_graph'])}" + + pycg_edges = [(e["source"], e["target"]) for e in obj["call_graph"] + if "pycg" in e["provenance"]] + assert len(pycg_edges) >= 15, \ + f"Expected >=15 PyCG edges, got {len(pycg_edges)}" + + # PyCG resolves virtual dispatch: Animal.describe calls speak() on subclasses + describe_targets = {t for s, t in pycg_edges if "describe" in s} + assert any("speak" in t for t in describe_targets), \ + "PyCG must find Animal.describe->*.speak virtual dispatch" + + targets = {e["target"] for e in obj["call_graph"]} + assert any("__init__" in t for t in targets), "Expected __init__ edges in class hierarchy" + + +def test_async_patterns_level1(cli_runner, single_functionalities__async_patterns): + """Level 1 on async_patterns: async functions appear in symbol table.""" + main_py = single_functionalities__async_patterns / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__async_patterns, + analysis_level=1, file_name=main_py) + assert len(obj["call_graph"]) > 0, "Level 1 must populate call_graph with Jedi edges" + sigs = {c["signature"] for mod in obj["symbol_table"].values() + for c in _all_callables(mod)} + assert any("fetch_data" in s for s in sigs) + assert any("async_main" in s or "main" in s for s in sigs) + + +def test_async_patterns_level2(cli_runner, single_functionalities__async_patterns): + """Level 2 on async_patterns: PyCG resolves async calls and asyncio stdlib edges. + + Key assertions: + - At least 15 total edges (observed ~31) + - PyCG finds asyncio.sleep in async functions (await targets) + - PyCG finds asyncio.gather in fetch_all + - Pipeline chain is fully connected (async_main->pipeline->fetch_all->process_url->fetch_data) + """ + main_py = single_functionalities__async_patterns / "main.py" + obj = _run_analysis(cli_runner, single_functionalities__async_patterns, + analysis_level=2, file_name=main_py) + assert len(obj["call_graph"]) >= 15, \ + f"Expected >=15 edges for async_patterns, got {len(obj['call_graph'])}" + + pycg_edges = [(e["source"], e["target"]) for e in obj["call_graph"] + if "pycg" in e["provenance"]] + assert len(pycg_edges) >= 8, \ + f"Expected >=8 PyCG edges, got {len(pycg_edges)}" + + pycg_targets = {t for _, t in pycg_edges} + assert any("asyncio" in t or "sleep" in t for t in pycg_targets), \ + "PyCG must resolve asyncio.sleep calls in async functions" + + all_edges = {(e["source"], e["target"]) for e in obj["call_graph"]} + assert any("pipeline" in s and "fetch_all" in t for s, t in all_edges), \ + "pipeline->fetch_all edge must be present" + assert any("process_url" in s and "fetch_data" in t for s, t in all_edges), \ + "process_url->fetch_data edge must be present" + + +# --------------------------------------------------------------------------- +# Whole-application fixtures — smoke tests +# --------------------------------------------------------------------------- + +def test_flask_level1(cli_runner, whole_applications__flask): + """Level 1 on Flask 3.0.3: symbol table populated.""" + obj = _run_analysis(cli_runner, whole_applications__flask, analysis_level=1) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) > 0, "Level 1 must populate call_graph with Jedi edges" + assert any("flask" in mod_path.lower() for mod_path in obj["symbol_table"]), \ + "Flask modules should be in symbol table" + + +def test_flask_level2(cli_runner, whole_applications__flask): + """Level 2 on Flask 3.0.3: PyCG substantially augments Jedi's edges. + + PyCG contributes >50% of total edges for a decorator-heavy codebase like Flask + (observed ~852 PyCG out of ~1450 total edges). + """ + obj = _run_analysis(cli_runner, whole_applications__flask, analysis_level=2) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) >= 500, \ + f"Expected >=500 edges for Flask, got {len(obj['call_graph'])}" + pycg_edges = [e for e in obj["call_graph"] if "pycg" in e["provenance"]] + assert len(pycg_edges) >= 200, \ + f"Expected >=200 PyCG edges for Flask, got {len(pycg_edges)}" + + +def test_requests_level1(cli_runner, whole_applications__requests): + """Level 1 on requests 2.31.0: symbol table populated.""" + obj = _run_analysis(cli_runner, whole_applications__requests, analysis_level=1) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) > 0, "Level 1 must populate call_graph with Jedi edges" + + +def test_requests_level2(cli_runner, whole_applications__requests): + """Level 2 on requests 2.31.0: PyCG resolves OO dispatch and session/adapter calls. + + PyCG contributes >50% of total edges for a clean OO codebase like requests + (observed ~724 PyCG out of ~1121 total edges). + """ + obj = _run_analysis(cli_runner, whole_applications__requests, analysis_level=2) + assert len(obj["symbol_table"]) > 0 + assert len(obj["call_graph"]) >= 400, \ + f"Expected >=400 edges for requests, got {len(obj['call_graph'])}" + pycg_edges = [e for e in obj["call_graph"] if "pycg" in e["provenance"]] + assert len(pycg_edges) >= 150, \ + f"Expected >=150 PyCG edges for requests, got {len(pycg_edges)}" + + +# --------------------------------------------------------------------------- +# Helper: flatten all callables from a serialised PyModule dict +# --------------------------------------------------------------------------- + +def _all_callables(module_dict: dict) -> list: + """Flatten all callable dicts from a serialised PyModule.""" + result = [] + for fn in module_dict.get("functions", {}).values(): + result.extend(_flatten_callable(fn)) + for cls in module_dict.get("classes", {}).values(): + result.extend(_flatten_class(cls)) + return result + + +def _flatten_callable(c: dict) -> list: + result = [c] + for inner in c.get("inner_callables", {}).values(): + result.extend(_flatten_callable(inner)) + for inner_cls in c.get("inner_classes", {}).values(): + result.extend(_flatten_class(inner_cls)) + return result + + +def _flatten_class(cls: dict) -> list: + result = [] + for method in cls.get("methods", {}).values(): + result.extend(_flatten_callable(method)) + for inner in cls.get("inner_classes", {}).values(): + result.extend(_flatten_class(inner)) + return result \ No newline at end of file diff --git a/test/test_pycg_sharding.py b/test/test_pycg_sharding.py new file mode 100644 index 0000000..c0629e6 --- /dev/null +++ b/test/test_pycg_sharding.py @@ -0,0 +1,111 @@ +"""Tests for PyCG executor scoping: dependency exclusion and the max_iter cap. + +These drive the real PyCG wrapper, so they require ``pycg`` (a level-2 install +dependency). They are deliberately tiny (a few files) so they run fast. +""" +from pathlib import Path + +import pytest + +pytest.importorskip("PyCG") + +from codeanalyzer.semantic_analysis.pycg.pycg_analysis import ( + PyCG, + _PyCGCallableResolver, + _shard_symlink_root, +) + + +def test_max_iter_default_and_override(tmp_path): + p = PyCG(tmp_path) + assert p.max_iter == PyCG._PYCG_MAX_ITER == 50 + assert PyCG(tmp_path, max_iter=7).max_iter == 7 + assert PyCG(tmp_path, max_iter=-1).max_iter == -1 + + +def test_adaptive_decomposition_splits_runaways(tmp_path, monkeypatch): + """A shard that 'runs away' is re-decomposed until its pieces converge. + + Drives the real adaptive loop + planner, but stubs the PyCG runner with a + size threshold: shards larger than the threshold time out (runaway); smaller + ones converge and yield one synthetic edge per file. A 16-file coupled + cluster must therefore be split across rounds until every piece is small + enough, with no files lost. + """ + from codeanalyzer.schema.py_schema import PyCallEdge, PyCallable, PyModule + from codeanalyzer.semantic_analysis.pycg.pycg_analysis import _PyCGCallableResolver + + # A loosely-coupled chain of 40 modules: splittable (not one atomic cycle). + st, jedi = {}, [] + for i in range(40): + path = f"/proj/m{i}.py" + st[path] = PyModule( + file_path=path, module_name=f"m{i}", + functions={"f": PyCallable(signature=f"m{i}.f", name="f", path=path)}, + ) + if i: + jedi.append(PyCallEdge(source=f"m{i-1}.f", target=f"m{i}.f", weight=1, + provenance=["jedi"])) + + # threshold >= the decomposition floor (10) so pieces can shrink enough to converge. + pycg = PyCG(tmp_path, shard_ceiling=40) + threshold = 12 # shards with > 12 files "diverge" + rounds_seen = [] + + def fake_runner(shards): + rounds_seen.append([len(s) for s in shards]) + edges, runaways = [], [] + for files in shards: + if len(files) > threshold: + runaways.append(files) + else: + edges += [PyCallEdge(source=f, target="x", weight=1, provenance=["pycg"]) + for f in files] + return edges, runaways + + monkeypatch.setattr(pycg, "_run_fileset_shards_seq", fake_runner) + edges = pycg._build_sharded_planned(jedi, st, _PyCGCallableResolver(set())) + + assert len(rounds_seen) >= 2, "runaway shard was never decomposed" + # Every shard that was finally accepted is within the convergence threshold. + assert all(sz <= threshold for sz in rounds_seen[-1]) + # No files lost: one pycg edge per file across all 16 modules. + assert len({e.source for e in edges}) == 40 + + +def test_pycg_does_not_follow_into_in_tree_dependency(tmp_path): + """An in-tree ``.codeanalyzer`` venv under project_dir must stay a ghost. + + PyCG bounds analysis to its ``package`` directory; running inside the + symlink mini-project keeps that bound on project source only, so imports + into a bundled dependency are recorded as ghost edges but never analysed. + Regression guard for the dep-reach blowup. + """ + proj = tmp_path + app = proj / "app" + app.mkdir() + (app / "__init__.py").write_text("") + (app / "main.py").write_text("import bigdep\ndef run():\n return bigdep.work()\n") + + # A bundled dependency with many internal functions: if PyCG followed into + # it, dozens of bigdep.fN definitions/edges would appear. + dep = proj / ".codeanalyzer" / "venv" / "site-packages" / "bigdep" + dep.mkdir(parents=True) + body = "".join(f"def f{i}(x):\n return f{(i + 1) % 50}(x)\n" for i in range(50)) + body += "def work():\n return f0(1)\n" + (dep / "__init__.py").write_text(body) + + pycg = PyCG(proj) + pycg._ensure_pycg_loaded() + resolver = _PyCGCallableResolver(set()) + entry_points = [str(app / "__init__.py"), str(app / "main.py")] + with _shard_symlink_root(entry_points, proj) as (root, eps): + edges = pycg._run_pycg_batch(eps, root, resolver, prefix="") + + nodes = {n for e in edges for n in (e.source, e.target)} + # bigdep is reachable as a ghost target ... + assert any(n.startswith("bigdep") for n in nodes) + # ... but none of its internals were analysed. + assert not [n for n in nodes if n.startswith("bigdep.f")] + # and the real app edge is present. + assert any(e.source == "app.main.run" and e.target == "bigdep.work" for e in edges) diff --git a/test/test_shard_planner.py b/test/test_shard_planner.py new file mode 100644 index 0000000..fe6142c --- /dev/null +++ b/test/test_shard_planner.py @@ -0,0 +1,188 @@ +"""Unit tests for the Jedi-driven PyCG shard planner. + +These exercise the pure partitioning logic (no PyCG required): module-graph +construction, SCC atomicity, budget enforcement, and the cut-ratio metric. +""" +from typing import List + +import networkx as nx +import pytest + +from codeanalyzer.schema.py_schema import PyCallEdge, PyCallable, PyClass, PyModule +from codeanalyzer.semantic_analysis.pycg.shard_planner import ( + build_module_graph, + plan_shards, +) + + +# ---------------------------------------------------------------------------- +# Builders +# ---------------------------------------------------------------------------- + +def _module(name: str, file_path: str, func_names: List[str]) -> PyModule: + fns = { + fn: PyCallable(signature=f"{name}.{fn}", name=fn, path=file_path) + for fn in func_names + } + return PyModule(file_path=file_path, module_name=name, functions=fns) + + +def _edge(src: str, dst: str, w: int = 1) -> PyCallEdge: + return PyCallEdge(source=src, target=dst, weight=w, provenance=["jedi"]) + + +def _cut_ratio(g: nx.DiGraph, file_shards: List[List[str]]) -> float: + # Graph nodes are file paths; partitions are lists of file paths. + shard_of = {f: i for i, files in enumerate(file_shards) for f in files} + total = cut = 0.0 + for u, v, w in g.edges(data="weight", default=1): + total += w + if shard_of.get(u) != shard_of.get(v): + cut += w + return cut / total if total else 0.0 + + +# ---------------------------------------------------------------------------- +# build_module_graph +# ---------------------------------------------------------------------------- + +def test_module_graph_keys_nodes_by_file_not_name(): + # Two distinct files share the stem "models" (module_name collision); the + # graph must keep them as separate nodes keyed by path, not collapse them. + st = { + "/pkg_a/models.py": _module("models", "/pkg_a/models.py", ["f"]), + "/pkg_b/models.py": _module("models", "/pkg_b/models.py", ["h"]), + } + edges = [ + _edge("models.f", "models.h", 3), # but which file? mapped by definition + ] + g = build_module_graph(st, edges) + assert set(g.nodes) == {"/pkg_a/models.py", "/pkg_b/models.py"} + assert g.nodes["/pkg_a/models.py"]["module_name"] == "models" + + +def test_module_graph_projects_callables_to_files(): + st = { + "/a.py": _module("a", "/a.py", ["f", "g"]), + "/b.py": _module("b", "/b.py", ["h"]), + } + edges = [ + _edge("a.f", "b.h", 3), # cross-file -> kept + _edge("a.f", "a.g", 5), # intra-file -> dropped + _edge("a.g", "ext.lib.x"), # external target -> dropped + ] + g = build_module_graph(st, edges) + assert set(g.nodes) == {"/a.py", "/b.py"} + assert g.has_edge("/a.py", "/b.py") and g["/a.py"]["/b.py"]["weight"] == 3 + assert g.number_of_edges() == 1 + + +def test_isolated_files_are_nodes(): + st = {"/a.py": _module("a", "/a.py", ["f"]), "/b.py": _module("b", "/b.py", ["g"])} + g = build_module_graph(st, []) # no edges + assert set(g.nodes) == {"/a.py", "/b.py"} + assert g.number_of_edges() == 0 + + +# ---------------------------------------------------------------------------- +# plan_shards +# ---------------------------------------------------------------------------- + +def _coupled_clusters_project(): + """pkg_a <-> pkg_b heavy cross-package coupling + isolated leaves.""" + st, edges = {}, [] + for i in range(4): + st[f"/pkg_a/m{i}.py"] = _module(f"pkg_a.m{i}", f"/pkg_a/m{i}.py", ["f"]) + st[f"/pkg_b/m{i}.py"] = _module(f"pkg_b.m{i}", f"/pkg_b/m{i}.py", ["g"]) + for i in range(4): + edges.append(_edge(f"pkg_a.m{i}.f", f"pkg_b.m{i}.g", 10)) + edges.append(_edge(f"pkg_b.m{i}.g", f"pkg_a.m{i}.f", 8)) + for i in range(3): + edges.append(_edge(f"pkg_a.m{i}.f", f"pkg_a.m{i+1}.f", 2)) + return st, edges + + +def test_budget_is_respected(): + st, edges = _coupled_clusters_project() + plan = plan_shards(st, edges, budget=4) + assert plan.metrics["max_shard_files"] <= 4 + assert plan.metrics["oversized_shards"] == 0 + + +def test_every_module_assigned_exactly_once(): + st, edges = _coupled_clusters_project() + plan = plan_shards(st, edges, budget=4) + assigned = [m for shard in plan.module_shards for m in shard] + assert sorted(assigned) == sorted(m.module_name for m in st.values()) + assert len(assigned) == len(set(assigned)) # no module duplicated + + +def test_beats_naive_per_package_cut_ratio(): + st, edges = _coupled_clusters_project() + g = build_module_graph(st, edges) + # Naive baseline: one shard per top-level directory (e.g. /pkg_a/...). + naive = {} + for m in st.values(): + top = m.file_path.split("/")[1] + naive.setdefault(top, []).append(m.file_path) + naive_ratio = _cut_ratio(g, list(naive.values())) + + plan = plan_shards(st, edges, budget=4) + assert plan.metrics["cut_ratio"] < naive_ratio + + +def test_import_cycle_is_never_split(): + # util.x <-> helpers.y form a cross-package cycle; must co-locate even + # though they live in different top-level packages. + st = { + "/util/x.py": _module("util.x", "/util/x.py", ["a"]), + "/helpers/y.py": _module("helpers.y", "/helpers/y.py", ["b"]), + } + edges = [_edge("util.x.a", "helpers.y.b", 5), _edge("helpers.y.b", "util.x.a", 5)] + plan = plan_shards(st, edges, budget=10) + shard_with_util = next(s for s in plan.module_shards if "util.x" in s) + assert "helpers.y" in shard_with_util + + +def test_oversized_atomic_cycle_is_flagged_not_dropped(): + # A single import cycle of 6 modules with a budget of 3 cannot be split + # without breaking edges; it must survive as one oversized shard. + st, edges = {}, [] + names = [f"cyc.m{i}" for i in range(6)] + for i, n in enumerate(names): + st[f"/cyc/m{i}.py"] = _module(n, f"/cyc/m{i}.py", ["f"]) + for i in range(6): # ring -> one big SCC + edges.append(_edge(f"{names[i]}.f", f"{names[(i + 1) % 6]}.f", 1)) + plan = plan_shards(st, edges, budget=3) + assert plan.metrics["oversized_shards"] >= 1 + assigned = [m for shard in plan.module_shards for m in shard] + assert sorted(assigned) == sorted(names) # nothing dropped + + +def test_determinism(): + st, edges = _coupled_clusters_project() + a = plan_shards(st, edges, budget=4) + b = plan_shards(st, edges, budget=4) + norm = lambda p: sorted(sorted(s) for s in p.module_shards) + assert norm(a) == norm(b) + + +def test_no_file_dropped_on_stem_collision(): + # Regression: module_name is only the file stem, so many files collide on + # name (every __init__.py, models.py, ...). Keying by name would drop all + # but one per stem. Every FILE must land in exactly one shard. + st, edges = {}, [] + for pkg in ("a", "b", "c", "d"): + for stem in ("__init__", "models", "views"): + path = f"/{pkg}/{stem}.py" + st[path] = _module(stem, path, ["f"]) + plan = plan_shards(st, edges, budget=5) + assigned = sorted(f for shard in plan.shards for f in shard) + assert assigned == sorted(st.keys()) # all 12 files present + assert len(assigned) == len(set(assigned)) # none duplicated + + +def test_empty_project(): + plan = plan_shards({}, [], budget=4) + assert plan.shards == [] + assert plan.metrics["cut_ratio"] == 0.0