diff --git a/java/.gitignore b/v4/.gitignore similarity index 100% rename from java/.gitignore rename to v4/.gitignore diff --git a/v4/java/.gitignore b/v4/java/.gitignore new file mode 100644 index 0000000..2f083d2 --- /dev/null +++ b/v4/java/.gitignore @@ -0,0 +1,11 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +/bin/ +.DS_Store +.settings +.project +.classpath +.gradle diff --git a/java/LICENSE b/v4/java/LICENSE similarity index 100% rename from java/LICENSE rename to v4/java/LICENSE diff --git a/java/README.md b/v4/java/README.md similarity index 100% rename from java/README.md rename to v4/java/README.md diff --git a/java/build.gradle b/v4/java/build.gradle similarity index 100% rename from java/build.gradle rename to v4/java/build.gradle diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/v4/java/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from java/gradle/wrapper/gradle-wrapper.jar rename to v4/java/gradle/wrapper/gradle-wrapper.jar diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/v4/java/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from java/gradle/wrapper/gradle-wrapper.properties rename to v4/java/gradle/wrapper/gradle-wrapper.properties diff --git a/java/gradlew b/v4/java/gradlew similarity index 100% rename from java/gradlew rename to v4/java/gradlew diff --git a/java/gradlew.bat b/v4/java/gradlew.bat similarity index 100% rename from java/gradlew.bat rename to v4/java/gradlew.bat diff --git a/java/settings.gradle b/v4/java/settings.gradle similarity index 100% rename from java/settings.gradle rename to v4/java/settings.gradle diff --git a/java/src/main/java/api/example/java/App.java b/v4/java/src/main/java/api/example/java/App.java similarity index 100% rename from java/src/main/java/api/example/java/App.java rename to v4/java/src/main/java/api/example/java/App.java diff --git a/java/src/main/java/api/example/java/Config.java b/v4/java/src/main/java/api/example/java/Config.java similarity index 100% rename from java/src/main/java/api/example/java/Config.java rename to v4/java/src/main/java/api/example/java/Config.java diff --git a/java/src/main/java/api/example/java/api/ClimateAPIs.java b/v4/java/src/main/java/api/example/java/api/ClimateAPIs.java similarity index 100% rename from java/src/main/java/api/example/java/api/ClimateAPIs.java rename to v4/java/src/main/java/api/example/java/api/ClimateAPIs.java diff --git a/java/src/main/java/api/example/java/api/ClimateOAuth.java b/v4/java/src/main/java/api/example/java/api/ClimateOAuth.java similarity index 100% rename from java/src/main/java/api/example/java/api/ClimateOAuth.java rename to v4/java/src/main/java/api/example/java/api/ClimateOAuth.java diff --git a/java/src/main/java/api/example/java/api/RequestClient.java b/v4/java/src/main/java/api/example/java/api/RequestClient.java similarity index 100% rename from java/src/main/java/api/example/java/api/RequestClient.java rename to v4/java/src/main/java/api/example/java/api/RequestClient.java diff --git a/java/src/main/java/api/example/java/controllers/AgronomicDataController.java b/v4/java/src/main/java/api/example/java/controllers/AgronomicDataController.java similarity index 100% rename from java/src/main/java/api/example/java/controllers/AgronomicDataController.java rename to v4/java/src/main/java/api/example/java/controllers/AgronomicDataController.java diff --git a/java/src/main/java/api/example/java/controllers/BaseController.java b/v4/java/src/main/java/api/example/java/controllers/BaseController.java similarity index 100% rename from java/src/main/java/api/example/java/controllers/BaseController.java rename to v4/java/src/main/java/api/example/java/controllers/BaseController.java diff --git a/java/src/main/java/api/example/java/controllers/FieldController.java b/v4/java/src/main/java/api/example/java/controllers/FieldController.java similarity index 100% rename from java/src/main/java/api/example/java/controllers/FieldController.java rename to v4/java/src/main/java/api/example/java/controllers/FieldController.java diff --git a/java/src/main/java/api/example/java/controllers/HomeController.java b/v4/java/src/main/java/api/example/java/controllers/HomeController.java similarity index 100% rename from java/src/main/java/api/example/java/controllers/HomeController.java rename to v4/java/src/main/java/api/example/java/controllers/HomeController.java diff --git a/java/src/main/java/api/example/java/controllers/LoginController.java b/v4/java/src/main/java/api/example/java/controllers/LoginController.java similarity index 100% rename from java/src/main/java/api/example/java/controllers/LoginController.java rename to v4/java/src/main/java/api/example/java/controllers/LoginController.java diff --git a/java/src/main/java/api/example/java/model/Activity.java b/v4/java/src/main/java/api/example/java/model/Activity.java similarity index 100% rename from java/src/main/java/api/example/java/model/Activity.java rename to v4/java/src/main/java/api/example/java/model/Activity.java diff --git a/java/src/main/java/api/example/java/model/ActivityResult.java b/v4/java/src/main/java/api/example/java/model/ActivityResult.java similarity index 100% rename from java/src/main/java/api/example/java/model/ActivityResult.java rename to v4/java/src/main/java/api/example/java/model/ActivityResult.java diff --git a/java/src/main/java/api/example/java/model/Field.java b/v4/java/src/main/java/api/example/java/model/Field.java similarity index 100% rename from java/src/main/java/api/example/java/model/Field.java rename to v4/java/src/main/java/api/example/java/model/Field.java diff --git a/java/src/main/java/api/example/java/model/FieldResult.java b/v4/java/src/main/java/api/example/java/model/FieldResult.java similarity index 100% rename from java/src/main/java/api/example/java/model/FieldResult.java rename to v4/java/src/main/java/api/example/java/model/FieldResult.java diff --git a/java/src/main/java/api/example/java/model/Parent.java b/v4/java/src/main/java/api/example/java/model/Parent.java similarity index 100% rename from java/src/main/java/api/example/java/model/Parent.java rename to v4/java/src/main/java/api/example/java/model/Parent.java diff --git a/java/src/main/java/api/example/java/model/TokenResponse.java b/v4/java/src/main/java/api/example/java/model/TokenResponse.java similarity index 100% rename from java/src/main/java/api/example/java/model/TokenResponse.java rename to v4/java/src/main/java/api/example/java/model/TokenResponse.java diff --git a/java/src/main/java/api/example/java/model/User.java b/v4/java/src/main/java/api/example/java/model/User.java similarity index 100% rename from java/src/main/java/api/example/java/model/User.java rename to v4/java/src/main/java/api/example/java/model/User.java diff --git a/java/src/main/resources/application.properties b/v4/java/src/main/resources/application.properties similarity index 100% rename from java/src/main/resources/application.properties rename to v4/java/src/main/resources/application.properties diff --git a/java/src/main/resources/static/favicon.png b/v4/java/src/main/resources/static/favicon.png similarity index 100% rename from java/src/main/resources/static/favicon.png rename to v4/java/src/main/resources/static/favicon.png diff --git a/java/src/main/resources/static/fv-login-button.png b/v4/java/src/main/resources/static/fv-login-button.png similarity index 100% rename from java/src/main/resources/static/fv-login-button.png rename to v4/java/src/main/resources/static/fv-login-button.png diff --git a/java/src/main/resources/templates/activities.ftl b/v4/java/src/main/resources/templates/activities.ftl similarity index 100% rename from java/src/main/resources/templates/activities.ftl rename to v4/java/src/main/resources/templates/activities.ftl diff --git a/java/src/main/resources/templates/fields.ftl b/v4/java/src/main/resources/templates/fields.ftl similarity index 100% rename from java/src/main/resources/templates/fields.ftl rename to v4/java/src/main/resources/templates/fields.ftl diff --git a/java/src/main/resources/templates/footer.ftl b/v4/java/src/main/resources/templates/footer.ftl similarity index 100% rename from java/src/main/resources/templates/footer.ftl rename to v4/java/src/main/resources/templates/footer.ftl diff --git a/java/src/main/resources/templates/home.ftl b/v4/java/src/main/resources/templates/home.ftl similarity index 100% rename from java/src/main/resources/templates/home.ftl rename to v4/java/src/main/resources/templates/home.ftl diff --git a/java/src/main/resources/templates/index.ftl b/v4/java/src/main/resources/templates/index.ftl similarity index 100% rename from java/src/main/resources/templates/index.ftl rename to v4/java/src/main/resources/templates/index.ftl diff --git a/java/src/main/resources/templates/standardPage.ftl b/v4/java/src/main/resources/templates/standardPage.ftl similarity index 100% rename from java/src/main/resources/templates/standardPage.ftl rename to v4/java/src/main/resources/templates/standardPage.ftl diff --git a/java/src/test/java/api/example/java/AppTest.java b/v4/java/src/test/java/api/example/java/AppTest.java similarity index 100% rename from java/src/test/java/api/example/java/AppTest.java rename to v4/java/src/test/java/api/example/java/AppTest.java diff --git a/python/.gitignore b/v4/python/.gitignore similarity index 100% rename from python/.gitignore rename to v4/python/.gitignore diff --git a/python/LICENSE b/v4/python/LICENSE similarity index 100% rename from python/LICENSE rename to v4/python/LICENSE diff --git a/python/README.md b/v4/python/README.md similarity index 100% rename from python/README.md rename to v4/python/README.md diff --git a/python/climate.py b/v4/python/climate.py similarity index 100% rename from python/climate.py rename to v4/python/climate.py diff --git a/python/file.py b/v4/python/file.py similarity index 100% rename from python/file.py rename to v4/python/file.py diff --git a/python/logger.py b/v4/python/logger.py similarity index 100% rename from python/logger.py rename to v4/python/logger.py diff --git a/python/main.py b/v4/python/main.py similarity index 100% rename from python/main.py rename to v4/python/main.py diff --git a/python/requirements.txt b/v4/python/requirements.txt similarity index 100% rename from python/requirements.txt rename to v4/python/requirements.txt diff --git a/python/res/fv-login-button.png b/v4/python/res/fv-login-button.png similarity index 100% rename from python/res/fv-login-button.png rename to v4/python/res/fv-login-button.png diff --git a/v5/python/.gitignore b/v5/python/.gitignore new file mode 100644 index 0000000..065992b --- /dev/null +++ b/v5/python/.gitignore @@ -0,0 +1,95 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# venv +api-example/ + +# env scripts +set_env_variables.sh \ No newline at end of file diff --git a/v5/python/climate.py b/v5/python/climate.py new file mode 100644 index 0000000..0299d3b --- /dev/null +++ b/v5/python/climate.py @@ -0,0 +1,256 @@ +""" +Climate API demo code. This module shows how to: + +- Log in with Climate +- Refresh the access_token +- Fetch growing seasons +- Fetch harvest reports + +License: +Copyright © 2022 Climate, LLC +""" + +import requests + +import os +import json +from base64 import b64encode +from urllib.parse import urlencode +from curlify import to_curl +from logger import Logger + + +json_content_type = 'application/json' + +base_login_uri = 'https://climate.com/static/app-login/index.html' +token_uri = 'https://api.climate.com/api/oauth/token' +api_uri = 'https://platform.climate.com' + + +def login_uri(client_id, scopes, redirect_uri): + """ + Builds the URI for 'Log In with FieldView' link. + The redirect_uri is a uri on your system (this app) that will handle the + authorization once the user has authenticated with FieldView. + """ + params = { + 'scope': scopes, + 'page': 'oidcauthn', + 'response_type': 'code', + 'client_id': client_id, + 'redirect_uri': redirect_uri + } + return '{}?{}'.format(base_login_uri, urlencode(params)) + + +def authorization_header(client_id, client_secret): + """ + Builds the authorization header unique to your company or application. + :param client_id: Provided by Climate. + :param client_secret: Provided by Climate. + :return: Basic authorization header. + """ + pair = '{}:{}'.format(client_id, client_secret) + encoded = b64encode(pair.encode('ascii')).decode('ascii') + return 'Basic {}'.format(encoded) + + +def authorize(login_code, client_id, client_secret, redirect_uri): + """ + Exchanges the login code provided on the redirect request for an + access_token and refresh_token. Also gets user data. + :param login_code: Authorization code returned from Log In with FieldView + on redirect uri. + :param client_id: Provided by Climate. + :param client_secret: Provided by Climate. + :param redirect_uri: Uri to your redirect page. Needs to be the same as + the redirect uri provided in the initial Log In with FieldView request. + :return: Object containing user data, access_token and refresh_token. + """ + headers = { + 'authorization': authorization_header(client_id, client_secret), + 'content-type': 'application/x-www-form-urlencoded', + 'accept': 'application/json' + } + data = { + 'grant_type': 'authorization_code', + 'redirect_uri': redirect_uri, + 'code': login_code + } + res = requests.post(token_uri, headers=headers, data=urlencode(data)) + Logger().info(to_curl(res.request)) + if res.status_code == 200: + return res.json() + + Logger().error("Auth failed: %s" % res.status_code) + Logger().error("Auth failed: %s" % res.json()) + return None + + +def reauthorize(refresh_token, client_id, client_secret): + """ + Access_tokens expire after 4 hours. At any point before the end of that + period you may request a new access_token (and refresh_token) by submitting + a POST request to the /api/oauth/token end-point. Note that the data + submitted is slightly different than on initial authorization. Refresh + tokens are good for 30 days from their date of issue. Once this end-point + is called, the refresh token that is passed to this call is immediately set + to expired one hour from "now" and the newly issues refresh token will + expire 30 days from "now". Make sure to store the new refresh token so you + can use it in the future to get a new auth tokens as needed. If you lose + the refresh token there is no effective way to retrieve a new refresh token + without having the user log in again. + :param refresh_token: refresh_token supplied by initial + (or subsequent refresh) call. + :param client_id: Provided by Climate. + :param client_secret: Provided by Climate. + :return: Object containing user data, access_token and refresh_token. + """ + headers = { + 'authorization': authorization_header(client_id, client_secret), + 'content-type': 'application/x-www-form-urlencoded', + 'accept': 'application/json' + } + data = { + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token + } + res = requests.post(token_uri, headers=headers, data=urlencode(data)) + Logger().info(to_curl(res.request)) + if res.status_code == 200: + return res.json() + + log_http_error(res) + return None + + +def bearer_token(token): + """ + Returns content of authorization header to be provided on all non-auth + API calls. + :param token: access_token returned from authorization call. + :return: Formatted header. + """ + return 'Bearer {}'.format(token) + + +def log_http_error(response): + """ + Private function to log errors on server console + :param response: http response object. + """ + if response.status_code == 403: + Logger().error("Permission error, current scopes are - {}".format( + os.environ['CLIMATE_API_SCOPES'])) + elif response.status_code == 400: + Logger().error("Bad request - {}".format(response.text)) + elif response.status_code == 401: + Logger().error("Unauthorized - {}".format(response.text)) + elif response.status_code == 404: + Logger().error("Resource not found - {}".format(response.text)) + elif response.status_code == 416: + Logger().error("Range Not Satisfiable - {}".format(response.text)) + elif response.status_code == 500: + Logger().error("Internal server error - {}".format(response.text)) + elif response.status_code == 503: + Logger().error("Server busy - {}".format(response.text)) + + +def growingSeasons(field_id, token, api_key): + """ + Retrieve the growing seasons from Climate. + :param field_id: UUID of field to retrieve. + :param token: access_token + :param api_key: Provided by Climate + :return: JSON object with a contentId which is the growingSeasonsContentId + """ + uri = '{}/v5/growingSeasons'.format(api_uri) + headers = { + 'authorization': bearer_token(token), + 'x-api-key': api_key + } + data = { + 'fieldId': field_id + } + + res = requests.post(uri, headers=headers, json=data) + Logger().info(to_curl(res.request)) + + if res.status_code == 202: + return res.json()['contentId'] + log_http_error(res) + return False + + +def growingSeasonsContents(content_id, token, api_key): + """ + Retrieve the growing seasons contents from Climate. + :param content_id: UUID of growingSeasonsContentsId to retrieve. + :param token: access_token + :param api_key: Provided by Climate + :return: JSON object with a list of UUID of the growingSeasonsId and year + """ + uri = '{}/v5/growingSeasonsContents/{}'.format(api_uri, content_id) + headers = { + 'authorization': bearer_token(token), + 'x-api-key': api_key + } + res = requests.get(uri, headers=headers) + Logger().info(to_curl(res.request)) + + if res.status_code == 200: + pretty_json = json.loads(res.text) + return json.dumps(pretty_json, indent=4, sort_keys=True) + log_http_error(res) + return False + + +def harvestReports(field_id, seasons, token, api_key): + """ + Retrieve the harvest reports from Climate. + :param field_id: UUID of field to retrieve. + :param seasons: UUID of growingSeasonsId to retrieve. + :param token: access_token + :param api_key: Provided by Climate + :return: JSON object with a harvest report id + """ + uri = '{}/v5/harvestReports'.format(api_uri) + headers = { + 'authorization': bearer_token(token), + 'x-api-key': api_key + } + data = { + 'fieldId': field_id, + 'growingSeasons': seasons + } + + res = requests.post(uri, headers=headers, json=data) + Logger().info(to_curl(res.request)) + + if res.status_code == 202: + return res.json()['id'] + log_http_error(res) + return False + + +def harvestReportsContents(report_id, token, api_key): + """ + Retrieve the harvest reports contents from Climate. + :param report_id: UUID of harvest report id to retrieve. + :param token: access_token + :param api_key: Provided by Climate + :return: JSON object with a list harvest reports + """ + uri = '{}/v5/harvestReportsContents/{}'.format(api_uri, report_id) + headers = { + 'authorization': bearer_token(token), + 'x-api-key': api_key + } + res = requests.get(uri, headers=headers) + Logger().info(to_curl(res.request)) + + if res.status_code == 200: + pretty_json = json.loads(res.text) + return json.dumps(pretty_json, indent=4, sort_keys=True) + log_http_error(res) + return False diff --git a/v5/python/logger.py b/v5/python/logger.py new file mode 100644 index 0000000..2159ebe --- /dev/null +++ b/v5/python/logger.py @@ -0,0 +1,28 @@ +import logging +import sys + + +class Logger: + """ + Simple singleton class to encapsulate logging to the Flask app object so + we can have unified logging. + """ + instance = None + + def __new__(cls, logger=None): + if not Logger.instance: + if not logger: + raise ValueError("No logger specified on creation of Logger\ + singleton.") + Logger.instance = logger + logger.setLevel(logging.INFO) + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.INFO) + formatter = logging.Formatter('%(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + return Logger.instance + return Logger.instance + + def __getattr__(self, name): + return getattr(self.instance, name) diff --git a/v5/python/main.py b/v5/python/main.py new file mode 100644 index 0000000..5c8a32a --- /dev/null +++ b/v5/python/main.py @@ -0,0 +1,308 @@ +""" +Partner example app + +Start a simple app server: + - allow user login via Climate + - retrieve harvest activity + - retrieve growing seasons + +We use Flask in this example to provide a simple HTTP server. You will notice +that some of the functions in this file are decorated with @app.route() which +registers them with Flask as functions to service requests to the specified +URIs. + +This file (main.py) provides the web UI and framework for the demo app. All +the work with the Climate API happens in climate.py. + +Note: For this example, only one "user" can be logged into the example app at +a time. + +License: +Copyright © 2022 Climate, LLC +""" + +import json +import os +from logger import Logger + +from flask import Flask, request, redirect, url_for +import climate + +# Configuration of your Climate partner credentials. This assumes you have +# placed them in your environment. You may +# also choose to just hard code them here if you prefer. + +CLIMATE_API_ID = os.environ['CLIMATE_API_ID'] # OAuth2 client ID +CLIMATE_API_SECRET = os.environ['CLIMATE_API_SECRET'] # OAuth2 client secret +CLIMATE_API_SCOPES = os.environ['CLIMATE_API_SCOPES'] # Oauth2 scope list +CLIMATE_API_KEY = os.environ['CLIMATE_API_KEY'] # X-Api-Key header +# Partner app server + +app = Flask(__name__) +logger = Logger(app.logger) + +# User state - only one user at a time. In your application this would be +# handled by your session management and backing +# storage. + +_state = {} + + +def set_state(**kwargs): + global _state + if 'access_token' in kwargs: + _state['access_token'] = kwargs['access_token'] + if 'refresh_token' in kwargs: + _state['refresh_token'] = kwargs['refresh_token'] + if 'user' in kwargs: + _state['user'] = kwargs['user'] + + +def clear_state(): + set_state(access_token=None, refresh_token=None, user=None, fields=None) + + +def state(key): + global _state + return _state.get(key) + + +def page(content): + return """ +

