PK!TVtmn/__init__.pyimport logging __version__ = '0.2.5' handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('[%(levelname)s] %(message)s')) logger = logging.getLogger('tmn') logger.addHandler(handler) logger.setLevel('CRITICAL') PK!+jtmn/configuration.pyimport logging import sys import uuid import docker from clint import resources from slugify import slugify from tmn import display from tmn.elements.network import Network from tmn.elements.service import Service from tmn.elements.volume import Volume from tmn.environments import environments logger = logging.getLogger('tmn') resources.init('tomochain', 'tmn') class Configuration: """docstring for Configuration.""" def __init__(self, name: str = None, net: str = None, pkey: str = None, start: bool = False, docker_url: str = None) -> None: self.networks = {} self.services = {} self.volumes = {} self.name = name self.net = net self.pkey = pkey or '' self.id = None self.force_recreate = False if not docker_url: self.client = docker.from_env() else: self.client = docker.DockerClient(base_url=docker_url) try: self.client.ping() except Exception as e: logger.error(e) display.error_docker() sys.exit('\n') if resources.user.read('name'): self._load() elif start: self._write() else: display.error_start_not_initialized() sys.exit('\n') self._compose() def _new_id(self) -> str: return uuid.uuid4().hex[:6] def _load(self) -> None: if self.name or self.net or self.pkey: display.warning_ignoring_start_options(resources.user.read('name')) self.id = resources.user.read('id') self.name = resources.user.read('name') self.net = resources.user.read('net') ####################################################################### # this is a dirty fix for retro compatiblity # # can be removed in some future version # # old `tmn` don't write the `id` or `net` option to disk # # screw with `tmn update` # # will ask to recreate as this is a breaking change # ####################################################################### if not self.id or not self.net: self.force_recreate = True self.net = 'devnet' ####################################################################### def _write(self) -> None: if not self.name: display.error_start_option_required('--name') sys.exit('\n') elif not self.net: display.error_start_option_required('--net') sys.exit('\n') elif not self.pkey: display.error_start_option_required('--pkey') sys.exit('\n') self._validate() self.id = self._new_id() resources.user.write('id', self.id) resources.user.write('name', self.name) resources.user.write('net', self.net) def _compose(self) -> None: self.networks['tmn'] = Network( name='{}_tmn'.format(self.name), client=self.client ) self.volumes['chaindata'] = Volume( name='{}_chaindata'.format(self.name), client=self.client ) tag = 'testnet' if self.net == 'testnet' else 'latest' self.services['metrics'] = Service( name='{}_metrics'.format(self.name), hostname='{}_{}'.format(self.name, self.id), image='tomochain/telegraf:{}'.format(tag), network=self.networks['tmn'].name, volumes={ '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro' }, '/sys': {'bind': '/rootfs/sys', 'mode': 'ro'}, '/proc': {'bind': '/rootfs/proc', 'mode': 'ro'}, '/etc': {'bind': '/rootfs/etc', 'mode': 'ro'} }, client=self.client ) self.services['tomochain'] = Service( name='{}_tomochain'.format(self.name), image='tomochain/node:{}'.format(tag), network=self.networks['tmn'].name, environment={ 'IDENTITY': '{}_{}'.format(self.name, self.id), 'PRIVATE_KEY': '{}'.format(self.pkey) }, volumes={ self.volumes['chaindata'].name: { 'bind': '/tomochain', 'mode': 'rw' } }, ports={'30303/udp': 30303, '30303/tcp': 30303}, client=self.client ) for container, variables in environments[self.net].items(): for variable, value in variables.items(): self.services[container].add_environment( name=variable, value=value ) def _validate(self) -> None: self.name = slugify(self.name) if len(self.name) < 5 or len(self.name) > 30: display.error_validation_option('--name', '5 to 30 characters ' 'slug') sys.exit('\n') if len(self.pkey) != 64: display.error_validation_option('--pkey', '64 characters hex ' 'string') sys.exit('\n') def remove(self) -> None: resources.user.delete('id') resources.user.delete('name') resources.user.delete('net') PK!I""tmn/display.pyimport sys import pastel pastel.add_style('hg', 'green') pastel.add_style('hgb', 'green', options=['bold']) pastel.add_style('hy', 'yellow') pastel.add_style('hyb', 'yellow', options=['bold']) pastel.add_style('link', 'yellow', options=['underscore']) pastel.add_style('und', options=['underscore']) pastel.add_style('warning', 'yellow') pastel.add_style('error', 'red') help_url = 'https://docs.tomochain.com/get-started/run-node' def newline(number: int = 1) -> None: "Print newlines" print('\n'*number, end='') def style(function): "Print and colorize strings with `pastel`" def wrapper(*args, **kwargs) -> None: print(pastel.colorize(function(*args, **kwargs))) return wrapper def style_no_new_line(function): "Print and colorize strings with `pastel`. No newline." def wrapper(*args) -> None: print(pastel.colorize(function(*args)), end='', flush=True) return wrapper @style def link(msg: str, url: str) -> str: "Return a pastel formated string for browser links" return '{msg} {url}'.format( msg=msg, url=url ) def link_docs() -> None: "Custom link message for documentation" link('Documentation on running a masternode:', help_url) @style def title(msg: str) -> str: "Return a pastel formated title string" return '{msg}\n'.format( msg=msg ) def title_start_masternode(name: str) -> None: "Title when starting a masternode" title('Starting masternode {}:'.format(name)) def title_stop_masternode(name: str) -> None: "Title when stopping a masternode" title('Stopping masternode {}:'.format(name)) def title_status_masternode(name: str) -> None: "Title when viewing a masternode status" title('Masternode {} status:'.format(name)) def title_inspect_masternode(name: str) -> None: "Title when inspecting a masternode" title('Masternode {} details:'.format(name)) def title_update_masternode(name: str) -> None: "Title when updating a masternode" title('Updating masternode {}:'.format(name)) def title_remove_masternode(name: str) -> None: "Title when removing a masternode" title('Removing masternode {}:'.format(name)) @style def subtitle(msg: str) -> str: "Return a pastel formated subtitle string" return '{msg}\n'.format( msg=msg ) def subtitle_create_volumes() -> None: "Subtitle when creating volumes" subtitle('Volumes') def subtitle_remove_volumes() -> None: "Subtitle when removing volumes" subtitle('Volumes') def subtitle_create_networks() -> None: "Subtitle when creating networks" subtitle('Networks') def subtitle_remove_networks() -> None: "Subtitle when removing networks" subtitle('Networks') def subtitle_create_containers() -> None: "Subtitle when creating containers" subtitle('Containers') def subtitle_remove_containers() -> None: "Subtitle when removing containers" subtitle('Containers') @style def detail(msg, content: str, indent: int = 1) -> str: "Return a pastel formated detail" return (' '*indent + '{msg}:\n'.format(msg=msg) + ' '*indent + '{content}'.format(content=content)) def detail_identity(content: str) -> None: "Custom detail message for the masternode identity" detail('Unique identity', content) def detail_coinbase(content: str) -> None: "Custom detail message for the masternode coinbase address" detail('Coinbase address (account public key)', content) @style_no_new_line def step(msg: str, indent: int = 1) -> str: "Return a pastel formated step with indentation." step = ' '*indent + '- {msg}... '.format( msg=msg ) return step def step_create_volume(name: str) -> None: "Custom step message for docker volumes creation" step('Creating {name}'.format( name=name )) def step_remove_volume(name: str) -> None: "Custom step message for docker volumes removal" step('Removing {name}'.format( name=name )) def step_create_network(name: str) -> None: "Custom step message for docker networks creation" step('Creating {name}'.format( name=name )) def step_remove_network(name: str) -> None: "Custom step message for docker networks creation" step('Removing {name}'.format( name=name )) def step_create_container(name: str) -> None: "Custom step message for docker container creation" step('Creating {name}'.format( name=name )) def step_start_container(name: str) -> None: "Custom step message for docker container starting" step('Starting {name}'.format( name=name )) def step_remove_container(name: str) -> None: "Custom step message for docker container starting" step('Removing {name}'.format( name=name )) def step_stop_container(name: str) -> None: "Custom step message for docker container stopping" step('Stopping {name}'.format( name=name )) @style def step_close(msg: str, color: str = 'green') -> str: "Return a pastel formated end of step" return '{msg}'.format( msg=msg, color=color ) def step_close_ok() -> None: "Custom close message when all ok" msg = 'ok' if sys.stdout.encoding == 'UTF-8': msg = '✔' step_close(msg) def step_close_nok() -> None: "Custom close message when all ok" msg = 'error' if sys.stdout.encoding == 'UTF-8': msg = '✗' step_close(msg, 'red') @style def status(name: str = '', status: str = 'absent', id: str = '', status_color: str = 'red') -> str: "Return a pastel formated end of step" if id: return ' {name}\t{status}({id})'.format( name=name, status=status, color=status_color, id=id ) else: return ' {name}\t{status}'.format( name=name, status=status, color=status_color, ) @style def warning(msg: str, newline: bool = True) -> str: "Return a pastel formated string for warnings" before = '' if newline: before = '\n' return before + '! warning: {msg}\n'.format( msg=msg ) def warning_ignoring_start_options(name: str) -> None: "Custom warning when tmn is ignoring the start options" warning( 'masternode {} is already configured\n'.format(name) + ' ' + 'ignoring start options\n' ) def warning_remove_masternode(name: str) -> None: "Custom warning when tmn is removing masternode but no confirmation" warning( 'you are about to remove masternode {}\n'.format(name) + ' ' + 'this will permanently delete its data\n' + ' ' + 'to confirm use the --confirm flag' ) @style def error(msg: str) -> str: "Return a pastel formated string for errors" return ( '\n! error: {msg}\n'.format(msg=msg) + ' ' + 'need help? {}'.format(help_url) ) def error_docker() -> None: "Custom error when docker is not accessible" error('could not access the docker daemon') def error_docker_api() -> None: "Custom error when docker is not accessible" error('something went wrong while doing stuff with docker') def error_start_not_initialized() -> None: "Custom error when `tmn start` has never been used with `--name` option" error( 'tmn doesn\'t manage any masternode yet\n' ' please use ' 'tmn start --name to get started' ) def error_start_option_required(option: str) -> None: "Custom error when `tmn start` is used with name but not the other options" error( '{} is required when starting a new masternode' .format(option) ) def error_validation_option(option: str, format: str) -> None: "Custom error when an option format is not valide" error( '{} is not valid\n'.format(option) + ' it should be a {}'.format(format) ) def error_breaking_change() -> None: "Custom error when breaking changes need to recreate the node" error( 'latest update introduced some non-retrocompatible changes\n' ' ' 'please recreate your node by deleting it\n' ' ' 'tmn remove --confirm\n' ' ' 'and creating it back with the same options as the old one\n' ' ' 'tmn start --name ... --net ... --pkey ...' ) PK!kKtmn/elements/__init__.pyfrom tmn.elements.network import Network from tmn.elements.service import Service from tmn.elements.volume import Volume __all__ = ['Network', 'Service', 'Volume'] PK!`Htmn/elements/network.pyimport logging import docker logger = logging.getLogger('tmn') class Network: """docstring for Network.""" def __init__(self, client: docker.DockerClient, name: str) -> None: self.name = name self.network = None self.client = client try: self.network = self.client.networks.get(self.name) except docker.errors.NotFound as e: logger.debug('network {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def create(self) -> bool: "create docker network" try: if self.network: return True else: self.client.networks.create(self.name) return True except docker.errors.APIError as e: logger.error(e) except docker.errors.ConnectionRefusedError as e: logger.error(e) def remove(self) -> bool: "delete docker network" try: if self.network: self.network.remove() self.network = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False PK!Jsstmn/elements/service.pyimport logging from typing import Dict, Union import docker logger = logging.getLogger('tmn') class Service: """docstring for Service.""" def __init__( self, client: docker.DockerClient, name: str, image: str = None, hostname: str = None, network: str = None, environment: Dict[str, str] = {}, volumes: Dict[str, Dict[str, str]] = {}, ports: Dict[str, Dict[str, str]] = {}, log_driver: str = 'json-file', log_opts: Dict[str, str] = {'max-size': '3g'} ) -> None: self.container = False self.image = image self.name = name self.environment = environment self.network = network self.hostname = hostname self.volumes = volumes self.ports = ports self.log_driver = log_driver self.log_opts = log_opts self.client = client try: self.container = self.client.containers.get(self.name) except docker.errors.NotFound as e: logger.debug('container {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def add_environment(self, name: str, value: str) -> None: "add a new environment to the service" self.environment[name] = value def add_volume(self, source: str, target: str, mode: str = 'rw') -> None: "add a new volume to the service" self.volumes[source] = {'bind': target, 'mode': mode} def add_port(self, source: str, target: str) -> None: "add a new port mapping to the service" self.ports[source] = target def create(self) -> bool: "create the service container" try: if self.container: return True else: self.client.images.pull(self.image) self.container = self.client.containers.create( image=self.image, name=self.name, hostname=self.hostname, network=self.network, environment=self.environment, volumes=self.volumes, log_config={'type': self.log_driver, 'config': self.log_opts}, detach=True ) return True except docker.errors.APIError as e: logger.error(e) return False def start(self) -> bool: "start the service container" try: if self.container: self.container.reload() if self.container.status in ['running', 'restarting']: return True elif self.container.status in ['paused']: self.container.unpause() return True else: self.container.start() return True else: return False except docker.errors.APIError as e: logger.error(e) return False def status(self) -> Union[str, bool]: "return the status of the service container" try: if self.container: self.container.reload() return self.container.status else: return 'absent' except docker.errors.APIError as e: logger.error(e) return False def execute(self, command: str) -> Union[str, bool]: "return the result of a command on the service container" try: if self.container: self.container.reload() return self.container.exec_run( '/bin/sh -c "{}"'.format(command) ).output.decode("utf-8") else: return False except docker.errors.APIError as e: logger.error(e) return False def stop(self) -> bool: "stop the service container" try: if self.container: self.container.reload() if self.container.status in ['created', 'exited', 'dead']: return True else: self.container.stop() return True else: return True except docker.errors.APIError as e: logger.error(e) return False def remove(self) -> bool: "stop the service container" try: if self.container: self.container.remove(force=True) self.container = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False def update(self) -> bool: "update the service container" try: if self.container: self.container.remove(force=True) return self.create() and self.start() else: return False except docker.errors.APIError as e: logger.error(e) return False PK!Ctmn/elements/volume.pyimport logging import docker logger = logging.getLogger('tmn') class Volume: """docstring for Volume.""" def __init__(self, client: docker.DockerClient, name: str) -> None: self.name = name self.volume = None self.client = client try: self.volume = self.client.volumes.get(self.name) except docker.errors.NotFound as e: logger.debug('volume {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def create(self) -> bool: "create docker volumes" try: if self.volume: return True else: self.client.volumes.create(self.name) return True except docker.errors.APIError as e: logger.error(e) return False def remove(self) -> bool: "delete docker volume" try: if self.volume: self.volume.remove(force=True) self.volume = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False PK!}WJ>aatmn/environments.pyenvironments = { 'testnet': { 'tomochain': { 'BOOTNODES': ( 'enode://4d3c2cc0ce7135c1778c6f1cfda623ab44b4b6db55289543d48ec' 'fde7d7111fd420c42174a9f2fea511a04cf6eac4ec69b4456bfaaae0e5bd2' '36107d3172b013@52.221.28.223:30301,enode://298780104303fcdb37' 'a84c5702ebd9ec660971629f68a933fd91f7350c54eea0e294b0857f1fd2e' '8dba2869fcc36b83e6de553c386cf4ff26f19672955d9f312@13.251.101.' '216:30301,enode://46dba3a8721c589bede3c134d755eb1a38ae7c5a4c6' '9249b8317c55adc8d46a369f98b06514ecec4b4ff150712085176818d18f5' '9a9e6311a52dbe68cff5b2ae@13.250.94.232:30301' ), 'NETSTATS_HOST': 'stats.testnet.tomochain.com', 'NETSTATS_PORT': '443', 'NETWORK_ID': '89', 'WS_SECRET': ( 'anna-coal-flee-carrie-zip-hhhh-tarry-laue-felon-rhine' ) }, 'metrics': { 'METRICS_ENDPOINT': 'https://metrics.testnet.tomochain.com' } }, 'devnet': { 'tomochain': { 'BOOTNODES': ( 'enode://f3d3d5d6cd0fdde8996722ff5b5a92f331029b2dcbdb9748f50db' '1421851a939eb660bf81a7ec7f359454aa0fd65fe4c03ae5c6bb2382b34df' 'aaca7eb6ecaf4e@52.77.194.164:30301,enode://34b923ddfcba1bfafd' 'd1ac7a030436f9fbdc565919189f5e62c8cadd798c239b5807a26ab7f6b96' 'a44200eb0399d1ebc2d9c1be94d2a774c8cc7660ff4c10367@13.228.93.2' '32:30301,enode://e2604862d18049e025f294d63d537f9f54309ff09e45' 'ed69ff4f18c984831f5ef45370053355301e3a4da95aba2698c6116f4d2a3' '47e5a5e0a3152ac0ae0f574@18.136.42.72:30301' ), 'NETSTATS_HOST': 'stats.devnet.tomochain.com', 'NETSTATS_PORT': '443', 'NETWORK_ID': '90', 'WS_SECRET': ( 'torn-fcc-caper-drool-jelly-zip-din-fraud-rater-darn' ) }, 'metrics': { 'METRICS_ENDPOINT': 'https://metrics.devnet.tomochain.com' } } } PK!攅!! tmn/tmn.pyimport logging import sys import click from tmn import display from tmn import __version__ from tmn.configuration import Configuration logger = logging.getLogger('tmn') docker_url = None @click.group(help=('Tomo MasterNode (tmn) is a cli tool to help you run a Tomo' 'chain masternode')) @click.option('--debug', is_flag=True, help='Enable debug mode') @click.option('--docker', metavar='URL', help='Url to the docker server') @click.version_option(version=__version__) def main(debug: bool, docker: str) -> None: "Cli entrypoint" global docker_url if debug: logger.setLevel('DEBUG') logger.debug('Debugging enabled') docker_url = docker @click.command(help='Display TomoChain documentation link') def docs() -> None: "Link to the documentation" display.link_docs() @click.command(help='Start your TomoChain masternode') @click.option('--name', metavar='NAME', help='Your masternode\'s name') @click.option('--net', type=click.Choice(['testnet', 'devnet']), help='The environment your masternode will connect to') @click.option('--pkey', metavar='KEY', help=('Private key of the account your ' 'masternode will collect rewards ' 'on')) def start(name: str, net: str, pkey: str) -> None: "Start the containers needed to run a masternode" configuration = Configuration(name=name, net=net, pkey=pkey, start=True, docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_start_masternode(configuration.name) # volumes display.subtitle_create_volumes() for _, value in configuration.volumes.items(): display.step_create_volume(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # networks display.subtitle_create_networks() for _, value in configuration.networks.items(): display.step_create_network(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # container # create display.subtitle_create_containers() for _, value in configuration.services.items(): display.step_create_container(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # start for _, value in configuration.services.items(): display.step_start_container(value.name) if value.start(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Stop your TomoChain masternode') def stop() -> None: "Stop the masternode containers" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_stop_masternode(configuration.name) for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Show the status of your TomoChain masternode') def status() -> None: "Show the status of the masternode containers" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_status_masternode(configuration.name) for _, service in configuration.services.items(): status = service.status() if status and status == 'absent': display.status( name=service.name ) if status and status in ['running']: display.status( name=service.name, status=status, id=service.container.short_id, status_color='green' ) elif status: display.status( name=service.name, status=status, id=service.container.short_id, ) else: display.status( name=service.name, status='error' ) display.newline() @click.command(help='Show details about your TomoChain masternode') def inspect() -> None: "Show details about the tomochain masternode" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_inspect_masternode(configuration.name) identity = configuration.services['tomochain'].execute( 'echo $IDENTITY' ) or 'container not running' display.detail_identity(identity) display.newline() coinbase = configuration.services['tomochain'].execute( 'tomo account list --keystore keystore 2> /dev/null | head -n 1 | cut ' '-d"{" -f 2 | cut -d"}" -f 1' ) if coinbase: coinbase = '0x{}'.format(coinbase) else: coinbase = 'container not running' display.detail_coinbase(coinbase) display.newline() @click.command(help='Update your masternode') def update() -> None: "Update the tomochain masternode with the lastest images" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_update_masternode(configuration.name) display.subtitle_remove_containers() # containers # stop for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() # remove for _, service in configuration.services.items(): display.step_remove_container(service.name) if service.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # create for _, value in configuration.services.items(): display.step_create_container(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # start for _, value in configuration.services.items(): display.step_start_container(value.name) if value.start(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Remove your TomoChain masternode') @click.option('--confirm', is_flag=True) def remove(confirm: bool) -> None: "Remove the masternode (containers, networks volumes)" configuration = Configuration(docker_url=docker_url) if not confirm: display.warning_remove_masternode(configuration.name) sys.exit('\n') display.title_remove_masternode(configuration.name) display.subtitle_remove_containers() # containers # stop for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() # remove for _, service in configuration.services.items(): display.step_remove_container(service.name) if service.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # networks display.subtitle_remove_networks() for _, network in configuration.networks.items(): display.step_remove_network(network.name) if network.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # volumes display.subtitle_remove_volumes() for _, volume in configuration.volumes.items(): display.step_remove_volume(volume.name) if volume.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() configuration.remove() main.add_command(docs) main.add_command(start) main.add_command(stop) main.add_command(status) main.add_command(inspect) main.add_command(update) main.add_command(remove) PK!H #$$tmn-0.2.5.dist-info/entry_points.txtN+I/N.,()*ͳb= MPK!HW"TTtmn-0.2.5.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!HIwtmn-0.2.5.dist-info/METADATAXmo6_qkA-9mu0li$ˆ!`Ze"SEe7^ %|^d‹wi2zљ(|U]e)|*w^XsS[(j)e\5Lܠ4T(NU*N"Ul%{¤a)ﱅO/^I%M,KUZY9 chÝd:َ ᜚(7a08*kf28ZZQE=.T퀱Ptc[+j/ɭ(Ks8:1S3xCo|yǛ.qG=<I{{?&[k$'d7yN~X#a)YT\G"^5B1v7vEɜ_&z{,z$]jU^j_+<( Þ¹XD(lO9Bz̪|ꟁiq,\VkUV:G-}E:|+/|pqTO>cX۷*fEY1S.NUbl@fOc: }Us_LEQDq y҉Mt|sr$g0,h؋NK]@3dRx>+x,*uW.|Bg ͊Ʉ +!5Յ'?ERX9e*jQ =ЛD4!"WZeF:@K0H~=A}=@EbcE?pTH8;>vʉϥ,YǤ"E.RL11F%Eh4Ev@˧2[l(RW=s/OXxc`$S?c9m^\S9g}jAv.ot]돕TQPQzxl O`4n:pMFlz`T~Bm: ipjhy #LL"ZIO}1OeQ[5(imf·뚀9n-M =_V)icS-B=V8iugB n/OϮÇ#= ܣ<62UR2FצTJdjsOɆ&`)73}&OI6M=}6u>p̄nQMc4268N $ eJ"ϳz%z6T!,S-Xn-܄dI_żCcQo@RTS}hC+S ;cPzMa[EPb>\S#f<% e/]Qo e ĚRm 8PK!H+ptmn-0.2.5.dist-info/RECORDmr@}2 .f\$l(@[CO?NRrRYW!y)NV#&>/錰=lI|AuA`~.6m8No]㓴O:3wR)MQ@?i[䢮-⠣!% jU+MiZn! !ge4&ʣ"¤㱺$nyaֵUSLQ⩖`'3e=qdHuh{PN^{G 9> 'rpon {]#1˼+g%{*rsޒM 9c8IӦ= cY8diFh2u@OJoJg<D '\W$*mt⳯=)Hb+*>^)G<Ҍ^U㑁|캴ҕ޿ꂈUc;yiƗJeod܎e'qn-p6ԇ~\ 虚--a>IO0a^P aaYtmn/environments.pyPK!攅!! ebtmn/tmn.pyPK!H #$$tmn-0.2.5.dist-info/entry_points.txtPK!HW"TTwtmn-0.2.5.dist-info/WHEELPK!HIwtmn-0.2.5.dist-info/METADATAPK!H+p4tmn-0.2.5.dist-info/RECORDPK s܎