glaip-sdk 0.0.18__py3-none-any.whl → 0.0.19__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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
5
6
  """
6
7
 
7
8
  import json
@@ -54,6 +55,201 @@ from glaip_sdk.utils.serialization import (
54
55
  console = Console()
55
56
 
56
57
 
58
+ def _is_sensitive_data(val: Any) -> bool:
59
+ """Check if value contains sensitive authentication data.
60
+
61
+ Args:
62
+ val: Value to check for sensitive information
63
+
64
+ Returns:
65
+ True if the value appears to contain sensitive data
66
+ """
67
+ if not isinstance(val, dict):
68
+ return False
69
+
70
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
71
+ return any(
72
+ pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns
73
+ )
74
+
75
+
76
+ def _format_preview_value(val: Any) -> str:
77
+ """Format a value for display in update preview with sensitive data redaction.
78
+
79
+ Args:
80
+ val: Value to format
81
+
82
+ Returns:
83
+ Formatted string representation of the value
84
+ """
85
+ if val is None:
86
+ return "[dim]None[/dim]"
87
+ if isinstance(val, dict):
88
+ # Redact sensitive fields in authentication
89
+ if _is_sensitive_data(val):
90
+ redacted = val.copy()
91
+ sensitive_patterns = {"token", "password", "secret", "key", "credential"}
92
+ for k in list(redacted.keys()):
93
+ if any(pattern in k.lower() for pattern in sensitive_patterns):
94
+ redacted[k] = "<REDACTED>"
95
+ return json.dumps(redacted, indent=2)
96
+ return json.dumps(val, indent=2)
97
+ if isinstance(val, str):
98
+ return f'"{val}"' if val else '""'
99
+ return str(val)
100
+
101
+
102
+ def _build_empty_override_warnings(empty_fields: list[str]) -> list[str]:
103
+ """Build warning lines for empty CLI overrides.
104
+
105
+ Args:
106
+ empty_fields: List of field names with empty string overrides
107
+
108
+ Returns:
109
+ List of formatted warning lines
110
+ """
111
+ if not empty_fields:
112
+ return []
113
+
114
+ warnings = [
115
+ "\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"
116
+ ]
117
+ warnings.extend(
118
+ f"- [yellow]{field}: will be set to empty string[/yellow]"
119
+ for field in empty_fields
120
+ )
121
+ return warnings
122
+
123
+
124
+ def _validate_import_payload_fields(import_payload: dict[str, Any]) -> bool:
125
+ """Validate that import payload contains updatable fields.
126
+
127
+ Args:
128
+ import_payload: Import payload to validate
129
+
130
+ Returns:
131
+ True if payload has updatable fields, False otherwise
132
+ """
133
+ updatable_fields = {"name", "transport", "description", "config", "authentication"}
134
+ has_updatable = any(field in import_payload for field in updatable_fields)
135
+
136
+ if not has_updatable:
137
+ available_fields = set(import_payload.keys())
138
+ print_markup(
139
+ "[yellow]⚠️ No updatable fields found in import file.[/yellow]\n"
140
+ f"[dim]Found fields: {', '.join(sorted(available_fields))}[/dim]\n"
141
+ f"[dim]Updatable fields: {', '.join(sorted(updatable_fields))}[/dim]"
142
+ )
143
+ return has_updatable
144
+
145
+
146
+ def _build_update_data_from_sources(
147
+ import_payload: dict[str, Any] | None,
148
+ mcp: Any,
149
+ name: str | None,
150
+ transport: str | None,
151
+ description: str | None,
152
+ config: str | None,
153
+ auth: str | None,
154
+ ) -> dict[str, Any]:
155
+ """Build update data from import payload and CLI flags.
156
+
157
+ Args:
158
+ import_payload: Optional import payload
159
+ mcp: Current MCP object
160
+ name: CLI name flag
161
+ transport: CLI transport flag
162
+ description: CLI description flag
163
+ config: CLI config flag
164
+ auth: CLI auth flag
165
+
166
+ Returns:
167
+ Dictionary with update data
168
+ """
169
+ update_data = {}
170
+
171
+ # Start with import data if available
172
+ if import_payload:
173
+ updatable_fields = [
174
+ "name",
175
+ "transport",
176
+ "description",
177
+ "config",
178
+ "authentication",
179
+ ]
180
+ for field in updatable_fields:
181
+ if field in import_payload:
182
+ update_data[field] = import_payload[field]
183
+
184
+ # CLI flags override import values
185
+ if name is not None:
186
+ update_data["name"] = name
187
+ if transport is not None:
188
+ update_data["transport"] = transport
189
+ if description is not None:
190
+ update_data["description"] = description
191
+ if config is not None:
192
+ parsed_config = parse_json_input(config)
193
+ config_transport = (
194
+ (transport or import_payload.get("transport"))
195
+ if import_payload
196
+ else (transport or getattr(mcp, "transport", None))
197
+ )
198
+ update_data["config"] = validate_mcp_config_structure(
199
+ parsed_config,
200
+ transport=config_transport,
201
+ source="--config",
202
+ )
203
+ if auth is not None:
204
+ parsed_auth = parse_json_input(auth)
205
+ update_data["authentication"] = validate_mcp_auth_structure(
206
+ parsed_auth, source="--auth"
207
+ )
208
+
209
+ return update_data
210
+
211
+
212
+ def _collect_cli_overrides(
213
+ name: str | None,
214
+ transport: str | None,
215
+ description: str | None,
216
+ config: str | None,
217
+ auth: str | None,
218
+ ) -> dict[str, Any]:
219
+ """Collect CLI flags that were explicitly provided.
220
+
221
+ Args:
222
+ name: CLI name flag
223
+ transport: CLI transport flag
224
+ description: CLI description flag
225
+ config: CLI config flag
226
+ auth: CLI auth flag
227
+
228
+ Returns:
229
+ Dictionary of provided CLI overrides
230
+ """
231
+ cli_overrides = {}
232
+ if name is not None:
233
+ cli_overrides["name"] = name
234
+ if transport is not None:
235
+ cli_overrides["transport"] = transport
236
+ if description is not None:
237
+ cli_overrides["description"] = description
238
+ if config is not None:
239
+ cli_overrides["config"] = config
240
+ if auth is not None:
241
+ cli_overrides["auth"] = auth
242
+ return cli_overrides
243
+
244
+
245
+ def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
246
+ """Render CLI error once and exit with non-zero status."""
247
+ handle_json_output(ctx, error=error)
248
+ if get_ctx_value(ctx, "view") != "json":
249
+ display_api_error(error, operation)
250
+ ctx.exit(1)
251
+
252
+
57
253
  @click.group(name="mcps", no_args_is_help=True)
58
254
  def mcps_group() -> None:
59
255
  """MCP management operations.