Partner API Demo Site

+ {content} +

Return home

+ """.format(content=content, home=url_for('home')) + +# Routes + + +@app.route('/') +def home(): + if state('user'): + return user_homepage() + return no_user_homepage() + + +def no_user_homepage(): + """ + This is logically the first place a user will come. On your site it will + be some page where you present them with a link to Log In with FieldView. + The main thing here is that you provide a correctly formulated link with + the required parameters and correct button image. + :return: None + """ + url = climate.login_uri(CLIMATE_API_ID, CLIMATE_API_SCOPES, redirect_uri()) + + content = """ +

Welcome to the Climate Partner Demo App.

+

Imagine that this page is your great web application and you + want to connect it with Climate FieldView. To do this, you need + to let your users establish a secure connection between your app + and FieldView. You do this using Log In with FieldView.

+

+ FieldView Login

+ """.format(url) + return page(content) + + +def user_homepage(): + """ + This page just demonstrates some basic Climate FieldView API operations + such as getting field details, accessing user information and and + refreshing the authorization token. + :return: None + """ + content = """ +

User name retrieved from FieldView: {first} {last}

+

Access Token: {access_token}

+

Refresh Token: {refresh_token} + (Refresh)

+
+

Growing Seasons

+

Harvest Reports

+
+

Log out

