Skip to content

new requests interface #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ install:
- pip install circuitpython-build-tools Sphinx sphinx-rtd-theme
- pip install --force-reinstall pylint==1.9.2
script:
- pylint adafruit_espatcontrol.py
- pylint adafruit_espatcontrol/*.py
- ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py)
- circuitpython-build-bundles --filename_prefix adafruit-circuitpython-espatcontrol --library_location .
- cd docs && sphinx-build -E -W -b html . _build/html && cd ..
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_espatcontrol`
`adafruit_espatcontrol.adafruit_espatcontrol`
====================================================
Use the ESP AT command sent to communicate with the Interwebs.
Expand Down Expand Up @@ -60,20 +60,6 @@ class OKError(Exception):
"""The exception thrown when we didn't get acknowledgement to an AT command"""
pass

class ESP_ATcontrol_socket:
"""A 'socket' compatible interface thru the ESP AT command set"""
def __init__(self, esp):
self._esp = esp

def getaddrinfo(self, host, port, # pylint: disable=too-many-arguments
family=0, socktype=0, proto=0, flags=0): # pylint: disable=unused-argument
"""Given a hostname and a port name, return a 'socket.getaddrinfo'
compatible list of tuples. Honestly, we ignore anything but host & port"""
if not isinstance(port, int):
raise RuntimeError("port must be an integer")
ipaddr = self._esp.nslookup(host)
return [(family, socktype, proto, '', (ipaddr, port))]

class ESP_ATcontrol:
"""A wrapper for AT commands to a connected ESP8266 or ESP32 module to do
some very basic internetting. The ESP module must be pre-programmed with
Expand Down Expand Up @@ -147,55 +133,18 @@ def begin(self):
except OKError:
pass #retry

def request_url(self, url, ssl=False, request_type="GET"):
"""Send an HTTP request to the URL. If the URL starts with https://
we will force SSL and use port 443. Otherwise, you can select whether
you want ssl by passing in a flag."""
if url.startswith("https://"):
ssl = True
url = url[8:]
if url.startswith("http://"):
url = url[7:]
domain, path = url.split('/', 1)
path = '/'+path
port = 80
conntype = self.TYPE_TCP
if ssl:
conntype = self.TYPE_SSL
port = 443
if not self.socket_connect(conntype, domain, port, keepalive=10, retries=3):
raise RuntimeError("Failed to connect to host")
request = request_type+" "+path+" HTTP/1.1\r\n"
request += "Host: "+domain+"\r\n"
request += "User-Agent: "+self.USER_AGENT+"\r\n"
request += "\r\n"
try:
self.socket_send(bytes(request, 'utf-8'))
except RuntimeError:
raise

reply = self.socket_receive(timeout=3).split(b'\r\n')
if self._debug:
print(reply)
try:
headerbreak = reply.index(b'')
except ValueError:
raise RuntimeError("Reponse wasn't valid HTML")
header = reply[0:headerbreak]
data = b'\r\n'.join(reply[headerbreak+1:]) # put back the way it was
self.socket_disconnect()
return (header, data)

def connect(self, settings):
"""Repeatedly try to connect to an access point with the details in
the passed in 'settings' dictionary. Be sure 'ssid' and 'password' are
defined in the settings dict! If 'timezone' is set, we'll also configure
SNTP"""
# Connect to WiFi if not already
retries = 3
while True:
try:
if not self._initialized:
if not self._initialized or retries == 0:
self.begin()
retries = 3
AP = self.remote_AP # pylint: disable=invalid-name
print("Connected to", AP[0])
if AP[0] != settings['ssid']:
Expand All @@ -210,6 +159,7 @@ def connect(self, settings):
return # yay!
except (RuntimeError, OKError) as exp:
print("Failed to connect, retrying\n", exp)
retries -= 1
continue

# *************************** SOCKET SETUP ****************************
Expand All @@ -223,10 +173,6 @@ def cipmux(self):
return int(reply[8:])
raise RuntimeError("Bad response to CIPMUX?")

def socket(self):
"""Create a 'socket' object"""
return ESP_ATcontrol_socket(self)

def socket_connect(self, conntype, remote, remote_port, *, keepalive=10, retries=1):
"""Open a socket. conntype can be TYPE_TCP, TYPE_UDP, or TYPE_SSL. Remote
can be an IP address or DNS (we'll do the lookup for you. Remote port
Expand Down Expand Up @@ -283,10 +229,11 @@ def socket_send(self, buffer, timeout=1):
return True

def socket_receive(self, timeout=5):
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-nested-blocks, too-many-branches
"""Check for incoming data over the open socket, returns bytes"""
incoming_bytes = None
bundle = b''
bundle = []
toread = 0
gc.collect()
i = 0 # index into our internal packet
stamp = time.monotonic()
Expand All @@ -312,6 +259,9 @@ def socket_receive(self, timeout=5):
except ValueError:
raise RuntimeError("Parsing error during receive", ipd)
i = 0 # reset the input buffer now that we know the size
elif i > 20:
i = 0 # Hmm we somehow didnt get a proper +IPD packet? start over

else:
self.hw_flow(False) # stop the flow
# read as much as we can!
Expand All @@ -322,11 +272,22 @@ def socket_receive(self, timeout=5):
if i == incoming_bytes:
#print(self._ipdpacket[0:i])
gc.collect()
bundle += self._ipdpacket[0:i]
bundle.append(self._ipdpacket[0:i])
gc.collect()
i = incoming_bytes = 0
else: # no data waiting
self.hw_flow(True) # start the floooow
return bundle
totalsize = sum([len(x) for x in bundle])
ret = bytearray(totalsize)
i = 0
for x in bundle:
for char in x:
ret[i] = char
i += 1
for x in bundle:
del x
gc.collect()
return ret

def socket_disconnect(self):
"""Close any open socket, if there is one"""
Expand Down Expand Up @@ -371,6 +332,7 @@ def is_connected(self):
self.begin()
try:
self.echo(False)
self.baudrate = self.baudrate
stat = self.status
if stat in (self.STATUS_APCONNECTED,
self.STATUS_SOCKETOPEN,
Expand Down
184 changes: 184 additions & 0 deletions adafruit_espatcontrol/adafruit_espatcontrol_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""
adapted from https://github.com/micropython/micropython-lib/tree/master/urequests
micropython-lib consists of multiple modules from different sources and
authors. Each module comes under its own licensing terms. Short name of
a license can be found in a file within a module directory (usually
metadata.txt or setup.py). Complete text of each license used is provided
at https://github.com/micropython/micropython-lib/blob/master/LICENSE
author='Paul Sokolovsky'
license='MIT'
"""

# pylint: disable=no-name-in-module

import gc
import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket

_the_interface = None # pylint: disable=invalid-name
def set_interface(iface):
"""Helper to set the global internet interface"""
global _the_interface # pylint: disable=invalid-name, global-statement
_the_interface = iface
socket.set_interface(iface)

class Response:
"""The response from a request, contains all the headers/content"""
headers = {}
encoding = None

def __init__(self, f):
self.raw = f
self.encoding = "utf-8"
self._cached = None
self.status_code = None
self.reason = None

def close(self):
"""Close, delete and collect the response data"""
if self.raw:
self.raw.close()
del self.raw
del self._cached
gc.collect()

@property
def content(self):
"""The HTTP content direct from the socket, as bytes"""
if self._cached is None:
try:
self._cached = self.raw.read()
finally:
self.raw.close()
self.raw = None
return self._cached

@property
def text(self):
"""The HTTP content, encoded into a string according to the HTTP
header encoding"""
return str(self.content, self.encoding)

def json(self):
"""The HTTP content, parsed into a json dictionary"""
import ujson
return ujson.loads(self.content)


# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
def request(method, url, data=None, json=None, headers=None, stream=None):
"""Perform an HTTP request to the given url which we will parse to determine
whether to use SSL ('https://') or not. We can also send some provided 'data'
or a json dictionary which we will stringify. 'headers' is optional HTTP headers
sent along. 'stream' is unused in this implementation"""
global _the_interface # pylint: disable=global-statement, invalid-name

if not headers:
headers = {}

try:
proto, dummy, host, path = url.split("/", 3)
except ValueError:
proto, dummy, host = url.split("/", 2)
path = ""
if proto == "http:":
port = 80
elif proto == "https:":
port = 443
else:
raise ValueError("Unsupported protocol: " + proto)

if ":" in host:
host, port = host.split(":", 1)
port = int(port)

addr_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0]
sock = socket.socket(addr_info[0], addr_info[1], addr_info[2])
resp = Response(sock) # our response

try:
conntype = _the_interface.TYPE_TCP
if proto == "https:":
conntype = _the_interface.TYPE_SSL
sock.connect(addr_info[-1], conntype)
sock.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
if "Host" not in headers:
sock.write(b"Host: %s\r\n" % host)
if "User-Agent" not in headers:
sock.write(b"User-Agent: Adafruit CircuitPython\r\n")
# Iterate over keys to avoid tuple alloc
for k in headers:
sock.write(k)
sock.write(b": ")
sock.write(headers[k])
sock.write(b"\r\n")
if json is not None:
assert data is None
import ujson
data = ujson.dumps(json)
sock.write(b"Content-Type: application/json\r\n")
if data:
sock.write(b"Content-Length: %d\r\n" % len(data))
sock.write(b"\r\n")
if data:
sock.write(data)

line = sock.readline()
#print(line)
line = line.split(None, 2)
status = int(line[1])
reason = ""
if len(line) > 2:
reason = line[2].rstrip()
while True:
line = sock.readline()
if not line or line == b"\r\n":
break

#print(line)
title, content = line.split(b': ', 1)
if title and content:
title = str(title.lower(), 'utf-8')
content = str(content, 'utf-8')
resp.headers[title] = content

if line.startswith(b"Transfer-Encoding:"):
if b"chunked" in line:
raise ValueError("Unsupported " + line)
elif line.startswith(b"Location:") and not 200 <= status <= 299:
raise NotImplementedError("Redirects not yet supported")

except OSError:
sock.close()
raise

resp.status_code = status
resp.reason = reason
return resp
# pylint: enable=too-many-branches, too-many-statements, unused-argument
# pylint: enable=too-many-arguments, too-many-locals

def head(url, **kw):
"""Send HTTP HEAD request"""
return request("HEAD", url, **kw)

def get(url, **kw):
"""Send HTTP GET request"""
return request("GET", url, **kw)

def post(url, **kw):
"""Send HTTP POST request"""
return request("POST", url, **kw)

def put(url, **kw):
"""Send HTTP PUT request"""
return request("PUT", url, **kw)

def patch(url, **kw):
"""Send HTTP PATCH request"""
return request("PATCH", url, **kw)

def delete(url, **kw):
"""Send HTTP DELETE request"""
return request("DELETE", url, **kw)
Loading