# ArchFX Cloud Python API Package
[![PyPI version](https://img.shields.io/pypi/v/archfx_cloud.svg)](https://pypi.python.org/pypi/archfx-cloud)
A python library for interacting with [ArchFX Cloud](https://archfx.io) Rest API.
## Installation
When it comes to using Python packages, it is always recommened you use a Python Virtual Env. Using Python 3, you can simply do
```bash
python3 -m venv ~/.virtualenv/archfx-cloud
source ~/.virtualenv/archfx-cloud/bin/activate
```
or follow one of the many tutorials to setup Python virtual environments.
Once you have set up a virtual, env, simply install the package with:
```bash
pip install archfx_cloud
```
Package is based on https://github.com/samgiles/slumber and https://github.com/iotile/python_iotile_cloud
## ArchFX Cloud Resource Overview
In a Rest API, Resources represent tables in the database. The following resources are available in **ArchFX Cloud**:
- **account**: Represent users. A user only has access to its own user profile
- **org**: Users belong to Organizations as members. Some of these users can act as admins for the organization.
- **site**: Organizations contain Sites. A site usually represents a geographical location.
- **area**: Sites can have Areas, which either represent a group of discrete manufacturing, or it is a group
of assembly lines. Areas usually represent Buildings or a set of machines of the same type.
- **line**: Sites or Areas can have Lines, which represent assembly lines. Lines are usually created within an area
but do not need to. But like areas, they do need to always belong to a Site.
- **machine**: A Machine represents the physical machine we are extracting data from. It is a virtual concept but
is usually one to one with a machine in the factory, and has a brand, model, serial number and maybe asset number.
- **device**: A device represent IOTile HW Taps, and/or Factory SW connectors that extract data from a Machine. a Given
**Machine** may have multiple HW taps or even multiple SW connectors, which is one there is a one to many
relationship between Machines and Devices
- **stream**: Streams represent a globally unique instance of data comming from a given device (sensor).
Streams represent the virtual data at the machine level (regardless of the device it came from). Over time,
a given stream may come from different devices (e.g. if a device breaks and needs to be replaced).
- **streampages**: A stream is a virtual concept that is build out of **device** data. Data on a device is
represented by a StreamPage. A stream page is always linked to a Stream, but may have a specific start/end
time period. A stream is therefore built out of stream pages, which may or may not come from the same device.
- **data**: Every Stream represents the time series data. This resource can be used to access this data.
### Globally Unique IDs
Most of the key records in the database use a universally unique ID, in the form of an ID Slug. We borrow the term slug
from blogging systems because we use it the same way to create unique but readable URLs.
The following are the resources that use a globally unique ID:
- Sites use **ps--0000-0001**
- Areas use **pa--0000-0001**
- Lines use **pl--0000-0001**
- Device **d--0000-0000-0000-0001** represent device 1. Note that this is also the Serial Number for the device itself,
and can be found on each IOTile Device. For virtual devices (SW connectors), the ID is also assigned from the same
place. Note that all device IDs are allocated and managed by https://iotile.cloud but that should be transparent to users.
All devices have the same `d--0000-0000` scope.
- Machines use the same standard as devices, except that the use a non-zero scope. e.g. **d--0000-0001-0000-0001**
- Sites, Areas, Lines, Machines and Devices can all have streams of data, but all share the same globally unique naming
standard. All streams are named based on `<parent_type>--<parent_id>--<device/machine_id>--<variable_id>` where the variable
is a 32bit identifier, usually following global IDs (e.g. `0000-5051` to represent `ProductReady`):
- **ss--0000-0001--0000-0000-0000-0000--0000-5051** represents a site stream for site `ps--0000-0001`
- **sa--0000-0001--0000-0000-0000-0000--0000-5051** represents an area stream for area `pa--0000-0001`
- **sl--0000-0001--0000-0000-0000-0000--0000-5051** represents a line stream for line `pl--0000-0001`
- **sl--0000-0001--0000-0000-0000-0001--0000-5051** represents a device stream for device `d--0000-0000-0000-0001` under line `pl--0000-0001`
- **sl--0000-0001--0000-0001-0000-0001--0000-5051** represents a machine stream for machine `d--0000-0001-0000-0001` under line `pl--0000-0001`
You can see how:
- Slug components are separated by a ‘--’ string
- A one or two character letter(s) represents the type of slug: 'p?', 'd', and 's?'
## User Guide
### Login and Logout
The Api class is used to login and logout from the ArchFX Cloud
Example:
```python
from archfx_cloud.api.connection import Api
c = Api('https://arch.arhfx.io')
ok = c.login(email=args.email, password=password)
if ok:
# Do something
c.logout()
```
If you have a JWT token, you can skip the login and just set the token:
```python
from archfx_cloud.api.connection import Api
c = Api('https://arch.arhfx.io')
c.set_token('big-ugly-token')
```
You can use the Api itself to login and get a token:
```python
from archfx_cloud.api.connection import Api
c = Api('https://arch.arhfx.io')
ok = c.login(email=args.email, password=password)
if ok:
token = c.token
# write out token or store in some secret .ini file
```
### Generic Rest API
The `Api(domain)` can be used to access any of the APIs in https://arch.archfx.io/api/v1/
The `Api(domain)` is generic and therefore will support any future resources supported by the ArchFX Cloud Rest API.
```python
from archfx_cloud.api.connection import Api
api = Api('https://arch.arhfx.io')
ok = api.login(email='user@example.com', password='my.pass')
## GET https://arch.archfx.io/api/v1/org/
## Note: Any kwargs passed to get(), post(), put(), delete() will be used as url parameters
api.org.get()
## POST https://arch.archfx.io/api/v1/org/
new = api.org.post({"name": "My new Org"})
## PUT https://arch.archfx.io/api/v1/org/{slug}/
api.org(new["slug"]).put({"about": "About Org"})
PATCH https://arch.archfx.io/api/v1/org/{slug}/
api.org(new["slug"]).patch({"about": "About new Org"})
## GET https://arch.archfx.io/api/v1/org/{slug}/
api.org(new["slug"]).get()
## DELETE https://arch.archfx.io/api/v1/org/{slug}/
## NOTE: Not all resources can be deleted by users
api.org(new["slug"]).delete()
```
You can pass arguments to any get() using
```python
# /api/v1/org/
for org in api.org.get()['results']:
# Pass any arguments as get(foo=1, bar='2'). e.g.
# /api/v1/site/?org__slug=<slug>
org_sites = c.site.get(org='{0}'.format(org['slug']))
```
You can also call nested resources/actions like this:
```python
# /api/v1/machine/<slug>/devices/
for org in api.machine.get()['results']:
# /api/v1/machine/<slug>/devices
associated_devices = c.machine(org['slug']).devices.get()
```
### Globaly unique ID slugs
To easily handle ID slugs, use the `utils.gid` package:
```python
parent = ArchFxParentSlug(5, ptype='pl)
assert(str(parent) == 'pl--0000-0005')
device = ArchFxDeviceSlug(10)
assert(str(device) == 'd--0000-0000-0000-000a')
id = ArchFxStreamSlug()
id.from_parts(parent=parent, device=device, variable='0000-5501)
assert(str(id) == 'sl--0000-0005--0000-0000-0000-000a--0000-5001')
parts = id.get_parts()
self.assertEqual(str(parts['parent']), str(parent))
self.assertEqual(str(parts['device']), str(device))
self.assertEqual(str(parts['variable']), '0000-5501')
# Other forms of use
device = ArchFxDeviceSlug('000a)
assert(str(device) == 'd--0000-0000-0000-000a')
device = ArchFxDeviceSlug('d--000a')
assert(str(device) == 'd--0000-0000-0000-000a')
device = ArchFxDeviceSlug(0xa)
assert(str(device) == 'd--0000-0000-0000-000a')
```
### BaseMain Utility Class
As you can see from the examples above, every script is likely to follow the following format:
```python
# Parse arguments from user and get password
# Login to server
# Do some real work
# Logout
```
To make it easy to add this boilerplate code, the BaseMain can be used to follow a predefined, opinionated flow
which basically configures the `logging` and `argsparse` python packages with a basic configuration during the
construction. Then the `main()` method runs the following flow, where each function call can be overwritten in your
own derived class
```python
self.domain = self.get_domain()
self.api = Api(self.domain)
self.before_login()
ok = self.login()
if ok:
self.after_login()
self.logout()
self.after_logout()
```
An example of how to use this class is shown below:
```python
class MyScript(BaseMain):
def add_extra_args(self):
# Add extra positional argument (as example)
self.parser.add_argument('foo', metavar='foo', type=str, help='RTFM')
def before_login(self):
logger.info('-----------')
def after_login(self):
# Main function to OVERWITE and do real work
do_some_real_work(self.api, self.args)
def login(self):
# Add extra message welcoming user
ok = super(MyScript, self).login()
if ok:
logger.info('Welcome {0}'.format(self.args.email))
return ok
def logout(self):
# Add extra message to say Goodbye
super(MyScript, self).logout()
logger.info('Goodbye!')
if __name__ == '__main__':
work = MyScript()
work.main()
```
### Uploading a Streamer Report
The `ArchFXDataPoint` and `ArchFXFlexibleDictionaryReport` helper classes can be used to generate a Streamer Report
compatible with ArchFX Cloud. A Streamer Report can be used to send several stream data records together.
Using Streamer Reports have several benefits over uploading data manually using the Rest API. Apart from the efficiency
of uploading multiple data points together, the streamer report ensures that data is not processed multiple times.
Each record has a sequential ID which ensures that the cloud will never process data that has already been processed,
allowing Streamer Reports to be uploaded multiple times without worrying about duplication.
The Streamer Report uses [msgpack](https://msgpack.org) as format, which is a compressed JSON file.
Next is a simple example for using these classes:
```python
from datetime import datetime
from io import BytesIO
from dateutil import parser
from archfx_cloud.api.connection import Api
from archfx_cloud.reports.report import ArchFXDataPoint
from archfx_cloud.reports.flexible_dictionary import ArchFXFlexibleDictionaryReport
# Create Data Points
reading = ArchFXDataPoint(
timestamp=parser.parse('2021-01-20T00:00:00.100000Z'),
stream='0001-5090',
value=2.0,
summary_data={'foo': 5, 'bar': 'foobar'},
raw_data=None,
reading_id=1000
)
events.append(reading)
reading = ArchFXDataPoint(
timestamp=parser.parse('2021-01-20T00:00:00.200000+00:00'),
stream='0001-5090',
value=3.0,
summary_data={'foo': 6, 'bar': 'foobar'},
reading_id=1001
)
events.append(reading)
# Create Report
sent_time = datetime.datetime.utcnow()
report = ArchFXFlexibleDictionaryReport.FromReadings(
device='d--1234',
data=events,
report_id=1003,
streamer=0xff,
sent_timestamp=sent_time
)
# Load Report to the Cloud
api = Api('https://arch.arhfx.io')
ok = api.login(email=args.email, password=password)
if ok:
fp = ("report.mp", BytesIO(report.encode()))
resp = api.streamer().report.upload_fp(fp=fp, timestamp=sent_time.isoformat())
```
## Requirements
archfx_cloud requires the following modules.
- Python 3.7+
- requests
- python-dateutil
## Development
To test, run `python setup.py test` or to run coverage analysis:
```bash
coverage run --source=archfx_cloud setup.py test
coverage report -m
```
## Deployment
To deploy to pypi:
1. Update `version.py` with new version number
1. Update `RELEASE.md` with description of new release
1. Run `python setup.py test` to ensure everything is ok
1. Commit all changes to master (PR is needed)
1. Once everythin commited, create a new version Tag. Deployment is triggered from that:
```bash
git tag -a v0.9.13 -m "v0.9.13"
git push origin v0.9.13
```
### Manual Release
All deployments should be done using the Ci/CD process (github actions)
but just for copleteness, this is how a manual deployments is done
```bash
# Test
python setup.py test
# Build
python setup.py sdist bdist_wheel
twine check dist/*
# Publish
twine upload dist/*
```
Raw data
{
"_id": null,
"home_page": "https://github.com/iotile/python_archfx_cloud",
"name": "archfx-cloud",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7,<4",
"maintainer_email": "",
"keywords": "iotile,archfx,arch,iiot,automation",
"author": "Arch Systems Inc.",
"author_email": "info@archsys.io",
"download_url": "https://files.pythonhosted.org/packages/53/5f/3db9f16eb17323c165d856df65ee7cdc8237089c835f02777baf7c0c5d34/archfx_cloud-0.16.0.tar.gz",
"platform": null,
"description": "# ArchFX Cloud Python API Package\n\n[![PyPI version](https://img.shields.io/pypi/v/archfx_cloud.svg)](https://pypi.python.org/pypi/archfx-cloud)\n\nA python library for interacting with [ArchFX Cloud](https://archfx.io) Rest API.\n\n## Installation\n\nWhen it comes to using Python packages, it is always recommened you use a Python Virtual Env. Using Python 3, you can simply do\n\n```bash\npython3 -m venv ~/.virtualenv/archfx-cloud\nsource ~/.virtualenv/archfx-cloud/bin/activate\n```\n\nor follow one of the many tutorials to setup Python virtual environments.\n\nOnce you have set up a virtual, env, simply install the package with:\n\n```bash\npip install archfx_cloud\n```\n\nPackage is based on https://github.com/samgiles/slumber and https://github.com/iotile/python_iotile_cloud\n\n## ArchFX Cloud Resource Overview\n\nIn a Rest API, Resources represent tables in the database. The following resources are available in **ArchFX Cloud**:\n\n- **account**: Represent users. A user only has access to its own user profile\n- **org**: Users belong to Organizations as members. Some of these users can act as admins for the organization.\n- **site**: Organizations contain Sites. A site usually represents a geographical location.\n- **area**: Sites can have Areas, which either represent a group of discrete manufacturing, or it is a group\nof assembly lines. Areas usually represent Buildings or a set of machines of the same type.\n- **line**: Sites or Areas can have Lines, which represent assembly lines. Lines are usually created within an area\nbut do not need to. But like areas, they do need to always belong to a Site.\n- **machine**: A Machine represents the physical machine we are extracting data from. It is a virtual concept but\nis usually one to one with a machine in the factory, and has a brand, model, serial number and maybe asset number.\n- **device**: A device represent IOTile HW Taps, and/or Factory SW connectors that extract data from a Machine. a Given\n**Machine** may have multiple HW taps or even multiple SW connectors, which is one there is a one to many\nrelationship between Machines and Devices\n- **stream**: Streams represent a globally unique instance of data comming from a given device (sensor).\nStreams represent the virtual data at the machine level (regardless of the device it came from). Over time,\na given stream may come from different devices (e.g. if a device breaks and needs to be replaced).\n- **streampages**: A stream is a virtual concept that is build out of **device** data. Data on a device is\nrepresented by a StreamPage. A stream page is always linked to a Stream, but may have a specific start/end\ntime period. A stream is therefore built out of stream pages, which may or may not come from the same device.\n- **data**: Every Stream represents the time series data. This resource can be used to access this data.\n\n### Globally Unique IDs\n\nMost of the key records in the database use a universally unique ID, in the form of an ID Slug. We borrow the term slug\nfrom blogging systems because we use it the same way to create unique but readable URLs.\n\nThe following are the resources that use a globally unique ID:\n\n- Sites use **ps--0000-0001**\n- Areas use **pa--0000-0001**\n- Lines use **pl--0000-0001**\n- Device **d--0000-0000-0000-0001** represent device 1. Note that this is also the Serial Number for the device itself,\nand can be found on each IOTile Device. For virtual devices (SW connectors), the ID is also assigned from the same\nplace. Note that all device IDs are allocated and managed by https://iotile.cloud but that should be transparent to users.\nAll devices have the same `d--0000-0000` scope.\n- Machines use the same standard as devices, except that the use a non-zero scope. e.g. **d--0000-0001-0000-0001**\n- Sites, Areas, Lines, Machines and Devices can all have streams of data, but all share the same globally unique naming\nstandard. All streams are named based on `<parent_type>--<parent_id>--<device/machine_id>--<variable_id>` where the variable\nis a 32bit identifier, usually following global IDs (e.g. `0000-5051` to represent `ProductReady`):\n - **ss--0000-0001--0000-0000-0000-0000--0000-5051** represents a site stream for site `ps--0000-0001`\n - **sa--0000-0001--0000-0000-0000-0000--0000-5051** represents an area stream for area `pa--0000-0001`\n - **sl--0000-0001--0000-0000-0000-0000--0000-5051** represents a line stream for line `pl--0000-0001`\n - **sl--0000-0001--0000-0000-0000-0001--0000-5051** represents a device stream for device `d--0000-0000-0000-0001` under line `pl--0000-0001`\n - **sl--0000-0001--0000-0001-0000-0001--0000-5051** represents a machine stream for machine `d--0000-0001-0000-0001` under line `pl--0000-0001`\n\nYou can see how:\n\n- Slug components are separated by a \u2018--\u2019 string\n- A one or two character letter(s) represents the type of slug: 'p?', 'd', and 's?'\n\n## User Guide\n\n### Login and Logout\n\nThe Api class is used to login and logout from the ArchFX Cloud\n\nExample:\n\n```python\nfrom archfx_cloud.api.connection import Api\n\nc = Api('https://arch.arhfx.io')\n\nok = c.login(email=args.email, password=password)\nif ok:\n # Do something\n\n c.logout()\n```\n\nIf you have a JWT token, you can skip the login and just set the token:\n\n```python\nfrom archfx_cloud.api.connection import Api\n\nc = Api('https://arch.arhfx.io')\n\nc.set_token('big-ugly-token')\n```\n\nYou can use the Api itself to login and get a token:\n\n```python\nfrom archfx_cloud.api.connection import Api\n\nc = Api('https://arch.arhfx.io')\n\nok = c.login(email=args.email, password=password)\nif ok:\n token = c.token\n # write out token or store in some secret .ini file\n```\n\n### Generic Rest API\n\nThe `Api(domain)` can be used to access any of the APIs in https://arch.archfx.io/api/v1/\n\nThe `Api(domain)` is generic and therefore will support any future resources supported by the ArchFX Cloud Rest API.\n\n```python\nfrom archfx_cloud.api.connection import Api\n\napi = Api('https://arch.arhfx.io')\nok = api.login(email='user@example.com', password='my.pass')\n\n## GET https://arch.archfx.io/api/v1/org/\n## Note: Any kwargs passed to get(), post(), put(), delete() will be used as url parameters\napi.org.get()\n\n## POST https://arch.archfx.io/api/v1/org/\nnew = api.org.post({\"name\": \"My new Org\"})\n\n## PUT https://arch.archfx.io/api/v1/org/{slug}/\napi.org(new[\"slug\"]).put({\"about\": \"About Org\"})\n\nPATCH https://arch.archfx.io/api/v1/org/{slug}/\napi.org(new[\"slug\"]).patch({\"about\": \"About new Org\"})\n\n## GET https://arch.archfx.io/api/v1/org/{slug}/\napi.org(new[\"slug\"]).get()\n\n## DELETE https://arch.archfx.io/api/v1/org/{slug}/\n## NOTE: Not all resources can be deleted by users\napi.org(new[\"slug\"]).delete()\n```\n\nYou can pass arguments to any get() using\n\n```python\n# /api/v1/org/\nfor org in api.org.get()['results']:\n # Pass any arguments as get(foo=1, bar='2'). e.g.\n # /api/v1/site/?org__slug=<slug>\n org_sites = c.site.get(org='{0}'.format(org['slug']))\n\n```\n\nYou can also call nested resources/actions like this:\n\n```python\n# /api/v1/machine/<slug>/devices/\nfor org in api.machine.get()['results']:\n # /api/v1/machine/<slug>/devices\n associated_devices = c.machine(org['slug']).devices.get()\n\n```\n\n### Globaly unique ID slugs\n\nTo easily handle ID slugs, use the `utils.gid` package:\n\n```python\nparent = ArchFxParentSlug(5, ptype='pl)\nassert(str(parent) == 'pl--0000-0005')\n\ndevice = ArchFxDeviceSlug(10)\nassert(str(device) == 'd--0000-0000-0000-000a')\n\n\nid = ArchFxStreamSlug()\nid.from_parts(parent=parent, device=device, variable='0000-5501)\nassert(str(id) == 'sl--0000-0005--0000-0000-0000-000a--0000-5001')\n\nparts = id.get_parts()\nself.assertEqual(str(parts['parent']), str(parent))\nself.assertEqual(str(parts['device']), str(device))\nself.assertEqual(str(parts['variable']), '0000-5501')\n\n# Other forms of use\ndevice = ArchFxDeviceSlug('000a)\nassert(str(device) == 'd--0000-0000-0000-000a')\ndevice = ArchFxDeviceSlug('d--000a')\nassert(str(device) == 'd--0000-0000-0000-000a')\ndevice = ArchFxDeviceSlug(0xa)\nassert(str(device) == 'd--0000-0000-0000-000a')\n```\n\n### BaseMain Utility Class\n\nAs you can see from the examples above, every script is likely to follow the following format:\n\n```python\n# Parse arguments from user and get password\n# Login to server\n# Do some real work\n# Logout\n```\n\nTo make it easy to add this boilerplate code, the BaseMain can be used to follow a predefined, opinionated flow\nwhich basically configures the `logging` and `argsparse` python packages with a basic configuration during the \nconstruction. Then the `main()` method runs the following flow, where each function call can be overwritten in your\nown derived class\n\n```python\n self.domain = self.get_domain()\n self.api = Api(self.domain)\n self.before_login()\n ok = self.login()\n if ok:\n self.after_login()\n self.logout()\n self.after_logout()\n```\n\nAn example of how to use this class is shown below:\n\n```python\nclass MyScript(BaseMain):\n\n def add_extra_args(self):\n # Add extra positional argument (as example)\n self.parser.add_argument('foo', metavar='foo', type=str, help='RTFM')\n\n def before_login(self):\n logger.info('-----------')\n\n def after_login(self):\n # Main function to OVERWITE and do real work\n do_some_real_work(self.api, self.args)\n\n def login(self):\n # Add extra message welcoming user\n ok = super(MyScript, self).login()\n if ok:\n logger.info('Welcome {0}'.format(self.args.email))\n return ok\n\n def logout(self):\n # Add extra message to say Goodbye\n super(MyScript, self).logout()\n logger.info('Goodbye!')\n\n\nif __name__ == '__main__':\n\n work = MyScript()\n work.main()\n```\n\n### Uploading a Streamer Report\n\nThe `ArchFXDataPoint` and `ArchFXFlexibleDictionaryReport` helper classes can be used to generate a Streamer Report\ncompatible with ArchFX Cloud. A Streamer Report can be used to send several stream data records together.\nUsing Streamer Reports have several benefits over uploading data manually using the Rest API. Apart from the efficiency\nof uploading multiple data points together, the streamer report ensures that data is not processed multiple times.\nEach record has a sequential ID which ensures that the cloud will never process data that has already been processed,\nallowing Streamer Reports to be uploaded multiple times without worrying about duplication.\n\nThe Streamer Report uses [msgpack](https://msgpack.org) as format, which is a compressed JSON file.\n\nNext is a simple example for using these classes:\n\n```python\nfrom datetime import datetime\nfrom io import BytesIO\nfrom dateutil import parser\nfrom archfx_cloud.api.connection import Api\nfrom archfx_cloud.reports.report import ArchFXDataPoint\nfrom archfx_cloud.reports.flexible_dictionary import ArchFXFlexibleDictionaryReport\n\n# Create Data Points\nreading = ArchFXDataPoint(\n timestamp=parser.parse('2021-01-20T00:00:00.100000Z'),\n stream='0001-5090',\n value=2.0,\n summary_data={'foo': 5, 'bar': 'foobar'},\n raw_data=None,\n reading_id=1000\n)\nevents.append(reading)\nreading = ArchFXDataPoint(\n timestamp=parser.parse('2021-01-20T00:00:00.200000+00:00'),\n stream='0001-5090',\n value=3.0,\n summary_data={'foo': 6, 'bar': 'foobar'},\n reading_id=1001\n)\nevents.append(reading)\n\n# Create Report\nsent_time = datetime.datetime.utcnow()\nreport = ArchFXFlexibleDictionaryReport.FromReadings(\n device='d--1234',\n data=events,\n report_id=1003,\n streamer=0xff,\n sent_timestamp=sent_time\n)\n\n# Load Report to the Cloud\napi = Api('https://arch.arhfx.io')\nok = api.login(email=args.email, password=password)\nif ok:\n fp = (\"report.mp\", BytesIO(report.encode()))\n resp = api.streamer().report.upload_fp(fp=fp, timestamp=sent_time.isoformat())\n```\n\n## Requirements\n\narchfx_cloud requires the following modules.\n\n- Python 3.7+\n- requests\n- python-dateutil\n\n## Development\n\nTo test, run `python setup.py test` or to run coverage analysis:\n\n```bash\ncoverage run --source=archfx_cloud setup.py test\ncoverage report -m\n```\n\n## Deployment\n\nTo deploy to pypi:\n\n1. Update `version.py` with new version number\n1. Update `RELEASE.md` with description of new release\n1. Run `python setup.py test` to ensure everything is ok\n1. Commit all changes to master (PR is needed)\n1. Once everythin commited, create a new version Tag. Deployment is triggered from that:\n\n```bash\ngit tag -a v0.9.13 -m \"v0.9.13\"\ngit push origin v0.9.13\n```\n\n### Manual Release\n\nAll deployments should be done using the Ci/CD process (github actions)\nbut just for copleteness, this is how a manual deployments is done\n\n```bash\n# Test\npython setup.py test\n# Build\npython setup.py sdist bdist_wheel\ntwine check dist/*\n# Publish\ntwine upload dist/*\n```\n\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Python client for https://archfx.io",
"version": "0.16.0",
"project_urls": {
"Homepage": "https://github.com/iotile/python_archfx_cloud"
},
"split_keywords": [
"iotile",
"archfx",
"arch",
"iiot",
"automation"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "a8f034d6d19224760acd181a7981af0607a3d1143f679b0f1e1f9e8b776c2e9e",
"md5": "c616610d4d66a703edd066861fd63c07",
"sha256": "819e4e62aa011cedf44735605d652cf6ecb631dc289385896d1d0562ee241244"
},
"downloads": -1,
"filename": "archfx_cloud-0.16.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c616610d4d66a703edd066861fd63c07",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7,<4",
"size": 23964,
"upload_time": "2024-02-16T10:52:47",
"upload_time_iso_8601": "2024-02-16T10:52:47.893503Z",
"url": "https://files.pythonhosted.org/packages/a8/f0/34d6d19224760acd181a7981af0607a3d1143f679b0f1e1f9e8b776c2e9e/archfx_cloud-0.16.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "535f3db9f16eb17323c165d856df65ee7cdc8237089c835f02777baf7c0c5d34",
"md5": "188ba888e1a913177409b3950229e585",
"sha256": "fe60da5d1c7d2bf5445a8b842d01e89a043b7a787fc877162b7467c58740ce8f"
},
"downloads": -1,
"filename": "archfx_cloud-0.16.0.tar.gz",
"has_sig": false,
"md5_digest": "188ba888e1a913177409b3950229e585",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7,<4",
"size": 26858,
"upload_time": "2024-02-16T10:52:50",
"upload_time_iso_8601": "2024-02-16T10:52:50.411552Z",
"url": "https://files.pythonhosted.org/packages/53/5f/3db9f16eb17323c165d856df65ee7cdc8237089c835f02777baf7c0c5d34/archfx_cloud-0.16.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-02-16 10:52:50",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "iotile",
"github_project": "python_archfx_cloud",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [],
"lcname": "archfx-cloud"
}