trendify2 2.0.4__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 (76) hide show
  1. trendify/__about__.py +14 -0
  2. trendify/__init__.py +250 -0
  3. trendify/base/__init__.py +36 -0
  4. trendify/base/helpers.py +76 -0
  5. trendify/base/pen.py +161 -0
  6. trendify/base/record.py +148 -0
  7. trendify/cli.py +403 -0
  8. trendify/color.py +260 -0
  9. trendify/examples.py +474 -0
  10. trendify/formats/__init__.py +28 -0
  11. trendify/formats/format2d.py +128 -0
  12. trendify/formats/table.py +55 -0
  13. trendify/generator/__init__.py +38 -0
  14. trendify/generator/generate.py +167 -0
  15. trendify/generator/histogrammer.py +62 -0
  16. trendify/generator/render.py +195 -0
  17. trendify/generator/table_builder.py +194 -0
  18. trendify/generator/xy_data_plotter.py +195 -0
  19. trendify/log.py +195 -0
  20. trendify/pipeline.py +136 -0
  21. trendify/plotting/__init__.py +48 -0
  22. trendify/plotting/axline.py +122 -0
  23. trendify/plotting/figure.py +389 -0
  24. trendify/plotting/histogram.py +282 -0
  25. trendify/plotting/point.py +89 -0
  26. trendify/plotting/scatter.py +100 -0
  27. trendify/plotting/trace.py +129 -0
  28. trendify/progress.py +43 -0
  29. trendify/store/__init__.py +30 -0
  30. trendify/store/db.py +119 -0
  31. trendify/store/record_store.py +448 -0
  32. trendify/store/tags.py +65 -0
  33. trendify/styling/__init__.py +30 -0
  34. trendify/styling/grid.py +125 -0
  35. trendify/styling/legend.py +217 -0
  36. trendify/styling/marker.py +168 -0
  37. trendify/typing.py +26 -0
  38. trendify/viewer/__init__.py +41 -0
  39. trendify/viewer/app.py +208 -0
  40. trendify/viewer/hydration.py +181 -0
  41. trendify/viewer/plot_config.py +102 -0
  42. trendify/viewer/routes/__init__.py +6 -0
  43. trendify/viewer/routes/api.py +360 -0
  44. trendify/viewer/routes/pages.py +39 -0
  45. trendify/viewer/tag_tree.py +148 -0
  46. trendify/viewer/templates/base.html +63 -0
  47. trendify/viewer/templates/index.html +131 -0
  48. trendify/viewer/templates/partials/__init__.py +1 -0
  49. trendify/viewer/templates/partials/footer.html +15 -0
  50. trendify/viewer/templates/partials/nav.html +181 -0
  51. trendify/viewer/templates/partials/plot_view.html +221 -0
  52. trendify/viewer/templates/partials/sidebar.html +163 -0
  53. trendify/viewer/templates/partials/table_view.html +43 -0
  54. trendify/viewer/templates/static/__init__.py +1 -0
  55. trendify/viewer/templates/static/assets/__init__.py +1 -0
  56. trendify/viewer/templates/static/assets/main-logo-white-bg.svg +75 -0
  57. trendify/viewer/templates/static/assets/main-logo.svg +66 -0
  58. trendify/viewer/templates/static/css/app.css +75 -0
  59. trendify/viewer/templates/static/js/main.js +2 -0
  60. trendify/viewer/templates/static/vendored/__init__.py +1 -0
  61. trendify/viewer/templates/static/vendored/alpine-3.15.12.min.js +5 -0
  62. trendify/viewer/templates/static/vendored/alpine-LICENSE.md +21 -0
  63. trendify/viewer/templates/static/vendored/dataTables-LICENSE.txt +3 -0
  64. trendify/viewer/templates/static/vendored/dataTables.dataTables.min.css +1 -0
  65. trendify/viewer/templates/static/vendored/dataTables.min.js +4 -0
  66. trendify/viewer/templates/static/vendored/jquery-3.7.1.min.js +2 -0
  67. trendify/viewer/templates/static/vendored/jquery-LICENSE.txt +3 -0
  68. trendify/viewer/templates/static/vendored/plotly-3.6.0.min.js +3882 -0
  69. trendify/viewer/templates/static/vendored/plotly-LICENSE.txt +21 -0
  70. trendify/viewer/templates/static/vendored/tailwind-4.3.2.global.js +947 -0
  71. trendify/viewer/templates/static/vendored/tailwind-LICENSE +21 -0
  72. trendify2-2.0.4.dist-info/METADATA +107 -0
  73. trendify2-2.0.4.dist-info/RECORD +76 -0
  74. trendify2-2.0.4.dist-info/WHEEL +4 -0
  75. trendify2-2.0.4.dist-info/entry_points.txt +2 -0
  76. trendify2-2.0.4.dist-info/licenses/LICENSE.txt +21 -0
