mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-29 02:32:05 +00:00
Multi select upstream providers to send spots to #95
This commit is contained in:
+19
-17
@@ -23,6 +23,7 @@ info:
|
||||
* **Breaking change:** POST `/spot` now expects `activities` (a list) and `activity_refs` in the `spot` object, rather than `sig` and `sig_refs`.
|
||||
* **Breaking change:** In the `/options` response, `sigs` has been renamed to `activities`, and within each activity, `sig_type` has been renamed to `activity_type`.
|
||||
* **Breaking change:** In the `/status` response, `sig_ref_data_providers` has been renamed to `activity_ref_data_providers`, and within each provider, `sig_name` has been renamed to `activity_name`.
|
||||
* **Breaking change:** In the POST `/spot` `handling` object, `submit_upstream` and `upstream_provider` have been replaced by `upstream_providers`, a list of provider names, so a spot can be sent to multiple upstream providers at once. `upstream_credentials` is now a map of provider name to that provider's credentials.
|
||||
|
||||
#### Upgrading a client from v2 to v3 API endpoints
|
||||
|
||||
@@ -940,6 +941,7 @@ components:
|
||||
- BIWOTA
|
||||
- COTA
|
||||
- PGA
|
||||
- Railways
|
||||
- Toilets
|
||||
example: POTA
|
||||
|
||||
@@ -1381,28 +1383,28 @@ components:
|
||||
description: >
|
||||
Optional server-side instructions for how to process this spot submission.
|
||||
properties:
|
||||
submit_upstream:
|
||||
type: boolean
|
||||
description: >
|
||||
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather than only adding it
|
||||
to this Spothole server. Requires `upstream_provider` to be set. Check `spot_submit_providers` in the
|
||||
`/options` response to see which activities and providers support this.
|
||||
default: false
|
||||
upstream_provider:
|
||||
upstream_providers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: >
|
||||
Name of the upstream provider to submit the spot to, e.g. "POTA" or "SOTA". Must
|
||||
match one of the provider names returned in `spot_submit_providers` for the chosen activity.
|
||||
example: POTA
|
||||
Names of upstream providers to forward the spot to (e.g. "POTA", "SOTA"), in addition to or instead
|
||||
of only adding it to this Spothole server. Each name must match one of the provider names returned in
|
||||
`spot_submit_providers` in the `/options` response for the chosen activity. Omit or leave empty to
|
||||
add the spot to this Spothole server only.
|
||||
example: [ POTA, ParksNPeaks ]
|
||||
upstream_credentials:
|
||||
type: object
|
||||
description: >
|
||||
Provider-specific credentials required to authenticate the upstream submission.
|
||||
The required keys depend on the provider. Credentials are used only for the upstream
|
||||
call and are never stored by Spothole.
|
||||
A map of provider name to the provider-specific credentials required to authenticate the upstream
|
||||
submission to that provider. The required keys depend on the provider. Credentials are used only for
|
||||
the upstream call and are never stored by Spothole.
|
||||
additionalProperties:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
example:
|
||||
ParksNPeaks:
|
||||
user_id: "12345"
|
||||
api_key: "abc123"
|
||||
captcha_token:
|
||||
@@ -2294,9 +2296,9 @@ components:
|
||||
type: object
|
||||
description: >
|
||||
A map of activity name to a list of provider names that support upstream spot submission for that
|
||||
activity. If an activity appears as a key here, the POST /spot endpoint accepts `submit_upstream: true`
|
||||
for spots with that activity, and will forward the spot to one of the listed providers. Omitted if no
|
||||
providers support upstream submission.
|
||||
activity. If an activity appears as a key here, the POST /spot endpoint accepts these provider names in
|
||||
`upstream_providers` for spots with that activity, and will forward the spot to any of the listed
|
||||
providers. Omitted if no providers support upstream submission.
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
|
||||
+50
-42
@@ -63,46 +63,46 @@ function updateUpstreamArea() {
|
||||
|
||||
$("#upstream-area").show();
|
||||
|
||||
// Update the provider selector
|
||||
$("#upstream-provider-select").empty();
|
||||
// Rebuild the provider checkboxes, keeping any that were already checked for the previous activity
|
||||
const previouslyChecked = getSelectedUpstreamProviders();
|
||||
const container = $("#upstream-providers").empty();
|
||||
$.each(providers, function (i, name) {
|
||||
$("#upstream-provider-select").append($('<option>', {value: name, text: name}));
|
||||
const id = "submit-upstream-" + i;
|
||||
const row = $("<div class='row g-2 align-items-center mb-1'></div>");
|
||||
const check = $("<div class='form-check'></div>");
|
||||
check.append($("<input class='form-check-input upstream-provider-check' type='checkbox'>")
|
||||
.attr("id", id).val(name).prop("checked", previouslyChecked.includes(name)));
|
||||
check.append($("<label class='form-check-label'></label>").attr("for", id).text(name));
|
||||
row.append($("<div class='col-auto'></div>").append(check));
|
||||
|
||||
// Add a credentials button if this provider has an authentication mechanism and we need input from the user
|
||||
if (PROVIDER_CREDENTIAL_SCHEMAS[name]) {
|
||||
const button = $("<button type='button' class='btn btn-sm btn-outline-secondary'></button>")
|
||||
.attr("aria-label", "Credentials for " + name)
|
||||
.html("<i class='fa-solid fa-key'></i> Credentials")
|
||||
.click(() => showCredentialsModal(name));
|
||||
row.append($("<div class='col-auto'></div>").append(button));
|
||||
}
|
||||
container.append(row);
|
||||
});
|
||||
|
||||
if (providers.length > 1) {
|
||||
$("#upstream-provider-label").text("upstream spot sources:");
|
||||
$("#upstream-provider-select-col").show();
|
||||
} else {
|
||||
$("#upstream-provider-label").text(providers[0]);
|
||||
$("#upstream-provider-select-col").hide();
|
||||
updateSpotholeOnlyCheck();
|
||||
}
|
||||
|
||||
// Show the credentials button if this provider has an authentication mechanism and we need input from the user
|
||||
updateCredentialsButton();
|
||||
// The "Spothole only" checkbox is checked when no upstream providers are selected
|
||||
function updateSpotholeOnlyCheck() {
|
||||
$("#spothole-only").prop("checked", getSelectedUpstreamProviders().length === 0);
|
||||
}
|
||||
|
||||
// Update the credentials button visibility based on selected provider
|
||||
function updateCredentialsButton() {
|
||||
const providerName = getSelectedUpstreamProvider();
|
||||
if (providerName && PROVIDER_CREDENTIAL_SCHEMAS[providerName]) {
|
||||
$("#upstream-credentials-btn").show();
|
||||
} else {
|
||||
$("#upstream-credentials-btn").hide();
|
||||
}
|
||||
// Get the names of the upstream providers the user has chosen to send the spot to
|
||||
function getSelectedUpstreamProviders() {
|
||||
return $(".upstream-provider-check:checked").map(function () {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
}
|
||||
|
||||
// Get the currently selected upstream provider name
|
||||
function getSelectedUpstreamProvider() {
|
||||
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
|
||||
if (providers.length === 0) return null;
|
||||
if (providers.length === 1) return providers[0];
|
||||
return $("#upstream-provider-select").val();
|
||||
}
|
||||
|
||||
// Show the credentials modal for the currently selected upstream provider
|
||||
function showCredentialsModal() {
|
||||
const providerName = getSelectedUpstreamProvider();
|
||||
if (!providerName || !PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
|
||||
// Show the credentials modal for the given upstream provider
|
||||
function showCredentialsModal(providerName) {
|
||||
if (!PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
|
||||
|
||||
const schema = PROVIDER_CREDENTIAL_SCHEMAS[providerName];
|
||||
const stored = loadCredentials(providerName);
|
||||
@@ -187,12 +187,13 @@ function addSpot() {
|
||||
}
|
||||
|
||||
// Upstream submission
|
||||
const submitUpstream = $("#submit-upstream").is(":checked");
|
||||
const upstreamProviderName = getSelectedUpstreamProvider();
|
||||
if (submitUpstream && upstreamProviderName) {
|
||||
handling["submit_upstream"] = true;
|
||||
handling["upstream_provider"] = upstreamProviderName;
|
||||
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
|
||||
const upstreamProviders = getSelectedUpstreamProviders();
|
||||
if (upstreamProviders.length > 0) {
|
||||
handling["upstream_providers"] = upstreamProviders;
|
||||
handling["upstream_credentials"] = {};
|
||||
$.each(upstreamProviders, function (i, name) {
|
||||
handling["upstream_credentials"][name] = loadCredentials(name);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax("/api/v3/spot", {
|
||||
@@ -262,14 +263,21 @@ $("#mode").change(function () {
|
||||
$(this).val($(this).val().trim().toUpperCase());
|
||||
});
|
||||
|
||||
// Update upstream area and credentials button when activity changes
|
||||
// Update upstream area when activity changes
|
||||
$("#activity").change(function () {
|
||||
updateUpstreamArea();
|
||||
});
|
||||
|
||||
// Update credentials button when provider selector changes
|
||||
$("#upstream-provider-select").change(function () {
|
||||
updateCredentialsButton();
|
||||
// Keep the "Spothole only" checkbox in sync with the upstream provider checkboxes
|
||||
$("#upstream-providers").on("change", ".upstream-provider-check", function () {
|
||||
updateSpotholeOnlyCheck();
|
||||
});
|
||||
|
||||
// Clicking "Spothole only" deselects all upstream providers. It can't be unchecked directly, as that would leave
|
||||
// nowhere to send the spot; the user unchecks it implicitly by choosing an upstream provider instead.
|
||||
$("#spothole-only").change(function () {
|
||||
$(".upstream-provider-check").prop("checked", false);
|
||||
updateSpotholeOnlyCheck();
|
||||
});
|
||||
|
||||
// Startup
|
||||
|
||||
+11
-23
@@ -71,6 +71,16 @@
|
||||
<input type="text" class="form-control storeable-text input-narrow" id="de-call"
|
||||
placeholder="N0CALL" required>
|
||||
</div>
|
||||
|
||||
<div id="upstream-area" class="mt-3" style="display:none;">
|
||||
<div class="form-label">Spot to:</div>
|
||||
<div class="form-check mb-1">
|
||||
<input class="form-check-input" type="checkbox" id="spothole-only" checked>
|
||||
<label class="form-check-label" for="spothole-only">Spothole only</label>
|
||||
</div>
|
||||
<div id="upstream-providers"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="submit" id="add-spot-submit" class="btn btn-primary mt-2em">
|
||||
<span id="add-spot-spinner" class="spinner-border spinner-border-sm me-1" aria-hidden="true"
|
||||
@@ -80,28 +90,6 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="upstream-area" class="mt-3" style="display:none;">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="submit-upstream">
|
||||
<label class="form-check-label" for="submit-upstream">
|
||||
Send spot to <span id="upstream-provider-label"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" id="upstream-provider-select-col" style="display:none;">
|
||||
<select id="upstream-provider-select" class="form-select form-select-sm"></select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="upstream-credentials-btn"
|
||||
onclick="showCredentialsModal();" style="display:none;">
|
||||
<i class="fa-solid fa-key"></i> Credentials
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recaptcha-area" class="mt-3" style="display:none;">
|
||||
<div id="recaptcha-widget"></div>
|
||||
</div>
|
||||
@@ -142,7 +130,7 @@
|
||||
const ALLOW_UPSTREAM_SPOTTING = {% raw safe_json_dumps(web_ui_options["allow_upstream_spotting"]) %};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1790514703"></script>
|
||||
<script src="/static/js/add-spot.js?v=1790516268"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1790514703"></script>
|
||||
<script src="/static/js/alerts.js?v=1790516268"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
const BANDS = {% raw safe_json_dumps(options["bands"]) %};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790514703"></script>
|
||||
<script src="/static/js/bands.js?v=1790514703"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790516268"></script>
|
||||
<script src="/static/js/bands.js?v=1790516268"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790514702" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790516268" type="text/css">
|
||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||
@@ -16,10 +16,10 @@
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
</script>
|
||||
|
||||
<script src="/static/js/utils.js?v=1790514702"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790514702"></script>
|
||||
<script src="/static/js/geo.js?v=1790514702"></script>
|
||||
<script src="/static/js/common.js?v=1790514702"></script>
|
||||
<script src="/static/js/utils.js?v=1790516268"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790516268"></script>
|
||||
<script src="/static/js/geo.js?v=1790516268"></script>
|
||||
<script src="/static/js/common.js?v=1790516268"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||
<script src="/static/js/conditions.js?v=1790514702"></script>
|
||||
<script src="/static/js/conditions.js?v=1790516268"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
modelling software, and other online tools on which Spothole's data is based. The vast majority of these are not
|
||||
profit-seeking and are made purely for the love of the hobby and to help others in the community. Spothole is
|
||||
standing on the shoulders of giants, who deserve a huge amount of thanks for all the work they put in.</p>
|
||||
<p>The following people have specifically helped develop Spothole either by contributing code, requesting features,
|
||||
or helping squash bugs: Steven M1SDH, Dan S50U, Matt W1CDN, Norby LX1NO, Andrew VK3ARR, Mario DL4MFM, Mark M5TEA,
|
||||
Michael G7VJR, Rob G7LAS, Ed DD5LP, Raph F4LUB, Luc ON7KEC, Richard GD4OFB, Daniel PY2TDB, Alan VK1AO,
|
||||
Andrew WK1AD, Ullrich DF5WC, Wayne N3CDF, Jonathan G4IVV, Erik N2EPE, Sorin YO9TSN, Matt HB9HWI, Priit ES1TEB,
|
||||
Leigh KG7WED, Jouni OH3CUF, Onno VK6FLAB, and Bruce WA7BNM. (If I've forgotten you, let me know!)</p>
|
||||
<p>The following people have specifically helped develop Spothole either by contributing code, allowing access to
|
||||
their data, providing guidance, requesting features, or helping squash bugs: Steven M1SDH, Dan S50U, Matt W1CDN,
|
||||
Norby LX1NO, Andrew VK3ARR, Mario DL4MFM, Mark M5TEA, Michael G7VJR, Rob G7LAS, Ed DD5LP, Raph F4LUB,
|
||||
Luc ON7KEC, Richard GD4OFB, Daniel PY2TDB, Alan VK1AO, Andrew WK1AD, Ullrich DF5WC, Wayne N3CDF, Jonathan G4IVV,
|
||||
Erik N2EPE, Sorin YO9TSN, Matt HB9HWI, Priit ES1TEB, Leigh KG7WED, Jouni OH3CUF, Onno VK6FLAB, Bruce WA7BNM, and
|
||||
Larry F5PYI. (If I've forgotten you, let me know!)</p>
|
||||
<p>Spothole is also dependent on a number of Python libraries, such as pyhamtools, and many JavaScript
|
||||
libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set, and MIT-licenced
|
||||
GeoJSON files for CQ and ITU zones from HA8TKS.</p>
|
||||
|
||||
+2
-2
@@ -115,8 +115,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790514702"></script>
|
||||
<script src="/static/js/map.js?v=1790514702"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790516267"></script>
|
||||
<script src="/static/js/map.js?v=1790516267"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -127,8 +127,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790514702"></script>
|
||||
<script src="/static/js/spots.js?v=1790514702"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790516267"></script>
|
||||
<script src="/static/js/spots.js?v=1790516267"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1790514703"></script>
|
||||
<script src="/static/js/status.js?v=1790516268"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
@@ -78,9 +79,9 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
handling = json_body.get("handling", {})
|
||||
|
||||
# Extract individual parameters that say how this spot should be handled by the server
|
||||
submit_upstream = handling.get("submit_upstream", False)
|
||||
upstream_provider_name = handling.get("upstream_provider", None)
|
||||
upstream_credentials = handling.get("upstream_credentials", {})
|
||||
upstream_provider_names = handling.get("upstream_providers", None) or []
|
||||
upstream_credentials = handling.get("upstream_credentials", None) or {}
|
||||
submit_upstream = len(upstream_provider_names) > 0
|
||||
captcha_token = handling.get("captcha_token", None)
|
||||
|
||||
# Verify CAPTCHA if required
|
||||
@@ -166,13 +167,18 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
|
||||
# Validate upstream submission requirements
|
||||
if submit_upstream and upstream_provider_name:
|
||||
is_cluster = isinstance(self._find_provider(upstream_provider_name, spot.activities), DXCluster)
|
||||
is_tiles = isinstance(self._find_provider(upstream_provider_name, spot.activities), Tiles)
|
||||
# Validate upstream submission requirements for each requested provider
|
||||
for upstream_provider_name in upstream_provider_names:
|
||||
provider = self._find_provider(upstream_provider_name, spot.activities)
|
||||
is_cluster = isinstance(provider, DXCluster)
|
||||
is_tiles = isinstance(provider, Tiles)
|
||||
if not spot.activity_refs and not is_tiles and not is_cluster:
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - an activity reference is required to submit upstream."))
|
||||
self.write(
|
||||
safe_json_dumps(
|
||||
f"Error - an activity reference is required to submit upstream to {upstream_provider_name}."
|
||||
)
|
||||
)
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
@@ -191,46 +197,27 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
|
||||
# Submit upstream if requested
|
||||
upstream_warning = None
|
||||
if submit_upstream and upstream_provider_name:
|
||||
provider = self._find_provider(upstream_provider_name, spot.activities)
|
||||
if provider:
|
||||
try:
|
||||
# Submit spot to the upstream provider. Run in a separate thread otherwise this blocks the whole
|
||||
# web server for everyone!
|
||||
await IOLoop.current().run_in_executor(None, provider.submit_spot, spot, upstream_credentials)
|
||||
# Trigger a re-poll after 3 second so the spot appears quickly. (Submitting to a cluster node is
|
||||
# slower than this, but we get data as a live stream from cluster anyway, so force_poll does
|
||||
# nothing in that case. This is really just for the HTTP providers when we submit a spot to them)
|
||||
threading.Timer(3.0, provider.force_poll).start()
|
||||
except NotImplementedError as e:
|
||||
upstream_warning = str(e)
|
||||
except SpotSubmissionError as e:
|
||||
logger.warning(f"Upstream submission to {upstream_provider_name} was not accepted: {e}")
|
||||
upstream_warning = (
|
||||
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed: {e}"
|
||||
# Submit upstream to all requested providers in parallel, collecting any warnings
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
self._submit_upstream(name, spot, upstream_credentials.get(name, {}))
|
||||
for name in upstream_provider_names
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
|
||||
upstream_warning = (
|
||||
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
|
||||
)
|
||||
elif spot.activities:
|
||||
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {', '.join(spot.activities)} spots."
|
||||
else:
|
||||
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for spots with no activity."
|
||||
upstream_warnings = [w for w in results if w]
|
||||
any_upstream_succeeded = len(upstream_warnings) < len(upstream_provider_names)
|
||||
|
||||
# If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
|
||||
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
||||
# we were but it failed, we should still add it to our database anyway.
|
||||
if not submit_upstream or upstream_warning:
|
||||
# If we successfully submitted the spot to at least one upstream provider, don't add it direct to Spothole,
|
||||
# otherwise it will be a duplicate with what immediately comes back from the API. But if we weren't asked to
|
||||
# send it upstream, or we were but every submission failed, we should still add it to our database anyway.
|
||||
if not any_upstream_succeeded:
|
||||
spot.source = "API"
|
||||
spot.infer_missing()
|
||||
self._spots.set(spot.id, spot)
|
||||
|
||||
if upstream_warning:
|
||||
self.write(safe_json_dumps(f"Warning - {upstream_warning}"))
|
||||
if upstream_warnings:
|
||||
saved_locally_note = "" if any_upstream_succeeded else " The spot was saved to Spothole only."
|
||||
self.write(safe_json_dumps(f"Warning - {' '.join(upstream_warnings)}{saved_locally_note}"))
|
||||
self.set_status(201)
|
||||
else:
|
||||
self.write(safe_json_dumps("OK"))
|
||||
@@ -245,6 +232,33 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
|
||||
async def _submit_upstream(self, upstream_provider_name, spot, credentials) -> str | None:
|
||||
"""Submit a spot to the named upstream provider. Returns None on success, or a warning message on failure."""
|
||||
|
||||
provider = self._find_provider(upstream_provider_name, spot.activities)
|
||||
if not provider:
|
||||
if spot.activities:
|
||||
return f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {', '.join(spot.activities)} spots."
|
||||
return f"No enabled provider named '{upstream_provider_name}' supports upstream submission for spots with no activity."
|
||||
|
||||
try:
|
||||
# Submit spot to the upstream provider. Run in a separate thread otherwise this blocks the whole web server
|
||||
# for everyone!
|
||||
await IOLoop.current().run_in_executor(None, provider.submit_spot, spot, credentials)
|
||||
# Trigger a re-poll after 3 second so the spot appears quickly. (Submitting to a cluster node is slower than
|
||||
# this, but we get data as a live stream from cluster anyway, so force_poll does nothing in that case. This
|
||||
# is really just for the HTTP providers when we submit a spot to them)
|
||||
threading.Timer(3.0, provider.force_poll).start()
|
||||
return None
|
||||
except NotImplementedError as e:
|
||||
return str(e)
|
||||
except SpotSubmissionError as e:
|
||||
logger.warning(f"Upstream submission to {upstream_provider_name} was not accepted: {e}")
|
||||
return f"Upstream submission to {upstream_provider_name} failed: {e}"
|
||||
except Exception:
|
||||
logger.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
|
||||
return f"Upstream submission to {upstream_provider_name} failed."
|
||||
|
||||
def _find_provider(self, provider_name, activities) -> SpotProvider | None:
|
||||
"""Find an enabled provider by name that can submit spots for at least one of the given activities. If there
|
||||
are no activities, find one that can submit spots with no activity."""
|
||||
|
||||
@@ -116,6 +116,8 @@ class V2APISpotHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIS
|
||||
def translate(body):
|
||||
if isinstance(body, dict) and isinstance(body.get("spot"), dict):
|
||||
body["spot"] = self._translate_v2_spot(body["spot"])
|
||||
if isinstance(body, dict) and isinstance(body.get("handling"), dict):
|
||||
body["handling"] = self._translate_v2_handling(body["handling"])
|
||||
return body
|
||||
|
||||
translate_json_body(self.request, translate)
|
||||
@@ -141,6 +143,21 @@ class V2APISpotHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIS
|
||||
spot_data["activity_refs"] = refs
|
||||
return spot_data
|
||||
|
||||
@staticmethod
|
||||
def _translate_v2_handling(handling):
|
||||
"""Translate the "handling" object provided by a client calling the add spot method in v2 format into v3
|
||||
format. v2 had a "submit_upstream" flag with a single "upstream_provider" and its credentials, whereas v3 has
|
||||
a list of "upstream_providers" and a map of provider name to credentials."""
|
||||
|
||||
handling = dict(handling)
|
||||
submit_upstream = handling.pop("submit_upstream", False)
|
||||
provider_name = handling.pop("upstream_provider", None)
|
||||
credentials = handling.pop("upstream_credentials", None) or {}
|
||||
if submit_upstream and provider_name:
|
||||
handling["upstream_providers"] = [provider_name]
|
||||
handling["upstream_credentials"] = {provider_name: credentials}
|
||||
return handling
|
||||
|
||||
|
||||
def collapse_activities(obj):
|
||||
"""Utility method to replace the "activities" list in a spot or alert JSON object with a single "activity" value,
|
||||
|
||||
Reference in New Issue
Block a user