Skip to content

Commit

Permalink
Various adjustments to improve performance and maintainability
Browse files Browse the repository at this point in the history
- Changed validation of config.ini entries
- Added logging module
- Updated documentation (README.md)
- Updated dependencies and migrated to pipenv
- Implemented production webserver (cherrypy)
- Code formatting
  • Loading branch information
fabieu committed Jul 15, 2021
1 parent e06fa62 commit adb2891
Show file tree
Hide file tree
Showing 17 changed files with 598 additions and 96 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Custom
config.ini
*.token
.vscode/

# Byte-compiled / optimized / DLL files
Expand Down
20 changes: 20 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
flask = "*"
requests = "*"
cachetools = "*"
jinja2 = "*"
pyyaml = "*"
python-dateutil = "*"
flask-cors = "*"
cherrypy = "*"

[dev-packages]
autopep8 = "*"

[requires]
python_version = "3"
342 changes: 342 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

54 changes: 36 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,60 @@
# SureFlap API
This project provides a standalone RESTful API for [SureFlap Products](https://www.surepetcare.com/en-gb).The main functionality of this API is to provide a wrapper for the official SureFlap API for maintainability, simplicity and connectivity. This enables you to call the API from a variance of IoT devices and other applications more easily. The API is completely written in Python3. And the best is, you can get started within a few minutes. Just check out the documentation below.

## Getting started
![](./docs/demo_1.jpg)

### Requirements
- Python 3.X
- pip (already installed with Python3)
This project provides a standalone RESTful API for [SureFlap Products](https://www.surepetcare.com).The main functionality of this API is to provide a wrapper for the official SureFlap API for maintainability, simplicity and connectivity. This enables you to call the API from a variance of IoT devices and other applications more easily. The API is completely written in Python3. And the best is, you can get started within a few minutes. Just check out the documentation below.

### Installation
 

Clone this repository to any directory on your machine
## Requirements
- Python >= 3.5
- pipenv

 

## Installation

Clone this repository to any directory on your system:

```bash
git clone https://github.com/fabieu/sureflap-api.git
```

Move into the cloned repository via:
Move into the cloned repository:

```bash
cd ./sureflap-api
```

If you dont want to install the packages in your main package repository you can use a virtual environment. For more instructions visit the [official Python documentation](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/).
This project utilizes **Pipenv**, a production-ready tool that aims to bring the best of all packaging worlds to the Python world. It harnesses Pipfile, pip, and virtualenv into one single command. You can read more about Pipenv [here](https://pipenv-fork.readthedocs.io/en/latest/).

Installing required dependencies:
Installing required dependencies in a virtual environment with Pipenv:

```python
pip install -r requirements.txt
```bash
pipenv install
```

### Configuration
 

## Configuration

Before you can start exploring the API you have to rename the `config.ini.sample` in the root of the cloned repository to `config.ini` and edit the email and password settings. Here you need to insert the credentials for your SureFlap Petcare Account.

 

## Usage

Start the Flask API Server with the following command:

```bash
pipenv run python server.py
```


For the usage of the REST API take a look at the provided OpenAPI Specification on the main page of the webserver (http://localhost:3001). There you can find everything you need to know about the given methods and how to call them. Be aware that CORS is enabled. This is necessary to so that the SwaggerUI is working properly

 

### PM2 Setup (Optional)

Install the daemon process manager PM2 that will help you manage and keep your application online:
Expand All @@ -44,20 +66,16 @@ npm install pm2 -g
Add the application to PM2:

```bash
pm2 start server.py --watch --log /var/log/sureflap.log --time --name SureFlap
pm2 start pipenv run server.py --watch --time --name SureFlap_API
```

Enable PM2 to restart the application on reboot:

```bash
pm2 startup

pm2 save
```

## Usage

For the usage of the REST API take a look at the provided OpenAPI Specification on the main page of the webserver (http://localhost:3001). There you can find everything you need to know about the given methods and how to call them.

## Roadmap

Expand Down
6 changes: 3 additions & 3 deletions config.ini.sample
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[api]
endpoint = "https://app.api.surehub.io"
port = 2311

[user]
email = "SureFlapEmail"
password = "SureFlapPassword"
email = "SureFlap E-Mail"
password = "SureFlap Password"
Binary file added docs/demo_1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions logging.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 1
formatters:
simple:
datefmt: '%Y-%m-%d %H:%M:%S'
format: '%(asctime)s %(levelname)s %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
loggers:
sureflap:
level: INFO
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
Binary file removed requirements.txt
Binary file not shown.
14 changes: 7 additions & 7 deletions ressources/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

cache = TTLCache(maxsize=128, ttl=86400)


def getToken():
try:
_ = cache["token"]
Expand All @@ -17,24 +18,23 @@ def getToken():
if not cache["token"] == None:
return cache["token"]
else:
uri = config.endpoint + "/api/auth/login"
uri = config.ENDPOINT + "/api/auth/login"

postParams = {
"email_address" : config.email,
"password" : config.password,
"device_id" : random.randrange(1000000000, 9999999999)
"email_address": config.EMAIL,
"password": config.PASSWORD,
"device_id": random.randrange(1000000000, 9999999999)
}

response = requests.post(uri, data=postParams)

if response.ok:
data = json.loads(response.text)
cache["token"] = data['data']['token']

while cache["token"] is None:
time.sleep(0.3)

return cache["token"]
else:
abort(response.status_code)

106 changes: 79 additions & 27 deletions ressources/config.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,80 @@
import configparser
from configparser import ConfigParser
import os
import validators

try:
config = configparser.ConfigParser()
config.read(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..', 'config.ini')))

endpoint = config['api']['endpoint'].replace('"', '')
email = config['user']['email'].replace('"', '')
password = config['user']['password'].replace('"', '')
except:
print('No valid config.ini found! Please make sure you rename "config.ini.sample" to "config.ini" and edit the settings correctly.')
os._exit(0)


def validate():
if email != "SureFlapEmail" and password != "SureFlapPassword":
if validators.url(endpoint):
return True
else:
print("Invalid endpoint provided. Please check the syntax!")
return False
else:
print("Please edit the config.ini first!")
return False


import logging
import logging.config
from requests.models import ProtocolError
import yaml

# Initialize logging
with open('logging.conf', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)

logger = logging.getLogger("sureflap")


# Global configuration variables
ENDPOINT = "https://app.api.surehub.io"


class InvalidConfig(Exception):
pass


class ConfigValidator(ConfigParser):
def __init__(self, config_file):
super(ConfigValidator, self).__init__()

self.config_file = config_file
open(config_file)
self.read(config_file)
self.validate_config()

def validate_config(self):
required_values = {
'api': {
'port': None,
},
'user': {
'email': None,
'password': None,
},
}

for section, keys in required_values.items():
if section not in self:
raise InvalidConfig(
f'{self.__class__.__name__}: Missing section "{section}" in {self.config_file}')

for key, values in keys.items():
if key not in self[section] or self[section][key] in ('', 'YOUR_PERSONAL_ACCESS_TOKEN'):
raise InvalidConfig(
f'{self.__class__.__name__}: Missing value for "{key}" in section "{section}" in {self.config_file}')

if values:
if self[section][key] not in values:
allowed_values = f"[{(', '.join(map(str, values)))}]"
raise InvalidConfig(
f'{self.__class__.__name__}: Invalid value for "{key}" under section "{section}" in {self.config_file} - allowed values are {allowed_values}')


def init_config():
try:
config = ConfigValidator(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'config.ini')))

global EMAIL, PASSWORD, PORT
PORT = config['api']['port']
EMAIL = config['user']['email'].replace('"', '')
PASSWORD = config['user']['password'].replace('"', '')
return True

except FileNotFoundError:
logger.error(
'No config.ini found! Please make sure you rename "config.ini.sample" to "config.ini" and edit the settings correctly.')

except InvalidConfig as e:
logger.error(e)
raise SystemExit(2)

except:
logger.error("Configuration was not successfull!")
5 changes: 3 additions & 2 deletions ressources/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import requests
import json


def getDashboard():
uri = config.endpoint + "/api/me/start"
uri = config.ENDPOINT + "/api/me/start"

headers = {'Authorization': 'Bearer %s' % auth.getToken()}

Expand All @@ -14,4 +15,4 @@ def getDashboard():
data = json.loads(response.text)
return data['data']
else:
abort(response.status_code)
abort(response.status_code)
7 changes: 4 additions & 3 deletions ressources/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import requests
import json


def getDevices():
uri = config.endpoint + "/api/device"
uri = config.ENDPOINT + "/api/device"

headers = {'Authorization': 'Bearer %s' % auth.getToken()}

Expand All @@ -18,13 +19,13 @@ def getDevices():


def getDeviceByID(id):
uri = config.endpoint + "/api/device/" + id
uri = config.ENDPOINT + "/api/device/" + id

headers = {'Authorization': 'Bearer %s' % auth.getToken()}
payload = {'with[]': ['children', 'status', 'control']}

response = requests.get(uri, headers=headers, params=payload)

if response.ok:
data = json.loads(response.text)
return data['data']
Expand Down
12 changes: 7 additions & 5 deletions ressources/households.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,31 @@
import requests
import json


def getHouseholds():
uri = config.endpoint + "/api/household"
uri = config.ENDPOINT + "/api/household"

headers = {'Authorization': 'Bearer %s' % auth.getToken()}

response = requests.get(uri, headers=headers)

if response.ok:
data = json.loads(response.text)
return data['data']
else:
abort(response.status_code)


def getHouseholdByID(id):
uri = config.endpoint + "/api/household/" + id
uri = config.ENDPOINT + "/api/household/" + id

headers = {'Authorization': 'Bearer %s' % auth.getToken()}
payload = {'with[]': ['pets', 'users']}

response = requests.get(uri, headers=headers, params=payload)

if response.ok:
data = json.loads(response.text)
return data['data']
else:
abort(response.status_code)
abort(response.status_code)
Loading

0 comments on commit adb2891

Please sign in to comment.