catocli 3.0.26__py3-none-any.whl → 3.0.30__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.

Potentially problematic release.


This version of catocli might be problematic. Click here for more details.

@@ -47,16 +47,16 @@ from ..parsers.query_enterpriseDirectory import query_enterpriseDirectory_parse
47
47
  from ..parsers.query_devices import query_devices_parse
48
48
  from ..parsers.query_accountSnapshot import query_accountSnapshot_parse
49
49
  from ..parsers.query_catalogs import query_catalogs_parse
50
- from ..parsers.query_site import query_site_parse
51
50
  from ..parsers.query_xdr import query_xdr_parse
52
- from ..parsers.query_policy import query_policy_parse
53
- from ..parsers.query_container import query_container_parse
51
+ from ..parsers.query_site import query_site_parse
54
52
  from ..parsers.query_groups import query_groups_parse
53
+ from ..parsers.query_container import query_container_parse
54
+ from ..parsers.query_policy import query_policy_parse
55
55
  from ..parsers.mutation_xdr import mutation_xdr_parse
56
- from ..parsers.mutation_sites import mutation_sites_parse
57
- from ..parsers.mutation_container import mutation_container_parse
58
56
  from ..parsers.mutation_site import mutation_site_parse
57
+ from ..parsers.mutation_sites import mutation_sites_parse
59
58
  from ..parsers.mutation_policy import mutation_policy_parse
59
+ from ..parsers.mutation_container import mutation_container_parse
60
60
  from ..parsers.mutation_admin import mutation_admin_parse
61
61
  from ..parsers.mutation_accountManagement import mutation_accountManagement_parse
62
62
  from ..parsers.mutation_sandbox import mutation_sandbox_parse
