Skip to content

Commit e8a9031

Browse files
author
Jim Bennett
committed
Removing other dependencies and moving to minimal code
1 parent 40ab461 commit e8a9031

19 files changed

+274
-68
lines changed

README.rst

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,11 @@ This driver depends on:
4545

4646
* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
4747
* `Adafruit CircuitPython MiniMQTT <https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT>`_
48-
49-
* `CircuitPython Base64 <https://github.com/jimbobbennett/CircuitPython_Base64>`_
50-
* `CircuitPython Parse <https://github.com/jimbobbennett/CircuitPython_Parse>`_
48+
* `Adafruit CircuitPython Requests <https://github.com/adafruit/Adafruit_CircuitPython_Requests>`_
5149

5250
Please ensure all dependencies are available on the CircuitPython filesystem.
5351
This is easily achieved by downloading
54-
`the Adafruit library and driver bundle <https://github.com/adafruit/Adafruit_CircuitPython_Bundle>`_
55-
and
56-
`the CircuitPython community library and driver bundle <https://github.com/adafruit/CircuitPython_Community_Bundle>`_
52+
`the Adafruit library and driver bundle <https://github.com/adafruit/Adafruit_CircuitPython_Bundle>`_.
5753

5854
Usage Example
5955
=============

adafruit_azureiot/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@
3737
3838
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
3939
* Adafruit's ESP32SPI library: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI
40-
* Community HMAC library: https://github.com/jimbobbennett/CircuitPython_HMAC
41-
* Community base64 library: https://github.com/jimbobbennett/CircuitPython_Base64
42-
* Community Parse library: https://github.com/jimbobbennett/CircuitPython_Parse
4340
"""
4441

4542
from .iot_error import IoTError

adafruit_azureiot/base64.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2020 Jim Bennett
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
13+
# all 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
21+
# THE SOFTWARE.
22+
"""
23+
`base64`
24+
================================================================================
25+
26+
RFC 3548: Base64 Data Encodings
27+
28+
29+
* Author(s): Jim Bennett
30+
31+
Implementation Notes
32+
--------------------
33+
34+
**Software and Dependencies:**
35+
36+
* Adafruit CircuitPython firmware for the supported boards:
37+
https://github.com/adafruit/circuitpython/releases
38+
39+
"""
40+
41+
import adafruit_binascii as binascii
42+
43+
__all__ = ["b64encode", "b64decode"]
44+
45+
46+
def _bytes_from_decode_data(data: str):
47+
try:
48+
return data.encode("ascii")
49+
except:
50+
raise ValueError("string argument should contain only ASCII characters")
51+
52+
53+
def b64encode(toencode: bytes) -> bytes:
54+
"""Encode a byte string using Base64.
55+
56+
toencode is the byte string to encode. Optional altchars must be a byte
57+
string of length 2 which specifies an alternative alphabet for the
58+
'+' and '/' characters. This allows an application to
59+
e.g. generate url or filesystem safe Base64 strings.
60+
61+
The encoded byte string is returned.
62+
"""
63+
# Strip off the trailing newline
64+
return binascii.b2a_base64(toencode)[:-1]
65+
66+
67+
def b64decode(todecode: str) -> bytes:
68+
"""Decode a Base64 encoded byte string.
69+
70+
todecode is the byte string to decode. Optional altchars must be a
71+
string of length 2 which specifies the alternative alphabet used
72+
instead of the '+' and '/' characters.
73+
74+
The decoded string is returned. A binascii.Error is raised if todecode is
75+
incorrectly padded.
76+
77+
If validate is False (the default), non-base64-alphabet characters are
78+
discarded prior to the padding check. If validate is True,
79+
non-base64-alphabet characters in the input result in a binascii.Error.
80+
"""
81+
return binascii.a2b_base64(_bytes_from_decode_data(todecode))

adafruit_azureiot/device_registration.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@
3232
import gc
3333
import json
3434
import time
35-
import circuitpython_parse as parse
3635
import adafruit_requests as requests
3736
import adafruit_logging as logging
3837
from adafruit_logging import Logger
3938
from . import constants
39+
from .quote import quote
4040
from .keys import compute_derived_symmetric_key
4141

4242
# Azure HTTP error status codes
@@ -96,9 +96,8 @@ def _loop_assign(self, operation_id, headers) -> str:
9696
constants.DPS_API_VERSION,
9797
)
9898
self._logger.info("- iotc :: _loop_assign :: " + uri)
99-
target = parse.urlparse(uri)
10099

101-
response = self._run_get_request_with_retry(target.geturl(), headers)
100+
response = self._run_get_request_with_retry(uri, headers)
102101

103102
try:
104103
data = response.json()
@@ -193,7 +192,7 @@ def register_device(self, expiry: int) -> str:
193192
# pylint: disable=C0103
194193
sr = self._id_scope + "%2Fregistrations%2F" + self._device_id
195194
sig_no_encode = compute_derived_symmetric_key(self._key, sr + "\n" + str(expiry))
196-
sig_encoded = parse.quote(sig_no_encode, "~()*!.'")
195+
sig_encoded = quote(sig_no_encode, "~()*!.'")
197196
auth_string = "SharedAccessSignature sr=" + sr + "&sig=" + sig_encoded + "&se=" + str(expiry) + "&skn=registration"
198197

199198
headers = {
@@ -213,13 +212,12 @@ def register_device(self, expiry: int) -> str:
213212
self._device_id,
214213
constants.DPS_API_VERSION,
215214
)
216-
target = parse.urlparse(uri)
217215

218216
self._logger.info("Connecting...")
219-
self._logger.info("URL: " + target.geturl())
217+
self._logger.info("URL: " + uri)
220218
self._logger.info("body: " + json.dumps(body))
221219

222-
response = self._run_put_request_with_retry(target.geturl(), body, headers)
220+
response = self._run_put_request_with_retry(uri, body, headers)
223221

224222
data = None
225223
try:

adafruit_azureiot/hmac.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
# THE SOFTWARE.
2222
"""
23-
`circuitpython_hmac`
23+
`HMAC`
2424
================================================================================
2525
2626
HMAC (Keyed-Hashing for Message Authentication) Python module.

