Compare commits

..

30 Commits

Author SHA1 Message Date
f3nris 9fbe592421 feat(crowdstrike): version bounds and relative dates on device_count (1.7.0)
agent_version_lt/lte/gt/gte count the sensors below or above a build. FQL
compares versions as text, so the script scrolls the matching device IDs,
reads their versions and compares number by number. first_seen / last_seen
bounds also take a duration relative to now (24h, 7d).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgjV26rz6VcP262dX4vBrD
2026-10-19 20:09:31 +00:00
f3nris 6d36b4e958 feat(harfanglab): version bounds on endpoint_count (1.9.0)
version_lt, version_lte, version_gt and version_gte count the agents below or
above a build. The API matches a version only exactly, so the script pages
through the agents the other filters keep and compares number by number;
unreadable versions are reported apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgjV26rz6VcP262dX4vBrD
2026-10-19 19:31:41 +00:00
f3nris 90c6c80843 feat(harfanglab): instance-level tenant scope (1.8.0)
An instance can carry the tenant of the client it serves, for clients sharing
one multi-tenant manager. endpoint_count uses it, for the count and the fleet
total beside it, when the block leaves its tenant filter empty; a tenant named
on the block still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgjV26rz6VcP262dX4vBrD
2026-10-19 19:23:40 +00:00
f3nris c67343b7cf feat(sentinelone): instance-level account scope (1.20.0)
An instance can carry the account IDs of the client it serves, for clients
sharing one multi-tenant console. count_agents, get_threats, get_alerts and
get_unified_alerts use them when the block leaves its account filter empty;
an account named on the block still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgjV26rz6VcP262dX4vBrD
2026-10-19 19:08:10 +00:00
f3nris 2bc7601177 fix(mail-templates): callout_title points to the badge in the mail language (1.0.4)
Its description gave "⚠ SLA ALERT" as the default, which read as a
value to type: a French mail ended up with an English box title. It now
says to leave the field empty for the badge in the mail language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41
2026-10-19 09:32:17 +00:00
f3nris 2a74bbc436 feat(mail-templates): markdown inputs in a multi-line field (1.0.3)
description and callout_text declare format: markdown, which Riposte
0.322+ edits in a text area that keeps line breaks; older versions
ignore it and \n still works. README documents format: multiline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41
2026-10-19 09:17:27 +00:00
f3nris 983414f278 fix(mail-templates): read \n as a line break in one-line inputs (1.0.2)
The playbook editor's input field is a single <input>, which drops the
newlines of a pasted text: the whole description reached the script as
one line starting with ## and rendered as a single heading. description
and callout_text without a real line break now take the two characters
\n as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41
2026-10-19 09:10:52 +00:00
f3nris 046810c1f6 fix(mail-templates): run under Riposte's exec() runner (1.0.1)
The runner execs a script without __file__ or __name__: the Test button
failed on __file__ and render-mail-template never reached main(). The
test no longer imports the renderer, and the renderer calls main()
unconditionally like the other integrations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41
2026-10-19 08:49:46 +00:00
f3nris dcf257d887 feat(mail-templates): render ACKnowledge SOC emails from markdown (1.0.0)
render-mail-template turns a template, a subject and a markdown
description into the dark ACKnowledge SOC mail (header badge, optional
callout and action button, signature with the logo, SOC or client
footer, fr/en). Nothing is sent: subject, html_body and text_body go to
exchange-send-mail or mail-sender, which embed the data-URI logo.

Eight templates (sla_breach, incident_notification, incident_update,
incident_closure, action_required, escalation, threat_bulletin, info).
The markdown covers GFM tables, code blocks and {{level}} pills; raw
HTML in the inputs is escaped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41
2026-10-19 08:40:32 +00:00
f3nris 5085d96628 feat(webhook-relay): map Rapid7 InsightIDR basic rule alerts (1.1.0)
fetch_rapid7_alerts opens the alert InsightIDR Essential posts as a JSON
string in a single payload field (JSON or form body) and returns one flat
item per relayed message; the correlation id is the dedup key. Own mapper
and alert type, written from a real delivery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa1QDbsPTYiVREv2Cvs3Zn
2026-10-19 07:56:45 +00:00
f3nris c358814fe7 feat(microsoft-exchange-mail): send mail from Exchange Online over Graph (1.0.0)
Mail Sender speaks SMTP and Defender for Office 365 only reads and
purges, so nothing in the catalogue could send from an Exchange Online
mailbox with an app registration (tenant ID, client ID, client secret).

send_mail posts to /users/{mailbox}/sendMail: text or HTML, inline
data-URI images as cid attachments, CC/BCC, Reply-To, importance and
saveToSentItems. Over 3 MB of attachments the message goes through a
draft and upload sessions instead, since sendMail caps the request at
4 MB; that path needs Mail.ReadWrite. reply_mail and forward_mail take
the Graph id or the internet Message-ID (resolved with a $filter, which
needs Mail.ReadBasic.All). test_connection reads the token's roles and
fails when Mail.Send is missing, without sending anything.