@@ -182,16 +182,16 @@ query_enterpriseDirectory_parser = query_enterpriseDirectory_parse(query_subpars
182
182
  query_devices_parser = query_devices_parse(query_subparsers)
183
183
  query_accountSnapshot_parser = query_accountSnapshot_parse(query_subparsers)
184
184
  query_catalogs_parser = query_catalogs_parse(query_subparsers)
185
- query_site_parser = query_site_parse(query_subparsers)
186
185
  query_xdr_parser = query_xdr_parse(query_subparsers)
187
- query_policy_parser = query_policy_parse(query_subparsers)
188
- query_container_parser = query_container_parse(query_subparsers)
186
+ query_site_parser = query_site_parse(query_subparsers)
189
187
  query_groups_parser = query_groups_parse(query_subparsers)
188
+ query_container_parser = query_container_parse(query_subparsers)
189
+ query_policy_parser = query_policy_parse(query_subparsers)
190
190
  mutation_xdr_parser = mutation_xdr_parse(mutation_subparsers)
191
- mutation_sites_parser = mutation_sites_parse(mutation_subparsers)
192
- mutation_container_parser = mutation_container_parse(mutation_subparsers)
193
191
  mutation_site_parser = mutation_site_parse(mutation_subparsers)
192
+ mutation_sites_parser = mutation_sites_parse(mutation_subparsers)
194
193
  mutation_policy_parser = mutation_policy_parse(mutation_subparsers)
194
+ mutation_container_parser = mutation_container_parse(mutation_subparsers)
195
195
  mutation_admin_parser = mutation_admin_parse(mutation_subparsers)
196
196
  mutation_accountManagement_parser = mutation_accountManagement_parse(mutation_subparsers)
197
197
  mutation_sandbox_parser = mutation_sandbox_parse(mutation_subparsers)
@@ -165,17 +165,6 @@ def build_wide_timeseries_header(dimension_names: List[str], measures: List[str]
165
165
  return header
166
166
 
167
167
 
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
168
  def format_to_csv(response_data: Dict[str, Any], operation_name: str) -> str:
180
169
  """
181
170
  Main function to format response data to CSV based on operation type
@@ -82,19 +82,33 @@ class JSONExample:
82
82
  """Format the JSON example for the specific platform - show only the best format"""
83
83
  if platform_info.platform == 'windows':
84
84
  if platform_info.shell == 'powershell':
85
- # For PowerShell, show multiple options to work around quote stripping issues
85
+ # For PowerShell, show here-string format
86
86
  return self._format_powershell_comprehensive(command_name)
87
87
  else:
88
88
  # For cmd, use single-line with escaped quotes
89
89
  single_line = json.dumps(self.parsed_json) if self.parsed_json else self.json_data.replace('\n', ' ')
90
90
  escaped_json = single_line.replace('"', '\\"')
91
- return [f'catocli {command_name} "{escaped_json}" -p']
91
+ # Extract flags from command template
92
+ flags = self._extract_flags_from_template()
93
+ return [f'catocli {command_name} "{escaped_json}" {flags}'.strip()]
92
94
  else:
93
- # For Unix-like systems, use multi-line
95
+ # For Unix-like systems, use multi-line with proper formatting
94
96
  return [self.command_template.format(command=command_name, json=self.json_data)]
95
97
 
98
+ def _extract_flags_from_template(self) -> str:
99
+ """Extract flags from the command template (everything after {json})"""
100
+ # Extract everything after '{json}' in the template
101
+ if "'{json}'" in self.command_template:
102
+ parts = self.command_template.split("'{json}'")
103
+ if len(parts) > 1:
104
+ return parts[1].strip()
105
+ return ""
106
+
96
107
  def _format_powershell_comprehensive(self, command_name: str) -> List[str]:
97
108
  """Format PowerShell here-string example with proper quote escaping"""
109
+ # Extract flags from command template
110
+ flags = self._extract_flags_from_template()
111
+
98
112
  # Escape double quotes in JSON for PowerShell compatibility
99
113
  escaped_json = self.json_data.replace('"', '\\"')
100
114
 
@@ -103,7 +117,7 @@ class JSONExample:
103
117
  "# PowerShell (using here-string):",
104
118
  f"catocli {command_name} @'",
105
119
  escaped_json,
106
- "'@ -p"
120
+ f"'@ {flags}".strip() if flags else "'@"
107
121
  ]
108
122
 
109
123
  return examples
@@ -227,52 +241,94 @@ class UniversalHelpFormatter:
227
241
  readme_examples = self._extract_from_readme(command_path)
228
242
  if readme_examples:
229
243
  for example in readme_examples:
230
- # Check if this example starts with a comment (lines starting with #)
231
- if example.startswith('#') and '\n' in example:
232
- # This is a comment followed by a command - need to format the command part
244
+ # Check if this is a header or description line (not a command)
245
+ if example.startswith('###') or (example.startswith('-') and 'catocli' not in example):
246
+ # This is a header or description - preserve as-is
247
+ help_lines.append(example)
248
+ continue
249
+
250
+ # Check if this example starts with a comment followed by a command
251
+ if example.startswith('#') and '\n' in example and 'catocli' in example:
252
+ # This is a comment followed by a command - extract both parts
233
253
  lines = example.split('\n', 1)
234
254
  if len(lines) == 2:
235
255
  comment_line = lines[0]
236
- command_line = lines[1]
256
+ command_part = lines[1]
237
257
 
238
- # Check if command has JSON and format appropriately
239
- if '{' in command_line and '}' in command_line:
240
- json_match = self._extract_json_from_example(command_line)
258
+ # Check if command has multi-line JSON and format appropriately
259
+ if "'{" in command_part and "}'" in command_part:
260
+ json_match = self._extract_json_from_example(command_part)
241
261
  if json_match:
262
+ # Extract command flags (everything after }')
263
+ flags = ""
264
+ if "}'" in command_part:
265
+ flags_match = re.search(r"}'\s*(.*)$", command_part, re.MULTILINE)
266
+ if flags_match:
267
+ flags = flags_match.group(1).strip()
268
+
269
+ # Create command template with flags
270
+ if flags:
271
+ command_template = f"catocli {{command}} '{{json}}' {flags}"
272
+ else:
273
+ command_template = "catocli {command} '{json}'"
274
+
242
275
  # Create JSONExample and apply platform-specific formatting
243
- json_example = JSONExample(json_match)
276
+ json_example = JSONExample(json_match, command_template)
244
277
  formatted_commands = json_example.format_for_platform(self.platform_info, command_name)
245
278
  # Add comment first, then formatted commands
246
279
  help_lines.append(comment_line)
247
280
  help_lines.extend(formatted_commands)
248
281
  else:
249
282
  # JSON extraction failed, format as simple command
250
- formatted_command = self._format_simple_command_for_platform(command_line)
283
+ formatted_command = self._format_simple_command_for_platform(command_part)
251
284
  help_lines.append(comment_line)
252
285
  help_lines.append(formatted_command)
253
286
  else:
254
287
  # Simple command - apply platform formatting
255
- formatted_command = self._format_simple_command_for_platform(command_line)
288
+ formatted_command = self._format_simple_command_for_platform(command_part)
256
289
  help_lines.append(comment_line)
257
290
  help_lines.append(formatted_command)
258
291
  else:
259
292
  # Fallback - preserve as-is
260
293
  help_lines.append(example)
261
- elif '{' in example and '}' in example:
262
- # Check if this is a multi-line JSON example that can be platform-formatted
263
- json_match = self._extract_json_from_example(example)
264
- if json_match and not example.startswith('#'):
265
- # Create JSONExample and apply platform-specific formatting
266
- json_example = JSONExample(json_match)
267
- formatted_examples = json_example.format_for_platform(self.platform_info, command_name)
268
- help_lines.extend(formatted_examples)
294
+ elif 'catocli' in example and '{' in example and '}' in example:
295
+ # This is a catocli command with JSON - check if multi-line
296
+ if '\n' in example and "'{" in example:
297
+ # Multi-line JSON command
298
+ json_match = self._extract_json_from_example(example)
299
+ if json_match:
300
+ # Extract command flags (everything after }')
301
+ flags = ""
302
+ if "}'" in example:
303
+ flags_match = re.search(r"}'\s*(.*)$", example, re.MULTILINE)
304
+ if flags_match:
305
+ flags = flags_match.group(1).strip()
306
+
307
+ # Create command template with flags
308
+ if flags:
309
+ command_template = f"catocli {{command}} '{{json}}' {flags}"
310
+ else:
311
+ command_template = "catocli {command} '{json}'"
312
+
313
+ # Create JSONExample and apply platform-specific formatting
314
+ json_example = JSONExample(json_match, command_template)
315
+ formatted_examples = json_example.format_for_platform(self.platform_info, command_name)
316
+ help_lines.extend(formatted_examples)
317
+ else:
318
+ # Preserve as-is if JSON extraction fails
319
+ help_lines.append(example)
269
320
  else:
270
- # Preserve as-is if it has comments or JSON extraction fails
271
- help_lines.append(example)
321
+ # Single-line command with JSON - format as simple command
322
+ formatted_example = self._format_simple_command_for_platform(example)
323
+ help_lines.append(formatted_example)
272
324
  else:
273
- # Simple command examples without JSON - apply platform formatting
274
- formatted_example = self._format_simple_command_for_platform(example)
275
- help_lines.append(formatted_example)
325
+ # Simple command examples without JSON or description text - apply platform formatting
326
+ if 'catocli' in example:
327
+ formatted_example = self._format_simple_command_for_platform(example)
328
+ help_lines.append(formatted_example)
329
+ else:
330
+ # Not a command - preserve as-is (likely description text)
331
+ help_lines.append(example)
276
332
  help_lines.append("") # Add spacing between examples
277
333
 
278
334
  description_examples = []
@@ -307,7 +363,7 @@ class UniversalHelpFormatter:
307
363
  return "\n".join(help_lines)
308
364
 
309
365
  def _extract_from_readme(self, command_path: str) -> List[str]:
310
- """Extract all catocli examples from README.md files with comments"""
366
+ """Extract catocli examples from README.md files, prioritizing Additional Examples section"""
311
367
  examples = []
312
368
 
313
369
  # Find README.md file
@@ -328,21 +384,48 @@ class UniversalHelpFormatter:
328
384
 
329
385
  command_name = command_path.replace('_', ' ')
330
386
 
331
- # Check if "### Examples Summary" or "### Additional Examples" sections exist
332
- has_examples_summary = "### Examples Summary" in content or "### Additional Examples" in content
333
- if has_examples_summary:
334
- # Extract and display the examples summary section
335
- summary_pattern = r'### (?:Examples Summary|Additional Examples)\n(.*?)(?=\n###|\n## |\Z)'
336
- summary_match = re.search(summary_pattern, content, re.DOTALL)
337
- if summary_match:
338
- summary_content = summary_match.group(1).strip()
339
- examples.append(f"\n### Examples Summary")
340
- examples.append(summary_content)
387
+ # Check if "## Advanced Usage" or "### Additional Examples" section exists
388
+ has_advanced_usage = "## Advanced Usage" in content
389
+ has_additional_examples = "### Additional Examples" in content
390
+
391
+ # Define content to parse
392
+ content_to_parse = content
393
+ if has_advanced_usage:
394
+ # Extract the Advanced Usage section (up to #### which is next level heading)
395
+ advanced_pattern = r'## Advanced Usage\n(.*?)(?=\n####|\Z)'
396
+ advanced_match = re.search(advanced_pattern, content, re.DOTALL)
397
+ if advanced_match:
398
+ content_to_parse = advanced_match.group(1)
399
+ # Add a header for the examples
400
+ examples.append("### Examples")
401
+ # Extract list items from Additional Examples intro (before first # heading)
402
+ intro_pattern = r'### Additional Examples\n(.*?)(?=\n#[^#])'
403
+ intro_match = re.search(intro_pattern, content_to_parse, re.DOTALL)
404
+ if intro_match and intro_match.group(1).strip():
405
+ intro_text = intro_match.group(1).strip()
406
+ # Only add if it contains bullet points or descriptions
407
+ if '-' in intro_text:
408
+ examples.append(intro_text)
409
+ examples.append("") # Add spacing
410
+ elif has_additional_examples:
411
+ # Fallback: Extract only the Additional Examples section
412
+ additional_pattern = r'### Additional Examples\n(.*?)(?=\n####|\n## |\Z)'
413
+ additional_match = re.search(additional_pattern, content, re.DOTALL)
414
+ if additional_match:
415
+ content_to_parse = additional_match.group(1)
416
+ examples.append("### Examples")
417
+ # Extract list items from the section intro
418
+ intro_pattern = r'^(.*?)(?=\n#[^#])'
419
+ intro_match = re.search(intro_pattern, content_to_parse, re.DOTALL)
420
+ if intro_match and intro_match.group(1).strip():
421
+ intro_text = intro_match.group(1).strip()
422
+ if '-' in intro_text:
423
+ examples.append(intro_text)
341
424
  examples.append("") # Add spacing
342
425
 
343
- # Extract ALL catocli commands from markdown code blocks
426
+ # Extract catocli commands from markdown code blocks
344
427
  code_block_pattern = r'```(?:bash|shell|json)?\n(.*?)```'
345
- matches = re.findall(code_block_pattern, content, re.DOTALL)
428
+ matches = re.findall(code_block_pattern, content_to_parse, re.DOTALL)
346
429
 
347
430
  for match in matches:
348
431
  # Split the match into individual lines and extract catocli commands with comments
@@ -375,10 +458,14 @@ class UniversalHelpFormatter:
375
458
  # Keep the full command line including the opening bracket
376
459
  elif in_multiline_json and current_command:
377
460
  # We're in a multi-line JSON block - preserve exact formatting
378
- if stripped_line == "}'":
379
- # End of multi-line JSON
461
+ if "}'" in stripped_line:
462
+ # End of multi-line JSON - include any flags after }'
380
463
  current_command += '\n' + line
381
464
  in_multiline_json = False
465
+ # Append the completed command
466
+ examples.append(current_command)
467
+ current_command = None
468
+ current_comment = None
382
469
  else:
383
470
  # Continue multi-line JSON with exact indentation
384
471
  current_command += '\n' + line
@@ -402,13 +489,14 @@ class UniversalHelpFormatter:
402
489
  if current_command and not in_multiline_json:
403
490
  examples.append(current_command)
404
491
 
405
- # Also look for inline catocli commands (in backticks)
406
- inline_pattern = r'`(catocli[^`]+)`'
407
- inline_matches = re.findall(inline_pattern, content)
408
-
409
- for cmd in inline_matches:
410
- if command_name in cmd and cmd not in examples:
411
- examples.append(cmd)
492
+ # If no Additional Examples section was found, also look for inline catocli commands (in backticks)
493
+ if not has_additional_examples:
494
+ inline_pattern = r'`(catocli[^`]+)`'
495
+ inline_matches = re.findall(inline_pattern, content)
496
+
497
+ for cmd in inline_matches:
498
+ if command_name in cmd and cmd not in examples:
499
+ examples.append(cmd)
412
500
 
413
501
  except Exception as e:
414
502
  print(f"Warning: Could not parse README for {command_path}: {e}")
@@ -465,20 +553,21 @@ class UniversalHelpFormatter:
465
553
  def _extract_json_from_example(self, example: str) -> str:
466
554
  """Extract JSON data from a catocli command example"""
467
555
  try:
468
- # Look for JSON in single quotes first (most common)
469
- single_quote_pattern = r"catocli[^']*'([^']+)'"
470
- match = re.search(single_quote_pattern, example)
556
+ # Look for multi-line JSON first (between '{ and }' with potential flags after)
557
+ # This pattern captures everything between '{ and }' (non-greedy)
558
+ multiline_pattern = r"'(\{[\s\S]*?\})'(?:\s+[-\w])*"
559
+ match = re.search(multiline_pattern, example)
471
560
  if match:
472
561
  json_str = match.group(1)
473
562
  # Validate it's JSON by trying to parse it
474
563
  json.loads(json_str)
475
564
  return json_str
476
565
 
477
- # Look for multi-line JSON (between '{ and }')
478
- multiline_pattern = r"'\{([\s\S]*?)\}'"
479
- match = re.search(multiline_pattern, example)
566
+ # Fallback to simple single-line JSON in single quotes
567
+ single_quote_pattern = r"catocli[^']*'([^']+)'"
568
+ match = re.search(single_quote_pattern, example)
480
569
  if match:
481
- json_str = "{" + match.group(1) + "}"
570
+ json_str = match.group(1)
482
571
  # Validate it's JSON by trying to parse it
483
572
  json.loads(json_str)
484
573
  return json_str
catocli/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "3.0.26"
1
+ __version__ = "3.0.30"
2
2
  __cato_host__ = "https://api.catonetworks.com/api/v1/graphql2"
@@ -7,7 +7,8 @@ from graphql_client.api_client import ApiException
7
7
  from ..customLib import writeDataToFile, makeCall, getAccountID
8
8
 
9
9
  def strip_ids_recursive(data):
10
- """Recursively strip id attributes from data structure, but only from objects that contain only 'id' and 'name' keys"""
10
+ """Recursively strip id attributes from data structure, but only from objects that contain only 'id' and 'name' keys.
11
+ If a name contains a backslash character, keep the id and remove the name instead."""
11
12
  try:
12
13
  if isinstance(data, dict):
13
14
  # Check if this dict should have its 'id' stripped
@@ -15,10 +16,19 @@ def strip_ids_recursive(data):
15
16
  dict_keys = set(data.keys())
16
17
  should_strip_id = dict_keys == {'id', 'name'} or dict_keys == {'name', 'id'}
17
18
 
19
+ # Check if name contains backslash
20
+ name_has_backslash = should_strip_id and 'name' in data and isinstance(data['name'], str) and '\\' in data['name']
21
+
18
22
  result = {}
19
23
  for k, v in data.items():
20
- if k == 'id' and should_strip_id:
21
- # Skip this 'id' key only if this object contains only id and name
24
+ if name_has_backslash:
25
+ # If name contains backslash, keep id and skip name
26
+ if k == 'name':
27
+ continue
28
+ else:
29
+ result[k] = strip_ids_recursive(v)
30
+ elif k == 'id' and should_strip_id:
31
+ # Normal case: Skip id key if this object contains only id and name
22
32
  continue
23
33
  else:
24
34
  # Keep the key and recursively process the value
@@ -262,6 +262,9 @@ def createRequest(args, configuration):
262
262
  json_data
263
263
  )
264
264
  print(f"Sentinel API response code: {result_code}")
265
+
266
+ # Return None to prevent JSON output to stdout when streaming to network/sentinel
267
+ return None
265
268
 
266
269
  # Apply formatting based on effective format
267
270
  if effective_format == 'raw' or not default_override:
@@ -42,7 +42,10 @@ catocli query accountMetrics '{
42
42
  ### Additional Examples
43
43
  - Example all values and lables
44
44
  - Example all values and lables for a single account
45
- - Example all values and lables for a single user
45
+ - Monitor all key performance indicators for a specific site:
46
+ - Analyze network performance for specific users:
47
+ - Get a simple health snapshot without filters:
48
+ - Focus on bandwidth utilization with packet loss metrics
46
49
  - Last hour no filters
47
50
 
48
51
  # Example all values and lables
@@ -88,34 +91,78 @@ catocli query accountMetrics '{
88
91
  }'
89
92
  ```
90
93
 
91
- # Example all values and lables for a single user
94
+ # Monitor all key performance indicators for a specific site:
92
95
 
93
96
  ```bash
94
- # Example all values and lables for a single user
97
+ # Monitor all key performance indicators for a specific site:
95
98
  catocli query accountMetrics '{
96
99
  "buckets": 24,
100
+ "groupDevices": true,
101
+ "groupInterfaces": true,
97
102
  "labels": [
103
+ "bytesDownstream",
104
+ "bytesUpstream",
98
105
  "health",
99
- "jitterDownstream",
106
+ "lastMileLatency",
107
+ "lastMilePacketLoss",
108
+ "rtt"
109
+ ],
110
+ "siteIDs": ["132814"],
111
+ "timeFrame": "last.P1D",
112
+ "perSecond": true,
113
+ "toRate": true
114
+ }' -f csv --csv-filename=accountmetrics_site.csv
115
+ ```
116
+
117
+ # Analyze network performance for specific users:
118
+
119
+ ```bash
120
+ # Analyze network performance for specific users:
121
+ catocli query accountMetrics '{
122
+ "buckets": 24,
123
+ "labels": [
124
+ "health",
125
+ "jitterDownstream",
100
126
  "jitterUpstream",
101
127
  "lastMileLatency",
102
128
  "lastMilePacketLoss",
103
- "lostDownstream",
104
- "lostDownstreamPcnt",
105
- "lostUpstream",
106
- "lostUpstreamPcnt",
107
- "packetsDiscardedDownstream",
108
- "packetsDiscardedDownstreamPcnt",
109
- "packetsDiscardedUpstream",
110
- "packetsDiscardedUpstreamPcnt",
111
129
  "packetsDownstream",
112
130
  "packetsUpstream"
113
131
  ],
114
- "timeFrame": "last.PT1H",
115
- "userIDs": [
116
- "0"
117
- ]
118
- }'
132
+ "timeFrame": "last.P1D",
133
+ "userIDs": ["1000000"]
134
+ }' -f csv --csv-filename=accountmetrics_user.csv
135
+ ```
136
+
137
+ # Get a simple health snapshot without filters:
138
+
139
+ ```bash
140
+ # Get a simple health snapshot without filters:
141
+ catocli query accountMetrics '{
142
+ "timeFrame": "last.PT1H"
143
+ }' -f csv --csv-filename=accountmetrics_health.csv
144
+ ```
145
+
146
+ # Focus on bandwidth utilization with packet loss metrics
147
+
148
+ ```bash
149
+ # Focus on bandwidth utilization with packet loss metrics
150
+ catocli query accountMetrics '{
151
+ "buckets": 48,
152
+ "labels": [
153
+ "bytesDownstream",
154
+ "bytesUpstream",
155
+ "bytesTotal",
156
+ "bytesDownstreamMax",
157
+ "bytesUpstreamMax",
158
+ "lostDownstreamPcnt",
159
+ "lostUpstreamPcnt"
160
+ ],
161
+ "siteIDs": ["132814"],
162
+ "timeFrame": "last.P2D",
163
+ "perSecond": true,
164
+ "withMissingData": true
165
+ }' -f csv --csv-filename=accountmetrics_packet_loss.csv
119
166
  ```
120
167
 
121
168
  # Last hour no filters