@@ -440,10 +636,7 @@ def create(
440
636
  handle_rich_output(ctx, rich_panel)
441
637
 
442
638
  except Exception as e:
443
- handle_json_output(ctx, error=e)
444
- if get_ctx_value(ctx, "view") != "json":
445
- display_api_error(e, "MCP creation")
446
- raise click.ClickException(str(e))
639
+ _handle_cli_error(ctx, e, "MCP creation")
447
640
 
448
641
 
449
642
  def _handle_mcp_export(
@@ -762,9 +955,50 @@ def connect(ctx: Any, config_file: str) -> None:
762
955
  raise click.ClickException(str(e))
763
956
 
764
957
 
958
+ def _generate_update_preview(
959
+ mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]
960
+ ) -> str:
961
+ """Generate formatted preview of changes for user confirmation.
962
+
963
+ Args:
964
+ mcp: Current MCP object
965
+ update_data: Data that will be sent in update request
966
+ cli_overrides: CLI flags that were explicitly provided
967
+
968
+ Returns:
969
+ Formatted preview string showing old→new values
970
+ """
971
+ lines = [
972
+ f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"
973
+ ]
974
+
975
+ empty_overrides = []
976
+
977
+ # Show each field that will be updated
978
+ for field, new_value in update_data.items():
979
+ old_value = getattr(mcp, field, None)
980
+
981
+ # Track empty CLI overrides
982
+ if field in cli_overrides and cli_overrides[field] == "":
983
+ empty_overrides.append(field)
984
+
985
+ old_display = _format_preview_value(old_value)
986
+ new_display = _format_preview_value(new_value)
987
+
988
+ lines.append(f"- [cyan]{field}[/cyan]: {old_display} → {new_display}")
989
+
990
+ # Add warnings for empty CLI overrides
991
+ lines.extend(_build_empty_override_warnings(empty_overrides))
992
+
993
+ return "\n".join(lines)
994
+
995
+
765
996
  @mcps_group.command()
766
997
  @click.argument("mcp_ref")
767
998
  @click.option("--name", help="New MCP name")
999
+ @click.option(
1000
+ "--transport", type=click.Choice(["http", "sse"]), help="New transport protocol"
1001
+ )
768
1002
  @click.option("--description", help="New description")
769
1003
  @click.option(
770
1004
  "--config",
@@ -776,62 +1010,105 @@ def connect(ctx: Any, config_file: str) -> None:
776
1010
  "auth",
777
1011
  help="JSON authentication object or @file reference (e.g., @auth.json)",
778
1012
  )
1013
+ @click.option(
1014
+ "--import",
1015
+ "import_file",
1016
+ type=click.Path(exists=True, dir_okay=False, readable=True),
1017
+ help="Import MCP configuration from JSON or YAML export",
1018
+ )
1019
+ @click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
779
1020
  @output_flags()
780
1021
  @click.pass_context
781
1022
  def update(
782
1023
  ctx: Any,
783
1024
  mcp_ref: str,
784
1025
  name: str | None,
1026
+ transport: str | None,
785
1027
  description: str | None,
786
1028
  config: str | None,
787
1029
  auth: str | None,
1030
+ import_file: str | None,
1031
+ y: bool,
788
1032
  ) -> None:
789
1033
  """Update an existing MCP with new configuration values.
790
1034
 
1035
+ You can update an MCP by providing individual fields via CLI options, or by
1036
+ importing from a file and optionally overriding specific fields.
1037
+
791
1038
  Args:
792
1039
  ctx: Click context containing output format preferences
793
1040
  mcp_ref: MCP reference (ID or name)
794
1041
  name: New MCP name (optional)
1042
+ transport: New transport protocol (optional)
795
1043
  description: New description (optional)
796
1044
  config: New JSON configuration string or @file reference (optional)
797
1045
  auth: New JSON authentication object or @file reference (optional)
1046
+ import_file: Optional path to import configuration from export file.
1047
+ CLI options override imported values.
1048
+ y: Skip confirmation prompt when using --import
798
1049
 
799
1050
  Raises:
800
1051
  ClickException: If MCP not found, JSON invalid, or no fields specified
801
1052
 
802
1053
  Note:
803
- At least one field must be specified for update.
804
- Uses PUT for complete updates or PATCH for partial updates.
1054
+ Must specify either --import OR at least one CLI field.
1055
+ CLI options override imported values when both are specified.
1056
+ Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1057
+
1058
+ Examples:
1059
+ Update with CLI options:
1060
+ aip mcps update my-mcp --name new-name --transport sse
1061
+
1062
+ Import from file:
1063
+ aip mcps update my-mcp --import mcp-export.json
1064
+
1065
+ Import with overrides:
1066
+ aip mcps update my-mcp --import mcp-export.json --name new-name -y
805
1067
  """
806
1068
  try:
807
1069
  client = get_client(ctx)
808
1070
 
1071
+ # Validate that at least one update method is provided
1072
+ cli_flags_provided = any(
1073
+ v is not None for v in [name, transport, description, config, auth]
1074
+ )
1075
+ if not import_file and not cli_flags_provided:
1076
+ raise click.ClickException(
1077
+ "No update fields specified. Use --import file or at least one of: "
1078
+ "--name, --transport, --description, --config, --auth"
1079
+ )
1080
+
809
1081
  # Resolve MCP using helper function
810
1082
  mcp = _resolve_mcp(ctx, client, mcp_ref)
811
1083
 
812
- # Build update data
813
- update_data = {}
814
- if name is not None:
815
- update_data["name"] = name
816
- if description is not None:
817
- update_data["description"] = description
818
- if config is not None:
819
- parsed_config = parse_json_input(config)
820
- update_data["config"] = validate_mcp_config_structure(
821
- parsed_config,
822
- transport=getattr(mcp, "transport", None),
823
- source="--config",
824
- )
825
- if auth is not None:
826
- parsed_auth = parse_json_input(auth)
827
- update_data["authentication"] = validate_mcp_auth_structure(
828
- parsed_auth, source="--auth"
829
- )
1084
+ # Load and validate import data if provided
1085
+ import_payload = None
1086
+ if import_file:
1087
+ import_payload = _load_import_ready_payload(import_file)
1088
+ if not _validate_import_payload_fields(import_payload):
1089
+ return
1090
+
1091
+ # Build update data from import and CLI flags
1092
+ update_data = _build_update_data_from_sources(
1093
+ import_payload, mcp, name, transport, description, config, auth
1094
+ )
830
1095
 
831
1096
  if not update_data:
832
1097
  raise click.ClickException("No update fields specified")
833
1098
 
834
- # Update MCP (automatically chooses PUT or PATCH based on provided fields)
1099
+ # Show confirmation preview for import-based updates (unless -y flag)
1100
+ if import_payload and not y:
1101
+ cli_overrides = _collect_cli_overrides(
1102
+ name, transport, description, config, auth
1103
+ )
1104
+ preview = _generate_update_preview(mcp, update_data, cli_overrides)
1105
+ print_markup(preview)
1106
+
1107
+ if not click.confirm("\nContinue with update?", default=False):
1108
+ print_markup("[yellow]Update cancelled.[/yellow]")
1109
+ return
1110
+
1111
+ # Update MCP
835
1112
  with spinner_context(
836
1113
  ctx,
837
1114
  "[bold blue]Updating MCP…[/bold blue]",
@@ -843,10 +1120,7 @@ def update(
843
1120
  handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
844
1121
 
845
1122
  except Exception as e:
846
- handle_json_output(ctx, error=e)
847
- if get_ctx_value(ctx, "view") != "json":
848
- display_api_error(e, "MCP update")
849
- raise click.ClickException(str(e))
1123
+ _handle_cli_error(ctx, e, "MCP update")
850
1124
 
851
1125
 
852
1126
  @mcps_group.command()
@@ -896,7 +1170,4 @@ def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
896
1170
  handle_rich_output(ctx, display_deletion_success("MCP", mcp.name))
897
1171
 
898
1172
  except Exception as e:
899
- handle_json_output(ctx, error=e)
900
- if get_ctx_value(ctx, "view") != "json":
901
- display_api_error(e, "MCP deletion")
902
- raise click.ClickException(str(e))
1173
+ _handle_cli_error(ctx, e, "MCP deletion")
@@ -12,6 +12,25 @@ from pathlib import Path
12
12
  from typing import Any
13
13
 
14
14
  import click
15
+ import yaml
16
+
17
+
18
+ def _looks_like_file_path(value: str) -> bool:
19
+ """Check if string looks like a file reference.
20
+
21
+ Args:
22
+ value: String to check for file-like patterns
23
+
24
+ Returns:
25
+ True if the string appears to be a file path
26
+ """
27
+ return (
28
+ value.lower().endswith((".json", ".yaml", ".yml"))
29
+ or "/" in value
30
+ or "\\" in value # Path separators
31
+ or value.startswith(("./", "../")) # Relative paths
32
+ or value.count(".") > 1 # Likely a filename with extension
33
+ )
15
34
 
16
35
 
17
36
  def _format_file_error(
@@ -40,16 +59,16 @@ def _format_file_error(
40
59
 
41
60
 
42
61
  def _parse_json_from_file(file_path_str: str) -> Any:
43
- """Parse JSON from a file path.
62
+ """Parse JSON or YAML from a file path.
44
63
 
45
64
  Args:
46
- file_path_str: Path to the JSON file (without @ prefix).
65
+ file_path_str: Path to the JSON or YAML file (without @ prefix).
47
66
 
48
67
  Returns:
49
68
  Parsed dictionary from file.
50
69
 
51
70
  Raises:
52
- click.ClickException: If file not found, not readable, empty, or invalid JSON.
71
+ click.ClickException: If file not found, not readable, empty, or invalid format.
53
72
  """
54
73
  # Resolve relative paths against CWD
55
74
  file_path = Path(file_path_str)
@@ -86,18 +105,35 @@ def _parse_json_from_file(file_path_str: str) -> Any:
86
105
  _format_file_error("File is empty", file_path_str, file_path)
87
106
  )
88
107
 
89
- # Parse JSON from file content
90
- try:
91
- return json.loads(content)
92
- except json.JSONDecodeError as e:
93
- raise click.ClickException(
94
- _format_file_error(
95
- "Invalid JSON in file",
96
- file_path_str,
97
- file_path,
98
- detail=f"Error: {e.msg} at line {e.lineno}, column {e.colno}",
108
+ # Determine file format and parse accordingly
109
+ file_ext = file_path.suffix.lower()
110
+
111
+ if file_ext in [".yaml", ".yml"]:
112
+ # Parse YAML from file content
113
+ try:
114
+ return yaml.safe_load(content)
115
+ except yaml.YAMLError as e:
116
+ raise click.ClickException(
117
+ _format_file_error(
118
+ "Invalid YAML in file",
119
+ file_path_str,
120
+ file_path,
121
+ detail=f"Error: {e}",
122
+ )
123
+ )
124
+ else:
125
+ # Default to JSON parsing
126
+ try:
127
+ return json.loads(content)
128
+ except json.JSONDecodeError as e:
129
+ raise click.ClickException(
130
+ _format_file_error(
131
+ "Invalid JSON in file",
132
+ file_path_str,
133
+ file_path,
134
+ detail=f"Error: {e.msg} at line {e.lineno}, column {e.colno}",
135
+ )
99
136
  )
100
- )
101
137
 
102
138
 
103
139
  def parse_json_input(value: str | None) -> Any:
@@ -119,6 +155,9 @@ def parse_json_input(value: str | None) -> Any:
119
155
  >>> parse_json_input('@/path/to/config.json')
120
156
  # Returns content of config.json parsed as JSON
121
157
 
158
+ >>> parse_json_input('/path/to/config.json')
159
+ # Fallback: treats as file path if JSON parsing fails
160
+
122
161
  >>> parse_json_input(None)
123
162
  None
124
163
  """
@@ -134,6 +173,15 @@ def parse_json_input(value: str | None) -> Any:
134
173
  try:
135
174
  return json.loads(value)
136
175
  except json.JSONDecodeError as e:
176
+ # Check if the value looks like a file path and provide helpful hint
177
+ if _looks_like_file_path(trimmed):
178
+ raise click.ClickException(
179
+ f"Invalid JSON in inline value\n"
180
+ f"Error: {e.msg} at line {e.lineno}, column {e.colno}\n"
181
+ f"\n💡 Did you mean to load this from a file? "
182
+ f"File-based config values should start with @ (e.g., @{trimmed})"
183
+ )
184
+
137
185
  raise click.ClickException(
138
186
  f"Invalid JSON in inline value\n"
139
187
  f"Error: {e.msg} at line {e.lineno}, column {e.colno}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
@@ -7,7 +7,7 @@ glaip_sdk/cli/auth.py,sha256=eYdtGmJ3XgiO96hq_69GF6b3W-aRWZrDQ-6bHuaRX4M,13517
7
7
  glaip_sdk/cli/commands/__init__.py,sha256=x0CZlZbZHoHvuzfoTWIyEch6WmNnbPzxajrox6riYp0,173
8
8
  glaip_sdk/cli/commands/agents.py,sha256=FtWGhbl4QRlqxXFNMEnZUpw5mjQ0KPjY0_6o0hYyoaU,41251
9
9
  glaip_sdk/cli/commands/configure.py,sha256=h2GgBpnBWYHAhp3zqkAiy8QNgwPD_7pToZBZRpuMoNM,7869
10
- glaip_sdk/cli/commands/mcps.py,sha256=V41Te3IG8jNFRMb1v1XQV1t461uxpPsrmDFIO6cZPOk,28185
10
+ glaip_sdk/cli/commands/mcps.py,sha256=wqUbkQ6qUUhr6B0n2jJxPUkbxHwfsEnYvdLKew_qorM,36800
11
11
  glaip_sdk/cli/commands/models.py,sha256=G1ce-wZOfvMP6SMnIVuSQ89CF444Kz8Ja6nrNOQXCqU,1729
12
12
  glaip_sdk/cli/commands/tools.py,sha256=YfkB7HRBGcAOC6N-wXTV5Ch5XTXqjKTtyq-Cfb0-18c,18908
13
13
  glaip_sdk/cli/config.py,sha256=jCLJxTBAnOU6EJI6JjcpwUTEAWCJRoALbMrhOvvAofc,946
@@ -19,7 +19,7 @@ glaip_sdk/cli/masking.py,sha256=BOZjwUqxQf3LQlYgUMwq7UYgve8x4_1Qk04ixiJJPZ8,4399
19
19
  glaip_sdk/cli/mcp_validators.py,sha256=SeDbgXkRuBXyDtCmUMpL-1Vh7fmGldz-shAaHhOqbCc,10125
20
20
  glaip_sdk/cli/pager.py,sha256=n9ypOGPPSaseJlwPG1X38qSz1yV3pjRWunzA4xx5E7M,8052
21
21
  glaip_sdk/cli/parsers/__init__.py,sha256=Ycd4HDfYmA7GUGFt0ndBPBo5uTbv15XsXnYUj-a89ug,183
22
- glaip_sdk/cli/parsers/json_input.py,sha256=6NqzVM5l8g0pwCNLKeTVL9SeLM9W_Fl4H__X0dfaQEA,4039
22
+ glaip_sdk/cli/parsers/json_input.py,sha256=ZBhJNUR4bKDX3A5s9lRvuCicaztvQyP0lWuiNYMIO08,5721
23
23
  glaip_sdk/cli/resolution.py,sha256=jXUNpKKhs30n7Ke0uz1Hbny5DTo2_sxvchIhTbeBubE,2393
24
24
  glaip_sdk/cli/rich_helpers.py,sha256=ByUOmK16IisoXWE7nEiI55BF1KWDrm6KCYAxqHu0XOU,825
25
25
  glaip_sdk/cli/slash/__init__.py,sha256=Vdv6Y8bu-pA8dxDlyP4XrhudBPivztUozhLAz9vaLig,682
@@ -67,7 +67,7 @@ glaip_sdk/utils/rich_utils.py,sha256=-Ij-1bIJvnVAi6DrfftchIlMcvOTjVmSE0Qqax0EY_s
67
67
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
68
68
  glaip_sdk/utils/serialization.py,sha256=AFbucakFaCtQDfcgsm2gHZ1iZDA8OaJSZUsS6FhWFR0,12820
69
69
  glaip_sdk/utils/validation.py,sha256=QNORcdyvuliEs4EH2_mkDgmoyT9utgl7YNhaf45SEf8,6992
70
- glaip_sdk-0.0.18.dist-info/METADATA,sha256=IaSz5FAxBzrHDKtbgjyg6h4vyClUS7C3M4ovlax0rv4,5164
71
- glaip_sdk-0.0.18.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
72
- glaip_sdk-0.0.18.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
73
- glaip_sdk-0.0.18.dist-info/RECORD,,
70
+ glaip_sdk-0.0.19.dist-info/METADATA,sha256=CxTXRqseeY6CsuhudOT_ddbxuK5WLfkkM6oSaaMSxdY,5164
71
+ glaip_sdk-0.0.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
72
+ glaip_sdk-0.0.19.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
73
+ glaip_sdk-0.0.19.dist-info/RECORD,,