licos-platform-cli 0.2.14__tar.gz → 0.3.1__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.
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/.gitignore +3 -1
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/PKG-INFO +2 -2
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/pyproject.toml +2 -2
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/help_commands.py +209 -20
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/main.py +4 -2
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/oauth_commands.py +6 -6
- licos_platform_cli-0.3.1/src/licos_platform_cli/uns_commands.py +427 -0
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/tests/test_cli.py +320 -16
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/__init__.py +0 -0
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/database_commands.py +0 -0
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/knowledge_commands.py +0 -0
- {licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/storage_commands.py +0 -0
|
@@ -12,6 +12,7 @@ packages/*/dist/
|
|
|
12
12
|
.vscode/
|
|
13
13
|
*.swp
|
|
14
14
|
*.swo
|
|
15
|
+
*.pyc
|
|
15
16
|
|
|
16
17
|
# OS
|
|
17
18
|
.DS_Store
|
|
@@ -33,12 +34,13 @@ crates/industrial/industrial-stack.env
|
|
|
33
34
|
# Build
|
|
34
35
|
*.log
|
|
35
36
|
*.pid
|
|
36
|
-
crates/industrial/bin/
|
|
37
37
|
.licos
|
|
38
38
|
.tmp
|
|
39
39
|
.playwright-cli
|
|
40
40
|
|
|
41
41
|
/tmp
|
|
42
|
+
crates/industrial/bin/
|
|
43
|
+
|
|
42
44
|
tools/android-sdk-cache/*.zip
|
|
43
45
|
tools/codegraph-cache/*.tgz
|
|
44
46
|
tools/codegraph-cache/*.tgz
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "licos-platform-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "LICOS platform CLI"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"licos-platform-sdk>=0.
|
|
11
|
+
"licos-platform-sdk>=0.4.1",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
{licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/help_commands.py
RENAMED
|
@@ -451,7 +451,7 @@ KNOWLEDGE_HELP = {
|
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
|
|
454
|
-
OAUTH_HELP = {
|
|
454
|
+
OAUTH_HELP = {
|
|
455
455
|
"topic": "oauth",
|
|
456
456
|
"description": "Manage LICOS OAuth2/OIDC login applications from trusted project runtime code.",
|
|
457
457
|
"runtime": RUNTIME_ENV,
|
|
@@ -495,14 +495,201 @@ OAUTH_HELP = {
|
|
|
495
495
|
"grants": {"description": "List application grants.", "parameters": [], "example": "licos-platform oauth grants app-id"},
|
|
496
496
|
"revoke-grant": {"description": "Revoke one grant.", "parameters": [], "example": "licos-platform oauth revoke-grant grant-id"},
|
|
497
497
|
},
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
UNS_HELP = {
|
|
502
|
+
"topic": "uns",
|
|
503
|
+
"description": "Project-scoped UNS generation, reconciliation, resource search, and business runtime access.",
|
|
504
|
+
"runtime": RUNTIME_ENV,
|
|
505
|
+
"rules": [
|
|
506
|
+
"Runtime identity selects the project; never pass tenant, workspace, project, external service URL, or token arguments.",
|
|
507
|
+
"Query only the blocks and protocols needed for the current document.",
|
|
508
|
+
"discover, validate, plan, and finalize are read-only for external systems; apply performs writes.",
|
|
509
|
+
"Run apply only after reviewing the current plan, and pass its exact documentHash and planHash with --confirm.",
|
|
510
|
+
"UNS.json must not contain platform IDs, external IDs, credentials, MQTT infrastructure IDs, or credential-bearing RTSP URLs.",
|
|
511
|
+
"Use resource-search before reference bindings; a reference selector must resolve exactly one existing resource.",
|
|
512
|
+
"Use either a current-project node code or --path for business access. Data writes/deletions, point control, and video control require explicit confirmation.",
|
|
513
|
+
],
|
|
514
|
+
"sdk": {
|
|
515
|
+
"python": "from licos_platform_sdk import uns\nplanned = uns.plan(document)\nresult = uns.apply(document, expected_document_hash=planned['documentHash'], expected_plan_hash=planned['planHash'], confirmed=True)",
|
|
516
|
+
},
|
|
517
|
+
"commands": {
|
|
518
|
+
"schema-current": {
|
|
519
|
+
"description": "Get the current complete UNS Schema and block index.",
|
|
520
|
+
"parameters": [],
|
|
521
|
+
"example": "licos-platform uns schema-current",
|
|
522
|
+
},
|
|
523
|
+
"schema": {
|
|
524
|
+
"description": "Get a complete versioned UNS Schema.",
|
|
525
|
+
"parameters": [{"name": "--version", "required": False, "description": "Schema version, default 1.0."}],
|
|
526
|
+
"example": "licos-platform uns schema --version 1.0",
|
|
527
|
+
},
|
|
528
|
+
"schema-blocks": {
|
|
529
|
+
"description": "List available blocks for one Schema version.",
|
|
530
|
+
"parameters": [{"name": "--version", "required": False, "description": "Schema version, default 1.0."}],
|
|
531
|
+
"example": "licos-platform uns schema-blocks",
|
|
532
|
+
},
|
|
533
|
+
"schema-block": {
|
|
534
|
+
"description": "Get one block and optionally its direct dependencies.",
|
|
535
|
+
"parameters": [
|
|
536
|
+
{"name": "block_code", "required": True, "description": "Block code such as collection-point or uns-node."},
|
|
537
|
+
{"name": "--depth", "required": False, "description": "Dependency depth from 0 to 3."},
|
|
538
|
+
],
|
|
539
|
+
"example": "licos-platform uns schema-block collection-point --depth 1",
|
|
540
|
+
},
|
|
541
|
+
"protocol-schema": {
|
|
542
|
+
"description": "Get one protocol-specific collection-point Schema and capability status.",
|
|
543
|
+
"parameters": [{"name": "protocol", "required": True, "description": "Protocol code such as opcua."}],
|
|
544
|
+
"example": "licos-platform uns protocol-schema opcua",
|
|
545
|
+
},
|
|
546
|
+
"discover": {
|
|
547
|
+
"description": "Read a redacted snapshot of reusable industrial resources for the runtime project.",
|
|
548
|
+
"parameters": [
|
|
549
|
+
{"name": "--resource-type", "required": False, "description": "Resource block to return; can repeat."},
|
|
550
|
+
{"name": "--protocol", "required": False, "description": "Protocol capability detail to query; can repeat."},
|
|
551
|
+
],
|
|
552
|
+
"example": "licos-platform uns discover --resource-type connections --protocol opcua",
|
|
553
|
+
},
|
|
554
|
+
"resource-search": {
|
|
555
|
+
"description": "Search reusable points, video channels, or other resources and return a manifestSelector without external IDs.",
|
|
556
|
+
"parameters": [
|
|
557
|
+
{"name": "--resource-type", "required": True, "description": "Resource type such as collectionPoints or videoChannels."},
|
|
558
|
+
{"name": "--protocol", "required": False, "description": "Optional protocol filter."},
|
|
559
|
+
{"name": "--keyword", "required": False, "description": "Name or business-code search text."},
|
|
560
|
+
{"name": "--selector-json", "required": False, "description": "Exact business selector JSON object."},
|
|
561
|
+
],
|
|
562
|
+
"example": "licos-platform uns resource-search --resource-type collectionPoints --protocol opcua --keyword TEMP",
|
|
563
|
+
},
|
|
564
|
+
"bindings": {
|
|
565
|
+
"description": "List project UNS node codes, field contracts, and available runtime capabilities.",
|
|
566
|
+
"parameters": [],
|
|
567
|
+
"example": "licos-platform uns bindings",
|
|
568
|
+
},
|
|
569
|
+
"path-resolve": {
|
|
570
|
+
"description": "Resolve an authorized absolute or relative enterprise UNS path.",
|
|
571
|
+
"parameters": [{"name": "path", "required": True, "description": "UNS absolute or relative path."}],
|
|
572
|
+
"example": "licos-platform uns path-resolve project:/AREA_01/TEMP_01",
|
|
573
|
+
},
|
|
574
|
+
"validate": {
|
|
575
|
+
"description": "Validate a local JSON document in the runtime project scope.",
|
|
576
|
+
"parameters": [{"name": "--file", "required": True, "description": "Local UNS JSON file."}],
|
|
577
|
+
"example": "licos-platform uns validate --file UNS.json.candidate",
|
|
578
|
+
},
|
|
579
|
+
"plan": {
|
|
580
|
+
"description": "Create a deterministic read-only reconciliation plan for a local JSON document.",
|
|
581
|
+
"parameters": [{"name": "--file", "required": True, "description": "Local UNS JSON file."}],
|
|
582
|
+
"example": "licos-platform uns plan --file UNS.json",
|
|
583
|
+
},
|
|
584
|
+
"apply": {
|
|
585
|
+
"description": "Execute a previously reviewed plan after explicit confirmation and hash verification.",
|
|
586
|
+
"parameters": [
|
|
587
|
+
{"name": "--file", "required": True, "description": "The exact local UNS JSON document that was planned."},
|
|
588
|
+
{"name": "--document-hash", "required": True, "description": "documentHash returned by the reviewed plan."},
|
|
589
|
+
{"name": "--plan-hash", "required": True, "description": "planHash returned by the reviewed plan."},
|
|
590
|
+
{"name": "--confirm", "required": True, "description": "Explicitly confirm external writes."},
|
|
591
|
+
],
|
|
592
|
+
"example": "licos-platform uns apply --file UNS.json --document-hash <documentHash> --plan-hash <planHash> --confirm",
|
|
593
|
+
},
|
|
594
|
+
"finalize": {
|
|
595
|
+
"description": "Validate and atomically install a candidate as project-root UNS.json, revalidate it, and return a read-only plan.",
|
|
596
|
+
"parameters": [{"name": "--file", "required": True, "description": "Candidate JSON file inside LICOS_PROJECT_PATH."}],
|
|
597
|
+
"example": "licos-platform uns finalize --file .UNS.json.candidate",
|
|
598
|
+
},
|
|
599
|
+
"data-query": {
|
|
600
|
+
"description": "Query stored rows through a data or point UNS node.",
|
|
601
|
+
"parameters": [
|
|
602
|
+
{"name": "node_code", "required": False, "description": "Current-project UNS node code; mutually exclusive with --path."},
|
|
603
|
+
{"name": "--path", "required": False, "description": "Authorized UNS path; mutually exclusive with node_code."},
|
|
604
|
+
],
|
|
605
|
+
"example": "licos-platform uns data-query ORDERS --page-size 50",
|
|
606
|
+
},
|
|
607
|
+
"data-write": {
|
|
608
|
+
"description": "Write structured fields through a UNS node after explicit confirmation.",
|
|
609
|
+
"parameters": [
|
|
610
|
+
{"name": "node_code", "required": False, "description": "Current-project UNS node code; mutually exclusive with --path."},
|
|
611
|
+
{"name": "--path", "required": False, "description": "Authorized UNS path; mutually exclusive with node_code."},
|
|
612
|
+
{"name": "--fields-json", "required": True, "description": "Field-value JSON object."},
|
|
613
|
+
{"name": "--confirm", "required": True, "description": "Explicitly confirm the write."},
|
|
614
|
+
],
|
|
615
|
+
"example": "licos-platform uns data-write ORDERS --fields-json '{\"amount\":12}' --confirm",
|
|
616
|
+
},
|
|
617
|
+
"data-delete": {
|
|
618
|
+
"description": "Delete exact rows returned by a prior UNS query after explicit confirmation.",
|
|
619
|
+
"parameters": [
|
|
620
|
+
{"name": "node_code", "required": False, "description": "Current-project UNS node code; mutually exclusive with --path."},
|
|
621
|
+
{"name": "--path", "required": False, "description": "Authorized UNS path; mutually exclusive with node_code."},
|
|
622
|
+
{"name": "--rows-json", "required": True, "description": "Non-empty JSON array containing exact queried row objects."},
|
|
623
|
+
{"name": "--confirm", "required": True, "description": "Explicitly confirm the deletion."},
|
|
624
|
+
],
|
|
625
|
+
"example": "licos-platform uns data-delete ORDERS --rows-json '[{\"orderNo\":\"SO-1\"}]' --confirm",
|
|
626
|
+
},
|
|
627
|
+
"point-current": {
|
|
628
|
+
"description": "Read the current value of a bound UNS point.",
|
|
629
|
+
"parameters": [
|
|
630
|
+
{"name": "node_code", "required": False, "description": "Current-project point code; mutually exclusive with --path."},
|
|
631
|
+
{"name": "--path", "required": False, "description": "Authorized point path; mutually exclusive with node_code."},
|
|
632
|
+
],
|
|
633
|
+
"example": "licos-platform uns point-current TEMP_01",
|
|
634
|
+
},
|
|
635
|
+
"point-history": {
|
|
636
|
+
"description": "Read an ISO-8601 time range of historical point values.",
|
|
637
|
+
"parameters": [
|
|
638
|
+
{"name": "node_code", "required": False, "description": "Current-project point code; mutually exclusive with --path."},
|
|
639
|
+
{"name": "--path", "required": False, "description": "Authorized point path; mutually exclusive with node_code."},
|
|
640
|
+
{"name": "--start-time / --end-time", "required": True, "description": "UTC ISO-8601 range."},
|
|
641
|
+
],
|
|
642
|
+
"example": "licos-platform uns point-history TEMP_01 --start-time 2026-07-13T00:00:00Z --end-time 2026-07-14T00:00:00Z",
|
|
643
|
+
},
|
|
644
|
+
"point-write": {
|
|
645
|
+
"description": "Dry-run or explicitly confirm a writable point control with audit reason and ticket.",
|
|
646
|
+
"parameters": [
|
|
647
|
+
{"name": "node_code", "required": False, "description": "Current-project point code; mutually exclusive with --path."},
|
|
648
|
+
{"name": "--path", "required": False, "description": "Authorized point path; mutually exclusive with node_code."},
|
|
649
|
+
{"name": "--value-json", "required": True, "description": "Typed JSON scalar or value."},
|
|
650
|
+
{"name": "--reason / --ticket", "required": True, "description": "Audit context."},
|
|
651
|
+
{"name": "--request-id", "required": False, "description": "Idempotency/correlation key; generated by the platform when omitted."},
|
|
652
|
+
{"name": "--dry-run / --confirm", "required": True, "description": "Dry-run or explicit execution confirmation."},
|
|
653
|
+
],
|
|
654
|
+
"example": "licos-platform uns point-write MOTOR_START --value-json true --reason 'start line' --ticket WO-100 --dry-run",
|
|
655
|
+
},
|
|
656
|
+
"video-stream": {
|
|
657
|
+
"description": "Get credential-safe playback options for a UNS video node.",
|
|
658
|
+
"parameters": [
|
|
659
|
+
{"name": "node_code", "required": False, "description": "Current-project video code; mutually exclusive with --path."},
|
|
660
|
+
{"name": "--path", "required": False, "description": "Authorized video path; mutually exclusive with node_code."},
|
|
661
|
+
],
|
|
662
|
+
"example": "licos-platform uns video-stream CAMERA_01",
|
|
663
|
+
},
|
|
664
|
+
"video-snapshot": {
|
|
665
|
+
"description": "Capture a snapshot from a UNS video node.",
|
|
666
|
+
"parameters": [
|
|
667
|
+
{"name": "node_code", "required": False, "description": "Current-project video code; mutually exclusive with --path."},
|
|
668
|
+
{"name": "--path", "required": False, "description": "Authorized video path; mutually exclusive with node_code."},
|
|
669
|
+
],
|
|
670
|
+
"example": "licos-platform uns video-snapshot CAMERA_01",
|
|
671
|
+
},
|
|
672
|
+
"video-control": {
|
|
673
|
+
"description": "Execute an explicitly confirmed PTZ or preset action.",
|
|
674
|
+
"parameters": [
|
|
675
|
+
{"name": "node_code", "required": False, "description": "Current-project video code; mutually exclusive with --path."},
|
|
676
|
+
{"name": "--path", "required": False, "description": "Authorized video path; mutually exclusive with node_code."},
|
|
677
|
+
{"name": "--action", "required": True, "description": "move, stop, preset-set, or preset-goto."},
|
|
678
|
+
{"name": "--parameters-json", "required": False, "description": "Action-specific JSON parameters."},
|
|
679
|
+
{"name": "--confirm", "required": True, "description": "Explicitly confirm video control."},
|
|
680
|
+
],
|
|
681
|
+
"example": "licos-platform uns video-control CAMERA_01 --action move --parameters-json '{\"direction\":\"left\"}' --confirm",
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
HELP_TOPICS = {
|
|
502
688
|
"storage": STORAGE_HELP,
|
|
503
689
|
"database": DATABASE_HELP,
|
|
504
690
|
"knowledge": KNOWLEDGE_HELP,
|
|
505
|
-
"oauth": OAUTH_HELP,
|
|
691
|
+
"oauth": OAUTH_HELP,
|
|
692
|
+
"uns": UNS_HELP,
|
|
506
693
|
}
|
|
507
694
|
|
|
508
695
|
|
|
@@ -514,20 +701,22 @@ def _cmd_help(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
514
701
|
if not args.topic:
|
|
515
702
|
return {
|
|
516
703
|
"description": "Structured help for LICOS platform CLI topics.",
|
|
517
|
-
"usage": [
|
|
518
|
-
"licos-platform help storage",
|
|
519
|
-
"licos-platform help storage upload-file",
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
704
|
+
"usage": [
|
|
705
|
+
"licos-platform help storage",
|
|
706
|
+
"licos-platform help storage upload-file",
|
|
707
|
+
"licos-platform help database",
|
|
708
|
+
"licos-platform help database query",
|
|
709
|
+
"licos-platform help database export-orm",
|
|
710
|
+
"licos-platform help database studio-ensure",
|
|
711
|
+
"licos-platform help knowledge",
|
|
712
|
+
"licos-platform help knowledge search",
|
|
713
|
+
"licos-platform help oauth",
|
|
714
|
+
"licos-platform help oauth configure-project",
|
|
715
|
+
"licos-platform help uns",
|
|
716
|
+
"licos-platform help uns protocol-schema",
|
|
717
|
+
],
|
|
718
|
+
"topics": sorted(HELP_TOPICS),
|
|
719
|
+
}
|
|
531
720
|
topic = HELP_TOPICS[args.topic]
|
|
532
721
|
if not args.command:
|
|
533
722
|
return topic
|
|
@@ -11,7 +11,8 @@ from .database_commands import add_database_parser
|
|
|
11
11
|
from .help_commands import add_help_parser
|
|
12
12
|
from .knowledge_commands import add_knowledge_parser
|
|
13
13
|
from .oauth_commands import add_oauth_parser
|
|
14
|
-
from .storage_commands import add_storage_parser
|
|
14
|
+
from .storage_commands import add_storage_parser
|
|
15
|
+
from .uns_commands import add_uns_parser
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _print_json(value: Any) -> None:
|
|
@@ -33,7 +34,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
33
34
|
add_storage_parser(subparsers)
|
|
34
35
|
add_database_parser(subparsers)
|
|
35
36
|
add_knowledge_parser(subparsers)
|
|
36
|
-
add_oauth_parser(subparsers)
|
|
37
|
+
add_oauth_parser(subparsers)
|
|
38
|
+
add_uns_parser(subparsers)
|
|
37
39
|
return parser
|
|
38
40
|
|
|
39
41
|
|
{licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/oauth_commands.py
RENAMED
|
@@ -137,12 +137,12 @@ def _cmd_revoke_grant(args: argparse.Namespace) -> Any:
|
|
|
137
137
|
return oauth.revoke_grant(args.grant_id)
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
def _cmd_configure_project(args: argparse.Namespace) -> dict[str, Any]:
|
|
141
|
-
public_base_url = (
|
|
142
|
-
args.public_base_url
|
|
143
|
-
or os.environ.get("LICOS_PLATFORM_PUBLIC_BASE_URL")
|
|
144
|
-
or os.environ.get("LICOS_OAUTH_PUBLIC_BASE_URL")
|
|
145
|
-
)
|
|
140
|
+
def _cmd_configure_project(args: argparse.Namespace) -> dict[str, Any]:
|
|
141
|
+
public_base_url = (
|
|
142
|
+
args.public_base_url
|
|
143
|
+
or os.environ.get("LICOS_PLATFORM_PUBLIC_BASE_URL")
|
|
144
|
+
or os.environ.get("LICOS_OAUTH_PUBLIC_BASE_URL")
|
|
145
|
+
)
|
|
146
146
|
if not public_base_url:
|
|
147
147
|
raise argparse.ArgumentTypeError(
|
|
148
148
|
"--public-base-url, LICOS_PLATFORM_PUBLIC_BASE_URL, or LICOS_OAUTH_PUBLIC_BASE_URL is required"
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from licos_platform_sdk import PlatformSdkError, uns
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Handler = Callable[[argparse.Namespace], Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
|
|
17
|
+
parser.set_defaults(handler=handler)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _document(path: str) -> dict[str, Any]:
|
|
21
|
+
file_path = Path(path)
|
|
22
|
+
try:
|
|
23
|
+
payload = json.loads(file_path.read_text(encoding="utf-8"))
|
|
24
|
+
except OSError as exc:
|
|
25
|
+
raise argparse.ArgumentTypeError(f"cannot read UNS file: {exc}") from exc
|
|
26
|
+
except json.JSONDecodeError as exc:
|
|
27
|
+
raise argparse.ArgumentTypeError(
|
|
28
|
+
f"invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
|
|
29
|
+
) from exc
|
|
30
|
+
if not isinstance(payload, dict):
|
|
31
|
+
raise argparse.ArgumentTypeError("UNS document root must be an object")
|
|
32
|
+
return payload
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _json_object(value: str | None, label: str) -> dict[str, Any]:
|
|
36
|
+
if value is None:
|
|
37
|
+
return {}
|
|
38
|
+
try:
|
|
39
|
+
payload = json.loads(value)
|
|
40
|
+
except json.JSONDecodeError as exc:
|
|
41
|
+
raise argparse.ArgumentTypeError(f"{label} must be valid JSON: {exc.msg}") from exc
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
raise argparse.ArgumentTypeError(f"{label} must be a JSON object")
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _json_value(value: str, label: str) -> Any:
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(value)
|
|
50
|
+
except json.JSONDecodeError as exc:
|
|
51
|
+
raise argparse.ArgumentTypeError(f"{label} must be valid JSON: {exc.msg}") from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _json_object_array(value: str, label: str) -> list[dict[str, Any]]:
|
|
55
|
+
payload = _json_value(value, label)
|
|
56
|
+
if not isinstance(payload, list) or not payload:
|
|
57
|
+
raise argparse.ArgumentTypeError(f"{label} must be a non-empty JSON array")
|
|
58
|
+
if any(not isinstance(item, dict) or not item for item in payload):
|
|
59
|
+
raise argparse.ArgumentTypeError(f"{label} must contain only non-empty JSON objects")
|
|
60
|
+
return payload
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cmd_schema_current(_args: argparse.Namespace) -> Any:
|
|
64
|
+
return uns.schema_current()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cmd_schema(args: argparse.Namespace) -> Any:
|
|
68
|
+
return uns.schema(args.version)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cmd_schema_blocks(args: argparse.Namespace) -> Any:
|
|
72
|
+
return uns.schema_blocks(args.version)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cmd_schema_block(args: argparse.Namespace) -> Any:
|
|
76
|
+
return uns.schema_block(args.block_code, version=args.version, depth=args.depth)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cmd_protocol_schema(args: argparse.Namespace) -> Any:
|
|
80
|
+
return uns.protocol_schema(args.protocol, version=args.version)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cmd_discover(args: argparse.Namespace) -> Any:
|
|
84
|
+
return uns.discover(resource_types=args.resource_types, protocols=args.protocols)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cmd_resource_search(args: argparse.Namespace) -> Any:
|
|
88
|
+
return uns.search_resources(
|
|
89
|
+
args.resource_type,
|
|
90
|
+
protocol=args.protocol,
|
|
91
|
+
keyword=args.keyword,
|
|
92
|
+
selector=_json_object(args.selector_json, "selector"),
|
|
93
|
+
page=args.page,
|
|
94
|
+
page_size=args.page_size,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cmd_bindings(_args: argparse.Namespace) -> Any:
|
|
99
|
+
return uns.bindings()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cmd_path_resolve(args: argparse.Namespace) -> Any:
|
|
103
|
+
return uns.resolve_path(args.path)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _target(args: argparse.Namespace) -> tuple[str, bool]:
|
|
107
|
+
node_code = getattr(args, "node_code", None)
|
|
108
|
+
path = getattr(args, "path", None)
|
|
109
|
+
if bool(node_code) == bool(path):
|
|
110
|
+
raise argparse.ArgumentTypeError("provide exactly one node_code or --path")
|
|
111
|
+
return (path, True) if path else (node_code, False)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _cmd_validate(args: argparse.Namespace) -> Any:
|
|
115
|
+
return uns.validate(_document(args.file))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _cmd_plan(args: argparse.Namespace) -> Any:
|
|
119
|
+
return uns.plan(_document(args.file))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cmd_apply(args: argparse.Namespace) -> Any:
|
|
123
|
+
if not args.confirm:
|
|
124
|
+
raise argparse.ArgumentTypeError("UNS apply requires --confirm")
|
|
125
|
+
if not args.document_hash.strip() or not args.plan_hash.strip():
|
|
126
|
+
raise argparse.ArgumentTypeError("UNS apply requires non-empty document and plan hashes")
|
|
127
|
+
return uns.apply(
|
|
128
|
+
_document(args.file),
|
|
129
|
+
expected_document_hash=args.document_hash,
|
|
130
|
+
expected_plan_hash=args.plan_hash,
|
|
131
|
+
confirmed=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cmd_data_query(args: argparse.Namespace) -> Any:
|
|
136
|
+
target, by_path = _target(args)
|
|
137
|
+
handler = uns.query_path_data if by_path else uns.query_node_data
|
|
138
|
+
return handler(
|
|
139
|
+
target,
|
|
140
|
+
page_num=args.page_num,
|
|
141
|
+
page_size=args.page_size,
|
|
142
|
+
pagination_mode=args.pagination_mode,
|
|
143
|
+
page_token=args.page_token,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cmd_data_write(args: argparse.Namespace) -> Any:
|
|
148
|
+
if not args.confirm:
|
|
149
|
+
raise argparse.ArgumentTypeError("UNS data write requires --confirm")
|
|
150
|
+
target, by_path = _target(args)
|
|
151
|
+
handler = uns.write_path_data if by_path else uns.write_node_data
|
|
152
|
+
return handler(
|
|
153
|
+
target,
|
|
154
|
+
_json_object(args.fields_json, "field values"),
|
|
155
|
+
confirmed=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cmd_data_delete(args: argparse.Namespace) -> Any:
|
|
160
|
+
if not args.confirm:
|
|
161
|
+
raise argparse.ArgumentTypeError("UNS data deletion requires --confirm")
|
|
162
|
+
target, by_path = _target(args)
|
|
163
|
+
handler = uns.delete_path_data if by_path else uns.delete_node_data
|
|
164
|
+
return handler(
|
|
165
|
+
target,
|
|
166
|
+
_json_object_array(args.rows_json, "rows"),
|
|
167
|
+
confirmed=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_point_current(args: argparse.Namespace) -> Any:
|
|
172
|
+
target, by_path = _target(args)
|
|
173
|
+
return uns.path_point_current(target) if by_path else uns.point_current(target)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _cmd_point_history(args: argparse.Namespace) -> Any:
|
|
177
|
+
target, by_path = _target(args)
|
|
178
|
+
handler = uns.path_point_history if by_path else uns.point_history
|
|
179
|
+
return handler(
|
|
180
|
+
target,
|
|
181
|
+
start_time=args.start_time,
|
|
182
|
+
end_time=args.end_time,
|
|
183
|
+
page=args.page,
|
|
184
|
+
page_size=args.page_size,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cmd_point_write(args: argparse.Namespace) -> Any:
|
|
189
|
+
if not args.dry_run and not args.confirm:
|
|
190
|
+
raise argparse.ArgumentTypeError("UNS point write requires --confirm unless --dry-run is used")
|
|
191
|
+
target, by_path = _target(args)
|
|
192
|
+
handler = uns.write_path_point if by_path else uns.write_point
|
|
193
|
+
return handler(
|
|
194
|
+
target,
|
|
195
|
+
_json_value(args.value_json, "point value"),
|
|
196
|
+
reason=args.reason,
|
|
197
|
+
ticket=args.ticket,
|
|
198
|
+
request_id=args.request_id,
|
|
199
|
+
dry_run=args.dry_run,
|
|
200
|
+
confirmed=args.confirm,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cmd_video_stream(args: argparse.Namespace) -> Any:
|
|
205
|
+
target, by_path = _target(args)
|
|
206
|
+
return uns.path_video_stream_options(target) if by_path else uns.video_stream_options(target)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _cmd_video_snapshot(args: argparse.Namespace) -> Any:
|
|
210
|
+
target, by_path = _target(args)
|
|
211
|
+
return uns.path_video_snapshot(target) if by_path else uns.video_snapshot(target)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _cmd_video_control(args: argparse.Namespace) -> Any:
|
|
215
|
+
if not args.confirm:
|
|
216
|
+
raise argparse.ArgumentTypeError("UNS video control requires --confirm")
|
|
217
|
+
target, by_path = _target(args)
|
|
218
|
+
handler = uns.control_path_video if by_path else uns.control_video
|
|
219
|
+
return handler(
|
|
220
|
+
target,
|
|
221
|
+
args.action,
|
|
222
|
+
parameters=_json_object(args.parameters_json, "video control parameters"),
|
|
223
|
+
confirmed=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _cmd_finalize(args: argparse.Namespace) -> Any:
|
|
228
|
+
project_root = Path(os.environ.get("LICOS_PROJECT_PATH", "/workspace/projects")).resolve()
|
|
229
|
+
candidate = Path(args.file).resolve()
|
|
230
|
+
target = project_root / "UNS.json"
|
|
231
|
+
if not candidate.is_relative_to(project_root):
|
|
232
|
+
raise argparse.ArgumentTypeError("candidate file must be inside LICOS_PROJECT_PATH")
|
|
233
|
+
if candidate == target:
|
|
234
|
+
raise argparse.ArgumentTypeError("candidate file must not already be UNS.json")
|
|
235
|
+
|
|
236
|
+
document = _document(str(candidate))
|
|
237
|
+
validation = uns.validate(document)
|
|
238
|
+
if not validation.get("valid"):
|
|
239
|
+
raise PlatformSdkError("candidate validation failed; inspect `licos-platform uns validate` output")
|
|
240
|
+
|
|
241
|
+
project_root.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
previous = target.read_bytes() if target.exists() else None
|
|
243
|
+
staging_path: Path | None = None
|
|
244
|
+
target_replaced = False
|
|
245
|
+
try:
|
|
246
|
+
with tempfile.NamedTemporaryFile(
|
|
247
|
+
mode="wb", prefix=".UNS.json.", suffix=".tmp", dir=project_root, delete=False
|
|
248
|
+
) as staging:
|
|
249
|
+
staging.write(candidate.read_bytes())
|
|
250
|
+
staging.flush()
|
|
251
|
+
os.fsync(staging.fileno())
|
|
252
|
+
staging_path = Path(staging.name)
|
|
253
|
+
os.replace(staging_path, target)
|
|
254
|
+
staging_path = None
|
|
255
|
+
target_replaced = True
|
|
256
|
+
|
|
257
|
+
final_document = _document(str(target))
|
|
258
|
+
final_validation = uns.validate(final_document)
|
|
259
|
+
if not final_validation.get("valid"):
|
|
260
|
+
raise PlatformSdkError("final UNS.json validation failed; the previous file was restored")
|
|
261
|
+
plan = uns.plan(final_document)
|
|
262
|
+
candidate.unlink(missing_ok=True)
|
|
263
|
+
return {
|
|
264
|
+
"installed": True,
|
|
265
|
+
"target": "UNS.json",
|
|
266
|
+
"validation": final_validation,
|
|
267
|
+
"plan": plan,
|
|
268
|
+
}
|
|
269
|
+
except BaseException:
|
|
270
|
+
if staging_path is not None:
|
|
271
|
+
staging_path.unlink(missing_ok=True)
|
|
272
|
+
if target_replaced:
|
|
273
|
+
_restore_target(target, previous)
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _restore_target(target: Path, previous: bytes | None) -> None:
|
|
278
|
+
if previous is None:
|
|
279
|
+
target.unlink(missing_ok=True)
|
|
280
|
+
return
|
|
281
|
+
with tempfile.NamedTemporaryFile(
|
|
282
|
+
mode="wb", prefix=".UNS.restore.", suffix=".tmp", dir=target.parent, delete=False
|
|
283
|
+
) as restore:
|
|
284
|
+
restore.write(previous)
|
|
285
|
+
restore.flush()
|
|
286
|
+
os.fsync(restore.fileno())
|
|
287
|
+
restore_path = Path(restore.name)
|
|
288
|
+
os.replace(restore_path, target)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def add_uns_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
292
|
+
parser = subparsers.add_parser("uns", help="Inspect, validate, plan, and apply project UNS manifests")
|
|
293
|
+
commands = parser.add_subparsers(dest="uns_command", required=True)
|
|
294
|
+
|
|
295
|
+
_set_handler(commands.add_parser("schema-current", help="Get the current UNS Schema"), _cmd_schema_current)
|
|
296
|
+
|
|
297
|
+
schema = commands.add_parser("schema", help="Get a complete versioned UNS Schema")
|
|
298
|
+
schema.add_argument("--version", default="1.0")
|
|
299
|
+
_set_handler(schema, _cmd_schema)
|
|
300
|
+
|
|
301
|
+
blocks = commands.add_parser("schema-blocks", help="List versioned UNS Schema blocks")
|
|
302
|
+
blocks.add_argument("--version", default="1.0")
|
|
303
|
+
_set_handler(blocks, _cmd_schema_blocks)
|
|
304
|
+
|
|
305
|
+
block = commands.add_parser("schema-block", help="Get one UNS Schema block")
|
|
306
|
+
block.add_argument("block_code")
|
|
307
|
+
block.add_argument("--version", default="1.0")
|
|
308
|
+
block.add_argument("--depth", type=int, default=1, choices=range(0, 4))
|
|
309
|
+
_set_handler(block, _cmd_schema_block)
|
|
310
|
+
|
|
311
|
+
protocol = commands.add_parser("protocol-schema", help="Get one collection-point protocol Schema")
|
|
312
|
+
protocol.add_argument("protocol")
|
|
313
|
+
protocol.add_argument("--version", default="1.0")
|
|
314
|
+
_set_handler(protocol, _cmd_protocol_schema)
|
|
315
|
+
|
|
316
|
+
discover = commands.add_parser("discover", help="Discover reusable project industrial resources without writes")
|
|
317
|
+
discover.add_argument("--resource-type", action="append", dest="resource_types", default=None)
|
|
318
|
+
discover.add_argument("--protocol", action="append", dest="protocols", default=None)
|
|
319
|
+
_set_handler(discover, _cmd_discover)
|
|
320
|
+
|
|
321
|
+
search = commands.add_parser("resource-search", help="Search reusable resources with a manifest selector")
|
|
322
|
+
search.add_argument("--resource-type", required=True)
|
|
323
|
+
search.add_argument("--protocol")
|
|
324
|
+
search.add_argument("--keyword")
|
|
325
|
+
search.add_argument("--selector-json")
|
|
326
|
+
search.add_argument("--page", type=int, default=1)
|
|
327
|
+
search.add_argument("--page-size", type=int, default=20)
|
|
328
|
+
_set_handler(search, _cmd_resource_search)
|
|
329
|
+
|
|
330
|
+
_set_handler(commands.add_parser("bindings", help="List project UNS business bindings"), _cmd_bindings)
|
|
331
|
+
|
|
332
|
+
path_resolve = commands.add_parser(
|
|
333
|
+
"path-resolve", help="Resolve an absolute or relative enterprise UNS path"
|
|
334
|
+
)
|
|
335
|
+
path_resolve.add_argument("path")
|
|
336
|
+
_set_handler(path_resolve, _cmd_path_resolve)
|
|
337
|
+
|
|
338
|
+
validate = commands.add_parser("validate", help="Validate one local UNS JSON document")
|
|
339
|
+
validate.add_argument("--file", required=True)
|
|
340
|
+
_set_handler(validate, _cmd_validate)
|
|
341
|
+
|
|
342
|
+
plan = commands.add_parser("plan", help="Create a read-only reconciliation plan for one UNS JSON document")
|
|
343
|
+
plan.add_argument("--file", required=True)
|
|
344
|
+
_set_handler(plan, _cmd_plan)
|
|
345
|
+
|
|
346
|
+
apply_command = commands.add_parser(
|
|
347
|
+
"apply", help="Apply one previously reviewed UNS reconciliation plan"
|
|
348
|
+
)
|
|
349
|
+
apply_command.add_argument("--file", required=True)
|
|
350
|
+
apply_command.add_argument("--document-hash", required=True)
|
|
351
|
+
apply_command.add_argument("--plan-hash", required=True)
|
|
352
|
+
apply_command.add_argument("--confirm", action="store_true")
|
|
353
|
+
_set_handler(apply_command, _cmd_apply)
|
|
354
|
+
|
|
355
|
+
finalize = commands.add_parser(
|
|
356
|
+
"finalize", help="Validate and atomically install a candidate as project UNS.json, then create a plan"
|
|
357
|
+
)
|
|
358
|
+
finalize.add_argument("--file", required=True)
|
|
359
|
+
_set_handler(finalize, _cmd_finalize)
|
|
360
|
+
|
|
361
|
+
data_query = commands.add_parser("data-query", help="Query stored data through a UNS node")
|
|
362
|
+
data_query.add_argument("node_code", nargs="?")
|
|
363
|
+
data_query.add_argument("--path")
|
|
364
|
+
data_query.add_argument("--page-num", type=int, default=1)
|
|
365
|
+
data_query.add_argument("--page-size", type=int, default=50)
|
|
366
|
+
data_query.add_argument("--pagination-mode", choices=("OFFSET", "CURSOR"), default="OFFSET")
|
|
367
|
+
data_query.add_argument("--page-token")
|
|
368
|
+
_set_handler(data_query, _cmd_data_query)
|
|
369
|
+
|
|
370
|
+
data_write = commands.add_parser("data-write", help="Write structured data through a UNS node")
|
|
371
|
+
data_write.add_argument("node_code", nargs="?")
|
|
372
|
+
data_write.add_argument("--path")
|
|
373
|
+
data_write.add_argument("--fields-json", required=True)
|
|
374
|
+
data_write.add_argument("--confirm", action="store_true")
|
|
375
|
+
_set_handler(data_write, _cmd_data_write)
|
|
376
|
+
|
|
377
|
+
data_delete = commands.add_parser("data-delete", help="Delete exact stored rows through a UNS node")
|
|
378
|
+
data_delete.add_argument("node_code", nargs="?")
|
|
379
|
+
data_delete.add_argument("--path")
|
|
380
|
+
data_delete.add_argument("--rows-json", required=True)
|
|
381
|
+
data_delete.add_argument("--confirm", action="store_true")
|
|
382
|
+
_set_handler(data_delete, _cmd_data_delete)
|
|
383
|
+
|
|
384
|
+
point_current = commands.add_parser("point-current", help="Read the current value of a UNS point")
|
|
385
|
+
point_current.add_argument("node_code", nargs="?")
|
|
386
|
+
point_current.add_argument("--path")
|
|
387
|
+
_set_handler(point_current, _cmd_point_current)
|
|
388
|
+
|
|
389
|
+
point_history = commands.add_parser("point-history", help="Read historical values of a UNS point")
|
|
390
|
+
point_history.add_argument("node_code", nargs="?")
|
|
391
|
+
point_history.add_argument("--path")
|
|
392
|
+
point_history.add_argument("--start-time", required=True)
|
|
393
|
+
point_history.add_argument("--end-time", required=True)
|
|
394
|
+
point_history.add_argument("--page", type=int, default=1)
|
|
395
|
+
point_history.add_argument("--page-size", type=int, default=100)
|
|
396
|
+
_set_handler(point_history, _cmd_point_history)
|
|
397
|
+
|
|
398
|
+
point_write = commands.add_parser("point-write", help="Write or dry-run a controllable UNS point")
|
|
399
|
+
point_write.add_argument("node_code", nargs="?")
|
|
400
|
+
point_write.add_argument("--path")
|
|
401
|
+
point_write.add_argument("--value-json", required=True)
|
|
402
|
+
point_write.add_argument("--reason", required=True)
|
|
403
|
+
point_write.add_argument("--ticket", required=True)
|
|
404
|
+
point_write.add_argument("--request-id")
|
|
405
|
+
point_write.add_argument("--dry-run", action="store_true")
|
|
406
|
+
point_write.add_argument("--confirm", action="store_true")
|
|
407
|
+
_set_handler(point_write, _cmd_point_write)
|
|
408
|
+
|
|
409
|
+
video_stream = commands.add_parser("video-stream", help="Get playback options for a UNS video node")
|
|
410
|
+
video_stream.add_argument("node_code", nargs="?")
|
|
411
|
+
video_stream.add_argument("--path")
|
|
412
|
+
_set_handler(video_stream, _cmd_video_stream)
|
|
413
|
+
|
|
414
|
+
video_snapshot = commands.add_parser("video-snapshot", help="Capture a snapshot from a UNS video node")
|
|
415
|
+
video_snapshot.add_argument("node_code", nargs="?")
|
|
416
|
+
video_snapshot.add_argument("--path")
|
|
417
|
+
_set_handler(video_snapshot, _cmd_video_snapshot)
|
|
418
|
+
|
|
419
|
+
video_control = commands.add_parser("video-control", help="Run an explicitly confirmed video control")
|
|
420
|
+
video_control.add_argument("node_code", nargs="?")
|
|
421
|
+
video_control.add_argument("--path")
|
|
422
|
+
video_control.add_argument(
|
|
423
|
+
"--action", required=True, choices=("move", "stop", "preset-set", "preset-goto")
|
|
424
|
+
)
|
|
425
|
+
video_control.add_argument("--parameters-json")
|
|
426
|
+
video_control.add_argument("--confirm", action="store_true")
|
|
427
|
+
_set_handler(video_control, _cmd_video_control)
|
|
@@ -14,8 +14,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk
|
|
|
14
14
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
15
15
|
|
|
16
16
|
from licos_platform_cli import main as cli_main
|
|
17
|
-
from licos_platform_cli import database_commands, knowledge_commands, oauth_commands, storage_commands
|
|
18
|
-
from licos_platform_sdk import ApiError
|
|
17
|
+
from licos_platform_cli import database_commands, knowledge_commands, oauth_commands, storage_commands, uns_commands
|
|
18
|
+
from licos_platform_sdk import ApiError, PlatformSdkError
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def run_cli(*args: str) -> tuple[int, str, str]:
|
|
@@ -46,7 +46,7 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
46
46
|
self.assertEqual(code, 0)
|
|
47
47
|
self.assertEqual(stderr, "")
|
|
48
48
|
payload = json.loads(stdout)
|
|
49
|
-
self.assertEqual(payload["topics"], ["database", "knowledge", "oauth", "storage"])
|
|
49
|
+
self.assertEqual(payload["topics"], ["database", "knowledge", "oauth", "storage", "uns"])
|
|
50
50
|
self.assertIn("licos-platform help storage", payload["usage"])
|
|
51
51
|
|
|
52
52
|
def test_structured_help_storage_command_describes_parameters(self) -> None:
|
|
@@ -74,17 +74,321 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
74
74
|
self.assertIn("studio-ensure", payload["commands"])
|
|
75
75
|
self.assertNotIn("LICOS_DATABASE_ENVIRONMENT", {item["name"] for item in payload["runtime"]})
|
|
76
76
|
|
|
77
|
-
def test_structured_help_knowledge_topic_describes_default_search(self) -> None:
|
|
77
|
+
def test_structured_help_knowledge_topic_describes_default_search(self) -> None:
|
|
78
78
|
code, stdout, stderr = run_cli("help", "knowledge")
|
|
79
79
|
|
|
80
80
|
self.assertEqual(code, 0)
|
|
81
81
|
self.assertEqual(stderr, "")
|
|
82
82
|
payload = json.loads(stdout)
|
|
83
83
|
self.assertEqual(payload["topic"], "knowledge")
|
|
84
|
-
self.assertIn("search", payload["commands"])
|
|
85
|
-
self.assertTrue(any("Search all accessible datasets by default" in rule for rule in payload["rules"]))
|
|
86
|
-
|
|
87
|
-
def
|
|
84
|
+
self.assertIn("search", payload["commands"])
|
|
85
|
+
self.assertTrue(any("Search all accessible datasets by default" in rule for rule in payload["rules"]))
|
|
86
|
+
|
|
87
|
+
def test_structured_help_uns_exposes_only_confirmed_apply(self) -> None:
|
|
88
|
+
code, stdout, stderr = run_cli("help", "uns")
|
|
89
|
+
|
|
90
|
+
self.assertEqual(code, 0)
|
|
91
|
+
self.assertEqual(stderr, "")
|
|
92
|
+
payload = json.loads(stdout)
|
|
93
|
+
self.assertIn("apply", payload["commands"])
|
|
94
|
+
parameters = {item["name"] for item in payload["commands"]["apply"]["parameters"]}
|
|
95
|
+
self.assertEqual(parameters, {"--file", "--document-hash", "--plan-hash", "--confirm"})
|
|
96
|
+
|
|
97
|
+
def test_uns_validate_loads_json_and_calls_sdk(self) -> None:
|
|
98
|
+
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
99
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
100
|
+
path = Path(tmp) / "UNS.json"
|
|
101
|
+
path.write_text(json.dumps(document), encoding="utf-8")
|
|
102
|
+
with mock.patch.object(uns_commands.uns, "validate", return_value={"valid": True}) as validate:
|
|
103
|
+
code, stdout, stderr = run_cli("uns", "validate", "--file", str(path))
|
|
104
|
+
|
|
105
|
+
self.assertEqual(code, 0)
|
|
106
|
+
self.assertEqual(stderr, "")
|
|
107
|
+
self.assertEqual(json.loads(stdout), {"valid": True})
|
|
108
|
+
validate.assert_called_once_with(document)
|
|
109
|
+
|
|
110
|
+
def test_uns_discover_forwards_only_filters(self) -> None:
|
|
111
|
+
with mock.patch.object(uns_commands.uns, "discover", return_value={"resources": {}}) as discover:
|
|
112
|
+
code, stdout, stderr = run_cli(
|
|
113
|
+
"uns", "discover", "--resource-type", "connections", "--protocol", "opcua"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.assertEqual(code, 0)
|
|
117
|
+
self.assertEqual(stderr, "")
|
|
118
|
+
self.assertEqual(json.loads(stdout), {"resources": {}})
|
|
119
|
+
discover.assert_called_once_with(resource_types=["connections"], protocols=["opcua"])
|
|
120
|
+
|
|
121
|
+
def test_uns_resource_search_forwards_structured_selector(self) -> None:
|
|
122
|
+
with mock.patch.object(uns_commands.uns, "search_resources", return_value={"items": []}) as search:
|
|
123
|
+
code, stdout, stderr = run_cli(
|
|
124
|
+
"uns", "resource-search",
|
|
125
|
+
"--resource-type", "collectionPoints",
|
|
126
|
+
"--protocol", "opcua",
|
|
127
|
+
"--selector-json", '{"tagCode":"TEMP_01"}',
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.assertEqual(code, 0)
|
|
131
|
+
self.assertEqual(stderr, "")
|
|
132
|
+
self.assertEqual(json.loads(stdout), {"items": []})
|
|
133
|
+
search.assert_called_once_with(
|
|
134
|
+
"collectionPoints",
|
|
135
|
+
protocol="opcua",
|
|
136
|
+
keyword=None,
|
|
137
|
+
selector={"tagCode": "TEMP_01"},
|
|
138
|
+
page=1,
|
|
139
|
+
page_size=20,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_uns_point_write_allows_dry_run_without_execution_confirmation(self) -> None:
|
|
143
|
+
with mock.patch.object(uns_commands.uns, "write_point", return_value={"dryRun": True}) as write:
|
|
144
|
+
code, stdout, stderr = run_cli(
|
|
145
|
+
"uns", "point-write", "MOTOR_START",
|
|
146
|
+
"--value-json", "true",
|
|
147
|
+
"--reason", "validate control",
|
|
148
|
+
"--ticket", "WO-100",
|
|
149
|
+
"--dry-run",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.assertEqual(code, 0)
|
|
153
|
+
self.assertEqual(stderr, "")
|
|
154
|
+
self.assertEqual(json.loads(stdout), {"dryRun": True})
|
|
155
|
+
write.assert_called_once_with(
|
|
156
|
+
"MOTOR_START",
|
|
157
|
+
True,
|
|
158
|
+
reason="validate control",
|
|
159
|
+
ticket="WO-100",
|
|
160
|
+
request_id=None,
|
|
161
|
+
dry_run=True,
|
|
162
|
+
confirmed=False,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def test_uns_path_resolve_calls_sdk_without_creating_a_manifest(self) -> None:
|
|
166
|
+
path = "workspace:/project/project-2/FACTORY/TEMP"
|
|
167
|
+
with mock.patch.object(
|
|
168
|
+
uns_commands.uns,
|
|
169
|
+
"resolve_path",
|
|
170
|
+
return_value={"canonicalPath": "uns://enterprise/tenant-1/organization/org-1/"
|
|
171
|
+
"workspace/workspace-1/project/project-2/FACTORY/TEMP"},
|
|
172
|
+
) as resolve:
|
|
173
|
+
code, stdout, stderr = run_cli("uns", "path-resolve", path)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(code, 0)
|
|
176
|
+
self.assertEqual(stderr, "")
|
|
177
|
+
self.assertIn("canonicalPath", json.loads(stdout))
|
|
178
|
+
resolve.assert_called_once_with(path)
|
|
179
|
+
|
|
180
|
+
def test_uns_data_query_accepts_enterprise_path_instead_of_node_code(self) -> None:
|
|
181
|
+
path = "enterprise:/organization/org-1/workspace/workspace-2/project/project-2/ORDERS"
|
|
182
|
+
with mock.patch.object(
|
|
183
|
+
uns_commands.uns, "query_path_data", return_value={"rows": []}
|
|
184
|
+
) as query:
|
|
185
|
+
code, stdout, stderr = run_cli("uns", "data-query", "--path", path)
|
|
186
|
+
|
|
187
|
+
self.assertEqual(code, 0)
|
|
188
|
+
self.assertEqual(stderr, "")
|
|
189
|
+
self.assertEqual(json.loads(stdout), {"rows": []})
|
|
190
|
+
query.assert_called_once_with(
|
|
191
|
+
path,
|
|
192
|
+
page_num=1,
|
|
193
|
+
page_size=50,
|
|
194
|
+
pagination_mode="OFFSET",
|
|
195
|
+
page_token=None,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def test_uns_data_query_rejects_node_code_and_path_together(self) -> None:
|
|
199
|
+
with mock.patch.object(uns_commands.uns, "query_path_data") as query:
|
|
200
|
+
code, stdout, stderr = run_cli(
|
|
201
|
+
"uns", "data-query", "ORDERS", "--path", "project:/ORDERS"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self.assertEqual(code, 2)
|
|
205
|
+
self.assertEqual(stdout, "")
|
|
206
|
+
self.assertIn("exactly one", stderr)
|
|
207
|
+
query.assert_not_called()
|
|
208
|
+
|
|
209
|
+
def test_uns_data_delete_requires_confirmation_and_forwards_rows(self) -> None:
|
|
210
|
+
rows = [{"recordId": "row-1", "amount": 12}]
|
|
211
|
+
with mock.patch.object(
|
|
212
|
+
uns_commands.uns, "delete_node_data", return_value={"deletedCount": 1}
|
|
213
|
+
) as delete:
|
|
214
|
+
code, stdout, stderr = run_cli(
|
|
215
|
+
"uns", "data-delete", "ORDERS",
|
|
216
|
+
"--rows-json", json.dumps(rows),
|
|
217
|
+
"--confirm",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self.assertEqual(code, 0)
|
|
221
|
+
self.assertEqual(stderr, "")
|
|
222
|
+
self.assertEqual(json.loads(stdout), {"deletedCount": 1})
|
|
223
|
+
delete.assert_called_once_with("ORDERS", rows, confirmed=True)
|
|
224
|
+
|
|
225
|
+
def test_uns_data_delete_does_not_call_sdk_without_confirmation(self) -> None:
|
|
226
|
+
with mock.patch.object(uns_commands.uns, "delete_node_data") as delete:
|
|
227
|
+
code, stdout, stderr = run_cli(
|
|
228
|
+
"uns", "data-delete", "ORDERS",
|
|
229
|
+
"--rows-json", '[{"recordId":"row-1"}]',
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self.assertEqual(code, 2)
|
|
233
|
+
self.assertEqual(stdout, "")
|
|
234
|
+
self.assertIn("requires --confirm", stderr)
|
|
235
|
+
delete.assert_not_called()
|
|
236
|
+
|
|
237
|
+
def test_uns_apply_requires_confirmation_and_forwards_reviewed_hashes(self) -> None:
|
|
238
|
+
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
239
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
240
|
+
path = Path(tmp) / "UNS.json"
|
|
241
|
+
path.write_text(json.dumps(document), encoding="utf-8")
|
|
242
|
+
with mock.patch.object(uns_commands.uns, "apply", return_value={"status": "SUCCESS"}) as apply:
|
|
243
|
+
code, stdout, stderr = run_cli(
|
|
244
|
+
"uns", "apply",
|
|
245
|
+
"--file", str(path),
|
|
246
|
+
"--document-hash", "document-hash",
|
|
247
|
+
"--plan-hash", "plan-hash",
|
|
248
|
+
"--confirm",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.assertEqual(code, 0)
|
|
252
|
+
self.assertEqual(stderr, "")
|
|
253
|
+
self.assertEqual(json.loads(stdout), {"status": "SUCCESS"})
|
|
254
|
+
apply.assert_called_once_with(
|
|
255
|
+
document,
|
|
256
|
+
expected_document_hash="document-hash",
|
|
257
|
+
expected_plan_hash="plan-hash",
|
|
258
|
+
confirmed=True,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def test_uns_apply_does_not_write_without_confirmation(self) -> None:
|
|
262
|
+
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
263
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
264
|
+
path = Path(tmp) / "UNS.json"
|
|
265
|
+
path.write_text(json.dumps(document), encoding="utf-8")
|
|
266
|
+
with mock.patch.object(uns_commands.uns, "apply") as apply:
|
|
267
|
+
code, stdout, stderr = run_cli(
|
|
268
|
+
"uns", "apply",
|
|
269
|
+
"--file", str(path),
|
|
270
|
+
"--document-hash", "document-hash",
|
|
271
|
+
"--plan-hash", "plan-hash",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.assertEqual(code, 2)
|
|
275
|
+
self.assertEqual(stdout, "")
|
|
276
|
+
self.assertIn("requires --confirm", stderr)
|
|
277
|
+
apply.assert_not_called()
|
|
278
|
+
|
|
279
|
+
def test_uns_finalize_atomically_replaces_target_after_validation(self) -> None:
|
|
280
|
+
document = {"schemaVersion": "1.0", "resources": {}, "uns": {"nodes": []}}
|
|
281
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
282
|
+
root = Path(tmp)
|
|
283
|
+
candidate = root / ".UNS.json.candidate"
|
|
284
|
+
candidate.write_text(json.dumps(document), encoding="utf-8")
|
|
285
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
286
|
+
with mock.patch.object(uns_commands.uns, "validate", return_value={"valid": True}) as validate:
|
|
287
|
+
with mock.patch.object(uns_commands.uns, "plan", return_value={"planHash": "hash"}) as plan:
|
|
288
|
+
code, stdout, stderr = run_cli("uns", "finalize", "--file", str(candidate))
|
|
289
|
+
|
|
290
|
+
self.assertEqual(code, 0)
|
|
291
|
+
self.assertEqual(stderr, "")
|
|
292
|
+
self.assertFalse(candidate.exists())
|
|
293
|
+
self.assertEqual(json.loads((root / "UNS.json").read_text(encoding="utf-8")), document)
|
|
294
|
+
self.assertEqual(json.loads(stdout)["plan"], {"planHash": "hash"})
|
|
295
|
+
self.assertEqual(validate.call_count, 2)
|
|
296
|
+
plan.assert_called_once_with(document)
|
|
297
|
+
|
|
298
|
+
def test_uns_finalize_keeps_existing_file_when_candidate_is_invalid(self) -> None:
|
|
299
|
+
existing = {"schemaVersion": "1.0", "metadata": {"name": "existing"}}
|
|
300
|
+
candidate_document = {"schemaVersion": "1.0", "metadata": {"name": "candidate"}}
|
|
301
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
302
|
+
root = Path(tmp)
|
|
303
|
+
target = root / "UNS.json"
|
|
304
|
+
candidate = root / ".UNS.json.candidate"
|
|
305
|
+
target.write_text(json.dumps(existing), encoding="utf-8")
|
|
306
|
+
candidate.write_text(json.dumps(candidate_document), encoding="utf-8")
|
|
307
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
308
|
+
with mock.patch.object(uns_commands.uns, "validate", return_value={"valid": False}):
|
|
309
|
+
with mock.patch.object(uns_commands.uns, "plan") as plan:
|
|
310
|
+
code, stdout, stderr = run_cli("uns", "finalize", "--file", str(candidate))
|
|
311
|
+
|
|
312
|
+
self.assertEqual(code, 1)
|
|
313
|
+
self.assertEqual(stdout, "")
|
|
314
|
+
self.assertIn("candidate validation failed", stderr)
|
|
315
|
+
self.assertEqual(json.loads(target.read_text(encoding="utf-8")), existing)
|
|
316
|
+
self.assertTrue(candidate.exists())
|
|
317
|
+
plan.assert_not_called()
|
|
318
|
+
|
|
319
|
+
def test_uns_finalize_restores_existing_file_when_final_validation_fails(self) -> None:
|
|
320
|
+
existing = {"schemaVersion": "1.0", "metadata": {"name": "existing"}}
|
|
321
|
+
candidate_document = {"schemaVersion": "1.0", "metadata": {"name": "candidate"}}
|
|
322
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
323
|
+
root = Path(tmp)
|
|
324
|
+
target = root / "UNS.json"
|
|
325
|
+
candidate = root / ".UNS.json.candidate"
|
|
326
|
+
target.write_text(json.dumps(existing), encoding="utf-8")
|
|
327
|
+
candidate.write_text(json.dumps(candidate_document), encoding="utf-8")
|
|
328
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
329
|
+
with mock.patch.object(
|
|
330
|
+
uns_commands.uns,
|
|
331
|
+
"validate",
|
|
332
|
+
side_effect=[{"valid": True}, {"valid": False}],
|
|
333
|
+
):
|
|
334
|
+
with mock.patch.object(uns_commands.uns, "plan") as plan:
|
|
335
|
+
code, stdout, stderr = run_cli("uns", "finalize", "--file", str(candidate))
|
|
336
|
+
|
|
337
|
+
self.assertEqual(code, 1)
|
|
338
|
+
self.assertEqual(stdout, "")
|
|
339
|
+
self.assertIn("previous file was restored", stderr)
|
|
340
|
+
self.assertEqual(json.loads(target.read_text(encoding="utf-8")), existing)
|
|
341
|
+
self.assertTrue(candidate.exists())
|
|
342
|
+
plan.assert_not_called()
|
|
343
|
+
|
|
344
|
+
def test_uns_finalize_restores_existing_file_when_final_validation_raises(self) -> None:
|
|
345
|
+
existing = {"schemaVersion": "1.0", "metadata": {"name": "existing"}}
|
|
346
|
+
candidate_document = {"schemaVersion": "1.0", "metadata": {"name": "candidate"}}
|
|
347
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
348
|
+
root = Path(tmp)
|
|
349
|
+
target = root / "UNS.json"
|
|
350
|
+
candidate = root / ".UNS.json.candidate"
|
|
351
|
+
target.write_text(json.dumps(existing), encoding="utf-8")
|
|
352
|
+
candidate.write_text(json.dumps(candidate_document), encoding="utf-8")
|
|
353
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
354
|
+
with mock.patch.object(
|
|
355
|
+
uns_commands.uns,
|
|
356
|
+
"validate",
|
|
357
|
+
side_effect=[{"valid": True}, PlatformSdkError("validation unavailable")],
|
|
358
|
+
):
|
|
359
|
+
code, stdout, stderr = run_cli("uns", "finalize", "--file", str(candidate))
|
|
360
|
+
|
|
361
|
+
self.assertEqual(code, 1)
|
|
362
|
+
self.assertEqual(stdout, "")
|
|
363
|
+
self.assertIn("validation unavailable", stderr)
|
|
364
|
+
self.assertEqual(json.loads(target.read_text(encoding="utf-8")), existing)
|
|
365
|
+
self.assertTrue(candidate.exists())
|
|
366
|
+
|
|
367
|
+
def test_uns_finalize_restores_existing_file_when_plan_raises(self) -> None:
|
|
368
|
+
existing = {"schemaVersion": "1.0", "metadata": {"name": "existing"}}
|
|
369
|
+
candidate_document = {"schemaVersion": "1.0", "metadata": {"name": "candidate"}}
|
|
370
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
371
|
+
root = Path(tmp)
|
|
372
|
+
target = root / "UNS.json"
|
|
373
|
+
candidate = root / ".UNS.json.candidate"
|
|
374
|
+
target.write_text(json.dumps(existing), encoding="utf-8")
|
|
375
|
+
candidate.write_text(json.dumps(candidate_document), encoding="utf-8")
|
|
376
|
+
with mock.patch.dict(os.environ, {"LICOS_PROJECT_PATH": tmp}, clear=False):
|
|
377
|
+
with mock.patch.object(uns_commands.uns, "validate", return_value={"valid": True}):
|
|
378
|
+
with mock.patch.object(
|
|
379
|
+
uns_commands.uns,
|
|
380
|
+
"plan",
|
|
381
|
+
side_effect=PlatformSdkError("planning unavailable"),
|
|
382
|
+
):
|
|
383
|
+
code, stdout, stderr = run_cli("uns", "finalize", "--file", str(candidate))
|
|
384
|
+
|
|
385
|
+
self.assertEqual(code, 1)
|
|
386
|
+
self.assertEqual(stdout, "")
|
|
387
|
+
self.assertIn("planning unavailable", stderr)
|
|
388
|
+
self.assertEqual(json.loads(target.read_text(encoding="utf-8")), existing)
|
|
389
|
+
self.assertTrue(candidate.exists())
|
|
390
|
+
|
|
391
|
+
def test_storage_files_wraps_sdk_and_outputs_json(self) -> None:
|
|
88
392
|
with mock.patch.object(storage_commands.storage, "list_files", return_value=[{"id": "file-1"}]) as list_files:
|
|
89
393
|
code, stdout, stderr = run_cli("storage", "files")
|
|
90
394
|
|
|
@@ -291,12 +595,12 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
291
595
|
"clientId": "client-3",
|
|
292
596
|
}
|
|
293
597
|
with tempfile.TemporaryDirectory() as tmp:
|
|
294
|
-
env = {
|
|
295
|
-
"LICOS_PROJECT_PATH": tmp,
|
|
296
|
-
"AGENT_PROJECT_ID": "project/1",
|
|
297
|
-
"LICOS_PLATFORM_PUBLIC_BASE_URL": "https://platform.example",
|
|
298
|
-
"LICOS_DEPLOY_PUBLIC_BASE_URL": "https://deploy.example",
|
|
299
|
-
}
|
|
598
|
+
env = {
|
|
599
|
+
"LICOS_PROJECT_PATH": tmp,
|
|
600
|
+
"AGENT_PROJECT_ID": "project/1",
|
|
601
|
+
"LICOS_PLATFORM_PUBLIC_BASE_URL": "https://platform.example",
|
|
602
|
+
"LICOS_DEPLOY_PUBLIC_BASE_URL": "https://deploy.example",
|
|
603
|
+
}
|
|
300
604
|
with mock.patch.dict(os.environ, env, clear=False):
|
|
301
605
|
with mock.patch.object(oauth_commands.oauth, "ensure_project_app", return_value=app) as ensure:
|
|
302
606
|
code, stdout, stderr = run_cli(
|
|
@@ -332,8 +636,8 @@ class PlatformCliTests(unittest.TestCase):
|
|
|
332
636
|
)
|
|
333
637
|
config = json.loads((Path(tmp) / "config" / "licos-oauth.json").read_text(encoding="utf-8"))
|
|
334
638
|
self.assertEqual(config["redirectUriConfigs"], redirects)
|
|
335
|
-
self.assertEqual(config["publicBaseUrl"], "https://platform.example")
|
|
336
|
-
self.assertEqual(json.loads(stdout)["selectionMode"], "auto-apply")
|
|
639
|
+
self.assertEqual(config["publicBaseUrl"], "https://platform.example")
|
|
640
|
+
self.assertEqual(json.loads(stdout)["selectionMode"], "auto-apply")
|
|
337
641
|
|
|
338
642
|
def test_oauth_configure_without_redirect_requires_stable_project_metadata(self) -> None:
|
|
339
643
|
with tempfile.TemporaryDirectory() as tmp:
|
|
File without changes
|
{licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/database_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/knowledge_commands.py
RENAMED
|
File without changes
|
{licos_platform_cli-0.2.14 → licos_platform_cli-0.3.1}/src/licos_platform_cli/storage_commands.py
RENAMED
|
File without changes
|