Kubernetes integration
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import connexion
|
||||
|
||||
from openapi_server import encoder
|
||||
|
||||
|
||||
def main():
|
||||
app = connexion.App(__name__, specification_dir='./openapi/')
|
||||
app.app.json_encoder = encoder.JSONEncoder
|
||||
app.add_api('openapi.yaml',
|
||||
arguments={'title': 'Stadt MG - Drohne'},
|
||||
pythonic_params=True)
|
||||
app.run(port=8080)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
import connexion
|
||||
import six
|
||||
|
||||
from openapi_server import util
|
||||
from pymongo import MongoClient
|
||||
from flask import Response
|
||||
|
||||
from kubernetes import client, config
|
||||
|
||||
logging.basicConfig(format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s')
|
||||
logger = logging.getLogger("API")
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.info("Hello there")
|
||||
|
||||
config.load_incluster_config() # or config.load_kube_config()
|
||||
collection = MongoClient("mongo").get_database("stadtmg").get_collection("predictions")
|
||||
|
||||
with client.ApiClient() as api_client:
|
||||
app_api_instance = client.AppsV1Api(api_client)
|
||||
core_api_instance = client.CoreV1Api(api_client)
|
||||
|
||||
|
||||
def detect_post(body=None): # noqa: E501
|
||||
"""detect_post
|
||||
|
||||
# noqa: E501
|
||||
|
||||
:param body:
|
||||
:type body: str
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
image_id = str(uuid4())
|
||||
logger.debug(f"Processing image '{image_id}'")
|
||||
while collection.find_one({"id": image_id}) is not None:
|
||||
image_id = str(uuid4())
|
||||
|
||||
collection.insert_one({
|
||||
"id": image_id,
|
||||
"input": body,
|
||||
})
|
||||
|
||||
job_name = f"bodenerkennung-{image_id}"
|
||||
metadata = client.V1ObjectMeta(
|
||||
name=job_name,
|
||||
labels={
|
||||
"io.kompose.service": job_name,
|
||||
},
|
||||
namespace="stadtmg",
|
||||
)
|
||||
spec = client.V1JobSpec(
|
||||
backoff_limit=0,
|
||||
ttl_seconds_after_finished=500,
|
||||
template=dict(
|
||||
spec=dict(
|
||||
containers=[
|
||||
dict(
|
||||
name="bodenerkennung",
|
||||
image="masasana.azurecr.io/stadt_mg_bodenerkennung:1.1.1",
|
||||
imagePullPolicy="Always",
|
||||
command=["python", "predict.py"],
|
||||
args=[
|
||||
"--source", "mongo://mongo",
|
||||
"--image_id", image_id,
|
||||
"--category_json", "",
|
||||
]
|
||||
)
|
||||
],
|
||||
imagePullSecrets=[{"name": "acr-secret"}],
|
||||
restartPolicy="Never",
|
||||
)
|
||||
),
|
||||
)
|
||||
logger.debug(metadata)
|
||||
logger.debug(spec)
|
||||
|
||||
job = client.V1Job(
|
||||
api_version="batch/v1",
|
||||
kind="Job",
|
||||
metadata=metadata,
|
||||
spec=spec,
|
||||
)
|
||||
logger.debug(job)
|
||||
|
||||
batch_api = client.BatchV1Api()
|
||||
batch_api.create_namespaced_job("stadtmg", job)
|
||||
|
||||
return Response(image_id, status=200)
|
||||
|
||||
|
||||
def image_image_id_get(image_id): # noqa: E501
|
||||
"""image_image_id_get
|
||||
|
||||
# noqa: E501
|
||||
|
||||
:param image_id:
|
||||
:type image_id:
|
||||
|
||||
:rtype: file
|
||||
"""
|
||||
db_object = collection.find_one({"id": image_id})
|
||||
if db_object is None:
|
||||
return Response(f"Image with id '{image_id}' not found", status=404)
|
||||
|
||||
image = db_object.get("output")
|
||||
if image is None:
|
||||
return Response(status=204)
|
||||
|
||||
return Response(image, status=200, mimetype="image/png")
|
||||
@@ -0,0 +1,3 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from connexion.apps.flask_app import FlaskJSONEncoder
|
||||
import six
|
||||
|
||||
from openapi_server.models.base_model_ import Model
|
||||
|
||||
|
||||
class JSONEncoder(FlaskJSONEncoder):
|
||||
include_nulls = False
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, Model):
|
||||
dikt = {}
|
||||
for attr, _ in six.iteritems(o.openapi_types):
|
||||
value = getattr(o, attr)
|
||||
if value is None and not self.include_nulls:
|
||||
continue
|
||||
attr = o.attribute_map[attr]
|
||||
dikt[attr] = value
|
||||
return dikt
|
||||
return FlaskJSONEncoder.default(self, o)
|
||||
@@ -0,0 +1,5 @@
|
||||
# coding: utf-8
|
||||
|
||||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
# import models into model package
|
||||
@@ -0,0 +1,69 @@
|
||||
import pprint
|
||||
|
||||
import six
|
||||
import typing
|
||||
|
||||
from openapi_server import util
|
||||
|
||||
T = typing.TypeVar('T')
|
||||
|
||||
|
||||
class Model(object):
|
||||
# openapiTypes: The key is attribute name and the
|
||||
# value is attribute type.
|
||||
openapi_types = {}
|
||||
|
||||
# attributeMap: The key is attribute name and the
|
||||
# value is json key in definition.
|
||||
attribute_map = {}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: typing.Type[T], dikt) -> T:
|
||||
"""Returns the dict as a model"""
|
||||
return util.deserialize_model(dikt, cls)
|
||||
|
||||
def to_dict(self):
|
||||
"""Returns the model properties as a dict
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for attr, _ in six.iteritems(self.openapi_types):
|
||||
value = getattr(self, attr)
|
||||
if isinstance(value, list):
|
||||
result[attr] = list(map(
|
||||
lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
|
||||
value
|
||||
))
|
||||
elif hasattr(value, "to_dict"):
|
||||
result[attr] = value.to_dict()
|
||||
elif isinstance(value, dict):
|
||||
result[attr] = dict(map(
|
||||
lambda item: (item[0], item[1].to_dict())
|
||||
if hasattr(item[1], "to_dict") else item,
|
||||
value.items()
|
||||
))
|
||||
else:
|
||||
result[attr] = value
|
||||
|
||||
return result
|
||||
|
||||
def to_str(self):
|
||||
"""Returns the string representation of the model
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return pprint.pformat(self.to_dict())
|
||||
|
||||
def __repr__(self):
|
||||
"""For `print` and `pprint`"""
|
||||
return self.to_str()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Returns true if both objects are equal"""
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __ne__(self, other):
|
||||
"""Returns true if both objects are not equal"""
|
||||
return not self == other
|
||||
@@ -0,0 +1,60 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
description: Stadt MG - Drohne
|
||||
title: Stadt MG - Drohne
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://drohne.masasana.ai
|
||||
tags:
|
||||
- name: Drohne
|
||||
paths:
|
||||
/detect:
|
||||
post:
|
||||
operationId: detect_post
|
||||
requestBody:
|
||||
content:
|
||||
image/*:
|
||||
schema:
|
||||
format: binary
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
format: uuid
|
||||
type: string
|
||||
description: ok
|
||||
tags:
|
||||
- Drohne
|
||||
x-openapi-router-controller: openapi_server.controllers.drohne_controller
|
||||
/image/{image_id}:
|
||||
get:
|
||||
operationId: image_image_id_get
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: image_id
|
||||
required: true
|
||||
schema:
|
||||
format: uuid
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
image/*:
|
||||
schema:
|
||||
format: binary
|
||||
type: string
|
||||
description: ok
|
||||
"204":
|
||||
description: The image is still in processing and no content can be provided
|
||||
just yet.
|
||||
"404":
|
||||
description: This id doesn't exist
|
||||
tags:
|
||||
- Drohne
|
||||
x-openapi-router-controller: openapi_server.controllers.drohne_controller
|
||||
components:
|
||||
schemas: {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
|
||||
import connexion
|
||||
from flask_testing import TestCase
|
||||
|
||||
from openapi_server.encoder import JSONEncoder
|
||||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
|
||||
def create_app(self):
|
||||
logging.getLogger('connexion.operation').setLevel('ERROR')
|
||||
app = connexion.App(__name__, specification_dir='../openapi/')
|
||||
app.app.json_encoder = JSONEncoder
|
||||
app.add_api('openapi.yaml', pythonic_params=True)
|
||||
return app.app
|
||||
@@ -0,0 +1,52 @@
|
||||
# coding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
import unittest
|
||||
|
||||
from flask import json
|
||||
from six import BytesIO
|
||||
|
||||
from openapi_server.test import BaseTestCase
|
||||
|
||||
|
||||
class TestDrohneController(BaseTestCase):
|
||||
"""DrohneController integration test stubs"""
|
||||
|
||||
@unittest.skip("image/* not supported by Connexion")
|
||||
def test_detect_post(self):
|
||||
"""Test case for detect_post
|
||||
|
||||
|
||||
"""
|
||||
body = (BytesIO(b'some file data'), 'file.txt')
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'image/*',
|
||||
}
|
||||
response = self.client.open(
|
||||
'/detect',
|
||||
method='POST',
|
||||
headers=headers,
|
||||
data=json.dumps(body),
|
||||
content_type='image/*')
|
||||
self.assert200(response,
|
||||
'Response body is : ' + response.data.decode('utf-8'))
|
||||
|
||||
def test_image_image_id_get(self):
|
||||
"""Test case for image_image_id_get
|
||||
|
||||
|
||||
"""
|
||||
headers = {
|
||||
'Accept': 'image/*',
|
||||
}
|
||||
response = self.client.open(
|
||||
'/image/<image_id>'.format(image_id='image_id_example'),
|
||||
method='GET',
|
||||
headers=headers)
|
||||
self.assert200(response,
|
||||
'Response body is : ' + response.data.decode('utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,32 @@
|
||||
# coding: utf-8
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
import typing
|
||||
|
||||
def is_generic(klass):
|
||||
""" Determine whether klass is a generic class """
|
||||
return type(klass) == typing.GenericMeta
|
||||
|
||||
def is_dict(klass):
|
||||
""" Determine whether klass is a Dict """
|
||||
return klass.__extra__ == dict
|
||||
|
||||
def is_list(klass):
|
||||
""" Determine whether klass is a List """
|
||||
return klass.__extra__ == list
|
||||
|
||||
else:
|
||||
|
||||
def is_generic(klass):
|
||||
""" Determine whether klass is a generic class """
|
||||
return hasattr(klass, '__origin__')
|
||||
|
||||
def is_dict(klass):
|
||||
""" Determine whether klass is a Dict """
|
||||
return klass.__origin__ == dict
|
||||
|
||||
def is_list(klass):
|
||||
""" Determine whether klass is a List """
|
||||
return klass.__origin__ == list
|
||||
@@ -0,0 +1,142 @@
|
||||
import datetime
|
||||
|
||||
import six
|
||||
import typing
|
||||
from openapi_server import typing_utils
|
||||
|
||||
|
||||
def _deserialize(data, klass):
|
||||
"""Deserializes dict, list, str into an object.
|
||||
|
||||
:param data: dict, list or str.
|
||||
:param klass: class literal, or string of class name.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if klass in six.integer_types or klass in (float, str, bool, bytearray):
|
||||
return _deserialize_primitive(data, klass)
|
||||
elif klass == object:
|
||||
return _deserialize_object(data)
|
||||
elif klass == datetime.date:
|
||||
return deserialize_date(data)
|
||||
elif klass == datetime.datetime:
|
||||
return deserialize_datetime(data)
|
||||
elif typing_utils.is_generic(klass):
|
||||
if typing_utils.is_list(klass):
|
||||
return _deserialize_list(data, klass.__args__[0])
|
||||
if typing_utils.is_dict(klass):
|
||||
return _deserialize_dict(data, klass.__args__[1])
|
||||
else:
|
||||
return deserialize_model(data, klass)
|
||||
|
||||
|
||||
def _deserialize_primitive(data, klass):
|
||||
"""Deserializes to primitive type.
|
||||
|
||||
:param data: data to deserialize.
|
||||
:param klass: class literal.
|
||||
|
||||
:return: int, long, float, str, bool.
|
||||
:rtype: int | long | float | str | bool
|
||||
"""
|
||||
try:
|
||||
value = klass(data)
|
||||
except UnicodeEncodeError:
|
||||
value = six.u(data)
|
||||
except TypeError:
|
||||
value = data
|
||||
return value
|
||||
|
||||
|
||||
def _deserialize_object(value):
|
||||
"""Return an original value.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
def deserialize_date(string):
|
||||
"""Deserializes string to date.
|
||||
|
||||
:param string: str.
|
||||
:type string: str
|
||||
:return: date.
|
||||
:rtype: date
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string).date()
|
||||
except ImportError:
|
||||
return string
|
||||
|
||||
|
||||
def deserialize_datetime(string):
|
||||
"""Deserializes string to datetime.
|
||||
|
||||
The string should be in iso8601 datetime format.
|
||||
|
||||
:param string: str.
|
||||
:type string: str
|
||||
:return: datetime.
|
||||
:rtype: datetime
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string)
|
||||
except ImportError:
|
||||
return string
|
||||
|
||||
|
||||
def deserialize_model(data, klass):
|
||||
"""Deserializes list or dict to model.
|
||||
|
||||
:param data: dict, list.
|
||||
:type data: dict | list
|
||||
:param klass: class literal.
|
||||
:return: model object.
|
||||
"""
|
||||
instance = klass()
|
||||
|
||||
if not instance.openapi_types:
|
||||
return data
|
||||
|
||||
for attr, attr_type in six.iteritems(instance.openapi_types):
|
||||
if data is not None \
|
||||
and instance.attribute_map[attr] in data \
|
||||
and isinstance(data, (list, dict)):
|
||||
value = data[instance.attribute_map[attr]]
|
||||
setattr(instance, attr, _deserialize(value, attr_type))
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def _deserialize_list(data, boxed_type):
|
||||
"""Deserializes a list and its elements.
|
||||
|
||||
:param data: list to deserialize.
|
||||
:type data: list
|
||||
:param boxed_type: class literal.
|
||||
|
||||
:return: deserialized list.
|
||||
:rtype: list
|
||||
"""
|
||||
return [_deserialize(sub_data, boxed_type)
|
||||
for sub_data in data]
|
||||
|
||||
|
||||
def _deserialize_dict(data, boxed_type):
|
||||
"""Deserializes a dict and its elements.
|
||||
|
||||
:param data: dict to deserialize.
|
||||
:type data: dict
|
||||
:param boxed_type: class literal.
|
||||
|
||||
:return: deserialized dict.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {k: _deserialize(v, boxed_type)
|
||||
for k, v in six.iteritems(data)}
|
||||
Reference in New Issue
Block a user