nucore 0.2.0__py3-none-any.whl
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.
- nu/__init__.py +216 -0
- nu/_config/__init__.py +50 -0
- nu/_config/branding/__init__.py +28 -0
- nu/_config/branding/ansi.py +96 -0
- nu/_config/branding/header.py +80 -0
- nu/_config/branding/logo.py +61 -0
- nu/_config/branding/palette.py +11 -0
- nu/_config/config.py +89 -0
- nu/_config/constants.py +33 -0
- nu/_config/telemetry.py +149 -0
- nu/context/__init__.py +85 -0
- nu/context/_refs.py +46 -0
- nu/context/attrs/__init__.py +47 -0
- nu/context/attrs/interactions.py +336 -0
- nu/context/attrs/refs.py +192 -0
- nu/context/fabric/__init__.py +37 -0
- nu/context/fabric/lifecycle.py +560 -0
- nu/context/fabric/protocol.py +90 -0
- nu/context/fabric/queries.py +63 -0
- nu/context/fabric/refs.py +116 -0
- nu/core/AUTHORING.md +141 -0
- nu/core/__init__.py +269 -0
- nu/core/_stream.py +51 -0
- nu/core/access.py +556 -0
- nu/core/arithmetic.py +680 -0
- nu/core/bitwise.py +317 -0
- nu/core/cast.py +618 -0
- nu/core/cast_fns.py +109 -0
- nu/core/comparison.py +391 -0
- nu/core/conditional.py +184 -0
- nu/core/dynamic.py +94 -0
- nu/core/io.py +371 -0
- nu/core/iteration.py +249 -0
- nu/core/literal.py +73 -0
- nu/core/logical.py +231 -0
- nu/core/reduction.py +468 -0
- nu/core/reflection.py +391 -0
- nu/core/repr.py +415 -0
- nu/core/sentinel.py +185 -0
- nu/core/transform.py +397 -0
- nu/domains/__init__.py +1 -0
- nu/domains/service/__init__.py +15 -0
- nu/domains/service/dsl.py +112 -0
- nu/domains/service/refs.py +59 -0
- nu/domains/shape/__init__.py +98 -0
- nu/domains/shape/dsl.py +252 -0
- nu/domains/shape/forms/__init__.py +42 -0
- nu/domains/shape/forms/collection.py +124 -0
- nu/domains/shape/forms/item.py +99 -0
- nu/domains/shape/forms/mapping.py +49 -0
- nu/domains/shape/forms/sequence.py +45 -0
- nu/domains/shape/forms/set_.py +45 -0
- nu/domains/shape/interactions.py +282 -0
- nu/domains/shape/refs/__init__.py +66 -0
- nu/domains/shape/refs/base.py +177 -0
- nu/domains/shape/refs/item.py +46 -0
- nu/domains/shape/refs/mapping.py +74 -0
- nu/domains/shape/refs/sequence.py +86 -0
- nu/domains/shape/refs/set_.py +61 -0
- nu/domains/shape/refs/shape.py +115 -0
- nu/domains/shape/refs/shapes_mapping.py +113 -0
- nu/domains/shape/refs/shapes_sequence.py +115 -0
- nu/engine/__init__.py +54 -0
- nu/engine/compilation/__init__.py +32 -0
- nu/engine/compilation/attribution.py +65 -0
- nu/engine/compilation/compile.py +51 -0
- nu/engine/compilation/emit.py +37 -0
- nu/engine/compilation/index.py +52 -0
- nu/engine/compilation/program.py +217 -0
- nu/engine/evaluation/__init__.py +13 -0
- nu/engine/evaluation/protocol.py +49 -0
- nu/engine/structure/__init__.py +41 -0
- nu/engine/structure/attribute.py +111 -0
- nu/engine/structure/exceptions.py +22 -0
- nu/engine/structure/schema.py +133 -0
- nu/engine/structure/term.py +204 -0
- nu/engine/structure/types.py +20 -0
- nu/engine/validation/__init__.py +38 -0
- nu/engine/validation/law.py +186 -0
- nu/engine/validation/predicate.py +59 -0
- nu/factory/__init__.py +24 -0
- nu/factory/core.py +214 -0
- nu/factory/host.py +67 -0
- nu/flows/__init__.py +64 -0
- nu/flows/control.py +472 -0
- nu/flows/noop.py +55 -0
- nu/flows/parallel/__init__.py +53 -0
- nu/flows/parallel/_base.py +66 -0
- nu/flows/parallel/_scheduling.py +319 -0
- nu/flows/parallel/anyn.py +64 -0
- nu/flows/parallel/laws.py +106 -0
- nu/flows/parallel/parallel.py +163 -0
- nu/flows/parallel/race.py +62 -0
- nu/flows/raise_.py +109 -0
- nu/flows/react.py +282 -0
- nu/flows/strategy.py +58 -0
- nu/flows/stream.py +154 -0
- nu/forms/__init__.py +61 -0
- nu/forms/collections/__init__.py +23 -0
- nu/forms/collections/abc/__init__.py +145 -0
- nu/forms/collections/abc/collection.py +67 -0
- nu/forms/collections/abc/container.py +57 -0
- nu/forms/collections/abc/iterable.py +93 -0
- nu/forms/collections/abc/mapping.py +514 -0
- nu/forms/collections/abc/mapping_interactions.py +650 -0
- nu/forms/collections/abc/sequence.py +414 -0
- nu/forms/collections/abc/sequence_interactions.py +650 -0
- nu/forms/collections/abc/set_.py +589 -0
- nu/forms/collections/abc/set_interactions.py +966 -0
- nu/forms/collections/abc/shared_interactions.py +59 -0
- nu/forms/collections/abc/sized.py +45 -0
- nu/forms/collections/abc/sliceable.py +70 -0
- nu/forms/collections/dict_.py +421 -0
- nu/forms/collections/iterator_.py +115 -0
- nu/forms/collections/list_.py +489 -0
- nu/forms/collections/set_.py +518 -0
- nu/forms/collections/tuple_.py +352 -0
- nu/forms/collections/views.py +263 -0
- nu/forms/primitives/__init__.py +198 -0
- nu/forms/primitives/any_.py +1125 -0
- nu/forms/primitives/bool_.py +266 -0
- nu/forms/primitives/bytes_.py +1160 -0
- nu/forms/primitives/bytes_interactions.py +1714 -0
- nu/forms/primitives/float_.py +657 -0
- nu/forms/primitives/int_.py +974 -0
- nu/forms/primitives/none_.py +143 -0
- nu/forms/primitives/sentinel_.py +57 -0
- nu/forms/primitives/str_.py +1333 -0
- nu/forms/primitives/str_interactions.py +1896 -0
- nu/info/__init__.py +129 -0
- nu/info/builder.py +116 -0
- nu/info/call.py +167 -0
- nu/info/core/__init__.py +18 -0
- nu/info/core/contract/__init__.py +38 -0
- nu/info/core/contract/call.py +68 -0
- nu/info/core/contract/check.py +113 -0
- nu/info/core/contract/sections.py +20 -0
- nu/info/core/docstring/__init__.py +36 -0
- nu/info/core/docstring/args.py +57 -0
- nu/info/core/docstring/blocks.py +113 -0
- nu/info/core/docstring/example.py +121 -0
- nu/info/core/docstring/notes.py +33 -0
- nu/info/core/source/__init__.py +35 -0
- nu/info/core/source/code.py +51 -0
- nu/info/core/source/module.py +61 -0
- nu/info/core/source/mro.py +136 -0
- nu/info/core/source/signature.py +117 -0
- nu/info/core/source/unpack.py +49 -0
- nu/info/form.py +69 -0
- nu/info/interaction.py +140 -0
- nu/info/interactions.py +243 -0
- nu/info/record.py +90 -0
- nu/info/ref.py +67 -0
- nu/inspect/__init__.py +29 -0
- nu/inspect/annotate.py +222 -0
- nu/inspect/render.py +233 -0
- nu/inspect/shape.py +567 -0
- nu/lang/__init__.py +196 -0
- nu/lang/args.py +80 -0
- nu/lang/attributes/__init__.py +48 -0
- nu/lang/attributes/cardinality.py +88 -0
- nu/lang/attributes/effects.py +114 -0
- nu/lang/attributes/execution.py +112 -0
- nu/lang/attributes/names.py +58 -0
- nu/lang/attributes/slots.py +37 -0
- nu/lang/attributes/sort.py +232 -0
- nu/lang/forms.py +110 -0
- nu/lang/helpers/__init__.py +44 -0
- nu/lang/helpers/compilation.py +24 -0
- nu/lang/helpers/evaluation.py +184 -0
- nu/lang/helpers/run.py +81 -0
- nu/lang/helpers/validation.py +23 -0
- nu/lang/kinds.py +181 -0
- nu/lang/laws/__init__.py +43 -0
- nu/lang/laws/cardinality.py +46 -0
- nu/lang/laws/composition.py +186 -0
- nu/lang/laws/effects.py +103 -0
- nu/lang/laws/execution.py +32 -0
- nu/lang/laws/observability.py +41 -0
- nu/lang/laws/predicates.py +94 -0
- nu/lang/laws/refs.py +30 -0
- nu/lang/laws/spans.py +63 -0
- nu/lang/nu.py +84 -0
- nu/lang/runtime/__init__.py +29 -0
- nu/lang/runtime/context/__init__.py +17 -0
- nu/lang/runtime/context/attributes.py +77 -0
- nu/lang/runtime/context/context.py +359 -0
- nu/lang/runtime/runtime.py +199 -0
- nu/lang/runtime/utils/__init__.py +19 -0
- nu/lang/runtime/utils/budget.py +71 -0
- nu/lang/runtime/utils/loop.py +90 -0
- nu/lang/sentinels.py +98 -0
- nu/lang/typeinfo.py +242 -0
- nu/prog/__init__.py +67 -0
- nu/prog/_construct_worker.py +144 -0
- nu/prog/brace.py +207 -0
- nu/prog/constructors.py +477 -0
- nu/prog/diagnostics.py +99 -0
- nu/prog/eval.py +211 -0
- nu/prog/eval_promise.py +97 -0
- nu/prog/forms.py +180 -0
- nu/prog/laws.py +48 -0
- nu/prog/load.py +213 -0
- nu/prog/source.py +203 -0
- nu/py.typed +0 -0
- nu/reactive/__init__.py +30 -0
- nu/reactive/interactions.py +358 -0
- nu/reactive/protocol.py +56 -0
- nu/spans/AUTHORING.md +213 -0
- nu/spans/__init__.py +24 -0
- nu/spans/bracket.py +238 -0
- nu/spans/policy.py +654 -0
- nu/tree/__init__.py +82 -0
- nu/tree/effects.py +113 -0
- nu/tree/flow.py +102 -0
- nu/tree/query.py +57 -0
- nu/tree/rewrite.py +194 -0
- nu/tree/walk.py +71 -0
- nucore-0.2.0.dist-info/METADATA +45 -0
- nucore-0.2.0.dist-info/RECORD +222 -0
- nucore-0.2.0.dist-info/WHEEL +4 -0
- nucore-0.2.0.dist-info/licenses/LICENSE.md +201 -0
nu/__init__.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
"""Nu: the interaction primitive.
|
|
3
|
+
|
|
4
|
+
Build apps in one primitive that spans your whole stack (databases, UIs,
|
|
5
|
+
AI agents, and services). No glue. 50x less code.
|
|
6
|
+
|
|
7
|
+
Two ways to import. Grab what you need flat from the root:
|
|
8
|
+
|
|
9
|
+
from nu import Int, Sequential, Retry, AttrRef, Nu, run
|
|
10
|
+
|
|
11
|
+
Or reach a subpackage by dot-access:
|
|
12
|
+
|
|
13
|
+
import nu
|
|
14
|
+
nu.forms.Int nu.core.Add nu.flows.Sequential
|
|
15
|
+
nu.spans.Retry nu.shape.Shape nu.tree.map_nodes
|
|
16
|
+
nu.mem.IntRef nu.kv.presets.memory_storage
|
|
17
|
+
nu.ui.Page nu.std.uuid.UUID
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
23
|
+
from pkgutil import extend_path as _extend_path
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# `nu` is split across two distributions: `nucore` (this file, the kernel)
|
|
28
|
+
# and `nustd` (the fabrics: std, mem, kv, service, llm, cc, http, proxy, mp,
|
|
29
|
+
# cluster, ui). Installed as wheels the two land in the same
|
|
30
|
+
# `site-packages/nu/` and nothing special is needed; installed editable they
|
|
31
|
+
# sit in two separate `src/` trees, so widen `__path__` to pick up both. Only
|
|
32
|
+
# this file exists as `nu/__init__.py` — the kernel owns the package root.
|
|
33
|
+
__path__ = _extend_path(__path__, __name__)
|
|
34
|
+
del _extend_path
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
__version__ = _version("nucore")
|
|
38
|
+
except PackageNotFoundError:
|
|
39
|
+
__version__ = "0.0.0+dev"
|
|
40
|
+
del _version, PackageNotFoundError
|
|
41
|
+
|
|
42
|
+
from ._config import bootstrap as _bootstrap
|
|
43
|
+
|
|
44
|
+
_bootstrap()
|
|
45
|
+
del _bootstrap
|
|
46
|
+
|
|
47
|
+
# Subpackage namespaces for dot-access.
|
|
48
|
+
# Early group: pure layers with no dependency on the flat root surface.
|
|
49
|
+
from . import context, core, engine, factory, flows, forms, info, lang, prog, spans, tree
|
|
50
|
+
from .domains import shape
|
|
51
|
+
|
|
52
|
+
# Flat re-exports: the program-authoring surface.
|
|
53
|
+
from .context import *
|
|
54
|
+
from .core import *
|
|
55
|
+
from .flows import *
|
|
56
|
+
from .forms import *
|
|
57
|
+
from .spans import *
|
|
58
|
+
|
|
59
|
+
# Shape DSL only; fabric atoms (Load, SetCmd, ...) stay at nu.shape.*.
|
|
60
|
+
from .domains.shape import Shape, Slot
|
|
61
|
+
from .domains.service import Method, Service
|
|
62
|
+
|
|
63
|
+
# The program pipeline (nu.prog fabric). Flat: ``nu.LoadNu`` beside ``nu.Eval``,
|
|
64
|
+
# because ``Eval(LoadNu(src))`` is one authoring idiom, and ``nu.Program``
|
|
65
|
+
# beside them because it is the Form you write that pair with - it sits with
|
|
66
|
+
# the other Forms on the flat surface, not with the machinery. The brace
|
|
67
|
+
# resource and the error types stay at ``nu.prog.*``, the same way
|
|
68
|
+
# ``nu.mp.MpWorker`` does.
|
|
69
|
+
from .prog import Eval, LoadNu, Program
|
|
70
|
+
|
|
71
|
+
# Language essentials, curated to the building blocks of a Nu program:
|
|
72
|
+
# - the root kind (``Nu``, ``TypedNu``);
|
|
73
|
+
# - the kind hierarchy (``Ref``, ``Interaction``, ``Query`` / ``ScalarQuery``
|
|
74
|
+
# / ``StreamQuery`` / ``Reduction``, ``Command``, ``Action`` /
|
|
75
|
+
# ``ScalarAction`` / ``StreamAction``, ``Flow`` / ``Strategy`` /
|
|
76
|
+
# ``Control``, ``Span`` / ``Bracket`` / ``Policy``);
|
|
77
|
+
# - the argument aliases used in kind signatures (``Arg`` and the typed
|
|
78
|
+
# ``*Arg`` aliases).
|
|
79
|
+
from .lang import (
|
|
80
|
+
Action,
|
|
81
|
+
Arg,
|
|
82
|
+
BoolArg,
|
|
83
|
+
Bracket,
|
|
84
|
+
BytesArg,
|
|
85
|
+
Command,
|
|
86
|
+
Context,
|
|
87
|
+
Control,
|
|
88
|
+
DictArg,
|
|
89
|
+
FloatArg,
|
|
90
|
+
Flow,
|
|
91
|
+
Form,
|
|
92
|
+
FrozenSetArg,
|
|
93
|
+
IntArg,
|
|
94
|
+
Interaction,
|
|
95
|
+
ListArg,
|
|
96
|
+
NoneArg,
|
|
97
|
+
Nu,
|
|
98
|
+
Policy,
|
|
99
|
+
Query,
|
|
100
|
+
Reduction,
|
|
101
|
+
Ref,
|
|
102
|
+
Runtime,
|
|
103
|
+
ScalarAction,
|
|
104
|
+
ScalarQuery,
|
|
105
|
+
SetArg,
|
|
106
|
+
Span,
|
|
107
|
+
StrArg,
|
|
108
|
+
Strategy,
|
|
109
|
+
StreamAction,
|
|
110
|
+
StreamQuery,
|
|
111
|
+
TupleArg,
|
|
112
|
+
TypedNu,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Atom builders (nu.factory subpackage): factories + @host.
|
|
116
|
+
from .factory import (
|
|
117
|
+
InteractionFactory,
|
|
118
|
+
host,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Entry points.
|
|
122
|
+
from .lang.helpers import (
|
|
123
|
+
acollect,
|
|
124
|
+
aeval,
|
|
125
|
+
afirst,
|
|
126
|
+
alast,
|
|
127
|
+
arun,
|
|
128
|
+
collect,
|
|
129
|
+
compile,
|
|
130
|
+
eval,
|
|
131
|
+
eval_in_loop,
|
|
132
|
+
first,
|
|
133
|
+
run,
|
|
134
|
+
run_in_loop,
|
|
135
|
+
validate,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Late subpackage namespaces: fabric adapters and higher layers that reach into
|
|
139
|
+
# the flat root surface (e.g. ``from nu import Shape``). These are heavy and
|
|
140
|
+
# transitively pull large trees (storage backends, UI runtime, RPC, ...), so
|
|
141
|
+
# they load lazily on first attribute access via ``__getattr__`` below. The
|
|
142
|
+
# ``TYPE_CHECKING`` block gives IDEs and type-checkers the real modules so
|
|
143
|
+
# ``nu.mem.IntRef`` etc. resolve statically with full completion / go-to-def.
|
|
144
|
+
if TYPE_CHECKING:
|
|
145
|
+
from . import cc, cluster, http, kv, llm, mem, mp, proxy, service, std, ui
|
|
146
|
+
|
|
147
|
+
# NOTE: several flat re-exports above shadow Python builtins at module scope
|
|
148
|
+
# — coercion atoms (``set``/``frozenset``/``tuple``/``list``/``dict``/``int``/
|
|
149
|
+
# ``float``/``str``/``bool``), IO atoms (``print``/``input``), and language
|
|
150
|
+
# helpers (``compile``/``eval``). They stay reachable as ``nu.set`` etc., but
|
|
151
|
+
# ``from nu import *`` skips them (see ``_SHADOWS_BUILTIN`` below) so callers
|
|
152
|
+
# don't get their builtins silently swapped. Any set/dict-builder logic in
|
|
153
|
+
# THIS file must use literals (``{...}``) — never the shadowed callables.
|
|
154
|
+
#
|
|
155
|
+
# Every name below ships in the `nustd` distribution, not in `nucore`.
|
|
156
|
+
# The two wheels merge into one ``site-packages/nu/`` directory (only the
|
|
157
|
+
# kernel carries ``nu/__init__.py``), so on a kernel-only install these
|
|
158
|
+
# submodules are simply absent — hence the install hint in __getattr__.
|
|
159
|
+
# The value is the extra that pulls the fabric's backend, or None when the
|
|
160
|
+
# fabric is pure stdlib and plain ``nustd`` is enough.
|
|
161
|
+
_LAZY = {
|
|
162
|
+
"cc": "cc",
|
|
163
|
+
"cluster": "cluster",
|
|
164
|
+
"http": "http",
|
|
165
|
+
"kv": "kv",
|
|
166
|
+
"llm": "llm",
|
|
167
|
+
"mem": "mem",
|
|
168
|
+
"mp": None,
|
|
169
|
+
"proxy": "proxy",
|
|
170
|
+
"service": None,
|
|
171
|
+
"std": None,
|
|
172
|
+
"ui": "ui",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def __getattr__(name):
|
|
177
|
+
import importlib
|
|
178
|
+
import importlib.util
|
|
179
|
+
|
|
180
|
+
if name not in _LAZY:
|
|
181
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
182
|
+
try:
|
|
183
|
+
mod = importlib.import_module(f".{name}", __name__)
|
|
184
|
+
except ImportError as exc:
|
|
185
|
+
# Distinguish "nustd is not installed" from "the fabric is there but
|
|
186
|
+
# its backend blew up" — the second must surface the real error.
|
|
187
|
+
if importlib.util.find_spec(f"{__name__}.{name}") is not None:
|
|
188
|
+
raise
|
|
189
|
+
extra = _LAZY[name]
|
|
190
|
+
target = f"nustd[{extra}]" if extra else "nustd"
|
|
191
|
+
msg = f"nu.{name} requires nustd: pip install {target}"
|
|
192
|
+
raise AttributeError(msg) from exc
|
|
193
|
+
globals()[name] = mod
|
|
194
|
+
return mod
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def __dir__():
|
|
198
|
+
return sorted({*globals(), *_LAZY})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# __all__ = every name bound above (minus privates, the ``__future__`` shim,
|
|
202
|
+
# the internal ``domains`` layer, and any name shadowing a Python builtin)
|
|
203
|
+
# plus the lazy fabric + alias names, so ``from nu import *`` and IDE
|
|
204
|
+
# discovery see them without forcing import. Builtin-shadowing names stay
|
|
205
|
+
# reachable as ``nu.<name>`` — the module dict is unchanged, only ``__all__``
|
|
206
|
+
# is filtered — so ``import *`` cannot silently rebind ``set``/``print``/etc.
|
|
207
|
+
# in the caller's namespace.
|
|
208
|
+
import builtins as _builtins # noqa: E402 (placed here to avoid the shadowed scope above)
|
|
209
|
+
|
|
210
|
+
_HIDDEN = {"annotations", "TYPE_CHECKING", "domains"}
|
|
211
|
+
_SHADOWS_BUILTIN = {n for n in dir(_builtins) if not n.startswith("_")}
|
|
212
|
+
_names = dir()
|
|
213
|
+
__all__ = sorted(
|
|
214
|
+
({*_names, *_LAZY} - _HIDDEN - _SHADOWS_BUILTIN) - {n for n in _names if n.startswith("_")}
|
|
215
|
+
)
|
|
216
|
+
del _builtins, _names
|
nu/_config/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Internal: ``~/.nu`` user config and anonymous usage telemetry.
|
|
2
|
+
|
|
3
|
+
Layout:
|
|
4
|
+
|
|
5
|
+
- :mod:`nu._config.constants` -- all paths, defaults, and PostHog
|
|
6
|
+
wiring. Single source of truth.
|
|
7
|
+
- :mod:`nu._config.config` -- read/write ``~/.nu/config.json``,
|
|
8
|
+
telemetry opt-out flag, stable ``distinct_id``.
|
|
9
|
+
- :mod:`nu._config.telemetry` -- fire-and-forget PostHog capture.
|
|
10
|
+
|
|
11
|
+
Hot path: :func:`bootstrap` does one ``exists()`` syscall and returns
|
|
12
|
+
immediately when the first-run marker is already in place; the config
|
|
13
|
+
and telemetry submodules are only imported on first-run bootstrap and
|
|
14
|
+
explicit CLI / nudle calls.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .constants import CONFIG_PATH as CONFIG_PATH
|
|
18
|
+
from .constants import FIRST_RUN_MARKER_PATH as FIRST_RUN_MARKER_PATH
|
|
19
|
+
from .constants import HOME as HOME
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def bootstrap() -> None:
|
|
23
|
+
"""First-touch bootstrap for ``~/.nu``.
|
|
24
|
+
|
|
25
|
+
Fast path: one ``exists()`` syscall on the first-run marker; the
|
|
26
|
+
marker is only written after a successful capture, which itself
|
|
27
|
+
only runs once the config file exists, so its presence implies
|
|
28
|
+
both are in place.
|
|
29
|
+
|
|
30
|
+
Otherwise: create the config on first ever import, and (re)fire
|
|
31
|
+
the first-run event on every import until the POST gets a 2xx and
|
|
32
|
+
drops the marker. That covers short-lived commands
|
|
33
|
+
(``nu --help``, ``python -c "import nu"``) where a daemon POST
|
|
34
|
+
would otherwise be killed on process exit. Silent on any failure.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
if FIRST_RUN_MARKER_PATH.exists():
|
|
38
|
+
return
|
|
39
|
+
from .telemetry import capture_first_run
|
|
40
|
+
|
|
41
|
+
if not CONFIG_PATH.exists():
|
|
42
|
+
from .config import create_default
|
|
43
|
+
|
|
44
|
+
capture_first_run(create_default())
|
|
45
|
+
else:
|
|
46
|
+
from .config import distinct_id
|
|
47
|
+
|
|
48
|
+
capture_first_run(distinct_id())
|
|
49
|
+
except Exception: # noqa: S110
|
|
50
|
+
pass
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Nu brand kit: palette + logo + shared header. Consumed by nudle and nucli.
|
|
2
|
+
|
|
3
|
+
Stdlib only -- it renders straight to ANSI escapes (see :mod:`.ansi`), so it
|
|
4
|
+
sits in the kernel without pulling a terminal library in behind it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .ansi import RESET, color_enabled, paint
|
|
10
|
+
from .header import DOCS_URL, GITHUB_URL, TAGLINE, header, render_header
|
|
11
|
+
from .logo import logo, logo_lines
|
|
12
|
+
from .palette import BLUE, PURPLE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BLUE",
|
|
17
|
+
"DOCS_URL",
|
|
18
|
+
"GITHUB_URL",
|
|
19
|
+
"PURPLE",
|
|
20
|
+
"RESET",
|
|
21
|
+
"TAGLINE",
|
|
22
|
+
"color_enabled",
|
|
23
|
+
"header",
|
|
24
|
+
"logo",
|
|
25
|
+
"logo_lines",
|
|
26
|
+
"paint",
|
|
27
|
+
"render_header",
|
|
28
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Minimal ANSI SGR helpers -- the brand kit's whole rendering layer.
|
|
2
|
+
|
|
3
|
+
Stdlib only, on purpose: the brand kit lives in the kernel now, and the
|
|
4
|
+
kernel takes no dependency beyond ``typing-extensions`` and ``cloudpickle``.
|
|
5
|
+
Everything here is a plain string transform.
|
|
6
|
+
|
|
7
|
+
Two rules govern whether escapes are emitted at all:
|
|
8
|
+
|
|
9
|
+
- ``NO_COLOR`` set to a non-empty value disables them (https://no-color.org).
|
|
10
|
+
- the target stream must be a tty; piping ``nu`` into a file or a pager
|
|
11
|
+
must never leak escape junk.
|
|
12
|
+
|
|
13
|
+
Attribute order in the emitted sequence matches what rich produced before
|
|
14
|
+
(bold, dim, underline, then the truecolor foreground) so terminal output is
|
|
15
|
+
byte-identical to the previous rich-rendered brand kit.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from typing import IO
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["RESET", "color_enabled", "paint"]
|
|
30
|
+
|
|
31
|
+
RESET = "\x1b[0m"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def color_enabled(stream: IO[str] | None = None) -> bool:
|
|
35
|
+
"""Whether ``stream`` should get ANSI escapes.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
stream: the destination. Defaults to ``sys.stdout``.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
``True`` only when ``NO_COLOR`` is unset/empty and the stream is a
|
|
42
|
+
tty. A stream that raises or has no ``isatty`` counts as not a tty.
|
|
43
|
+
"""
|
|
44
|
+
if os.environ.get("NO_COLOR"):
|
|
45
|
+
return False
|
|
46
|
+
stream = sys.stdout if stream is None else stream
|
|
47
|
+
try:
|
|
48
|
+
return bool(stream.isatty())
|
|
49
|
+
except Exception:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hex_to_rgb(color: str) -> tuple[int, int, int]:
|
|
54
|
+
"""``"#7a4ce0"`` -> ``(122, 76, 224)``."""
|
|
55
|
+
value = color.lstrip("#")
|
|
56
|
+
return int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def paint(
|
|
60
|
+
text: str,
|
|
61
|
+
*,
|
|
62
|
+
fg: str | None = None,
|
|
63
|
+
bold: bool = False,
|
|
64
|
+
dim: bool = False,
|
|
65
|
+
underline: bool = False,
|
|
66
|
+
enabled: bool = True,
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Wrap ``text`` in one SGR sequence, or return it untouched.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
text: the string to style.
|
|
72
|
+
fg: foreground as a ``#rrggbb`` hex string, or ``None``.
|
|
73
|
+
bold: emit SGR 1.
|
|
74
|
+
dim: emit SGR 2.
|
|
75
|
+
underline: emit SGR 4.
|
|
76
|
+
enabled: when ``False``, or when no attribute is requested, ``text``
|
|
77
|
+
comes back unchanged with no escapes at all.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The styled string, always terminated by a reset when styled.
|
|
81
|
+
"""
|
|
82
|
+
if not enabled or not text:
|
|
83
|
+
return text
|
|
84
|
+
codes: list[str] = []
|
|
85
|
+
if bold:
|
|
86
|
+
codes.append("1")
|
|
87
|
+
if dim:
|
|
88
|
+
codes.append("2")
|
|
89
|
+
if underline:
|
|
90
|
+
codes.append("4")
|
|
91
|
+
if fg is not None:
|
|
92
|
+
r, g, b = _hex_to_rgb(fg)
|
|
93
|
+
codes.append(f"38;2;{r};{g};{b}")
|
|
94
|
+
if not codes:
|
|
95
|
+
return text
|
|
96
|
+
return f"\x1b[{';'.join(codes)}m{text}{RESET}"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Shared Nu header: logo on the left, tagline + version + urls on the right.
|
|
2
|
+
|
|
3
|
+
Used by the CLI (`nu` with no args) and the nudle server banner -- one
|
|
4
|
+
shared visual identity across every user-facing entrypoint.
|
|
5
|
+
|
|
6
|
+
The two columns are laid out by hand (a fixed-width left cell, four spaces
|
|
7
|
+
of gutter, a right cell padded to its widest line, right block vertically
|
|
8
|
+
centred against the mark) so the kernel needs no table renderer.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from .ansi import color_enabled, paint
|
|
18
|
+
from .logo import WIDTH as _LOGO_WIDTH
|
|
19
|
+
from .logo import logo_lines
|
|
20
|
+
from .palette import BLUE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from typing import IO
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TAGLINE = "The interaction primitive"
|
|
28
|
+
DOCS_URL = "https://nustack.dev"
|
|
29
|
+
GITHUB_URL = "https://github.com/nustackdev/nu"
|
|
30
|
+
|
|
31
|
+
# Gutter between the mark and the label block.
|
|
32
|
+
_GUTTER = " "
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _nu_version() -> str:
|
|
36
|
+
try:
|
|
37
|
+
return version("nucore")
|
|
38
|
+
except PackageNotFoundError:
|
|
39
|
+
return "0.0.0+dev"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _side(color: bool) -> list[tuple[str, str]]:
|
|
43
|
+
"""The right-hand label block as ``(plain, rendered)`` lines."""
|
|
44
|
+
ver = f"v{_nu_version()}"
|
|
45
|
+
return [
|
|
46
|
+
(TAGLINE, TAGLINE),
|
|
47
|
+
(ver, paint(ver, fg=BLUE, bold=True, enabled=color)),
|
|
48
|
+
("", ""),
|
|
49
|
+
(DOCS_URL, paint(DOCS_URL, fg=BLUE, underline=True, enabled=color)),
|
|
50
|
+
("", ""),
|
|
51
|
+
(GITHUB_URL, paint(GITHUB_URL, fg=BLUE, underline=True, enabled=color)),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def header(*, color: bool = True) -> str:
|
|
56
|
+
"""The header block as one string, blank line above and below."""
|
|
57
|
+
# ``logo`` ends on a newline, so the mark occupies one blank row past its
|
|
58
|
+
# last glyph row -- that row is part of the block and gets padded too.
|
|
59
|
+
# Every glyph row is exactly ``_LOGO_WIDTH`` cells wide, so the left column
|
|
60
|
+
# needs no padding of its own; the trailing row is that width in spaces.
|
|
61
|
+
left = [*logo_lines(color=color), " " * _LOGO_WIDTH]
|
|
62
|
+
|
|
63
|
+
side = _side(color)
|
|
64
|
+
right_width = max(len(plain) for plain, _ in side)
|
|
65
|
+
# Vertically centre the labels against the mark, rich's ``vertical="middle"``.
|
|
66
|
+
top = (len(left) - len(side)) // 2
|
|
67
|
+
blank = ("", "")
|
|
68
|
+
rows = [blank] * top + side + [blank] * (len(left) - len(side) - top)
|
|
69
|
+
|
|
70
|
+
lines = [""]
|
|
71
|
+
for mark, (plain, rendered) in zip(left, rows, strict=True):
|
|
72
|
+
lines.append(mark + _GUTTER + rendered + " " * (right_width - len(plain)))
|
|
73
|
+
lines.append("")
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def render_header(file: IO[str] | None = None) -> None:
|
|
78
|
+
"""Print the shared Nu header: logo on the left, labels on the right."""
|
|
79
|
+
stream = sys.stdout if file is None else file
|
|
80
|
+
print(header(color=color_enabled(stream)), file=stream, flush=True)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Nu mark rendered as terminal braille glyphs.
|
|
2
|
+
|
|
3
|
+
Compact 2x4 sub-pixel braille packing of the SVG mark -- two interlocking
|
|
4
|
+
glyphs, purple top-left and blue bottom-right matching the SVG gradient
|
|
5
|
+
stops.
|
|
6
|
+
|
|
7
|
+
Baked once from the source PNG so this module has zero runtime deps beyond
|
|
8
|
+
the stdlib.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .ansi import paint
|
|
14
|
+
from .palette import BLUE, PURPLE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_COLOR = {"P": PURPLE, "B": BLUE}
|
|
18
|
+
|
|
19
|
+
# 16 cols x 8 rows braille grid; each row is (glyphs, per-cell color tag —
|
|
20
|
+
# 'P' purple, 'B' blue, '.' blank).
|
|
21
|
+
_LOGO: tuple[tuple[str, str], ...] = (
|
|
22
|
+
("⠀⢀⣤⣶⣾⣿⣿⣿⣿⣿⣿⡇⠀⢰⣿⣿", ".PPPPPPPPPPP.PPB"),
|
|
23
|
+
("⣰⣿⣿⠟⠋⠉⠉⠉⠉⣿⣿⡇⠀⢸⣿⣿", "PPPPPPPPPPPP.BBB"),
|
|
24
|
+
("⣿⣿⡇⠀⠀⣀⣀⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP..PP..PPB.BBB"),
|
|
25
|
+
("⣿⣿⡇⠀⢸⣿⣿⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP.PPP..BBB.BBB"),
|
|
26
|
+
("⣿⣿⡇⠀⢸⣿⣿⠀⠀⣿⣿⡇⠀⢸⣿⣿", "PPP.PPP..BBB.BBB"),
|
|
27
|
+
("⣿⣿⡇⠀⢸⣿⣿⠀⠀⠉⠉⠀⠀⢸⣿⣿", "PPP.PBB..BB..BBB"),
|
|
28
|
+
("⣿⣿⡇⠀⢸⣿⣿⣀⣀⣀⣀⣠⣴⣿⣿⠏", "PPP.BBBBBBBBBBBB"),
|
|
29
|
+
("⣿⣿⠇⠀⢸⣿⣿⣿⣿⣿⣿⡿⠿⠛⠁⠀", "PBB.BBBBBBBBBBB."),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Every row is the same width; the header lays the mark out against this.
|
|
33
|
+
WIDTH = len(_LOGO[0][0])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def logo_lines(*, color: bool = True) -> list[str]:
|
|
37
|
+
"""The mark, one rendered string per row.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
color: emit ANSI escapes. When ``False`` the glyphs come back bare.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Eight strings, each ``WIDTH`` glyphs wide once escapes are stripped.
|
|
44
|
+
"""
|
|
45
|
+
return [_row(chars, colors, color) for chars, colors in _LOGO]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def logo(*, color: bool = True) -> str:
|
|
49
|
+
"""The mark as one block of eight rows, each terminated by a newline."""
|
|
50
|
+
return "".join(f"{line}\n" for line in logo_lines(color=color))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _row(chars: str, colors: str, color: bool) -> str:
|
|
54
|
+
"""One grid row, collapsed into runs of equal colour tag."""
|
|
55
|
+
out: list[str] = []
|
|
56
|
+
start = 0
|
|
57
|
+
for i in range(1, len(colors) + 1):
|
|
58
|
+
if i == len(colors) or colors[i] != colors[start]:
|
|
59
|
+
out.append(paint(chars[start:i], fg=_COLOR.get(colors[start]), enabled=color))
|
|
60
|
+
start = i
|
|
61
|
+
return "".join(out)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Nu brand palette -- the SVG gradient stops, canonical for the whole codebase.
|
|
2
|
+
|
|
3
|
+
Import ``PURPLE``/``BLUE`` from here whenever you need an accent; there is no
|
|
4
|
+
"cyan" or ad-hoc third color in the Nu palette.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PURPLE = "#7a4ce0"
|
|
11
|
+
BLUE = "#3e72e7"
|
nu/_config/config.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""User config at ``~/.nu/config.json``.
|
|
2
|
+
|
|
3
|
+
One JSON file, machine-managed. Holds the telemetry opt-out flag and a
|
|
4
|
+
stable anonymous ``distinct_id``. Created on first `nu` import or CLI
|
|
5
|
+
run; that first-touch is the signal that a human actually installed
|
|
6
|
+
and used Nu (see :mod:`nu._config.telemetry`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from .constants import CONFIG_DEFAULTS, CONFIG_PATH, HOME
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load() -> dict[str, Any]:
|
|
25
|
+
"""Read the config file; return {} if it does not exist or is unreadable."""
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(CONFIG_PATH.read_text())
|
|
28
|
+
except (OSError, json.JSONDecodeError):
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def save(cfg: dict[str, Any]) -> None:
|
|
33
|
+
"""Write config atomically (temp + rename)."""
|
|
34
|
+
HOME.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
tmp = CONFIG_PATH.with_suffix(".json.tmp")
|
|
36
|
+
tmp.write_text(json.dumps(cfg, indent=2, sort_keys=True) + "\n")
|
|
37
|
+
tmp.replace(CONFIG_PATH)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_default() -> str:
|
|
41
|
+
"""Create the config with defaults; return the new distinct_id.
|
|
42
|
+
|
|
43
|
+
Caller must have checked that :data:`CONFIG_PATH` does not exist --
|
|
44
|
+
this always writes.
|
|
45
|
+
"""
|
|
46
|
+
did = str(uuid.uuid4())
|
|
47
|
+
save({**CONFIG_DEFAULTS, "distinct_id": did})
|
|
48
|
+
return did
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def telemetry_enabled() -> bool:
|
|
52
|
+
"""Effective telemetry state (config flag, defaulting on)."""
|
|
53
|
+
return bool(load().get("telemetry", True))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_telemetry(on: bool) -> None:
|
|
57
|
+
"""Flip the telemetry flag, preserving the rest of the config."""
|
|
58
|
+
cfg = load() or {**CONFIG_DEFAULTS, "distinct_id": str(uuid.uuid4())}
|
|
59
|
+
cfg["telemetry"] = bool(on)
|
|
60
|
+
save(cfg)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def distinct_id() -> str:
|
|
64
|
+
"""Stable anonymous id. Generated on first :func:`create_default` call."""
|
|
65
|
+
did = load().get("distinct_id")
|
|
66
|
+
if isinstance(did, str) and did:
|
|
67
|
+
return did
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_dev_install() -> bool:
|
|
72
|
+
"""True when Nu is imported from a source checkout (editable install).
|
|
73
|
+
|
|
74
|
+
Heuristic: a ``pyproject.toml`` sits within a few levels above
|
|
75
|
+
``nu/__init__.py`` (the ``src/nu`` layout used in this repo). Env
|
|
76
|
+
override ``NU_DEV=1`` forces dev mode; ``NU_DEV=0`` forces prod.
|
|
77
|
+
"""
|
|
78
|
+
override = os.environ.get("NU_DEV")
|
|
79
|
+
if override == "1":
|
|
80
|
+
return True
|
|
81
|
+
if override == "0":
|
|
82
|
+
return False
|
|
83
|
+
here = Path(__file__).resolve()
|
|
84
|
+
for depth in (3, 4, 5):
|
|
85
|
+
if depth >= len(here.parents):
|
|
86
|
+
break
|
|
87
|
+
if (here.parents[depth] / "pyproject.toml").exists():
|
|
88
|
+
return True
|
|
89
|
+
return False
|
nu/_config/constants.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Single source of truth for ``~/.nu`` paths and PostHog wiring.
|
|
2
|
+
|
|
3
|
+
Everything the ``_config`` subpackage would otherwise hardcode lives
|
|
4
|
+
here: filesystem layout under ``~/.nu``, config defaults, and the
|
|
5
|
+
PostHog capture endpoint. Env vars ``NU_POSTHOG_TOKEN`` /
|
|
6
|
+
``NU_POSTHOG_HOST`` override the baked-in defaults (local testing
|
|
7
|
+
against a different PostHog project).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
HOME = Path.home() / ".nu"
|
|
17
|
+
CONFIG_PATH = HOME / "config.json"
|
|
18
|
+
DEMOS_DIR = HOME / "demos"
|
|
19
|
+
FIRST_RUN_MARKER_PATH = HOME / "first_run.sent"
|
|
20
|
+
|
|
21
|
+
CONFIG_DEFAULTS: dict[str, object] = {"telemetry": True}
|
|
22
|
+
|
|
23
|
+
# PostHog Project API key -- public write-only token, safe to embed
|
|
24
|
+
# (same one baked into the docs site's client bundle).
|
|
25
|
+
POSTHOG_TOKEN = os.environ.get(
|
|
26
|
+
"NU_POSTHOG_TOKEN", "phc_yRpWfpoMeeRM8sKMPGPEsGy8ygNHJTpsGwrsikfpRfFz"
|
|
27
|
+
)
|
|
28
|
+
POSTHOG_HOST = os.environ.get("NU_POSTHOG_HOST", "https://t.nustack.dev").rstrip("/")
|
|
29
|
+
POSTHOG_CAPTURE_PATH = "/i/v0/e/"
|
|
30
|
+
|
|
31
|
+
FIRST_RUN_EVENT = "nu_first_run"
|
|
32
|
+
|
|
33
|
+
HTTP_TIMEOUT_SECONDS = 3
|