flowfabricpy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lynker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowfabricpy
3
+ Version: 0.1.0
4
+ Summary: Python client for Flowfabric
5
+ Author-email: Paul Linza <plinza@lynker.com>, Mike Johnson <mjohnson@lynker.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: requests
11
+ Dynamic: license-file
12
+
13
+ # flowfabricpy: Effortless Python Access to FlowFabric API
14
+
15
+ `flowfabricpy` is a powerful Python client for the FlowFabric API, providing seamless access to hydrologic forecasts, reanalysis, rating curves, and datasets. With robust authentication and automatic token caching, you can focus on data science, not on data munging plumbing.
16
+
17
+ ## Key Features
18
+ - **Evolving catalog** of harmonized data from multiple models
19
+ - **Single method access** to all models - both retrospective and forecast
20
+ - **One-time authentication**: Log in once, and your token is cached for future use.
21
+ - **Arrow IPC support**: Fast, memory-efficient data transfer
22
+
23
+ ## Installation
24
+ ```py
25
+ # Install from GitHub:
26
+ pip install git+https://github.com/lynker-spatial/flowfabric-py#egg=flowfabricpy
27
+ ```
28
+
29
+ ## Quick Start
30
+ ```py
31
+ # 1. List available datasets
32
+ datasets = flowfabric_list_datasets()
33
+ print(datasets)
34
+
35
+ # 2. Query streamflow forecast (first call prompts login, then caches token)
36
+ # More on atuhentication below ...
37
+ tbl = flowfabric_streamflow_query(
38
+ dataset_id = "nws_owp_nwm_analysis",
39
+ feature_ids = ["101", "1001"],
40
+ issue_time = "latest"
41
+ )
42
+ print(tbl)
43
+
44
+ # 3. Query streamflow reanalysis data
45
+ tbl_re = flowfabric_streamflow_query(
46
+ "nws_owp_nwm_reanalysis_3_0",
47
+ feature_ids = ["101", "1001"],
48
+ start_time = "2018-01-01",
49
+ end_time = "2018-01-31"
50
+ )
51
+ print(tbl_re)
52
+
53
+ # 4. Query ratings
54
+ ratings = flowfabric_ratings_query(
55
+ feature_ids = ["101", "1001"],
56
+ type = "rem"
57
+ )
58
+ print(ratings)
59
+ ```
60
+
61
+ ## Authentication & Token Caching
62
+
63
+ Using this platform requires authentication via the Lynker Spatial Portal. Accounts are free to set up and use. If use exceeds costs we can burden we will reach out to power users to better understand how we can help.
64
+
65
+ To get access, users can create an account here: https://proxy.lynker-spatial.com/
66
+
67
+ The first API call will prompt you to log in via browser. Your token is then cached and reused for all future calls—no repeated browser prompts!
68
+
69
+ **Manual token refresh:**
70
+ ```py
71
+ flowfabric_refresh_token() # Forces re-authentication and updates cached token
72
+ ```
73
+
74
+ **Advanced:**
75
+ You can always pass a token explicitly:
76
+ ```py
77
+ token = flowfabric_get_token()['id_token']
78
+ healthz = flowfabric_healthz(token = token)
79
+ ```
80
+ ## Troubleshooting
81
+ - If you see repeated browser prompts, call `flowfabric_refresh_token()` once, then retry your queries.
82
+ - If you switch users, manually refresh the token.
83
+ - Use `verbose = TRUE` in any endpoint for detailed debug output.
84
+
85
+ ## Learn More
86
+ - See the vignettes for advanced usage, authentication, and custom queries.
87
+ - All API responses are Arrow tables for high-performance analytics.
@@ -0,0 +1,75 @@
1
+ # flowfabricpy: Effortless Python Access to FlowFabric API
2
+
3
+ `flowfabricpy` is a powerful Python client for the FlowFabric API, providing seamless access to hydrologic forecasts, reanalysis, rating curves, and datasets. With robust authentication and automatic token caching, you can focus on data science, not on data munging plumbing.
4
+
5
+ ## Key Features
6
+ - **Evolving catalog** of harmonized data from multiple models
7
+ - **Single method access** to all models - both retrospective and forecast
8
+ - **One-time authentication**: Log in once, and your token is cached for future use.
9
+ - **Arrow IPC support**: Fast, memory-efficient data transfer
10
+
11
+ ## Installation
12
+ ```py
13
+ # Install from GitHub:
14
+ pip install git+https://github.com/lynker-spatial/flowfabric-py#egg=flowfabricpy
15
+ ```
16
+
17
+ ## Quick Start
18
+ ```py
19
+ # 1. List available datasets
20
+ datasets = flowfabric_list_datasets()
21
+ print(datasets)
22
+
23
+ # 2. Query streamflow forecast (first call prompts login, then caches token)
24
+ # More on atuhentication below ...
25
+ tbl = flowfabric_streamflow_query(
26
+ dataset_id = "nws_owp_nwm_analysis",
27
+ feature_ids = ["101", "1001"],
28
+ issue_time = "latest"
29
+ )
30
+ print(tbl)
31
+
32
+ # 3. Query streamflow reanalysis data
33
+ tbl_re = flowfabric_streamflow_query(
34
+ "nws_owp_nwm_reanalysis_3_0",
35
+ feature_ids = ["101", "1001"],
36
+ start_time = "2018-01-01",
37
+ end_time = "2018-01-31"
38
+ )
39
+ print(tbl_re)
40
+
41
+ # 4. Query ratings
42
+ ratings = flowfabric_ratings_query(
43
+ feature_ids = ["101", "1001"],
44
+ type = "rem"
45
+ )
46
+ print(ratings)
47
+ ```
48
+
49
+ ## Authentication & Token Caching
50
+
51
+ Using this platform requires authentication via the Lynker Spatial Portal. Accounts are free to set up and use. If use exceeds costs we can burden we will reach out to power users to better understand how we can help.
52
+
53
+ To get access, users can create an account here: https://proxy.lynker-spatial.com/
54
+
55
+ The first API call will prompt you to log in via browser. Your token is then cached and reused for all future calls—no repeated browser prompts!
56
+
57
+ **Manual token refresh:**
58
+ ```py
59
+ flowfabric_refresh_token() # Forces re-authentication and updates cached token
60
+ ```
61
+
62
+ **Advanced:**
63
+ You can always pass a token explicitly:
64
+ ```py
65
+ token = flowfabric_get_token()['id_token']
66
+ healthz = flowfabric_healthz(token = token)
67
+ ```
68
+ ## Troubleshooting
69
+ - If you see repeated browser prompts, call `flowfabric_refresh_token()` once, then retry your queries.
70
+ - If you switch users, manually refresh the token.
71
+ - Use `verbose = TRUE` in any endpoint for detailed debug output.
72
+
73
+ ## Learn More
74
+ - See the vignettes for advanced usage, authentication, and custom queries.
75
+ - All API responses are Arrow tables for high-performance analytics.
@@ -0,0 +1,104 @@
1
+ .. flowfabricpy documentation master file, created by
2
+ sphinx-quickstart on Wed Feb 11 13:17:49 2026.
3
+ You can adapt this file completely to your liking, but it should at least
4
+ contain the root `toctree` directive.
5
+
6
+ flowfabricpy: Effortless Python Access to FlowFabric API
7
+ ========================================================
8
+
9
+ **flowfabricpy** is a powerful Python client for the FlowFabric API, providing seamless access to hydrologic forecasts, reanalysis, rating curves, and datasets. With robust authentication and automatic token caching, you can focus on data science, not on data munging plumbing.
10
+
11
+ Key Features
12
+ ------------
13
+ - **Evolving catalog** of harmonized data from multiple models
14
+ - **Single method access** to all models - both retrospective and forecast
15
+ - **One-time authentication**: Log in once, and your token is cached for future use.
16
+ - **Arrow IPC support**: Fast, memory-efficient data transfer
17
+
18
+ Installation
19
+ ------------
20
+ .. code-block:: python
21
+
22
+ # Install from GitHub:
23
+ pip install git+https://github.com/lynker-spatial/flowfabric-py#egg=flowfabricpy
24
+
25
+ Quick Start
26
+ -----------
27
+ .. code-block:: python
28
+
29
+ # 1. List available datasets
30
+ datasets = flowfabric_list_datasets()
31
+ print(datasets)
32
+
33
+ # 2. Query streamflow forecast (first call prompts login, then caches token)
34
+ # More on atuhentication below ...
35
+ tbl = flowfabric_streamflow_query(
36
+ dataset_id = "nws_owp_nwm_analysis",
37
+ feature_ids = ["101", "1001"],
38
+ issue_time = "latest"
39
+ )
40
+ print(tbl)
41
+
42
+ # 3. Query streamflow reanalysis data
43
+ tbl_re = flowfabric_streamflow_query(
44
+ "nws_owp_nwm_reanalysis_3_0",
45
+ feature_ids = ["101", "1001"],
46
+ start_time = "2018-01-01",
47
+ end_time = "2018-01-31"
48
+ )
49
+ print(tbl_re)
50
+
51
+ # 4. Query ratings
52
+ ratings = flowfabric_ratings_query(
53
+ feature_ids = ["101", "1001"],
54
+ type = "rem"
55
+ )
56
+ print(ratings)
57
+
58
+ Authentication & Token Caching
59
+ ------------------------------
60
+
61
+ Using this platform requires authentication via the Lynker Spatial Portal. Accounts are free to set up and use. If use exceeds costs we can burden we will reach out to power users to better understand how we can help.
62
+
63
+ To get access, users can create an account here: https://proxy.lynker-spatial.com/
64
+
65
+ The first API call will prompt you to log in via browser. Your token is then cached and reused for all future calls—no repeated browser prompts!
66
+
67
+
68
+ **Manual token refresh:**
69
+
70
+ .. code-block:: python
71
+
72
+ flowfabric_refresh_token() # Forces re-authentication and updates cached token
73
+
74
+
75
+ **Advanced:**
76
+ You can always pass a token explicitly:
77
+
78
+ .. code-block:: python
79
+
80
+ token = flowfabric_get_token()['id_token']
81
+ healthz = flowfabric_healthz(token = token)
82
+
83
+ Troubleshooting
84
+ ---------------
85
+ - If you see repeated browser prompts, call `flowfabric_refresh_token()` once, then retry your queries.
86
+ - If you switch users, manually refresh the token.
87
+ - Use `verbose = TRUE` in any endpoint for detailed debug output.
88
+
89
+ Learn More
90
+ ----------
91
+ - See the vignettes for advanced usage, authentication, and custom queries.
92
+ - All API responses are Arrow tables for high-performance analytics.
93
+
94
+ .. toctree::
95
+ :maxdepth: 2
96
+ :caption: Contents:
97
+
98
+ Contents
99
+ --------
100
+ .. toctree::
101
+ getting-started
102
+ authentication
103
+ advanced
104
+
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flowfabricpy"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.8"
9
+ dependencies = ["requests"]
10
+ description = "Python client for Flowfabric"
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ authors = [{name = "Paul Linza", email = "plinza@lynker.com"}, {name = "Mike Johnson", email = "mjohnson@lynker.com"}]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,34 @@
1
+ # __init__.py
2
+ from .auth import flowfabric_get_token, flowfabric_refresh_token
3
+ from .catalog_utils import auto_streamflow_params
4
+ from .flowfabric_http import flowfabric_get, flowfabric_post
5
+ from .client import (
6
+ flowfabric_list_datasets,
7
+ flowfabric_get_dataset,
8
+ flowfabric_get_latest_run,
9
+ flowfabric_streamflow_query,
10
+ flowfabric_streamflow_estimate,
11
+ flowfabric_ratings_query,
12
+ flowfabric_ratings_estimate,
13
+ flowfabric_stage_query,
14
+ flowfabric_healthz,
15
+ flowfabric_inundation_ids,
16
+ get_bearer_token
17
+ )
18
+
19
+ __all__ = ['flowfabric_get_token',
20
+ 'flowfabric_refresh_token',
21
+ 'auto_streamflow_params',
22
+ 'flowfabric_list_datasets',
23
+ 'flowfabric_get',
24
+ 'flowfabric_post',
25
+ 'flowfabric_get_dataset',
26
+ 'flowfabric_get_latest_run',
27
+ 'flowfabric_streamflow_query',
28
+ 'flowfabric_streamflow_estimate',
29
+ 'flowfabric_ratings_query',
30
+ 'flowfabric_ratings_estimate',
31
+ 'flowfabric_stage_query',
32
+ 'flowfabric_healthz',
33
+ 'flowfabric_inundation_ids',
34
+ 'get_bearer_token']
@@ -0,0 +1,176 @@
1
+ # auth.py
2
+ """
3
+ Functions for getting an authentication token
4
+ """
5
+ import json
6
+ import os
7
+ import threading
8
+ import webbrowser
9
+ import time
10
+ from threading import Lock
11
+ from pathlib import Path
12
+
13
+ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
14
+ token_file = Path.home() / ".flowfabric_token.json"
15
+
16
+ import base64
17
+ import hashlib
18
+ import secrets
19
+ import requests
20
+ from requests_oauthlib import OAuth2Session
21
+ from http.server import BaseHTTPRequestHandler, HTTPServer
22
+ from urllib.parse import urlparse, parse_qs
23
+
24
+ # token lock
25
+ token_lock = Lock()
26
+
27
+ # helper function to load token
28
+ def load_token():
29
+ """
30
+ Helper function to load a token. Do not call directly.
31
+ :return:
32
+ """
33
+ if not token_file.exists():
34
+ return None
35
+ try:
36
+ with token_file.open("r") as f:
37
+ return json.load(f)
38
+ except Exception:
39
+ return None
40
+
41
+ # helper function to save the token
42
+ def save_token(token):
43
+ """
44
+ Helper function to save a token. Do not call directly.
45
+ :param token: Token
46
+ :type token: dict
47
+ :return:
48
+ """
49
+ token_file.parent.mkdir(parents=True, exist_ok=True)
50
+ with token_file.open("w") as f:
51
+ json.dump(token, f)
52
+
53
+ # helper function
54
+ # only returns True if both the expiration time & token exist & are not expired
55
+ def token_is_valid(token):
56
+ """
57
+ Helper function to determine the validity of a token. Do not call directly.
58
+ :return: Bool representing validity
59
+ """
60
+ if not token:
61
+ return False
62
+
63
+ expires_at = token.get("expires_at")
64
+ if not expires_at:
65
+ return False
66
+
67
+ return expires_at - 60 > time.time() # extra time as a buffer
68
+
69
+ # handle the OAuth callback
70
+ class OAuthCallbackHandler(BaseHTTPRequestHandler):
71
+ """
72
+ Class to handle the OAuth callback
73
+ """
74
+ auth_code = None
75
+ auth_state = None
76
+
77
+ def do_GET(self):
78
+ query = parse_qs(urlparse(self.path).query)
79
+ OAuthCallbackHandler.auth_code = query.get("code", [None])[0]
80
+ OAuthCallbackHandler.auth_state = query.get("state", [None])[0]
81
+
82
+ self.send_response(200)
83
+ self.end_headers()
84
+ self.wfile.write(b"Authentication complete. You may close this window.")
85
+
86
+ threading.Thread(target=self.server.shutdown).start()
87
+
88
+ # login and get a token
89
+ def flowfabric_get_token(force_refresh=False):
90
+ """
91
+ Get and cache an authentication token
92
+
93
+ :param force_refresh: (Optional) force refresh token
94
+ :type force_refresh: bool or None
95
+
96
+ :return: The cached authentication token
97
+ """
98
+ # dumps the cached token
99
+ if force_refresh:
100
+ token_file.unlink(missing_ok=True)
101
+
102
+ # return current token if it is still valid
103
+ token = load_token()
104
+ if token_is_valid(token):
105
+ return token
106
+
107
+ # otherwise generate a new token
108
+ with token_lock:
109
+ provider = requests.get(
110
+ "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_em0hAPqnS/.well-known/openid-configuration"
111
+ )
112
+ json_prov = provider.json()
113
+
114
+ client_id = "1he6ti5109b9t6r1ifd4brecpl"
115
+ redirect_uri = "http://localhost:57777"
116
+ scope = ["openid", "profile", "email", "phone"]
117
+
118
+ # PKCE
119
+ code_verifier = secrets.token_urlsafe(64)
120
+ code_challenge = base64.urlsafe_b64encode(
121
+ hashlib.sha256(code_verifier.encode()).digest()
122
+ ).decode().rstrip("=")
123
+
124
+ oauth = OAuth2Session(
125
+ client_id=client_id,
126
+ redirect_uri=redirect_uri,
127
+ scope=scope,
128
+ )
129
+
130
+ authorization_url, state = oauth.authorization_url(
131
+ json_prov["authorization_endpoint"],
132
+ code_challenge=code_challenge,
133
+ code_challenge_method="S256",
134
+ )
135
+
136
+ # Start callback server
137
+ server = HTTPServer(("localhost", 57777), OAuthCallbackHandler)
138
+ thread = threading.Thread(target=server.serve_forever)
139
+ thread.daemon = True
140
+ thread.start()
141
+
142
+ webbrowser.open(authorization_url)
143
+
144
+ timeout = time.time() + 60
145
+ # Wait for redirect
146
+ while OAuthCallbackHandler.auth_code is None:
147
+ if time.time() > timeout:
148
+ server.shutdown()
149
+ thread.join()
150
+ server.server_close()
151
+ time.sleep(0.1)
152
+
153
+ server.shutdown()
154
+ thread.join()
155
+ server.server_close()
156
+
157
+ token = oauth.fetch_token(
158
+ json_prov["token_endpoint"],
159
+ code=OAuthCallbackHandler.auth_code,
160
+ code_verifier=code_verifier,
161
+ include_client_id=True,
162
+ )
163
+
164
+ provider.close()
165
+ save_token(token)
166
+ return token
167
+
168
+ # refresh token
169
+ def flowfabric_refresh_token():
170
+ """
171
+ Force refresh the cached token
172
+
173
+ :return: The new token
174
+ """
175
+ token = flowfabric_get_token(force_refresh=True)
176
+ return token
@@ -0,0 +1,70 @@
1
+ # catalog_utils.py
2
+ """
3
+ Contains a function to get recommended streamflow parameters.
4
+ """
5
+ import requests
6
+ import pandas as pd
7
+
8
+ # autofill streamflow parameters
9
+ def auto_streamflow_params(dataset_id):
10
+ """
11
+ Auto-generate streamflow parameters for a given dataset
12
+
13
+ :param dataset_id: The id of the dataset
14
+ :type dataset_id: str
15
+
16
+ :return: Dictionary containing the recommended streamflow parameters
17
+ """
18
+ catalog = requests.get("https://flowfabric.lynker-spatial.com/catalog")
19
+ json_data = catalog.json()['provider_groups']
20
+ df = pd.json_normalize(json_data, record_path="datasets")
21
+ datasets = df.to_dict(orient='records')
22
+ data = next((dataset for dataset in datasets if dataset.get("id") == dataset_id), None)
23
+
24
+ if data is None:
25
+ print("[auto_streamflow_params] Error: dataset not found")
26
+ return None
27
+
28
+ # determine if it is a reanalysis or not
29
+ is_reanalysis = False
30
+ if 'query_mode' in data and data['query_mode'] == 'absolute':
31
+ is_reanalysis = True
32
+ if 'configuration' in data and 'reanalysis' in data['configuration']:
33
+ is_reanalysis = True
34
+
35
+ if is_reanalysis:
36
+ if 'default_start_time' in data:
37
+ start_time = data['default_start_time']
38
+ elif 'min_time' in data:
39
+ start_time = data['min_time']
40
+ else:
41
+ start_time = "2018-01-01T00:00:00Z"
42
+
43
+ if 'default_end_time' in data:
44
+ end_time = data['default_end_time']
45
+ elif 'max_time' in data:
46
+ end_time = data['max_time']
47
+ else:
48
+ end_time = "2018-01-31T23:59:59Z"
49
+
50
+ params = {
51
+ 'query_mode': 'absolute',
52
+ 'start_time': start_time,
53
+ 'end_time': end_time,
54
+ 'scope': data['default_scope'] if 'default_scope' in data else 'all',
55
+ 'format': data['default_format'] if 'default_format' in data else 'json',
56
+ 'mode': data['default_mode'] if 'default_mode' in data else 'sync',
57
+ }
58
+ else:
59
+ params = {
60
+ 'query_mode': 'run',
61
+ 'issue_time': data['issue_time'] if 'issue_time' in data else 'latest',
62
+ 'scope': data['default_scope'] if 'default_scope' in data else 'all',
63
+ 'lead_start': data['lead_start'] if 'lead_start' in data else 0,
64
+ 'lead_end': data['lead_end'] if 'lead_end' in data else 0,
65
+ 'format': data['default_format'] if 'default_format' in data else 'json',
66
+ 'mode': data['default_mode'] if 'default_mode' in data else 'sync',
67
+ }
68
+
69
+ catalog.close()
70
+ return params