pretext-sim 0.3.2__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.
pretext/render.py ADDED
@@ -0,0 +1,247 @@
1
+ """Rich rendering and plain-text export of simulations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterable
7
+
8
+ from rich.console import Console, Group, RenderableType
9
+ from rich.markdown import Markdown
10
+ from rich.panel import Panel
11
+ from rich.rule import Rule
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from .models import Profile, Settings, Simulation
16
+
17
+ DISCLAIMER = (
18
+ "Fictional exercise. Domains, names, and systems are invented. "
19
+ "Use against authorized targets only."
20
+ )
21
+ DIFFICULTY_LABELS = {
22
+ 1: "broad, high volume",
23
+ 2: "lightly targeted",
24
+ 3: "targeted",
25
+ 4: "researched, multi-step",
26
+ 5: "bespoke, multi-day",
27
+ }
28
+
29
+
30
+ def console(*, no_color: bool = False, width: int | None = None, stderr: bool = False) -> Console:
31
+ return Console(
32
+ no_color=no_color,
33
+ width=width,
34
+ highlight=False,
35
+ soft_wrap=False,
36
+ stderr=stderr,
37
+ )
38
+
39
+
40
+ def _difficulty(value: int) -> Text:
41
+ filled = "●" * value + "○" * (5 - value)
42
+ label = DIFFICULTY_LABELS.get(value, "targeted")
43
+ return Text(f"{filled} {value}/5 · {label}", style="magenta")
44
+
45
+
46
+ SECTIONS = ("pretext", "opening", "escalation", "red_flags")
47
+
48
+
49
+ def render_simulation(
50
+ simulation: Simulation,
51
+ profile: Profile,
52
+ out: Console,
53
+ *,
54
+ sections: tuple[str, ...] = SECTIONS,
55
+ ) -> None:
56
+ header = Table.grid(padding=(0, 2))
57
+ header.add_column(style="bold cyan", justify="right")
58
+ header.add_column()
59
+ header.add_row("Persona", profile.name)
60
+ header.add_row("Role", f"{profile.role} · {profile.company}")
61
+ header.add_row("Style", profile.communication_style)
62
+ header.add_row("Difficulty", _difficulty(simulation.difficulty))
63
+ if simulation.objective:
64
+ header.add_row("Objective", simulation.objective)
65
+
66
+ out.print(
67
+ Panel(
68
+ header,
69
+ title="[bold]Simulation[/bold]",
70
+ subtitle=f"[dim]{profile.name}[/dim]",
71
+ border_style="cyan",
72
+ expand=False,
73
+ )
74
+ )
75
+
76
+ if "pretext" in sections:
77
+ out.print(Rule("[bold]Pretext narrative[/bold]", style="cyan"))
78
+ out.print(Markdown(simulation.pretext))
79
+
80
+ if "opening" not in sections:
81
+ return _render_tail(simulation, out, sections)
82
+
83
+ message_body: list[RenderableType] = []
84
+ if simulation.subject_line:
85
+ message_body.append(Text(f"Subject: {simulation.subject_line}", style="bold"))
86
+ message_body.append(Text(""))
87
+ message_body.append(Markdown(simulation.opening_message))
88
+ out.print(
89
+ Panel(
90
+ Group(*message_body),
91
+ title="[bold]Opening message[/bold]",
92
+ border_style="green",
93
+ expand=False,
94
+ )
95
+ )
96
+
97
+ _render_tail(simulation, out, sections)
98
+
99
+
100
+ def _render_tail(simulation: Simulation, out: Console, sections: tuple[str, ...]) -> None:
101
+ if "escalation" in sections:
102
+ _render_escalation(simulation, out)
103
+ if "red_flags" in sections:
104
+ _render_red_flags(simulation, out)
105
+ out.print(Rule(style="dim"))
106
+ out.print(Text(DISCLAIMER, style="dim italic"))
107
+
108
+
109
+ def _render_escalation(simulation: Simulation, out: Console) -> None:
110
+ out.print(Rule("[bold]Escalation path[/bold]", style="cyan"))
111
+ steps = Table(show_header=True, header_style="bold cyan", expand=True, box=None)
112
+ steps.add_column("#", width=3, justify="right")
113
+ steps.add_column("Channel", width=11)
114
+ steps.add_column("Action", ratio=3)
115
+ steps.add_column("Goal", ratio=2)
116
+ for step in simulation.escalation_path:
117
+ steps.add_row(
118
+ str(step.stage),
119
+ Text(step.channel.value, style="yellow"),
120
+ Text(step.action),
121
+ Text(step.goal, style="dim"),
122
+ )
123
+ if step.artifact:
124
+ steps.add_row("", "", Text(f"↳ {step.artifact}", style="dim italic"), "")
125
+ out.print(steps)
126
+
127
+
128
+ def _render_red_flags(simulation: Simulation, out: Console) -> None:
129
+ out.print(Rule("[bold]Red flags for defenders[/bold]", style="cyan"))
130
+ for index, flag in enumerate(simulation.red_flags, start=1):
131
+ out.print(
132
+ Panel(
133
+ Group(
134
+ Markdown(flag.why_it_matters),
135
+ Text(""),
136
+ Text(f"Defender action: {flag.defender_action}", style="bold green"),
137
+ ),
138
+ title=f"[bold]{index}. {flag.title}[/bold]",
139
+ border_style="red",
140
+ expand=False,
141
+ )
142
+ )
143
+
144
+
145
+ def render_settings(settings: Settings, out: Console) -> None:
146
+ table = Table(show_header=False, box=None, padding=(0, 2))
147
+ table.add_column(style="bold cyan", justify="right")
148
+ table.add_column()
149
+ table.add_row("Endpoint", settings.endpoint)
150
+ table.add_row("Model", settings.model)
151
+ table.add_row("Key", "set" if settings.api_key else "[red]missing[/red]")
152
+ table.add_row("Temperature", f"{settings.temperature:g}")
153
+ table.add_row("Max tokens", str(settings.max_tokens))
154
+ table.add_row("Timeout", f"{settings.timeout:g}s")
155
+ table.add_row("Retries", str(settings.retries))
156
+ if settings.seed is not None:
157
+ table.add_row("Seed", str(settings.seed))
158
+ out.print(
159
+ Panel(
160
+ table,
161
+ title="[bold]Resolved configuration[/bold]",
162
+ border_style="cyan",
163
+ expand=False,
164
+ )
165
+ )
166
+
167
+
168
+ def render_profile(profile: Profile, out: Console) -> None:
169
+ table = Table(show_header=False, box=None, padding=(0, 2))
170
+ table.add_column(style="bold cyan", justify="right")
171
+ table.add_column()
172
+ for label, value in (
173
+ ("Name", profile.name),
174
+ ("Role", profile.role),
175
+ ("Company", profile.company),
176
+ ("Style", profile.communication_style),
177
+ ("Department", profile.department),
178
+ ("Tenure", profile.tenure),
179
+ ("Reports to", profile.reports_to),
180
+ ("Systems", ", ".join(profile.systems) or None),
181
+ ("Notes", profile.notes),
182
+ ):
183
+ if value:
184
+ table.add_row(label, value)
185
+ out.print(Panel(table, title="[bold]Profile OK[/bold]", border_style="green", expand=False))
186
+
187
+
188
+ def to_markdown(
189
+ simulation: Simulation,
190
+ profile: Profile,
191
+ *,
192
+ sections: tuple[str, ...] = SECTIONS,
193
+ ) -> str:
194
+ lines = [
195
+ f"# Social-engineering simulation: {profile.name}",
196
+ "",
197
+ f"- **Role:** {profile.role} at {profile.company}",
198
+ f"- **Communication style:** {profile.communication_style}",
199
+ f"- **Difficulty:** {simulation.difficulty}/5",
200
+ ]
201
+ if simulation.objective:
202
+ lines.append(f"- **Objective:** {simulation.objective}")
203
+ if "pretext" in sections:
204
+ lines += ["", "## Pretext narrative", "", simulation.pretext]
205
+ if "opening" in sections:
206
+ lines += ["", "## Opening message", ""]
207
+ if simulation.subject_line:
208
+ lines += [f"**Subject:** {simulation.subject_line}", ""]
209
+ lines += [simulation.opening_message]
210
+ if "escalation" in sections:
211
+ lines += ["", "## Escalation path", ""]
212
+ for step in simulation.escalation_path:
213
+ lines.append(
214
+ f"{step.stage}. **{step.channel.value}** · {step.action} _(goal: {step.goal})_"
215
+ )
216
+ if step.artifact:
217
+ lines.append(f" - Artifact: {step.artifact}")
218
+ if "red_flags" in sections:
219
+ lines += ["", "## Red flags for defenders", ""]
220
+ for index, flag in enumerate(simulation.red_flags, start=1):
221
+ lines += [
222
+ f"{index}. **{flag.title}**",
223
+ f" - Why it matters: {flag.why_it_matters}",
224
+ f" - Defender action: {flag.defender_action}",
225
+ ]
226
+ lines += ["", "---", "", f"_{DISCLAIMER}_", ""]
227
+ return "\n".join(lines)
228
+
229
+
230
+ def to_json(
231
+ simulation: Simulation,
232
+ profile: Profile,
233
+ *,
234
+ sections: tuple[str, ...] = SECTIONS,
235
+ ) -> str:
236
+ payload: dict[str, object] = {
237
+ "profile": profile.model_dump(mode="json"),
238
+ "simulation": simulation.model_dump(mode="json"),
239
+ "sections": list(sections),
240
+ "disclaimer": DISCLAIMER,
241
+ }
242
+ return json.dumps(payload, indent=2, ensure_ascii=False)
243
+
244
+
245
+ def render_warnings(warnings: Iterable[str], out: Console) -> None:
246
+ for warning in warnings:
247
+ out.print(Text(f"! {warning}", style="yellow"))
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.5
2
+ Name: pretext-sim
3
+ Version: 0.3.2
4
+ Summary: Generate labeled social-engineering simulations for security training
5
+ Author: pretext contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: cli,phishing,red-team,security,security-awareness,training
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2.6
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich>=13.7
24
+ Requires-Dist: typer>=0.12
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.10; extra == 'dev'
27
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ <div align="center">
34
+
35
+ # pretext
36
+
37
+ **Turn a one-page persona into a runnable social-engineering exercise.**
38
+
39
+ Narrative, opening message, escalation path, and the red flags defenders should
40
+ have caught, generated, validated, then rendered where you need it.
41
+
42
+ [![ci](https://github.com/sjdsjadjajdjsa/pretext/actions/workflows/ci.yml/badge.svg)](https://github.com/sjdsjadjajdjsa/pretext/actions/workflows/ci.yml)
43
+ ![python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)
44
+ ![license](https://img.shields.io/badge/license-MIT-blue.svg)
45
+
46
+ <img src="docs/demo.png" alt="pretext rendering a simulation in the terminal" width="820">
47
+
48
+ </div>
49
+
50
+ > **Authorized training only.** Everything produced is a fictional exercise.
51
+ > Domains, names, and systems are invented unless you say otherwise. pretext never
52
+ > sends anything to a target; it writes the material and the analysis. Run an
53
+ > exercise against your own people only after your security team agrees to it, and
54
+ > check the lures before sending.
55
+
56
+ ## Why
57
+
58
+ Security-awareness decks are generic ("you receive an email with a typo"), so staff
59
+ learn to spot a template rather than a technique. A useful exercise is grounded in
60
+ one role at one company: what that person controls, who they trust, and what an
61
+ operator would do next.
62
+
63
+ Doing that by hand costs an afternoon per persona. `pretext` takes a YAML profile
64
+ and produces the exercise in one call, defender's side included, so training value
65
+ does not depend on whoever wrote the scenario remembering to add it.
66
+
67
+ ## What you get
68
+
69
+ | Section | Content |
70
+ | ------- | ------- |
71
+ | Pretext narrative | The operation, attacker-side: harvested detail, forged artifact, why contact lands today |
72
+ | Opening message | Usable verbatim, in the sender's voice, carrying the sending address and one lure |
73
+ | Escalation path | 3-6 stages across channels, each handing over a new prop or introducing a new person |
74
+ | Red flags | Observable tells tied to this exercise, each with a defender action, including one only clear in hindsight |
75
+
76
+ `docs/running-an-exercise.md` covers authorization, delivery, scoring and the
77
+ debrief, which is the half the tool cannot do.
78
+
79
+ Every output carries a disclaimer. [`sample-output.md`](sample-output.md) is a
80
+ complete Markdown export; [`docs/demo.png`](docs/demo.png) is the terminal view.
81
+
82
+ ## Install
83
+
84
+ ```sh
85
+ uvx pretext-sim generate persona.yaml # no install
86
+ pipx install pretext-sim # or a permanent command
87
+ ```
88
+
89
+ Python 3.10+. The distribution is `pretext-sim`; the command is `pretext`. For the
90
+ source tree: `uv sync --extra dev` then `uv run pretext`.
91
+
92
+ ## Quickstart
93
+
94
+ ```sh
95
+ pretext example -o persona.yaml # fictional profile to edit
96
+ pretext validate persona.yaml # schema check, no API call
97
+ pretext generate persona.yaml --dry-run # resolved config + exact prompt, no call
98
+ pretext generate persona.yaml -f markdown -o exercise.md
99
+ ```
100
+
101
+ Point it at any OpenAI-compatible chat completions endpoint with a flag, env var, or
102
+ `.env`:
103
+
104
+ ```sh
105
+ export PRETEXT_API_KEY=sk-...
106
+ export PRETEXT_BASE_URL=http://localhost:11434/v1 # local proxy, if you prefer
107
+ export PRETEXT_MODEL=gpt-oss:20b
108
+ ```
109
+
110
+ Or name a known host instead of the URL: `--provider openai|openrouter|venice|ollama|lmstudio`
111
+ (`PRETEXT_PROVIDER`), which fills the base URL. An explicit `--base-url` wins. Local
112
+ providers need no key.
113
+
114
+ `pretext config` shows what resolves, with the key masked.
115
+
116
+ ## Profile
117
+
118
+ Only four fields are required (`name`, `role`, `company`, `communication_style`).
119
+ The rest sharpen the result.
120
+
121
+ ```yaml
122
+ name: Dana Iyer
123
+ role: Senior Payroll Analyst
124
+ company: Northwind Logistics
125
+ communication_style: Brisk and formal, mixes payroll shorthand with chatty asides
126
+ department: People Operations
127
+ tenure: 3 months
128
+ reports_to: Marta Kwan
129
+ systems: [Workday, the payroll mailbox]
130
+ notes: New enough to the team to be unsure of process.
131
+ ```
132
+
133
+ Optional: `department`, `tenure`, `reports_to`, `systems`, `notes`. Unknown keys are
134
+ rejected so typos surface early. Fixture personas live in
135
+ [`tests/fixtures/profiles`](tests/fixtures/profiles).
136
+
137
+ ## Command reference
138
+
139
+ | Command | Purpose |
140
+ | ------- | ------- |
141
+ | `generate PROFILE` | Produce one exercise |
142
+ | `validate PROFILE` | Validate without calling the API |
143
+ | `example` | Print the bundled fictional profile |
144
+ | `batch DIR` | One exercise per `*.yaml` in DIR (files starting `_` are skipped) |
145
+ | `doctor` | Probe the endpoint: config, TLS, reachability, model, JSON mode |
146
+ | `schema` | Print the JSON Schema model output must satisfy (`--all` adds the profile) |
147
+ | `config` | Show resolved settings, key masked |
148
+
149
+ Flags on `generate`:
150
+
151
+ | Flag | Effect |
152
+ | ---- | ------ |
153
+ | `--channel` | Force the opening channel (email, phone, sms, chat, in_person, linkedin) |
154
+ | `--extra` | Extra direction for the model |
155
+ | `--format` | `rich` (default), `markdown`, `json` |
156
+ | `--out` | Write to a file; plain text unless a format is chosen |
157
+ | `--sections` | Render only some of `pretext,opening,escalation,red_flags` |
158
+ | `--from-file` | Replay a saved response; no API call, no key |
159
+ | `--dry-run` | Print resolved config and prompt, send nothing; `--out` writes the prompt |
160
+ | `--usage` | Token counts on stderr |
161
+ | `--base-url`, `--model`, `--api-key` | Override endpoint settings |
162
+ | `--temperature`, `--max-tokens`, `--timeout`, `--retries`, `--seed` | Sampling and transport |
163
+ | `--no-color` | Plain terminal output |
164
+
165
+ Exit codes: `0` success, `1` bad profile/config/API/schema failure, `2` usage error,
166
+ `130` interrupted.
167
+
168
+ `--from-file` accepts a `--format json` export, a bare simulation object, or a raw
169
+ chat completion body, so formatting work stays free and offline.
170
+
171
+ ## Diagnose first
172
+
173
+ ```sh
174
+ pretext doctor # config, TLS, /models, JSON mode
175
+ pretext doctor --full # also runs one real simulation through the schema
176
+ ```
177
+
178
+ Exits `1` on any failure and prints the provider's own error, so a wrong model name
179
+ or a blocked key shows up before a batch run wastes retries.
180
+
181
+ ## Batch runs
182
+
183
+ For a whole team, point `batch` at a directory and it writes one file per profile,
184
+ continuing past failures:
185
+
186
+ ```sh
187
+ pretext batch profiles/ --out-dir packet/ --format markdown
188
+ ```
189
+
190
+ Progress and a `wrote 7/9` summary go to stderr, the failure reasons are listed, and
191
+ the exit code is `1` if anything failed. Endpoint flags are the same as `generate`;
192
+ `--sections`, `--format` and `--usage` apply to the run as a whole.
193
+
194
+ ## Configuration
195
+
196
+ First hit wins:
197
+
198
+ 1. Flags (`--base-url`, `--model`, `--api-key`, `--provider`)
199
+ 2. Environment: the same names under `PRETEXT_` (`PRETEXT_API_KEY`, `PRETEXT_MODEL`, ...)
200
+ 3. `.env` in the working directory
201
+ 4. `OPENAI_API_KEY` for the key, then `https://api.openai.com/v1` and `gpt-4o-mini`
202
+
203
+ The key is never printed and never written to output.
204
+
205
+ ## How it works
206
+
207
+ ```
208
+ profile.yaml ─> Profile ─> prompt ─> POST /chat/completions ─> extract JSON
209
+ │
210
+ rich | markdown | json <─ render <─ validate <────────────────┘
211
+ ```
212
+
213
+ Model output is untrusted. It is parsed out of fences or prose, validated against a
214
+ strict pydantic schema, and only then rendered. Escalation stages must run 1..n with
215
+ no gaps, `red_flags` needs at least two entries, and unknown keys are rejected, so a
216
+ loose model fails loudly instead of producing a half-filled exercise.
217
+
218
+ Diagnostics go to stderr, keeping stdout pipeable: `pretext generate p.yaml -f json | jq`.
219
+
220
+ ## Releasing
221
+
222
+ Tagging `vX.Y.Z` runs `release.yml`: it checks the tag against `__version__`, gates
223
+ on lint, format, types and tests, builds, then publishes a release whose notes come
224
+ from `CHANGELOG.md`. Version bumps and changelog sections land before the tag.
225
+
226
+ ## Development
227
+
228
+ ```sh
229
+ uv run pytest --cov=pretext # offline: MockTransport and a stubbed client
230
+ uv run ruff check src tests
231
+ uv run mypy
232
+ ```
233
+
234
+ The suite needs no key. `conftest.py` chdirs each test into a temp directory so your
235
+ own `.env` cannot leak in, and pins the terminal width so Rich reflow stays stable.
236
+
237
+ `scripts/capture_corpus.py` refreshes `tests/fixtures/corpus`, which
238
+ `tests/test_corpus.py` checks for structural promises.
239
+
240
+ `AGENTS.md` documents the layout, the hardening rules, and the traps for agent
241
+ contributors. `CONTRIBUTING.md` covers process; security reports follow `SECURITY.md`.
242
+
243
+ ## Status
244
+
245
+ Pre-1.0: the CLI surface and JSON schema can still move between minor versions;
246
+ `CHANGELOG.md` records what changed.
247
+
248
+ Output has been checked against gpt-4o-mini, gemini-2.5-flash, claude-haiku-4.5 and
249
+ llama-3.3-70b through OpenRouter, plus venice-uncensored-1-2 and
250
+ qwen3-235b-a22b-instruct on Venice. All six held the JSON contract and used distinct
251
+ channels. Weak models will still need prompt tuning, and the suite covers the
252
+ pipeline rather than any one model's prose.
253
+
254
+ Not planned: delivery to targets, click tracking, an SMTP client. pretext writes the
255
+ exercise; sending stays your job.
256
+
257
+ ## License
258
+
259
+ MIT. See [`LICENSE`](LICENSE).
@@ -0,0 +1,12 @@
1
+ pretext/__init__.py,sha256=b2SsiOk-VyOt7PxcuorCfQ7JSP5_geKBwAbSlWbyQfk,590
2
+ pretext/cli.py,sha256=wp07Fg_DkeFIHnNfkbtfHbF0NWQAKGYH0_enK9WFZZg,20199
3
+ pretext/client.py,sha256=sNFeBZLUzKg5AcVrwGscBE6lwS6e73e1Kbb12164Yrw,8143
4
+ pretext/models.py,sha256=tmjPrqbswtxoybeOZQ9mI1W90gl9SdYA6QeuPvTu-5s,10474
5
+ pretext/prompt.py,sha256=yQ49GF3F9jD_-xW_FiHCfW04Aq6bWNnVcHxVPztdb30,5559
6
+ pretext/render.py,sha256=IYT6h6mO4TWKYJBHAOMjWAS1IqcFH6upHHZqoHL0730,8302
7
+ pretext/data/example-profile.yaml,sha256=4Lspy5Wh0NH4XMUAo05Pd0RHbBWPxfVoVY5svqMd4Ac,419
8
+ pretext_sim-0.3.2.dist-info/METADATA,sha256=uWhfeJOP6S_dRou4SVoy22OfdvVpJDbzWZV6yhOFbKE,10195
9
+ pretext_sim-0.3.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ pretext_sim-0.3.2.dist-info/entry_points.txt,sha256=hbPonwsPJ7IZoLDhG_VEcLa2kvWNe7ZYpE9pjCvz4L8,44
11
+ pretext_sim-0.3.2.dist-info/licenses/LICENSE,sha256=ax7hQ3Ou5brcygpbq86G4ndIiwolij1Fhs1RHVBdfZ0,1077
12
+ pretext_sim-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pretext = pretext.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pretext contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.