trendify/cli.py ADDED
@@ -0,0 +1,403 @@
1
+ """
2
+ `trendify` CLI, built on Typer. It is a thin argument-parsing layer over `TrendifyPipeline`:
3
+ every command resolves its arguments then calls straight into the same pipeline class a Python
4
+ caller would use directly, so the CLI and the Python API can never drift apart.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import glob
10
+ import importlib
11
+ import importlib.util
12
+ import logging
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+ from typing import cast
18
+
19
+ import typer
20
+ from rich.align import Align
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.text import Text
24
+
25
+ from trendify.__about__ import __version__
26
+ from trendify.base.record import RecordGenerator
27
+ from trendify.color import Color
28
+ from trendify.examples import make_example_data
29
+ from trendify.log import setup_logger
30
+ from trendify.pipeline import TrendifyPipeline
31
+ from trendify.store.record_store import RecordStore
32
+
33
+ __all__ = ["app"]
34
+
35
+ logger = logging.getLogger(__name__)
36
+ console = Console()
37
+
38
+ _LOGO_LINES = [
39
+ "████████╗██████╗ ███████╗███╗ ██╗██████╗ ██╗███████╗██╗ ██╗",
40
+ "╚══██╔══╝██╔══██╗██╔════╝████╗ ██║██╔══██╗██║██╔════╝╚██╗ ██╔╝",
41
+ " ██║ ██████╔╝█████╗ ██╔██╗ ██║██║ ██║██║█████╗ ╚████╔╝ ",
42
+ " ██║ ██╔══██╗██╔══╝ ██║╚██╗██║██║ ██║██║██╔══╝ ╚██╔╝ ",
43
+ " ██║ ██║ ██║███████╗██║ ╚████║██████╔╝██║██║ ██║ ",
44
+ " ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝╚═╝ ╚═╝ ",
45
+ ]
46
+ _SHADES = [
47
+ Color.ROSE_300,
48
+ Color.ROSE_400,
49
+ Color.ROSE_500,
50
+ Color.ROSE_600,
51
+ Color.ROSE_700,
52
+ Color.ROSE_800,
53
+ ]
54
+
55
+
56
+ def print_logo() -> None:
57
+ body = Text()
58
+ for i, (line, shade) in enumerate(zip(_LOGO_LINES, _SHADES)):
59
+ body.append(line, style=f"bold {shade}")
60
+ if i != len(_LOGO_LINES) - 1:
61
+ body.append("\n")
62
+
63
+ console.print(
64
+ Panel(
65
+ Align.center(body),
66
+ expand=False,
67
+ border_style="indian_red1",
68
+ subtitle=f"[dim]v{__version__}[/dim]",
69
+ )
70
+ )
71
+
72
+
73
+ app = typer.Typer(
74
+ name="trendify",
75
+ help="Generate visual records and static assets from raw data.",
76
+ rich_markup_mode="rich",
77
+ no_args_is_help=True,
78
+ )
79
+
80
+
81
+ def version_callback(value: bool) -> None:
82
+ if value:
83
+ console.print(
84
+ f"[bold indian_red1]trendify[/bold indian_red1] [bold white]{__version__}"
85
+ )
86
+ raise typer.Exit()
87
+
88
+
89
+ @app.callback(
90
+ epilog="Check out the [link=https://github.com/Hydrowelder/trendify/]full documentation[/link] for more info."
91
+ )
92
+ def main(
93
+ ctx: typer.Context,
94
+ version: bool = typer.Option(
95
+ False,
96
+ "--version",
97
+ callback=version_callback,
98
+ is_eager=True,
99
+ help="Show version and exit.",
100
+ ),
101
+ ) -> None:
102
+ """
103
+ [bold indian_red1]Trendify:[/bold indian_red1] Generate visual records and static assets from raw data.
104
+ """
105
+
106
+
107
+ InputDirectoriesOption = typer.Option(
108
+ ...,
109
+ "-i",
110
+ "--input-directories",
111
+ help="Raw data directories (or glob patterns) to map the record generator over.",
112
+ )
113
+ RecordGeneratorOption = typer.Option(
114
+ ...,
115
+ "-g",
116
+ "--record-generator",
117
+ help=(
118
+ "RecordGenerator to map over input directories, given as 'module:function', "
119
+ "'module:Class.method', or '/path/to/file.py:function'."
120
+ ),
121
+ )
122
+ OutputDirectoryOption = typer.Option(
123
+ ...,
124
+ "-o",
125
+ "--output-directory",
126
+ help="Directory the pipeline reads/writes under.",
127
+ )
128
+ NProcsOption = typer.Option(
129
+ 1,
130
+ "-n",
131
+ "--n-procs",
132
+ help="Number of parallel worker processes.",
133
+ min=1,
134
+ max=os.cpu_count(),
135
+ )
136
+ VerboseOption = typer.Option(
137
+ 0, "-v", "--verbose", count=True, help="Increase log verbosity."
138
+ )
139
+ QuietOption = typer.Option(
140
+ 0, "--quiet", "-q", count=True, help="Decrease log verbosity."
141
+ )
142
+ SkipTablesOption = typer.Option(
143
+ False, "--skip-tables", help="Suppress TableEntry CSV output."
144
+ )
145
+ SkipXyPlotsOption = typer.Option(
146
+ False,
147
+ "--skip-xy-plots",
148
+ help="Suppress Point2D/Scatter2D/Trace2D/AxLine plot output.",
149
+ )
150
+ SkipHistogramsOption = typer.Option(
151
+ False,
152
+ "--skip-histograms",
153
+ help="Suppress HistogramEntry plot output.",
154
+ )
155
+ DbPathArgument = typer.Argument(
156
+ ...,
157
+ help="Path to a trendify.db file.",
158
+ )
159
+ ExampleWorkdirOption = typer.Option(
160
+ ...,
161
+ "-w",
162
+ "--workdir",
163
+ help="Directory in which to generate sample data.",
164
+ )
165
+ ExampleNFoldersOption = typer.Option(
166
+ 10,
167
+ "-n",
168
+ "--n-folders",
169
+ help="Number of sample data sets (subfolders) to generate.",
170
+ )
171
+ ExampleNColsOption = typer.Option(
172
+ 3,
173
+ "--n-cols",
174
+ help=(
175
+ "Data columns per results.csv. The first 3 are the plotted waveforms; any beyond that "
176
+ "are unused filler that only make reading each file more expensive."
177
+ ),
178
+ min=3,
179
+ )
180
+ HostOption = typer.Option(
181
+ "127.0.0.1",
182
+ "--host",
183
+ help="Interface to bind the dashboard server to.",
184
+ )
185
+ PortOption = typer.Option(
186
+ 8000,
187
+ "--port",
188
+ help="Port to bind the dashboard server to.",
189
+ )
190
+
191
+
192
+ def _configure_logging(verbose: int, quiet: int, logo: bool = True) -> logging.Logger:
193
+ """
194
+ Configures the root logger via `trendify.log.setup_logger` (Rich console output plus a
195
+ rotating `trendify.log` file) rather than a one-off `logging.basicConfig`. This way a CLI
196
+ run gets the same beautiful, concurrent-safe logging setup a Python caller gets by calling
197
+ `setup_logger()` themselves, and `generate_records`'s multiprocess path picks up these
198
+ same handlers via its `QueueListener` (see `trendify.generator.generate`).
199
+ """
200
+ if logo:
201
+ print_logo()
202
+ level = logging.INFO - (verbose * 10) + (quiet * 10)
203
+ return setup_logger(level=level)
204
+
205
+
206
+ def _resolve_input_directories(patterns: list[str], ctx: typer.Context) -> list[Path]:
207
+ """
208
+ Expands glob patterns (and plain paths) into a list of directories. A match on a file
209
+ resolves to its parent directory.
210
+
211
+ `ctx.args` picks up bare paths that Click couldn't bind to `-i`/`--input-directories`
212
+ directly. Git Bash/MSYS2 glob-expands wildcard arguments to *native* (non-MSYS)
213
+ executables at process-spawn time, even when the argument was quoted in bash (bash
214
+ itself correctly leaves a quoted `*` alone, but MSYS's runtime re-expands it before the
215
+ native `trendify` executable ever sees argv). That turns `-i "data/*"` into
216
+ `-i data/1 data/2 data/3 ...`, but `-i` only consumes the one value immediately
217
+ following it, so `data/2`/`data/3`/etc. would otherwise be rejected as unexpected extra
218
+ arguments (the commands using this opt into that via
219
+ `context_settings={"allow_extra_args": True}`).
220
+ """
221
+ dirs: list[Path] = []
222
+ for pattern in [*patterns, *ctx.args]:
223
+ for match in glob.glob(pattern, root_dir=Path.cwd(), recursive=True):
224
+ path = Path(match)
225
+ dirs.append((path.parent if path.is_file() else path).resolve())
226
+ return dirs
227
+
228
+
229
+ def _import_from_path(module_name: str, file_path: Path) -> ModuleType:
230
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
231
+ if spec is None or spec.loader is None:
232
+ raise typer.BadParameter(f"Could not import {file_path} as a Python module")
233
+ module = importlib.util.module_from_spec(spec)
234
+ sys.modules[module_name] = module
235
+ spec.loader.exec_module(module)
236
+ return module
237
+
238
+
239
+ def _resolve_record_generator(spec: str) -> RecordGenerator:
240
+ """
241
+ Resolves a `"module:function"` / `"module:Class.method"` / `"/path/to/file.py:function"`
242
+ spec string into a callable.
243
+ """
244
+ # `rpartition` (not `partition`): a Windows absolute path's drive letter is itself a
245
+ # colon (`C:\...\gen.py:generate`), so splitting on the *first* colon mistakes the
246
+ # drive letter for the module/function separator. The attr path never contains a
247
+ # colon, so the *last* colon is always the real separator.
248
+ module_path, _, attr_path = spec.rpartition(":")
249
+ if not attr_path:
250
+ raise typer.BadParameter(
251
+ f"{spec!r} must be in the form 'module:function' or 'path/to/file.py:function'"
252
+ )
253
+
254
+ if Path(module_path).exists():
255
+ file_path = Path(module_path).resolve()
256
+ # `n_procs > 1` sends `record_generator` to worker processes via pickle, which
257
+ # serializes a module-level function as a `(module_name, qualname)` reference, not
258
+ # the code itself. Worker processes (forkserver/spawn) re-import that module by name
259
+ # using the `sys.path` snapshotted when the process pool starts, so the file's parent
260
+ # directory must be on `sys.path` *before* that happens, not just registered in this
261
+ # process's `sys.modules` (which `_import_from_path` alone would do).
262
+ parent_dir = str(file_path.parent)
263
+ if parent_dir not in sys.path:
264
+ sys.path.insert(0, parent_dir)
265
+ module = _import_from_path(file_path.stem, file_path)
266
+ else:
267
+ module = importlib.import_module(module_path)
268
+
269
+ obj: object = module
270
+ for part in attr_path.split("."):
271
+ obj = getattr(obj, part)
272
+ return cast(RecordGenerator, obj)
273
+
274
+
275
+ @app.command(name="generate", context_settings={"allow_extra_args": True})
276
+ def generate(
277
+ ctx: typer.Context,
278
+ input_directories: list[str] = InputDirectoriesOption,
279
+ record_generator: str = RecordGeneratorOption,
280
+ output_directory: Path = OutputDirectoryOption,
281
+ n_procs: int = NProcsOption,
282
+ verbose: int = VerboseOption,
283
+ quiet: int = QuietOption,
284
+ ) -> None:
285
+ """Generate tagged records from raw data directories."""
286
+ _configure_logging(verbose, quiet)
287
+ pipeline = TrendifyPipeline(output_dir=output_directory, n_procs=n_procs)
288
+ _total = pipeline.generate(
289
+ record_generator=_resolve_record_generator(record_generator),
290
+ data_dirs=_resolve_input_directories(input_directories, ctx),
291
+ )
292
+
293
+
294
+ @app.command(name="render")
295
+ def render(
296
+ ctx: typer.Context,
297
+ output_directory: Path = OutputDirectoryOption,
298
+ verbose: int = VerboseOption,
299
+ quiet: int = QuietOption,
300
+ ) -> None:
301
+ """Render CSV tables and matplotlib figures from already-generated records."""
302
+ _configure_logging(verbose, quiet)
303
+ pipeline = TrendifyPipeline(output_dir=output_directory)
304
+ pipeline.render()
305
+ typer.echo(f"Rendered assets to {pipeline.assets_dir}")
306
+
307
+
308
+ @app.command(name="run", context_settings={"allow_extra_args": True})
309
+ def run(
310
+ ctx: typer.Context,
311
+ input_directories: list[str] = InputDirectoriesOption,
312
+ record_generator: str = RecordGeneratorOption,
313
+ output_directory: Path = OutputDirectoryOption,
314
+ n_procs: int = NProcsOption,
315
+ verbose: int = VerboseOption,
316
+ quiet: int = QuietOption,
317
+ ) -> None:
318
+ """Generate records and render assets in one step."""
319
+ logger = _configure_logging(verbose, quiet)
320
+ pipeline = TrendifyPipeline(output_dir=output_directory, n_procs=n_procs)
321
+ total = pipeline.run(
322
+ record_generator=_resolve_record_generator(record_generator),
323
+ data_dirs=_resolve_input_directories(input_directories, ctx),
324
+ )
325
+ logger.info(f"Wrote {total} records; assets under {pipeline.assets_dir}")
326
+
327
+
328
+ @app.command(name="example-data")
329
+ def example_data(
330
+ ctx: typer.Context,
331
+ workdir: Path = ExampleWorkdirOption,
332
+ n_folders: int = ExampleNFoldersOption,
333
+ n_cols: int = ExampleNColsOption,
334
+ verbose: int = VerboseOption,
335
+ quiet: int = QuietOption,
336
+ ) -> None:
337
+ """Generate sample raw-data directories for exercising the trendify pipeline."""
338
+ logger = _configure_logging(verbose, quiet)
339
+ make_example_data(workdir=workdir, n_folders=n_folders, n_cols=n_cols)
340
+ logger.info(f"Wrote {n_folders} sample data folders under {workdir / 'models'}")
341
+
342
+
343
+ def get_local_ip() -> str:
344
+ """Returns the actual local IP address of this machine."""
345
+ import socket
346
+
347
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
348
+ try:
349
+ # Does not actually need to connect to 8.8.8.8 to work
350
+ s.connect(("8.8.8.8", 1))
351
+ ip = s.getsockname()[0]
352
+ except Exception:
353
+ ip = "127.0.0.1"
354
+ finally:
355
+ s.close()
356
+ return ip
357
+
358
+
359
+ @app.command(name="viewer")
360
+ def viewer(
361
+ ctx: typer.Context,
362
+ db_path: Path = DbPathArgument,
363
+ host: str = HostOption,
364
+ port: int = PortOption,
365
+ verbose: int = VerboseOption,
366
+ quiet: int = QuietOption,
367
+ ) -> None:
368
+ """Launch a local web dashboard for browsing a trendify.db's records."""
369
+ _configure_logging(verbose, quiet, logo=False)
370
+ db_path = db_path.resolve()
371
+ if not db_path.exists():
372
+ raise typer.BadParameter(f"No such database file: {db_path}")
373
+
374
+ # Fail fast on a bad/corrupt db before uvicorn even starts, rather than a 500 on first request.
375
+ with RecordStore.open(db_path, readonly=True):
376
+ pass
377
+
378
+ # Deferred import: keeps `trendify.cli` import light for callers of generate/render/run
379
+ # who never touch the dashboard (FastAPI/Jinja2/uvicorn stay unimported until needed).
380
+ from trendify.viewer.app import create_app, make_server
381
+
382
+ connection_info = (
383
+ f"Local: [bold indian_red1 u]http://127.0.0.1:{port}[/bold indian_red1 u]"
384
+ )
385
+ if host == "0.0.0.0":
386
+ connection_info += f"\nMobile: [bold indian_red1 u]http://{get_local_ip()}:{port}[/bold indian_red1 u]"
387
+ else:
388
+ connection_info += "\n\n[dim]Tip: To view on other devices (and to make pages shareable), run with[/dim] [yellow]--host 0.0.0.0[/yellow]"
389
+
390
+ console.print(
391
+ Panel(
392
+ f"""\n{connection_info}\n\n[yellow]Press CTRL+C to stop[/yellow]""",
393
+ border_style="indian_red1",
394
+ expand=False,
395
+ title="Trendify Viewer is Live!",
396
+ )
397
+ )
398
+
399
+ make_server(create_app(db_path), host=host, port=port).run()
400
+
401
+
402
+ if __name__ == "__main__":
403
+ app()
trendify/color.py ADDED
@@ -0,0 +1,260 @@
1
+ """
2
+ `Color`: the Tailwind CSS color palette as a `StrEnum` of hex values, for consistent styling
3
+ defaults across `Pen`/`Marker`/`HistogramStyle` and friends.
4
+ """
5
+
6
+ import logging
7
+ from enum import StrEnum
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ __all__ = ["Color"]
12
+
13
+
14
+ class Color(StrEnum):
15
+ """Contains color aliases and converters to go between the various color formats. The color values are defined by the standard [Tailwind CSS](https://tailwindcss.com/docs/colors) color theme."""
16
+
17
+ BLACK = "#000000"
18
+ WHITE = "#FFFFFF"
19
+ SLATE_50 = "#F8FAFC"
20
+ SLATE_100 = "#F1F5F9"
21
+ SLATE_200 = "#E2E8F0"
22
+ SLATE_300 = "#CBD5E1"
23
+ SLATE_400 = "#94A3B8"
24
+ SLATE_500 = "#64748B"
25
+ SLATE_600 = "#475569"
26
+ SLATE_700 = "#334155"
27
+ SLATE_800 = "#1E293B"
28
+ SLATE_900 = "#0F172A"
29
+ SLATE_950 = "#020617"
30
+ GRAY_50 = "#F9FAFB"
31
+ GRAY_100 = "#F3F4F6"
32
+ GRAY_200 = "#E5E7EB"
33
+ GRAY_300 = "#D1D5DB"
34
+ GRAY_400 = "#9CA3AF"
35
+ GRAY_500 = "#6B7280"
36
+ GRAY_600 = "#4B5563"
37
+ GRAY_700 = "#374151"
38
+ GRAY_800 = "#1F2937"
39
+ GRAY_900 = "#111827"
40
+ GRAY_950 = "#030712"
41
+ ZINC_50 = "#FAFAFA"
42
+ ZINC_100 = "#F4F4F5"
43
+ ZINC_200 = "#E4E4E7"
44
+ ZINC_300 = "#D4D4D8"
45
+ ZINC_400 = "#A1A1AA"
46
+ ZINC_500 = "#71717A"
47
+ ZINC_600 = "#52525B"
48
+ ZINC_700 = "#3F3F46"
49
+ ZINC_800 = "#27272A"
50
+ ZINC_900 = "#18181B"
51
+ ZINC_950 = "#09090B"
52
+ NEUTRAL_50 = "#FAFAFA"
53
+ NEUTRAL_100 = "#F5F5F5"
54
+ NEUTRAL_200 = "#E5E5E5"
55
+ NEUTRAL_300 = "#D4D4D4"
56
+ NEUTRAL_400 = "#A3A3A3"
57
+ NEUTRAL_500 = "#737373"
58
+ NEUTRAL_600 = "#525252"
59
+ NEUTRAL_700 = "#404040"
60
+ NEUTRAL_800 = "#262626"
61
+ NEUTRAL_900 = "#171717"
62
+ NEUTRAL_950 = "#0A0A0A"
63
+ STONE_50 = "#FAFAF9"
64
+ STONE_100 = "#F5F5F4"
65
+ STONE_200 = "#E7E5E4"
66
+ STONE_300 = "#D6D3D1"
67
+ STONE_400 = "#A8A29E"
68
+ STONE_500 = "#78716C"
69
+ STONE_600 = "#57534E"
70
+ STONE_700 = "#44403C"
71
+ STONE_800 = "#292524"
72
+ STONE_900 = "#1C1917"
73
+ STONE_950 = "#0C0A09"
74
+ RED_50 = "#FEF2F2"
75
+ RED_100 = "#FEE2E2"
76
+ RED_200 = "#FECACA"
77
+ RED_300 = "#FCA5A5"
78
+ RED_400 = "#F87171"
79
+ RED_500 = "#EF4444"
80
+ RED_600 = "#DC2626"
81
+ RED_700 = "#B91C1C"
82
+ RED_800 = "#991B1B"
83
+ RED_900 = "#7F1D1D"
84
+ RED_950 = "#450A0A"
85
+ ORANGE_50 = "#FFF7ED"
86
+ ORANGE_100 = "#FFEDD5"
87
+ ORANGE_200 = "#FED7AA"
88
+ ORANGE_300 = "#FDBA74"
89
+ ORANGE_400 = "#FB923C"
90
+ ORANGE_500 = "#F97316"
91
+ ORANGE_600 = "#EA580C"
92
+ ORANGE_700 = "#C2410C"
93
+ ORANGE_800 = "#9A3412"
94
+ ORANGE_900 = "#7C2D12"
95
+ ORANGE_950 = "#431407"
96
+ AMBER_50 = "#FFFBEB"
97
+ AMBER_100 = "#FEF3C7"
98
+ AMBER_200 = "#FDE68A"
99
+ AMBER_300 = "#FCD34D"
100
+ AMBER_400 = "#FBBF24"
101
+ AMBER_500 = "#F59E0B"
102
+ AMBER_600 = "#D97706"
103
+ AMBER_700 = "#B45309"
104
+ AMBER_800 = "#92400E"
105
+ AMBER_900 = "#78350F"
106
+ AMBER_950 = "#451A03"
107
+ YELLOW_50 = "#FEFCE8"
108
+ YELLOW_100 = "#FEF9C3"
109
+ YELLOW_200 = "#FEF08A"
110
+ YELLOW_300 = "#FDE047"
111
+ YELLOW_400 = "#FACC15"
112
+ YELLOW_500 = "#EAB308"
113
+ YELLOW_600 = "#CA8A04"
114
+ YELLOW_700 = "#A16207"
115
+ YELLOW_800 = "#854D0E"
116
+ YELLOW_900 = "#713F12"
117
+ YELLOW_950 = "#422006"
118
+ LIME_50 = "#F7FEE7"
119
+ LIME_100 = "#ECFCCB"
120
+ LIME_200 = "#D9F99D"
121
+ LIME_300 = "#BEF264"
122
+ LIME_400 = "#A3E635"
123
+ LIME_500 = "#84CC16"
124
+ LIME_600 = "#65A30D"
125
+ LIME_700 = "#4D7C0F"
126
+ LIME_800 = "#3F6212"
127
+ LIME_900 = "#365314"
128
+ LIME_950 = "#1A2E05"
129
+ GREEN_50 = "#F0FDF4"
130
+ GREEN_100 = "#DCFCE7"
131
+ GREEN_200 = "#BBF7D0"
132
+ GREEN_300 = "#86EFAC"
133
+ GREEN_400 = "#4ADE80"
134
+ GREEN_500 = "#22C55E"
135
+ GREEN_600 = "#16A34A"
136
+ GREEN_700 = "#15803D"
137
+ GREEN_800 = "#166534"
138
+ GREEN_900 = "#14532D"
139
+ GREEN_950 = "#052E16"
140
+ EMERALD_50 = "#ECFDF5"
141
+ EMERALD_100 = "#D1FAE5"
142
+ EMERALD_200 = "#A7F3D0"
143
+ EMERALD_300 = "#6EE7B7"
144
+ EMERALD_400 = "#34D399"
145
+ EMERALD_500 = "#10B981"
146
+ EMERALD_600 = "#059669"
147
+ EMERALD_700 = "#047857"
148
+ EMERALD_800 = "#065F46"
149
+ EMERALD_900 = "#064E3B"
150
+ EMERALD_950 = "#022C22"
151
+ TEAL_50 = "#F0FDFA"
152
+ TEAL_100 = "#CCFBF1"
153
+ TEAL_200 = "#99F6E4"
154
+ TEAL_300 = "#5EEAD4"
155
+ TEAL_400 = "#2DD4BF"
156
+ TEAL_500 = "#14B8A6"
157
+ TEAL_600 = "#0D9488"
158
+ TEAL_700 = "#0F766E"
159
+ TEAL_800 = "#115E59"
160
+ TEAL_900 = "#134E4A"
161
+ TEAL_950 = "#042F2E"
162
+ CYAN_50 = "#ECFEFF"
163
+ CYAN_100 = "#CFFAFE"
164
+ CYAN_200 = "#A5F3FC"
165
+ CYAN_300 = "#67E8F9"
166
+ CYAN_400 = "#22D3EE"
167
+ CYAN_500 = "#06B6D4"
168
+ CYAN_600 = "#0891B2"
169
+ CYAN_700 = "#0E7490"
170
+ CYAN_800 = "#155E75"
171
+ CYAN_900 = "#164E63"
172
+ CYAN_950 = "#083344"
173
+ SKY_50 = "#F0F9FF"
174
+ SKY_100 = "#E0F2FE"
175
+ SKY_200 = "#BAE6FD"
176
+ SKY_300 = "#7DD3FC"
177
+ SKY_400 = "#38BDF8"
178
+ SKY_500 = "#0EA5E9"
179
+ SKY_600 = "#0284C7"
180
+ SKY_700 = "#0369A1"
181
+ SKY_800 = "#075985"
182
+ SKY_900 = "#0C4A6E"
183
+ SKY_950 = "#082F49"
184
+ BLUE_50 = "#EFF6FF"
185
+ BLUE_100 = "#DBEAFE"
186
+ BLUE_200 = "#BFDBFE"
187
+ BLUE_300 = "#93C5FD"
188
+ BLUE_400 = "#60A5FA"
189
+ BLUE_500 = "#3B82F6"
190
+ BLUE_600 = "#2563EB"
191
+ BLUE_700 = "#1D4ED8"
192
+ BLUE_800 = "#1E40AF"
193
+ BLUE_900 = "#1E3A8A"
194
+ BLUE_950 = "#172554"
195
+ INDIGO_50 = "#EEF2FF"
196
+ INDIGO_100 = "#E0E7FF"
197
+ INDIGO_200 = "#C7D2FE"
198
+ INDIGO_300 = "#A5B4FC"
199
+ INDIGO_400 = "#818CF8"
200
+ INDIGO_500 = "#6366F1"
201
+ INDIGO_600 = "#4F46E5"
202
+ INDIGO_700 = "#4338CA"
203
+ INDIGO_800 = "#3730A3"
204
+ INDIGO_900 = "#312E81"
205
+ INDIGO_950 = "#1E1B4B"
206
+ VIOLET_50 = "#F5F3FF"
207
+ VIOLET_100 = "#EDE9FE"
208
+ VIOLET_200 = "#DDD6FE"
209
+ VIOLET_300 = "#C4B5FD"
210
+ VIOLET_400 = "#A78BFA"
211
+ VIOLET_500 = "#8B5CF6"
212
+ VIOLET_600 = "#7C3AED"
213
+ VIOLET_700 = "#6D28D9"
214
+ VIOLET_800 = "#5B21B6"
215
+ VIOLET_900 = "#4C1D95"
216
+ VIOLET_950 = "#2E1065"
217
+ PURPLE_50 = "#FAF5FF"
218
+ PURPLE_100 = "#F3E8FF"
219
+ PURPLE_200 = "#E9D5FF"
220
+ PURPLE_300 = "#D8B4FE"
221
+ PURPLE_400 = "#C084FC"
222
+ PURPLE_500 = "#A855F7"
223
+ PURPLE_600 = "#9333EA"
224
+ PURPLE_700 = "#7E22CE"
225
+ PURPLE_800 = "#6B21A8"
226
+ PURPLE_900 = "#581C87"
227
+ PURPLE_950 = "#3B0764"
228
+ FUCHSIA_50 = "#FDF4FF"
229
+ FUCHSIA_100 = "#FAE8FF"
230
+ FUCHSIA_200 = "#F5D0FE"
231
+ FUCHSIA_300 = "#F0ABFC"
232
+ FUCHSIA_400 = "#E879F9"
233
+ FUCHSIA_500 = "#D946EF"
234
+ FUCHSIA_600 = "#C026D3"
235
+ FUCHSIA_700 = "#A21CAF"
236
+ FUCHSIA_800 = "#86198F"
237
+ FUCHSIA_900 = "#701A75"
238
+ FUCHSIA_950 = "#4A044E"
239
+ PINK_50 = "#FDF2F8"
240
+ PINK_100 = "#FCE7F3"
241
+ PINK_200 = "#FBCFE8"
242
+ PINK_300 = "#F9A8D4"
243
+ PINK_400 = "#F472B6"
244
+ PINK_500 = "#EC4899"
245
+ PINK_600 = "#DB2777"
246
+ PINK_700 = "#BE185D"
247
+ PINK_800 = "#9D174D"
248
+ PINK_900 = "#831843"
249
+ PINK_950 = "#500724"
250
+ ROSE_50 = "#FFF1F2"
251
+ ROSE_100 = "#FFE4E6"
252
+ ROSE_200 = "#FECDD3"
253
+ ROSE_300 = "#FDA4AF"
254
+ ROSE_400 = "#FB7185"
255
+ ROSE_500 = "#F43F5E"
256
+ ROSE_600 = "#E11D48"
257
+ ROSE_700 = "#BE123C"
258
+ ROSE_800 = "#9F1239"
259
+ ROSE_900 = "#881337"
260
+ ROSE_950 = "#4C0519"