forkcast-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ node_modules/
2
+ dist/
3
+ .forkcast/
4
+ coverage/
5
+ *.log
6
+ .DS_Store
7
+ .env
8
+ .env.local
9
+ *.tgz
10
+
11
+ # Python
12
+ .venv/
13
+ __pycache__/
14
+ *.py[cod]
15
+ *.egg-info/
16
+ .pytest_cache/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ python/dist/
20
+ python/build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alik Agarwala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: forkcast-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Forkcast: self-hosted A/B testing & feature flags. Sticky bucketing identical to the JS SDK, targeting, kill switch, FastAPI and Flask integrations.
5
+ Project-URL: Homepage, https://github.com/Alik-Agarwala/forkcast
6
+ Project-URL: Documentation, https://github.com/Alik-Agarwala/forkcast/blob/main/docs/python.md
7
+ Project-URL: Repository, https://github.com/Alik-Agarwala/forkcast
8
+ Project-URL: Issues, https://github.com/Alik-Agarwala/forkcast/issues
9
+ Project-URL: Changelog, https://github.com/Alik-Agarwala/forkcast/blob/main/CHANGELOG.md
10
+ Author: Prantika Ghosh
11
+ Maintainer: Alik Agarwala
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: a/b testing,ab-testing,experiments,fastapi,feature-flags,feature-toggles,flask,kill-switch,rollout,split-testing
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Framework :: FastAPI
17
+ Classifier: Framework :: Flask
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
+ Classifier: Topic :: Software Development :: Libraries
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1; extra == 'dev'
32
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
33
+ Requires-Dist: flask>=2.2; extra == 'dev'
34
+ Requires-Dist: httpx>=0.24; extra == 'dev'
35
+ Requires-Dist: mypy>=1.8; extra == 'dev'
36
+ Requires-Dist: pytest>=7; extra == 'dev'
37
+ Requires-Dist: ruff>=0.5; extra == 'dev'
38
+ Requires-Dist: twine>=5; extra == 'dev'
39
+ Provides-Extra: fastapi
40
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
41
+ Provides-Extra: flask
42
+ Requires-Dist: flask>=2.2; extra == 'flask'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # Forkcast for Python
46
+
47
+ [![PyPI](https://img.shields.io/pypi/v/forkcast-sdk.svg)](https://pypi.org/project/forkcast-sdk/)
48
+ [![Python](https://img.shields.io/pypi/pyversions/forkcast-sdk.svg)](https://pypi.org/project/forkcast-sdk/)
49
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Alik-Agarwala/forkcast/blob/main/LICENSE)
50
+
51
+ The Python SDK for [Forkcast](https://github.com/Alik-Agarwala/forkcast), the self-hosted A/B testing and feature flag tool. Use it in **FastAPI**, **Flask**, Django, scripts and workers.
52
+
53
+ - Same buckets as the JavaScript SDK. A user gets the same treatment from your Python API and your React frontend.
54
+ - Local evaluation. Config is fetched once and kept fresh in the background, so every `get_treatment()` is pure hashing with no network call.
55
+ - Works offline. If the server is unreachable, everything falls back to `C` (control) and your app never crashes.
56
+ - No dependencies. Only the Python standard library. FastAPI and Flask helpers are optional.
57
+ - Typed (`py.typed`). Python 3.10+.
58
+
59
+ ```bash
60
+ pip install forkcast-sdk
61
+ ```
62
+
63
+ You import it as `forkcast`.
64
+
65
+ ## Quick start
66
+
67
+ 1. Start a Forkcast server and dashboard (needs Node.js 18+):
68
+
69
+ ```bash
70
+ npx forkcast start
71
+ ```
72
+
73
+ Open http://localhost:4400, create an experiment (say `new-checkout`) and copy a client key from **Environments**.
74
+
75
+ 2. Ask for a treatment in Python:
76
+
77
+ ```python
78
+ from forkcast import Forkcast
79
+
80
+ forkcast = Forkcast(
81
+ api_url="http://localhost:4400",
82
+ client_key="fc_prod_...", # from the dashboard
83
+ )
84
+
85
+ fc = forkcast.for_user({"id": "user-42", "country": "IN", "plan": "pro"})
86
+
87
+ if fc.get_treatment("new-checkout") == "T1":
88
+ show_one_click_checkout()
89
+ else: # "C", the control
90
+ show_cart_page()
91
+
92
+ fc.track("purchase", value=49.99) # conversion for the results page
93
+ ```
94
+
95
+ 3. Change traffic, targeting or the kill switch in the dashboard. Python picks it up within 30 seconds (or instantly with `realtime=True`). There's no redeploy.
96
+
97
+ ## FastAPI
98
+
99
+ ```python
100
+ from fastapi import Depends, FastAPI, Request
101
+ from forkcast import Forkcast, ScopedForkcast
102
+ from forkcast.integrations.fastapi import forkcast_dependency, forkcast_lifespan
103
+
104
+ forkcast = Forkcast(api_url="http://localhost:4400", client_key="fc_prod_...")
105
+
106
+
107
+ def current_user(request: Request) -> dict:
108
+ return {"id": request.headers.get("x-user-id"), "country": request.headers.get("cf-ipcountry")}
109
+
110
+
111
+ get_forkcast = forkcast_dependency(forkcast, current_user)
112
+ app = FastAPI(lifespan=forkcast_lifespan(forkcast)) # flushes events on shutdown
113
+
114
+
115
+ @app.get("/checkout")
116
+ def checkout(fc: ScopedForkcast = Depends(get_forkcast)):
117
+ return {"one_click": fc.get_treatment("new-checkout") == "T1"}
118
+ ```
119
+
120
+ ## Flask
121
+
122
+ ```python
123
+ from flask import Flask, request, session
124
+ from forkcast import Forkcast
125
+ from forkcast.integrations.flask import FlaskForkcast, current_forkcast
126
+
127
+ app = Flask(__name__)
128
+ forkcast = Forkcast(api_url="http://localhost:4400", client_key="fc_prod_...")
129
+ FlaskForkcast(
130
+ app,
131
+ forkcast,
132
+ get_user=lambda: {
133
+ "id": session.get("user_id"),
134
+ "country": request.headers.get("CF-IPCountry"),
135
+ },
136
+ )
137
+
138
+
139
+ @app.get("/checkout")
140
+ def checkout():
141
+ if current_forkcast.get_treatment("new-checkout") == "T1":
142
+ return "one-click checkout"
143
+ return "cart page"
144
+ ```
145
+
146
+ In Jinja templates you can write `{% if forkcast.is_enabled("dark-mode") %}`.
147
+
148
+ ## More
149
+
150
+ - Full Python guide, covering every option and method, Django, gunicorn, testing and local mode: [docs/python.md](https://github.com/Alik-Agarwala/forkcast/blob/main/docs/python.md)
151
+ - Concepts (C/T1/T2, traffic, targeting, environments): [docs/concepts.md](https://github.com/Alik-Agarwala/forkcast/blob/main/docs/concepts.md)
152
+ - Main project, JavaScript SDK and dashboard: [github.com/Alik-Agarwala/forkcast](https://github.com/Alik-Agarwala/forkcast)
153
+
154
+ MIT licensed.
@@ -0,0 +1,110 @@
1
+ # Forkcast for Python
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/forkcast-sdk.svg)](https://pypi.org/project/forkcast-sdk/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/forkcast-sdk.svg)](https://pypi.org/project/forkcast-sdk/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Alik-Agarwala/forkcast/blob/main/LICENSE)
6
+
7
+ The Python SDK for [Forkcast](https://github.com/Alik-Agarwala/forkcast), the self-hosted A/B testing and feature flag tool. Use it in **FastAPI**, **Flask**, Django, scripts and workers.
8
+
9
+ - Same buckets as the JavaScript SDK. A user gets the same treatment from your Python API and your React frontend.
10
+ - Local evaluation. Config is fetched once and kept fresh in the background, so every `get_treatment()` is pure hashing with no network call.
11
+ - Works offline. If the server is unreachable, everything falls back to `C` (control) and your app never crashes.
12
+ - No dependencies. Only the Python standard library. FastAPI and Flask helpers are optional.
13
+ - Typed (`py.typed`). Python 3.10+.
14
+
15
+ ```bash
16
+ pip install forkcast-sdk
17
+ ```
18
+
19
+ You import it as `forkcast`.
20
+
21
+ ## Quick start
22
+
23
+ 1. Start a Forkcast server and dashboard (needs Node.js 18+):
24
+
25
+ ```bash
26
+ npx forkcast start
27
+ ```
28
+
29
+ Open http://localhost:4400, create an experiment (say `new-checkout`) and copy a client key from **Environments**.
30
+
31
+ 2. Ask for a treatment in Python:
32
+
33
+ ```python
34
+ from forkcast import Forkcast
35
+
36
+ forkcast = Forkcast(
37
+ api_url="http://localhost:4400",
38
+ client_key="fc_prod_...", # from the dashboard
39
+ )
40
+
41
+ fc = forkcast.for_user({"id": "user-42", "country": "IN", "plan": "pro"})
42
+
43
+ if fc.get_treatment("new-checkout") == "T1":
44
+ show_one_click_checkout()
45
+ else: # "C", the control
46
+ show_cart_page()
47
+
48
+ fc.track("purchase", value=49.99) # conversion for the results page
49
+ ```
50
+
51
+ 3. Change traffic, targeting or the kill switch in the dashboard. Python picks it up within 30 seconds (or instantly with `realtime=True`). There's no redeploy.
52
+
53
+ ## FastAPI
54
+
55
+ ```python
56
+ from fastapi import Depends, FastAPI, Request
57
+ from forkcast import Forkcast, ScopedForkcast
58
+ from forkcast.integrations.fastapi import forkcast_dependency, forkcast_lifespan
59
+
60
+ forkcast = Forkcast(api_url="http://localhost:4400", client_key="fc_prod_...")
61
+
62
+
63
+ def current_user(request: Request) -> dict:
64
+ return {"id": request.headers.get("x-user-id"), "country": request.headers.get("cf-ipcountry")}
65
+
66
+
67
+ get_forkcast = forkcast_dependency(forkcast, current_user)
68
+ app = FastAPI(lifespan=forkcast_lifespan(forkcast)) # flushes events on shutdown
69
+
70
+
71
+ @app.get("/checkout")
72
+ def checkout(fc: ScopedForkcast = Depends(get_forkcast)):
73
+ return {"one_click": fc.get_treatment("new-checkout") == "T1"}
74
+ ```
75
+
76
+ ## Flask
77
+
78
+ ```python
79
+ from flask import Flask, request, session
80
+ from forkcast import Forkcast
81
+ from forkcast.integrations.flask import FlaskForkcast, current_forkcast
82
+
83
+ app = Flask(__name__)
84
+ forkcast = Forkcast(api_url="http://localhost:4400", client_key="fc_prod_...")
85
+ FlaskForkcast(
86
+ app,
87
+ forkcast,
88
+ get_user=lambda: {
89
+ "id": session.get("user_id"),
90
+ "country": request.headers.get("CF-IPCountry"),
91
+ },
92
+ )
93
+
94
+
95
+ @app.get("/checkout")
96
+ def checkout():
97
+ if current_forkcast.get_treatment("new-checkout") == "T1":
98
+ return "one-click checkout"
99
+ return "cart page"
100
+ ```
101
+
102
+ In Jinja templates you can write `{% if forkcast.is_enabled("dark-mode") %}`.
103
+
104
+ ## More
105
+
106
+ - Full Python guide, covering every option and method, Django, gunicorn, testing and local mode: [docs/python.md](https://github.com/Alik-Agarwala/forkcast/blob/main/docs/python.md)
107
+ - Concepts (C/T1/T2, traffic, targeting, environments): [docs/concepts.md](https://github.com/Alik-Agarwala/forkcast/blob/main/docs/concepts.md)
108
+ - Main project, JavaScript SDK and dashboard: [github.com/Alik-Agarwala/forkcast](https://github.com/Alik-Agarwala/forkcast)
109
+
110
+ MIT licensed.
@@ -0,0 +1,97 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "forkcast-sdk"
7
+ dynamic = ["version"]
8
+ description = "Python SDK for Forkcast: self-hosted A/B testing & feature flags. Sticky bucketing identical to the JS SDK, targeting, kill switch, FastAPI and Flask integrations."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Prantika Ghosh" }]
14
+ maintainers = [{ name = "Alik Agarwala" }]
15
+ keywords = [
16
+ "ab-testing",
17
+ "a/b testing",
18
+ "experiments",
19
+ "feature-flags",
20
+ "feature-toggles",
21
+ "split-testing",
22
+ "rollout",
23
+ "kill-switch",
24
+ "fastapi",
25
+ "flask",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Framework :: FastAPI",
30
+ "Framework :: Flask",
31
+ "Intended Audience :: Developers",
32
+ "Operating System :: OS Independent",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3 :: Only",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Programming Language :: Python :: 3.11",
37
+ "Programming Language :: Python :: 3.12",
38
+ "Programming Language :: Python :: 3.13",
39
+ "Programming Language :: Python :: 3.14",
40
+ "Topic :: Software Development :: Libraries",
41
+ "Typing :: Typed",
42
+ ]
43
+ dependencies = []
44
+
45
+ [project.optional-dependencies]
46
+ fastapi = ["fastapi>=0.100"]
47
+ flask = ["flask>=2.2"]
48
+ dev = [
49
+ "pytest>=7",
50
+ "fastapi>=0.100",
51
+ "httpx>=0.24",
52
+ "flask>=2.2",
53
+ "ruff>=0.5",
54
+ "mypy>=1.8",
55
+ "build>=1",
56
+ "twine>=5",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/Alik-Agarwala/forkcast"
61
+ Documentation = "https://github.com/Alik-Agarwala/forkcast/blob/main/docs/python.md"
62
+ Repository = "https://github.com/Alik-Agarwala/forkcast"
63
+ Issues = "https://github.com/Alik-Agarwala/forkcast/issues"
64
+ Changelog = "https://github.com/Alik-Agarwala/forkcast/blob/main/CHANGELOG.md"
65
+
66
+ [tool.hatch.version]
67
+ path = "src/forkcast/_version.py"
68
+
69
+ [tool.hatch.build.targets.wheel]
70
+ packages = ["src/forkcast"]
71
+
72
+ [tool.hatch.build.targets.sdist]
73
+ include = ["src/forkcast", "tests", "README.md", "LICENSE", "pyproject.toml"]
74
+
75
+ [tool.pytest.ini_options]
76
+ testpaths = ["tests"]
77
+ addopts = "-q"
78
+ filterwarnings = ["ignore:Using `httpx` with `starlette.testclient`"]
79
+
80
+ [tool.ruff]
81
+ line-length = 120
82
+ target-version = "py310"
83
+ src = ["src", "tests"]
84
+
85
+ [tool.ruff.lint]
86
+ select = ["E", "F", "W", "I", "B", "UP", "BLE"]
87
+ ignore = ["UP006", "UP007", "UP035", "UP045"]
88
+
89
+ [tool.ruff.lint.per-file-ignores]
90
+ "tests/*" = ["B008"]
91
+ "../examples/*" = ["B008"]
92
+
93
+ [tool.mypy]
94
+ python_version = "3.10"
95
+ strict = true
96
+ packages = ["forkcast"]
97
+ mypy_path = "src"
@@ -0,0 +1,52 @@
1
+ """Forkcast: self-hosted A/B testing and feature flags.
2
+
3
+ Python SDK for the Forkcast server. Evaluation is local and deterministic, and
4
+ buckets users exactly like the JavaScript SDK, so a user gets the same
5
+ treatment from your Python API and your React frontend.
6
+
7
+ from forkcast import Forkcast
8
+
9
+ forkcast = Forkcast(api_url="http://localhost:4400", client_key="fc_prod_...")
10
+ fc = forkcast.for_user({"id": "user-42", "country": "IN"})
11
+ if fc.get_treatment("new-checkout") == "T1":
12
+ ...
13
+
14
+ Framework helpers live in ``forkcast.integrations.fastapi`` and
15
+ ``forkcast.integrations.flask``.
16
+ """
17
+
18
+ from ._version import __version__
19
+ from .client import (
20
+ OVERRIDE_QUERY_PARAM,
21
+ Forkcast,
22
+ ForkcastError,
23
+ ScopedForkcast,
24
+ create_forkcast,
25
+ parse_override_param,
26
+ )
27
+ from .evaluate import evaluate, pick_variant
28
+ from .hashing import BUCKETS, bucket_of, murmur3
29
+ from .targeting import OPERATORS, compare_semver, get_attribute, match_audience, match_condition
30
+ from .types import CONTROL, Evaluation
31
+
32
+ __all__ = [
33
+ "BUCKETS",
34
+ "CONTROL",
35
+ "OPERATORS",
36
+ "OVERRIDE_QUERY_PARAM",
37
+ "Evaluation",
38
+ "Forkcast",
39
+ "ForkcastError",
40
+ "ScopedForkcast",
41
+ "__version__",
42
+ "bucket_of",
43
+ "compare_semver",
44
+ "create_forkcast",
45
+ "evaluate",
46
+ "get_attribute",
47
+ "match_audience",
48
+ "match_condition",
49
+ "murmur3",
50
+ "parse_override_param",
51
+ "pick_variant",
52
+ ]
@@ -0,0 +1,138 @@
1
+ """JavaScript value semantics needed for exact parity with the JS SDK.
2
+
3
+ Targeting rules are evaluated by both SDKs, so a condition such as
4
+ ``plan eq "pro"`` or ``age gte 18`` must give the same answer in Python and
5
+ JavaScript. JS converts values with ``String(x)`` and ``Number(x)``; these
6
+ helpers reproduce those conversions for JSON-like Python values.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import re
13
+ from decimal import Decimal
14
+ from typing import Any
15
+
16
+
17
+ class _Undefined:
18
+ """Stand-in for JavaScript ``undefined`` (a missing attribute or value)."""
19
+
20
+ _instance: _Undefined | None = None
21
+
22
+ def __new__(cls) -> _Undefined:
23
+ if cls._instance is None:
24
+ cls._instance = super().__new__(cls)
25
+ return cls._instance
26
+
27
+ def __repr__(self) -> str:
28
+ return "UNDEFINED"
29
+
30
+ def __bool__(self) -> bool:
31
+ return False
32
+
33
+
34
+ UNDEFINED: Any = _Undefined()
35
+
36
+
37
+ def is_nullish(value: Any) -> bool:
38
+ """``value == null`` in JavaScript (``null`` or ``undefined``)."""
39
+ return value is None or value is UNDEFINED
40
+
41
+
42
+ def _number_to_string(value: float) -> str:
43
+ """``Number.prototype.toString()`` for a finite, non-zero float."""
44
+ sign = "-" if value < 0 else ""
45
+ # repr() gives the shortest round-tripping digits, exactly like JS.
46
+ _, digits_tuple, exponent = Decimal(repr(abs(value))).as_tuple()
47
+ digits = "".join(str(d) for d in digits_tuple).rstrip("0") or "0"
48
+ k = len(digits)
49
+ # Trailing zeros were stripped, so recompute the decimal point position.
50
+ n = int(exponent) + len("".join(str(d) for d in digits_tuple))
51
+ if k <= n <= 21:
52
+ return sign + digits + "0" * (n - k)
53
+ if 0 < n <= 21:
54
+ return sign + digits[:n] + "." + digits[n:]
55
+ if -6 < n <= 0:
56
+ return sign + "0." + "0" * (-n) + digits
57
+ e = n - 1
58
+ mantissa = digits[0] + ("." + digits[1:] if k > 1 else "")
59
+ return f"{sign}{mantissa}e{'+' if e > 0 else '-'}{abs(e)}"
60
+
61
+
62
+ def js_string(value: Any) -> str:
63
+ """``String(value)`` in JavaScript."""
64
+ if value is UNDEFINED:
65
+ return "undefined"
66
+ if value is None:
67
+ return "null"
68
+ if isinstance(value, bool):
69
+ return "true" if value else "false"
70
+ if isinstance(value, int):
71
+ # JSON numbers are doubles in JS, so huge integers lose precision there.
72
+ return str(value) if abs(value) <= 2**53 else js_string(float(value))
73
+ if isinstance(value, float):
74
+ if math.isnan(value):
75
+ return "NaN"
76
+ if math.isinf(value):
77
+ return "Infinity" if value > 0 else "-Infinity"
78
+ if value == 0:
79
+ return "0"
80
+ return _number_to_string(value)
81
+ if isinstance(value, str):
82
+ return value
83
+ if isinstance(value, (list, tuple)):
84
+ return ",".join("" if is_nullish(v) else js_string(v) for v in value)
85
+ if isinstance(value, dict):
86
+ return "[object Object]"
87
+ return str(value)
88
+
89
+
90
+ _DECIMAL = re.compile(r"^[+-]?(?:\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)$")
91
+ _PREFIXED = {"0x": 16, "0o": 8, "0b": 2}
92
+
93
+
94
+ def js_number(value: Any) -> float:
95
+ """``Number(value)`` in JavaScript."""
96
+ if value is UNDEFINED:
97
+ return math.nan
98
+ if value is None:
99
+ return 0.0
100
+ if isinstance(value, bool):
101
+ return 1.0 if value else 0.0
102
+ if isinstance(value, (int, float)):
103
+ try:
104
+ return float(value)
105
+ except OverflowError:
106
+ return math.inf if value > 0 else -math.inf
107
+ if isinstance(value, str):
108
+ s = value.strip()
109
+ if s == "":
110
+ return 0.0
111
+ if s in ("Infinity", "+Infinity"):
112
+ return math.inf
113
+ if s == "-Infinity":
114
+ return -math.inf
115
+ base = _PREFIXED.get(s[:2].lower())
116
+ if base:
117
+ try:
118
+ return float(int(s[2:], base))
119
+ except ValueError:
120
+ return math.nan
121
+ if _DECIMAL.match(s):
122
+ try:
123
+ return float(s)
124
+ except (ValueError, OverflowError):
125
+ return math.nan
126
+ return math.nan
127
+ if isinstance(value, (list, tuple)):
128
+ return js_number(js_string(value))
129
+ return math.nan
130
+
131
+
132
+ _LEADING_INT = re.compile(r"^\s*([+-]?\d+)")
133
+
134
+
135
+ def js_parse_int(value: str) -> int:
136
+ """``parseInt(value, 10) || 0`` in JavaScript."""
137
+ m = _LEADING_INT.match(value)
138
+ return int(m.group(1)) if m else 0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"