+ """.format(first=state('user')['firstname'], + last=state('user')['lastname'], + access_token=state('access_token'), + refresh_token=state('refresh_token'), + refresh=url_for('refresh_token'), + logout=url_for('logout_redirect'), + growing_seasons=url_for('growing_seasons'), + harvest_reports=url_for('harvest_reports')) + return page(content) + + +@app.route('/login-redirect') +def login_redirect(): + """ + This is the page a user will come back to after having successfully logged + in with FieldView. The URI was provided as one of the parameters to the + login URI above. The "code" parameter in the URI's query string contains + the access_token and refresh_token. + :return: + """ + code = request.args['code'] + if code: + resp = climate.authorize(code, + CLIMATE_API_ID, + CLIMATE_API_SECRET, + redirect_uri()) + if resp: + # Store tokens and user in state for subsequent requests. + access_token = resp['access_token'] + refresh_token = resp['refresh_token'] + set_state(user=resp['user'], + access_token=access_token, + refresh_token=refresh_token) + + return redirect(url_for('home')) + + +def redirect_uri(): + """ + :return: Returns uri for redirection after Log In with FieldView. + """ + return url_for('login_redirect', _external=True) + + +@app.route('/refresh-token') +def refresh_token(): + """ + This route doesn't have any page associated with it; it just refreshes the + authorization token and redirects back to the home page. As a by-product, + this also refreshes the user data. + :return: + """ + resp = climate.reauthorize(state('refresh_token'), + CLIMATE_API_ID, + CLIMATE_API_SECRET) + if resp: + # Store tokens and user in state for subsequent requests. + access_token = resp['access_token'] + refresh_token = resp['refresh_token'] + set_state(user=resp['user'], access_token=access_token, + refresh_token=refresh_token) + + return redirect(url_for('home')) + + +@app.route('/logout-redirect') +def logout_redirect(): + """ + Clears all current user data. Does not make any Climate API calls. + :return: + """ + clear_state() + return redirect(url_for('home')) + + +@app.route('/growingSeasons', methods=['GET', 'POST']) +def growing_seasons(): + """ + Initially (when method=GET) render the growing seasons form to collect + information necessary for the growingSeasons request. When the form is + POSTed, invoke the actual Climate API. + :return: HTML form or the Growing Seasons Id + """ + if request.method == 'POST': + field_id = request.form['field_id'] + growing_seasons_id = climate.growingSeasons( + field_id, state('access_token'), CLIMATE_API_KEY) + growing_seasons_contents_url = url_for( + 'growing_seasons_contents', growing_seasons_id=growing_seasons_id) + content = """ +

