diff --git a/README.md b/README.md
index e331b0f..9e7e275 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,9 @@
-## Python Client
+# Python Client
## Installation
`pip install labstack`
-## Quick Start
-
-[Sign up](https://labstack.com/signup) to get an API key
-
-Create a file `app.py` with the following content:
-
-```python
-from labstack import Client, APIError
-
-client = Client('', '')
-
-try:
- response = client.barcode_generate(format='qr_code', content='https://labstack.com')
- client.download(response['id'], '/tmp/' + response['name'])
-except APIError as error:
- print(error)
-```
-
-From terminal run your app:
-
-```sh
-python app.py
-```
-
-## [API](https://labstack.com/api) | [Forum](https://forum.labstack.com)
\ No newline at end of file
+## [Docs](https://labstack.com/docs) | [Forum](https://forum.labstack.com)
\ No newline at end of file
diff --git a/labstack/__init__.py b/labstack/__init__.py
index cc9f2cc..e69de29 100644
--- a/labstack/__init__.py
+++ b/labstack/__init__.py
@@ -1 +0,0 @@
-from .client import Client, APIError
\ No newline at end of file
diff --git a/labstack/client.py b/labstack/client.py
deleted file mode 100644
index 248888e..0000000
--- a/labstack/client.py
+++ /dev/null
@@ -1,199 +0,0 @@
-import requests
-
-API_URL = 'https://api.labstack.com'
-
-class _Interceptor(requests.auth.AuthBase):
- def __init__(self, api_key):
- self.api_key = api_key
-
- def __call__(self, r):
- r.headers['Authorization'] = 'Bearer ' + self.api_key
- return r
-
-class Client():
- def __init__(self, account_id, api_key):
- self.api_key = api_key
- self.interceptor = _Interceptor(api_key)
-
- def _error(self, r):
- return not 200 <= r.status_code < 300
-
- def download(self, id, path):
- r = requests.get('{}/download/{}'.format(API_URL, id), stream=True)
- with open(path, 'wb') as f:
- for chunk in r.iter_content(chunk_size=1024):
- if chunk:
- f.write(chunk)
- f.flush()
-
- def currency_convert(self, from=None, to=None, value=None):
- params = {
- 'from': from,
- 'to': to,
- 'value': value
- }
- r = requests.get(API_URL + '/currency/convert', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def currency_rates(self, base=None):
- params = {'base': base}
- r = requests.get(API_URL + '/currency/rates', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def email_verify(self, email=None):
- params = {'email': email}
- r = requests.get(API_URL + '/email/verify', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def geocode_address(self, location=None, longitude=None, latitude=None, osm_tag=None, limit=None):
- params = {
- 'location': location,
- 'longitude': longitude,
- 'latitude': latitude,
- 'osm_tag': osm_tag,
- 'limit': limit
- }
- r = requests.get(API_URL + '/geocode/address', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def geocode_ip(self, ip=None):
- params = {'ip': ip}
- r = requests.get(API_URL + '/geocode/ip', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def geocode_reverse(self, longitude=None, latitude=None, limit=None):
- params = {
- 'longitude': longitude,
- 'latitude': latitude,
- 'limit': limit
- }
- r = requests.get(API_URL + '/geocode/reverse', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def compress_audio(self, file=None):
- files = {'file': open(file, 'rb')}
- r = requests.post(API_URL + '/compress/audio', auth=self.interceptor, files=files)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def compress_image(self, file=None):
- files = {'file': open(file, 'rb')}
- r = requests.post(API_URL + '/compress/image', auth=self.interceptor, files=files)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def compress_pdf(self, file=None):
- files = {'file': open(file, 'rb')}
- r = requests.post(API_URL + '/compress/pdf', auth=self.interceptor, files=files)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def compress_video(self, file=None):
- files = {'file': open(file, 'rb')}
- r = requests.post(API_URL + '/compress/video', auth=self.interceptor, files=files)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def watermark_image(self, file=None, text=None, font=None, size=None, color=None, opacity=None,
- position=None, margin=None):
- files = {'file': open(file, 'rb')}
- data = {
- 'text': text,
- 'font': font,
- 'size': size,
- 'color': color,
- 'opacity': opacity,
- 'position': position,
- 'margin': margin
- }
- r = requests.post('{}/watermark/image'.format(API_URL), auth=self.interceptor,
- files=files, data=data)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def nlp_sentiment(self, text=None):
- json = {'text': text}
- r = requests.post(API_URL + '/text/sentiment', auth=self.interceptor,
- json=json)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def nlp_spellcheck(self, text=None):
- json = {'text': text}
- r = requests.post(API_URL + '/text/spellcheck', auth=self.interceptor,
- json=json)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def nlp_summary(self, text=None, url=None, language=None, length=None):
- json = {
- 'text': text,
- 'url': url,
- 'language': language,
- 'length': length
- }
- r = requests.post(API_URL + '/text/summary', auth=self.interceptor,
- json=json)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
- def webpage_pdf(self, url=None, layout=None, format=None):
- params = {
- 'url': url,
- 'layout': layout
- 'format': format,
- }
- r = requests.get(API_URL + '/webpage/pdf', auth=self.interceptor,
- params=params)
- data = r.json()
- if self._error(r):
- raise APIError(data['code'], data['message'])
- return data
-
-class APIError(Exception):
- def __init__(self, code, message):
- self.code = code
- self.message = message
-
- def __str__(self):
- return self.messag
\ No newline at end of file
diff --git a/labstack/common.py b/labstack/common.py
deleted file mode 100644
index d9045dc..0000000
--- a/labstack/common.py
+++ /dev/null
@@ -1 +0,0 @@
-API_URL = 'https://api.labstack.com'
\ No newline at end of file
diff --git a/labstack/cube.py b/labstack/cube.py
new file mode 100644
index 0000000..69024ba
--- /dev/null
+++ b/labstack/cube.py
@@ -0,0 +1,76 @@
+import os
+import time
+import socket
+import threading
+from datetime import datetime
+from apscheduler.schedulers.background import BackgroundScheduler
+import requests
+import psutil
+
+sched = BackgroundScheduler()
+
+class Cube():
+ def __init__(self, api_key, node=socket.gethostname(), batch_size=60,
+ dispatch_interval=60, tags=None):
+ self.api_key = api_key
+ self.node = node
+ self.batch_size = batch_size
+ self.dispatch_interval = dispatch_interval
+ self.tags = tags
+ self.start_time = time.time()
+ self.uptime = 0
+ self.cpu = 0.0
+ self.memory = 0
+ self.active_requests = 0
+ self.requests = []
+ self.lock = threading.Lock()
+
+ def system():
+ p = psutil.Process(os.getpid())
+ self.uptime = int(time.time() - self.start_time)
+ self.cpu = p.cpu_percent()
+ self.memory = p.memory_full_info().rss
+
+ sched.add_job(self._dispatch, 'interval', seconds=dispatch_interval)
+ sched.add_job(system, 'interval', seconds=1)
+ sched.start()
+
+ def _dispatch(self):
+ if not self.requests:
+ return
+
+ r = requests.post('https://api.labstack.com/cube', headers={
+ 'User-Agent': 'labstack/cube',
+ 'Authorization': 'Bearer ' + self.api_key
+ }, json=self.requests)
+ if not 200 <= r.status_code < 300:
+ # TOTO: handler error
+ print('cube error', r.json())
+
+ # Reset requests
+ self.requests.clear()
+
+ def start(self, request):
+ with self.lock:
+ self.active_requests += 1
+
+ request['time'] = int(datetime.now().timestamp() * 1000000)
+ request['active'] = self.active_requests
+ request['node'] = self.node
+ request['uptime'] = self.uptime
+ request['cpu'] = self.cpu
+ request['memory'] = self.memory
+ request['tags'] = self.tags
+ self.requests.append(request)
+
+ return request
+
+ def stop(self, request):
+ with self.lock:
+ self.active_requests -= 1
+ request['latency'] = int(datetime.now().timestamp() * 1000000) - request['time']
+
+ # Dispatch batch
+ if len(self.requests) >= self.batch_size:
+ threading.Thread(target=self._dispatch).start()
+
\ No newline at end of file
diff --git a/labstack/django.py b/labstack/django.py
new file mode 100644
index 0000000..d9471e4
--- /dev/null
+++ b/labstack/django.py
@@ -0,0 +1,35 @@
+from django.conf import settings
+from .cube import Cube
+from .util import strip_port
+
+def cube(get_response):
+ options = settings.CUBE
+ c = Cube(**options)
+
+ def middleware(request):
+ r = c.start({
+ 'id': request.META.get('HTTP_X_REQUEST_ID'),
+ 'host': strip_port(request.get_host()),
+ 'path': request.path,
+ 'method': request.method,
+ 'bytes_in': int(request.META.get('CONTENT_LENGTH') or 0),
+ # TODO: revisit
+ 'remote_ip': request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR'),
+ 'client_id': request.META.get('HTTP_X_CLIENT_ID'),
+ 'user_agent': request.META.get('HTTP_USER_AGENT')
+ })
+
+ response = get_response(request)
+
+ # https://docs.djangoproject.com/en/2.0/_modules/django/middleware/common/#CommonMiddleware
+ if not response.streaming and not response.has_header('Content-Length'):
+ response['Content-Length'] = str(len(response.content))
+
+ r['id'] = r['id'] or response.get('X-Request-ID')
+ r['status'] = response.status_code
+ r['bytes_out'] = int(response.get('Content-Length') or 0)
+ c.stop(r)
+
+ return response
+
+ return middleware
\ No newline at end of file
diff --git a/labstack/flask.py b/labstack/flask.py
new file mode 100644
index 0000000..8867cda
--- /dev/null
+++ b/labstack/flask.py
@@ -0,0 +1,29 @@
+from flask import request, g
+from .cube import Cube
+from .util import strip_port
+
+def cube(app, api_key, **kwargs):
+ c = Cube(api_key, **kwargs)
+
+ @app.before_request
+ def before_request():
+ g._r = c.start({
+ 'id': request.headers.get('X-Request-ID'),
+ 'host': strip_port(request.host),
+ 'path': request.path,
+ 'method': request.method,
+ 'bytes_in': int(request.headers.get('Content-Length') or 0),
+ # TODO: revisit
+ 'remote_ip': request.headers.get('X-Forwarded-For', request.remote_addr),
+ 'client_id': request.headers.get('X-Client-ID'),
+ 'user_agent': request.headers.get('User-Agent')
+ })
+
+ @app.after_request
+ def after_request(response):
+ r = g._r
+ r['id'] = r['id'] or response.headers.get('X-Request-ID')
+ r['status'] = response.status_code
+ r['bytes_out'] = int(response.headers.get('Content-Length') or 0)
+ c.stop(r)
+ return response
diff --git a/labstack/util.py b/labstack/util.py
new file mode 100644
index 0000000..61dbfa1
--- /dev/null
+++ b/labstack/util.py
@@ -0,0 +1,8 @@
+def strip_port(host):
+ colon = host.find(':')
+ if colon == -1:
+ return host
+ i = host.find(']')
+ if i != -1:
+ return host[host.find('(')+1:i]
+ return host[:colon]
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b5cf773..7957e8b 100644
--- a/setup.py
+++ b/setup.py
@@ -2,20 +2,25 @@
setup(
name='labstack',
- version='0.21.0',
- description='Official Python client library for the LabStack API',
+ version='0.21.3',
+ description='Official Python client library for the LabStack platform',
long_description='``_',
- keywords='image compress, image resize, text summary, barcode generate, barcode scan',
+ keywords='api, testing, monitoring, analytics',
url='https://github.com/labstack/labstack-python',
author='Vishal Rana',
author_email='vr@labstack.com',
license='MIT',
packages=['labstack'],
install_requires=[
- 'requests==2.18.1'
+ 'requests==2.18.4',
+ 'psutil==5.4.3',
+ 'APScheduler==3.5.1'
+ ],
+ extra_requires=[
+ 'Flask==0.12.2'
],
classifiers=[
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6'
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6'
]
)
diff --git a/test/_django/_django/__init__.py b/test/_django/_django/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/_django/_django/settings.py b/test/_django/_django/settings.py
new file mode 100644
index 0000000..c32dd6a
--- /dev/null
+++ b/test/_django/_django/settings.py
@@ -0,0 +1,126 @@
+"""
+Django settings for _django project.
+
+Generated by 'django-admin startproject' using Django 2.0.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.0/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '7#)abfoo7&0_+a2&b904pe+&q%w-q_rc*@+$lpe3qsai43u!)0'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'labstack.django.cube',
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = '_django.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = '_django.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.0/howto/static-files/
+
+STATIC_URL = '/static/'
+
+CUBE = {
+ 'api_key': os.getenv('LABSTACK_KEY'),
+ 'batch_size': 1
+}
\ No newline at end of file
diff --git a/test/_django/_django/urls.py b/test/_django/_django/urls.py
new file mode 100644
index 0000000..8833d28
--- /dev/null
+++ b/test/_django/_django/urls.py
@@ -0,0 +1,21 @@
+"""_django URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/2.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+]
diff --git a/test/_django/_django/wsgi.py b/test/_django/_django/wsgi.py
new file mode 100644
index 0000000..fb45744
--- /dev/null
+++ b/test/_django/_django/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for _django project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "_django.settings")
+
+application = get_wsgi_application()
diff --git a/test/_django/db.sqlite3 b/test/_django/db.sqlite3
new file mode 100644
index 0000000..e69de29
diff --git a/test/_django/manage.py b/test/_django/manage.py
new file mode 100755
index 0000000..37f6e5a
--- /dev/null
+++ b/test/_django/manage.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "_django.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
diff --git a/test/_flask.py b/test/_flask.py
new file mode 100644
index 0000000..f5dff90
--- /dev/null
+++ b/test/_flask.py
@@ -0,0 +1,12 @@
+import os
+from flask import Flask
+from labstack.flask import cube
+
+app = Flask(__name__)
+
+# Cube
+cube(app, os.getenv('LABSTACK_KEY'), batch_size=1)
+
+@app.route("/")
+def hello():
+ return "Hello, World!"