Skip to main content

drift

Your code and your config are in a long-distance relationship. drift finds where they've stopped talking.

Zero dependencies. One file. Point it at a Python project and it flags the quiet ways code and configuration fall out of sync, before the crash does.

Install

pip install drift-linter

Yes, the distribution is drift-linter: the name drift on PyPI belongs to a dead-squatted Python-2-era package (0.0.7, roughly a decade stale), and squatting on a name that's already someone else's is not how you start. The tool itself stays drift:

drift path/to/project
drift bot.py config_loader.py
drift --json --strict .

Rules

R1 unexpected_kwarg — calls passing keyword arguments the callee cannot accept. Story: I once shipped a patch that called SignalHistory(total_r=...) while the class still lacked the field. The bot crashed on startup. My fault, my fix. drift catches that class of bug statically: if a call passes a keyword the callee's signature doesn't declare, that's a partial patch apply waiting to happen.

R2 config_drift — config keys read but never defined (they will silently default, and silent defaults are how trading bots lose money), and keys defined but never read (dead config is how settings stop mattering). Reads with an explicit fallback (config.get(key, default)) are reported as warnings, not errors — that's a documented default, not a silent one.

R3 magic_number — bare numbers doing a named constant's job. 0.4 three times is a margin that escaped; it should have a name and a config key. Int literals report as ints (7 appears 3 times, not 7.0); repetition inside test files is skipped — vectors and fixtures are data, not unnamed constants.

R4 phantom_name — names used but never defined. The typo that doesn't crash your linter, just silently defaults.

Usage

python3 drift.py path/to/project
python3 drift.py bot.py config_loader.py
python3 drift.py --json --strict .

Exit code 1 when there are errors (or warnings with --strict), 0 otherwise. Rule ids: R1, R2, R3, R4. Pick with --rules R1,R4.

What it understands

  • Class __init__ and module-level function signatures, same-file and cross-file, with simple inheritance, **kwargs, positional-only args.
  • Config dicts assigned to config-ish names (config, settings, env, ..., including DEFAULT_SETTINGS-style constants), .env / .env.example files, and reads via config.get(), os.getenv(), os.environ, subscripts, and local aliases (s = self.state.settings).
  • Config loaded from outside the scanned code (json.load, yaml.safe_load, tomllib.load, os.environ.copy()) is treated as external: its keys can't be known, so reads through it are never flagged. A literal json.loads('{...}') is the opposite — its inline keys count as defined.
  • Helper lookups like get_val(["a", "b"], default) prove keys are read (no false "dead config") without inventing errors about them.
  • Repeated numeric literals, except the ones everyone uses (0, 1, 2) and constants already named in UPPER_CASE.
  • Names defined by assignment, imports, parameters, comprehensions, walrus.
  • from x import * (R4): star imports resolve against the scanned tree — static __all__ wins exactly, otherwise every non-underscore module-level name is exported, and transitive chains follow. Unresolvable targets (external modules, dynamic __all__, import cycles) keep the legacy whole-file skip: drift never guesses what a star might have brought in.

What it does not understand (yet)

  • Dynamic code: exec, eval, getattr chains, metaprogramming.
  • Cross-file phantom names outside star imports (R4 is per-file; R1 and R2 are project-wide; from x import * resolves cross-file since v0.1.14).
  • Classes without __init__ that inherit from unknown bases.
  • Ambiguous names (two classes with the same name) are skipped, not guessed.
  • R1 matches call targets by name, not import path. Same-file resolution is precise (self./cls./ClassName. methods, same-project files); a call to a name that only exists as a same-named function elsewhere in the scan can still be misattributed. That is the cost of a zero-dependency static pass; use --rules to scope, or rename.

Honest about limits, like any good linter should be.

