From 1c1bd79c70cc2f0258e927015a31d6a21708ac09 Mon Sep 17 00:00:00 2001 From: Martin Lindvall Date: Fri, 5 Jan 2024 15:11:33 +0100 Subject: [PATCH] Initial reversion --- README.md | 12 + examples/python/ia_app_basic/.gitignore | 32 +++ examples/python/ia_app_basic/README.md | 58 ++++ examples/python/ia_app_basic/flask_run.bat | 4 + examples/python/ia_app_basic/flask_run.sh | 6 + .../python/ia_app_basic/pcaddemo/__init__.py | 3 + .../python/ia_app_basic/pcaddemo/__main__.py | 6 + .../ia_app_basic/pcaddemo/analysisapi.py | 83 ++++++ .../python/ia_app_basic/pcaddemo/geometry.py | 16 ++ .../python/ia_app_basic/pcaddemo/version.py | 23 ++ .../python/ia_app_basic/pcaddemo/webserver.py | 258 ++++++++++++++++++ examples/python/ia_app_basic/poetry.lock | 137 ++++++++++ examples/python/ia_app_basic/pyproject.toml | 39 +++ examples/python/ia_app_basic/setup.py | 33 +++ 14 files changed, 710 insertions(+) create mode 100644 examples/python/ia_app_basic/.gitignore create mode 100644 examples/python/ia_app_basic/README.md create mode 100644 examples/python/ia_app_basic/flask_run.bat create mode 100755 examples/python/ia_app_basic/flask_run.sh create mode 100644 examples/python/ia_app_basic/pcaddemo/__init__.py create mode 100644 examples/python/ia_app_basic/pcaddemo/__main__.py create mode 100644 examples/python/ia_app_basic/pcaddemo/analysisapi.py create mode 100644 examples/python/ia_app_basic/pcaddemo/geometry.py create mode 100644 examples/python/ia_app_basic/pcaddemo/version.py create mode 100644 examples/python/ia_app_basic/pcaddemo/webserver.py create mode 100644 examples/python/ia_app_basic/poetry.lock create mode 100644 examples/python/ia_app_basic/pyproject.toml create mode 100644 examples/python/ia_app_basic/setup.py diff --git a/README.md b/README.md index aff37b7..a14b188 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ # Sectra Digital Pathology (DPAT) ImageAnalysis (IA) API - Software Development Kit (SDK) +## About +This is a work in progress repository providing examples on how to integrate with the Sectra IA-API. + +Currently only a basic application exist that returns (random) results given user input. + +See the `examples` folder for more info. A roadmap will be provided soon. + +## Changelog + +### 2024-01-05 + +- Added basic example `examples/python/ia_app_basic/` showing how an app can accept user input and return results for a specific sub-area of a WSI. diff --git a/examples/python/ia_app_basic/.gitignore b/examples/python/ia_app_basic/.gitignore new file mode 100644 index 0000000..ef67b8a --- /dev/null +++ b/examples/python/ia_app_basic/.gitignore @@ -0,0 +1,32 @@ +# Temporary Python files +*.pyc +*.egg-info +__pycache__ +.ipynb_checkpoints + +# Temporary OS files +Icon* + +# Temporary virtual environment files +/.cache/ +/.venv/ + +# Temporary server files +.env +*.pid + +# Build and release directories +/build/ +/dist/ +/debug/ +*.spec + +# Sublime Text +*.sublime-workspace + +# Eclipse +.settings + +# Don't include +scripts +.python-version diff --git a/examples/python/ia_app_basic/README.md b/examples/python/ia_app_basic/README.md new file mode 100644 index 0000000..7995e1e --- /dev/null +++ b/examples/python/ia_app_basic/README.md @@ -0,0 +1,58 @@ +pcaddemo +======== + +## About +This is a demo repository for a simple Sectra Image Analysis API (IA-API) implementation. + +Author: Martin Lindvall, martin.lindvall@sectra.com + +## Application function + +- Input: User draws a polygon around a region of interest ( taggedPolygon, see `webserver.py:app_return_registerinfo` ) +- Output: a graphical primitive, or a "patch gallery" (see `webserver.py:DEMO_TYPE` and `webserver.py:app_on_userinput` ) + +## Development notes +Uses a simple flask server for responding to IA-API requests and a requests-based client for outgoing communications with the IA-API.. + +For simplicity, uses no async libraries. For load-scaling, simply run with number of workers processes corresponding to expected max number of simultaneous users. Tools such as gunicorn or a similar pre-forking worker server is recommended to spawn the workers. + +Should have no trouble scaling as long as number of max simultaneous users are below 1000 or so. + +### Install and run + +You can run this example in a virtualenv using poetry, or directly in the current python environment. + +Using poetry: +``` +poetry run python pcaddemo/__main__.py +``` + +As plain-old python: + +``` +python setup.py develop +python pcaddemo/__main__.py +``` + +## Sectra Server configuration + +Running this demo starts a web server on 0.0.0.0 (all local ips) listening on port 5001. + +You need to configure the Sectra Pathology Server (SPS) to call this server. This involves: + +1. Ensuring that the SPS can reach this host over the network (ensure there is a route and that firewall rules allow it) +2. Registering the IA-API app in the configuration interface (see chapter 7 in the System Administrator Manual) + - in brief: goto https:///sectrapathologyimport/config + - log in, click Image Analysis Applications + - Under Server Side Applications, click 'register new' + - Enter the URL where your started web server (this repository) is running, as reachable from the pathology server. Example: `http://my-ia-app-server:5001/iademo` + - Press 'Retrieve registration Info' + - The fields should be populated, press *Save* + - Per default, the app is disabled. Click the 'disabled' button to toggle it to enabled. + +If succesful, you should now be able to right-click in any Pathology Image and select your new IA-APP (you might need to refresh any running sessions). + + +## Tested with + +- DPAT 3.4 on 2024-01-05 diff --git a/examples/python/ia_app_basic/flask_run.bat b/examples/python/ia_app_basic/flask_run.bat new file mode 100644 index 0000000..dd2bff7 --- /dev/null +++ b/examples/python/ia_app_basic/flask_run.bat @@ -0,0 +1,4 @@ +set FLASK_APP=pcaddemo\webserver +set FLASK_RUN_PORT=5005 +set FLASK_DEBUG=1 +flask run diff --git a/examples/python/ia_app_basic/flask_run.sh b/examples/python/ia_app_basic/flask_run.sh new file mode 100755 index 0000000..61d08b3 --- /dev/null +++ b/examples/python/ia_app_basic/flask_run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +export FLASK_APP=pcaddemo/webserver +export FLASK_RUN_PORT=5005 +export FLASK_RUN_HOST=0.0.0.0 +export FLASK_DEBUG=1 +flask run diff --git a/examples/python/ia_app_basic/pcaddemo/__init__.py b/examples/python/ia_app_basic/pcaddemo/__init__.py new file mode 100644 index 0000000..84b15d8 --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/__init__.py @@ -0,0 +1,3 @@ +""" pcad demo """ + +from .version import __version__ diff --git a/examples/python/ia_app_basic/pcaddemo/__main__.py b/examples/python/ia_app_basic/pcaddemo/__main__.py new file mode 100644 index 0000000..996470a --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/__main__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +if __name__ == "__main__": + from pcaddemo.webserver import app, BIND_PORT + + app.run(host="0.0.0.0", port=BIND_PORT, debug=False) diff --git a/examples/python/ia_app_basic/pcaddemo/analysisapi.py b/examples/python/ia_app_basic/pcaddemo/analysisapi.py new file mode 100644 index 0000000..6c013e7 --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/analysisapi.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +""" +analysisapi -- python client for the Sectra Pathology Image Analysis API + +This is a requests-style, non-async implementation. +""" + +import logging as log +from io import BytesIO +import time +import requests + +DEFAULT_TIMEOUT = ( + 3.0 * 2 + 0.05, + 30.0 * 2, +) # ( connect_timeout, read_timeout ) in seconds + + +class AnalysisApi: + def __init__(self, url, hookname, token=None, api_version="1.5"): + """ + @param url -- typically https://localhost/SectraPathologyServer/external/imageanalysis/v1 + """ + self.url = url + self.name = hookname + self.token = token + self._session = requests.Session() + self.api_version = api_version + + def _auth_get(self, url, ok_codes=[200]): + """make GET request adding the bearer token""" + r = self._session.get(url, headers=self._headers(), timeout=DEFAULT_TIMEOUT) + if not r.status_code in ok_codes: + r.raise_for_status() + return r + + def slideinfo(self, slide_id): + """GET slide metadata for slide""" + r = self._auth_get("{}/slides/{}/info".format(self.url, slide_id)) + slide_info = r.json()["blocks"][0]["slides"][0] # yeah, weird + return slide_info + + def tile(self, slide_id, dzilvl, col, row, zstack=0, format="jpg"): + """retrieve an image tile""" + tile_url = "{}/images/{}_files/{}/{}_{}_{}.{}".format( + self.url, slide_id, dzilvl, col, row, zstack, format + ) + r = self._session.get( + tile_url, headers=self._headers(), timeout=DEFAULT_TIMEOUT + ) + if not r.status_code == 200: + r.raise_for_status() + return BytesIO(r.content) + + def store_result(self, result_data): + """store result into sectra pathology db""" + url = f"{self.url}/hooks/{self.name}/results/" + r = self._session.post( + url, headers=self._headers(), timeout=DEFAULT_TIMEOUT, json=result_data + ) + if not r.status_code == 200: + r.raise_for_status() + parsed = r.json() + return parsed + + def update_result(self, result_data): + """update existing result""" + result_id = result_data["id"] + url = f"{self.url}/hooks/{self.name}/results/{result_id}" + r = self._session.put( + url, headers=self._headers(), timeout=DEFAULT_TIMEOUT, json=result_data + ) + if not r.status_code == 200: + r.raise_for_status() + parsed = r.json() + return parsed + + def _headers(self): + return { + "Authorization": "Bearer {}".format(self.token), + "X-Sectra-ApiVersion": f"{self.api_version}", + } diff --git a/examples/python/ia_app_basic/pcaddemo/geometry.py b/examples/python/ia_app_basic/pcaddemo/geometry.py new file mode 100644 index 0000000..e07db3f --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/geometry.py @@ -0,0 +1,16 @@ +import json +import random + +from shapely.geometry import Polygon, Point + + +def sectra_polygon_to_shapely(data_polygon): + return Polygon([(pt["x"], pt["y"]) for pt in data_polygon["points"]]) + + +def random_point_in_polygon(poly): + minx, miny, maxx, maxy = poly.bounds + while True: + p = Point(random.uniform(minx, maxx), random.uniform(miny, maxy)) + if poly.contains(p): + return p diff --git a/examples/python/ia_app_basic/pcaddemo/version.py b/examples/python/ia_app_basic/pcaddemo/version.py new file mode 100644 index 0000000..c3f08ae --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/version.py @@ -0,0 +1,23 @@ +""" +version info +""" +__version__ = "0.1.0" + +# this is the minimum version we require, we do not support +# servers that run earlier versions. +# +# Bump this number when you add expectations on API datastructures or endpoints only +# present from version X.Y onward. +# +# 1.5 here corresponds to DPAT 3.0 and we make use of /slides//info endpoint +# only available from this version and onward +SECTRA_IA_API_MIN_VERSION = "1.5" + +# this is the highest version we've explicitly tested with +# higher-capable servers should fallback to this api version to ensure compatability +# +# Bump this to the highest number you've tested with. +# If you are lazy, set: +# SECTRA_IA_API_MAX_VERSION = SECTRA_IA_API_MIN_VERSION +# (you might lose some enhancements that you do not rely on anyway) +SECTRA_IA_API_MAX_VERSION = "1.8" diff --git a/examples/python/ia_app_basic/pcaddemo/webserver.py b/examples/python/ia_app_basic/pcaddemo/webserver.py new file mode 100644 index 0000000..e964bfa --- /dev/null +++ b/examples/python/ia_app_basic/pcaddemo/webserver.py @@ -0,0 +1,258 @@ +import os +import json +import traceback +from functools import reduce + +from flask import Flask, request, jsonify + +from pcaddemo.version import ( + __version__, + SECTRA_IA_API_MIN_VERSION, + SECTRA_IA_API_MAX_VERSION, +) +from pcaddemo.geometry import sectra_polygon_to_shapely, random_point_in_polygon +from pcaddemo.analysisapi import AnalysisApi + +app = Flask(__name__) + +APP_NAME = "ImageAnalysisDemo" +APP_MANUFACTURER = "Sectra (demo)" +BIND_PORT = 5005 +DEMO_TYPE = "gallery" # use 'polygon' or 'gallery' + + +def json_resp(data): + """serialize data to JSON and add appropriate IA-api headers""" + resp = jsonify(data) + resp.headers.set("X-Sectra-ApiVersion", SECTRA_IA_API_MIN_VERSION) + resp.headers.set("X-Sectra-SoftwareVersion", __version__) + return resp + + +@app.route("/", methods=["GET"]) +def index(): + """ + Just a hello world page. The json is not part of the Sectra API. + Useful for debugging a running service. + """ + return json_resp( + [ + { + "name": APP_NAME, + "version": __version__, + "route": "/iademo", + "sectra-apiversion": SECTRA_IA_API_MIN_VERSION, + } + ] + ) + + +# IA-API Implementation +# ===================== + + +@app.route("/iademo", methods=["GET"]) +def app_return_registerinfo(): + """ + route for user-assisted registration in the SectraPathologyImport/config UI + + This URL is what should go into the configure URL + """ + hostname = f"127.0.0.1:{BIND_PORT}" + if "Host" in request.headers: + hostname = request.headers["Host"] + + # NOTE: as of DPAT 3.4 / IA-API v1.8 this will prepopulate all fields in the registration + # except Image Notifications. So if you need Notifications you must tell the admin who configs the application + # to enable it. + data = { + "applicationId": APP_NAME, + "displayName": APP_NAME, + "url": f"http://{hostname}/iademo", + "manufacturer": APP_MANUFACTURER, + "inputTemplate": {"type": "taggedPolygon", "content": {"tags": []}}, + "context": {}, + } + return json_resp(data) + + +@app.route("/iademo", methods=["POST"]) +def app_on_userinput(): + """ + Dispatch (routing) for user operations. + + This route is called when the app is activated by the user + who then draws (creates), modifies or deletes an area (taggedPolygon) + + Additionally, the same route is called when the user clicks buttons + in the patchCollection-type result. + """ + response = {} + data = request.get_json() + + # save_filename = f"./debug/{data['action']}_tmp.json" + # with open(save_filename, 'w') as file: + # json.dump(data, file) + + # data['action'] :: create, modify, delete, cancel + if data["action"] == "create": + if DEMO_TYPE == "polygon": + return app_create_primitiveArea(data) + else: + return app_create_patchCollection(data) + elif data["action"] == "modify": + for button in data["input"]["data"]["result"]["content"]["actions"]: + if button["state"] == 1: + # this is a button press + button["state"] = 0 # reset the button so dispatch doesn't need to know + return app_modify_button(button["id"], data) + # user modified a previously created result + return app_modify(data) + elif data["action"] == "delete": + return app_delete(data) + elif data["action"] == "cancel": + return json_resp([]) + return json_resp(response) + + +def app_create_primitiveArea(data): + """ + user wants a result for a given input area + + this demo implementation simply returns the input geometry with a label attached + """ + input_polygon = data["input"]["content"]["polygon"] + plg = sectra_polygon_to_shapely(input_polygon) + # determine suitable placement of label text + min_y_pt = reduce( + lambda pt1, pt2: pt2 if pt2[1] < pt1[1] else pt1, plg.exterior.coords + ) + + text = "0 demo-positive cells found." + + response_result = { + "type": "primitive", + "content": { + "style": {"fillStyle": None, "size": None, "strokeStyle": "#FFA500"}, + "polygons": [input_polygon], # same area as user input, + "labels": [ + {"location": {"x": min_y_pt[0], "y": min_y_pt[1]}, "label": text} + ], + }, + } + + # note : result returned must have database id, so we are responsible for storing this to server + server_api = AnalysisApi( + data["callbackInfo"]["url"], + data["applicationId"], + token=data["callbackInfo"]["token"], + api_version=SECTRA_IA_API_MIN_VERSION, + ) + store_data = { + "slideId": data["slideId"], + "displayResult": text, # text as shown in the annotation list (L) + "displayProperties": {"Cell Count": "0 cells"}, + "applicationVersion": __version__, + "data": {"context": None, "result": response_result}, + } + response = server_api.store_result(store_data) + # the response we get back is in a suitable format for passing back to the caller + return json_resp(response) + + +def app_create_patchCollection(data): + """ + user wants a result for a given input area + + this demo implementation returns a patch gallery with 10 random points + """ + input_polygon = data["input"]["content"]["polygon"] + plg = sectra_polygon_to_shapely(input_polygon) + random_points = [random_point_in_polygon(plg) for _x in range(0, 10)] + patches = [ + # tag: 0 is "A" (index into tags definition below) + # sortKeyValue determines the order of patches in the UI + {"tag": 0, "position": {"x": p.x, "y": p.y}, "sortKeyValue": 1.0} + for p in random_points + ] + + response_result = { + "type": "patchCollection", + "content": { + "polygons": [input_polygon], + "patches": patches, + "tags": ["A", "B", "C"], # these are the categories the user can change + "description": "App Description", + "magnification": 10, # zoom level to get the patch in + "patchSize": 64, # size in pixels of each patch + "statuses": {"allowVerify": {"value": True, "message": "allowed."}}, + "actions": [ + # these are buttons user can click + {"id": "mycmd", "state": 0, "name": "MyCmd", "tooltip": "execute MyCmd"} + ], + }, + } + # note : patchCollection result returned must have database id, + # so must store this to server + server_api = AnalysisApi( + data["callbackInfo"]["url"], + data["applicationId"], + token=data["callbackInfo"]["token"], + api_version=SECTRA_IA_API_MIN_VERSION, + ) + store_data = { + "slideId": data["slideId"], + "displayResult": "myText", + "displayProperties": {"Cell Ratios": "10/0/0 (A/B/C)"}, + "resultVersion": "1.0", + "data": {"context": None, "result": response_result}, + } + response = server_api.store_result(store_data) + return json_resp(response) + + +def app_modify(data): + """result has been modified by user""" + # just store the updated result on the server and send back to user + server_api = AnalysisApi( + data["callbackInfo"]["url"], + data["applicationId"], + token=data["callbackInfo"]["token"], + api_version=SECTRA_IA_API_MIN_VERSION, + ) + response = server_api.update_result(data["input"]) + return json_resp(response) + + +def app_modify_button(button_id, data): + """when button in patch gallery is pressed by user""" + print("button-press", button_id) + return json_resp(data["input"]) + + +def app_delete(data): + """result has been deleted by the user, and our app is informed""" + # do not intervene (will get the result deleted) + return json_resp({}) + + +# easy test and run setup +# ======================= + + +# set default exception handler +def defaultHandler(e): + code = 500 + if hasattr(e, "code"): + code = e.code + print("--- error ---") + print("ERROR:", str(e)) + traceback.print_tb(e.__traceback__) + return json.dumps({"error": str(e)}), code + + +app.config["TRAP_HTTP_EXCEPTIONS"] = True +app.register_error_handler(Exception, defaultHandler) + +if __name__ == "__main__": + app.run(port=BIND_PORT, debug=True) diff --git a/examples/python/ia_app_basic/poetry.lock b/examples/python/ia_app_basic/poetry.lock new file mode 100644 index 0000000..77825a2 --- /dev/null +++ b/examples/python/ia_app_basic/poetry.lock @@ -0,0 +1,137 @@ +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.6.16" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "main" +description = "A simple framework for building complex web applications." +name = "flask" +optional = false +python-versions = "*" +version = "1.0.3" + +[package.dependencies] +Jinja2 = ">=2.10" +Werkzeug = ">=0.14" +click = ">=5.1" +itsdangerous = ">=0.24" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "main" +description = "Various helpers to pass data to untrusted environments and back." +name = "itsdangerous" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A small but fast and easy to use stand-alone template engine written in pure python." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.1" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "Minimalistic wrapper for Python logging." +name = "minilog" +optional = false +python-versions = ">=3.4,<4.0" +version = "1.2.3" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[[package]] +category = "main" +description = "Geometric objects, predicates, and operations" +name = "shapely" +optional = false +python-versions = "*" +version = "1.6.4.post2" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.25.3" + +[[package]] +category = "main" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.15.4" + +[metadata] +content-hash = "5260b7306299e94ac9dea2d015847e43b33a7c983fae06ee6e63e209e48d483a" +python-versions = ">=3.6,<4" + +[metadata.hashes] +certifi = ["046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", "945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +flask = ["ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3", "e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"] +idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"] +jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] +markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] +minilog = ["226f7c40f9857ef1d86e659b27720476699045b9e619846fed8e5f378407bee7", "d6b1f769bf548e7c9a8223eb008514dbc5f9b4764fec3e132bed0d585bb1b6e8"] +requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] +shapely = ["0378964902f89b8dbc332e5bdfa08e0bc2f7ab39fecaeb17fbb2a7699a44fe71", "34e7c6f41fb27906ccdf2514ee44a5774b90b39a256b6511a6a57d11ffe64999", "3ca69d4b12e2b05b549465822744b6a3a1095d8488cc27b2728a06d3c07d0eee", "3e9388f29bd81fcd4fa5c35125e1fbd4975ee36971a87a90c093f032d0e9de24", "3ef28e3f20a1c37f5b99ea8cf8dcb58e2f1a8762d65ed2d21fd92bf1d4811182", "523c94403047eb6cacd7fc1863ebef06e26c04d8a4e7f8f182d49cd206fe787e", "5d22a1a705c2f70f61ccadc696e33d922c1a92e00df8e1d58a6ade14dd7e3b4f", "714b6680215554731389a1bbdae4cec61741aa4726921fa2b2b96a6f578a2534", "7dfe1528650c3f0dc82f41a74cf4f72018288db9bfb75dcd08f6f04233ec7e78", "ba58b21b9cf3c33725f7f530febff9ed6a6846f9d0bf8a120fc74683ff919f89", "c4b87bb61fc3de59fc1f85e71a79b0c709dc68364d9584473697aad4aa13240f", "ebb4d2bee7fac3f6c891fcdafaa17f72ab9c6480f6d00de0b2dc9a5137dfe342"] +urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] +werkzeug = ["865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", "a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"] diff --git a/examples/python/ia_app_basic/pyproject.toml b/examples/python/ia_app_basic/pyproject.toml new file mode 100644 index 0000000..748e46c --- /dev/null +++ b/examples/python/ia_app_basic/pyproject.toml @@ -0,0 +1,39 @@ +[tool] +[tool.poetry] +name = "pcaddemo" +version = "0.1.0" +description = "" +authors = ["Martin Lindvall "] +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.6,<4" +Flask = ">=1.0" +minilog = ">=1.2" +requests = ">=2.22" +shapely = ">=1.6" + +[tool.poetry.dev-dependencies] + +[tool.poetry.scripts] +dephell = "dephell.cli:entrypoint" + +[tool.dephell] +[tool.dephell.main] +from = {format = "poetry", path = "pyproject.toml"} +to = {format = "setuppy", path = "setup.py"} +versioning = "semver" + +#[tool.dephell.lock] +#from = {format = "poetry", path = "pyproject.toml"} +#to = {format = "poetrylock", path = "poetry.lock"} + +[tool.dephell.poetry] +# read dependencies from poetry format +from = {format = "poetry", path = "pyproject.toml"} +# run command `poetry install` +command = "poetry install -vvv" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/examples/python/ia_app_basic/setup.py b/examples/python/ia_app_basic/setup.py new file mode 100644 index 0000000..db5a5e7 --- /dev/null +++ b/examples/python/ia_app_basic/setup.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# DO NOT EDIT THIS FILE! +# This file has been autogenerated by dephell <3 +# https://github.com/dephell/dephell + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +import os.path + +readme = "" +here = os.path.abspath(os.path.dirname(__file__)) +readme_path = os.path.join(here, "README.md") +if os.path.exists(readme_path): + with open(readme_path, "rb") as stream: + readme = stream.read().decode("utf8") + +setup( + long_description=readme, + name="pcaddemo", + version="0.1.0", + description="pcad demo", + python_requires="<4,>=3.6", + author="Martin Lindvall", + author_email="martin.lindvall@sectra.com", + entry_points={"console_scripts": ["dephell = dephell.cli:entrypoint"]}, + packages=["pcaddemo"], + package_data={}, + install_requires=["flask>=1.0", "minilog>=1.2", "requests>=2.22", "shapely>=1.6"], +)