Growing Seasons

+

Growing Seasons Ids: + {growing_seasons_id} + """.format( + growing_seasons_contents_url=growing_seasons_contents_url, + growing_seasons_id=growing_seasons_id) + return page(content) + content = """ +

Growing Seasons

+
+

Field ID:

+

+
+ """ + return page(content) + + +@app.route('/growingSeasonsContents/', methods=['GET']) +def growing_seasons_contents(growing_seasons_id): + """ + This page shows the growing seasons content id and year of the growing + season + :return: response from Climate's GrowingSeasonsConents API + """ + response = climate.growingSeasonsContents( + growing_seasons_id, state('access_token'), CLIMATE_API_KEY) + content = """ +

Growing Seasons Contents

+
{}
+ """.format(response) + return page(content) + + +@app.route('/harvestReports', methods=['GET', 'POST']) +def harvest_reports(): + """ + Initially (when method=GET) render the harvest reports form to collect + information necessary for the harvestReports request. When the form is + POSTed, invoke the actual Climate API. + :return: HTML form or the Harvest Reports Id + """ + if request.method == 'POST': + field_id = request.form['field_id'] + seasons = request.form['seasons'].replace(' ', '').split(',') + harvest_report_id = climate.harvestReports( + field_id, seasons, state('access_token'), CLIMATE_API_KEY) + harvest_report_contents_url = url_for( + 'harvest_report_contents', harvest_report_id=harvest_report_id) + content = """ +