adafruit_azureiot/iot_mqtt.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
import time
3434
import adafruit_minimqtt as minimqtt
3535
from adafruit_minimqtt import MQTT
36-
import circuitpython_parse as parse
3736
import adafruit_logging as logging
3837
from .iot_error import IoTError
3938
from .keys import compute_derived_symmetric_key
39+
from .quote import quote
4040
from . import constants
4141

4242
# pylint: disable=R0903
@@ -108,7 +108,7 @@ def _gen_sas_token(self) -> str:
108108
token_expiry = int(time.time() + self._token_expires)
109109
uri = self._hostname + "%2Fdevices%2F" + self._device_id
110110
signed_hmac_sha256 = compute_derived_symmetric_key(self._key, uri + "\n" + str(token_expiry))
111-
signature = parse.quote(signed_hmac_sha256, "~()*!.'")
111+
signature = quote(signed_hmac_sha256, "~()*!.'")
112112
if signature.endswith("\n"): # somewhere along the crypto chain a newline is inserted
113113
signature = signature[:-1]
114114
token = "SharedAccessSignature sr={}&sig={}&se={}".format(uri, signature, token_expiry)
@@ -447,6 +447,7 @@ def loop(self) -> None:
447447
return
448448

449449
self._mqtts.loop()
450+
gc.collect()
450451

451452
def send_device_to_cloud_message(self, message, system_properties: dict = None) -> None:
452453
"""Send a device to cloud message from this device to Azure IoT Hub

adafruit_azureiot/keys.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2020 Jim Bennett
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
13+
# all 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
21+
# THE SOFTWARE.
122
"""Computes a derived symmetric key from a secret and a message
223
:param str secret: The secret to use for the key
324
:param str msg: The message to use for the key
425
:returns: The derived symmetric key
526
:rtype: bytes
627
"""
728

8-
import circuitpython_base64 as base64
29+
from .base64 import b64decode, b64encode
930
from .hmac import new_hmac
1031

1132

@@ -16,5 +37,4 @@ def compute_derived_symmetric_key(secret: str, msg: str) -> bytes:
1637
:returns: The derived symmetric key
1738
:rtype: bytes
1839
"""
19-
secret = base64.b64decode(secret)
20-
return base64.b64encode(new_hmac(secret, msg=msg.encode("utf8")).digest())
40+
return b64encode(new_hmac(b64decode(secret), msg=msg.encode("utf8")).digest())

adafruit_azureiot/mpy.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
~/Downloads/mpy-cross/mpy-cross __init__.py
2+
~/Downloads/mpy-cross/mpy-cross constants.py
3+
~/Downloads/mpy-cross/mpy-cross device_registration.py
4+
~/Downloads/mpy-cross/mpy-cross hmac.py
5+
~/Downloads/mpy-cross/mpy-cross iot_error.py
6+
~/Downloads/mpy-cross/mpy-cross iot_mqtt.py
7+
~/Downloads/mpy-cross/mpy-cross iotcentral_device.py
8+
~/Downloads/mpy-cross/mpy-cross iothub_device.py
9+
~/Downloads/mpy-cross/mpy-cross keys.py
10+
~/Downloads/mpy-cross/mpy-cross quote.py
11+
~/Downloads/mpy-cross/mpy-cross base64.py

