diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b42097e865c14e2dde215fccdd2e303d6c103b36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/__pycache__/ \ No newline at end of file diff --git a/controlPlatform/__init__.py b/controlPlatform/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..197f7ec6e22a0392ef81d12dc326863c44ba24cf --- /dev/null +++ b/controlPlatform/__init__.py @@ -0,0 +1,14 @@ +from .extensions.baseband import Baseband +from .extensions.basic import Basic +from .extensions.boot import Boot +from .extensions.calibrator import Calibrator +from .extensions.central import Central +from .extensions.dds import Dds +from .extensions.fmcw import Fmcw +from .extensions.pll import Pll +from .extensions.radar import Radar +from .extensions.receiver import Receiver +from .extensions.transmitter import Transmitter +from .extensions.trigger import Trigger +from .extensions.sampler import Sampler +from .extension import Interface, Packager \ No newline at end of file diff --git a/controlPlatform/extension.py b/controlPlatform/extension.py new file mode 100644 index 0000000000000000000000000000000000000000..4c2c8108659b6cdfa81a8b66e4c384dedf982372 --- /dev/null +++ b/controlPlatform/extension.py @@ -0,0 +1,111 @@ +import struct +import logging + +class Packager(): + """ + Generates a bytestream for transmission of data. It gets the configuration bytes and the raw data to send. + + The raw data can be: + single values of int or float + lists of int or float (or both) + """ + + def __init__(self): + super().__init__() + + def __singleToByteArray(self, x, length): + """ + Converts a single value of float or int to its representation in bytes. + """ + if isinstance(x, float): + return bytearray(struct.pack('<d', x)) + elif isinstance(x, int): + return bytearray(x.to_bytes(length, 'little')) + + def __toByteArray(self, x, length): + """ + Converts a value or list to a bytestream. + """ + if isinstance(x, list): + listByteArray = bytearray() + for element in x: + listByteArray += self.__singleToByteArray(element, length) + return listByteArray + else: + return self.__singleToByteArray(x, length) + + def serializeConfig(self, conf, inputs = None, length = None): + """ + Converts a configuration input to a stream of bytes + """ + if length is None: + length = 0 + + confBytes = bytearray(conf) + if inputs is not None: # isinstance(inputs, float) or isinstance(inputs, int) or inputs: + valueBytes = self.__toByteArray(inputs, length) + return confBytes+valueBytes + else: + return confBytes + + def serializeConfigBin(self, conf, inputs): + """ + Converts a configuration input, together with a binary payload input to a stream of bytes + """ + confBytes = bytearray(conf) + bytearray(inputs) + return confBytes + +class Interface(): + """ + Interface prototype for extension with special interface functions + """ + def __init__(self, functionality): + super().__init__() + + self.functionality = functionality + self.packager = Packager() + + def send(self, keys, *args): + """ + Generic sending function + """ + command = self.functionality.keyList.walkKeychain(keys) + return self.packager.serializeConfig(command, *args) + + def sendBinary(self, keys, binary): + command = self.functionality.keyList.walkKeychain(keys) + return self.packager.serializeConfigBin(command, binary) + + def sendDouble(self, keys, value): + """ + Send the platform keys field with a float value + """ + return self.send(keys, float(value), 8) + + def sendInt(self, keys, value): + """ + Send the platform keys field with an integer value + """ + return self.send(keys, int(value), 4) + +class __Extension__(): + """ + Base class for different platform modes + """ + def __init__(self, child, platform, configHandler, extName, debug = None): + # Connect variables + self.child = child + self.platform = platform + self.name = extName + self.functionality = self.platform.functionality + self.configHandler = configHandler + + self.interface = Interface(self.functionality) + + # Setup logging + if debug is None: + self.logger = logging.getLogger("general_logs.net.Extension") + else: + self.logger = logging.getLogger("general_logs.net.Extension@%s" % debug) + + self.logger.setLevel(logging.DEBUG) diff --git a/controlPlatform/extensions/baseband.py b/controlPlatform/extensions/baseband.py new file mode 100644 index 0000000000000000000000000000000000000000..de3e9892345f6b0775312af77020c758d83abb1c --- /dev/null +++ b/controlPlatform/extensions/baseband.py @@ -0,0 +1,23 @@ +from ..extension import __Extension__ + +class Baseband(__Extension__): + """ + Extends a platform with baseband control functions + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Baseband") + + def setStageLevel(self, level, stage): + """ + Sets baseband level for a certain stage + """ + self.configHandler.communicate((self.interface.sendDouble(["CONFIG", "EXTERNAL", "BB", stage], level))) + + def setLevels(self, **levels): + """ + Sets baseband levels on a platform + """ + print("Set baseband levels on %s." % self.platform.name) + + for stage in levels: + self.setStageLevel(levels[stage], stage) \ No newline at end of file diff --git a/controlPlatform/extensions/basic.py b/controlPlatform/extensions/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..f85a3cb984c9d138026246cdf2c8976be2ca9f45 --- /dev/null +++ b/controlPlatform/extensions/basic.py @@ -0,0 +1,151 @@ +from ..extension import __Extension__ + + +class Basic(__Extension__): + """ + No mode is yet discovered for this platform + """ + + def __init__(self, platform, configHandler, family): + super().__init__(self, platform, configHandler, "Basic") + + # The family is used for resolving properties of a platform, based on its unique identifier + self.family = family + + # Unique identifier, e.g. MAC address + self.uuid = None + + # A color code for this platform + self.colorCode = None + + # Unique name, e.g. Niko + self.uname = None + + # Type of this platform + self.platformType = None + + # Family member. The name and position of this platform within the framework + self.member = None + + def informIndex(self, index): + """ + Informs a platform about its position in the array by index. This is mostly + relevant for transmitters that select a trigger impulse in the burst of triggers by index + """ + try: + self.configHandler.communicate(self.interface.sendInt( + ["CONFIG", "SYS", "IDX"], index)) + + except AttributeError: + # This platform has no index, since it has no position + pass + + def informRxCount(self, rx_cnt): + """ + Informs a platform about total number of receivers + """ + self.configHandler.communicate(self.interface.sendInt( + ["CONFIG", "SYS", "EXTERNAL", "SYS_RX_CNT"], rx_cnt)) + + def informTxCount(self, tx_cnt): + """ + Informs a platform about total number of receivers + """ + self.configHandler.communicate(self.interface.sendInt( + ["CONFIG", "SYS", "EXTERNAL", "SYS_TX_CNT"], tx_cnt)) + + def reset(self): + """ + Sends a soft reset signal to the platform + """ + print("Reset %s to bootloader." % self.platform.name) + self.configHandler.communicate(self.interface.send(["REQUEST", "RST"])) + + def scanTest(self): + """ + Connects to the platform for testing + """ + return self.configHandler.testconn() + + def getType(self): + """ + Scans the connected platform for its type, if there is a platform at the specified address + """ + # Ask platform for its type + platformType = self.configHandler.communicate( + self.interface.send(["REQUEST", "TYPE", "NAME"])) + + # hello! + return platformType.pop() + + def getSecondaryType(self): + """ + Scans the connected platform for its secondary type, if there is a platform at the specified address + """ + # Ask platform for its secondary type + secondaryType = self.configHandler.communicate( + self.interface.send(["REQUEST", "TYPE", "EXTENSION"])) + + # hello! + return secondaryType.pop() + + def getId(self): + """ + Asks for platform unique identifier + """ + + # Extracts the unique id from the network handler + uuid = self.configHandler.communicate( + self.interface.send(["REQUEST", "TYPE", "UUID"]), sections=[("uint8", None)]) + + # Convert the uuid to a string for display and storage, byte by byte + uuidStr = '0x' + + for field in uuid: + uuidStr = '%s%02x' % (uuidStr, field) + + return uuidStr + + def learnName(self): + """ + This platform now learns its own name and properties and stores it + """ + self.member = self.family.getMemberByUuid(self.uuid) + + # Store platform position as a top level property for later sorting + if self.member is not None: + # Platform is known + self.platform.pos = self.member.position + self.platform.name = self.member.name + + def scan(self): + """ + Scan for this platform on the network + Return object ID for the task + """ + self.platform.exists = self.scanTest() + + if self.platform.exists: + self.identify() + + return self.platform + + def informIndex(self): + """ + Inform platform of its index + """ + try: + self.informIndex(self.platform.idx) + except: + # Only inform platforms that have an index. + pass + + def identify(self): + """ + Extracts general information from platform, as well as uuid/name + """ + self.platformType = self.getType() + self.platformSecondaryType = self.getSecondaryType() + self.uuid = self.getId() + + self.learnName() diff --git a/controlPlatform/extensions/boot.py b/controlPlatform/extensions/boot.py new file mode 100644 index 0000000000000000000000000000000000000000..c4c7ec2481c1411b80ce0c94c49e996399cc6c89 --- /dev/null +++ b/controlPlatform/extensions/boot.py @@ -0,0 +1,30 @@ +from ..extension import __Extension__ + +class Boot(__Extension__): + """ + Extends a platform by the boot mode + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Boot") + + def flashErase(self): + """ + Erase the flash on the connected platform + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "FLASH", "ERASE"])) + + def flashWrite(self, data): + """ + Erase the flash on the connected platform + """ + self.configHandler.communicate(self.interface.sendBinary(["CONFIG", "FLASH", "WRITE"], data)) + + def flash(self, data): + print("Flashing %s." % self.platform.name) + self.flashErase() + self.flashWrite(data) + self.run() + + def run(self): + print("Run flash on %s." % self.platform.name) + self.configHandler.communicate(self.interface.send(["CONFIG", "FLASH", "RESET"])) \ No newline at end of file diff --git a/controlPlatform/extensions/calibrator.py b/controlPlatform/extensions/calibrator.py new file mode 100644 index 0000000000000000000000000000000000000000..c86083370ab0a4b3e3bd530f6a1c8c5a58dd9231 --- /dev/null +++ b/controlPlatform/extensions/calibrator.py @@ -0,0 +1,8 @@ +from ..extension import __Extension__ + +class Calibrator(__Extension__): + """ + Extends a platform by the "Calibrator" mode + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Calibrator") diff --git a/controlPlatform/extensions/central.py b/controlPlatform/extensions/central.py new file mode 100644 index 0000000000000000000000000000000000000000..824e1192255ea4f2155ff1e88c1af72c08edc8f3 --- /dev/null +++ b/controlPlatform/extensions/central.py @@ -0,0 +1,8 @@ +from ..extension import __Extension__ + +class Central(__Extension__): + """ + Extends a platform by the "Central" mode + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Central") diff --git a/controlPlatform/extensions/dds.py b/controlPlatform/extensions/dds.py new file mode 100644 index 0000000000000000000000000000000000000000..d4375c767a810f3fdbc1c63272c0abfdaa62b597 --- /dev/null +++ b/controlPlatform/extensions/dds.py @@ -0,0 +1,14 @@ +from ..extension import __Extension__ + +class Dds(__Extension__): + """ + Extends a platform with "Dds" functionality + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "DDS") + + def configure(self): + """ + Signal to the DDS platform that configuration is finished and that DDS can now be programmed + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "EXTERNAL", "DDS", "FINISHED"])) diff --git a/controlPlatform/extensions/fmcw.py b/controlPlatform/extensions/fmcw.py new file mode 100644 index 0000000000000000000000000000000000000000..56a5836e4d931b762ad1c711378473a59a545d6e --- /dev/null +++ b/controlPlatform/extensions/fmcw.py @@ -0,0 +1,67 @@ +from ..extension import __Extension__ + + +class Fmcw(__Extension__): + """ + Extends a platform with "Fmcw" functionality + """ + + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "FMCW") + + def fmcwConfigBandwidth(self, bandwidth): + """ + Program chirp bandwidth + """ + self.configHandler.communicate(self.interface.sendDouble( + ["CONFIG", "EXTERNAL", "FMCW", "CHIRP_BANDWIDTH"], bandwidth)) + + def fmcwConfigFrequency(self, startFreq): + """ + Program starting frequency of the chirp + """ + self.configHandler.communicate( + self.interface.sendDouble(["CONFIG", "EXTERNAL", "FMCW", "CHIRP_BASE_F"], startFreq)) + + def fmcwConfigOffsetFrequency(self, offsetFreq): + """ + Program starting frequency of the chirp + """ + self.configHandler.communicate(self.interface.sendDouble( + ["CONFIG", "EXTERNAL", "FMCW", "CHIRP_OFF_F"], offsetFreq)) + + def fmcwConfigHopFrequency(self, hopFreq): + """ + Program hopping frequency between chirps + """ + self.configHandler.communicate(self.interface.sendDouble( + ["CONFIG", "EXTERNAL", "FMCW", "CHIRP_HOP_F"], hopFreq)) + + def fmcwConfigDuration(self, duration): + """ + Program duration of the chirp + """ + self.configHandler.communicate(self.interface.sendDouble( + ["CONFIG", "EXTERNAL", "FMCW", "CHIRP_DURATION"], duration)) + + def fmcwConfigSamplingDelay(self, duration): + """ + Program delay after which amplifier switches on and starts to transmit (compensation) + """ + self.configHandler.communicate(self.interface.sendDouble( + ["CONFIG", "EXTERNAL", "FMCW", "TXDELAY"], duration)) + + def configure(self, bw, duration, fc, foffset=0, fhop=0, samplingDelay=0): + """ + Configures the FMCW settings on the platform + """ + + print("Configure FMCW on %s." % self.platform.name) + + # Transmit individual configuration settings + self.fmcwConfigBandwidth(bw) + self.fmcwConfigFrequency(fc) + self.fmcwConfigOffsetFrequency(foffset) + self.fmcwConfigHopFrequency(fhop) + self.fmcwConfigDuration(duration) + self.fmcwConfigSamplingDelay(samplingDelay) diff --git a/controlPlatform/extensions/pll.py b/controlPlatform/extensions/pll.py new file mode 100644 index 0000000000000000000000000000000000000000..40564d843151696b75a69cda37dad0eb45ff8462 --- /dev/null +++ b/controlPlatform/extensions/pll.py @@ -0,0 +1,15 @@ +from ..extension import __Extension__ + +class Pll(__Extension__): + """ + Extends a platform with "Pll" functionality + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "PLL") + + def configure(self): + """ + Signal to the PLL platform that configuration is finished and that PLL can now be programmed + """ + self.handler.communicate( + self.interface.send(["CONFIG", "EXTERNAL", "PLL", "FINISHED"])) \ No newline at end of file diff --git a/controlPlatform/extensions/radar.py b/controlPlatform/extensions/radar.py new file mode 100644 index 0000000000000000000000000000000000000000..b142fec38b9264bb8fce3bcd660b364886393d9b --- /dev/null +++ b/controlPlatform/extensions/radar.py @@ -0,0 +1,31 @@ +from ..extension import __Extension__ + +class Radar(__Extension__): + """ + Extends a platform with "Radar" functionality + """ + + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Radar") + + def informRxCount(self, rxCount): + """ + Informs the platform about the total count of receivers + """ + self.configHandler.communicate(self.interface.sendInt(["CONFIG", "SYS", "EXTERNAL", "SYS_RX_CNT"], rxCount)) + + def informTxCount(self, txCount): + """ + Informs the platform about the total count of transmitters + """ + self.configHandler.communicate(self.interface.sendInt(["CONFIG", "SYS", "EXTERNAL", "SYS_TX_CNT"], txCount)) + + def configure(self, rxCnt, txCnt): + """ + Configures the Radar settings on the platform + """ + print("Configure radar system settings on %s." % self.platform.name) + + # Transmit individual configuration settings + self.informRxCount(rxCnt) + self.informTxCount(txCnt) diff --git a/controlPlatform/extensions/receiver.py b/controlPlatform/extensions/receiver.py new file mode 100644 index 0000000000000000000000000000000000000000..0a09a02cbc8626cf801b56469a167f4952b88d33 --- /dev/null +++ b/controlPlatform/extensions/receiver.py @@ -0,0 +1,8 @@ +from ..extension import __Extension__ + +class Receiver(__Extension__): + """ + Extends a platform with the "Receiver" mode + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Receiver") \ No newline at end of file diff --git a/controlPlatform/extensions/sampler.py b/controlPlatform/extensions/sampler.py new file mode 100644 index 0000000000000000000000000000000000000000..74c36e296dd7f70387d1d314ec189d03a6a28698 --- /dev/null +++ b/controlPlatform/extensions/sampler.py @@ -0,0 +1,71 @@ +from ..extension import __Extension__ + +from networkUtilities import PlatformHandler +import numpy as np + +class Sampler(__Extension__): + """ + Extends a platform with ADC sampling capability + """ + + def __init__(self, platform, configHandler, dataHandler): + super().__init__(self, platform, configHandler, "Sampler") + + self.configHandler = configHandler + self.dataHandler = dataHandler + + def configDuration(self, duration): + """ + Program duration of the sampling + """ + self.configHandler.communicate(self.interface.sendDouble(["CONFIG", "SAMPLING", "DURATION"], duration)) + + def configFinished(self): + """ + Signal end of sampler configuration + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "SAMPLING", "FINISHED"])) + + def start(self): + """ + Make ready for sampling! This connects to the platform and opens the connection. Platform starts sampling automatically. + """ + self.dataHandler.connect() + + def stop(self): + """ + End sampling by disconnecting from the platform. + """ + self.dataHandler.disconnect() + + def get(self): + """ + Single shot sampling and storage on this platform + """ + # Do the sampling + self.adcBitFormatString = "int16" + rawBlock = self.dataHandler.receive(leaveOpen=True, sections=[("uint32", 8), (self.adcBitFormatString, None)]) + + measurement = np.asarray(rawBlock) + + measurementIndex = measurement[0] + errorCounter = measurement[1] + data = measurement[2:] + + try: + result = ((data, self.platform.idx), measurementIndex, errorCounter) + except AttributeError: + result = ((data, 0), measurementIndex, errorCounter) + + return result + + def configure(self, duration): + """ + Configures the sampling settings on the platform + """ + + print("Configure Sampling on %s." % self.platform.name) + + # Transmit individual configuration settings + self.configDuration(duration) + self.configFinished() diff --git a/controlPlatform/extensions/transmitter.py b/controlPlatform/extensions/transmitter.py new file mode 100644 index 0000000000000000000000000000000000000000..d7ca8374a32f59cf50651f744203d98b479db6fd --- /dev/null +++ b/controlPlatform/extensions/transmitter.py @@ -0,0 +1,8 @@ +from ..extension import __Extension__ + +class Transmitter(__Extension__): + """ + Extends a platform with transmitter functions + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Transmitter") diff --git a/controlPlatform/extensions/trigger.py b/controlPlatform/extensions/trigger.py new file mode 100644 index 0000000000000000000000000000000000000000..268a78a2235d61ba08e0fec020d34e6bfc9d1247 --- /dev/null +++ b/controlPlatform/extensions/trigger.py @@ -0,0 +1,54 @@ +from ..extension import __Extension__ + +class Trigger(__Extension__): + """ + Extends a platform with trigger functions + """ + def __init__(self, platform, configHandler): + super().__init__(self, platform, configHandler, "Trigger") + + def force(self): + """ + Forces a trigger impulse in all receivers. This will be asynchronous + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "TRIGGER", "FORCE"])) + + def start(self): + """ + Starts the trigger impulses (or burst) on the trigger board (central platform) + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "TRIGGER", "START"])) + + def stop(self): + """ + Stops the trigger impulses. Also resets the frequency hopping counter. + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "TRIGGER", "STOP"])) + + def burst(self, burst): + """ + Sets the burst count of trigger impulses + """ + self.configHandler.communicate(self.interface.sendInt(["CONFIG", "TRIGGER", "BURST"], burst)) + + def rate(self, rate): + """ + Sets a new trigger frequency on the trigger board + """ + self.configHandler.communicate(self.interface.sendDouble(["CONFIG", "TRIGGER", "RATE"], rate)) + + def finish(self): + """ + Triggers configuration on embedded hardware + """ + self.configHandler.communicate(self.interface.send(["CONFIG", "TRIGGER", "FINISHED"])) + + def configure(self, rate = 1, burst = 1): + """ + Configure periodic trigger + """ + print("Configure periodic trigger on %s." % self.platform.name) + + self.burst(burst) + self.rate(rate) + self.finish() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..85d3f4d3907b7b44a13cd584c2c36e254e6736f1 --- /dev/null +++ b/setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Note: To use the 'upload' functionality of this file, you must: +# $ pipenv install twine --dev + +import io +import os +import sys +from shutil import rmtree + +from setuptools import find_packages, setup, Command + +# Package meta-data. +NAME = 'controlPlatform' +DESCRIPTION = 'Provides defenitions and extensions for the digital sampling platform.' +URL = '' +EMAIL = 'adrian@figueroa.eu' +AUTHOR = 'Adrian Figueroa' +REQUIRES_PYTHON = '>=3.6.0' +VERSION = '0.1.0' + +# What packages are required for this module to be executed? +REQUIRED = [ + 'extensionObject', + 'networkUtilities', + 'numpy' +] + +# What packages are optional? +EXTRAS = { +} + +# The rest you shouldn't have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for that! + +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.md' is present in your MANIFEST.in file! +try: + with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = '\n' + f.read() +except FileNotFoundError: + long_description = DESCRIPTION + +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + project_slug = NAME.lower().replace("-", "_").replace(" ", "_") + with open(os.path.join(here, project_slug, '__version__.py')) as f: + exec(f.read(), about) +else: + about['__version__'] = VERSION + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = 'Build and publish the package.' + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + pass + + self.status('Building Source and Wheel (universal) distribution…') + os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) + + self.status('Uploading the package to PyPI via Twine…') + os.system('twine upload dist/*') + + self.status('Pushing git tags…') + os.system('git tag v{0}'.format(about['__version__'])) + os.system('git push --tags') + + sys.exit() + + +# Where the magic happens: +setup( + name=NAME, + version=about['__version__'], + description=DESCRIPTION, + long_description=long_description, + long_description_content_type='text/markdown', + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), + # If your package is a single module, use this instead of 'packages': + # py_modules=['mypackage'], + + # entry_points={ + # 'console_scripts': ['mycli=mymodule:cli'], + # }, + install_requires=REQUIRED, + extras_require=EXTRAS, + include_package_data=True, + license='MIT', + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy' + ], + # $ setup.py publish support. + cmdclass={ + 'upload': UploadCommand, + }, +)