openscript 0.4.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.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,162 @@
1
+ """Check 1 of ``compiled-program.md`` section 3.5: is this a compiled program at all.
2
+
3
+ This is the half of verification that reads an untrusted object and answers that
4
+ question before anything else runs, because every later check indexes into a
5
+ table this one proves is a table.
6
+
7
+ Every failure here is OS6018. A malformed instruction list, an unreadable
8
+ encoding and a field of the wrong type are not three fixes: each is a defect of
9
+ the compiler that wrote the program and none of them is repairable by hand. What
10
+ varies is the location, and the location is a field path, so whoever wrote the
11
+ compiler is told ``outputs.plots[2].channel`` rather than "structure".
12
+
13
+ One difference from the first engine, and it is this interpreter's alone: its
14
+ own ``True`` is an instance of its integer type, so every numeric test below
15
+ refuses a boolean explicitly. Without that line a program carrying ``true``
16
+ where a channel index belongs would verify, and the failure would arrive later
17
+ as a wrong number.
18
+ """
19
+
20
+ from typing import Any, Callable, Optional, Sequence, Set
21
+
22
+ from .diagnostics import Diagnostic, malformed
23
+
24
+ #: The tags a constant pool entry may carry, section 2.9.
25
+ CONSTANT_TAGS = ("z", "b", "n", "s", "c")
26
+
27
+
28
+ class ShapeCheck:
29
+ """Collects the first failure and stops: a malformed program has no second opinion."""
30
+
31
+ def __init__(self) -> None:
32
+ self._failure: Optional[Diagnostic] = None
33
+
34
+ def problem(self) -> Optional[Diagnostic]:
35
+ return self._failure
36
+
37
+ def fail(self, path: str, reason: str) -> bool:
38
+ if self._failure is None:
39
+ self._failure = malformed(path, reason)
40
+ return False
41
+
42
+ def object(self, value: Any, path: str) -> bool:
43
+ if not isinstance(value, dict):
44
+ return self.fail(path, "an object was required")
45
+ return True
46
+
47
+ def array(self, value: Any, path: str) -> bool:
48
+ if not isinstance(value, list):
49
+ return self.fail(path, "an array was required")
50
+ return True
51
+
52
+ def number(self, value: Any, path: str) -> bool:
53
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
54
+ return self.fail(path, "a finite number was required")
55
+ if value != value or value in (float("inf"), float("-inf")):
56
+ return self.fail(path, "a finite number was required")
57
+ return True
58
+
59
+ def whole(self, value: Any, path: str) -> bool:
60
+ if not self.number(value, path):
61
+ return False
62
+ if float(value) != int(value):
63
+ return self.fail(path, "a whole number was required")
64
+ return True
65
+
66
+ def string(self, value: Any, path: str) -> bool:
67
+ if not isinstance(value, str):
68
+ return self.fail(path, "a string was required")
69
+ return True
70
+
71
+ def boolean(self, value: Any, path: str) -> bool:
72
+ if not isinstance(value, bool):
73
+ return self.fail(path, "a true or false was required")
74
+ return True
75
+
76
+ def index(self, value: Any, path: str, size: int, table: str) -> bool:
77
+ """A whole number that indexes a table, with the table's size in the message."""
78
+ if not self.whole(value, path):
79
+ return False
80
+ if value < 0 or value >= size:
81
+ return self.fail(path, f"{value} is outside {table}, which holds {size}")
82
+ return True
83
+
84
+ def one(self, value: Any, path: str, allowed: Sequence[str]) -> bool:
85
+ if not self.string(value, path):
86
+ return False
87
+ if value not in allowed:
88
+ return self.fail(path, f"{value} is not one of {', '.join(allowed)}")
89
+ return True
90
+
91
+ def nullable(self, value: Any, path: str, check: Callable[[Any, str], bool]) -> bool:
92
+ """A field that may be a value or null, which is a value a script could write."""
93
+ return True if value is None else check(value, path)
94
+
95
+
96
+ def check_colour(shape: ShapeCheck, value: Any, path: str) -> bool:
97
+ if not shape.array(value, path):
98
+ return False
99
+ if len(value) != 4:
100
+ return shape.fail(path, "a colour is four numbers")
101
+ for at in range(3):
102
+ if not shape.whole(value[at], f"{path}[{at}]"):
103
+ return False
104
+ channel = value[at]
105
+ if channel < 0 or channel > 255:
106
+ return shape.fail(f"{path}[{at}]", f"a channel runs 0 to 255 and this is {channel}")
107
+ if not shape.number(value[3], f"{path}[3]"):
108
+ return False
109
+ alpha = value[3]
110
+ if alpha < 0 or alpha > 1:
111
+ return shape.fail(f"{path}[3]", f"an alpha runs 0 to 1 and this is {alpha}")
112
+ return True
113
+
114
+
115
+ def check_constant(shape: ShapeCheck, value: Any, path: str) -> bool:
116
+ """One constant pool entry, section 2.9: a two element array of tag and value."""
117
+ if not shape.array(value, path):
118
+ return False
119
+ if len(value) != 2:
120
+ return shape.fail(path, "a pool entry is a tag and a value")
121
+ tag = value[0]
122
+ if not shape.one(tag, f"{path}[0]", CONSTANT_TAGS):
123
+ return False
124
+ held = value[1]
125
+ if tag == "z":
126
+ return True if held is None else shape.fail(f"{path}[1]", "the absent entry holds null")
127
+ if tag == "b":
128
+ return shape.boolean(held, f"{path}[1]")
129
+ if tag == "n":
130
+ return shape.number(held, f"{path}[1]")
131
+ if tag == "s":
132
+ return shape.string(held, f"{path}[1]")
133
+ return check_colour(shape, held, f"{path}[1]")
134
+
135
+
136
+ def check_field(shape: ShapeCheck, value: Any, path: str, keys: Set[str]) -> bool:
137
+ """A declaration field, section 2.3: a value, or the input reference.
138
+
139
+ The key half of check 10 lives here, because this is the one walk that visits
140
+ every field that may hold a reference. The value half runs later, with the
141
+ host's settings in hand, and is OS6019 rather than OS6018: that value came
142
+ from a settings dialog and the user who typed it can correct it.
143
+ """
144
+ if value is None or isinstance(value, (bool, str)):
145
+ return True
146
+ if isinstance(value, (int, float)):
147
+ return shape.number(value, path)
148
+ if isinstance(value, list):
149
+ for at, one in enumerate(value):
150
+ if not shape.number(one, f"{path}[{at}]"):
151
+ return False
152
+ return True
153
+ if not shape.object(value, path):
154
+ return False
155
+ if "input" not in value:
156
+ return shape.fail(path, "an object here is an input reference")
157
+ key = value["input"]
158
+ if not shape.string(key, f"{path}.input"):
159
+ return False
160
+ if key not in keys:
161
+ return shape.fail(path, f"it names the input {key}, which inputs[] does not declare")
162
+ return True
@@ -0,0 +1,256 @@
1
+ """Check 1 of section 3.5 over the program's own tables, and check 10's key half.
2
+
3
+ ``verify_shape.py`` answers whether a field is a number; this walks the whole
4
+ program asking it, table by table, and checks that every index into a table is
5
+ in range: the constant pool, slots, cells, states, registers, channels, library
6
+ functions, call sites, loops and functions. It is separate from ``verify.py``
7
+ because it is one long traversal with no decisions in it, and the decisions
8
+ there, which version, which capability, which budget, are what a reader comes
9
+ looking for.
10
+ """
11
+
12
+ from typing import Any, Dict, List, Sequence, Set
13
+
14
+ from .verify_requests import MachineTables, check_machine_tables, check_requests
15
+ from .verify_shape import ShapeCheck, check_constant, check_field
16
+
17
+ META_KINDS = ("study", "strategy")
18
+ CHANNEL_TYPES = ("number", "string", "color", "bool")
19
+ EFFECTS = ("none", "signal", "order", "draw", "log")
20
+
21
+ #: Every declaration option of ``meta``, section 2.3.
22
+ META_FIELDS = (
23
+ "title",
24
+ "short",
25
+ "overlay",
26
+ "precision",
27
+ "format",
28
+ "range",
29
+ "scale",
30
+ "group",
31
+ "onUnconfirmed",
32
+ )
33
+
34
+ #: The tables a later minor of this format major added, with the minor that added each.
35
+ #:
36
+ #: Section 9.2's rule read from the other side, which 9.4 step 3 states and
37
+ #: ``spec/decisions.md`` minute 56 settled: a table a later minor added and an
38
+ #: earlier program lacks reads as empty, never as a refusal, because 9.5's first
39
+ #: line is a promise about that program. A program stamped at this minor or a
40
+ #: later one has no such excuse. Section 2 says an empty table is written as an
41
+ #: empty array and never omitted, so its absence there is the defect check 1
42
+ #: exists for, and the version is what tells the two apart.
43
+ #:
44
+ #: ``spec/format-history.json`` records the same additions, one entry per format
45
+ #: version, and this engine's tests read that file rather than a list of their
46
+ #: own, so a version the history gains is a case the tests gain.
47
+ ADDED_AT_MINOR = (("requests", 1),)
48
+
49
+
50
+ def minor_of(raw: Dict[str, Any]) -> int:
51
+ """The minor of a ``major.minor`` the version step has already proved is one."""
52
+ parts = str(raw["openscript"]["format"]).split(".")
53
+ return int(parts[1]) if len(parts) > 1 else 0
54
+
55
+
56
+ def supply_added_tables(raw: Dict[str, Any]) -> None:
57
+ """Writes the empty table an earlier minor is owed into the program itself.
58
+
59
+ Into the object rather than into a local, because every later step reads the
60
+ program as its own shape and would find the table missing again: the
61
+ verifier hands the same object on, and the engine holds it.
62
+ """
63
+ minor = minor_of(raw)
64
+ for table, added in ADDED_AT_MINOR:
65
+ if table in raw or minor >= added:
66
+ continue
67
+ raw[table] = []
68
+
69
+
70
+ ARRAY_TABLES = (
71
+ "requires",
72
+ "inputs",
73
+ "channels",
74
+ "consts",
75
+ "series",
76
+ "cells",
77
+ "states",
78
+ "functions",
79
+ "callSites",
80
+ "loops",
81
+ "code",
82
+ "requests",
83
+ )
84
+
85
+ OBJECT_TABLES = ("compiler", "source", "meta", "limits", "lib", "outputs", "frame", "debug")
86
+
87
+
88
+ def check_tables(shape: ShapeCheck, raw: Any) -> bool:
89
+ """Check 1 over every table the machine indexes, and every declaration field."""
90
+ if not shape.object(raw, "the program"):
91
+ return False
92
+ supply_added_tables(raw)
93
+ for name in ARRAY_TABLES:
94
+ if not shape.array(raw.get(name), name):
95
+ return False
96
+ for name in OBJECT_TABLES:
97
+ if not shape.object(raw.get(name), name):
98
+ return False
99
+
100
+ limits = raw["limits"]
101
+ if not shape.whole(limits.get("loops"), "limits.loops"):
102
+ return False
103
+ if limits.get("history") is not None and not shape.whole(limits["history"], "limits.history"):
104
+ return False
105
+
106
+ frame = raw["frame"]
107
+ if not shape.whole(frame.get("slots"), "frame.slots"):
108
+ return False
109
+
110
+ lib = raw["lib"]
111
+ if not shape.whole(lib.get("manifest"), "lib.manifest"):
112
+ return False
113
+ if not shape.array(lib.get("functions"), "lib.functions"):
114
+ return False
115
+ for at, entry in enumerate(lib["functions"]):
116
+ path = f"lib.functions[{at}]"
117
+ if not shape.object(entry, path):
118
+ return False
119
+ if not shape.string(entry.get("name"), f"{path}.name"):
120
+ return False
121
+ if not shape.whole(entry.get("arity"), f"{path}.arity"):
122
+ return False
123
+ if not shape.boolean(entry.get("state"), f"{path}.state"):
124
+ return False
125
+ if not shape.one(entry.get("effect"), f"{path}.effect", EFFECTS):
126
+ return False
127
+
128
+ for at, entry in enumerate(raw["consts"]):
129
+ if not check_constant(shape, entry, f"consts[{at}]"):
130
+ return False
131
+
132
+ keys: Set[str] = set()
133
+ for at, one in enumerate(raw["inputs"]):
134
+ path = f"inputs[{at}]"
135
+ if not shape.object(one, path):
136
+ return False
137
+ if not shape.string(one.get("key"), f"{path}.key"):
138
+ return False
139
+ if not shape.string(one.get("kind"), f"{path}.kind"):
140
+ return False
141
+ if not shape.index(one.get("slot"), f"{path}.slot", frame["slots"], "the frame"):
142
+ return False
143
+ if not check_constant(shape, one.get("default"), f"{path}.default"):
144
+ return False
145
+ keys.add(one["key"])
146
+
147
+ for at, channel in enumerate(raw["channels"]):
148
+ path = f"channels[{at}]"
149
+ if not shape.object(channel, path):
150
+ return False
151
+ if channel.get("id") != at:
152
+ return shape.fail(f"{path}.id", "a channel id is its position")
153
+ if not shape.one(channel.get("type"), f"{path}.type", CHANNEL_TYPES):
154
+ return False
155
+ for name in ("defer", "once"):
156
+ if not shape.boolean(channel.get(name), f"{path}.{name}"):
157
+ return False
158
+
159
+ tables = MachineTables(
160
+ series=raw["series"],
161
+ cells=raw["cells"],
162
+ states=raw["states"],
163
+ functions=raw["functions"],
164
+ call_sites=raw["callSites"],
165
+ loops=raw["loops"],
166
+ slots=frame["slots"],
167
+ lib_functions=len(lib["functions"]),
168
+ )
169
+ if not check_machine_tables(shape, "", tables):
170
+ return False
171
+
172
+ # 2.16: a read's body is walked on the same terms, because the same machine
173
+ # executes it over another instrument's bars.
174
+ if not check_requests(shape, "", raw["requests"], len(raw["series"]), len(lib["functions"]), keys):
175
+ return False
176
+
177
+ meta = raw["meta"]
178
+ if not shape.one(meta.get("kind"), "meta.kind", META_KINDS):
179
+ return False
180
+ for name in META_FIELDS:
181
+ if not check_field(shape, meta.get(name), f"meta.{name}", keys):
182
+ return False
183
+
184
+ debug = raw["debug"]
185
+ for name in ("pos", "fnPos"):
186
+ if not shape.array(debug.get(name), f"debug.{name}"):
187
+ return False
188
+
189
+ return _check_outputs(shape, raw["outputs"], len(raw["channels"]), frame["slots"], keys)
190
+
191
+
192
+ #: Each declaration group, the fields of it that name a channel, and the fields
193
+ #: that hold a value or the input reference that resolves to one.
194
+ DECLARATIONS = (
195
+ ("plots", ("channel",),
196
+ ("title", "color", "width", "lineStyle", "offset", "overlay", "scale")),
197
+ ("levels", ("channel",), ("title", "color", "lineStyle", "lineWidth")),
198
+ ("markers", ("channel",), ("position", "shape", "color", "textColor")),
199
+ ("alerts", ("condChannel",), ("title", "frequency")),
200
+ )
201
+
202
+ OUTPUT_GROUPS = ("plots", "fills", "levels", "markers", "tables", "alerts")
203
+
204
+
205
+ def _check_outputs(
206
+ shape: ShapeCheck, outputs: Any, channels: int, slots: int, keys: Set[str]
207
+ ) -> bool:
208
+ """Every declaration in ``outputs``, and every channel it points at."""
209
+ for group in OUTPUT_GROUPS:
210
+ if not shape.array(outputs.get(group), f"outputs.{group}"):
211
+ return False
212
+
213
+ def channel_field(holder: Any, path: str, name: str) -> bool:
214
+ return shape.index(holder.get(name), f"{path}.{name}", channels, "channels")
215
+
216
+ for group, channel_fields, value_fields in DECLARATIONS:
217
+ for at, one in enumerate(outputs[group]):
218
+ path = f"outputs.{group}[{at}]"
219
+ if not shape.object(one, path):
220
+ return False
221
+ for name in channel_fields:
222
+ if not channel_field(one, path, name):
223
+ return False
224
+ for name in value_fields:
225
+ if not check_field(shape, one.get(name), f"{path}.{name}", keys):
226
+ return False
227
+
228
+ for at, grid in enumerate(outputs["tables"]):
229
+ path = f"outputs.tables[{at}]"
230
+ if not shape.object(grid, path):
231
+ return False
232
+ if not shape.index(grid.get("slot"), f"{path}.slot", slots, "the frame"):
233
+ return False
234
+ for name in ("title", "position", "rows", "cols"):
235
+ if not check_field(shape, grid.get(name), f"{path}.{name}", keys):
236
+ return False
237
+
238
+ for paint in ("barColor", "background"):
239
+ value = outputs.get(paint)
240
+ if value is None:
241
+ continue
242
+ if not shape.object(value, f"outputs.{paint}"):
243
+ return False
244
+ if not channel_field(value, f"outputs.{paint}", "channel"):
245
+ return False
246
+
247
+ return True
248
+
249
+
250
+ def input_keys(raw: Any) -> List[str]:
251
+ """The declared settings keys, in the order of section 2.6."""
252
+ return [one["key"] for one in raw["inputs"]]
253
+
254
+
255
+ def channel_flags(raw: Any, name: str) -> Sequence[bool]:
256
+ return [bool(one[name]) for one in raw["channels"]]
openscript/version.py ADDED
@@ -0,0 +1,39 @@
1
+ """The two version numbers this engine declares, and why they are typed here.
2
+
3
+ ``openscript.format`` versions the compiled program's structure and
4
+ ``openscript.language`` versions meaning, and section 9.1 says they move at
5
+ different speeds for different reasons. An engine declares which majors it can
6
+ load and which language versions it has library semantics for, and section 9.4
7
+ refuses on both.
8
+
9
+ **These numbers exist elsewhere and this is the second copy, held by a check.**
10
+ The first engine has them generated from the page and from the package manifest,
11
+ because a number typed twice is a number that will disagree the day somebody
12
+ edits one copy. That generator writes one language's file. A package a host
13
+ installs cannot read the specification, which is not shipped with it, so the
14
+ numbers have to be in the package, and the honest arrangement is the one the
15
+ distribution file next door already uses for the package version: write it, and
16
+ have a check fail the build the day it drifts. ``tests/test_version.py`` reads
17
+ the sentence out of ``spec/compiled-program.md`` by the same pattern the
18
+ generator uses, so the page stays the source and this stays a copy that cannot
19
+ go stale quietly.
20
+ """
21
+
22
+ #: The compiled format this engine implements: the major it loads, and the
23
+ #: highest minor of that major it was written against.
24
+ FORMAT = "1.1"
25
+
26
+ #: The language versions this engine has library semantics for.
27
+ #:
28
+ #: An engine selects semantics by the program's ``language`` and not by the
29
+ #: newest it implements, so that a saved script never changes its numbers.
30
+ LANGUAGE_VERSIONS = (1,)
31
+
32
+
33
+ def format_major() -> int:
34
+ return int(FORMAT.split(".")[0])
35
+
36
+
37
+ def format_minor() -> int:
38
+ parts = FORMAT.split(".")
39
+ return int(parts[1]) if len(parts) > 1 else 0
openscript/zones.py ADDED
@@ -0,0 +1,118 @@
1
+ """Reading an instant in a named zone, and the one zone this engine can read.
2
+
3
+ `stdlib.md` section 12.1 fixes what a zone is: an IANA name, never a fixed
4
+ offset, because an offset held constant is silently wrong for half the year
5
+ anywhere that observes a seasonal clock change and nothing about the wrong half
6
+ looks wrong. Section 12.2 adds that the name is resolved against the host
7
+ runtime's own timezone database, that two engines agree as far as their databases
8
+ do, and that a name a database does not hold is OS6005 and never a guessed
9
+ offset.
10
+
11
+ **This engine holds one zone and declines the rest, and the reason is the
12
+ dependency rule rather than the calendar.** What the standard library can answer
13
+ is set out here rather than discovered by whoever meets it:
14
+
15
+ - The civil arithmetic needs nothing installed. ``civil.py`` is the proleptic
16
+ Gregorian formula, so ``UTC`` is exact on every machine and for every instant,
17
+ including the ones an interpreter's own date type refuses.
18
+ - The offsets do not come with the interpreter. The module that reads named
19
+ zones is in the standard library, but the table it reads is not: it looks for a
20
+ database the operating system ships, and where there is none it falls back to a
21
+ separately installed package of the same data. On the machine this stage was
22
+ written on, that search path is empty and the fallback package is what answers,
23
+ so an engine built on it would have taken a dependency the host never accepted,
24
+ and ``scripts/check-python.mjs`` could not have caught it: the check reads
25
+ import names against the interpreter's own list, and the import that resolves
26
+ a zone is one of the interpreter's own. It is the data behind it that is not.
27
+ - The consequence is worse than the dependency. `conformance.md` section 5
28
+ requires a case to produce the same result on every machine and to read nothing
29
+ outside the case directory, environment variables included, and that module
30
+ consults an environment variable for its search path. A run that answered a
31
+ session in one zone on a build machine and declined it on a laptop would be
32
+ reproducible on neither.
33
+
34
+ So a zone that is not ``UTC`` is not answered here under a guessed offset and not
35
+ answered from a table this repository copied, which would be the same staleness a
36
+ fixed offset has. It is declined, and the caller reports the case
37
+ ``unsupported`` naming the feature, which `conformance.md` section 8 defines and
38
+ counts separately from a pass. A host whose instruments trade in another zone
39
+ supplies the reader, which is the same footing `stdlib.md` section 12.2 puts every
40
+ engine on: the database is the host runtime's.
41
+
42
+ **The shape of the name is checked before any database is.** Section 12.2
43
+ requires it: "an engine applies this rule before it consults one: a script
44
+ refused on one engine has to be refused on every engine, and an abbreviation is
45
+ ambiguous in any case". So a name that is neither an area and a location nor
46
+ ``UTC`` is malformed on every engine, whatever any database would have said about
47
+ it, and that is a different answer from a well formed name this engine cannot
48
+ read. The two are told apart below because only one of them is this engine's
49
+ limit.
50
+ """
51
+
52
+ import re
53
+ from typing import Optional
54
+
55
+ from .civil import MAX_INSTANT, Civil, fields_at, instant_at, whole_instant
56
+
57
+ #: The one name with no area, which names one offset everywhere and is the whole
58
+ #: of what this engine's own calendar is right for.
59
+ READABLE = "UTC"
60
+
61
+ #: An area and a location, section 12.2's rule, applied before any database is.
62
+ _AREA_AND_LOCATION = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*/[A-Za-z][A-Za-z0-9+_/-]*$")
63
+
64
+
65
+ def named(zone: object) -> bool:
66
+ """Whether this is a zone name at all, as against an abbreviation or an offset.
67
+
68
+ ``"UTC"`` or an area and a location. A name that fails this is one every
69
+ engine refuses, and refusing it is OS6005's own sentence rather than a
70
+ limit of this engine's calendar.
71
+ """
72
+ if not isinstance(zone, str):
73
+ return False
74
+ return zone == READABLE or _AREA_AND_LOCATION.match(zone) is not None
75
+
76
+
77
+ def readable(zone: object) -> bool:
78
+ """Whether this engine can read a calendar in this zone.
79
+
80
+ True of one name. A well formed name this answers false to is not a bad
81
+ name: it is a zone whose offsets this engine does not hold, which is the
82
+ caller's ``unsupported`` and never an answer under another zone's clock.
83
+ """
84
+ return zone == READABLE
85
+
86
+
87
+ def fields_in(instant: float, zone: object) -> Optional[Civil]:
88
+ """The civil fields an instant has in a zone, or nothing where there are none.
89
+
90
+ Three ways to have none, and they are one answer here because the calls above
91
+ make one of them: a zone this engine does not read, a timestamp that is not a
92
+ number, and a magnitude past the largest instant a calendar reads.
93
+ """
94
+ if not readable(zone):
95
+ return None
96
+ if not isinstance(instant, (int, float)) or isinstance(instant, bool):
97
+ return None
98
+ whole = whole_instant(float(instant))
99
+ return None if whole is None else fields_at(whole)
100
+
101
+
102
+ def instant_of(fields: Civil, zone: object) -> Optional[int]:
103
+ """The instant a wall clock reading names in a zone, or nothing for no reading.
104
+
105
+ In ``UTC`` a reading names exactly one instant, so the two readings section
106
+ 12.2 settles for a zone that changes its clock do not arise: there is no hour
107
+ this zone skips and none it repeats. A zone that has them is one this engine
108
+ declines above, which is why that rule is stated there and the arithmetic is
109
+ not written here to be exercised by nothing.
110
+ """
111
+ if not readable(zone):
112
+ return None
113
+ found = instant_at(fields)
114
+ # Compared as it is rather than as a binary64: a year a script computed can
115
+ # be an integer with three hundred digits in it, and converting one of those
116
+ # to a float raises where the first engine answers absence. Nothing in this
117
+ # package raises, so the magnitude is compared and the answer is absence.
118
+ return None if abs(found) > MAX_INSTANT else found
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: openscript
3
+ Version: 0.4.0
4
+ Summary: An open trading language: the engine that runs a compiled program.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
7
+ Project-URL: Source, https://github.com/marketcalls/openscript
8
+ Keywords: trading,language,interpreter,indicators,backtesting
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # openscript
16
+
17
+ The engine that runs a compiled OpenScript program, in Python.
18
+
19
+ OpenScript is an open trading language. A script is compiled to a **compiled
20
+ program**: plain data, an instruction list, never generated code. This package
21
+ runs one.
22
+
23
+ It exists so that a platform can run a strategy where a JavaScript runtime is
24
+ not available, which for a production trading server is the ordinary case. The
25
+ compiler and the first engine are the `openalgo-script` package on npm; this is
26
+ the second engine, and the two are held to each other by a conformance suite
27
+ where any disagreement is a release blocker.
28
+
29
+ ## What it is
30
+
31
+ - **No compiler.** This package is handed a compiled program and never a script.
32
+ The program arrives as canonical text, which is what a run's hash is taken
33
+ over, so an engine cannot quietly run something other than what was recorded.
34
+ - **Nothing builds code out of text.** No string evaluator, no statement
35
+ executor, no import driven by hand, no object graph loaded out of bytes. That
36
+ is what lets a platform run many people's scripts in one process, and it is
37
+ enforced by a check rather than intended.
38
+ - **Zero dependencies.** The standard library only, and not the parts of it that
39
+ stop a run being reproducible. Measured on every build against the module
40
+ names the running interpreter says are its own.
41
+
42
+ ## Installing
43
+
44
+ ```
45
+ pip install openscript
46
+ ```
47
+
48
+ Python 3.12 or newer. Nothing else.
49
+
50
+ ## Using it
51
+
52
+ ```python
53
+ from openscript.run import load_text
54
+
55
+ loaded = load_text(program_text, settings, library)
56
+ if loaded.ok:
57
+ result = loaded.run.execute_bar(0, bar)
58
+ ```
59
+
60
+ A host loads a program once and pushes bars at it one at a time, keeping a
61
+ checkpoint so a bar that is still moving can be executed again and rolled back.
62
+ The conformance adapter is the other way in:
63
+
64
+ ```
65
+ python -m openscript --describe
66
+ python -m openscript <case-directory>
67
+ ```
68
+
69
+ ## Where the documentation is
70
+
71
+ The specification and the guides live in the repository:
72
+
73
+ - `docs/integrating/the-python-engine.md` for what is in this package and what a
74
+ host needs.
75
+ - `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
76
+ the bar cycle, the rollback a moving bar rests on, and the order boundary.
77
+ - `spec/` for the language, the compiled program format, the standard library
78
+ and the conformance suite.
79
+
80
+ ## Licence
81
+
82
+ Apache-2.0.