Changelog

  • 0.1.15 → PyPI — drift is now installable: pip install drift-linter. Distribution name is drift-linter because drift on PyPI is a dead-squatted Python-2-era Django package (0.0.7, ~a decade stale); the tool itself stays drift. Wheel + sdist built from a PEP 621 pyproject (setuptools>=77, PEP 639 license = "MIT"), clean-venv install verified, scan output byte-identical to a direct python3 drift.py run.

  • 0.1.15 — field test #8 (python-dateutil 2.9.0, 18 files). The corpus itself: 22 warnings, all real and hand-verified (Gauss Easter algorithm, day-of-year math, TZif byte reads — low urgency, textbook style, same verdict class as Sarah's 360.0). The run's real finds were drift's own, in three places. R1: WindowsError is a py2 builtin that py3 keeps as an OSError alias on Windows; except WindowsError: in platform-guarded compat code was flagged phantom — now a known compat name (pyflakes precedent), and a typo control still flags. R3: buckets keyed by exact value, not float() — a mixed int/float family used to print "7.0 appears 45 times" at an int anchor (44 int 7s plus relativedelta's days / 7.0); now it prints "7 / 7.0 appears 45 times", and ints past 2^53 no longer collapse into one bucket. JS engine (found by field parity on the real tree, not the corpus): the hand-typed BUILTINS list was missing the OSError family and Warning classes (except FileNotFoundError: false-flagged in the browser), and chained tuple assignment a, b = c = expr dropped the chain tail (phantom weekdays, lost range(7)) — fixed, with the builtins list now generated from dir(builtins) and a message tiebreaker in the final sort so same-line buckets order identically in both engines. 105/105 tests (6 new), 92/92 parity (5 new corpus cases), field parity byte-identical on the dateutil tree, self-scan clean, demo browser-verified.

  • 0.1.14 — cross-file R4, both engines. from x import * no longer blanks a whole file: sibling star imports resolve against the scan tree (static __all__ wins exactly; otherwise every non-underscore module-level name, including plain import x re-exports and sibling submodule names from relative from-imports; transitive chains resolve recursively). Unresolvable targets (external modules, dynamic __all__, import cycles) still keep the legacy whole-file skip — a blind spot that reports a clean bill of health by policy is the v0.1.12 footgun all over again, so partial resolution never guesses. Python engine (Phase A) plus the JS mirror (Phase B): 99/99 tests, 87/87 parity byte-identical (13 new corpus cases: sibling resolve, typo flagged, __all__ honored and empty, underscore not exported, external skip, relative package, transitive chain, mixed skip, cycle skip, plain-import re-export, sibling submodule name, dynamic __all__ skip), self-scan clean, demo browser-verified.

  • 0.1.13 — the data-collection fix, both engines (Sarah's field report, verification-script round). Sarah ran drift 0.1.12 on her own machine and came back with a disagreement: verify_refactor.py flagged 21 appears 3 times, and the 21s were dates in a test matrix (Jan 21, Jun 21, Dec 21). Her defense: "data isn't a promise to future me, it's data. Naming them DAY_21 would make the script worse." She's right — R3's premise is "same literal N times = one constant copied N times, a sync hazard"; three different dates sharing a digit are not one constant and are not meant to stay in sync. Fix: literals nested in collection literals (List/Tuple/Set, at any depth before a statement boundary) are data named by the collection — a date matrix, a port list, a fixture — and don't count toward repetition. Same spirit as the existing dict-value and test-file exclusions. Literals in logic positions (assignments, compares, call args, ranges) still flag: the requests control (3 appears 3 times) and the click positional-3 test both hold. Verified both matrix shapes (row tuples AND D(1, 21) call-arg rows) clean, both engines; Sarah's refactored algiers_sun.py still 0 findings; self-scan clean. 88/88 tests (3 new), 74 parity cases byte-identical.

  • 0.1.12 — field test #7 (Sarah's Algiers sunset calculator, 2 files). Her code: 1 finding, and it's a real one — 360.0 appears 6 times in algiers_sun.py (angle wrap, longitude → day-fraction, hour angle → day-fraction; lines 20/24/36/41/42), warning severity, every occurrence verified by hand. Textbook NOAA-math style, low urgency, correct to flag. The interesting find of the run was in drift itself: --rules magic_number silently scanned NOTHING. The CLI only understood the R1–R4 codes, so any descriptive name (or typo) produced a clean bill of health — the one output a tool like this must never produce by accident. --rules now accepts both forms (R3 / magic_number, case-insensitive) and rejects unknown names with exit code 2; analyze() raises ValueError for API callers. JS engine unchanged (demo runs all rules, no filtering to go wrong). 85/85 tests, 74 parity cases byte-identical, self-scan clean, demo browser-verified.

  • Field test #6 (tenacity, 12 src files, master @ 26f719d 2026-08-06) — second fully clean test, and the config-heaviest corpus yet. tenacity is nothing but configuration objects: every retry/stop/wait strategy is a class whose __init__ stores user settings, and BaseRetrying takes a dozen config-ish parameters. 0 findings — and the silence is verified, not assumed. All 12 files hand-read: every attribute bound in __init__ is read somewhere, the deprecated initial param in wait_exponential_jitter warns and reassigns, the two sys.version_info >= (3, x) idioms are correctly skipped as version tuples, zero env reads exist to misfire on. Planted-bug control on a copied tree: 4 decoys (one per rule) all caught at the right file and line — dead config key, unexpected kwarg, phantom name, repeated magic number. One decoy rejection worth recording: my first R2 decoy was TIMEOUT_BUDGET, and drift correctly did NOT treat it as config — BUDGET is not a config word, and a name that could be money shouldn't be scanned as settings. Renamed to RETRY_OPTIONS, flagged instantly. The v0.1.9–0.1.11 R2 hardening (literal-returning methods, binding shapes, user-set key contracts) holds on the corpus most likely to break it. No version bump: nothing to fix, nothing changed.

  • 0.1.11 — the binding-shape fix, both engines. R2 tracked config receivers only when a name was bound by =; every other binding form (with, async with, for, async for, except-as) left a config-ISH bound name looking like a config object. So async with aiohttp.ClientSession() as options: made options.get("timeout", 30) look like a config read with a fallback, with open(...) as config: made config["retries"] look like an undefined config key, and the for/except twins did the same. Those bound names are now plain locals in both engines — unless the source is itself config-ish (with Config() as config: keeps reading config, for config in cfg: too), mirroring the existing options = fetch() rule for assignments. While probing, the JS parser had a real gap underneath: statement-level parseFor stored the iterable as an ARRAY of expressions and walk() recursed into it as if it were a node, so every load inside a for/async-for iterable was invisible to the browser engine — for x in agen(): never flagged agen. parseFor now unwraps a single iterable and wraps multi-iterables (for z in a(), b():) as a Tuple, exactly like Python's ast, plus a defensive array guard in the walker. 82/82 tests (5 new), parity 69 → 74 cases (5 new), all byte-identical, self-scan clean, field-test corpora unchanged (click/pyjwt/requests identical before and after).

  • 0.1.10 — field test on click (17 src files, the CLI framework underneath httpie). 8 warnings, and this time the warning was the story: click is clean, drift was wrong 7 times out of 8. R3's repetition rule counted values the code had already named. Three new exemptions, one principle — a literal is not magic when the code names it: dict values (the _ansi_colors = {"green": 32, ...} table IS the named constant; 30/32/36 were flagged AT its definition), signature defaults (width: int = 36, col_max: int = 30 — the parameter is the name), and keyword-argument values (stacklevel=3 x5 in core.py — the keyword is the name; the same 3 passed positionally still flags). Also: 1 << 32 bit-magnitude idioms are the named form of 2**32, not a magic 32. The JS engine had a real gap underneath all this: parseArgs parsed defaults and threw them away, so the browser engine couldn't see width=36 at all — defaults are now stored, walked, and exempted in both engines. click: 8 → 4 warnings; the one real finding stands (three undocumented return 127 exit codes in open_url, comment on only the third); the other three are labeled residue (60/24 time conversions in format_eta, Win32 GetStdHandle(-10) handles, ANSI background offset). 77/77 unit tests (4 new), parity corpus 68 → 69 cases, both engines byte-identical, self-scan clean.

  • 0.1.9 — field test on PyJWT (26 files, library + test suite): 9 findings, all of them drift's fault. R2 missed the cleanest defaults pattern in the wild: self.options = self._get_default_options() where the method body is a bare return {dict literal}. PyJWS and PyJWT both build their option tables that way, so every read of verify_signature, require, strict_aud and enforce_minimum_key_length was flagged read-but-never-defined. R2 now tracks literal-returning methods (their keys are definitions when assigned to a config-ish target, including self.options = {...} attribute targets); non-literal calls still demote config-named locals to plain dicts. R3 fixed two things: int literals were reported as floats (7.0 appears 3 times for (x + 7) // 8 — now 7), and the repetition heuristic fired on test data (65537 x6, 1024 x5, leeway 5 x3 are vectors, not unnamed constants) — R3 now skips test files like R2. PyJWT: 9 findings → 3, all labeled residue: the ceil-div-by-8 idiom (7/8 in utils), base64 padding % 4 plus stacklevel=4, and EC coordinate length 32 — the warning doing its job, not noise. 0 real bugs in PyJWT, 2 real bugs in drift, fixed. Parity corpus 62 → 68 cases; 73/73 unit tests; both engines byte-identical; self-scan clean.

  • 0.1.8 — field test on requests (20 files): 2 warnings, both failed manual verification. R3 now skips three more structural idioms: version tuples/lists used directly in comparisons (assert (3, 0, 2) <= (major, minor, patch) < (8, 0, 0), [1, 3, 4] < crypto_version_list), literals compared against a subscript (_ver[0] == 3 is a Python-major check, not a magic constant), and chained HTTP status ranges (400 <= r.status_code < 500). requests: 2 warnings → 1 (the remaining one is four 3s doing byte/count work in encoding detection — the warning doing its job, not noise). The field test also exposed two engine gaps fixed in the JS port: the parser dropped the expressions in raise X() and assert X entirely, so every rule was blind inside them; and R4's use-line lists were emitted in unspecified AST-walk order — both engines now sort them, so findings point at the first source occurrence and parity is deterministic. Parity corpus 60 → 62 cases; 67/67 unit tests; both engines byte-identical; self-scan clean.

  • Field test #3 (python-dotenv 1.2.2, 20 files including its test suite): zero findings and zero misses. Every file hand-checked — no dead config, no phantom names, no dead version idioms; the conditional Popen import in cli.py is guarded by the same sys.platform == "win32" check that uses it, and every os.environ access is external by design. A planted-bug control run confirmed the whole tree is scanned, so the clean result is real. First field test with nothing to fix on either side — the httpie and requests hardening holds.

  • 0.1.7 — field test on httpie (89 files): 8 findings, all three config_drift findings failed manual verification, each for a distinct reason. R2 now resolves the RECEIVER, not just its tail name: an attribute chain (X.config, lexer.options) counts as config only when its ROOT is config-ish or self/cls — lexer.options.get('precise') is a pygments lexer option dict, not the app config, and is no longer flagged. Reads through config objects with an external key contract (env.config.get(...), bare self.get(...) inside a config class) are warning-tier, not errors: httpie's disable_update_warnings and developer_mode are documented user-set keys, and a UserDict config's missing keys default by design. Bare self['k']/self.get('k') inside a config-named class (or a class carrying a DEFAULTS-style dict) now counts as a read, so Config.default_options is no longer dead config. R3 skips version tuples under named constants ((3, 7)) and slice bounds (url[3:]) — both were flagged as repeated magic numbers on httpie. httpie: 8 findings / 2 errors → 5 findings / 0 errors. Parity corpus 56 → 60 cases; 64/64 unit tests; both engines byte-identical; self-scan clean.

  • 0.1.6match statements, end to end. Two real bugs found in the Python reference while probing it: a dict pattern (case {'cmd': c, **rest}:) crashed _add_match_names outright on the current MatchMapping AST shape, and case [a] as whole: silently missed the inner capture a (MatchAs recursion read the wrong field). Both fixed with regression tests. The JS demo engine went from a "tolerant stub" that skipped case bodies (phantom-name false positives on every capture, and case {'cmd': c, **rest}: bodies silently dropped) to a full pattern parser: capture/value/literal/wildcard/sequence (incl. open sequences like case host, *_ if ...:), mapping with **rest, class patterns (positional + keyword, attrs never bind), or-patterns with Python's intersection semantics (case a | b: binds only names bound by EVERY alternative), as patterns, guards, tuple subjects, and nested matches. match/case are now true soft keywords: case = 1, for case in ..., def match(): all parse as ordinary code, matching CPython. Pattern value/class names (Color.RED, Point(x=...)) load their roots exactly like the reference, so a genuinely undefined class name is still flagged. Parity corpus 47 → 56 cases, plus a 21-case edge sweep; both engines byte-identical on all of them. Demo sample now includes a match block.

  • 0.1.5 — the browser demo engine catches up with the CLI. The JS engine's R4 still had the async gap Python fixed in 0.1.4 (async with ... as x bindings were never registered as definitions, so the demo flagged session/resp as phantom names); R2 was a stripped port with the wrong bucket semantics (os.environ['K'] errored as a hard read, .get(k, default) errored instead of warning, os.getenv was silently ignored, no external-source tracking, no aliases, no scope awareness, no dash/underscore normalization, no env-doc awareness). R2 is now a faithful port of the Python reference: soft/ext/env/list read buckets, scope-keyed aliases/ext_names/plain_vars, comprehension shadowing, config-source classification, handler maps, super-init and pluginargument defs, get_option/set_option tracking, test-file skip, .env + .env.example docs, and dash/underscore normalization. R1 got the v0.1.3 treatment too: attribute calls on unresolvable receivers (unittest.main, obj.x) are never guessed against module-level functions, self./cls. calls resolve against the enclosing class's methods (inherited included), ClassName.method(...) checks class methods, and cross-file @pytest.fixture calls are treated as closure calls. The JS parser also learned the constructs real code uses: match/case as soft-keyword names (for case in ...), set comprehensions and bare generator expressions as sole call args, not in comparisons, | ^ & << >> operators, implicit adjacent-string concatenation (f"a" f"b"), yield a, b, lambda params without annotation-eating (lambda f: (f.x, f.y)), attribute access on keywords (.match(), and a comment-handling bug that swallowed the newline after stmt # comment and desynced the whole indent stack. f-string scans no longer treat attribute tails (kw.arg) or dotted calls ('.'.join(...)) as bare loads. The parity corpus grew 25 → 47 cases; both engines produce byte-identical findings on all 47, and the JS engine now self-scans drift.py + test_drift.py clean (parse errors 71 → 0). JS engine version now tracks the CLI (0.1.6).

  • 0.1.4 — streamlink field test: 184 findings, every one verified by hand. Zero real bugs in streamlink, but drift had seven distinct false-positive classes hiding them. Fixed, with regression tests: - R4: async with ... as x (including tuple unpacking), async for x in ..., and match pattern bindings were never registered as definitions — that alone was 17 phantom-name errors (nursery, frame_id, cm, ...). Sphinx-injected tags in docs/conf.py is now known.

    • R1: bare-name calls resolved against same-named functions anywhere in the project, ignoring the file's own imports (get_version from versioningit resolved to a CDP method). Resolution is now import-aware: external imports are skipped, same-project imports resolve to the right file, and @pytest.fixture-decorated callees are never guessed (a direct call to a fixture name is the fixture's returned closure).
    • R2: inline dict literals passed to config-ish constructors (super().__init__({...}), *Options(...)) count as definitions; @pluginargument("key") decorators define keys; get_option/set_option/ .set/.update are tracked; dash and underscore key forms are the same key (streamlink Options normalizes _-); handler maps (_MAP_GETTERS/_MAP_SETTERS-style dicts of key→callable) are wired keys, not dead config; a local options = fetch(...) is a plain dict, not config, but dict(cfg) copies stay config; test files are skipped (they deliberately read missing keys). Result on streamlink: 184 → 108 findings, errors 23 → 0 — and the 4 remaining dead-config warnings are REAL: sbscokr's id option and twitch's disable-ads/disable-hosting/disable-reruns are declared but never read anywhere in the plugin. Confirmed by reading the code. Known limitations left: deprecated-alias maps and setter-mapped options whose reads are fully dynamic (soop's afreeca-*, _OPTIONS_HTTP_ATTRS).
  • 0.1.3 — R1 no longer misattributes calls: self./cls. calls resolve against the enclosing class's own methods first (a same-named module-level function is a different callee), inherited methods count, and attribute calls on unresolvable receivers (unittest.main, obj.x) are skipped instead of being guessed against module-level functions. ClassName.method(...) calls are checked against the class's method signature. Crossedge's 7 exit_prices(position_side=...) errors were all false positives — the PaperBot method accepts that kwarg; errors 7 → 0.

  • 0.1.2 — R2 precision pass driven by the crossedge field test (215 findings, 93 of them config-drift false positives): external config sources (json.load / yaml / tomllib / os.environ) are no longer treated as in-repo definitions; DEFAULT_SETTINGS-style constants count as config; alias receivers are scope-aware (a loop variable reusing a config alias's name in another function is not fooled); .get(k, default) downgrades to a warning; .env.example counts as env documentation. Crossedge errors went 89 → 0; the 8 remaining dead-config warnings were confirmed real.

  • 0.1.1 — R4 builtins generated from the interpreter instead of a hand-typed list (FileNotFoundError regression, found on the httpie field test); R2 learned os.environ and .env files.

Why it exists

Every rule here comes from a bug I actually shipped or fixed in real trading code. The fixes kept teaching the same lesson: code and config drift apart quietly, and the crash comes later, at 3am, in production. drift is the 3am-crash insurance I wish I'd had.

— Rosie

Field report (v0.1.4, streamlink)

streamlink is a mature, heavily tested project — a hard target for a young linter. 184 findings. Verified every one by reading the code:

  • 6 R1 errors → all drift bugs (import-blind resolution, fixture callees)
  • 17 R4 errors → all drift bugs (async-with / async-for / match bindings)
  • 46 R2 errors → all drift blind spots (inline constructor dicts, pluginargument decorators, dash/underscore key normalization)
  • 4 R2 warnings → REAL FINDINGS: options declared but never read: sbscokr id, twitch disable-ads / disable-hosting / disable-reruns
  • 98 R3 warnings → mostly idiomatic hardcoded values (status codes, timeouts); low signal by design, warning-only

Zero NameErrors, zero wrong-kwarg crashes — the right answer for a project with 200+ contributors and CI on every PR. And the 4 dead options are exactly what drift is for: flags users can pass that do nothing.

— field notes, 2026-08-14

Field report (v0.1.1)

Ran against two real open-source projects and one trading bot in the wild.

httpie (mature, heavily tested): 39 findings → 33 after the builtins fix. Every remaining finding verified as a false positive of a known class (subprocess.run misattributed to a same-named module function; env vars read but set externally; pygments lexer options read via .get()). Zero real bugs — the right answer for a well-maintained codebase, and a good calibration check.

InstaPy (bot, less maintained): 52 findings, same false-positive classes. The config cluster pointed at a genuine robustness gap (config keys expected with no schema), even though none were in-repo literal bugs.

crypto-paper-bot (a trading bot mid-refactor): 215 findings. 13 phantom names — 3 were drift's own builtins bug, 10 were real missing imports left behind by a monolithic → multi-file split: urllib, asdict, logger, today_key, fetch_candles, diagnostics, time used but never imported. Every one is a NameError waiting for its code path to run. Fix: seven one-line imports (one lazy import to avoid a circular dependency) + one decision item (diagnostics() was never ported out of the monolith).

Also learned (and documented): self.method() calls resolve to the wrong same-named module function — R1 should prefer the enclosing class's method.

— field notes, 2026-08-13

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drift_linter-0.1.15.tar.gz (42.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drift_linter-0.1.15-py3-none-any.whl (31.7 kB view details)

Uploaded Python 3

File details

Details for the file drift_linter-0.1.15.tar.gz.

File metadata

  • Download URL: drift_linter-0.1.15.tar.gz
  • Upload date:
  • Size: 42.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.6

File hashes

Hashes for drift_linter-0.1.15.tar.gz
Algorithm Hash digest
SHA256 09988a21c1894bf8e0570fa36d475af9340960639aa3b1a28d99d5716ac8f4e9
MD5 b07591b679a2a54c2c23fcf5291d2646
BLAKE2b-256 4239f6a916acaee2e1d11fc84f000db00ccaca534bf30e843ac8889da073ecda

See more details on using hashes here.

File details

Details for the file drift_linter-0.1.15-py3-none-any.whl.

File metadata

  • Download URL: drift_linter-0.1.15-py3-none-any.whl
  • Upload date:
  • Size: 31.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.6

File hashes

Hashes for drift_linter-0.1.15-py3-none-any.whl
Algorithm Hash digest
SHA256 f87946b71fd0718471069b6fcfe9e32a15a83df361f381e4593b143c9772ae38
MD5 078d2f382aa856f93d1cc74c30ed6bc6
BLAKE2b-256 413278170b233f35fe9f97b6005809a9bab8149eb9dfabddb07dac7bf1404f1c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.15 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page