merakiops 0.1.0__tar.gz
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.
- merakiops-0.1.0/.github/workflows/python-publish.yml +71 -0
- merakiops-0.1.0/.gitignore +60 -0
- merakiops-0.1.0/CLAUDE.md +261 -0
- merakiops-0.1.0/LICENSE.txt +18 -0
- merakiops-0.1.0/PKG-INFO +393 -0
- merakiops-0.1.0/README.md +372 -0
- merakiops-0.1.0/REVIEW.md +103 -0
- merakiops-0.1.0/docs/batch-limits.md +102 -0
- merakiops-0.1.0/docs/usage.md +295 -0
- merakiops-0.1.0/pyproject.toml +64 -0
- merakiops-0.1.0/src/merakiops/__about__.py +4 -0
- merakiops-0.1.0/src/merakiops/__init__.py +9 -0
- merakiops-0.1.0/src/merakiops/action.py +182 -0
- merakiops-0.1.0/src/merakiops/action_batch.py +1061 -0
- merakiops-0.1.0/src/merakiops/result.py +78 -0
- merakiops-0.1.0/tests/__init__.py +3 -0
- merakiops-0.1.0/tests/test_action.py +290 -0
- merakiops-0.1.0/tests/test_action_batch.py +612 -0
- merakiops-0.1.0/tests/test_verify.py +631 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
tags:
|
|
14
|
+
- v*
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
release-build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.x"
|
|
29
|
+
|
|
30
|
+
- name: Build release distributions
|
|
31
|
+
run: |
|
|
32
|
+
# NOTE: put your own distribution build steps here.
|
|
33
|
+
python -m pip install build
|
|
34
|
+
python -m build
|
|
35
|
+
|
|
36
|
+
- name: Upload distributions
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: release-dists
|
|
40
|
+
path: dist/
|
|
41
|
+
|
|
42
|
+
pypi-publish:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
needs:
|
|
45
|
+
- release-build
|
|
46
|
+
permissions:
|
|
47
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
48
|
+
id-token: write
|
|
49
|
+
|
|
50
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
51
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
52
|
+
environment:
|
|
53
|
+
name: pypi
|
|
54
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
55
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
56
|
+
#
|
|
57
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
58
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
59
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Retrieve release distributions
|
|
63
|
+
uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: release-dists
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish release distributions to PyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
wheels/
|
|
12
|
+
.eggs/
|
|
13
|
+
pip-wheel-metadata/
|
|
14
|
+
share/python-wheels/
|
|
15
|
+
MANIFEST
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
ENV/
|
|
22
|
+
|
|
23
|
+
# Distribution / packaging
|
|
24
|
+
.Python
|
|
25
|
+
|
|
26
|
+
# Testing
|
|
27
|
+
.tox/
|
|
28
|
+
.nox/
|
|
29
|
+
.coverage
|
|
30
|
+
.coverage.*
|
|
31
|
+
.cache
|
|
32
|
+
nosetests.xml
|
|
33
|
+
coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
*.py,cover
|
|
36
|
+
htmlcov/
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
pytestdebug.log
|
|
39
|
+
|
|
40
|
+
# Type checking
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.dmypy.json
|
|
43
|
+
dmypy.json
|
|
44
|
+
.pytype/
|
|
45
|
+
|
|
46
|
+
# Hatch
|
|
47
|
+
.hatch/
|
|
48
|
+
|
|
49
|
+
# IDE / editors
|
|
50
|
+
.idea/
|
|
51
|
+
.vscode/
|
|
52
|
+
*.swp
|
|
53
|
+
*.swo
|
|
54
|
+
*~
|
|
55
|
+
.DS_Store
|
|
56
|
+
|
|
57
|
+
# Secrets / local config
|
|
58
|
+
.env
|
|
59
|
+
.env.*
|
|
60
|
+
*.toml.local
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# CLAUDE.md — merakiops
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
You are assisting in building **merakiops**: a Python library for creating, submitting, and verifying Meraki API action batches.
|
|
8
|
+
|
|
9
|
+
merakiops is built on top of **merakisync** and works with its typed model objects (`MerakiObj` subclasses). A merakisync object — a `Switchport`, `Vlan`, `Network`, etc. — is the starting point for every Action.
|
|
10
|
+
|
|
11
|
+
This library has exactly one purpose: **make it safe and reliable to batch configuration changes across thousands of Meraki networks.**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Developer context
|
|
16
|
+
|
|
17
|
+
The developer is a network engineer managing hundreds of Meraki networks. They:
|
|
18
|
+
|
|
19
|
+
- Value reliability and correctness above all else.
|
|
20
|
+
- Will hand this library to junior engineers and network engineers with minimal Python experience.
|
|
21
|
+
- Expect every function, parameter, and error message to be self-explanatory without reading source code.
|
|
22
|
+
- Want to be able to push changes to thousands of networks and verify the results programmatically.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Guiding philosophy
|
|
27
|
+
|
|
28
|
+
### 1. UNIX philosophy — do one thing well
|
|
29
|
+
|
|
30
|
+
This project handles exactly one concern: create, send, and verify Meraki action batches. Do not add scheduling, retry loops, reporting, alerting, or workflow orchestration. Those belong in the calling application.
|
|
31
|
+
|
|
32
|
+
### 2. Readable over clever
|
|
33
|
+
|
|
34
|
+
This library will be used by people with minimal Python experience. Prioritize clarity at every level:
|
|
35
|
+
|
|
36
|
+
- Simple > abstraction
|
|
37
|
+
- Explicit > implicit
|
|
38
|
+
- Named keyword arguments over positional where ambiguity exists
|
|
39
|
+
- Error messages that explain what to do, not just what went wrong
|
|
40
|
+
- No magic — no `__init_subclass__`, dynamic class factories, or metaprogramming
|
|
41
|
+
|
|
42
|
+
### 3. Production mindset
|
|
43
|
+
|
|
44
|
+
Assume thousands of networks. Failures must be:
|
|
45
|
+
- Logged before they propagate (log and re-raise; never swallow silently)
|
|
46
|
+
- Distinguishable — which batch? which action? which resource?
|
|
47
|
+
- Debuggable without a debugger attached
|
|
48
|
+
|
|
49
|
+
### 4. Safety by default
|
|
50
|
+
|
|
51
|
+
Action batches modify live production network infrastructure. Every default should minimize risk:
|
|
52
|
+
|
|
53
|
+
- `confirmed=False` by default — batches do not execute until `confirm()` is called
|
|
54
|
+
- `synchronous=False` by default
|
|
55
|
+
- No silent retries
|
|
56
|
+
- Explicit errors when a batch has already been submitted
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Project structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/merakiops/
|
|
64
|
+
├── __init__.py Public re-exports: Action, ActionBatch, Mismatch, VerifyResult.
|
|
65
|
+
├── __about__.py Version string only.
|
|
66
|
+
├── action.py Action class. One action = one API call within a batch.
|
|
67
|
+
├── action_batch.py ActionBatch class. Submission, confirmation, status, verification.
|
|
68
|
+
└── result.py VerifyResult and Mismatch dataclasses.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Data flow
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
MerakiObj (from merakisync)
|
|
77
|
+
│
|
|
78
|
+
└─► Action.update(obj) # changed fields only, single-resource path
|
|
79
|
+
Action.create(obj) # all fields, collection path
|
|
80
|
+
Action.destroy(obj) # no body, single-resource path
|
|
81
|
+
│
|
|
82
|
+
└─► ActionBatch.from_actions(org_id, actions)
|
|
83
|
+
│
|
|
84
|
+
└─► batch.create() # POST /organizations/{id}/actionBatches
|
|
85
|
+
batch.confirm() # PUT /organizations/{id}/actionBatches/{id}
|
|
86
|
+
batch.status() # GET /organizations/{id}/actionBatches/{id}
|
|
87
|
+
batch.verify() # model.get(source="meraki") + field comparison
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
No raw dicts passed to callers. No API calls outside the ActionBatch methods.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Action
|
|
95
|
+
|
|
96
|
+
### Rules
|
|
97
|
+
|
|
98
|
+
- Always created from a factory classmethod: `Action.update()`, `Action.create()`, `Action.destroy()`
|
|
99
|
+
- `frozen=True` — an Action must not change after creation
|
|
100
|
+
- `source_obj` is always stored when using factory classmethods — required for `verify()`
|
|
101
|
+
- `body` is `None` for destroy operations; `to_meraki_action()` omits the key entirely
|
|
102
|
+
|
|
103
|
+
### Valid operations
|
|
104
|
+
|
|
105
|
+
| Operation | When to use | Resource path | Body |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `update` | Modify fields on an existing resource | Single-resource path | Changed fields only (camelCase) |
|
|
108
|
+
| `create` | Create a new resource | Collection path | All fields (camelCase) |
|
|
109
|
+
| `destroy` | Delete a resource | Single-resource path | None (omitted) |
|
|
110
|
+
|
|
111
|
+
### create() and resource paths
|
|
112
|
+
|
|
113
|
+
`Action.create()` derives the collection path by stripping the last segment from `obj.resource_path`. This works for: `Network`, `Device`, `Switchport`, `Vlan`, `Ssid`, `Organization`.
|
|
114
|
+
|
|
115
|
+
`L3FirewallRule` and `DhcpServerPolicy` do not support create — use `Action.update()` for both. Their `resource_path` is already the collection/single endpoint used for updates.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## ActionBatch
|
|
120
|
+
|
|
121
|
+
### Rules
|
|
122
|
+
|
|
123
|
+
- Always created via `ActionBatch.from_actions()`, never instantiated directly
|
|
124
|
+
- Splits actions automatically: 100 actions per batch (async) or 20 (synchronous)
|
|
125
|
+
- `confirmed=False` default — batches do not execute until `confirm()` is called
|
|
126
|
+
- `synchronous=False` default
|
|
127
|
+
- `create()` sleeps 5 seconds after submission by default (pass `sleep_seconds=0` to disable)
|
|
128
|
+
- Each batch instance can only be submitted once — `create()` raises `RuntimeError` if `id` is set
|
|
129
|
+
- All methods that call the Meraki API accept an optional `dashboard` argument
|
|
130
|
+
|
|
131
|
+
### Method lifecycle
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
ActionBatch.run(org_id, actions) — full lifecycle: create → confirm → wait → verify
|
|
135
|
+
returns a single combined VerifyResult
|
|
136
|
+
|
|
137
|
+
ActionBatch.from_actions() — creates batch objects; splits if needed
|
|
138
|
+
│
|
|
139
|
+
└─► create() — submits to Meraki; sets self.id; sleeps 5s
|
|
140
|
+
└─► confirm() — executes the batch (only needed if confirmed=False)
|
|
141
|
+
└─► wait_until_complete() — polls until completed/failed (async batches only)
|
|
142
|
+
└─► status() — single poll of current batch state
|
|
143
|
+
└─► verify() — returns VerifyResult for this batch
|
|
144
|
+
|
|
145
|
+
ActionBatch.wait_for_all(batches) — polls all batches together until all finish
|
|
146
|
+
ActionBatch.verify_many(batches) — returns {batch: VerifyResult} with minimal API calls
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Async vs synchronous timing
|
|
150
|
+
|
|
151
|
+
For **async batches** (`synchronous=False`, the default), `create()` returns as soon as Meraki accepts the batch. Changes have not been applied yet. Always call `wait_until_complete()` or `wait_for_all()` before `verify()` or `verify_many()`, otherwise verify will compare against the pre-change state and report everything as mismatched.
|
|
152
|
+
|
|
153
|
+
For **synchronous batches** (`synchronous=True`), `create()` blocks until all actions complete. `wait_until_complete()` and `wait_for_all()` are no-ops for these batches.
|
|
154
|
+
|
|
155
|
+
### VerifyResult and Mismatch
|
|
156
|
+
|
|
157
|
+
All verify methods return `VerifyResult` (from `result.py`):
|
|
158
|
+
- `result.verified: list[Action]`
|
|
159
|
+
- `result.mismatched: list[Mismatch]` — each has `.action` and `.mismatches` (camelCase field → diff dict)
|
|
160
|
+
- `result.unverifiable: list[Action]`
|
|
161
|
+
- `result.batch_errors: list[str]` — Meraki execution errors from `batch.errors`
|
|
162
|
+
|
|
163
|
+
`run()` returns a single combined `VerifyResult` across all batches.
|
|
164
|
+
`verify_many()` returns `{batch: VerifyResult}`.
|
|
165
|
+
`verify()` returns a `VerifyResult` for that batch only.
|
|
166
|
+
|
|
167
|
+
### verify behavior
|
|
168
|
+
|
|
169
|
+
- Uses bulk fetching: 1 API call per org (Device/Network), 1 per serial (Switchport), 1 per network (Vlan/Ssid/L3FirewallRule/DhcpServerPolicy)
|
|
170
|
+
- `verify_many()` and `run()` are preferred for 10+ actions — pools fetches across all batches
|
|
171
|
+
- merakisync model.get() manages its own API connectivity; no `dashboard` arg accepted
|
|
172
|
+
- Supported models: `Network`, `Device`, `Switchport`, `Vlan`, `Ssid`, `L3FirewallRule`, `DhcpServerPolicy`, `Organization`
|
|
173
|
+
- Actions without `source_obj` → "unverifiable" (not an error)
|
|
174
|
+
- Unsupported model types → "unverifiable" (not an error)
|
|
175
|
+
- API fetch errors for a group → all actions in that group become "unverifiable"
|
|
176
|
+
- Destroy operations: resource not found → "verified"; resource still exists → "mismatched"
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Adding a model to the verify registry
|
|
181
|
+
|
|
182
|
+
When merakisync adds a new model that supports action batch operations:
|
|
183
|
+
|
|
184
|
+
1. Add a deferred import inside `_fetch_current_state()` in `action_batch.py`
|
|
185
|
+
2. Add an `elif cls is NewModel:` branch that calls `NewModel.get(source="meraki", ...)`
|
|
186
|
+
3. Return `True, result` (supported) or `True, None` (supported but not found)
|
|
187
|
+
4. Update the supported models list in this file, `README.md`, and `docs/usage.md`
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Import rules
|
|
192
|
+
|
|
193
|
+
- merakiops imports from merakisync's submodules directly, not from the merakisync package root
|
|
194
|
+
```python
|
|
195
|
+
# Correct
|
|
196
|
+
from merakisync.dashboard import get_dashboard
|
|
197
|
+
|
|
198
|
+
# Wrong — may cause circular imports in some environments
|
|
199
|
+
from merakisync import get_dashboard
|
|
200
|
+
```
|
|
201
|
+
- All Meraki API calls in `create()`, `confirm()`, `status()` go through `get_dashboard()`
|
|
202
|
+
- Model imports in `_fetch_current_state()` are deferred (inside the function body)
|
|
203
|
+
- `get_dashboard()` is imported inside the method body where it is used, not at the top of the file
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Meraki API limits
|
|
208
|
+
|
|
209
|
+
| Batch type | Max actions per batch |
|
|
210
|
+
|---|---|
|
|
211
|
+
| Asynchronous (`synchronous=False`) | 100 |
|
|
212
|
+
| Synchronous (`synchronous=True`) | 20 |
|
|
213
|
+
|
|
214
|
+
`from_actions()` handles splitting. You do not need to count actions manually.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Before writing code
|
|
219
|
+
|
|
220
|
+
Always, without exception:
|
|
221
|
+
|
|
222
|
+
1. **State your reasoning** — explain why the change is needed, what problem it solves, and what alternatives you considered and rejected. Do not skip this even for small changes.
|
|
223
|
+
2. **State your assumptions** — call out anything you are inferring about merakisync behavior, Meraki API behavior, or existing code contracts. If something is unclear, say so explicitly before starting.
|
|
224
|
+
3. **Write an implementation plan** — list the specific files and lines you intend to touch, in order. For any change that affects a public method signature, also list the docstring, README, and docs/usage.md sections that need updating.
|
|
225
|
+
4. **Identify risks** — call out any existing behavior that could break, any edge cases the change introduces, and any callers that depend on the current behavior.
|
|
226
|
+
5. **Check existing patterns** — read the relevant code in both merakiops and merakisync before proposing anything new. Do not invent abstractions that already exist.
|
|
227
|
+
6. **Confirm alignment** — wait for explicit approval before making any code changes.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Definition of done
|
|
232
|
+
|
|
233
|
+
A change is complete when:
|
|
234
|
+
- It works correctly for the intended use case
|
|
235
|
+
- Error messages are clear and actionable — they tell the user what to do
|
|
236
|
+
- All public methods have docstrings that explain the return value and any exceptions raised
|
|
237
|
+
- Edge cases are handled: empty actions list, already-submitted batch, `None` source_obj
|
|
238
|
+
- `README.md` and `docs/usage.md` reflect any changes to the public API
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## What not to do
|
|
243
|
+
|
|
244
|
+
- Do not add scheduling, retry logic, or workflow orchestration
|
|
245
|
+
- Do not add `async` code
|
|
246
|
+
- Do not introduce new dependencies without discussion
|
|
247
|
+
- Do not silently swallow exceptions — log and re-raise
|
|
248
|
+
- Do not make Meraki API calls outside of `create()`, `confirm()`, `status()`, and `verify()`
|
|
249
|
+
- Do not accept `source_obj=None` as valid for any factory classmethod
|
|
250
|
+
- Do not call `batch.create()` twice on the same instance
|
|
251
|
+
- Do not submit a batch with 0 actions
|
|
252
|
+
- Do not put credentials in source code
|
|
253
|
+
- Do not import from `merakisync` (the package root) inside method bodies — import from the submodule directly
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Reference
|
|
258
|
+
|
|
259
|
+
- Batch limits and chunking: `docs/batch-limits.md`
|
|
260
|
+
- Full usage guide with examples: `docs/usage.md`
|
|
261
|
+
- Meraki action batch API: https://developer.cisco.com/meraki/api-v1/create-organization-action-batch/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Nathan Anderson <nathanea05@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|