PK !
q tmn/__init__.py__version__ = '0.1.0'
PK ! ۻܞ tmn/compose.pyimport uuid
from tmn import configuration
volumes = ['chaindata']
networks = ['tmn_default']
environment = {
}
containers = {
'metrics': {
'image': 'tomochain/telegraf:latest',
'hostname': None,
'name': 'metrics',
'environment': {
'METRICS_ENDPOINT': None
},
'network': 'tmn_default',
'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'}
},
'detach': True
},
'tomochain': {
'image': 'tomochain/infra-tomochain:latest',
'name': 'tomochain',
'environment': {
'IDENTITY': None,
'PRIVATE_KEY': None,
'BOOTNODES': None,
'NETSTATS_HOST': None,
'NETSTATS_PORT': None,
'WS_SECRET': None
},
'network': 'tmn_default',
'ports': {'30303/udp': 30303, '30303/tcp': 30303},
'volumes': {
'chaindata': {'bind': '/tomochain/data', 'mode': 'rw'}
},
'detach': True
}
}
def process(name):
"""
Compose the containers with their variables
:param name: masternode name
:type name: str
"""
# custom
if configuration.read_conf('identity'):
identity = configuration.read_conf('identity')
else:
identity = '{}_{}'.format(name, uuid.uuid4().hex[:6])
configuration.write_conf('identity', identity)
containers['metrics']['hostname'] = identity
containers['tomochain']['environment']['IDENTITY'] = identity
for container in list(containers):
# add environment variables
for variable in list(containers[container]['environment']):
try:
containers[container]['environment'][variable] = (
environment[variable])
except KeyError:
# TODO add logging
pass
# rename containers
containers[container]['name'] = '{}_{}'.format(name, container)
PK ! .m. . tmn/configuration.pyimport sys
from clint import resources
import validators
from tmn import display
from tmn import networks
from tmn import compose
resources.init('tomochain', 'tmn')
name = None
def init(new_name=None, net=None, pkey=None):
"""
Init a configuration for a new masternode
:param new_name: new name of the masternode
:type new_name: str
:param net: network to use
:type net: str
:param pkey: private key to use as account for the masternode
:type pkey: str
"""
global name
create = False
conf_name = resources.user.read('name')
if conf_name:
if new_name:
display.warning_ignoring_start_options(conf_name)
name = conf_name
else:
if _validate_name(new_name):
name = new_name
create = True
elif not new_name:
display.error_start_not_initialized()
sys.exit()
else:
display.error_validation_option('--name',
'4 to 10 characters slug')
sys.exit()
if create:
if not net:
display.error_start_option_required('--net')
sys.exit()
elif not pkey:
display.error_start_option_required('--pkey')
sys.exit()
if not _validate_pkey(pkey):
display.error_validation_option('--pkey',
'64 characters hex string')
sys.exit()
else:
if net == 'devnet':
compose.environment = networks.devnet
if net == 'testnet':
compose.environment = networks.testnet
compose.environment['PRIVATE_KEY'] = pkey
resources.user.write('name', name)
def write_conf(conf, content):
"""
Write a configuration to a file
:param conf: name of the configuration
:type conf: str
:param content: content
:type content: str
"""
resources.user.write(conf, content)
def read_conf(conf):
"""
Read a configuration from a file
:param conf: name of the configuration
:type conf: str
:returns: the content of the configuration
:rtype: str
"""
return resources.user.read(conf)
def remove_conf(conf):
"""
remove a configuration file
:param conf: name of the configuration
:type conf: str
"""
resources.user.delete(conf)
def _validate_name(name):
"""
Validate a name string
:param name: name string
:type conf: str
:returns: is valid
:rtype: bool
"""
if (
name
and validators.slug(name)
and validators.length(name, min=4, max=10)
):
return True
else:
return False
def _validate_pkey(pkey):
"""
Validate a pkey string
:param name: pkey string
:type conf: str
:returns: is valid
:rtype: bool
"""
if (
pkey
and validators.length(pkey, min=64, max=64)
):
try:
int(pkey, 16)
return True
except ValueError:
return False
else:
return False
PK ! wD' ' tmn/display.pyimport 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/'
def newline(number=1):
"""
Print newlines
:param number: the number of newlines to print
:type number: int
"""
print('\n'*number, end='')
def style(function):
"""
Print and colorize strings with `pastel`
:param function: function to decorate
:type function: function
:returns: decorated function
:rtype: function
"""
def wrapper(*args, **kwargs):
print(pastel.colorize(function(*args, **kwargs)))
return wrapper
def style_no_new_line(function):
"""
Print and colorize strings with `pastel`. Don't add a new line at the end.
Decorator to print and colorize strings with `pastel`.
Don't add a new line at the end.
:param function: function to decorate
:type function: function
:returns: decorated function
:rtype: function
"""
def wrapper(*args):
print(pastel.colorize(function(*args)), end='', flush=True)
return wrapper
@style
def link(msg, url):
"""
Return a pastel formated string for browser links
:param msg: message
:type msg: str
:param url: website url
:type url: str
"""
return '{msg} {url}'.format(
msg=msg,
url=url
)
def link_docs():
"""
Custom link message for documentation
"""
link('Documentation on running a masternode:', help_url)
@style
def title(msg):
"""
Return a pastel formated title string
:param msg: title message
:type msg: str
:returns: subtitle formated
:rtype: str
"""
return '{msg}\n'.format(
msg=msg
)
def title_start_masternode(name):
"""
Title when starting a masternode
"""
title('Starting masternode {}:'.format(name))
def title_stop_masternode(name):
"""
Title when stopping a masternode
"""
title('Stopping masternode {}:'.format(name))
def title_status_masternode(name):
"""
Title when stopping a masternode
"""
title('Masternode {} status:'.format(name))
def title_inspect_masternode(name):
"""
Title when stopping a masternode
"""
title('Masternode {} details:'.format(name))
def title_remove_masternode(name):
"""
Title when stopping a masternode
"""
title('Removing masternode {}:'.format(name))
@style
def subtitle(msg):
"""
Return a pastel formated subtitle string
:param msg: subtitle message
:type msg: str
:returns: subtitle formated
:rtype: str
"""
return '{msg}\n'.format(
msg=msg
)
def subtitle_create_volumes():
"""
Subtitle when creating volumes
"""
subtitle('Volumes')
def subtitle_remove_volumes():
"""
Subtitle when removing volumes
"""
subtitle('Volumes')
def subtitle_create_networks():
"""
Subtitle when creating networks
"""
subtitle('Networks')
def subtitle_remove_networks():
"""
Subtitle when removing networks
"""
subtitle('Networks')
def subtitle_create_containers():
"""
Subtitle when creating containers
"""
subtitle('Containers')
def subtitle_remove_containers():
"""
Subtitle when removing containers
"""
subtitle('Containers')
@style
def detail(msg, content, indent=1):
"""
Return a pastel formated detail
:param msg: detail message
:type msg: str
:param content: detail content
:type content: str
:returns: `msg` formated
:rtype: str
"""
return (' '*indent
+ '{msg}:\n'.format(msg=msg)
+ ' '*indent
+ '{content}'.format(content=content))
def detail_identity(content):
"""
Custom detail message for the masternode identity
"""
detail('Unique identity', content)
def detail_coinbase(content):
"""
Custom detail message for the masternode coinbase address
"""
detail('Coinbase address (account public key)', content)
@style_no_new_line
def step(msg, indent=1):
"""
Return a pastel formated step with indentation.
One indent is two spaces.
:param msg: step message
:type msg: str
:param indent: number of idents
:type indent: int
:returns: `msg` formated
:rtype: str
"""
step = ' '*indent + '- {msg}... '.format(
msg=msg
)
return step
def step_create_masternode_volume(volume):
"""
Custom step message for docker volumes creation
"""
step('Creating {volume}'.format(
volume=volume
))
def step_remove_masternode_volume(volume):
"""
Custom step message for docker volumes removal
"""
step('Removing {volume}'.format(
volume=volume
))
def step_create_masternode_network(network):
"""
Custom step message for docker networks creatin
"""
step('Creating {network}'.format(
network=network
))
def step_remove_masternode_network(network):
"""
Custom step message for docker networks creatin
"""
step('Removing {network}'.format(
network=network
))
def step_create_masternode_container(container):
"""
Custom step message for docker container creation
"""
step('Creating {container}'.format(
container=container
))
def step_start_masternode_container(container):
"""
Custom step message for docker container starting
"""
step('Starting {container}'.format(
container=container
))
def step_remove_masternode_container(container):
"""
Custom step message for docker container starting
"""
step('Removing {container}'.format(
container=container
))
def step_stop_masternode_container(container):
"""
Custom step message for docker container stopping
"""
step('Stopping {container}'.format(
container=container
))
@style
def step_close(msg, color='green'):
"""
Return a pastel formated end of step
:param msg: task status of the step
:type msg: str
:returns: `msg` formated
:rtype: str
"""
return '{msg}>'.format(
msg=msg,
color=color
)
# TODO only use step_close_status
def step_close_created():
"""
Custom 'created' closing step message
"""
step_close('created')
def step_close_exists():
"""
Custom 'exists' closing step message
"""
step_close('exists')
def step_close_status(status):
"""
Custom 'status' closing step message
"""
step_close(status)
@style
def status(name='', status='absent', id='', status_color='red'):
"""
Return a pastel formated end of step
:param msg: task status of the step
:type msg: str
:returns: `msg` formated
:rtype: str
"""
if id:
return ' {name}\t{status}(>{id})>'.format(
name=name,
status=status,
color=status_color,
id=id
)
else:
return ' {name}\t{status}{id}>'.format(
name=name,
status=status,
color=status_color,
id=id
)
@style
def warning(msg, newline=True):
"""
Return a pastel formated string for warnings
:param msg: error message
:type msg: str
:returns: `msg` formated
:rtype: str
"""
before = ''
if newline:
before = '\n'
return before + '! warning: {msg}\n'.format(
msg=msg
)
def warning_ignoring_start_options(name):
"""
Custom warning when tmn is ignoring the start options
"""
warning(
'masternode {} is already configured\n'.format(name)
+ ' '
+ 'ignoring start options\n'
)
def warning_nothing_to_remove():
"""
Custom warning when tmn is removing docker objects but it's empty
"""
warning(
'nothing to remove', newline=False
)
def warning_remove_masternode(name):
"""
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):
"""
Return a pastel formated string for errors
:param msg: error message
:type msg: str
:returns: `msg` formated
:rtype: str
"""
return (
'\n! error: {msg}\n'.format(msg=msg)
+ ' '
+ 'need help? {}'.format(help_url)
)
def error_docker():
"""
Custom error when docker is not accessible
"""
error('could not access the docker daemon')
def error_docker_api():
"""
Custom error when docker is not accessible
"""
error('something went wrong while doing stuff with docker')
def error_start_not_initialized():
"""
Custom error when `tmn start` has never been used with the `--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):
"""
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, format):
"""
Custom error when an option format is not valide
"""
error(
'{} is not valid\n'.format(option)
+ ' it should be a {}'.format(format)
)
PK ! $E^' ^' tmn/masternode.pyimport sys
import docker as dockerpy
from tmn import compose
from tmn import display
from tmn import configuration
_client = None
def apierror(function):
"""
Decorator to catch `docker.errors.APIerror` Exception
:param function: function to decorate
:type function: function
:returns: decorated function
:rtype: function
"""
def wrapper(*args, **kwargs):
try:
function(*args, **kwargs)
except dockerpy.errors.APIError:
display.error_docker_api()
sys.exit()
return wrapper
def connect(url=None):
"""
Create the docker client. Try to ping the docker server to establish if
the connexion in successful
:param url: url to the docker deamon
:type url: str
:returns: is connected to Docker api
:rtype: bool
"""
global _client
if not url:
_client = dockerpy.from_env()
else:
_client = dockerpy.DockerClient(base_url=url)
return _ping()
def _ping():
"""
Try to ping the Docker daemon. Check if accessible.
:returns: is Docker api reachable
:rtype: bool
"""
try:
return _client.ping()
except Exception:
return False
def _create_volumes():
"""
Try to get the volumes defined in `compose.volumes`.
If it fails, create them.
"""
for volume in compose.volumes:
display.step_create_masternode_volume(volume)
try:
_client.volumes.get(volume)
display.step_close_exists()
except dockerpy.errors.NotFound:
_client.volumes.create(volume)
display.step_close_created()
display.newline()
def _remove_volumes():
"""
Remove Volumes
"""
for volume in compose.volumes:
display.step_remove_masternode_volume(volume)
try:
v = _client.volumes.get(volume)
v.remove(force=True)
display.step_close_status('removed')
except dockerpy.errors.NotFound:
display.step_close_status('absent')
display.newline()
def _create_networks():
"""
Try to get the networks defined in `compose.networks`.
If it fails, create them.
"""
for network in compose.networks:
display.step_create_masternode_network(network)
try:
_client.networks.get(network)
display.step_close_exists()
except dockerpy.errors.NotFound:
_client.networks.create(network)
display.step_close_created()
display.newline()
def _remove_networks():
"""
Remove networks
"""
for network in compose.networks:
display.step_remove_masternode_network(network)
try:
n = _client.networks.get(network)
n.remove()
display.step_close_status('removed')
except dockerpy.errors.NotFound:
display.step_close_status('absent')
display.newline()
def _get_existing_containers():
"""
Get from docker the containers defined in `compose.containers`.
:returns: The existing `docker.Container`
:rtype: list
"""
containers = {}
for key, value in compose.containers.items():
try:
container = _client.containers.get(value['name'])
containers[container.name] = container
except dockerpy.errors.NotFound:
pass
return containers
def _create_containers():
"""
Try to get the containers defined in `compose.containers`.
If it fails, create them.
:returns: The created or existing `docker.Container`
:rtype: list
"""
containers = {}
for key, value in compose.containers.items():
display.step_create_masternode_container(value['name'])
try:
container = _client.containers.get(value['name'])
display.step_close_exists()
except dockerpy.errors.NotFound:
# temporary, see https://github.com/docker/docker-py/issues/2101
_client.images.pull(value['image'])
container = _client.containers.create(**value)
display.step_close_created()
containers[container.name] = container
return containers
def _start_containers(containers):
"""
Verify the container status. If it's not restarting or running,
start them.
:param containers: dict of name:`docker.Container`
:type containers: dict
"""
for name, container in containers.items():
display.step_start_masternode_container(container.name)
container.reload()
# filtered status are:
# created|restarting|running|removing|paused|exited|dead
# might have to add all the status
if container.status in ['restarting', 'running']:
pass
elif container.status in ['paused']:
container.unpause()
elif container.status in ['created', 'exited', 'dead']:
container.start()
container.reload()
display.step_close_status(container.status)
def _remove_containers(containers):
"""
Remove given containers
:param containers: dict of name:`docker.Container`
:type containers: dict
"""
if not containers:
display.warning_nothing_to_remove()
else:
display.newline()
for name, container in containers.items():
display.step_remove_masternode_container(container.name)
container.remove(force=True)
display.step_close_status('removed')
if containers:
display.newline()
def _stop_containers(containers):
"""
Stop the given dict of `docker.Container`
:param containers: dict of name:`docker.Container`
:type containers: dict
"""
for name, container in containers.items():
display.step_stop_masternode_container(container.name)
container.reload()
# filtered status are:
# created|restarting|running|removing|paused|exited|dead
# might have to add all the status
if container.status in ['restarting', 'running', 'paused']:
container.stop()
elif container.status in ['created', 'exited', 'dead']:
pass
container.reload()
display.step_close_status(container.status)
def _status_containers(containers):
"""
Display the status of `CONTAINERS` w/ the passed list of `docker.Container`
:param containers: dict of `docker.Container`
:type containers: dict
"""
names = [
compose.containers[container]['name']
for container in list(compose.containers)
]
for name in names:
display_kwargs = {}
display_kwargs.update({'name': name})
try:
containers[name].reload()
if containers[name].status in ['running']:
display_kwargs.update({'status_color': 'green'})
display_kwargs.update({'status': containers[name].status})
display_kwargs.update({'id': containers[name].short_id})
except KeyError:
display_kwargs.update({'name': name})
display_kwargs.update({'name': name})
display.status(**display_kwargs)
def _get_coinbase():
"""
Retrieve the coinbase address from the account used by the masternode
:returns: coinbase address
:rtype: str
"""
container = _client.containers.get(compose.containers['tomochain']['name'])
return '0x' + container.exec_run(
'tomo account list --keystore keystore'
).output.decode("utf-8").replace('}', '{').split("{")[1]
def _get_identity():
"""
Retrieve the masternode identity
:returns: identity
:rtype: str
"""
container = _client.containers.get(compose.containers['tomochain']['name'])
return container.exec_run(
'/bin/sh -c "echo $IDENTITY"'
).output.decode("utf-8")
@apierror
def start(name):
"""
Start a masternode. Includes:
- process components
- creating volumes
- creating networks
- creating containers
- starting containers
:param name: name of the masternode
:type name: str
"""
compose.process(name)
display.subtitle_create_volumes()
_create_volumes()
display.subtitle_create_networks()
_create_networks()
display.subtitle_create_containers()
containers = _create_containers()
display.newline()
_start_containers(containers)
@apierror
def stop(name):
"""
Stop a masternode. Includes:
- process components
- getting the list of containers
- stoping them
:param name: name of the masternode
:type name: str
"""
compose.process(name)
containers = _get_existing_containers()
_stop_containers(containers)
@apierror
def status(name):
"""
Retrieve masternode status. Includes:
- process components
- getting the list of containers
- displaying their status
:param name: name of the masternode
:type name: str
"""
compose.process(name)
containers = _get_existing_containers()
_status_containers(containers)
@apierror
def remove(name):
"""
Remove masternode. Includes:
- process components
- stop containers
- remove containers, networks and volumes
- remove tmn persistent configuration
:param name: name of the masternode
:type name: str
"""
compose.process(name)
containers = _get_existing_containers()
display.subtitle_remove_containers()
_stop_containers(containers)
_remove_containers(containers)
display.subtitle_remove_networks()
_remove_networks()
display.subtitle_remove_volumes()
_remove_volumes()
configuration.remove_conf('name')
configuration.remove_conf('identity')
@apierror
def details(name):
"""
Remove masternode. Includes:
- process components
- stop containers
- remove containers, networks and volumes
- remove tmn persistent configuration
:param name: name of the masternode
:type name: str
"""
compose.process(name)
display.detail_identity(_get_identity())
display.detail_coinbase(_get_coinbase())
PK ! #6 | | tmn/networks.pytestnet = {
'METRICS_ENDPOINT': 'https://metrics.testnet.tomochain.com',
'BOOTNODES': (
'enode://4d3c2cc0ce7135c1778c6f1cfda623ab44b4b6db55289543d48ecf'
'de7d7111fd420c42174a9f2fea511a04cf6eac4ec69b4456bfaaae0e5bd236'
'107d3172b013@52.221.28.223:30301,enode://298780104303fcdb37a84'
'c5702ebd9ec660971629f68a933fd91f7350c54eea0e294b0857f1fd2e8dba'
'2869fcc36b83e6de553c386cf4ff26f19672955d9f312@13.251.101.216:3'
'0301,enode://46dba3a8721c589bede3c134d755eb1a38ae7c5a4c69249b8'
'317c55adc8d46a369f98b06514ecec4b4ff150712085176818d18f59a9e631'
'1a52dbe68cff5b2ae@13.250.94.232:30301'
),
'NETSTATS_HOST': 'stats.testnet.tomochain.com',
'NETSTATS_PORT': '443',
'WS_SECRET': (
'anna-coal-flee-carrie-zip-hhhh-tarry-laue-felon-rhine'
)
}
devnet = {
'METRICS_ENDPOINT': 'https://metrics.devnet.tomochain.com',
'BOOTNODES': (
'enode://f3d3d5d6cd0fdde8996722ff5b5a92f331029b2dcbdb9748f50db1'
'421851a939eb660bf81a7ec7f359454aa0fd65fe4c03ae5c6bb2382b34dfaa'
'ca7eb6ecaf4e@52.77.194.164:30301,enode://34b923ddfcba1bfafdd1a'
'c7a030436f9fbdc565919189f5e62c8cadd798c239b5807a26ab7f6b96a442'
'00eb0399d1ebc2d9c1be94d2a774c8cc7660ff4c10367@13.228.93.232:30'
'301,enode://e2604862d18049e025f294d63d537f9f54309ff09e45ed69ff'
'4f18c984831f5ef45370053355301e3a4da95aba2698c6116f4d2a347e5a5e'
'0a3152ac0ae0f574@18.136.42.72:30301'
),
'NETSTATS_HOST': 'stats.devnet.tomochain.com',
'NETSTATS_PORT': '443',
'WS_SECRET': (
'torn-fcc-caper-drool-jelly-zip-din-fraud-rater-darn'
)
}
PK ! 8>9Ts s
tmn/tmn.pyimport sys
import click
from tmn import __version__
from tmn import display
from tmn import masternode
from tmn import configuration
conf = None
@click.group(help='Tomo MasterNode (tmn) is a cli tool to help you run a '
+ 'Tomochain masternode')
@click.option('--dockerurl',
metavar='URL',
help='Url to the docker server')
@click.version_option(version=__version__)
def main(dockerurl):
"""
Cli entrypoint.
:param config: path to the configuration file
:type config: str
"""
if not masternode.connect(url=dockerurl):
display.error_docker()
sys.exit()
@click.command(help='Display Tomochain documentation link')
def docs():
"""
Link to the documentation
:param open: open the link in your navigator
:type open: bool
"""
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, net, pkey):
"""
Start the containers needed to run a masternode
"""
configuration.init(name, net, pkey)
display.title_start_masternode(configuration.name)
masternode.start(configuration.name)
@click.command(help='Stop your Tomochain masternode')
def stop():
"""
Stop the containers needed to run a masternode
"""
configuration.init()
display.title_stop_masternode(configuration.name)
masternode.stop(configuration.name)
@click.command(help='Show the status of your Tomochain masternode')
def status():
"""
Show the status of the masternode containers
"""
configuration.init()
display.title_status_masternode(configuration.name)
masternode.status(configuration.name)
@click.command(help='Show details about your Tomochain masternode')
def inspect():
"""
Show details about the tomochain masternode
"""
configuration.init()
display.title_inspect_masternode(configuration.name)
masternode.details(configuration.name)
@click.command(help='Remove your Tomochain masternode')
@click.option('--confirm', is_flag=True)
def remove(confirm):
"""
Remove the masternode completly (containers, networks volumes)
"""
configuration.init()
if not confirm:
display.warning_remove_masternode(configuration.name)
sys.exit()
display.title_remove_masternode(configuration.name)
masternode.remove(configuration.name)
main.add_command(docs)
main.add_command(start)
main.add_command(stop)
main.add_command(status)
main.add_command(inspect)
main.add_command(remove)
PK !H
# $ $ tmn-0.1.0.dist-info/entry_points.txtN+I/N.,()*ͳb= M PK !HW"T T tmn-0.1.0.dist-info/WHEEL
A
н#J."jm)Afb~
ڡ5G7hiޅF4+-3ڦ/̖?XPK !HH ? tmn-0.1.0.dist-info/METADATAWko6_qXXrM4YנMRbhD$@߾sI9/]{xxs,(:evɉ|i7U%|@;SɴMݠ2T(J>\jϗ'O?$B7*M~p^Zm
G+QRJr@2~u(+]qazo7{{995V?Z3'
v+x.CR7ދel{k,=Ϟ^mg/H(,=e5b3Q*Xd;,t˭=żf/V'F UX)W:734'Dq՛FE{V̔Ks;Qpwj.Çt2}+khH\1{azr&KSw+U\!>]Ir'')G#h͔xT5MCtNu
:kfV Q
?( 1U4@xnҬ=_7Ge됆Htz0 7HXӽzp4YE[?cecY/kP\WˮB,p(XXN?^_!]~~eN_p̦i[K[.^sdenSs$\PE,HX_++8%`E4pG֥/e<\ܷ
^D]FSk@8Zh~!pBaQyL,9/^UzqRL7RRD#&EczylB
.vc5ii=Sc@6h·pyn c|zW0EKb%S*;5yĺh1&߄V{tWˊKˬ:hF7倾786hc5t
msnkf`b! cynH 1ga!be.Ռ[,2SH^l'^&?4c,ǫ8͗PDS{#)u!'4ԡz穅+G#wi@-pcJ^Jn ]p]ne .HOaE{ފō6`jo
1@4hUX()?_J%b]HIK#85*#kf>CDɒ?[hwoA#t'.{,T4DåZPK !H43 tmn-0.1.0.dist-info/RECORDmɒ@ {UPl9 ɀE@q#SIm
*8qx棣;h; &:y"8]L~Y|\ڪ^b_/䜈}/y^}g!{R N]<掳fpcP.P^N$>'=*JNmAjA$]60@cR uI_:%+7j?֜u&%q8 aGW
b{K4"