adafruit_azureiot/quote.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2020 Jim Bennett
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
13+
# all 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
21+
# THE SOFTWARE.
22+
"""
23+
`quote`
24+
================================================================================
25+
26+
The quote function %-escapes all characters that are neither in the
27+
unreserved chars ("always safe") nor the additional chars set via the
28+
safe arg.
29+
30+
"""
31+
_ALWAYS_SAFE = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" b"abcdefghijklmnopqrstuvwxyz" b"0123456789" b"_.-~")
32+
_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE)
33+
SAFE_QUOTERS = {}
34+
35+
36+
def quote(bytes_val: bytes, safe="/"):
37+
"""The quote function %-escapes all characters that are neither in the
38+
unreserved chars ("always safe") nor the additional chars set via the
39+
safe arg.
40+
"""
41+
if not isinstance(bytes_val, (bytes, bytearray)):
42+
raise TypeError("quote_from_bytes() expected bytes")
43+
if not bytes_val:
44+
return ""
45+
if isinstance(safe, str):
46+
# Normalize 'safe' by converting to bytes and removing non-ASCII chars
47+
safe = safe.encode("ascii", "ignore")
48+
else:
49+
safe = bytes([char for char in safe if char < 128])
50+
if not bytes_val.rstrip(_ALWAYS_SAFE_BYTES + safe):
51+
return bytes_val.decode()
52+
try:
53+
quoter = SAFE_QUOTERS[safe]
54+
except KeyError:
55+
SAFE_QUOTERS[safe] = quoter = Quoter(safe).__getitem__
56+
return "".join([quoter(char) for char in bytes_val])
57+
58+
59+
# pylint: disable=C0103
60+
class defaultdict:
61+
"""
62+
Default Dict Implementation.
63+
64+
Defaultdcit that returns the key if the key is not found in dictionnary (see
65+
unswap in karma-lib):
66+
>>> d = defaultdict(default=lambda key: key)
67+
>>> d['foo'] = 'bar'
68+
>>> d['foo']
69+
'bar'
70+
>>> d['baz']
71+
'baz'
72+
DefaultDict that returns an empty string if the key is not found (see
73+
prefix in karma-lib for typical usage):
74+
>>> d = defaultdict(default=lambda key: '')
75+
>>> d['foo'] = 'bar'
76+
>>> d['foo']
77+
'bar'
78+
>>> d['baz']
79+
''
80+
Representation of a default dict:
81+
>>> defaultdict([('foo', 'bar')])
82+
defaultdict(None, {'foo': 'bar'})
83+
"""
84+
85+
@staticmethod
86+
# pylint: disable=W0613
87+
def __new__(cls, default_factory=None, **kwargs):
88+
self = super(defaultdict, cls).__new__(cls)
89+
# pylint: disable=C0103
90+
self.d = {}
91+
return self
92+
93+
def __init__(self, default_factory=None, **kwargs):
94+
self.d = kwargs
95+
self.default_factory = default_factory
96+
97+
def __getitem__(self, key):
98+
try:
99+
return self.d[key]
100+
except KeyError:
101+
val = self.__missing__(key)
102+
self.d[key] = val
103+
return val
104+
105+
def __setitem__(self, key, val):
106+
self.d[key] = val
107+
108+
def __delitem__(self, key):
109+
del self.d[key]
110+
111+
def __contains__(self, key):
112+
return key in self.d
113+
114+
def __missing__(self, key):
115+
if self.default_factory is None:
116+
raise KeyError(key)
117+
return self.default_factory()
118+
119+
120+
class Quoter(defaultdict):
121+
"""A mapping from bytes (in range(0,256)) to strings.
122+
123+
String values are percent-encoded byte values, unless the key < 128, and
124+
in the "safe" set (either the specified safe set, or default set).
125+
"""
126+
127+
# Keeps a cache internally, using defaultdict, for efficiency (lookups
128+
# of cached keys don't call Python code at all).
129+
def __init__(self, safe):
130+
"""safe: bytes object."""
131+
super(Quoter, self).__init__()
132+
self.safe = _ALWAYS_SAFE.union(safe)
133+
134+
def __missing__(self, b):
135+
# Handle a cache miss. Store quoted string in cache and return.
136+
res = chr(b) if b in self.safe else "%{:02X}".format(b)
137+
self[b] = res
138+
return res

0 commit comments

Comments
 (0)