nmos-cmd 0.0.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.
- nmos_cmd/__init__.py +1 -0
- nmos_cmd/__main__.py +5 -0
- nmos_cmd/cmd.py +312 -0
- nmos_cmd/utility.py +55 -0
- nmos_cmd/version.py +9 -0
- nmos_cmd-0.0.1.dist-info/METADATA +43 -0
- nmos_cmd-0.0.1.dist-info/RECORD +11 -0
- nmos_cmd-0.0.1.dist-info/WHEEL +5 -0
- nmos_cmd-0.0.1.dist-info/entry_points.txt +2 -0
- nmos_cmd-0.0.1.dist-info/licenses/LICENSE +21 -0
- nmos_cmd-0.0.1.dist-info/top_level.txt +1 -0
nmos_cmd/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .version import __version__
|
nmos_cmd/__main__.py
ADDED
nmos_cmd/cmd.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
import requests
|
|
5
|
+
import click
|
|
6
|
+
from .utility import get_folder_in_config as _F
|
|
7
|
+
from .utility import success, error, info, load_config, save_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NMOS:
|
|
11
|
+
|
|
12
|
+
def __init__(self, is04_ver=None, is04_port=None, is05_ver=None, is05_port=None):
|
|
13
|
+
cfg = load_config()
|
|
14
|
+
self.cfg = {'timeout': 30,
|
|
15
|
+
'is04_ver': '1.2',
|
|
16
|
+
'is04_port': 3212,
|
|
17
|
+
'is05_ver': '1.0',
|
|
18
|
+
'is05_port': 3215}
|
|
19
|
+
if "main" in cfg:
|
|
20
|
+
self.cfg.update(cfg['main'])
|
|
21
|
+
self.timeout = self._set_with_default(30, self.cfg['timeout'])
|
|
22
|
+
self.is04_ver = self._set_with_default(is04_ver, self.cfg['is04_ver'])
|
|
23
|
+
self.is04_port = self._set_with_default(is04_port, self.cfg['is04_port'])
|
|
24
|
+
self.is05_ver = self._set_with_default(is05_ver, self.cfg['is05_ver'])
|
|
25
|
+
self.is05_port = self._set_with_default(is05_port, self.cfg['is05_port'])
|
|
26
|
+
|
|
27
|
+
def _set_with_default(self, value, default=None):
|
|
28
|
+
if value is not None:
|
|
29
|
+
return value
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
def get_is04_url(self, device):
|
|
33
|
+
"""
|
|
34
|
+
Generate the IS04 url (node) for device
|
|
35
|
+
"""
|
|
36
|
+
return f"http://{device}:{self.is04_port}/x-nmos/node/v{self.is04_ver}"
|
|
37
|
+
|
|
38
|
+
def get_is05_url(self, device):
|
|
39
|
+
"""
|
|
40
|
+
Generate the IS05 url (connection) for device
|
|
41
|
+
"""
|
|
42
|
+
return f"http://{device}:{self.is05_port}/x-nmos/connection/v{self.is05_ver}"
|
|
43
|
+
|
|
44
|
+
def get_receiver(self, device, receiver):
|
|
45
|
+
"""
|
|
46
|
+
Return the receiver streams of "device" filtered by the name in "receiver"
|
|
47
|
+
|
|
48
|
+
The output is the dictionary: {stream label: stream id}
|
|
49
|
+
"""
|
|
50
|
+
result = {}
|
|
51
|
+
res = requests.get(f"{self.get_is04_url(device)}/devices/", timeout=self.timeout)
|
|
52
|
+
d = res.json()
|
|
53
|
+
if isinstance(receiver, str):
|
|
54
|
+
receiver = [receiver]
|
|
55
|
+
for dd in d:
|
|
56
|
+
for rid in dd["receivers"]:
|
|
57
|
+
r = requests.get(f"{self.get_is04_url(device)}/receivers/{rid}",
|
|
58
|
+
timeout=self.timeout)
|
|
59
|
+
r = r.json()
|
|
60
|
+
if not receiver or all(rec in r["label"] for rec in receiver):
|
|
61
|
+
result[r['label']] = r['id']
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def get_sender(self, device, sender, download=False):
|
|
66
|
+
"""
|
|
67
|
+
Return the sender streams of "device" filtered by the name in "sender"
|
|
68
|
+
|
|
69
|
+
The output is the dictionary: {stream label: stream id}, ant it will also
|
|
70
|
+
download the corresponding sdp to be used by the receiver streams.
|
|
71
|
+
"""
|
|
72
|
+
result = {}
|
|
73
|
+
res = requests.get(f"{self.get_is04_url(device)}/devices/", timeout=self.timeout)
|
|
74
|
+
d = res.json()
|
|
75
|
+
if isinstance(sender, str):
|
|
76
|
+
sender = [sender]
|
|
77
|
+
for dd in d:
|
|
78
|
+
for sid in dd["senders"]:
|
|
79
|
+
r = requests.get(f"{self.get_is04_url(device)}/senders/{sid}", timeout=self.timeout)
|
|
80
|
+
r = r.json()
|
|
81
|
+
if not sender or all(rec in r["label"] for rec in sender):
|
|
82
|
+
result[r['label']] = r['id']
|
|
83
|
+
if download:
|
|
84
|
+
self.download_sdp(r['manifest_href'], _F(device), r['id'])
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def download_sdp(self, url, folder, filename=""):
|
|
89
|
+
"""
|
|
90
|
+
Download the sdp file from url to folder/filename.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
response = requests.get(url, timeout=self.timeout)
|
|
94
|
+
# Raise an exception for bad status codes (4XX or 5XX)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
if not filename:
|
|
97
|
+
filename = Path(url).name
|
|
98
|
+
os.makedirs(folder, exist_ok=True)
|
|
99
|
+
with open(Path(folder)/filename, 'wb') as file:
|
|
100
|
+
file.write(response.content)
|
|
101
|
+
except requests.exceptions.RequestException as e:
|
|
102
|
+
error(f"Download '{url}' failed: {e}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def list_device(self, device, receiver, sender):
|
|
106
|
+
"""
|
|
107
|
+
List the sender and receiver streams from device
|
|
108
|
+
"""
|
|
109
|
+
res = requests.get(f"{self.get_is04_url(device)}/devices/", timeout=self.timeout)
|
|
110
|
+
d = res.json()
|
|
111
|
+
os.makedirs(_F(device), exist_ok=True)
|
|
112
|
+
for dd in d:
|
|
113
|
+
info(dd["description"])
|
|
114
|
+
for rid in dd["receivers"]:
|
|
115
|
+
r = requests.get(f"{self.get_is04_url(device)}/receivers/{rid}",
|
|
116
|
+
timeout=self.timeout)
|
|
117
|
+
r = r.json()
|
|
118
|
+
if not receiver or all(rx in r["label"] for rx in receiver):
|
|
119
|
+
info(f" {r['label']} {r['id']}")
|
|
120
|
+
for sid in dd["senders"]:
|
|
121
|
+
r = requests.get(f"{self.get_is04_url(device)}/senders/{sid}", timeout=self.timeout)
|
|
122
|
+
r = r.json()
|
|
123
|
+
if not sender or all(tx in r["label"] for tx in sender):
|
|
124
|
+
info(f" {r['label']} {r['id']} {r['manifest_href']}")
|
|
125
|
+
|
|
126
|
+
def _parse_device(self, device, port, ver):
|
|
127
|
+
"""
|
|
128
|
+
Parse the device, supported format IP@name or IP:port@name or IP:port:version@name
|
|
129
|
+
|
|
130
|
+
If port/version is defined in "device", it will take priority.
|
|
131
|
+
"""
|
|
132
|
+
ip, name = device.split('@')
|
|
133
|
+
ip = ip.split(':')
|
|
134
|
+
if port is None:
|
|
135
|
+
port = self.is04_port
|
|
136
|
+
if ver is None:
|
|
137
|
+
ver = self.is04_ver
|
|
138
|
+
if len(ip) == 1:
|
|
139
|
+
ip = ip[0]
|
|
140
|
+
elif len(ip) == 2:
|
|
141
|
+
ip, port = ip
|
|
142
|
+
elif len(ip) == 3:
|
|
143
|
+
ip, port, ver = ip
|
|
144
|
+
return ip, port, ver, name
|
|
145
|
+
|
|
146
|
+
def generate_patch(self, sender, sender_port, sender_version, receiver,
|
|
147
|
+
receiver_port, receiver_version, stream, output):
|
|
148
|
+
"""
|
|
149
|
+
Generate the config json file to connect the receiver streams to the
|
|
150
|
+
corresponding sender streams.
|
|
151
|
+
"""
|
|
152
|
+
info("Get sender streams")
|
|
153
|
+
tx_ip, tx_port, tx_ver, tx_name = self._parse_device(sender, sender_port, sender_version)
|
|
154
|
+
tx_d = NMOS(is04_port=tx_port, is04_ver=tx_ver)
|
|
155
|
+
tx = tx_d.get_sender(tx_ip, tx_name, True)
|
|
156
|
+
|
|
157
|
+
info("Get receiver streams")
|
|
158
|
+
rx_ip, rx_port, rx_ver, rx_name = self._parse_device(receiver, receiver_port,
|
|
159
|
+
receiver_version)
|
|
160
|
+
rx_d = NMOS(is04_port=rx_port, is04_ver=rx_ver)
|
|
161
|
+
rx = rx_d.get_receiver(rx_ip, rx_name)
|
|
162
|
+
mapping = []
|
|
163
|
+
info("Connect sender streams to receiver streams")
|
|
164
|
+
for s in stream:
|
|
165
|
+
s = s.split(":")
|
|
166
|
+
if len(s) == 1:
|
|
167
|
+
s = [s, s]
|
|
168
|
+
# find the tx stream
|
|
169
|
+
s_tx = [ r for r in tx if s[0] in r]
|
|
170
|
+
if len(s_tx) != 1:
|
|
171
|
+
error(f"Multiple or no streams found in sender for '{s[0]}'!")
|
|
172
|
+
return
|
|
173
|
+
s_tx = s_tx[0]
|
|
174
|
+
|
|
175
|
+
# find the rx stream
|
|
176
|
+
s_rx = [ r for r in rx if s[1] in r]
|
|
177
|
+
if len(s_rx) != 1:
|
|
178
|
+
error(f"Multiple or no streams found in receiver '{s[1]}'!")
|
|
179
|
+
return
|
|
180
|
+
s_rx = s_rx[0]
|
|
181
|
+
|
|
182
|
+
with open(Path(_F(tx_ip))/tx[s_tx], 'r', encoding='utf-8') as file:
|
|
183
|
+
sdp = file.read()
|
|
184
|
+
mapping.append([rx_ip, s_rx, rx[s_rx], sdp])
|
|
185
|
+
|
|
186
|
+
# save the patch configuration
|
|
187
|
+
p = Path(output)
|
|
188
|
+
if not p.suffix:
|
|
189
|
+
p = p.with_suffix('.json')
|
|
190
|
+
info(f"Save the config to {p}")
|
|
191
|
+
with open(p, "w", encoding='utf-8') as json_file:
|
|
192
|
+
json.dump(mapping, json_file, indent=4)
|
|
193
|
+
|
|
194
|
+
def apply_patch(self, cfg):
|
|
195
|
+
"""
|
|
196
|
+
Sent the PATCH command one by one defined in the configuration json file.
|
|
197
|
+
"""
|
|
198
|
+
with open(cfg, 'r', encoding='utf-8') as file:
|
|
199
|
+
cfg = json.load(file)
|
|
200
|
+
for c in cfg:
|
|
201
|
+
device, rx_name, rx, sdp = c
|
|
202
|
+
data = {"activation":{"mode": "activate_immediate",
|
|
203
|
+
"requested_time": None
|
|
204
|
+
},
|
|
205
|
+
"transport_file": {"data": "\r\n".join(sdp.lstrip().split('\n')),
|
|
206
|
+
"type": "application/sdp"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
url = f'{self.get_is05_url(device)}/single/receivers/{rx}/staged'
|
|
211
|
+
response = requests.patch(url, json=data, timeout=self.timeout)
|
|
212
|
+
if response.status_code != 200:
|
|
213
|
+
error(f"Patch '{rx_name}' failed: {response.status_code}")
|
|
214
|
+
error(f" {url}")
|
|
215
|
+
else:
|
|
216
|
+
success(f"Patch '{rx_name}' succeeded: {response.status_code}")
|
|
217
|
+
|
|
218
|
+
@click.group()
|
|
219
|
+
def cli():
|
|
220
|
+
"""
|
|
221
|
+
A tool to connect the NMOS sender streams to receiver streams.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
@cli.command(name="list", context_settings={'show_default': True})
|
|
225
|
+
@click.option('--device', help='the device IP')
|
|
226
|
+
@click.option('--port', default=3212, help='NMOS IS04 port')
|
|
227
|
+
@click.option('--version', default="1.2", type=click.Choice(['1.0', '1.1', '1.2', '1.3']),
|
|
228
|
+
help='NMOS IS04 version')
|
|
229
|
+
@click.option('--receiver', multiple=True, help='only show the receiver streams with specific name')
|
|
230
|
+
@click.option('--sender', multiple=True, help='only show the sender streams with specific name')
|
|
231
|
+
def list_device(device, port, version, receiver, sender):
|
|
232
|
+
"""
|
|
233
|
+
List streams of a device.
|
|
234
|
+
"""
|
|
235
|
+
n = NMOS(is04_ver=version, is04_port=port)
|
|
236
|
+
n.list_device(device, receiver, sender)
|
|
237
|
+
|
|
238
|
+
@cli.group()
|
|
239
|
+
def patch():
|
|
240
|
+
"""
|
|
241
|
+
Send NMOS PATCH command.
|
|
242
|
+
|
|
243
|
+
e.g., to connect the sender streams to receiver streams.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
@patch.command(name="config", context_settings={'show_default': True})
|
|
247
|
+
@click.option('--sender', help='sender device IP@name or IP:port@name or IP:port:version@name')
|
|
248
|
+
@click.option('--sender_port', default=3212, help='NMOS IS04 port')
|
|
249
|
+
@click.option('--sender_version', default="1.2", type=click.Choice(['1.0', '1.1', '1.2', '1.3']),
|
|
250
|
+
help='NMOS IS04 version')
|
|
251
|
+
@click.option('--receiver', help='receiver device IP@name or IP:port@name or IP:port:version@name')
|
|
252
|
+
@click.option('--receiver_port', default=3212, help='NMOS IS04 port')
|
|
253
|
+
@click.option('--receiver_version', default="1.2", type=click.Choice(['1.0', '1.1', '1.2', '1.3']),
|
|
254
|
+
help='NMOS IS04 version')
|
|
255
|
+
@click.option('--stream', default=["video:video", "audio output 1:audio input 1", "data:data"],
|
|
256
|
+
multiple=True, help='the stream to be configured, in format "sender stream"@"receiver stream"')
|
|
257
|
+
@click.option('--output', help='the output patch configuration filename')
|
|
258
|
+
def generate_patch(sender, sender_port, sender_version, receiver, receiver_port,
|
|
259
|
+
receiver_version, stream, output):
|
|
260
|
+
"""
|
|
261
|
+
Generate the configuration file to connect the sender streams to the receiver streams.
|
|
262
|
+
"""
|
|
263
|
+
n = NMOS()
|
|
264
|
+
n.generate_patch(sender, sender_port, sender_version, receiver, receiver_port,
|
|
265
|
+
receiver_version, stream, output)
|
|
266
|
+
|
|
267
|
+
@patch.command(name="apply", context_settings={'show_default': True})
|
|
268
|
+
@click.option('--config', 'cfg', default="config.json", help='the patch configuration file')
|
|
269
|
+
@click.option('--port', default=3215, help='NMOS IS05 port')
|
|
270
|
+
@click.option('--version', default='1.0', type=click.Choice(['1.0', '1.1']),
|
|
271
|
+
help='NMOS IS05 version')
|
|
272
|
+
def apply_patch(cfg, port, version):
|
|
273
|
+
"""
|
|
274
|
+
Apply the PATCH to the receiver streams defined in "--config".
|
|
275
|
+
"""
|
|
276
|
+
n = NMOS(is05_ver=version, is05_port=port)
|
|
277
|
+
n.apply_patch(cfg)
|
|
278
|
+
|
|
279
|
+
@cli.group(name="config", context_settings={'show_default': True})
|
|
280
|
+
def config():
|
|
281
|
+
"""
|
|
282
|
+
Set default option for each command/subcommand.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
@config.command(name="set", context_settings={'show_default': True})
|
|
286
|
+
@click.argument('key')
|
|
287
|
+
@click.argument('value')
|
|
288
|
+
def config_set(key, value):
|
|
289
|
+
"""
|
|
290
|
+
Set default option for each command.
|
|
291
|
+
|
|
292
|
+
For example, the following command will set the default port for "list" command to 3213
|
|
293
|
+
|
|
294
|
+
nmos-patch config set list.port 3213
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
cfg = load_config()
|
|
298
|
+
t = cfg
|
|
299
|
+
for k in key.split('.')[:-1]:
|
|
300
|
+
if k not in cfg:
|
|
301
|
+
t[k] = {}
|
|
302
|
+
t = t[k]
|
|
303
|
+
t[key.split('.')[-1]] = value
|
|
304
|
+
save_config(cfg)
|
|
305
|
+
|
|
306
|
+
@config.command(name="list", context_settings={'show_default': True})
|
|
307
|
+
def config_list():
|
|
308
|
+
"""
|
|
309
|
+
List all the default options
|
|
310
|
+
"""
|
|
311
|
+
cfg = load_config()
|
|
312
|
+
info(json.dumps(cfg, indent=4))
|
nmos_cmd/utility.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
import platformdirs
|
|
4
|
+
import click
|
|
5
|
+
import tqdm
|
|
6
|
+
from .version import PROJECT_NAME
|
|
7
|
+
|
|
8
|
+
def get_config_dir():
|
|
9
|
+
config_dir = platformdirs.user_config_dir(PROJECT_NAME, "nmos-cmd team")
|
|
10
|
+
return config_dir
|
|
11
|
+
|
|
12
|
+
def get_folder_in_config(folder):
|
|
13
|
+
return Path(get_config_dir())/folder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def echo(*args, **kwargs):
|
|
17
|
+
if not kwargs.get('tqdm_mode', False):
|
|
18
|
+
click.secho(*args, **kwargs)
|
|
19
|
+
else:
|
|
20
|
+
end = '\n'
|
|
21
|
+
if not kwargs.get('nl', True):
|
|
22
|
+
end = ''
|
|
23
|
+
kwargs.pop('nl', None)
|
|
24
|
+
tqdm.tqdm.write(click.style(*args, **kwargs), end=end)
|
|
25
|
+
|
|
26
|
+
def info(*args, **kwargs):
|
|
27
|
+
echo(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def error(*args, **kwargs):
|
|
30
|
+
if 'fg' not in kwargs:
|
|
31
|
+
kwargs['fg'] = 'red'
|
|
32
|
+
echo(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def warning(*args, **kwargs):
|
|
35
|
+
if 'fg' not in kwargs:
|
|
36
|
+
kwargs['fg'] = 'blue'
|
|
37
|
+
echo(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
def success(*args, **kwargs):
|
|
40
|
+
if 'fg' not in kwargs:
|
|
41
|
+
kwargs['fg'] = 'green'
|
|
42
|
+
info(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
def load_config():
|
|
45
|
+
cfg = {}
|
|
46
|
+
filename = Path(get_config_dir())/f"{PROJECT_NAME}.ini"
|
|
47
|
+
if filename.is_file():
|
|
48
|
+
with open(filename, 'r', encoding='utf-8') as file:
|
|
49
|
+
cfg = json.load(file)
|
|
50
|
+
return cfg
|
|
51
|
+
|
|
52
|
+
def save_config(cfg):
|
|
53
|
+
filename = Path(get_config_dir())/f"{PROJECT_NAME}.ini"
|
|
54
|
+
with open(filename, 'w', encoding='utf-8') as file:
|
|
55
|
+
json.dump(cfg, file, indent=4)
|
nmos_cmd/version.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nmos-cmd
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: nmos-cmd is a command line tool to send NMOS requests
|
|
5
|
+
Author-email: Tianzhu Qiao <tq@feiyilin.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tianzhuqiao/nmos-cmd
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: click>=8.1
|
|
13
|
+
Requires-Dist: packaging
|
|
14
|
+
Requires-Dist: platformdirs
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# nmos-patch
|
|
20
|
+
**nmos-cmd** is a tool to make it easier to send [NMOS](https://specs.amwa.tv/nmos/) requests.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
```
|
|
25
|
+
$ pip install nmos-cmd
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To start from terminal
|
|
29
|
+
```
|
|
30
|
+
$ nmos-cmd --help
|
|
31
|
+
Usage: nmos-cmd [OPTIONS] COMMAND [ARGS]...
|
|
32
|
+
|
|
33
|
+
A tool to connect the NMOS sender streams to receiver streams.
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--help Show this message and exit.
|
|
37
|
+
|
|
38
|
+
Commands:
|
|
39
|
+
config Set default option for each command/subcommand.
|
|
40
|
+
list List streams of a device.
|
|
41
|
+
patch Send NMOS PATCH command.
|
|
42
|
+
```
|
|
43
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
nmos_cmd/__init__.py,sha256=Jzb_RfgvXCrm_SQ4AfeGVi1N36YybxnM5mpyxrnihgI,33
|
|
2
|
+
nmos_cmd/__main__.py,sha256=9n_evVQeeWJM7ad-3rM4uF9uy03rYe-B7XKRrdP3fYY,102
|
|
3
|
+
nmos_cmd/cmd.py,sha256=1zj8g2tZQAmtxkQ5aGPvRpMntoFjQpmjryogBrqCW-U,12352
|
|
4
|
+
nmos_cmd/utility.py,sha256=p9gZKFXts2WkjT9ifV6EjSusW1HfrKhaINY3ZlKs7Zk,1407
|
|
5
|
+
nmos_cmd/version.py,sha256=8CIQlFZjA6U1eXPY8U73OUUWIYf4nCvlnsOb4yJ1KMA,193
|
|
6
|
+
nmos_cmd-0.0.1.dist-info/licenses/LICENSE,sha256=7s89MNphVRVRMzPSy7iJFZUdJa_96B-Dps6lb-JPWZ0,1090
|
|
7
|
+
nmos_cmd-0.0.1.dist-info/METADATA,sha256=uuiv-PSOviLuFpd9aASAqiAzw545nYt_TDX4OU70GPg,1031
|
|
8
|
+
nmos_cmd-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
nmos_cmd-0.0.1.dist-info/entry_points.txt,sha256=XujchMayA6fatP1x1dUuBwzUdCbLFrBKpg_olo931hg,52
|
|
10
|
+
nmos_cmd-0.0.1.dist-info/top_level.txt,sha256=N_aRZs3cuWMwBpB56JB34t8ZaL4Wim4Uc9yNKLEE43Y,9
|
|
11
|
+
nmos_cmd-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tianzhu Qiao
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nmos_cmd
|