sashiny 0.1.1__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.
sashiny/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ """SAShiny: use unchanged SAS programs as the server of Shiny applications.
2
+
3
+ SAS is a registered trademark of SAS Institute Inc. Shiny is a registered
4
+ trademark of Posit Software, PBC. This project is not affiliated with or
5
+ endorsed by either company.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from .contract import Contract, Issue, load, validate
11
+ from .engines import Engine, MockEngine, SaspyEngine, connect, load_profiles
12
+ from .lint import lint
13
+ from .pool import Pool
14
+ from .recording import load_recording, save_recording
15
+ from .result import LogLine, Result, TableResult
16
+ from .run import run
17
+ from .upload import UploadError, UploadFile
18
+ from .values import render_values
19
+ from .wrapper import reset_code, wrapper_code
20
+
21
+ __all__ = [
22
+ "Contract",
23
+ "Engine",
24
+ "Issue",
25
+ "LogLine",
26
+ "MockEngine",
27
+ "Pool",
28
+ "Result",
29
+ "SaspyEngine",
30
+ "TableResult",
31
+ "UploadError",
32
+ "UploadFile",
33
+ "__version__",
34
+ "app",
35
+ "connect",
36
+ "lint",
37
+ "load",
38
+ "load_profiles",
39
+ "load_recording",
40
+ "render_values",
41
+ "reset_code",
42
+ "run",
43
+ "save_recording",
44
+ "validate",
45
+ "wrapper_code",
46
+ ]
47
+
48
+ __version__ = "0.1.1"
49
+
50
+
51
+ def __getattr__(name: str) -> Any:
52
+ """Lazily expose ``app`` and ``modules``, which need the optional ``shiny``.
53
+
54
+ The core package must import without ``shiny`` installed (it is an optional
55
+ extra). ``app`` and ``modules`` are only imported when first accessed.
56
+ """
57
+ if name == "app":
58
+ from .app import app as _app # noqa: PLC0415 - optional dependency
59
+
60
+ globals()["app"] = _app
61
+ return _app
62
+ if name == "modules":
63
+ from importlib import import_module # noqa: PLC0415 - optional dependency
64
+
65
+ _modules = import_module(f"{__name__}.modules")
66
+ globals()["modules"] = _modules
67
+ return _modules
68
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,383 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/meharpsingh/SAShiny/spec/sashiny.schema.json",
4
+ "title": "SAShiny sidecar contract",
5
+ "description": "Schema for <program>.sashiny.yml files. Version 1. Structural rules only; semantic rules (S-codes) are defined in CONTRACT.md.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["sashiny", "program", "title", "outputs"],
9
+ "properties": {
10
+ "sashiny": {
11
+ "description": "Contract schema version.",
12
+ "const": 1
13
+ },
14
+ "program": {
15
+ "description": "Path to the SAS program, relative to this YAML file. Forward slashes only; no absolute paths; no parent-directory segments.",
16
+ "type": "string",
17
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(^|/)\\.\\.(/|$))[^\\\\]+\\.[Ss][Aa][Ss]$"
18
+ },
19
+ "id": {
20
+ "description": "Module id. Defaults to the program file name without extension, lower-cased, with invalid characters replaced by '_'.",
21
+ "$ref": "#/$defs/identifier"
22
+ },
23
+ "title": {"type": "string", "minLength": 1, "maxLength": 200},
24
+ "description": {"type": "string", "maxLength": 5000},
25
+ "version": {
26
+ "description": "Free-form version of this contract, chosen by its author.",
27
+ "type": "string",
28
+ "maxLength": 64
29
+ },
30
+ "inputs": {
31
+ "type": "object",
32
+ "propertyNames": {"$ref": "#/$defs/identifier"},
33
+ "additionalProperties": {"$ref": "#/$defs/input"}
34
+ },
35
+ "outputs": {
36
+ "type": "object",
37
+ "minProperties": 1,
38
+ "propertyNames": {"$ref": "#/$defs/identifier"},
39
+ "additionalProperties": {"$ref": "#/$defs/output"}
40
+ },
41
+ "run": {"$ref": "#/$defs/run"},
42
+ "ui": {"$ref": "#/$defs/ui"}
43
+ },
44
+
45
+ "$defs": {
46
+ "identifier": {
47
+ "description": "Input/output/module identifier. Also used as a Shiny id.",
48
+ "type": "string",
49
+ "pattern": "^[a-z][a-z0-9_]{0,31}$"
50
+ },
51
+ "sas_name": {
52
+ "description": "SAS name (macro variable, macro, column). 1-32 chars. Names starting with SYS, SASHINY or _SX_ (any case) are reserved.",
53
+ "type": "string",
54
+ "pattern": "^(?![Ss][Yy][Ss])(?![Ss][Aa][Ss][Hh][Ii][Nn][Yy])(?!_[Ss][Xx]_)[A-Za-z_][A-Za-z0-9_]{0,31}$"
55
+ },
56
+ "libref_member": {
57
+ "description": "Two-level SAS dataset name: libref.member.",
58
+ "type": "string",
59
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,7}\\.[A-Za-z_][A-Za-z0-9_]{0,31}$"
60
+ },
61
+ "work_member": {
62
+ "description": "Two-level dataset name in the WORK library.",
63
+ "type": "string",
64
+ "pattern": "^[Ww][Oo][Rr][Kk]\\.[A-Za-z_][A-Za-z0-9_]{0,31}$"
65
+ },
66
+ "iso_date": {
67
+ "type": "string",
68
+ "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"
69
+ },
70
+ "date_value": {
71
+ "description": "ISO date or the keyword 'today' (resolved by the client when the session starts).",
72
+ "anyOf": [{"$ref": "#/$defs/iso_date"}, {"const": "today"}]
73
+ },
74
+ "relative_path": {
75
+ "type": "string",
76
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(^|/)\\.\\.(/|$))[^\\\\]+$"
77
+ },
78
+ "text_render": {"enum": ["raw", "quoted", "dquoted"]},
79
+ "list_render": {"enum": ["quoted", "dquoted", "bare"]},
80
+ "date_render": {"enum": ["literal", "date9", "iso", "sasdate"]},
81
+
82
+ "choice_list": {
83
+ "type": "array",
84
+ "minItems": 1,
85
+ "maxItems": 1000,
86
+ "items": {
87
+ "anyOf": [
88
+ {"type": "string", "minLength": 1, "maxLength": 1000},
89
+ {
90
+ "type": "object",
91
+ "additionalProperties": false,
92
+ "required": ["value"],
93
+ "properties": {
94
+ "value": {"type": "string", "minLength": 1, "maxLength": 1000},
95
+ "label": {"type": "string", "minLength": 1, "maxLength": 200}
96
+ }
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ "choices_from": {
102
+ "description": "Choices read from a SAS dataset when the session starts.",
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": ["dataset", "value_column"],
106
+ "properties": {
107
+ "dataset": {"$ref": "#/$defs/libref_member"},
108
+ "value_column": {"$ref": "#/$defs/column_name"},
109
+ "label_column": {"$ref": "#/$defs/column_name"},
110
+ "max_choices": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 1000}
111
+ }
112
+ },
113
+ "column_name": {
114
+ "type": "string",
115
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,31}$"
116
+ },
117
+
118
+ "input_common": {
119
+ "type": "object",
120
+ "required": ["type", "label"],
121
+ "properties": {
122
+ "type": {"enum": ["numeric", "text", "choice", "multichoice", "date", "daterange", "dataset"]},
123
+ "label": {"type": "string", "minLength": 1, "maxLength": 200},
124
+ "help": {"type": "string", "maxLength": 1000},
125
+ "optional": {"type": "boolean", "default": false}
126
+ }
127
+ },
128
+
129
+ "input": {
130
+ "type": "object",
131
+ "required": ["type"],
132
+ "properties": {"type": {"enum": ["numeric", "text", "choice", "multichoice", "date", "daterange", "dataset"]}},
133
+ "allOf": [
134
+ {"if": {"properties": {"type": {"const": "numeric"}}}, "then": {"$ref": "#/$defs/input_numeric"}},
135
+ {"if": {"properties": {"type": {"const": "text"}}}, "then": {"$ref": "#/$defs/input_text"}},
136
+ {"if": {"properties": {"type": {"const": "choice"}}}, "then": {"$ref": "#/$defs/input_choice"}},
137
+ {"if": {"properties": {"type": {"const": "multichoice"}}}, "then": {"$ref": "#/$defs/input_multichoice"}},
138
+ {"if": {"properties": {"type": {"const": "date"}}}, "then": {"$ref": "#/$defs/input_date"}},
139
+ {"if": {"properties": {"type": {"const": "daterange"}}}, "then": {"$ref": "#/$defs/input_daterange"}},
140
+ {"if": {"properties": {"type": {"const": "dataset"}}}, "then": {"$ref": "#/$defs/input_dataset"}}
141
+ ]
142
+ },
143
+
144
+ "input_numeric": {
145
+ "allOf": [{"$ref": "#/$defs/input_common"}],
146
+ "unevaluatedProperties": false,
147
+ "properties": {
148
+ "type": {"const": "numeric"},
149
+ "macro": {"$ref": "#/$defs/sas_name"},
150
+ "min": {"type": "number"},
151
+ "max": {"type": "number"},
152
+ "step": {"type": "number", "exclusiveMinimum": 0},
153
+ "integer": {"type": "boolean", "default": false},
154
+ "default": {"type": "number"}
155
+ }
156
+ },
157
+ "input_text": {
158
+ "allOf": [{"$ref": "#/$defs/input_common"}],
159
+ "unevaluatedProperties": false,
160
+ "properties": {
161
+ "type": {"const": "text"},
162
+ "macro": {"$ref": "#/$defs/sas_name"},
163
+ "max_length": {"type": "integer", "minimum": 1, "maximum": 32000, "default": 256},
164
+ "pattern": {
165
+ "description": "Regular expression the whole value must match. Use only the portable subset defined in CONTRACT.md.",
166
+ "type": "string",
167
+ "minLength": 1,
168
+ "maxLength": 500
169
+ },
170
+ "placeholder": {"type": "string", "maxLength": 200},
171
+ "render": {"$ref": "#/$defs/text_render", "default": "raw"},
172
+ "default": {"type": "string", "maxLength": 32000}
173
+ }
174
+ },
175
+ "input_choice": {
176
+ "allOf": [
177
+ {"$ref": "#/$defs/input_common"},
178
+ {"oneOf": [{"required": ["choices"]}, {"required": ["choices_from"]}]}
179
+ ],
180
+ "unevaluatedProperties": false,
181
+ "properties": {
182
+ "type": {"const": "choice"},
183
+ "macro": {"$ref": "#/$defs/sas_name"},
184
+ "choices": {"$ref": "#/$defs/choice_list"},
185
+ "choices_from": {"$ref": "#/$defs/choices_from"},
186
+ "widget": {"enum": ["select", "radio"], "default": "select"},
187
+ "render": {"$ref": "#/$defs/text_render", "default": "raw"},
188
+ "default": {"type": "string", "minLength": 1, "maxLength": 1000}
189
+ }
190
+ },
191
+ "input_multichoice": {
192
+ "allOf": [
193
+ {"$ref": "#/$defs/input_common"},
194
+ {"oneOf": [{"required": ["choices"]}, {"required": ["choices_from"]}]}
195
+ ],
196
+ "unevaluatedProperties": false,
197
+ "properties": {
198
+ "type": {"const": "multichoice"},
199
+ "macro": {"$ref": "#/$defs/sas_name"},
200
+ "choices": {"$ref": "#/$defs/choice_list"},
201
+ "choices_from": {"$ref": "#/$defs/choices_from"},
202
+ "widget": {"enum": ["select", "checkbox"], "default": "select"},
203
+ "render": {"$ref": "#/$defs/list_render", "default": "quoted"},
204
+ "separator": {"enum": ["space", "comma"], "default": "space"},
205
+ "count_macro": {"$ref": "#/$defs/sas_name"},
206
+ "min_selected": {"type": "integer", "minimum": 0, "default": 1},
207
+ "max_selected": {"type": "integer", "minimum": 1},
208
+ "default": {
209
+ "type": "array",
210
+ "uniqueItems": true,
211
+ "items": {"type": "string", "minLength": 1, "maxLength": 1000}
212
+ }
213
+ }
214
+ },
215
+ "input_date": {
216
+ "allOf": [{"$ref": "#/$defs/input_common"}],
217
+ "unevaluatedProperties": false,
218
+ "properties": {
219
+ "type": {"const": "date"},
220
+ "macro": {"$ref": "#/$defs/sas_name"},
221
+ "min": {"$ref": "#/$defs/date_value"},
222
+ "max": {"$ref": "#/$defs/date_value"},
223
+ "render": {"$ref": "#/$defs/date_render", "default": "literal"},
224
+ "default": {"$ref": "#/$defs/date_value"}
225
+ }
226
+ },
227
+ "input_daterange": {
228
+ "allOf": [{"$ref": "#/$defs/input_common"}],
229
+ "unevaluatedProperties": false,
230
+ "properties": {
231
+ "type": {"const": "daterange"},
232
+ "macro": {
233
+ "description": "Exactly two macro variable names: [start, end]. Defaults to [<id>_start, <id>_end].",
234
+ "type": "array",
235
+ "minItems": 2,
236
+ "maxItems": 2,
237
+ "items": {"$ref": "#/$defs/sas_name"}
238
+ },
239
+ "min": {"$ref": "#/$defs/date_value"},
240
+ "max": {"$ref": "#/$defs/date_value"},
241
+ "render": {"$ref": "#/$defs/date_render", "default": "literal"},
242
+ "default": {
243
+ "type": "array",
244
+ "minItems": 2,
245
+ "maxItems": 2,
246
+ "items": {"$ref": "#/$defs/date_value"}
247
+ }
248
+ }
249
+ },
250
+ "input_dataset": {
251
+ "allOf": [{"$ref": "#/$defs/input_common"}],
252
+ "unevaluatedProperties": false,
253
+ "required": ["target"],
254
+ "properties": {
255
+ "type": {"const": "dataset"},
256
+ "macro": {
257
+ "description": "Optional macro variable set to the upper-cased target name when a file is supplied, or to an empty value otherwise.",
258
+ "$ref": "#/$defs/sas_name"
259
+ },
260
+ "target": {"$ref": "#/$defs/work_member"},
261
+ "accept": {
262
+ "type": "array",
263
+ "minItems": 1,
264
+ "uniqueItems": true,
265
+ "items": {"enum": ["csv", "xlsx", "xpt", "sas7bdat"]},
266
+ "default": ["csv", "xlsx", "xpt", "sas7bdat"]
267
+ },
268
+ "max_size_mb": {"type": "number", "exclusiveMinimum": 0, "maximum": 2048, "default": 50},
269
+ "columns": {
270
+ "description": "Columns the uploaded file must contain, with the SAS type they are converted to.",
271
+ "type": "array",
272
+ "minItems": 1,
273
+ "items": {
274
+ "type": "object",
275
+ "additionalProperties": false,
276
+ "required": ["name", "type"],
277
+ "properties": {
278
+ "name": {"$ref": "#/$defs/column_name"},
279
+ "type": {"enum": ["numeric", "character", "date"]}
280
+ }
281
+ }
282
+ },
283
+ "sample": {
284
+ "description": "Path, relative to this YAML, of a sample file the UI may offer as 'Use sample data'.",
285
+ "$ref": "#/$defs/relative_path"
286
+ }
287
+ }
288
+ },
289
+
290
+ "output": {
291
+ "type": "object",
292
+ "required": ["type"],
293
+ "properties": {"type": {"enum": ["table", "plot", "html", "log", "download"]}},
294
+ "allOf": [
295
+ {"if": {"properties": {"type": {"const": "table"}}}, "then": {"$ref": "#/$defs/output_table"}},
296
+ {"if": {"properties": {"type": {"const": "plot"}}}, "then": {"$ref": "#/$defs/output_plot"}},
297
+ {"if": {"properties": {"type": {"const": "html"}}}, "then": {"$ref": "#/$defs/output_html"}},
298
+ {"if": {"properties": {"type": {"const": "log"}}}, "then": {"$ref": "#/$defs/output_log"}},
299
+ {"if": {"properties": {"type": {"const": "download"}}}, "then": {"$ref": "#/$defs/output_download"}}
300
+ ]
301
+ },
302
+ "output_common": {
303
+ "type": "object",
304
+ "properties": {
305
+ "label": {"type": "string", "minLength": 1, "maxLength": 200},
306
+ "help": {"type": "string", "maxLength": 1000}
307
+ }
308
+ },
309
+ "output_table": {
310
+ "allOf": [{"$ref": "#/$defs/output_common"}],
311
+ "unevaluatedProperties": false,
312
+ "required": ["from"],
313
+ "properties": {
314
+ "type": {"const": "table"},
315
+ "from": {"$ref": "#/$defs/libref_member"},
316
+ "columns": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/column_name"}},
317
+ "max_rows": {"type": "integer", "minimum": 1, "maximum": 1000000, "default": 10000},
318
+ "formatted": {"type": "boolean", "default": false}
319
+ }
320
+ },
321
+ "output_plot": {
322
+ "allOf": [{"$ref": "#/$defs/output_common"}],
323
+ "unevaluatedProperties": false,
324
+ "properties": {
325
+ "type": {"const": "plot"},
326
+ "select": {
327
+ "description": "1-based position of the graph among all ODS graphs produced by the run. Omit to show all graphs.",
328
+ "type": "integer",
329
+ "minimum": 1
330
+ }
331
+ }
332
+ },
333
+ "output_html": {
334
+ "allOf": [{"$ref": "#/$defs/output_common"}],
335
+ "unevaluatedProperties": false,
336
+ "properties": {"type": {"const": "html"}}
337
+ },
338
+ "output_log": {
339
+ "allOf": [{"$ref": "#/$defs/output_common"}],
340
+ "unevaluatedProperties": false,
341
+ "properties": {
342
+ "type": {"const": "log"},
343
+ "scope": {"enum": ["program", "full"], "default": "program"}
344
+ }
345
+ },
346
+ "output_download": {
347
+ "allOf": [{"$ref": "#/$defs/output_common"}],
348
+ "unevaluatedProperties": false,
349
+ "required": ["format"],
350
+ "properties": {
351
+ "type": {"const": "download"},
352
+ "format": {"enum": ["csv", "xlsx", "xpt", "sas7bdat", "rtf", "pdf"]},
353
+ "from": {"$ref": "#/$defs/libref_member"},
354
+ "filename": {
355
+ "description": "Download file name without extension.",
356
+ "type": "string",
357
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$"
358
+ }
359
+ }
360
+ },
361
+
362
+ "run": {
363
+ "type": "object",
364
+ "additionalProperties": false,
365
+ "properties": {
366
+ "entry": {"enum": ["program", "macro"], "default": "program"},
367
+ "macro": {"$ref": "#/$defs/sas_name"},
368
+ "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 86400, "default": 300},
369
+ "reset_work": {"type": "boolean", "default": true}
370
+ }
371
+ },
372
+ "ui": {
373
+ "type": "object",
374
+ "additionalProperties": false,
375
+ "properties": {
376
+ "layout": {"enum": ["sidebar", "top"], "default": "sidebar"},
377
+ "order": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/identifier"}},
378
+ "run_label": {"type": "string", "minLength": 1, "maxLength": 50, "default": "Run"},
379
+ "auto_run": {"type": "boolean", "default": false}
380
+ }
381
+ }
382
+ }
383
+ }
sashiny/_schema.py ADDED
@@ -0,0 +1,18 @@
1
+ """Bundled JSON Schema for the SAShiny sidecar contract.
2
+
3
+ The schema is a copy of ``spec/sashiny.schema.json``, synced into the package
4
+ at build time so the library is self-contained (no dependency on the repo's
5
+ ``spec/`` directory at runtime).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ _SCHEMA_PATH = Path(__file__).resolve().parent / "_data" / "sashiny.schema.json"
15
+
16
+ SCHEMA: dict[str, Any] = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
17
+
18
+ __all__ = ["SCHEMA"]
sashiny/app.py ADDED
@@ -0,0 +1,60 @@
1
+ """App helpers for SAShiny.
2
+
3
+ ``app`` builds a multi-program Shiny app from a list of contracts, using the
4
+ Shiny modules and an engine. It is the one-liner entry point for hosting several
5
+ SAS programs as tabs in a single app.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .contract import Contract, load
14
+ from .engines import Engine
15
+ from .modules import program_server, program_ui
16
+
17
+ __all__ = ["app"]
18
+
19
+
20
+ def app(
21
+ contracts: list[Contract | str | Path],
22
+ engine: Engine,
23
+ *,
24
+ title: str = "SAShiny",
25
+ ) -> Any:
26
+ """Build a multi-program Shiny app from a list of contracts.
27
+
28
+ Args:
29
+ contracts: A list of :class:`Contract` objects or paths to
30
+ ``*.sashiny.yml`` files (loaded automatically).
31
+ engine: The engine to run each program on (mock or saspy).
32
+ title: The app title.
33
+
34
+ Returns:
35
+ A Shiny ``App`` with one tab per contract.
36
+
37
+ """
38
+ from shiny import App, ui # noqa: PLC0415 - optional dependency
39
+
40
+ loaded = [c if isinstance(c, Contract) else load(c) for c in contracts]
41
+
42
+ def _module_id(i: int, contract: Contract) -> str:
43
+ return contract.data.get("id") or f"program_{i}"
44
+
45
+ app_ui = ui.page_navbar(
46
+ *[
47
+ ui.nav_panel(
48
+ contract.data.get("title", f"Program {i}"),
49
+ program_ui(_module_id(i, contract), contract),
50
+ )
51
+ for i, contract in enumerate(loaded)
52
+ ],
53
+ title=title,
54
+ )
55
+
56
+ def server(input: Any, output: Any, session: Any) -> None:
57
+ for i, contract in enumerate(loaded):
58
+ program_server(_module_id(i, contract), contract, engine)
59
+
60
+ return App(app_ui, server)