├── tests ├── __init__.py ├── commands │ ├── test_version.py │ ├── test_serial.py │ ├── test_create.py │ ├── test_info.py │ ├── test_remove.py │ └── test_add.py ├── conftest.py ├── fixtures.py ├── utils │ ├── test_utils.py │ └── test_sync.py └── core │ └── test_packages.py ├── piku ├── template │ ├── project │ │ └── code.py │ ├── README.md │ ├── pyproject.toml │ └── .gitignore ├── __init__.py ├── core │ ├── utils │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── watcher.py │ │ ├── sync.py │ │ └── utils.py │ ├── errors.py │ ├── config.py │ ├── device.py │ ├── locker.py │ └── packages.py ├── commands │ ├── version.py │ ├── __init__.py │ ├── info.py │ ├── upgrade.py │ ├── install.py │ ├── remove.py │ ├── deploy.py │ ├── serial.py │ ├── add.py │ └── create.py └── main.py ├── poetry.toml ├── setup.py ├── .editorconfig ├── pyproject.toml ├── LICENSE ├── .gitignore ├── README.md ├── .pylintrc └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piku/template/project/code.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piku/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /piku/template/README.md: -------------------------------------------------------------------------------- 1 | # {{project}} 2 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /piku/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | from .sync import sync 3 | from .cache import cache 4 | from .watcher import watch 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # this is included only to support pip install -e of a poetry project so we can use project in editable mode 2 | 3 | import setuptools; setuptools.setup() 4 | -------------------------------------------------------------------------------- /piku/template/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.piku] 2 | project = "{{project}}" 3 | piku = "{{piku}}" 4 | circuit-python = "{{circuit_python}}" 5 | {% if board %} 6 | board = "{{board}}" 7 | {% endif %} 8 | 9 | [tool.piku.dependencies] 10 | -------------------------------------------------------------------------------- /tests/commands/test_version.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | from piku.commands.version import version_command 3 | 4 | 5 | def test_version(capsys): 6 | version_command(SimpleNamespace()) 7 | out, _ = capsys.readouterr() 8 | assert '.' in out 9 | -------------------------------------------------------------------------------- /tests/commands/test_serial.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | from piku.commands.serial import serial_command 3 | 4 | 5 | def test_serial(capsys): 6 | serial_command(SimpleNamespace(serial='bogus')) 7 | out, _ = capsys.readouterr() 8 | assert 'Connecting' in out 9 | -------------------------------------------------------------------------------- /piku/commands/version.py: -------------------------------------------------------------------------------- 1 | try: 2 | from importlib import metadata 3 | except ImportError: # for Python<3.8 4 | import importlib_metadata as metadata 5 | 6 | 7 | def get_version(): 8 | return metadata.version('piku') 9 | 10 | def version_command(args): 11 | print(get_version()) 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | 11 | [*.md] 12 | max_line_length = 0 13 | trim_trailing_whitespace = false 14 | 15 | [*.{json,jsonl,yml,yaml,toml}] 16 | charset = utf-8 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /piku/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .add import add_command 2 | from .deploy import deploy_command 3 | from .info import info_command 4 | from .install import install_command 5 | from .upgrade import upgrade_command 6 | from .create import create_command 7 | from .remove import remove_command 8 | from .serial import serial_command 9 | from .version import version_command 10 | -------------------------------------------------------------------------------- /piku/core/utils/cache.py: -------------------------------------------------------------------------------- 1 | from cachy import CacheManager 2 | from piku.core import config 3 | 4 | 5 | config = { 6 | 'default': 'file', 7 | 'serializer': 'json', 8 | 'stores': { 9 | 'file': { 10 | 'driver': 'file', 11 | 'path': config.cache_path 12 | }, 13 | 'dict': { 14 | 'driver': 'dict' 15 | } 16 | } 17 | } 18 | 19 | cache = CacheManager(config) 20 | -------------------------------------------------------------------------------- /piku/core/errors.py: -------------------------------------------------------------------------------- 1 | class PikuError(Exception): 2 | pass 3 | 4 | class PackageIndexNotFound(PikuError): 5 | def __init__(self, circuitpython_version): 6 | super().__init__(f'No package index found for CircuitPython version {circuitpython_version}') 7 | 8 | class PackageNotFound(PikuError): 9 | pass 10 | 11 | class VersionNotFound(PikuError): 12 | def __init__(self, package, version): 13 | super().__init__(f'{package} {version}') 14 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | '--all', action='store_true', default=False, help='run all tests, including those marked as slow' 7 | ) 8 | 9 | def pytest_configure(config): 10 | config.addinivalue_line('markers', 'slow: mark test as slow to run') 11 | 12 | def pytest_collection_modifyitems(config, items): 13 | if config.getoption('--all'): 14 | return 15 | skip_slow = pytest.mark.skip(reason='skipping slow tests, use --slow option to run') 16 | for item in items: 17 | if 'slow' in item.keywords: 18 | item.add_marker(skip_slow) 19 | -------------------------------------------------------------------------------- /piku/commands/info.py: -------------------------------------------------------------------------------- 1 | import os 2 | from piku.commands.version import get_version 3 | from piku.core import config, utils, packages 4 | 5 | 6 | def info_command(args): 7 | print(f'Piku Version: {get_version()}') 8 | print(f'Data directory: {config.data_path}') 9 | print(f'Backup directory: {config.backup_path}') 10 | 11 | if args.clear_cache: 12 | print('Clearing cache...') 13 | utils.remove(config.cache_path) 14 | os.makedirs(config.cache_path, exist_ok=True) 15 | print('Done') 16 | 17 | if args.packages: 18 | print('Availiable packages...') 19 | for package in packages.all(): 20 | print(f' * {package}') 21 | print('Done') 22 | -------------------------------------------------------------------------------- /tests/commands/test_create.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from types import SimpleNamespace 4 | from piku.core import utils 5 | from piku.commands.create import create_command 6 | 7 | 8 | def test_create(capsys): 9 | # create a temp directory 10 | project = 'bogus' 11 | test_path = os.path.join(tempfile.gettempdir(), 'piku') 12 | utils.remove(test_path) 13 | os.makedirs(test_path, exist_ok=True) 14 | cwd = os.getcwd() 15 | os.chdir(test_path) 16 | 17 | # create project 18 | create_command(SimpleNamespace(project=project, directory=None)) 19 | out, _ = capsys.readouterr() 20 | assert project in out 21 | assert 'pyproject.toml' in os.listdir(os.path.join(test_path, project)) 22 | utils.remove(test_path) 23 | os.chdir(cwd) 24 | -------------------------------------------------------------------------------- /piku/commands/upgrade.py: -------------------------------------------------------------------------------- 1 | from piku.core import config 2 | from piku.commands.add import add 3 | 4 | 5 | def upgrade_command(args): 6 | # check that we are in a piku project directory 7 | if not config.valid(): 8 | print('Failed: unable to find piku project in current directory.') 9 | return 10 | 11 | # re-add all packages as latest 12 | total_conflicts = set() 13 | dependencies = config.get('dependencies') 14 | for package in dependencies: 15 | previous, current, conflicts = add(package, 'latest') 16 | total_conflicts = total_conflicts.union(conflicts) 17 | 18 | # note conflicts 19 | if total_conflicts: 20 | print('Note, there may be multiple version requirements for following packages:') 21 | for c in total_conflicts: 22 | print(f' * {c}') 23 | -------------------------------------------------------------------------------- /piku/commands/install.py: -------------------------------------------------------------------------------- 1 | from piku.core import config 2 | from piku.commands.add import add 3 | 4 | 5 | def install_command(args): 6 | # check that we are in a piku project directory 7 | if not config.valid(): 8 | print('Failed: unable to find piku project in current directory.') 9 | return 10 | 11 | # add all dependencies 12 | total_conflicts = set() 13 | dependencies = config.get('dependencies') 14 | for package in dependencies: 15 | constraint = dependencies[package] 16 | previous, current, conflicts = add(package, constraint) 17 | total_conflicts = total_conflicts.union(conflicts) 18 | 19 | # note conflicts 20 | if total_conflicts: 21 | print('Note, there may be multiple version requirements for following packages:') 22 | for c in total_conflicts: 23 | print(f' * {c}') 24 | -------------------------------------------------------------------------------- /tests/commands/test_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from pathlib import Path 4 | from types import SimpleNamespace 5 | from piku.core import config 6 | from piku.commands.info import info_command 7 | from tests.fixtures import project 8 | 9 | 10 | def test_info(capsys, project): 11 | info_command(SimpleNamespace(packages=False, clear_cache=False)) 12 | out, _ = capsys.readouterr() 13 | assert 'Piku Version' in out 14 | 15 | def test_info_packages(capsys, project): 16 | info_command(SimpleNamespace(packages=True, clear_cache=False)) 17 | out, _ = capsys.readouterr() 18 | assert 'neopixel' in out 19 | 20 | @pytest.mark.slow 21 | def test_info_clear_cache(capsys): 22 | Path(os.path.join(config.cache_path, 'test')).touch() 23 | assert os.listdir(config.cache_path) 24 | info_command(SimpleNamespace(packages=False, clear_cache=True)) 25 | out, _ = capsys.readouterr() 26 | assert 'Clearing cache' in out 27 | assert not os.listdir(config.cache_path) 28 | -------------------------------------------------------------------------------- /piku/core/utils/watcher.py: -------------------------------------------------------------------------------- 1 | import time 2 | from watchdog.observers import Observer 3 | from watchdog.events import PatternMatchingEventHandler 4 | 5 | 6 | class EventHandler(PatternMatchingEventHandler): 7 | def __init__(self): 8 | super().__init__(ignore_directories=True, ignore_patterns=[ 9 | '*.pytest_cache*', 10 | '*__pycache__*', 11 | '*.cache', 12 | '*.pyc']) 13 | self.changed = True 14 | 15 | def check(self, handler): 16 | if self.changed: 17 | self.changed = False 18 | handler() 19 | 20 | def on_any_event(self, event): 21 | self.changed = True 22 | 23 | def watch(path, on_change): 24 | handler = EventHandler() 25 | observer = Observer() 26 | observer.schedule(handler, path, recursive=True) 27 | observer.start() 28 | try: 29 | while observer.is_alive(): 30 | handler.check(on_change) 31 | observer.join(0.25) 32 | time.sleep(.1) 33 | except KeyboardInterrupt: 34 | observer.stop() 35 | observer.join() 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "piku" 3 | version = "0.2.4" 4 | description = "" 5 | authors = [ 6 | "Mark Raleson ", 7 | "Tammy Cravit " 8 | ] 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/mraleson/piku.git" 12 | packages = [ 13 | { include="piku" } 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.8" 18 | pyserial = "^3.5" 19 | toml = "^0.10.2" 20 | appdirs = "^1.4.4" 21 | requests = "^2.27.1" 22 | Jinja2 = "^3.0.3" 23 | adafruit-board-toolkit = "^1.1.0" 24 | poetry-core = "1.0.7" 25 | cachy = "^0.3.0" 26 | watchdog = "^2.1.7" 27 | 28 | [tool.poetry.dev-dependencies] 29 | pytest = "^7.0.0" 30 | pylint-quotes = "^0.2.3" 31 | pylint = "^2.12.2" 32 | pytest-watcher = "^0.2.3" 33 | pytest-mock = "^3.7.0" 34 | 35 | [tool.poetry.scripts] 36 | piku = 'piku.main:main' 37 | 38 | [build-system] 39 | requires = ["poetry-core>=1.0.0"] 40 | build-backend = "poetry.core.masonry.api" 41 | 42 | [tool.pytest.ini_options] 43 | minversion = "6.0" 44 | addopts = "-ra -q" 45 | testpaths = [ "tests" ] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mraleson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/commands/test_remove.py: -------------------------------------------------------------------------------- 1 | import os 2 | from types import SimpleNamespace 3 | from piku.core import locker, config 4 | from piku.commands.add import add_command 5 | from piku.commands.remove import remove_command 6 | from tests.fixtures import project 7 | 8 | 9 | def test_remove(project): 10 | add_command(SimpleNamespace(package='adafruit_bus_device@5.1.6')) 11 | add_command(SimpleNamespace(package='adafruit_magtag')) 12 | add_command(SimpleNamespace(package='neopixel')) 13 | remove_command(SimpleNamespace(package='adafruit_bus_device')) 14 | assert 'adafruit_bus_device' in locker.load().keys() 15 | assert len(os.listdir(os.path.join(project, 'project', 'lib'))) == len(locker.load().keys()) 16 | remove_command(SimpleNamespace(package='adafruit_magtag')) 17 | assert 'neopixel' in locker.load().keys() and 'adafruit_pixelbuf' in locker.load().keys() 18 | assert len(os.listdir(os.path.join(project, 'project', 'lib'))) == len(locker.load().keys()) 19 | remove_command(SimpleNamespace(package='neopixel')) 20 | assert len(locker.load().keys()) == 0 21 | assert len(os.listdir(os.path.join(project, 'project', 'lib'))) == len(locker.load().keys()) 22 | -------------------------------------------------------------------------------- /piku/commands/remove.py: -------------------------------------------------------------------------------- 1 | from piku.core import config, packages, utils, locker 2 | 3 | 4 | def remove_command(args): 5 | package = args.package.lower() 6 | 7 | # check that we are in a piku project directory 8 | if not config.valid(): 9 | print('Failed: unable to find piku project in current directory.') 10 | return 11 | 12 | # remove package from pyproject.toml 13 | if not config.remove(f'tool.piku.dependencies.{package}'): 14 | print(f'Unable to find matching package {package} in pyproject.toml') 15 | suggestions = utils.similar(package, config.get('tool.piku.dependencies').keys()) 16 | if suggestions: 17 | print('Did you mean') 18 | for suggestion in suggestions: 19 | print(f' * {suggestion}') 20 | return 21 | 22 | # remove package to project toml file 23 | config.remove(f'dependencies.{package}') 24 | 25 | # update lock file 26 | existing_lock = locker.load() 27 | updated_lock, conflicts = locker.lock(existing_lock, removals=[package]) 28 | locker.save(updated_lock) 29 | 30 | # remove all packages no longer in lock file 31 | removed = set(existing_lock.keys()) - set(updated_lock.keys()) 32 | for package in removed: 33 | packages.remove(package, existing_lock) 34 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from types import SimpleNamespace 4 | import pytest 5 | from piku.core import utils, config 6 | from piku.commands.create import create_command 7 | 8 | 9 | @pytest.fixture 10 | def project(mocker, capsys): 11 | # create temp dirs 12 | temp_project_path = os.path.join(tempfile.gettempdir(), 'piku') 13 | utils.remove(temp_project_path) 14 | os.makedirs(temp_project_path, exist_ok=True) 15 | 16 | # change cwd 17 | cwd = os.getcwd() 18 | os.chdir(temp_project_path) 19 | 20 | # create a project 21 | project_name = 'bogus' 22 | create_command(SimpleNamespace(project=project_name, directory=None)) 23 | os.chdir(project_name) 24 | capsys.readouterr() 25 | 26 | # mock paths 27 | mocker.patch.object(config, 'config_path', os.path.join(os.getcwd(), 'pyproject.toml')) 28 | mocker.patch.object(config, 'lock_path', os.path.join(os.getcwd(), 'piku.lock')) 29 | 30 | 31 | yield os.getcwd() 32 | 33 | # clean mocks 34 | mocker.resetall() 35 | 36 | # clear up project 37 | utils.remove(temp_project_path) 38 | os.chdir(cwd) 39 | 40 | @pytest.fixture 41 | def tempdir(): 42 | temp_path = os.path.join(tempfile.gettempdir(), 'piku') 43 | os.makedirs(temp_path) 44 | yield temp_path 45 | utils.remove(temp_path) 46 | -------------------------------------------------------------------------------- /tests/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | from piku.core import utils 2 | 3 | 4 | def test_parse_semver(): 5 | assert utils.parse_semver('1.0.0').is_strictly_lower(utils.parse_semver('~2')) 6 | assert not utils.parse_semver('2.0.0').is_strictly_lower(utils.parse_semver('*')) 7 | assert utils.parse_semver('2').allows(utils.parse_semver('2.0.0')) 8 | assert utils.parse_semver('~2').allows(utils.parse_semver('2.0.0')) 9 | assert utils.parse_semver('latest').allows(utils.parse_semver('1.0.0')) 10 | 11 | def test_sort_semver(): 12 | items = ['1.0.2', '1.0.0', '2.0.0', '1.0.10', '1.0.1-alpha', 'bogus', '1.0.1'] 13 | assert utils.sort_versions(items) == ['bogus', '1.0.0', '1.0.1-alpha', '1.0.1', '1.0.2', '1.0.10', '2.0.0'] 14 | 15 | def test_binary_search(): 16 | a = [10, 20, 30, 40, 40, 40, 50] 17 | cmp = lambda a, b: a < b 18 | assert utils.bisect(a, 40, cmp) == 6 19 | assert utils.bisect(a, 0, cmp) == 0 20 | 21 | a = ['0.0.0', '1.0.0', '1.0.0', '2.0.1', '2.0.1', '2.0.1', '3.9.1'] 22 | cmp = lambda a, b: utils.parse_semver(a).is_strictly_lower(utils.parse_semver(b)) 23 | assert utils.bisect(a, '0.0.0', cmp) == 1 24 | assert utils.bisect(a, '1.0.0', cmp) == 3 25 | assert utils.bisect(a, '*', cmp) == 7 26 | assert utils.bisect(a, '2.*', cmp) == 6 27 | assert utils.bisect(a, '~1', cmp) == 3 28 | assert utils.bisect(a, 'latest', cmp) == 7 29 | -------------------------------------------------------------------------------- /tests/commands/test_add.py: -------------------------------------------------------------------------------- 1 | import os 2 | from types import SimpleNamespace 3 | from piku.core import locker, config 4 | from piku.commands.add import add_command 5 | from tests.fixtures import project 6 | 7 | 8 | def test_add_latest(capsys, project): 9 | add_command(SimpleNamespace(package='adafruit_bus_device@5.1.6')) 10 | add_command(SimpleNamespace(package='adafruit_magtag')) 11 | out, _ = capsys.readouterr() 12 | assert 'Note' in out and 'adafruit_bus_device' in out 13 | lock = locker.load() 14 | packages = set([ 15 | 'adafruit_magtag', 16 | 'adafruit_portalbase', 17 | 'adafruit_bitmap_font', 18 | 'adafruit_display_text', 19 | 'neopixel', 20 | 'adafruit_pixelbuf', 21 | 'adafruit_requests', 22 | 'adafruit_io', 23 | 'adafruit_esp32spi', 24 | 'adafruit_bus_device', 25 | 'simpleio', 26 | 'adafruit_fakerequests', 27 | 'adafruit_miniqr', 28 | 'neopixel', 29 | 'adafruit_requests', 30 | 'simpleio']) 31 | assert set(lock.keys()) == packages 32 | assert len(os.listdir(os.path.join(project, 'project', 'lib'))) == len(packages) 33 | deps = config.get('dependencies') 34 | assert 'adafruit_bus_device' in deps 35 | assert 'adafruit_magtag' in deps 36 | 37 | def test_add_bad_package(capsys, project): 38 | add_command(SimpleNamespace(package='neo')) 39 | # add_command(SimpleNamespace(package='adafruit_boardtest')) 40 | out, _ = capsys.readouterr() 41 | assert 'Did you mean' in out and 'neopixel' in out 42 | 43 | def test_add_bad_package_constraint(capsys, project): 44 | add_command(SimpleNamespace(package='neopixel@0.0.123')) 45 | # add_command(SimpleNamespace(package='adafruit_boardtest')) 46 | out, _ = capsys.readouterr() 47 | assert 'Unable to find' in out and 'neopixel' in out and '0.0.123' in out 48 | -------------------------------------------------------------------------------- /piku/commands/deploy.py: -------------------------------------------------------------------------------- 1 | from piku.core import config, device, utils 2 | 3 | 4 | def deploy(drive): 5 | print(f'Deploying project to device {drive}...') 6 | device.deploy(drive) 7 | print('Done') 8 | 9 | 10 | def deploy_command(args): 11 | # get device 12 | drive = args.device or device.find_device_path() 13 | 14 | # check that we are in a piku project directory 15 | if not config.valid(): 16 | print('Refusing to deploy, unable to find piku project in current directory.') 17 | return 18 | 19 | # check that we have a device found or specified 20 | if not drive: 21 | print('Unable find a device and deploy, please specify a device to deploy to.') 22 | return 23 | 24 | # check that device size and name are as expected to reduce chances of loading onto wrong device 25 | if not device.has_correct_size(drive): 26 | print('Refusing to deploy, specified CircuitPython drive is larger than expected (~16MB).') 27 | return 28 | if not device.has_correct_label(drive): 29 | print('Refusing to deploy, expected device to have "circuitpy" in path.') 30 | return 31 | 32 | # confirm deploy 33 | if not args.yes: 34 | print(f'Are you sure you want to deploy to device: {drive}?') 35 | print('WARNING THIS WILL REMOVE ALL OTHER FILES FROM THE DEVICE! PLEASE BE CAREFUL!') 36 | response = input('Are you sure? [y/n] ').lower() 37 | if response not in ['y', 'yes']: 38 | print('Exiting') 39 | return 40 | 41 | # backup device files before deploy 42 | print(f'Backing up device files from {drive} to {config.backup_path}...') 43 | device.backup(drive, config.backup_path) 44 | 45 | 46 | # synchronize files to device 47 | if not args.watch: 48 | deploy(drive) 49 | return 50 | 51 | # watch files and auto deploy 52 | utils.watch(config.get('source'), lambda: deploy(drive)) 53 | -------------------------------------------------------------------------------- /tests/core/test_packages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from tests.fixtures import tempdir 4 | from piku.core import packages, errors, utils 5 | 6 | 7 | def test_get_index(): 8 | index = packages.get_index() 9 | assert len(index) > 0 10 | 11 | def test_latest_target(): 12 | assert packages.latest_target() == '7' 13 | 14 | def test_all(): 15 | all = packages.all('7') 16 | assert 'neopixel' in all and 'dynamixel' in all 17 | 18 | def test_suggest(): 19 | suggestions = packages.suggest('neo', '7') 20 | assert suggestions == ['neopixel', 'neopixel_spi', 'adafruit_neokey', 'adafruit_neotrellis', 'asyncio'] 21 | 22 | def test_find(): 23 | assert utils.parse_semver(packages.find('neopixel', '*', target='7')) >= utils.parse_semver('6.2.4') 24 | assert packages.find('neopixel', '~6', target='7').startswith('6.') 25 | assert packages.find('neopixel', '6.0.3', target='7') == '6.0.3' 26 | assert packages.find('dynamixel', '*', target='7') == '0.0.0' 27 | assert utils.parse_semver(packages.find('adafruit_lis3mdl', 'latest', target='7')) >= utils.parse_semver('1.1.12') 28 | with pytest.raises(errors.PackageNotFound): 29 | packages.find('bogus', '*', target='7') 30 | with pytest.raises(errors.VersionNotFound): 31 | packages.find('neopixel', '~5', target='7') 32 | 33 | def test_info(): 34 | info = packages.info('neopixel', '6.2.4', target='7') 35 | assert info['package'] == 'neopixel' 36 | 37 | def test_dependencies(): 38 | dependencies = packages.dependencies('neopixel', '6.2.4', target='7') 39 | assert dependencies == {'adafruit_pixelbuf': '1.1.2'} 40 | dependencies = packages.dependencies('adafruit_boardtest', '1.2.9', target='7') 41 | assert dependencies == {'adafruit_bus_device': '5.1.2', 'adafruit_sdcard': '3.3.7'} 42 | 43 | def test_install(tempdir): 44 | lock = { 45 | 'neopixel': packages.get_index()['7']['neopixel']['6.2.4'] 46 | } 47 | packages.install('neopixel', lock, project_path=tempdir) 48 | assert 'neopixel.mpy' in os.listdir(os.path.join(tempdir, 'lib')) 49 | -------------------------------------------------------------------------------- /piku/commands/serial.py: -------------------------------------------------------------------------------- 1 | from serial import Serial 2 | from serial.serialutil import SerialException 3 | from serial.tools.miniterm import Miniterm 4 | import adafruit_board_toolkit.circuitpython_serial 5 | 6 | 7 | 8 | def default(): 9 | ports = adafruit_board_toolkit.circuitpython_serial.repl_comports() 10 | if not ports: 11 | return None 12 | return ports[0].device 13 | 14 | 15 | def serial_command(args): 16 | baud = 115200 17 | using_default = False 18 | port = args.serial 19 | if not port: 20 | using_default = True 21 | port = default() 22 | 23 | # print help text 24 | print(r'================================================================================') 25 | print(f'Connecting to {"DEFAULT" if using_default else "SPECIFIED"} serial device {port} using baud {baud}...') 26 | print(r' * ctrl-x: close terminal') 27 | print(r' * ctrl-c: enter python repl on device') 28 | print(r' * ctrl-d: exit python repl on device and reload') 29 | print(r'================================================================================') 30 | 31 | # confirm that a port was configured or a default port was found 32 | if not port: 33 | print('Failed to find the specified serial port or find a default serial port to use.') 34 | return 35 | 36 | # create serial port connection 37 | try: 38 | serial = Serial(port, baud) 39 | except SerialException: 40 | if using_default: 41 | print(f'Failed to open the default serial port {port}') 42 | else: 43 | print(f'Failed to open the configured serial port {port}') 44 | return 45 | 46 | # start terminal 47 | miniterm = Miniterm(serial, echo=False, eol='crlf') 48 | miniterm.exit_character = chr(0x18) # GS/CTRL+]) 49 | miniterm.menu_character = chr(0x14) # Menu: CTRL+T 50 | miniterm.raw = False 51 | miniterm.set_rx_encoding('UTF-8') 52 | miniterm.set_tx_encoding('UTF-8') 53 | miniterm.start() 54 | 55 | # wait for terminal exit 56 | try: 57 | miniterm.join(True) 58 | except KeyboardInterrupt: 59 | pass 60 | miniterm.join() 61 | miniterm.close() 62 | -------------------------------------------------------------------------------- /piku/commands/add.py: -------------------------------------------------------------------------------- 1 | from piku.core import config, errors, packages, locker 2 | 3 | 4 | # attempt to add a package given it name and a version constraint 5 | def add(package_name, package_constraint): 6 | # find maching package version 7 | package_version = packages.find(package_name, package_constraint) 8 | 9 | # add package to project toml file 10 | config.set(f'dependencies.{package_name}', package_constraint) 11 | 12 | # update locked packages 13 | existing_lock = locker.load() 14 | updated_lock, conflicts = locker.lock(existing_lock, additions=[(package_name, package_version)]) 15 | locker.save(updated_lock) 16 | 17 | # remove old packages that were updated or replaced 18 | for package in existing_lock: 19 | if package not in updated_lock or updated_lock[package]['version'] != existing_lock[package]['version']: 20 | packages.remove(package, existing_lock) 21 | 22 | # install all new packages that were updated or added 23 | for package in updated_lock: 24 | if package not in existing_lock or updated_lock[package]['version'] != existing_lock[package]['version']: 25 | packages.install(package, updated_lock) 26 | 27 | return existing_lock, updated_lock, conflicts 28 | 29 | def add_command(args): 30 | # check that we are in a piku project directory 31 | if not config.valid(): 32 | print('Failed: unable to find piku project in current directory.') 33 | return 34 | 35 | # parse package and constraint 36 | package = args.package 37 | constraint = 'latest' 38 | if '@' in args.package: 39 | package, constraint = args.package.split('@') 40 | 41 | # find a match for the required package 42 | try: 43 | previous, current, conflicts = add(package, constraint) 44 | if conflicts: 45 | print('Note, there may be multiple version requirements for following packages:') 46 | for c in conflicts: 47 | print(f' * {c}') 48 | 49 | except errors.PackageNotFound: 50 | print(f'Unable to resolve requested package: {package}') 51 | suggestions = packages.suggest(package) 52 | if suggestions: 53 | print('Did you mean') 54 | for suggestion in suggestions: 55 | print(f' * {suggestion}') 56 | return 57 | except errors.VersionNotFound: 58 | print(f'Unable to find {package} package matching version {constraint}') 59 | -------------------------------------------------------------------------------- /piku/template/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | ### Custom git ignore ### 126 | 127 | # Unit test / coverage reports 128 | htmlcov/ 129 | .tox/ 130 | .coverage 131 | .coverage.* 132 | .cache 133 | nosetests.xml 134 | coverage.xml 135 | *,cover 136 | 137 | # Ignore database files 138 | *.sqlite3 139 | *.dump 140 | *.sql 141 | *.db 142 | 143 | # Ignore log files 144 | *.log 145 | 146 | # Secret environment files 147 | *.env 148 | 149 | *.db.bak 150 | -------------------------------------------------------------------------------- /piku/core/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import toml 3 | from appdirs import user_data_dir 4 | 5 | 6 | config_path = os.path.join(os.getcwd(), 'pyproject.toml') 7 | lock_path = os.path.join(os.getcwd(), 'piku.lock') 8 | data_path = user_data_dir('piku', 'piku') 9 | backup_path = os.path.join(data_path, 'backup') 10 | cache_path = os.path.join(data_path, 'cache') 11 | bundle_path = os.path.join(cache_path, 'bundle') 12 | index_url = 'https://raw.githubusercontent.com/mraleson/piku-index/main/data/packages.json' 13 | 14 | defaults = { 15 | 'tool': 16 | { 17 | 'piku': { 18 | 'project': None, 19 | 'piku': None, 20 | 'circuit-python': None, 21 | 'board': None, 22 | 'source': './project', 23 | } 24 | } 25 | } 26 | 27 | 28 | # nested dictionary helpers 29 | def nget(dictionary, keys, default=None): 30 | for key in keys[:-1]: 31 | dictionary = dictionary.get(key, {}) 32 | return dictionary.get(keys[-1], default) 33 | 34 | def nset(dictionary, keys, value): 35 | for key in keys[:-1]: 36 | dictionary = dictionary.setdefault(key, {}) 37 | dictionary[keys[-1]] = value 38 | 39 | def ndel(dictionary, keys): 40 | for key in keys[:-1]: 41 | dictionary = dictionary.setdefault(key, {}) 42 | if keys[-1] in dictionary: 43 | value = dictionary[keys[-1]] 44 | del dictionary[keys[-1]] 45 | return value 46 | return None 47 | 48 | 49 | # load config 50 | def load(): 51 | return toml.load(config_path) 52 | 53 | # save config 54 | def save(config): 55 | with open(config_path, 'w') as file: 56 | toml.dump(config, file) 57 | 58 | # get value from nested dictionary with dotted pack a.b.c 59 | def get(dotpath): 60 | config = load() 61 | if not dotpath.startswith('tool.piku'): 62 | dotpath = 'tool.piku' + '.' + dotpath 63 | keys = dotpath.split('.') 64 | return nget(config, keys, nget(defaults, keys)) 65 | 66 | # set value from nested dictionary with dotted pack a.b.c 67 | def set(dotpath, value): 68 | config = load() 69 | if not dotpath.startswith('tool.piku'): 70 | dotpath = 'tool.piku' + '.' + dotpath 71 | keys = dotpath.split('.') 72 | nset(config, keys, value) 73 | save(config) 74 | 75 | # remove value from nested dictionary with dotted pack a.b.c 76 | def remove(dotpath): 77 | config = load() 78 | if not dotpath.startswith('tool.piku'): 79 | dotpath = 'tool.piku' + '.' + dotpath 80 | keys = dotpath.split('.') 81 | value = ndel(config, keys) 82 | save(config) 83 | return value 84 | 85 | # returns true if in a valid piku project 86 | def valid(): 87 | try: 88 | config = load() 89 | if not config.get('tool').get('piku'): 90 | return False 91 | except FileNotFoundError: 92 | return False 93 | return True 94 | -------------------------------------------------------------------------------- /piku/core/device.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import platform 4 | from subprocess import check_output 5 | from time import time 6 | from piku.core import utils, config 7 | 8 | 9 | # perhaps im the future this is configurable along with the sync ignore/exclude patterns 10 | backup_ignore_patterns = shutil.ignore_patterns( 11 | '.Trashes', 12 | '.Trashes/*', 13 | 'System Volume Information', 14 | 'System Volume Information/*', 15 | '.metadata_never_index', 16 | '.fseventsd', 17 | '*/._*' 18 | ) 19 | 20 | 21 | def backup(src, dst): 22 | # backup drive contents 23 | os.makedirs(dst, exist_ok=True) 24 | timestamp = int(time() * 1000) 25 | backup_path = os.path.join(dst, str(timestamp)) 26 | 27 | shutil.copytree(src, backup_path, ignore=backup_ignore_patterns) 28 | 29 | # remove old backups (keep the most recent 10) 30 | dirs = os.listdir(dst) 31 | dirs.sort(key=lambda x: -int(x)) 32 | for dir in dirs[10:]: 33 | utils.remove(os.path.join(dst, dir)) 34 | 35 | def deploy(device): 36 | source = config.get('tool.piku.source') 37 | utils.sync(source, device, exclude=['boot_out.txt', '.*'], verbosity=1) 38 | 39 | def has_correct_size(path): 40 | total, used, free = shutil.disk_usage(path) 41 | return 0 < total < 16E6 42 | 43 | def has_correct_label(path): 44 | if platform.system() == 'Windows': 45 | drive = path.split(':')[0] 46 | output = check_output(f'cmd /c vol {drive}:'.split()).decode() 47 | label = output.split('\r\n')[0].split(' ').pop() 48 | return 'circuitpy' in label.lower() 49 | return 'circuitpy' in path.lower() 50 | 51 | def find_device_path(): 52 | if platform.system() == 'Windows': 53 | output = check_output('wmic logicaldisk where drivetype=2 get DeviceId , VolumeName'.split()).decode() 54 | drives = [ 55 | line.split(' ')[0] 56 | for line in output.split('\r\n') 57 | if 'circuitpy' in line.lower() 58 | ] 59 | return drives[0] if drives else None 60 | if platform.system() == 'Linux': 61 | output = check_output('lsblk -l -o mountpoint,label,rm'.split()).decode() 62 | lines = [line.split() for line in output.split('\n')] 63 | drives = [ 64 | line[0] 65 | for line in lines 66 | if len(line) == 3 and 'circuitpy' in line[1].lower() and line[2] == '1' 67 | ] 68 | return drives[0] if drives else None 69 | if platform.system() == 'Darwin': 70 | output = check_output('mount'.split()).decode() 71 | lines = [line.split() for line in output.split('\n')] 72 | drives = [ 73 | line[2] 74 | for line in lines 75 | if len(line) > 4 and 'circuitpy' in line[2].lower() 76 | ] 77 | return drives[0] if drives else None 78 | return None 79 | -------------------------------------------------------------------------------- /piku/commands/create.py: -------------------------------------------------------------------------------- 1 | import os 2 | from jinja2 import Template 3 | from piku.core import utils, packages 4 | from piku.commands.version import get_version 5 | 6 | 7 | def template(path, context): 8 | with open(path, 'r') as f: 9 | template = Template(f.read()) 10 | with open(path, 'w') as f: 11 | f.write(template.render(**context)) 12 | 13 | def create_command(args): 14 | # build template context 15 | project_path = args.directory or f'./{args.project}' 16 | toml_path = os.path.join(project_path, 'pyproject.toml') 17 | readme_path = os.path.join(project_path, 'README.md') 18 | gitignore_path = os.path.join(project_path, '.gitignore') 19 | code_path = os.path.join(project_path, 'project') 20 | template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../template') 21 | context = { 22 | 'project': args.project, 23 | 'piku': get_version(), 24 | 'circuit_python': packages.latest_target(), 25 | 'device': '', 26 | 'serial': '' 27 | } 28 | 29 | # show piku version 30 | print(f'Piku v{context["piku"]} creating project {args.project}') 31 | 32 | # check that path doesnt exist 33 | if os.path.exists(project_path) and not args.directory: 34 | print(f'Unable to create project: directory {project_path} already exists.') 35 | return 36 | os.makedirs(project_path, exist_ok=True) 37 | 38 | # create pyproject.toml if it doesn't exist (fail if it does) 39 | if not os.path.exists(toml_path): 40 | utils.copy(os.path.join(template_dir, 'pyproject.toml'), toml_path) 41 | template(toml_path, context) 42 | else: 43 | print(f'Unable to create project: {toml_path} already exists.') 44 | return 45 | 46 | # create readme if it doesn't exist 47 | if not os.path.exists(readme_path): 48 | utils.copy(os.path.join(template_dir, 'README.md'), readme_path) 49 | template(readme_path, context) 50 | 51 | # create gitignore if it doesn't already exist 52 | if not os.path.exists(gitignore_path): 53 | utils.copy(os.path.join(template_dir, '.gitignore'), gitignore_path) 54 | template(readme_path, context) 55 | 56 | # create default code folder if it doesn't exist 57 | if not os.path.exists(code_path): 58 | utils.copy(os.path.join(template_dir, 'project'), code_path, contents=True) 59 | 60 | print('Done.') 61 | 62 | # in the future allow loading an example and dependencies for a specific board 63 | 64 | # # select board 65 | # print('Please enter the name of your CircuitPython board:') 66 | # context['board'] = input().strip() 67 | 68 | # # select device 69 | # print('Please enter the usb drive path of your device CircuitPython enabled device:') 70 | # context['device'] = input().strip() 71 | # 72 | # # select serial port 73 | # print('Please enter the serial port for your CircuitPython device:') 74 | # context['serial'] = input().strip() 75 | -------------------------------------------------------------------------------- /tests/utils/test_sync.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import filecmp 4 | import tempfile 5 | import pytest 6 | from piku import __version__ 7 | from piku.core.utils import sync 8 | 9 | 10 | # helpers 11 | temp_dir = tempfile.gettempdir() 12 | 13 | def make_dir(base, dir=''): 14 | os.makedirs(os.path.join(base, dir), exist_ok=True) 15 | 16 | def make_file(base, path, contents): 17 | full_path = os.path.join(base, path) 18 | dir = os.path.dirname(full_path) 19 | if dir: make_dir(dir) 20 | with open(full_path, 'w', encoding='utf-8') as f: 21 | f.write(contents) 22 | 23 | def assert_match(a_base, a, b_base, b): 24 | assert filecmp.cmp(os.path.join(a_base, a), os.path.join(b_base, b), shallow=True), \ 25 | f'Expected files or dirs {a} and {b} to match.' 26 | 27 | def assert_contents(base, path, contents): 28 | p = os.path.join(base, path) 29 | with open(p, 'r', encoding='utf-8') as f: 30 | assert f.read() == contents 31 | 32 | def assert_exists(base, path): 33 | p = os.path.join(base, path) 34 | assert os.path.isfile(p) or os.path.isdir(p), \ 35 | f'Expected file or dir {p} to exist.' 36 | 37 | def assert_missing(base, path): 38 | p = os.path.join(base, path) 39 | assert not os.path.isfile(p) and not os.path.isdir(p), \ 40 | f'Expected file or dir {p} to exist.' 41 | 42 | 43 | # fixtures 44 | @pytest.fixture 45 | def example_files(): 46 | 47 | # make source dir 48 | src = os.path.join(temp_dir, 'piku/src') 49 | make_dir(src) 50 | make_file(src, 'a.txt', 'a') 51 | make_file(src, 'b/b.txt', 'b') 52 | make_file(src, 'c.txt', 'c') 53 | make_file(src, 'd.txt', 'd') 54 | make_file(src, 'i.txt', 'i') 55 | make_dir(src, 'empty') 56 | 57 | # make destination dir 58 | dst = os.path.join(temp_dir, 'piku/dst') 59 | make_dir(dst) 60 | make_file(dst, 'a.txt', 'a') 61 | make_file(dst, 'b/b.txt', 'b') 62 | make_file(dst, 'd.txt', 'bogus') 63 | make_file(dst, 'i.txt', 'dont copy over me') 64 | make_file(dst, 'keep/keep.txt', 'dont remove me') 65 | make_file(dst, '.ignore_me', 'dont remove me') 66 | make_dir(dst, 'empty_to_remove') 67 | 68 | yield (src, dst) 69 | 70 | # clean up 71 | shutil.rmtree(os.path.join(temp_dir, 'piku')) 72 | 73 | 74 | # tests 75 | def test_sync(example_files): 76 | src, dst = example_files 77 | sync(src, dst, exclude=['i.txt', 'keep/keep.txt', '.*']) 78 | assert_match(src, 'a.txt', dst, 'a.txt') 79 | assert_match(src, 'b/b.txt', dst, 'a.txt') 80 | assert_match(src, 'c.txt', dst, 'c.txt') 81 | assert_match(src, 'd.txt', dst, 'd.txt') 82 | assert_exists(dst, 'empty') 83 | assert_missing(dst, 'z.txt') 84 | assert_missing(dst, 'empty_to_remove') 85 | 86 | assert_exists(dst, 'i.txt') 87 | assert_contents(dst, 'i.txt', 'dont copy over me') 88 | assert_exists(dst, 'keep/keep.txt') 89 | assert_exists(dst, '.ignore_me') 90 | assert_contents(dst, 'keep/keep.txt', 'dont remove me') 91 | -------------------------------------------------------------------------------- /piku/core/utils/sync.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fnmatch 3 | from piku.core.utils import checksum, tree, copy, remove 4 | 5 | # helpers 6 | def different(a, b): 7 | if os.path.isdir(a) and os.path.isdir(b): 8 | return False 9 | if os.path.isfile(a) and os.path.isfile(b): 10 | return os.path.getsize(a) != os.path.getsize(b) or checksum(a) != checksum(b) 11 | return True 12 | 13 | def ignored(path, patterns): 14 | for pattern in patterns: 15 | if fnmatch.filter([path], pattern): 16 | return True 17 | return False 18 | 19 | 20 | # fsync all files to prevent a late OS flush to device triggering a reload 21 | def fsync_dir(dir_path): 22 | print(f'fsync {dir_path}') 23 | # sync dir meta data 24 | try: 25 | dir_fd = os.open(dir_path, os.O_RDONLY) 26 | os.fsync(dir_fd) 27 | finally: 28 | os.close(dir_fd) 29 | 30 | # sync dir 31 | for root, dirs, files in os.walk(dir_path, onerror=None): 32 | 33 | # sync files 34 | for file in files: 35 | path = os.path.join(root, file) 36 | with open(path, 'rb') as f: 37 | os.fsync(f.fileno()) 38 | 39 | # sync dirs 40 | for dir in dirs: 41 | path = os.path.join(root, dir) 42 | fsync_dir(path) 43 | 44 | # synchronize 45 | def sync(src_dir, dst_dir, exclude=None, verbosity=1): 46 | src_tree = tree(src_dir) 47 | dst_tree = tree(dst_dir) 48 | to_rm = dst_tree - src_tree 49 | to_ignore = {os.path.join(dst_dir, i) for i in (exclude or [])} 50 | changes_detected = False 51 | 52 | # create any missing dir and copy over missing files (sorted so dirs are created files) 53 | for path in sorted(src_tree): 54 | src = os.path.join(src_dir, path) 55 | dst = os.path.join(dst_dir, path) 56 | if ignored(dst, to_ignore): 57 | if verbosity > 1: 58 | print(f'* Ignoring copy {dst}') 59 | elif different(src, dst): 60 | if verbosity > 0: 61 | print(f'* Copying {src} to {dst}') 62 | copy(src, dst, recursive=False) 63 | changes_detected = True 64 | else: 65 | if verbosity > 1: 66 | print(f'* Files {src} {dst} are the same') 67 | 68 | # remove files that are in destination but not in source (reverse sorted so files rm before dirs) 69 | for path in reversed(sorted(to_rm)): 70 | full_path = os.path.join(dst_dir, path) 71 | if ignored(full_path, to_ignore): 72 | if verbosity > 1: 73 | print(f'* Ignoring remove {full_path}') 74 | else: 75 | if verbosity > 0: 76 | print(f'* Removing {full_path}') 77 | remove(full_path, recursive=False) 78 | changes_detected = True 79 | 80 | # sync file information to prevent delayed soft reboot on circuit python device 81 | fsync_dir(dst_dir) 82 | 83 | if verbosity > 0 and not changes_detected: 84 | print('Nothing to deploy, no source differences were detected.') 85 | -------------------------------------------------------------------------------- /piku/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import argparse 5 | import traceback 6 | from piku import commands 7 | 8 | 9 | def main(): 10 | # create parser 11 | parser = argparse.ArgumentParser(description='') 12 | subparsers = parser.add_subparsers(dest='parser') 13 | parsers = {} 14 | 15 | # create command 16 | p = subparsers.add_parser('create', help='create new CircuitPython project') 17 | p.set_defaults(cmd=commands.create_command) 18 | p.add_argument('project', help='project name') 19 | p.add_argument('directory', nargs='?', default=None, help='optional path to existing project directory') 20 | 21 | # initialize command 22 | p = subparsers.add_parser('initialize', help='initialize new CircuitPython project in current directory') 23 | p.set_defaults(cmd=commands.create_command) 24 | p.add_argument('--project', default=os.path.basename(os.getcwd()), help=argparse.SUPPRESS) 25 | p.add_argument('--directory', default=os.getcwd(), help=argparse.SUPPRESS) 26 | 27 | # add command 28 | p = subparsers.add_parser('add', help='download and add package to project') 29 | p.set_defaults(cmd=commands.add_command) 30 | p.add_argument('package', help='package name [neopixel, neopixel@^6, neopixel@preferred, neopixel@latest]') 31 | 32 | # remove command 33 | p = subparsers.add_parser('remove', help='remove package from project') 34 | p.set_defaults(cmd=commands.remove_command) 35 | p.add_argument('package', help='package name [neopixel, my_package_name]') 36 | 37 | # install command 38 | p = subparsers.add_parser('install', help='install project dependencies') 39 | p.set_defaults(cmd=commands.install_command) 40 | 41 | # upgrade command 42 | p = subparsers.add_parser('upgrade', help='upgrade all project dependencies to latest') 43 | p.set_defaults(cmd=commands.upgrade_command) 44 | 45 | # serial command 46 | p = subparsers.add_parser('serial', help='connect usb serial port of device') 47 | p.set_defaults(cmd=commands.serial_command) 48 | p.add_argument('-s', '--serial', default=None, help='serial port to connect to') 49 | 50 | # deploy command 51 | p = subparsers.add_parser('deploy', help='deploy project to device') 52 | p.set_defaults(cmd=commands.deploy_command) 53 | p.add_argument('-d', '--device', default=None, help='path of device to deploy to project to') 54 | p.add_argument('-y', '--yes', action='store_true', help='deploy to discovered device without confirmation dialog') 55 | p.add_argument('-w', '--watch', action='store_true', help='auto deploy when project files') 56 | 57 | # version command 58 | p = subparsers.add_parser('version', help='show piku version') 59 | p.set_defaults(cmd=commands.version_command) 60 | 61 | # info command 62 | p = subparsers.add_parser('info', help='show additional piku information') 63 | p.set_defaults(cmd=commands.info_command) 64 | p.add_argument('--clear-cache', action='store_true', help='clear package cache') 65 | p.add_argument('-p', '--packages', action='store_true', help='list availiable packages') 66 | 67 | # parse and execute 68 | args = parser.parse_args() 69 | if hasattr(args, 'cmd'): 70 | try: 71 | args.cmd(args) 72 | except Exception as exc: 73 | print(traceback.format_exc(), end='') 74 | print('Failed.') 75 | sys.exit() 76 | else: 77 | parsers.get(args.parser, parser).print_help() 78 | 79 | 80 | if __name__ == '__main__': 81 | try: 82 | main() 83 | except KeyboardInterrupt: 84 | print('') 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | # Atom per project settings 155 | .atom 156 | -------------------------------------------------------------------------------- /piku/core/utils/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import shutil 4 | import hashlib 5 | import difflib 6 | import requests 7 | from poetry.core.semver import parse_constraint, exceptions 8 | 9 | 10 | def checksum(path): 11 | hasher = hashlib.md5() 12 | with open(path, 'rb') as f: 13 | while chunk := f.read(128 * hasher.block_size): 14 | hasher.update(chunk) 15 | return hasher.digest() 16 | 17 | def tree(path): 18 | paths = [] 19 | for root, dirs, files in os.walk(path): 20 | paths.extend([os.path.relpath(os.path.join(root, d), path) for d in dirs]) 21 | paths.extend([os.path.relpath(os.path.join(root, f), path) for f in files]) 22 | return set(paths) 23 | 24 | def copy(src, dst, recursive=True, contents=False): 25 | if os.path.isdir(src): 26 | if recursive: 27 | if contents: 28 | return shutil.copytree(src, dst, dirs_exist_ok=True) 29 | return shutil.copytree(src, os.path.join(dst, os.path.basename(src)), dirs_exist_ok=True) 30 | return os.makedirs(dst, exist_ok=True) 31 | return shutil.copy2(src, dst) 32 | 33 | def remove(path, recursive=True): 34 | if os.path.isdir(path): 35 | if recursive: 36 | shutil.rmtree(path) 37 | else: 38 | try: 39 | os.rmdir(path) 40 | except OSError: 41 | pass # raised if dir is not empty, this should only happen when a file was ignored in the folder 42 | else: 43 | # This is a workaround for the fact that on macOS, removing 44 | # extended attribute/resource fork files (._*) will fail once 45 | # the file they belong to is removed. 46 | try: 47 | os.remove(path) 48 | except FileNotFoundError: 49 | pass 50 | 51 | def similar(value, options): 52 | compounds = sorted([k for k in options if value in k], key=len) 53 | close = difflib.get_close_matches(value, options, 3, 0.3) 54 | temp = [c for c in close if c not in set(compounds)] 55 | compounds.extend(temp) 56 | return compounds 57 | 58 | def download(url, path): 59 | with requests.get(url, stream=True) as r: 60 | with open(path, 'wb') as f: 61 | shutil.copyfileobj(r.raw, f) 62 | return path 63 | 64 | def matches_semver(constraint, version): 65 | if version is None: return False 66 | return parse_semver(constraint).allows(parse_semver(version)) 67 | 68 | patch_pattern = re.compile(r'^(\d+)$') 69 | def parse_semver(constraint): 70 | # fix latest key word not parsing 71 | if constraint.endswith('latest'): 72 | constraint = '*'.join(constraint.rsplit('latest', 1)) 73 | 74 | # fix constraint not assuming @7 means @7.* 75 | matches = patch_pattern.findall(constraint) 76 | if matches: 77 | constraint = f'{matches[-1]}.*'.join(constraint.rsplit(matches[-1], 1)) 78 | 79 | return parse_constraint(constraint) 80 | 81 | # return a list of semvers sorted in order 82 | def sort_versions(items, reverse=False): 83 | def key(item): 84 | try: 85 | return parse_semver(item) 86 | except exceptions.ParseConstraintError: 87 | return parse_semver('0.0.0') 88 | return sorted(items, key=key, reverse=reverse) 89 | 90 | # cmp(a, b) should returns true if a is less than b else false 91 | def bisect(a, x, cmp, lo=0, hi=None): 92 | if lo < 0: 93 | raise ValueError('lo must be non-negative') 94 | if hi is None: 95 | hi = len(a) 96 | while lo < hi: 97 | mid = (lo + hi) // 2 98 | if cmp(x, a[mid]): 99 | hi = mid 100 | else: 101 | lo = mid + 1 102 | return lo 103 | -------------------------------------------------------------------------------- /piku/core/locker.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | from piku.core import config, packages 4 | 5 | 6 | # load lock file 7 | def load(): 8 | try: 9 | with open(config.lock_path, 'r') as file: 10 | return json.load(file) 11 | except FileNotFoundError: 12 | return {} 13 | 14 | # save lock file 15 | def save(locked): 16 | with open(config.lock_path, 'w') as file: 17 | json.dump(locked, file, indent=2) 18 | 19 | # get lock dictionary fingerprint set for comparing lock dictionaries 20 | def fingerprint(locked): 21 | return {f'{k}{v["version"]}' for k, v in locked.items()} 22 | 23 | # lock returns an updated locked packages dictionary given an existing one and any additions or removals 24 | # existing: an existing locked packages dictionary 25 | # additions: a list of (package, constraint) we want to add 26 | # removals: a list of packages we want to remove 27 | def lock(existing, additions=None, removals=None): 28 | # create new locked packages dictionary 29 | project_dependencies = config.get('dependencies') 30 | locked = deepcopy(existing or {}) 31 | additions = additions or [] 32 | removals = removals or [] 33 | 34 | # add any new packages to the package lock 35 | for (package, version) in additions: 36 | locked[package] = packages.info(package, version) 37 | 38 | # remove requested packages from lock 39 | for package in removals: 40 | del locked[package] 41 | 42 | # expand dependencies 43 | previous = None 44 | conflicts = set() 45 | while previous is None or fingerprint(locked) != fingerprint(previous): 46 | previous = locked.copy() 47 | expanded = locked.copy() 48 | for package in locked: 49 | package_info = locked[package] 50 | dependencies = package_info['dependencies'] 51 | for dep in dependencies: 52 | dep_info = packages.info(dep, dependencies[dep]) 53 | if dep not in project_dependencies: 54 | if dep in expanded and expanded[dep]['version'] != dep_info['version']: 55 | conflicts.add(dep) 56 | expanded[dep] = dep_info 57 | else: 58 | conflicts.add(dep) 59 | locked = expanded 60 | 61 | # clean orphan dependencies 62 | previous = None 63 | while previous is None or fingerprint(locked) != fingerprint(previous): 64 | previous = locked.copy() 65 | expanded = locked.copy() 66 | for package in locked: 67 | orphaned = True 68 | for other in locked: 69 | if package in locked[other]['dependencies']: 70 | orphaned = False 71 | if package not in project_dependencies and orphaned: 72 | del expanded[package] 73 | locked = expanded 74 | 75 | return locked, conflicts 76 | 77 | 78 | # from piku.core import config, packages, project 79 | # 80 | # { 81 | # packages: { 82 | # package_name: { 83 | # version: package-bundle-version-build 84 | # dependencies: ['depenency_name'] 85 | # } 86 | # dependencies: { 87 | # depenency_name: { 88 | # version: package-bundle-version-build 89 | # requested: ['package_name'] 90 | # } 91 | # } 92 | # 93 | # # generate a lock file and return a list of dependencies matched versions 94 | # def lock(dependencies): 95 | # locked = {} 96 | # matched = {} 97 | # 98 | # for package, constraint in dependencies.items(): 99 | # # find build containing requested package 100 | # package_match = packages.find(package, constraint) 101 | # (bundle, version, build) = package_match 102 | # package_deps = packages.dependencies(package, bundle, build) 103 | # locked[''] 104 | # 105 | # 106 | # def install(locked): 107 | # pass 108 | -------------------------------------------------------------------------------- /piku/core/packages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | import requests 4 | from piku.core import config, utils, errors 5 | 6 | 7 | # get package index 8 | @utils.cache('dict') 9 | @utils.cache('file', minutes=60 * 24) 10 | def get_index(): 11 | response = requests.get(config.index_url) 12 | if response.status_code != 200: 13 | raise errors.PikuError('Unable to download package index') 14 | return response.json().get('index') 15 | 16 | # list latest circuit python version target 17 | def latest_target(): 18 | index = get_index() 19 | return sorted(list(index.keys()))[-1] 20 | 21 | # list all packages in index 22 | def all(target=None): 23 | if target is None: target = config.get('circuit-python') 24 | index = get_index() 25 | return sorted(list(index.get(target).keys())) 26 | 27 | # suggest a package lexically similar to the one provided 28 | def suggest(package, target=None): 29 | return utils.similar(package, all(target)) 30 | 31 | # find version matching package 32 | def find(package, constraint, target=None): 33 | # get all packages 34 | if target is None: target = config.get('circuit-python') 35 | packages = get_index().get(target) 36 | 37 | # check that target version has packages 38 | if not packages: 39 | raise errors.PackageIndexNotFound(target) 40 | 41 | # check package exists 42 | if package not in packages: 43 | raise errors.PackageNotFound(package) 44 | 45 | # find latest matching version 46 | versions = utils.sort_versions(list(packages[package].keys()), reverse=True) 47 | for version in versions: 48 | matched = utils.matches_semver(constraint, version) or version == constraint 49 | if matched: 50 | return version 51 | raise errors.VersionNotFound(package, constraint) 52 | 53 | # returns a package's dependencies as specified in the bundle's package index 54 | def dependencies(package, version, target=None): 55 | if target is None: target = config.get('circuit-python') 56 | packages = get_index().get(target) 57 | return packages[package][version]['dependencies'] 58 | 59 | # get package info 60 | def info(package, version, target=None): 61 | if target is None: target = config.get('circuit-python') 62 | packages = get_index().get(target) 63 | return packages[package][version] 64 | 65 | # removes a package from lib 66 | def remove(package, lock, project_path=None): 67 | project_path = project_path or config.get('source') 68 | path = lock[package]['path'] 69 | package_base_name = os.path.basename(os.path.normpath(path)) 70 | library_path = os.path.join(project_path, 'lib') 71 | package_path = os.path.join(library_path, package_base_name) 72 | utils.remove(package_path) 73 | utils.remove(os.path.normpath(package_path) + '.mpy') 74 | 75 | # installs a package to lib 76 | def install(package, lock, project_path=None): 77 | # find bundle url 78 | package_info = lock[package] 79 | bundle_info = package_info['bundle'] 80 | bundle_url = bundle_info['url'] 81 | bundle_key = f'{bundle_info["name"]}-{bundle_info["build"]}-{bundle_info["target"]}' 82 | 83 | # download zip 84 | zip_path = os.path.join(config.bundle_path, f'{bundle_key}.zip') 85 | if not os.path.exists(zip_path): 86 | os.makedirs(config.bundle_path, exist_ok=True) 87 | utils.download(bundle_url, zip_path) 88 | 89 | # extract bundle zip 90 | extracted_path = os.path.join(config.bundle_path, f'{bundle_key}') 91 | if not os.path.exists(extracted_path): 92 | os.makedirs(extracted_path, exist_ok=True) 93 | with zipfile.ZipFile(zip_path, 'r') as zip: 94 | zip.extractall(extracted_path) 95 | extracted_path = os.path.join(extracted_path, os.listdir(extracted_path)[0]) 96 | 97 | # copy package to project 98 | project_path = project_path or config.get('source') 99 | library_path = os.path.join(project_path, 'lib') 100 | if os.path.isdir(project_path): 101 | os.makedirs(library_path, exist_ok=True) 102 | package_path = os.path.join(extracted_path, package_info['path']) 103 | if not os.path.exists(package_path): 104 | package_path = package_path + '.mpy' 105 | 106 | return utils.copy(package_path, library_path) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Piku 2 | Piku is small command line utility for managing CircuitPython projects 3 | 4 | The purpose of this project is to make creating a CircuitPython project, installing packages, deploying, and connecting to a CircuitPython device easy to do from the command line. 5 | 6 | ### Warning 7 | This tool is in early development please be careful when deploying and confirm before deploying that you are only deploying your CircuitPython device, not another drive or device. 8 | 9 | 10 | --- 11 | 12 | 13 | # Quick Start 14 | 15 | ### Installation 16 | Piku is a command line tool that can be installed on Windows, macOS, and Linux using pip3 or pipx. 17 | 18 | ``` 19 | pipx install piku 20 | ``` 21 | 22 | ### Usage 23 | After piku is installed you can now create, deploy, add packages, and upgrade CircuitPython projects. You can also use piku to connect to and debug your devices. 24 | 25 | ``` 26 | usage: piku [-h] {create,add,remove,install,upgrade,serial,deploy,version,info} ... 27 | create create new CircuitPython project 28 | add download and add package to project 29 | remove remove package from project 30 | install install project dependencies 31 | upgrade upgrade all project dependencies to latest 32 | serial connect usb serial port of device 33 | deploy deploy project to device 34 | version show piku version 35 | info show additional piku information 36 | 37 | ``` 38 | 39 | 40 | --- 41 | 42 | 43 | # Documentation 44 | 45 | 46 | ### Installation 47 | Piku can be installed on Windows, macOS, or Linux. This documentation is a work in progress, is you find issues please feel free to update them and make a pull request. To install Piku please ensure that you have at least **Python 3.8** and **pipx** or **pip3** installed, then run. Installing via **pipx** is preferred when there are 48 | dependency conflicts with other tools on your system. 49 | 50 | ``` 51 | pipx install piku 52 | ``` 53 | 54 | Or alternatively: 55 | 56 | ``` 57 | python3 -m pip install --user piku 58 | ``` 59 | 60 | After Piku is installed you should be able to run Piku from the command line. You can test this by executing: 61 | 62 | ``` 63 | piku version 64 | ``` 65 | 66 | ##### Additional Steps for Linux 67 | 68 | Some linux computers do not have the default pip user bin directory included in the PATH. If installing via pip you may add this directory to your PATH or install without the `--user` argument. 69 | 70 | https://unix.stackexchange.com/questions/612779/warning-not-on-path-when-i-tried-to-install-python-extensions-im-new-to-linu 71 | 72 | After installation if your user does not have permissions to use the serial port, you may need to add your user to the `dialout` group. 73 | https://askubuntu.com/questions/58119/changing-permissions-on-serial-port#answer-58122 74 | 75 | ### Preparing your Device 76 | 77 | Before creating a project you must have CircuitPython installed on your device, and have your device serial and USB drivers installed. Please check the CircuitPython website for instructions or the documentation of the board you have purchased. When your done you should be able to see your drive mounted as a USB drive named `CIRCUITPY`. 78 | 79 | https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython 80 | 81 | 82 | ### Creating a Project 83 | 84 | To create a new Piku project from the command line type: 85 | 86 | ``` 87 | piku create example 88 | ``` 89 | 90 | This will create a new directory with the name of your project and a few folders and files inside. After you have created a project to use Piku, enter the directory of the project you just created to use Piku: 91 | 92 | ``` 93 | cd example 94 | ``` 95 | 96 | 97 | ### Deploying your Project 98 | After you have created a project and modified the main `code.py` file you can deploy your project to a connected CircuitPython device. To deploy your project find the path of your `CIRCUITPY` UDB drive. Then type: 99 | 100 | ``` 101 | piku deploy -d 102 | ``` 103 | 104 | ***WARNING!!*** 105 | Deploying will remove other files from your device. Piku attempts to check that the device is actually a CircuitPython device, and backup your old files, but you still need to be very careful. 106 | 107 | You can also let Piku find your device by running deploy with no device argument: 108 | 109 | ``` 110 | piku deploy 111 | ``` 112 | 113 | After you have confirmed multiple times that you are deploying to the correct device you can also live on the wild side and skip the confirmation dialog using the `-y` command line argument. Please be careful. 114 | 115 | If changes have been made in your project code, the CircuitPython device should automatically detect and change files and reload. 116 | 117 | 118 | ### Connecting to your Device 119 | 120 | You can also connect to your CircuitPython device's serial port using Piku. To do this just use the serial command from your Piku project directory: 121 | 122 | ``` 123 | piku serial 124 | ``` 125 | 126 | If you are unable to connect, please confirm that you have the serial drivers for your device installed and you have permission to use the serial port (see installation instructions). If you know the serial port, or Piku is connecting to the wront port you can also try specifying it directly via the `-s` command line flag. 127 | 128 | Once connected you can exit by typing `ctrl-x`, enter the CircuitPython REPL by hitting `ctrl-c` and `ctrl-d` to exit the CircuitPython REPL. 129 | 130 | 131 | ### Adding CircuitPython Packages/Libraries 132 | 133 | You can easily download and add CircuitPython packages from the official Bundle or Community bundle using the command. For example to download and add the `neopixel` package you would type: 134 | 135 | ``` 136 | piku add neopixel 137 | ``` 138 | 139 | The specified package and its dependencies will be downloaded and added to your `lib` folder and your `pyproject.toml` file. You can also remove this package by typing: 140 | 141 | ``` 142 | piku remove neopixel 143 | ``` 144 | 145 | You can also install specific versions of packages by specifying in a similar way to other package manages: 146 | 147 | ``` 148 | piku add neopixel@~6 149 | ``` 150 | 151 | or 152 | 153 | ``` 154 | piku add neopixel@~6.1.2 155 | ``` 156 | 157 | You can also specify the target CircuitPython version (6 or 7) in your pyproject.toml file. One word of warning: package dependencies are often not broadly specified and may clash if you are not installing the latest versions of packages. 158 | 159 | ### Upgrading Packages 160 | 161 | You can upgrade all packages by running the upgrade command. 162 | ``` 163 | piku upgrade 164 | ``` 165 | 166 | You can also upgrade a single package by adding the latest version. 167 | ``` 168 | piku add neopixel 169 | ``` 170 | 171 | ### A Complete Example with Adafruit Feather Sense 172 | 173 | ##### Creating the Example Project 174 | Assuming you have successfully installed Piku, here is a complete example on how to create and deploy a Piku project to the [Adafruit Feather Sense](https://www.adafruit.com/product/4516) board using a Linux computer. 175 | 176 | First setup the board to user CircuitPython following the instructions found here: 177 | https://learn.adafruit.com/adafruit-feather-sense/circuitpython-on-feather-sense 178 | 179 | After your board is setup and mounts as a `CIRCUITPY` USB drive create a new Piku project and enter the project directory. 180 | ``` 181 | piku create example 182 | cd example 183 | ``` 184 | 185 | ##### Adding Main Example Program 186 | After you have created a project you will need to edit `project/code.py` which is the main file for your project (this is a CircuitPython convention). Open `project/code.py` and paste the following [Demo Code from AdaFruit](https://learn.adafruit.com/adafruit-feather-sense/circuitpython-sense-demo). 187 | 188 | ```py 189 | # SPDX-FileCopyrightText: 2020 Kattni Rembor for Adafruit Industries 190 | # 191 | # SPDX-License-Identifier: MIT 192 | # 193 | """Sensor demo for Adafruit Feather Sense. Prints data from each of the sensors.""" 194 | import time 195 | import array 196 | import math 197 | import board 198 | import audiobusio 199 | import adafruit_apds9960.apds9960 200 | import adafruit_bmp280 201 | import adafruit_lis3mdl 202 | import adafruit_lsm6ds.lsm6ds33 203 | import adafruit_sht31d 204 | 205 | i2c = board.I2C() 206 | 207 | apds9960 = adafruit_apds9960.apds9960.APDS9960(i2c) 208 | bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c) 209 | lis3mdl = adafruit_lis3mdl.LIS3MDL(i2c) 210 | lsm6ds33 = adafruit_lsm6ds.lsm6ds33.LSM6DS33(i2c) 211 | sht31d = adafruit_sht31d.SHT31D(i2c) 212 | microphone = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, 213 | sample_rate=16000, bit_depth=16) 214 | 215 | def normalized_rms(values): 216 | minbuf = int(sum(values) / len(values)) 217 | return int(math.sqrt(sum(float(sample - minbuf) * 218 | (sample - minbuf) for sample in values) / len(values))) 219 | 220 | apds9960.enable_proximity = True 221 | apds9960.enable_color = True 222 | 223 | # Set this to sea level pressure in hectoPascals at your location for accurate altitude reading. 224 | bmp280.sea_level_pressure = 1013.25 225 | 226 | while True: 227 | samples = array.array('H', [0] * 160) 228 | microphone.record(samples, len(samples)) 229 | 230 | print("\nFeather Sense Sensor Demo") 231 | print("---------------------------------------------") 232 | print("Proximity:", apds9960.proximity) 233 | print("Red: {}, Green: {}, Blue: {}, Clear: {}".format(*apds9960.color_data)) 234 | print("Temperature: {:.1f} C".format(bmp280.temperature)) 235 | print("Barometric pressure:", bmp280.pressure) 236 | print("Altitude: {:.1f} m".format(bmp280.altitude)) 237 | print("Magnetic: {:.3f} {:.3f} {:.3f} uTesla".format(*lis3mdl.magnetic)) 238 | print("Acceleration: {:.2f} {:.2f} {:.2f} m/s^2".format(*lsm6ds33.acceleration)) 239 | print("Gyro: {:.2f} {:.2f} {:.2f} dps".format(*lsm6ds33.gyro)) 240 | print("Humidity: {:.1f} %".format(sht31d.relative_humidity)) 241 | print("Sound level:", normalized_rms(samples)) 242 | time.sleep(0.3) 243 | ``` 244 | 245 | ##### Installing Packages 246 | Next install the required libraries for the AdaFruit Feather Sense example: 247 | ``` 248 | piku add adafruit_apds9960 249 | piku add adafruit_bmp280 250 | piku add adafruit_lis3mdl 251 | piku add adafruit_lsm6ds 252 | piku add adafruit_sht31d 253 | piku add neopixel 254 | ``` 255 | 256 | These packages should now found to your project `lib` folder, and your `pyproject.toml` file. Confirm this by listing the files in your `lib` directory using `ls project/lib`. The ls command should return something the following if all packages were installed properly: 257 | ``` 258 | adafruit_apds9960 259 | adafruit_bus_device 260 | adafruit_lsm6ds 261 | adafruit_register 262 | neopixel.mpy 263 | adafruit_bmp280.mpy 264 | adafruit_lis3mdl.mpy 265 | adafruit_pixelbuf.mpy 266 | adafruit_sht31d.mpy 267 | ``` 268 | 269 | Your pyproject.toml file should now look something like this: 270 | ``` 271 | [tool.piku] 272 | project = "example" 273 | piku = "0.1.8" 274 | circuit-python = "7" 275 | 276 | [tool.piku.dependencies] 277 | neopixel = "latest" 278 | adafruit_bmp280 = "latest" 279 | adafruit_apds9960 = "latest" 280 | adafruit_lis3mdl = "latest" 281 | adafruit_lsm6ds = "latest" 282 | adafruit_sht31d = "latest" 283 | ``` 284 | 285 | ##### Deploying to the Device 286 | Now make sure your device is mounted as a USB drive and find the device's mount point. This should be something like `/media/username/CIRCUITPY/` or a drive letter on windows. ***Make note of this!*** 287 | 288 | After your device is connected and mounted, you can deploy your code using the deploy command: 289 | ``` 290 | piku deploy -d 291 | ``` 292 | 293 | Next before deploying confirm that the device selected is the correct device. When Piku deploys it first attempts to validate its a CircuitPython device and then tries to backup the contents of the device in a temporary location. After validation and backup it loads your files onto the device and removes almost all other files 294 | 295 | You can also forego the `-d ` argument and let Piku attempt to find your device, but please confirm that you are deploying to the correct device so you don't lost any data. 296 | 297 | ##### Connecting to the Serial Port 298 | After your code is deployed you can connect to the serial port to see your code in action. It make take a minute for the device to reload. To connect to the serial port run: 299 | 300 | ``` 301 | piku serial 302 | ``` 303 | 304 | Piku will attempt to connect to the first available serial port and reach your device, if you have more than one serial port you may need to specify which port via the command like arguments. Hit `ctrl-x` to exit or `ctrl-c`/`ctrl-d` to enter/exit the CircuitPython REPL. 305 | 306 | Thanks it! Happy hacking! 307 | 308 | 309 | # Contributing 310 | 311 | Contributions are very welcome, my time to work on the project is limited. Please post issues and pull requests on Github if you would like to help forward the project. 312 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins=pylint_quotes 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time. See also the "--disable" option for examples. 51 | #enable= 52 | 53 | # Disable the message, report, category or checker with the given id(s). You 54 | # can either give multiple identifiers separated by comma (,) or put this 55 | # option multiple times (only on the command line, not in the configuration 56 | # file where it should appear only once).You can also use "--disable=all" to 57 | # disable everything first and then reenable specific checks. For example, if 58 | # you want to run only the similarities checker, you can use "--disable=all 59 | # --enable=similarities". If you want to run only the classes checker, but have 60 | # no Warning level messages displayed, use"--disable=all --enable=classes 61 | # --disable=W" 62 | disable= 63 | C0321, 64 | C0103, 65 | C1001, 66 | W0232, 67 | E1101, 68 | W0622, 69 | R0903, 70 | C0330, 71 | E0401, 72 | C0412, 73 | C0411, 74 | W1514, 75 | missing-docstring, 76 | import-star-module-level, 77 | old-octal-literal, 78 | oct-method, 79 | print-statement, 80 | unpacking-in-except, 81 | parameter-unpacking, 82 | backtick, 83 | old-raise-syntax, 84 | old-ne-operator, 85 | long-suffix, 86 | dict-view-method, 87 | dict-iter-method, 88 | metaclass-assignment, 89 | next-method-called, 90 | raising-string, 91 | indexing-exception, 92 | raw_input-builtin, 93 | long-builtin, 94 | file-builtin, 95 | execfile-builtin, 96 | coerce-builtin, 97 | cmp-builtin, 98 | buffer-builtin, 99 | basestring-builtin, 100 | apply-builtin, 101 | filter-builtin-not-iterating, 102 | using-cmp-argument, 103 | useless-suppression, 104 | range-builtin-not-iterating, 105 | suppressed-message, 106 | no-absolute-import, 107 | old-division, 108 | cmp-method, 109 | reload-builtin, 110 | zip-builtin-not-iterating, 111 | intern-builtin, 112 | unichr-builtin, 113 | reduce-builtin, 114 | standarderror-builtin, 115 | unicode-builtin, 116 | xrange-builtin, 117 | coerce-method, 118 | delslice-method, 119 | getslice-method, 120 | setslice-method, 121 | input-builtin, 122 | round-builtin, 123 | hex-method, 124 | nonzero-method, 125 | map-builtin-not-iterating 126 | 127 | 128 | [REPORTS] 129 | 130 | # Set the output format. Available formats are text, parseable, colorized, msvs 131 | # (visual studio) and html. You can also give a reporter class, eg 132 | # mypackage.mymodule.MyReporterClass. 133 | output-format=text 134 | 135 | # Put messages in a separate file for each module / package specified on the 136 | # command line instead of printing them on stdout. Reports (if any) will be 137 | # written in a file name "pylint_global.[txt|html]". 138 | files-output=no 139 | 140 | # Tells whether to display a full report or only the messages 141 | reports=yes 142 | 143 | # Python expression which should return a note less than 10 (10 is the highest 144 | # note). You have access to the variables errors warning, statement which 145 | # respectively contain the number of errors / warnings messages and the total 146 | # number of statements analyzed. This is used by the global evaluation report 147 | # (RP0004). 148 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 149 | 150 | # Template used to display messages. This is a python new-style format string 151 | # used to format the message information. See doc for all details 152 | #msg-template= 153 | 154 | 155 | [VARIABLES] 156 | 157 | # Tells whether we should check for unused import in __init__ files. 158 | init-import=no 159 | 160 | # A regular expression matching the name of dummy variables (i.e. expectedly 161 | # not used). 162 | dummy-variables-rgx=_$|dummy 163 | 164 | # List of additional names supposed to be defined in builtins. Remember that 165 | # you should avoid to define new builtins when possible. 166 | additional-builtins= 167 | 168 | # List of strings which can identify a callback function by name. A callback 169 | # name must start or end with one of those strings. 170 | callbacks=cb_,_cb 171 | 172 | 173 | [SIMILARITIES] 174 | 175 | # Minimum lines number of a similarity. 176 | min-similarity-lines=4 177 | 178 | # Ignore comments when computing similarities. 179 | ignore-comments=yes 180 | 181 | # Ignore docstrings when computing similarities. 182 | ignore-docstrings=yes 183 | 184 | # Ignore imports when computing similarities. 185 | ignore-imports=no 186 | 187 | 188 | [BASIC] 189 | 190 | # List of builtins function names that should not be used, separated by a comma 191 | bad-functions=map,filter,input 192 | 193 | # Good variable names which should always be accepted, separated by a comma 194 | good-names=i,j,k,ex,Run,_ 195 | 196 | # Bad variable names which should always be refused, separated by a comma 197 | bad-names=foo,bar,baz,toto,tutu,tata 198 | 199 | # Colon-delimited sets of names that determine each other's naming style when 200 | # the name regexes allow several styles. 201 | name-group= 202 | 203 | # Include a hint for the correct naming format with invalid-name 204 | include-naming-hint=no 205 | 206 | # Regular expression matching correct function names 207 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 208 | 209 | # Naming hint for function names 210 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 211 | 212 | # Regular expression matching correct variable names 213 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 214 | 215 | # Naming hint for variable names 216 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 217 | 218 | # Regular expression matching correct constant names 219 | const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ 220 | 221 | # Naming hint for constant names 222 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 223 | 224 | # Regular expression matching correct attribute names 225 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 226 | 227 | # Naming hint for attribute names 228 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 229 | 230 | # Regular expression matching correct argument names 231 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 232 | 233 | # Naming hint for argument names 234 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 235 | 236 | # Regular expression matching correct class attribute names 237 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 238 | 239 | # Naming hint for class attribute names 240 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 241 | 242 | # Regular expression matching correct inline iteration names 243 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 244 | 245 | # Naming hint for inline iteration names 246 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 247 | 248 | # Regular expression matching correct class names 249 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 250 | 251 | # Naming hint for class names 252 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 253 | 254 | # Regular expression matching correct module names 255 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 256 | 257 | # Naming hint for module names 258 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 259 | 260 | # Regular expression matching correct method names 261 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 262 | 263 | # Naming hint for method names 264 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 265 | 266 | # Regular expression which should only match function or class names that do 267 | # not require a docstring. 268 | no-docstring-rgx=^_ 269 | 270 | # Minimum line length for functions/classes that require docstrings, shorter 271 | # ones are exempt. 272 | docstring-min-length=-1 273 | 274 | 275 | [ELIF] 276 | 277 | # Maximum number of nested blocks for function / method body 278 | max-nested-blocks=5 279 | 280 | 281 | [MISCELLANEOUS] 282 | 283 | # List of note tags to take in consideration, separated by a comma. 284 | notes=FIXME,XXX,TODO 285 | 286 | 287 | [LOGGING] 288 | 289 | # Logging modules to check that the string format arguments are in logging 290 | # function parameter format 291 | logging-modules=logging 292 | 293 | 294 | [TYPECHECK] 295 | 296 | # Tells whether missing members accessed in mixin class should be ignored. A 297 | # mixin class is detected if its name ends with "mixin" (case insensitive). 298 | ignore-mixin-members=yes 299 | 300 | # List of module names for which member attributes should not be checked 301 | # (useful for modules/projects where namespaces are manipulated during runtime 302 | # and thus existing member attributes cannot be deduced by static analysis. It 303 | # supports qualified module names, as well as Unix pattern matching. 304 | ignored-modules= 305 | 306 | # List of classes names for which member attributes should not be checked 307 | # (useful for classes with attributes dynamically set). This supports can work 308 | # with qualified names. 309 | ignored-classes= 310 | 311 | # List of members which are set dynamically and missed by pylint inference 312 | # system, and so shouldn't trigger E1101 when accessed. Python regular 313 | # expressions are accepted. 314 | generated-members= 315 | 316 | 317 | [SPELLING] 318 | 319 | # Spelling dictionary name. Available dictionaries: none. To make it working 320 | # install python-enchant package. 321 | spelling-dict= 322 | 323 | # List of comma separated words that should not be checked. 324 | spelling-ignore-words= 325 | 326 | # A path to a file that contains private dictionary; one word per line. 327 | spelling-private-dict-file= 328 | 329 | # Tells whether to store unknown words to indicated private dictionary in 330 | # --spelling-private-dict-file option instead of raising a message. 331 | spelling-store-unknown-words=no 332 | 333 | 334 | [FORMAT] 335 | 336 | # Maximum number of characters on a single line. 337 | max-line-length=120 338 | 339 | # Regexp for a line that is allowed to be longer than the limit. 340 | ignore-long-lines=^\s*(# )??$ 341 | 342 | # Allow the body of an if to be on the same line as the test if there is no 343 | # else. 344 | single-line-if-stmt=no 345 | 346 | # List of optional constructs for which whitespace checking is disabled. `dict- 347 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 348 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 349 | # `empty-line` allows space-only lines. 350 | no-space-check=trailing-comma,dict-separator 351 | 352 | # Maximum number of lines in a module 353 | max-module-lines=1000 354 | 355 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 356 | # tab). 357 | indent-string=' ' 358 | 359 | # Number of spaces of indent required inside a hanging or continued line. 360 | indent-after-paren=4 361 | 362 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 363 | expected-line-ending-format= 364 | 365 | 366 | [CLASSES] 367 | 368 | # List of method names used to declare (i.e. assign) instance attributes. 369 | defining-attr-methods=__init__,__new__,setUp 370 | 371 | # List of valid names for the first argument in a class method. 372 | valid-classmethod-first-arg=cls 373 | 374 | # List of valid names for the first argument in a metaclass class method. 375 | valid-metaclass-classmethod-first-arg=mcs 376 | 377 | # List of member names, which should be excluded from the protected access 378 | # warning. 379 | exclude-protected=_asdict,_fields,_replace,_source,_make 380 | 381 | 382 | [IMPORTS] 383 | 384 | # Deprecated modules which should not be used, separated by a comma 385 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 386 | 387 | # Create a graph of every (i.e. internal and external) dependencies in the 388 | # given file (report RP0402 must not be disabled) 389 | import-graph= 390 | 391 | # Create a graph of external dependencies in the given file (report RP0402 must 392 | # not be disabled) 393 | ext-import-graph= 394 | 395 | # Create a graph of internal dependencies in the given file (report RP0402 must 396 | # not be disabled) 397 | int-import-graph= 398 | 399 | 400 | [DESIGN] 401 | 402 | # Maximum number of arguments for function / method 403 | max-args=5 404 | 405 | # Argument names that match this expression will be ignored. Default to name 406 | # with leading underscore 407 | ignored-argument-names=_.* 408 | 409 | # Maximum number of locals for function / method body 410 | max-locals=15 411 | 412 | # Maximum number of return / yield for function / method body 413 | max-returns=6 414 | 415 | # Maximum number of branch for function / method body 416 | max-branches=12 417 | 418 | # Maximum number of statements in function / method body 419 | max-statements=50 420 | 421 | # Maximum number of parents for a class (see R0901). 422 | max-parents=7 423 | 424 | # Maximum number of attributes for a class (see R0902). 425 | max-attributes=7 426 | 427 | # Minimum number of public methods for a class (see R0903). 428 | min-public-methods=2 429 | 430 | # Maximum number of public methods for a class (see R0904). 431 | max-public-methods=20 432 | 433 | # Maximum number of boolean expressions in a if statement 434 | max-bool-expr=5 435 | 436 | 437 | [EXCEPTIONS] 438 | 439 | # Exceptions that will emit a warning when being caught. Defaults to 440 | # "Exception" 441 | overgeneral-exceptions=Exception 442 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "adafruit-board-toolkit" 3 | version = "1.1.0" 4 | description = "CircuitPython board identification and information" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.5.0" 8 | 9 | [package.dependencies] 10 | pyserial = "*" 11 | 12 | [[package]] 13 | name = "appdirs" 14 | version = "1.4.4" 15 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 16 | category = "main" 17 | optional = false 18 | python-versions = "*" 19 | 20 | [[package]] 21 | name = "astroid" 22 | version = "2.9.3" 23 | description = "An abstract syntax tree for Python with inference support." 24 | category = "dev" 25 | optional = false 26 | python-versions = ">=3.6.2" 27 | 28 | [package.dependencies] 29 | lazy-object-proxy = ">=1.4.0" 30 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 31 | wrapt = ">=1.11,<1.14" 32 | 33 | [[package]] 34 | name = "atomicwrites" 35 | version = "1.4.0" 36 | description = "Atomic file writes." 37 | category = "dev" 38 | optional = false 39 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 40 | 41 | [[package]] 42 | name = "attrs" 43 | version = "21.4.0" 44 | description = "Classes Without Boilerplate" 45 | category = "dev" 46 | optional = false 47 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 48 | 49 | [package.extras] 50 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 51 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 52 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 53 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 54 | 55 | [[package]] 56 | name = "cachy" 57 | version = "0.3.0" 58 | description = "Cachy provides a simple yet effective caching library." 59 | category = "main" 60 | optional = false 61 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 62 | 63 | [package.extras] 64 | redis = ["redis (>=3.3.6,<4.0.0)"] 65 | memcached = ["python-memcached (>=1.59,<2.0)"] 66 | msgpack = ["msgpack-python (>=0.5,<0.6)"] 67 | 68 | [[package]] 69 | name = "certifi" 70 | version = "2021.10.8" 71 | description = "Python package for providing Mozilla's CA Bundle." 72 | category = "main" 73 | optional = false 74 | python-versions = "*" 75 | 76 | [[package]] 77 | name = "charset-normalizer" 78 | version = "2.0.11" 79 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 80 | category = "main" 81 | optional = false 82 | python-versions = ">=3.5.0" 83 | 84 | [package.extras] 85 | unicode_backport = ["unicodedata2"] 86 | 87 | [[package]] 88 | name = "colorama" 89 | version = "0.4.4" 90 | description = "Cross-platform colored terminal text." 91 | category = "dev" 92 | optional = false 93 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 94 | 95 | [[package]] 96 | name = "idna" 97 | version = "3.3" 98 | description = "Internationalized Domain Names in Applications (IDNA)" 99 | category = "main" 100 | optional = false 101 | python-versions = ">=3.5" 102 | 103 | [[package]] 104 | name = "iniconfig" 105 | version = "1.1.1" 106 | description = "iniconfig: brain-dead simple config-ini parsing" 107 | category = "dev" 108 | optional = false 109 | python-versions = "*" 110 | 111 | [[package]] 112 | name = "isort" 113 | version = "5.10.1" 114 | description = "A Python utility / library to sort Python imports." 115 | category = "dev" 116 | optional = false 117 | python-versions = ">=3.6.1,<4.0" 118 | 119 | [package.extras] 120 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 121 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 122 | colors = ["colorama (>=0.4.3,<0.5.0)"] 123 | plugins = ["setuptools"] 124 | 125 | [[package]] 126 | name = "jinja2" 127 | version = "3.0.3" 128 | description = "A very fast and expressive template engine." 129 | category = "main" 130 | optional = false 131 | python-versions = ">=3.6" 132 | 133 | [package.dependencies] 134 | MarkupSafe = ">=2.0" 135 | 136 | [package.extras] 137 | i18n = ["Babel (>=2.7)"] 138 | 139 | [[package]] 140 | name = "lazy-object-proxy" 141 | version = "1.7.1" 142 | description = "A fast and thorough lazy object proxy." 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=3.6" 146 | 147 | [[package]] 148 | name = "markupsafe" 149 | version = "2.0.1" 150 | description = "Safely add untrusted strings to HTML/XML markup." 151 | category = "main" 152 | optional = false 153 | python-versions = ">=3.6" 154 | 155 | [[package]] 156 | name = "mccabe" 157 | version = "0.6.1" 158 | description = "McCabe checker, plugin for flake8" 159 | category = "dev" 160 | optional = false 161 | python-versions = "*" 162 | 163 | [[package]] 164 | name = "packaging" 165 | version = "21.3" 166 | description = "Core utilities for Python packages" 167 | category = "dev" 168 | optional = false 169 | python-versions = ">=3.6" 170 | 171 | [package.dependencies] 172 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 173 | 174 | [[package]] 175 | name = "platformdirs" 176 | version = "2.4.1" 177 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=3.7" 181 | 182 | [package.extras] 183 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 184 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 185 | 186 | [[package]] 187 | name = "pluggy" 188 | version = "0.13.1" 189 | description = "plugin and hook calling mechanisms for python" 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 193 | 194 | [package.extras] 195 | dev = ["pre-commit", "tox"] 196 | 197 | [[package]] 198 | name = "poetry-core" 199 | version = "1.0.7" 200 | description = "Poetry PEP 517 Build Backend" 201 | category = "main" 202 | optional = false 203 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 204 | 205 | [[package]] 206 | name = "py" 207 | version = "1.11.0" 208 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 209 | category = "dev" 210 | optional = false 211 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 212 | 213 | [[package]] 214 | name = "pylint" 215 | version = "2.12.2" 216 | description = "python code static checker" 217 | category = "dev" 218 | optional = false 219 | python-versions = ">=3.6.2" 220 | 221 | [package.dependencies] 222 | astroid = ">=2.9.0,<2.10" 223 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 224 | isort = ">=4.2.5,<6" 225 | mccabe = ">=0.6,<0.7" 226 | platformdirs = ">=2.2.0" 227 | toml = ">=0.9.2" 228 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 229 | 230 | [[package]] 231 | name = "pylint-quotes" 232 | version = "0.2.3" 233 | description = "Quote consistency checker for PyLint.." 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=3.6" 237 | 238 | [package.dependencies] 239 | pylint = ">=2.8.0" 240 | 241 | [[package]] 242 | name = "pyparsing" 243 | version = "3.0.7" 244 | description = "Python parsing module" 245 | category = "dev" 246 | optional = false 247 | python-versions = ">=3.6" 248 | 249 | [package.extras] 250 | diagrams = ["jinja2", "railroad-diagrams"] 251 | 252 | [[package]] 253 | name = "pyserial" 254 | version = "3.5" 255 | description = "Python Serial Port Extension" 256 | category = "main" 257 | optional = false 258 | python-versions = "*" 259 | 260 | [package.extras] 261 | cp2110 = ["hidapi"] 262 | 263 | [[package]] 264 | name = "pytest" 265 | version = "7.0.0" 266 | description = "pytest: simple powerful testing with Python" 267 | category = "dev" 268 | optional = false 269 | python-versions = ">=3.6" 270 | 271 | [package.dependencies] 272 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 273 | attrs = ">=19.2.0" 274 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 275 | iniconfig = "*" 276 | packaging = "*" 277 | pluggy = ">=0.12,<2.0" 278 | py = ">=1.8.2" 279 | tomli = ">=1.0.0" 280 | 281 | [package.extras] 282 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 283 | 284 | [[package]] 285 | name = "pytest-mock" 286 | version = "3.7.0" 287 | description = "Thin-wrapper around the mock package for easier use with pytest" 288 | category = "dev" 289 | optional = false 290 | python-versions = ">=3.7" 291 | 292 | [package.dependencies] 293 | pytest = ">=5.0" 294 | 295 | [package.extras] 296 | dev = ["pre-commit", "tox", "pytest-asyncio"] 297 | 298 | [[package]] 299 | name = "pytest-watcher" 300 | version = "0.2.3" 301 | description = "Continiously runs pytest on changes in *.py files" 302 | category = "dev" 303 | optional = false 304 | python-versions = ">=3.6.2,<4.0.0" 305 | 306 | [package.dependencies] 307 | watchdog = ">=2.0.0" 308 | 309 | [[package]] 310 | name = "requests" 311 | version = "2.27.1" 312 | description = "Python HTTP for Humans." 313 | category = "main" 314 | optional = false 315 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 316 | 317 | [package.dependencies] 318 | certifi = ">=2017.4.17" 319 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 320 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 321 | urllib3 = ">=1.21.1,<1.27" 322 | 323 | [package.extras] 324 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 325 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 326 | 327 | [[package]] 328 | name = "toml" 329 | version = "0.10.2" 330 | description = "Python Library for Tom's Obvious, Minimal Language" 331 | category = "main" 332 | optional = false 333 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 334 | 335 | [[package]] 336 | name = "tomli" 337 | version = "2.0.1" 338 | description = "A lil' TOML parser" 339 | category = "dev" 340 | optional = false 341 | python-versions = ">=3.7" 342 | 343 | [[package]] 344 | name = "typing-extensions" 345 | version = "4.0.1" 346 | description = "Backported and Experimental Type Hints for Python 3.6+" 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=3.6" 350 | 351 | [[package]] 352 | name = "urllib3" 353 | version = "1.26.8" 354 | description = "HTTP library with thread-safe connection pooling, file post, and more." 355 | category = "main" 356 | optional = false 357 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 358 | 359 | [package.extras] 360 | brotli = ["brotlipy (>=0.6.0)"] 361 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 362 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 363 | 364 | [[package]] 365 | name = "watchdog" 366 | version = "2.1.7" 367 | description = "Filesystem events monitoring" 368 | category = "main" 369 | optional = false 370 | python-versions = ">=3.6" 371 | 372 | [package.extras] 373 | watchmedo = ["PyYAML (>=3.10)"] 374 | 375 | [[package]] 376 | name = "wrapt" 377 | version = "1.13.3" 378 | description = "Module for decorators, wrappers and monkey patching." 379 | category = "dev" 380 | optional = false 381 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 382 | 383 | [metadata] 384 | lock-version = "1.1" 385 | python-versions = "^3.8" 386 | content-hash = "00fd012497056c579153fc8085b724be4f9a69ae1de90c92f26eb8e8b2701d08" 387 | 388 | [metadata.files] 389 | adafruit-board-toolkit = [ 390 | {file = "adafruit-board-toolkit-1.1.0.tar.gz", hash = "sha256:61e19c30854764230138f4c1d65bf3b5d6ef22866396b231df7123a5a2507f27"}, 391 | ] 392 | appdirs = [ 393 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 394 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 395 | ] 396 | astroid = [ 397 | {file = "astroid-2.9.3-py3-none-any.whl", hash = "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"}, 398 | {file = "astroid-2.9.3.tar.gz", hash = "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877"}, 399 | ] 400 | atomicwrites = [ 401 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 402 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 403 | ] 404 | attrs = [ 405 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 406 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 407 | ] 408 | cachy = [ 409 | {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, 410 | {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, 411 | ] 412 | certifi = [ 413 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 414 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 415 | ] 416 | charset-normalizer = [ 417 | {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, 418 | {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, 419 | ] 420 | colorama = [ 421 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 422 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 423 | ] 424 | idna = [ 425 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 426 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 427 | ] 428 | iniconfig = [ 429 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 430 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 431 | ] 432 | isort = [ 433 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 434 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 435 | ] 436 | jinja2 = [ 437 | {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, 438 | {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, 439 | ] 440 | lazy-object-proxy = [ 441 | {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, 442 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, 443 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, 444 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, 445 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, 446 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, 447 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, 448 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, 449 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, 450 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, 451 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, 452 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, 453 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, 454 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, 455 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, 456 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, 457 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, 458 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, 459 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, 460 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, 461 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, 462 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, 463 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, 464 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, 465 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, 466 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, 467 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, 468 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, 469 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, 470 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, 471 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, 472 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, 473 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, 474 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, 475 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, 476 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, 477 | {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, 478 | ] 479 | markupsafe = [ 480 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 481 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 482 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 483 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 484 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 485 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, 486 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, 487 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, 488 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 489 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 490 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 491 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 492 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 493 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 494 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 495 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 496 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 497 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 498 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 499 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, 500 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, 501 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, 502 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 503 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 504 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 505 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 506 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 507 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 508 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 509 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 510 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 511 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 512 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 513 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, 514 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, 515 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, 516 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 517 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 518 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 519 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 520 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 521 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 522 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 523 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 524 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 525 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 526 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 527 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 528 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, 529 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, 530 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, 531 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 532 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 533 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 534 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 535 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 536 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 537 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 538 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 539 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 540 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 541 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 542 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 543 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, 544 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, 545 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, 546 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 547 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 548 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 549 | ] 550 | mccabe = [ 551 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 552 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 553 | ] 554 | packaging = [ 555 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 556 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 557 | ] 558 | platformdirs = [ 559 | {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, 560 | {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, 561 | ] 562 | pluggy = [ 563 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 564 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 565 | ] 566 | poetry-core = [ 567 | {file = "poetry-core-1.0.7.tar.gz", hash = "sha256:98c11c755a16ef6c5673c22ca94a3802a7df4746a0853a70b6fae8b9f5cac206"}, 568 | {file = "poetry_core-1.0.7-py2.py3-none-any.whl", hash = "sha256:4f8a7f5390d772f42c4c4c3f188e6424b802cb4b57466c6633a1b9ac36f18a43"}, 569 | ] 570 | py = [ 571 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 572 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 573 | ] 574 | pylint = [ 575 | {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, 576 | {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, 577 | ] 578 | pylint-quotes = [ 579 | {file = "pylint-quotes-0.2.3.tar.gz", hash = "sha256:2d6bb3fa8a1a85af3af8a0ca875a719ac5bcdb735c45756284699d809c109c95"}, 580 | {file = "pylint_quotes-0.2.3-py2.py3-none-any.whl", hash = "sha256:89decd985d3c019314da630f5e3fe0e0df951c2310525fbd6e710bca329c810e"}, 581 | ] 582 | pyparsing = [ 583 | {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, 584 | {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, 585 | ] 586 | pyserial = [ 587 | {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, 588 | {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, 589 | ] 590 | pytest = [ 591 | {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, 592 | {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, 593 | ] 594 | pytest-mock = [ 595 | {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, 596 | {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, 597 | ] 598 | pytest-watcher = [ 599 | {file = "pytest-watcher-0.2.3.tar.gz", hash = "sha256:1937dd97e72caafd371d8cea7b3d70c88ff4fe35e6cdecb29c41bbdcbf1dcc2b"}, 600 | {file = "pytest_watcher-0.2.3-py3-none-any.whl", hash = "sha256:af935963399509a5b0e855740ba7227852f1a7fccfbb1cbb79fa19a445af02d2"}, 601 | ] 602 | requests = [ 603 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 604 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 605 | ] 606 | toml = [ 607 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 608 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 609 | ] 610 | tomli = [ 611 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 612 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 613 | ] 614 | typing-extensions = [ 615 | {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, 616 | {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, 617 | ] 618 | urllib3 = [ 619 | {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, 620 | {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, 621 | ] 622 | watchdog = [ 623 | {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, 624 | {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, 625 | {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, 626 | {file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"}, 627 | {file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"}, 628 | {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"}, 629 | {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"}, 630 | {file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"}, 631 | {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"}, 632 | {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"}, 633 | {file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"}, 634 | {file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"}, 635 | {file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"}, 636 | {file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"}, 637 | {file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"}, 638 | {file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"}, 639 | {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"}, 640 | {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"}, 641 | {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, 642 | {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, 643 | {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, 644 | {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, 645 | {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, 646 | {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, 647 | ] 648 | wrapt = [ 649 | {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, 650 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, 651 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, 652 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, 653 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, 654 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, 655 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, 656 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, 657 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, 658 | {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, 659 | {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, 660 | {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, 661 | {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, 662 | {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, 663 | {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, 664 | {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, 665 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, 666 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, 667 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, 668 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, 669 | {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, 670 | {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, 671 | {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, 672 | {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, 673 | {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, 674 | {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, 675 | {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, 676 | {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, 677 | {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, 678 | {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, 679 | {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, 680 | {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, 681 | {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, 682 | {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, 683 | {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, 684 | {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, 685 | {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, 686 | {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, 687 | {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, 688 | {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, 689 | {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, 690 | {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, 691 | {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, 692 | {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, 693 | {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, 694 | {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, 695 | {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, 696 | {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, 697 | {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, 698 | {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, 699 | {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, 700 | ] 701 | --------------------------------------------------------------------------------