Harvest Report

+

Harvest Report Id: + {harvest_report_id}

+

Return home

+ """.format(harvest_report_contents_url=harvest_report_contents_url, + harvest_report_id=harvest_report_id) + return page(content) + content = """ +

Harvest Report

+
+

Field ID:

+ + +

+
+ """ + return page(content) + + +@app.route('/harvestReportsContents/', methods=['GET']) +def harvest_report_contents(harvest_report_id): + """ + This page shows the harvest reports of the inputted growing seasons + :return: response from Climate's harvestReportsContents API + """ + response = climate.harvestReportsContents( + harvest_report_id, state('access_token'), CLIMATE_API_KEY) + content = """ +

Harvest Reports Contents

+
{response}
+ """.format(response=response) + return page(content) + + +# start app + + +if __name__ == '__main__': + clear_state() + app.run( + host="localhost", + port=8080 + ) diff --git a/v5/python/requirements.txt b/v5/python/requirements.txt new file mode 100644 index 0000000..f567ec4 --- /dev/null +++ b/v5/python/requirements.txt @@ -0,0 +1,15 @@ +autopep8==1.6.0 +certifi==2021.10.8 +charset-normalizer==2.0.12 +click==8.0.4 +curlify==2.2.1 +Flask==2.0.3 +idna==3.3 +itsdangerous==2.1.1 +Jinja2==3.0.3 +MarkupSafe==2.1.1 +pycodestyle==2.8.0 +requests==2.27.1 +toml==0.10.2 +urllib3==1.26.9 +Werkzeug==2.0.3 diff --git a/v5/python/static/fv-login-button.png b/v5/python/static/fv-login-button.png new file mode 100644 index 0000000..d20b7f8 Binary files /dev/null and b/v5/python/static/fv-login-button.png differ