Exercised against a stubbed Graph: request bodies, the draft path with a
7 MB file in three ranges and no bearer on the upload URL, and Riposte's
ParseManifest. Not yet tried against a real tenant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 03:29:32 +00:00
f3nris 815db080f5 feat(webhook-relay): map relayed messages into OCSF, not only custom fields (1.0.1)
The generic mapper now fills the OCSF fields a relayed message has a
place for: metadata.logged_time, log_provider and log_name, the checks
the delivery passed as metadata.labels (relay:hmac…), and raw_data. The
sender's address goes to unmapped.relay.sender_ip rather than
src_endpoint.ip: it is the vendor's cloud, and as an entity it would
correlate every alert of that vendor. metadata.product.name no longer
holds the source id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa1QDbsPTYiVREv2Cvs3Zn
2026-10-16 15:48:27 +00:00
f3nris 6b76270cc6 feat(webhook-relay): read the alerts vendors push to riposte-relay (1.0.0)
Riposte accepts no inbound connection, so a vendor that can only push
(Rapid7 InsightIDR Essential basic detection rules) posts to the relay,
and an alert rule on this integration reads one relay source back over
outbound HTTPS.

fetch_messages takes the rule's watermark as `since` and walks every
page within the run, since the next watermark is this run's instant. A
window holding more than `limit` fails the run instead of truncating, so
the watermark stays put. relay_status and test_connection tell a 404
from the proxy (this Riposte's address is not listed) from a refused
token. The generic mapper keeps the raw body, the sender's address and
the checks the delivery passed (custom:webhook_verified).

Tested against a local relay: 2503 messages over three pages, a
millisecond watermark, the limit, unknown source and bad token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa1QDbsPTYiVREv2Cvs3Zn
2026-10-16 11:23:03 +00:00
f3nris ff461464fd fix(crowdstrike): resolve_detection fails when Falcon changed nothing (1.6.3)
The alerts PATCH answers 200 to a refused update (errors, resources_affected
0) and the command reported it as a success. It now exits in error with
Falcon's reasons and the ids sent; an empty update is refused up front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBnLYwQbUCaf5vpv77N14Q
2026-10-15 12:08:05 +00:00
f3nris 88407dd1b4 feat(sekoia-xdr): list_alerts attaches each alert's events (1.3.0)
An alert only summarises its detection; addresses, user, process and
command line live in the events tagged with its short id. with_events
runs one event search per alert (alert_short_ids, first_seen_at - 1 min
to now, as Sekoia's XSOAR integration does), in parallel within the
60 s command budget, and nests the dotted ECS names. A search not done
in time leaves events_pending + events_job_uuid: the alert still goes
out, as the rule's watermark would not offer it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBnLYwQbUCaf5vpv77N14Q
2026-10-15 10:10:56 +00:00
f3nris 8eed7ed9fb fix(crowdstrike): NG-SIEM detections are filed under thirdparty (1.6.2)
On a real tenant the Unified Detections raised from ingested FortiGate
logs are product 'thirdparty'; 'ngsiem' matched nothing. The command now
reads thirdparty and ngsiem by default (products input), and its mapper
is rewritten from real payloads: source product/vendor, matched host,
connection endpoints and ports, vendor signature id, Falcon priority,
console link, and a description built from what fired on which connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBnLYwQbUCaf5vpv77N14Q
2026-10-15 08:32:36 +00:00
f3nris f34c87f0c8 fix(crowdstrike): say what an empty alert search matched (1.6.1)
search_detections and search_ngsiem_detections now return a query block
(FQL sent, Falcon's total, errors) next to resources, so an empty run can
be told apart from a wrong region or an API client that sees no alerts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:09:39 +00:00
f3nris 0444e120fa feat(crowdstrike): search Next-Gen SIEM detections (1.6.0)
NG-SIEM correlation rules file their detections in the unified Alerts
API under product 'ngsiem'. search_detections defaults to epp/ldt and
maps device/process/file blocks those detections do not carry.

New search_ngsiem_detections command (product clause always kept, filter
ANDed, created_from watermark), its own first-cut mapper on the fields
every unified alert carries, and a CrowdStrike NG-SIEM Detection type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:53:05 +00:00
f3nris 585c06385c fix(harfanglab): retry transient API errors (1.7.3)
HarfangLab answers a 500 now and then on a call that passes when sent
again. Each script's request() now retries up to three times (1, 2, 4 s):
GET on 429/500/502/503/504, writes only on 429/502/503 so an isolation,
a kill or a job is never sent twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:28:03 +00:00
f3nris 725e6921f4 feat(jira): watch_issues reports what changed on a set of tickets (1.4.0)
Given up to 50 keys and an instant, returns the issues updated since with
their status, new comments (author id, plain-text body, own comments
flagged) and status transitions, plus the instance's own account. JQL dates
are converted to the account's time zone; malformed keys are never queried;
a deleted key falls back to per-key reads instead of failing the batch.

get_comments gains order, start_at and author_id, and decodes HTML entities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:59:42 +00:00
f3nris e973ccb673 feat(cortex-xdr): endpoint_count counts agents below a target build (1.10.0)
agent_version_lt, compared number by number in the script, as the
endpoints API does not filter on the agent version either. Folded into
the unpushed 1.10.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaiNEwG2W5Y9fMo4qXcesr
2026-10-13 08:49:29 +00:00
f3nris 62157e5dee feat(cortex-xdr): endpoint_count counts by operational status (1.10.0)
The endpoints API has no filter on operational_status, so the endpoints
the other filters match are read 100 at a time and counted by the script.
Lets an agent reporting playbook count the agents in error, as
SentinelOne's operationalStatesNin does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaiNEwG2W5Y9fMo4qXcesr
2026-10-13 08:43:18 +00:00
Claude 91855fce4f fix(sentinelone): count the host's other alerts from what was read (1.19.1)
A real run answered one other alert and named none: the count came
from the console's total minus the alert itself, and that total did
not hold the alert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 11:37:19 +00:00
Claude 6f51520fb5 feat(sentinelone): investigate_alert, one call for an agent (1.19.0)
Wraith spent ten calls on a real unified alert and still lost the host:
it took the UAM asset id for an agent id and the raw agent list ran past
what a tool result carries. investigate_alert reads the alert, its
detail, the host's agent (by id or UUID, then name), the other alerts
and threats on that host around the detection, and the file SHA1's
reputation, blocklist and fleet-wide threats, summarised in one answer.
The operator sheet now starts with it (budget 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 11:27:36 +00:00
f3nris 80de5399a6 rapid7-insightidr 1.14.9: a user_authentications answer that fits
Riposte gives a tool result 8000 characters; two full summaries ran to 11903
and were cut at 3903, the asset list half gone. The answer now keeps only the
fields an authentication question is answered from (destination and source
asset and address, domain, result, logon type, service) and lists up to 40
new destination assets in full: about 2.5 KB for 1000 events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 08:14:10 +00:00
f3nris fb560cc937 rapid7-insightidr 1.14.8: user_authentications counts the user's own logons
The keyword search matches the account name anywhere in a line, so it brought
other accounts' logons: on a real run the only "new" event reported was
another user's failed password. Only events whose account/user field is the
user (DOMAIN\\user and user@domain normalized) are summarized now, with
keyword_matches and user_matches both reported. A first authentication is
recognized as Rapid7 flags it, a boolean new_authentication field, as well as
the word. The event limit cannot be set below 10000 (the agent passed 1000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 07:53:57 +00:00
f3nris 3bfcebdf21 rapid7-insightidr 1.14.7: user_authentications, one call for a UBA alert
A new read command answers what a behaviour authentication alert asks —
which assets the user authenticated to around the alert, and which were new —
in one call: it resolves the Asset Authentication log set, builds the
keyword query and the 24 h window itself, and summarizes all events and,
apart, those marked new_authentication. Across real runs gpt-oss mangled log
set ids, computed the window a day off, put a filter inside the LEQL (HTTP
400) and read ten lines out of a thousand; this leaves it nothing to get
wrong. Static calls it first for such alerts; list_investigation_alerts
points to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 07:45:31 +00:00
f3nris ae1effafcd rapid7-insightidr 1.14.6: narrow a log result by field, no query syntax
query_log_set takes only="field=value[, …]", applied to the events' fields
after the search (case ignored), and says how many matched before it. Static
runs a second summary of the same query restricted to what the alert is about
— for a "new assets" alert, the new authentications — and names their
destination assets: the first real run summarized 978 events but left the 64
new ones mixed in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 07:27:36 +00:00
f3nris 889cf660f6 rapid7-insightidr 1.14.5: query_log_set can summarize; Static always does
summarize=true returns, over every matching event, each field's most frequent
values with counts, the first and last event time and five samples, instead of
the raw events. On a real run Static got 1000 raw events, could read about
ten, and reported the hosts and accounts of those ten; 300 events now fit in
about 1.5 KB. Static passes summarize always and quotes the returned window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 07:18:09 +00:00
f3nris 847f31ce17 zscaler 1.1.2: withdraw the Relay operator
It could only read a sandbox report by MD5 — no web logs — so it added noise to
the crew lead's routing without answering an investigation question. Riposte
retires the installed agent on the next update of the integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RhL2gy6Sx6Y3FEsyBE3mZ
2026-10-12 07:12:54 +00:00
150 changed files with 6495 additions and 649 deletions
+12
View File
@@ -141,6 +141,18 @@ issue_type_name:
Riposte refuses an `options_from` command that is not `risk: read`: opening a Riposte refuses an `options_from` command that is not `risk: read`: opening a
field must never change anything in the customer's tool. field must never change anything in the customer's tool.
### Multi-line inputs
`format: multiline` (or `format: markdown`, which says what the text is) makes
the playbook editor show a resizable text area instead of a one-line field
(Riposte 0.322+). Use it for a mail body, a comment, a markdown description:
a one-line field turns the line breaks of a pasted text into spaces. Older
Riposte versions ignore `format` and keep the one-line field.
```yaml
description: { type: string, format: markdown, description: "Body in markdown" }
```
### `risk:` — the agent approval gate ### `risk:` — the agent approval gate
Every command declares a tier. It is what Riposte's agent approval gate reads: Every command declares a tier. It is what Riposte's agent approval gate reads:
File diff suppressed because one or more lines are too long
@@ -135,14 +135,57 @@ def count(filters):
return int(reply.get("total_count") or 0) return int(reply.get("total_count") or 0)
def version(v):
# "8.5.0.12345" -> (8, 5, 0, 12345); a shorter target such as "8.5" reads as
# 8.5.0.0, so it compares against every build of that release line.
parts = []
for x in str(v or "").strip().split("."):
digits = "".join(c for c in x if c.isdigit())
parts.append(int(digits) if digits else 0)
while len(parts) < 4:
parts.append(0)
return tuple(parts)
def count_local(filters, keep):
# The endpoints grid filters neither on operational_status nor on the agent
# version, so the endpoints the other filters match are read 100 at a time —
# the API's page ceiling — and counted here. One call per hundred endpoints.
n, start = 0, 0
while True:
request_data = {"search_from": start, "search_to": start + 100, "filters": filters}
reply = post("/endpoints/get_endpoint/", request_data).get("reply") or {}
page = reply.get("endpoints") or []
n += sum(1 for e in page if keep(e))
start += len(page)
if not page or start >= int(reply.get("total_count") or 0):
return n
def main(): def main():
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
filters = build_filters(inputs) filters = build_filters(inputs)
result = {"count": count(filters), "filters": filters} result = {"filters": filters}
tests = []
wanted = {x.upper() for x in csv(inputs.get("operational_status"))}
if wanted:
tests.append(lambda e: str(e.get("operational_status") or "").upper() in wanted)
result["operational_status"] = sorted(wanted)
below = str(inputs.get("agent_version_lt") or "").strip()
if below:
target = version(below)
# An endpoint that reports no version is not counted as outdated: it is
# unknown, and calling it outdated would inflate the figure.
tests.append(lambda e: bool(e.get("agent_version")) and version(e.get("agent_version")) < target)
result["agent_version_lt"] = below
if tests:
result["count"] = count_local(filters, lambda e: all(t(e) for t in tests))
else:
result["count"] = count(filters)
# The fleet total is the denominator the filtered count is read against: it # The fleet total is the denominator the filtered count is read against: it
# turns a bare number into a proportion, and makes a filter that matched the # turns a bare number into a proportion, and makes a filter that matched the
# whole estate — because it was too broad — visible instead of plausible. # whole estate — because it was too broad — visible instead of plausible.
result["total"] = count([]) if filters else result["count"] result["total"] = count([]) if filters or tests else result["count"]
print(json.dumps(result)) print(json.dumps(result))
@@ -0,0 +1,3 @@
name: "CrowdStrike NG-SIEM Detection"
color: "#c1121f"
icon: "shield"
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
name: "CrowdStrike Next-Gen SIEM Detections → OCSF"
description: "Map of a Falcon Next-Gen SIEM detection (alerts/entities/alerts/v2 on products 'thirdparty' and 'ngsiem', results_path = resources) to OCSF. Read from real FortiGate IPS detections: a third-party detection carries no sensor process or file block, but names its source product and vendor, the source and target endpoints with ports and protocol, the host Falcon matched it to (enriched_entities) and the vendor's own signature id. It has no description: the incident's is built from the source product, the signature and the connection."
field_mappings:
title: "name"
# Plain path: the ingestion engine reads informational/low/medium/high/critical natively.
severity: "severity_name"
# Third-party detections carry no description of their own; say what fired,
# from which product, on which connection.
description: "jsonata: description ? description : $join(source_products, ', ') & ' — ' & name & (source_endpoint_address_ip4 ? ' — ' & source_endpoint_address_ip4 & (local_port ? ':' & local_port : '') & ' → ' & target_endpoint_address_ip4 & (remote_port ? ':' & remote_port : '') & (protocol ? ' (' & protocol & ')' : '') : '')"
# results_path = resources; source_path is JSONata over ONE alert object.
# Paths absent from a given alert are skipped at ingestion, so extra entries are safe.
ocsf:
# ── Finding / analytic ────────────────────────────────────────────
- { source_path: "composite_id", ocsf_field: "finding_info.uid" }
- { source_path: "name", ocsf_field: "finding_info.title" }
- { source_path: "description", ocsf_field: "finding_info.desc" }
- { source_path: "categorization", ocsf_field: "finding_info.types" }
- { source_path: "data_domains", ocsf_field: "finding_info.data_sources" }
- { source_path: "falcon_host_link", ocsf_field: "finding_info.src_url" }
- { source_path: "created_timestamp", ocsf_field: "finding_info.created_time" }
- { source_path: "updated_timestamp", ocsf_field: "finding_info.modified_time" }
# `time` is when the source product saw the event (timestamp), ahead of
# Falcon's own record of the detection (created_timestamp, ~20 s later).
- { source_path: "timestamp", ocsf_field: "time" }
# The vendor's signature id (FortiGate IPS 100012) identifies the rule;
# pattern_id is Falcon's generic third-party bucket (300000 for all of them).
- { source_path: "name", ocsf_field: "finding_info.analytic.name" }
- { source_path: "vendor_pattern_id", ocsf_field: "finding_info.analytic.uid" }
- { source_path: "confidence", ocsf_field: "confidence" }
- { source_path: "priority_value", ocsf_field: "risk_score" }
- { source_path: "status", ocsf_field: "status" }
# ── Source product — the log the detection came from ──────────────
- { source_path: "source_products[0]", ocsf_field: "metadata.product.name" }
- { source_path: "source_vendors[0]", ocsf_field: "metadata.product.vendor_name" }
- { source_path: "product", ocsf_field: "metadata.product.feature.name" }
- { source_path: "cid", ocsf_field: "cloud.account.uid" }
# ── Host Falcon matched the detection to ──────────────────────────
- { source_path: "agent_id", ocsf_field: "device.uid" }
- { source_path: "enriched_entities.hostnames[0]", ocsf_field: "device.hostname" }
# ── Connection ────────────────────────────────────────────────────
- { source_path: "source_endpoint_address_ip4 ? source_endpoint_address_ip4 : source_ips[0]", ocsf_field: "src_endpoint.ip" }
- { source_path: "source_hosts[0]", ocsf_field: "src_endpoint.hostname" }
- { source_path: "$number(local_port)", ocsf_field: "src_endpoint.port" }
- { source_path: "target_endpoint_address_ip4 ? target_endpoint_address_ip4 : destination_ips[0]", ocsf_field: "dst_endpoint.ip" }
- { source_path: "$number(remote_port)", ocsf_field: "dst_endpoint.port" }
- { source_path: "protocol", ocsf_field: "connection_info.protocol_name" }
- { source_path: "users[0]", ocsf_field: "actor.user.name" }
# ── MITRE ATT&CK (correlation rules that declare it) ──────────────
- { source_path: "tactic", ocsf_field: "attacks.tactic.name" }
- { source_path: "tactic_id", ocsf_field: "attacks.tactic.uid" }
- { source_path: "technique", ocsf_field: "attacks.technique.name" }
- { source_path: "technique_id", ocsf_field: "attacks.technique.uid" }
@@ -1,4 +1,4 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import datetime, json, os, re, sys, urllib.request, urllib.parse, urllib.error
SECRETS = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) SECRETS = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
INPUTS = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) INPUTS = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
@@ -61,6 +61,44 @@ RANGE_FILTERS = [
] ]
# A bound on first_seen / last_seen can also be relative to now ("24h", "7d",
# "30m"), so a scheduled playbook counts the sensors seen in the last day without
# computing a date. It is turned into the UTC instant here.
RELATIVE = re.compile(r"^\s*(\d+)\s*([mhd])\s*$", re.I)
UNITS = {"m": "minutes", "h": "hours", "d": "days"}
def instant(v):
m = RELATIVE.match(str(v))
if not m:
return v
delta = datetime.timedelta(**{UNITS[m.group(2).lower()]: int(m.group(1))})
return (datetime.datetime.now(datetime.timezone.utc) - delta).strftime("%Y-%m-%dT%H:%M:%SZ")
# Sensor version bounds. FQL compares agent_version as text, where 7.9 sorts
# above 7.18, so these are compared here, number by number, over the devices the
# other filters keep.
VERSION_BOUNDS = {
"agent_version_lt": lambda v, t: v < t,
"agent_version_lte": lambda v, t: v <= t,
"agent_version_gt": lambda v, t: v > t,
"agent_version_gte": lambda v, t: v >= t,
}
SCROLL = 5000
DETAILS = 1000
def parse_version(v):
parts = re.findall(r"\d+", str(v or ""))
return tuple(int(x) for x in parts) if parts else None
def pad(a, b):
n = max(len(a), len(b))
return a + (0,) * (n - len(a)), b + (0,) * (n - len(b))
def literal(v): def literal(v):
# FQL quotes strings with single quotes, so a value carrying one would end # FQL quotes strings with single quotes, so a value carrying one would end
# the literal early and change what the query means. # the literal early and change what the query means.
@@ -82,7 +120,7 @@ def build_fql(inputs):
for name, prop, operator in RANGE_FILTERS: for name, prop, operator in RANGE_FILTERS:
value = inputs.get(name) value = inputs.get(name)
if value: if value:
clauses.append(prop + ":" + operator + literal(value)) clauses.append(prop + ":" + operator + literal(instant(value)))
# Escape hatch for any property this command does not name explicitly: raw # Escape hatch for any property this command does not name explicitly: raw
# FQL, ANDed with the rest. # FQL, ANDed with the rest.
raw = str(inputs.get("filter") or "").strip() raw = str(inputs.get("filter") or "").strip()
@@ -110,9 +148,59 @@ def count(tok, fql):
return int(total) return int(total)
def version_bounds(inputs):
bounds = {}
for field in VERSION_BOUNDS:
value = str(inputs.get(field) or "").strip()
if value:
target = parse_version(value)
if target is None:
raise ValueError(field + " must be a version such as 7.18.18803, got: " + repr(value))
bounds[field] = target
return bounds
def count_by_version(tok, fql, bounds):
"""Every device ID the filter keeps, then their sensor versions, compared
here. A device with no readable version is left out and counted apart."""
ids, offset = [], None
while True:
params = [("limit", SCROLL)]
if fql:
params.insert(0, ("filter", fql))
if offset:
params.append(("offset", offset))
reply = call("GET", "/devices/queries/devices-scroll/v1", tok, params=params)
page = reply.get("resources") or []
ids.extend(page)
pagination = (reply.get("meta") or {}).get("pagination") or {}
offset = pagination.get("offset")
if not page or not offset or len(ids) >= int(pagination.get("total") or 0):
break
matched, unreadable = 0, 0
for i in range(0, len(ids), DETAILS):
reply = call("POST", "/devices/entities/devices/v2", tok, body={"ids": ids[i:i + DETAILS]})
for device in reply.get("resources") or []:
version = parse_version(device.get("agent_version"))
if version is None:
unreadable += 1
elif all(VERSION_BOUNDS[f](*pad(version, t)) for f, t in bounds.items()):
matched += 1
return matched, len(ids), unreadable
def main(): def main():
tok = token() tok = token()
fql = build_fql(INPUTS) fql = build_fql(INPUTS)
bounds = version_bounds(INPUTS)
if bounds:
matched, scanned, unreadable = count_by_version(tok, fql, bounds)
result = {"count": matched, "filter": fql,
"agent_version_bounds": {f: INPUTS[f] for f in bounds},
"scanned": scanned, "unreadable_version": unreadable}
result["total"] = count(tok, "")
print(json.dumps(result))
return
result = {"count": count(tok, fql), "filter": fql} result = {"count": count(tok, fql), "filter": fql}
# The estate total is the denominator the filtered count is read against: it # The estate total is the denominator the filtered count is read against: it
# turns a bare number into a proportion, and makes a filter that matched the # turns a bare number into a proportion, and makes a filter that matched the
@@ -62,8 +62,23 @@ def main():
action_params.append({"name": "append_comment", "value": str(INPUTS["comment"])}) action_params.append({"name": "append_comment", "value": str(INPUTS["comment"])})
if INPUTS.get("assign_to_uuid"): if INPUTS.get("assign_to_uuid"):
action_params.append({"name": "assign_to_uuid", "value": str(INPUTS["assign_to_uuid"])}) action_params.append({"name": "assign_to_uuid", "value": str(INPUTS["assign_to_uuid"])})
if not action_params:
raise ValueError("nothing to change: give a status, a verdict, a comment or an assignee")
body = {"action_parameters": action_params, "composite_ids": ids} body = {"action_parameters": action_params, "composite_ids": ids}
result = call("PATCH", "/alerts/entities/alerts/v3", tok, body=body) result = call("PATCH", "/alerts/entities/alerts/v3", tok, body=body)
# Falcon answers 200 to an update it refused: the reasons sit in errors and
# meta.writes.resources_affected stays at 0. Passing that on as a success
# left an incident closed in Riposte and still open in the console.
affected = ((result.get("meta") or {}).get("writes") or {}).get("resources_affected")
if result.get("errors") or affected == 0:
print(json.dumps({
"error": "Falcon updated no alert" if affected == 0 else "Falcon refused part of the update",
"resources_affected": affected,
"errors": result.get("errors") or [],
# The PATCH takes composite ids (<cid>:<id>), not the alert's bare id.
"composite_ids": ids,
}))
sys.exit(1)
print(json.dumps(result)) print(json.dumps(result))
@@ -51,6 +51,14 @@ def main():
result = call("POST", "/alerts/entities/alerts/v2", tok, body={"composite_ids": ids}) result = call("POST", "/alerts/entities/alerts/v2", tok, body={"composite_ids": ids})
else: else:
result = {"resources": []} result = {"resources": []}
# An empty page says nothing on its own: keep what the query answered — the
# FQL actually sent, Falcon's total for it and any errors — so a run with no
# alerts reads as "matched nothing" rather than as a silent failure.
result["query"] = {
"filter": fql,
"total": ((found.get("meta") or {}).get("pagination") or {}).get("total"),
"errors": found.get("errors") or [],
}
print(json.dumps(result)) print(json.dumps(result))
@@ -0,0 +1,78 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
SECRETS = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
INPUTS = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
BASE = SECRETS.get("base_url", "https://api.crowdstrike.com").rstrip("/")
def token():
data = urllib.parse.urlencode({"client_id": SECRETS.get("client_id", ""), "client_secret": SECRETS.get("client_secret", "")}).encode()
req = urllib.request.Request(BASE + "/oauth2/token", data=data, headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read()).get("access_token", "")
def call(method, path, tok, params=None, body=None):
url = BASE + path
if params:
# params is a list of (key, value) tuples to allow repeated keys (e.g. ids)
url += "?" + urllib.parse.urlencode(params)
data = json.dumps(body).encode() if body is not None else None
headers = {"Authorization": "Bearer " + tok, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def csv(v):
return [x.strip() for x in str(v or "").split(",") if x.strip()]
def main():
tok = token()
# Next-Gen SIEM files its detections in the same unified Alerts API as the
# EDR, under two products: 'thirdparty' for the alerts it raises from ingested
# third-party logs (a FortiGate IPS hit, say) and 'ngsiem' for those of the
# tenant's own correlation rules. The product clause is always kept, so a
# filter narrows these detections and never widens past them.
products = [p.strip().strip("'\"") for p in str(INPUTS.get("products") or "thirdparty,ngsiem").split(",") if p.strip()]
# ',' is OR in FQL; parenthesised so the group binds before the '+' ANDs.
fql = "(" + ",".join("product:'" + p.replace("'", "\\'") + "'" for p in products) + ")"
extra = str(INPUTS.get("filter") or "").strip()
if extra:
fql += "+(" + extra + ")"
# The alert rule's watermark lands in created_from, folded into the FQL as
# created_timestamp (see search_detections for why it is not the filter input).
since = str(INPUTS.get("created_from") or "").strip().strip("'\"")
if since:
fql += "+created_timestamp:>'" + since + "'"
limit = INPUTS.get("limit", 50)
params = [("filter", fql), ("limit", limit), ("sort", "created_timestamp.desc")]
found = call("GET", "/alerts/queries/alerts/v2", tok, params=params)
ids = found.get("resources", []) or []
if ids:
result = call("POST", "/alerts/entities/alerts/v2", tok, body={"composite_ids": ids})
else:
result = {"resources": []}
# An empty page says nothing on its own: keep what the query answered — the
# FQL actually sent, Falcon's total for it and any errors — so a run with no
# alerts reads as "matched nothing" rather than as a silent failure.
result["query"] = {
"filter": fql,
"total": ((found.get("meta") or {}).get("pagination") or {}).get("total"),
"errors": found.get("errors") or [],
}
print(json.dumps(result))
try:
main()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
File diff suppressed because one or more lines are too long
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
+20 -1
View File
@@ -1,16 +1,35 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
def csv(v): def csv(v):
return [x.strip() for x in str(v or "").split(",") if x.strip()] return [x.strip() for x in str(v or "").split(",") if x.strip()]
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def csv(v): def csv(v):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,4 +1,4 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
FIELD_TYPES = { FIELD_TYPES = {
"name": 0, "path": 1, "username": 3, "commandline": 4, "name": 0, "path": 1, "username": 3, "commandline": 4,
@@ -8,12 +8,31 @@ FIELD_TYPES = {
OPERATORS = {"eq": 0, "not_eq": 1, "contains": 2, "not_contains": 3} OPERATORS = {"eq": 0, "not_eq": 1, "contains": 2, "not_contains": 3}
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, re, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -51,6 +70,28 @@ RANGE_FILTERS = {
} }
# Version bounds. The API matches a version only exactly, and a range lookup it
# does not support would be dropped without a word — so these are compared here,
# number by number (4.10 is above 4.9, which a string comparison denies).
VERSION_BOUNDS = {
"version_lt": lambda v, t: v < t,
"version_lte": lambda v, t: v <= t,
"version_gt": lambda v, t: v > t,
"version_gte": lambda v, t: v >= t,
}
PAGE = 500
def parse_version(v):
parts = re.findall(r"\d+", str(v or ""))
return tuple(int(x) for x in parts) if parts else None
def pad(a, b):
n = max(len(a), len(b))
return a + (0,) * (n - len(a)), b + (0,) * (n - len(b))
def to_bool(v): def to_bool(v):
if isinstance(v, bool): if isinstance(v, bool):
return v return v
@@ -67,7 +108,13 @@ def main():
"Content-Type": "application/json", "Content-Type": "application/json",
} }
# === per-command logic === # === per-command logic ===
filters = {} # An instance bound to one tenant of a shared, multi-tenant manager carries it
# in its config: it scopes the count, and the fleet total beside it, unless the
# block names a tenant itself.
scope = {}
if not str(inputs.get("tenant") or "").strip() and str(secrets.get("tenant") or "").strip():
scope["tenant"] = str(secrets["tenant"]).strip()
filters = dict(scope)
for field in EXACT_FILTERS: for field in EXACT_FILTERS:
value = inputs.get(field) value = inputs.get(field)
if value is None or value == "": if value is None or value == "":
@@ -84,17 +131,48 @@ def main():
if len(kv) == 2 and kv[0].strip(): if len(kv) == 2 and kv[0].strip():
filters[kv[0].strip()] = kv[1] filters[kv[0].strip()] = kv[1]
bounds = {}
for field in VERSION_BOUNDS:
value = str(inputs.get(field) or "").strip()
if value:
target = parse_version(value)
if target is None:
raise ValueError(field + " must be a version such as 4.10.2, got: " + repr(value))
bounds[field] = target
# The agent list is a DRF paginated collection: `count` is the total number # The agent list is a DRF paginated collection: `count` is the total number
# of matches, independent of the page size. limit=1 keeps the payload tiny. # of matches, independent of the page size. limit=1 keeps the payload tiny.
agents = base + "/api/data/endpoint/Agent/" agents = base + "/api/data/endpoint/Agent/"
if bounds:
# A version bound reads every agent the other filters keep and counts
# here. An agent with no readable version is left out and counted apart,
# rather than guessed above or below the bound.
count, scanned, unreadable, offset = 0, 0, 0, 0
while True:
page = request("GET", agents + qs(dict(filters, limit=PAGE, offset=offset)), headers)
rows = page.get("results") or []
for agent in rows:
scanned += 1
version = parse_version(agent.get("version"))
if version is None:
unreadable += 1
continue
if all(VERSION_BOUNDS[f](*pad(version, t)) for f, t in bounds.items()):
count += 1
offset += len(rows)
if not rows or offset >= page.get("count", 0):
break
result = {"count": count, "filters": dict(filters, **{f: inputs[f] for f in bounds}),
"scanned": scanned, "unreadable_version": unreadable}
else:
matched = request("GET", agents + qs(dict(filters, limit=1, offset=0)), headers) matched = request("GET", agents + qs(dict(filters, limit=1, offset=0)), headers)
result = {"count": matched.get("count", 0), "filters": filters} result = {"count": matched.get("count", 0), "filters": filters}
# The API silently drops a query parameter it does not recognise as a # The API silently drops a query parameter it does not recognise as a
# filter, which yields the unfiltered total dressed up as a filtered count. # filter, which yields the unfiltered total dressed up as a filtered count.
# Returning the fleet total alongside makes that failure visible. # Returning the fleet total alongside makes that failure visible.
if filters: if filters != scope or bounds:
everything = request("GET", agents + qs({"limit": 1, "offset": 0}), headers) everything = request("GET", agents + qs(dict(scope, limit=1, offset=0)), headers)
result["total"] = everything.get("count", 0) result["total"] = everything.get("count", 0)
else: else:
result["total"] = result["count"] result["total"] = result["count"]
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,13 +1,32 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
SEVERITIES = ["low", "medium", "high", "critical"] SEVERITIES = ["low", "medium", "high", "critical"]
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers): def request(method, url, headers):
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, headers=headers, method=method) req = urllib.request.Request(url, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=90) as resp: with urllib.request.urlopen(req, timeout=90) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
+20 -1
View File
@@ -1,13 +1,32 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
SEVERITIES = ["low", "medium", "high", "critical"] SEVERITIES = ["low", "medium", "high", "critical"]
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers): def request(method, url, headers):
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, headers=headers, method=method) req = urllib.request.Request(url, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=90) as resp: with urllib.request.urlopen(req, timeout=90) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,4 +1,4 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
FIELD_TYPES = { FIELD_TYPES = {
"name": 0, "path": 1, "username": 3, "commandline": 4, "name": 0, "path": 1, "username": 3, "commandline": 4,
@@ -8,12 +8,31 @@ FIELD_TYPES = {
OPERATORS = {"eq": 0, "not_eq": 1, "contains": 2, "not_contains": 3} OPERATORS = {"eq": 0, "not_eq": 1, "contains": 2, "not_contains": 3}
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def main(): def main():
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
+20 -1
View File
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):
@@ -1,12 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error import json, os, sys, time, urllib.request, urllib.parse, urllib.error
RETRIES = 3
def retryable(method, status):
# HarfangLab answers a 500 now and then on a call that passes when sent again.
# A read is safe to resend; a write only on an answer that says the action was
# not run (rate limit, gateway down), so an isolation or a job is never sent twice.
if status in (429, 502, 503):
return True
return method == "GET" and status in (500, 504)
def request(method, url, headers, body=None): def request(method, url, headers, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None data = json.dumps(body).encode("utf-8") if body is not None else None
for attempt in range(RETRIES + 1):
req = urllib.request.Request(url, data=data, headers=headers, method=method) req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else {} return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
if attempt == RETRIES or not retryable(method, e.code):
raise
e.close()
time.sleep(2 ** attempt)
def qs(params): def qs(params):

Some files were not shown because too many files have changed in this diff Show More