PK!s<י**flake8_nitpick/__init__.py"""Main module.""" __version__ = "0.8.0" PK!9X  flake8_nitpick/config.py"""Configuration of the plugin itself.""" import itertools import logging from pathlib import Path from shutil import rmtree from typing import Any, Dict, List, MutableMapping, Optional, Union import requests import toml from slugify import slugify from flake8_nitpick.constants import ( DEFAULT_NITPICK_STYLE_URL, NAME, NITPICK_STYLE_TOML, ROOT_FILES, ROOT_PYTHON_FILES, UNIQUE_SEPARATOR, ) from flake8_nitpick.files.pyproject_toml import PyProjectTomlFile from flake8_nitpick.files.setup_cfg import SetupCfgFile from flake8_nitpick.generic import climb_directory_tree, flatten, rmdir_if_empty, unflatten from flake8_nitpick.types import PathOrStr, TomlDict, YieldFlake8Error from flake8_nitpick.utils import NitpickMixin LOG = logging.getLogger("flake8.nitpick") class NitpickConfig(NitpickMixin): """Plugin configuration, read from the project config.""" error_base_number = 200 _singleton_instance: Optional["NitpickConfig"] = None def __init__(self) -> None: """Init instance.""" self.root_dir: Optional[Path] = None self.cache_dir: Optional[Path] = None self.main_python_file: Optional[Path] = None self.pyproject_dict: MutableMapping[str, Any] = {} self.tool_nitpick_dict: Dict[str, Any] = {} self.style_dict: MutableMapping[str, Any] = {} self.nitpick_dict: MutableMapping[str, Any] = {} self.files: Dict[str, Any] = {} def find_root_dir(self, starting_file: PathOrStr) -> bool: """Find the root dir of the Python project: the dir that has one of the `ROOT_FILES`. Also clear the cache dir the first time the root dir is found. """ if self.root_dir: return True found_files = climb_directory_tree( starting_file, ROOT_FILES + (PyProjectTomlFile.file_name, SetupCfgFile.file_name) ) if not found_files: LOG.error("No files found while climbing directory tree from %s", str(starting_file)) return False self.root_dir = found_files[0].parent self.clear_cache_dir() return True def find_main_python_file(self) -> bool: """Find the main Python file in the root dir, the one that will be used to report Flake8 warnings.""" if not self.root_dir: return False for the_file in itertools.chain( [self.root_dir / root_file for root_file in ROOT_PYTHON_FILES], self.root_dir.glob("*.py") ): if the_file.exists(): self.main_python_file = Path(the_file) LOG.info("Found the file %s", the_file) return True return False def clear_cache_dir(self) -> None: """Clear the cache directory (on the project root or on the current directory).""" if not self.root_dir: return cache_root: Path = self.root_dir / ".cache" self.cache_dir = cache_root / NAME rmtree(str(self.cache_dir), ignore_errors=True) rmdir_if_empty(cache_root) def load_toml(self) -> YieldFlake8Error: """Load TOML configuration from files.""" pyproject_path: Path = self.root_dir / PyProjectTomlFile.file_name if pyproject_path.exists(): self.pyproject_dict: TomlDict = toml.load(str(pyproject_path)) self.tool_nitpick_dict: TomlDict = self.pyproject_dict.get("tool", {}).get("nitpick", {}) try: self.style_dict: TomlDict = self.find_styles() except FileNotFoundError as err: yield self.flake8_error(2, str(err)) return self.nitpick_dict: TomlDict = self.style_dict.get("nitpick", {}) self.files = self.nitpick_dict.get("files", {}) def find_styles(self) -> TomlDict: """Search for one or multiple style files.""" style_value: Union[str, List[str]] = self.tool_nitpick_dict.get("style", "") styles: List[str] = [style_value] if isinstance(style_value, str) else style_value all_flattened: TomlDict = {} for style in styles: if style.startswith("http"): # If the style is a URL, save the contents in the cache dir style_path = self.load_style_from_url(style) LOG.info("Loading style from URL: %s", style_path) elif style: style_path = Path(style) if not style_path.exists(): raise FileNotFoundError(f"Style file does not exist: {style}") LOG.info("Loading style from file: %s", style_path) else: paths = climb_directory_tree(self.root_dir, [NITPICK_STYLE_TOML]) if paths: style_path = paths[0] LOG.info("Loading style from directory tree: %s", style_path) else: style_path = self.load_style_from_url(DEFAULT_NITPICK_STYLE_URL) LOG.info( "Loading default Nitpick style %s into local file %s", DEFAULT_NITPICK_STYLE_URL, style_path ) flattened_style_dict: TomlDict = flatten(toml.load(str(style_path)), separator=UNIQUE_SEPARATOR) all_flattened.update(flattened_style_dict) return unflatten(all_flattened, separator=UNIQUE_SEPARATOR) def load_style_from_url(self, url: str) -> Path: """Load a style file from a URL.""" if not self.cache_dir: raise FileNotFoundError("Cache dir does not exist") response = requests.get(url) if not response.ok: raise FileNotFoundError(f"Error {response} fetching style URL {url}") contents = response.text style_path = self.cache_dir / f"{slugify(url)}.toml" self.cache_dir.mkdir(parents=True, exist_ok=True) style_path.write_text(contents) return style_path @classmethod def get_singleton(cls) -> "NitpickConfig": """Init the global singleton instance of the plugin configuration, needed by all file checkers.""" if cls._singleton_instance is None: cls._singleton_instance = cls() return cls._singleton_instance @classmethod def reset_singleton(cls): """Reset the singleton instance. Useful on automated tests, to simulate ``flake8`` execution.""" cls._singleton_instance = None PK!87r@@flake8_nitpick/constants.py"""Constants.""" NAME = "flake8-nitpick" ERROR_PREFIX = "NIP" NITPICK_STYLE_TOML = "nitpick-style.toml" DEFAULT_NITPICK_STYLE_URL = f"https://raw.githubusercontent.com/andreoliwa/flake8-nitpick/master/{NITPICK_STYLE_TOML}" ROOT_PYTHON_FILES = ("setup.py", "manage.py", "autoapp.py") ROOT_FILES = ("requirements*.txt", "Pipfile") + ROOT_PYTHON_FILES #: Special unique separator for :py:meth:`flatten()` and :py:meth:`unflatten()`, # to avoid collision with existing key values (e.g. the default dot separator "." can be part of a pyproject.toml key). UNIQUE_SEPARATOR = "$#@" PK! flake8_nitpick/files/__init__.py"""Files that are checked by Nitpick.""" # TODO: load all modules under files/*, so get_subclasses() detects them. import flake8_nitpick.files.pre_commit # noqa PK!2 flake8_nitpick/files/base.py"""Base file checker.""" from pathlib import Path from flake8_nitpick.types import TomlDict, YieldFlake8Error from flake8_nitpick.utils import NitpickMixin class BaseFile(NitpickMixin): """Base class for file checkers.""" file_name: str error_base_number = 300 def __init__(self) -> None: """Init instance.""" from flake8_nitpick.config import NitpickConfig self.config = NitpickConfig.get_singleton() self.error_prefix = f"File: {self.file_name}: " self.file_path: Path = self.config.root_dir / self.file_name # Configuration for this file as a TOML dict, taken from the style file. self.file_dict: TomlDict = self.config.style_dict.get(self.toml_key, {}) # Nitpick configuration for this file as a TOML dict, taken from the style file. self.nitpick_file_dict: TomlDict = self.config.nitpick_dict.get("files", {}).get(self.file_name, {}) @property def toml_key(self): """Remove the dot in the beginning of the file name, otherwise it's an invalid TOML key.""" return self.file_name.lstrip(".") def check_exists(self) -> YieldFlake8Error: """Check if the file should exist; if there is style configuration for the file, then it should exist. The file should exist when there is any rule configured for it in the style file, TODO: add this to the docs """ should_exist: bool = self.config.files.get(self.toml_key, bool(self.file_dict or self.nitpick_file_dict)) file_exists = self.file_path.exists() if should_exist and not file_exists: suggestion = self.suggest_initial_contents() phrases = ["Missing file"] missing_message = self.nitpick_file_dict.get("missing_message", "") if missing_message: phrases.append(missing_message) if suggestion: phrases.append(f"Suggested content:\n{suggestion}") yield self.flake8_error(1, ". ".join(phrases)) elif not should_exist and file_exists: yield self.flake8_error(2, "File should be deleted") elif file_exists: yield from self.check_rules() def check_rules(self) -> YieldFlake8Error: """Check rules for this file. It should be overridden by inherited class if they need.""" return [] def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return "" PK!_"flake8_nitpick/files/pre_commit.py"""Checker for the .pre-commit-config.yaml config file.""" from typing import Any, Dict, List, Tuple import dictdiffer import yaml from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import find_object_by_key from flake8_nitpick.types import YieldFlake8Error class PreCommitFile(BaseFile): """Check the pre-commit config file.""" file_name = ".pre-commit-config.yaml" error_base_number = 330 KEY_REPOS = "repos" KEY_HOOKS = "hooks" KEY_REPO = "repo" def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" suggested = self.file_dict.copy() for repo in suggested.get(self.KEY_REPOS, []): repo[self.KEY_HOOKS] = yaml.safe_load(repo[self.KEY_HOOKS]) return yaml.dump(suggested, default_flow_style=False) def check_rules(self) -> YieldFlake8Error: """Check the rules for the pre-commit hooks.""" actual = yaml.safe_load(self.file_path.open()) or {} if self.KEY_REPOS not in actual: yield self.flake8_error(1, "Missing 'repos' in file") return actual_root = actual.copy() actual_root.pop(self.KEY_REPOS, None) expected_root = self.file_dict.copy() expected_root.pop(self.KEY_REPOS, None) for diff_type, key, values in dictdiffer.diff(expected_root, actual_root): if diff_type == dictdiffer.REMOVE: yield from self.show_missing_keys(key, values) elif diff_type == dictdiffer.CHANGE: yield from self.compare_different_keys(key, values[0], values[1]) yield from self.check_repos(actual) def check_repos(self, actual: Dict[str, Any]): """Check the repositories configured in pre-commit.""" actual_repos: List[dict] = actual[self.KEY_REPOS] or [] expected_repos: List[dict] = self.file_dict.get(self.KEY_REPOS, []) for index, expected_repo_dict in enumerate(expected_repos): repo_name = expected_repo_dict.get(self.KEY_REPO) if not repo_name: yield self.flake8_error(2, f"Style file is missing {self.KEY_REPO!r} key in repo #{index}") continue actual_repo_dict = find_object_by_key(actual_repos, self.KEY_REPO, repo_name) if not actual_repo_dict: yield self.flake8_error(3, f"Repo {repo_name!r} does not exist under {self.KEY_REPOS!r}") continue if self.KEY_HOOKS not in actual_repo_dict: yield self.flake8_error(4, f"Missing {self.KEY_HOOKS!r} in repo {repo_name!r}") continue actual_hooks = actual_repo_dict.get(self.KEY_HOOKS) or [] yaml_expected_hooks = expected_repo_dict.get(self.KEY_HOOKS) if not yaml_expected_hooks: yield self.flake8_error(5, f"Style file is missing {self.KEY_HOOKS!r} in repo {repo_name!r}") continue expected_hooks: List[dict] = yaml.safe_load(yaml_expected_hooks) for expected_dict in expected_hooks: hook_id = expected_dict.get("id") if not hook_id: yield self.flake8_error(6, f"Style file is missing 'id' in hook:\n{expected_dict!r}") continue actual_dict = find_object_by_key(actual_hooks, "id", hook_id) if not actual_dict: expected_yaml = self.format_hook(expected_dict) yield self.flake8_error(7, f"Missing hook with id {hook_id!r}:\n{expected_yaml}") continue def show_missing_keys(self, key, values: List[Tuple[str, Any]]): """Show the keys that are not present in a section.""" missing = dict(values) output = yaml.dump(missing, default_flow_style=False) yield self.flake8_error(8, f"Missing keys:\n{output}") def compare_different_keys(self, key, raw_expected: Any, raw_actual: Any): """Compare different keys.""" if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)): # A boolean "True" or "true" might have the same effect on YAML. actual = str(raw_actual).lower() expected = str(raw_expected).lower() else: actual = raw_actual expected = raw_expected if actual != expected: example = yaml.dump({key: raw_expected}, default_flow_style=False) yield self.flake8_error(9, f"Expected value {raw_expected!r} in key, got {raw_actual!r}\n{example}") @staticmethod def format_hook(expected_dict: dict) -> str: """Format the hook so it's easy to copy and paste it to the .yaml file: ID goes first, indent with spaces.""" lines = yaml.dump(expected_dict) output: List[str] = [] for line in lines.split("\n"): if line.startswith("id:"): output.insert(0, f" - {line}") else: output.append(f" {line}") return "\n".join(output) PK!^4ZZ&flake8_nitpick/files/pyproject_toml.py"""Checker for the pyproject.toml config file.""" import toml from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import flatten, unflatten from flake8_nitpick.types import YieldFlake8Error class PyProjectTomlFile(BaseFile): """Check pyproject.toml.""" file_name = "pyproject.toml" error_base_number = 310 def check_rules(self) -> YieldFlake8Error: """Check missing key/value pairs in pyproject.toml.""" actual = flatten(self.config.pyproject_dict) expected = flatten(self.file_dict) if expected.items() <= actual.items(): return missing_dict = unflatten({k: v for k, v in expected.items() if k not in actual}) if missing_dict: missing_toml = toml.dumps(missing_dict) yield self.flake8_error(1, f"Missing values:\n{missing_toml}") diff_dict = unflatten({k: v for k, v in expected.items() if k in actual and expected[k] != actual[k]}) if diff_dict: diff_toml = toml.dumps(diff_dict) yield self.flake8_error(2, f"Different values:\n{diff_toml}") PK!YY!flake8_nitpick/files/setup_cfg.py"""Checker for the setup.cfg config file.""" import itertools from configparser import ConfigParser from io import StringIO from typing import Any, List, Set, Tuple import dictdiffer from flake8_nitpick.files.base import BaseFile from flake8_nitpick.types import YieldFlake8Error class SetupCfgFile(BaseFile): """Check setup.cfg.""" file_name = "setup.cfg" error_base_number = 320 COMMA_SEPARATED_VALUES = "comma_separated_values" comma_separated_values: Set[str] expected_sections: Set[str] missing_sections: Set[str] def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return self.get_missing_output() def get_missing_output(self, actual_sections: Set[str] = None) -> str: """Get a missing output string example from the missing sections in setup.cfg.""" self.expected_sections = set(self.file_dict.keys()) self.missing_sections = self.expected_sections - (actual_sections or set()) if self.missing_sections: missing_cfg = ConfigParser() for section in sorted(self.missing_sections): missing_cfg[section] = self.file_dict[section] return self.get_example_cfg(missing_cfg) return "" def check_rules(self) -> YieldFlake8Error: """Check missing sections and missing key/value pairs in setup.cfg.""" if not self.file_path.exists(): return self.comma_separated_values = set(self.nitpick_file_dict.pop(self.COMMA_SEPARATED_VALUES, [])) setup_cfg = ConfigParser() setup_cfg.read_file(self.file_path.open()) actual_sections = set(setup_cfg.sections()) missing = self.get_missing_output(actual_sections) if missing: yield self.flake8_error(1, f"Missing sections:\n{missing}") generators = [] for section in self.expected_sections - self.missing_sections: expected_dict = self.file_dict[section] actual_dict = dict(setup_cfg[section]) for diff_type, key, values in dictdiffer.diff(expected_dict, actual_dict): if diff_type == dictdiffer.CHANGE: generators.append(self.compare_different_keys(section, key, values[0], values[1])) elif diff_type == dictdiffer.REMOVE: generators.append(self.show_missing_keys(section, key, values)) for error in itertools.chain(*generators): yield error def compare_different_keys(self, section, key, raw_expected: Any, raw_actual: Any) -> YieldFlake8Error: """Compare different keys, with special treatment when they are lists or numeric.""" combined = f"{section}.{key}" if combined in self.comma_separated_values: # The values might contain spaces actual_set = {s.strip() for s in raw_actual.split(",")} expected_set = {s.strip() for s in raw_expected.split(",")} missing = expected_set - actual_set if missing: yield self.flake8_error(2, f"Missing values in key\n[{section}]\n{key} = {','.join(sorted(missing))}") return if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)): # A boolean "True" or "true" has the same effect on setup.cfg. actual = str(raw_actual).lower() expected = str(raw_expected).lower() else: actual = raw_actual expected = raw_expected if actual != expected: yield self.flake8_error( 3, f"Expected value {raw_expected!r} in key, got {raw_actual!r}\n[{section}]\n{key} = {raw_expected}" ) def show_missing_keys(self, section, key, values: List[Tuple[str, Any]]) -> YieldFlake8Error: """Show the keys that are not present in a section.""" missing_cfg = ConfigParser() missing_cfg[section] = dict(values) output = self.get_example_cfg(missing_cfg) yield self.flake8_error(4, f"Missing keys in section:\n{output}") @staticmethod def get_example_cfg(config_parser: ConfigParser) -> str: """Print an example of a config parser in a string instead of a file.""" string_stream = StringIO() config_parser.write(string_stream) output = string_stream.getvalue().strip() return output PK!;$qE E flake8_nitpick/generic.py"""Generic functions and classes.""" import collections from pathlib import Path from typing import Any, Iterable, List, Optional from flake8_nitpick.types import PathOrStr def get_subclasses(cls): """Recursively get subclasses of a parent class.""" subclasses = [] for subclass in cls.__subclasses__(): subclasses.append(subclass) subclasses += get_subclasses(subclass) return subclasses def flatten(dict_, parent_key="", separator="."): """Flatten a nested dict.""" items = [] for key, value in dict_.items(): new_key = parent_key + separator + key if parent_key else key if isinstance(value, collections.abc.MutableMapping): items.extend(flatten(value, new_key, separator=separator).items()) else: items.append((new_key, value)) return dict(items) def unflatten(dict_, separator="."): """Turn back a flattened dict into a nested dict.""" items = {} for k, v in dict_.items(): keys = k.split(separator) sub_items = items for ki in keys[:-1]: try: sub_items = sub_items[ki] except KeyError: sub_items[ki] = {} sub_items = sub_items[ki] sub_items[keys[-1]] = v return items def climb_directory_tree(starting_path: PathOrStr, file_patterns: Iterable[str]) -> Optional[List[Path]]: """Climb the directory tree looking for file patterns.""" current_dir: Path = Path(starting_path).absolute() if current_dir.is_file(): current_dir = current_dir.parent while current_dir.root != str(current_dir): for root_file in file_patterns: found_files = list(current_dir.glob(root_file)) if found_files: return found_files current_dir = current_dir.parent return None def find_object_by_key(list_: List[dict], search_key: str, search_value: Any) -> dict: """Find an object in a list, using a key/value pair to search.""" for obj in list_: if obj.get(search_key) == search_value: return obj return {} def rmdir_if_empty(path_or_str: PathOrStr): """Remove the directory if empty.""" path = Path(path_or_str) if not path.exists(): return try: next(path.iterdir()) except StopIteration: path.rmdir() PK!/T flake8_nitpick/plugin.py"""Plugin module.""" import itertools import logging from pathlib import Path import attr from flake8_nitpick import __version__ from flake8_nitpick.config import NitpickConfig from flake8_nitpick.constants import NAME, ROOT_PYTHON_FILES from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import get_subclasses from flake8_nitpick.types import YieldFlake8Error from flake8_nitpick.utils import NitpickMixin LOG = logging.getLogger("flake8.nitpick") @attr.s(hash=False) class NitpickChecker(NitpickMixin): """Main plugin class.""" # Plugin config name = NAME version = __version__ # NitpickMixin error_base_number = 100 # Attributes config: NitpickConfig # Plugin arguments passed by Flake8 tree = attr.ib(default=None) filename = attr.ib(default="(none)") def run(self) -> YieldFlake8Error: """Run the check plugin.""" self.config = NitpickConfig().get_singleton() if not self.config.find_root_dir(self.filename): yield self.flake8_error(1, "No root dir found (is this a Python project?)") return if not self.config.find_main_python_file(): yield self.flake8_error( 2, "None of those Python files was found in the root dir" + f" {self.config.root_dir}: {', '.join(ROOT_PYTHON_FILES)}", ) return current_python_file = Path(self.filename) if current_python_file.absolute() != self.config.main_python_file.absolute(): # Only report warnings once, for the main Python file of this project. LOG.info("Ignoring file: %s", self.filename) return LOG.info("Nitpicking file: %s", self.filename) yield from itertools.chain(self.config.load_toml(), self.check_absent_files()) for checker_class in get_subclasses(BaseFile): checker = checker_class() yield from checker.check_exists() return [] def check_absent_files(self) -> YieldFlake8Error: """Check absent files.""" for file_name, delete_message in self.config.files.get("absent", {}).items(): file_path: Path = self.config.root_dir / file_name if not file_path.exists(): continue full_message = f"File {file_name} should be deleted" if delete_message: full_message += f": {delete_message}" yield self.flake8_error(3, full_message) PK!ˑ"  flake8_nitpick/types.py"""Types.""" from pathlib import Path from typing import Any, Dict, Generator, List, Tuple, Type, Union PathOrStr = Union[Path, str] TomlDict = Dict[str, Any] Flake8Error = Tuple[int, int, str, Type] YieldFlake8Error = Union[List, Generator[Flake8Error, Any, Any]] PK!j]]flake8_nitpick/utils.py"""Utilities.""" from flake8_nitpick.constants import ERROR_PREFIX from flake8_nitpick.types import Flake8Error class NitpickMixin: """A helper mixin to raise flake8 errors.""" error_base_number: int = 0 error_prefix: str = "" def flake8_error(self, error_number: int, error_message: str) -> Flake8Error: """Return a flake8 error as a tuple.""" final_number = self.error_base_number + error_number from flake8_nitpick.plugin import NitpickChecker return 1, 0, f"{ERROR_PREFIX}{final_number} {self.error_prefix}{error_message.strip()}", NitpickChecker PK!H6=/flake8_nitpick-0.8.0.dist-info/entry_points.txtNINK(I+ϋ ed&g䔦gYA٩E\\PK!p̘88&flake8_nitpick-0.8.0.dist-info/LICENSEMIT License Copyright (c) 2018 Wagner Augusto Andreoli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU$flake8_nitpick-0.8.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H 'flake8_nitpick-0.8.0.dist-info/METADATAXmo_[Dvܝڴ5{1n(Z+j|ݥe>3KJIAD>0t22VLE?dfb{mXhW>JDF'M43*S]W UJ+JXh.bH%+mK&"of"DBƦVut)qݸ5.UY? +ɵsMvze>EbT鍜VǪ6j6I fIIĺ&Si03Himyk= U.u6;K 9W(8f/_D&~4^dZ ߨU']ml&^PgZd/) 5gD.F>'Zlkȶ!Kq^!:\uǃ+WPbiS9Y~]252I,|) _+ϑYW_}Pr6ҨAY̴kĹe7:`9TgecJZqL:Nݡ%F[Ge݁o3^Uծ4h߯7FWEIPumS9ztSp}Rn 8'`us}34kFVM{#wWH&J;c2tk䃶g0֟ uI^%o_]@'evw=^QJ&iUӥLRYZӷ}'+S,+3Qqsp=NN/ξ{Y/1?5rUqYn_娃k*O0Ai 2gNF~ˇ)?ɣ 86";6!OMDf d5+   v]Y" &%Fd>cʔ(A%/hXYYt (^ q\[e#> Ux//e<cdL`<)c.$q"{OQwbt 5j'Dz>&%2(/ CAA-֑8`Lwn֬rg4qkH|\eV3c^ MX4"% AmƬԅX*.?cA!h)M+єJSs?Q+fȷ kb?G??ScjS =&s%.h>@L=4O=эmԓQG~_o/]uv:YxjE#2+P߂:xEǢ\OR-gE;RwefKVeNvrrÿ/|4[4+mh"FHs*$2ٞw.G,2ݨ%զkJNFя/Y`{T2^pkЬ3ޫ\kGMUq7u"6qG%qJ68OYxNXǃjnn@lƒMtJx4>OEG 9"ޣŰL X&[!2tW%T%rBZ BOأO ~!C{..ƵaIǴˋczMf S QmvP|1|oVFЋ|]w&l6GdKiuLgh@%X$J4d>Қh} \[SIW0!# |C Z!RwlJRWTz"'Ϭ}GBm{hUxg'ړuw#/Ha*|M?Zv>=!ޑy4z:KG[u]E=,^`Nj'LV)ͽe魙Q0,{*،=12kgm'FtZkZOڻ.w9dw޻jPò>i*6_#B8_gOC00;lClʻL;gd,6yCN#F_ ގn_d#>͕nSiOh ztFD*/vig\Ðj!yњ^Π_-E6< *\}Oŧ_A3dmw#[CL%(y<İ0.𹪗bt FSSCTSҡþO5訃ʡitxXzqtwۀZk B@R`ti9,PK!Hi2%flake8_nitpick-0.8.0.dist-info/RECORD˒H}? T"\R).*!B&O?51NY՗qNo H[~& |&[;3J˒F[esp%*Tknr=>J O\Q|Ŧ'džs\9 v҃a zo#< Hh(y29K}D :LYgv.LEe<==M(n'xxM;mǀE{t