python-jcli 0.1.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jcli/__init__.py +1 -1
- jcli/cli.py +565 -51
- jcli/cli_helpers.py +5 -42
- jcli/skills/jcli/SKILL.md +15 -2
- jcli/skills/jcli/auth/SKILL.md +86 -0
- jcli/skills/jcli/build/SKILL.md +123 -0
- jcli/skills/jcli/credential/SKILL.md +14 -5
- jcli/skills/jcli/job/SKILL.md +20 -4
- jcli/skills/jcli/node/SKILL.md +4 -2
- jcli/skills/jcli/pipeline/SKILL.md +3 -3
- jcli/skills/jcli/plugin/SKILL.md +3 -0
- jcli/skills/jcli/system/SKILL.md +15 -2
- jcli/skills/jcli/view/SKILL.md +4 -3
- jcli/specs/_auth.yaml +15 -0
- jcli/{plugins/skills.py → specs/plugins/jcli_commands.py} +124 -63
- jcli/specs/plugins/jenkins_auth.py +75 -0
- jcli/specs/plugins/jenkins_methods.py +332 -0
- jcli/specs/plugins/jenkins_pipeline_validate.py +51 -0
- jcli/specs/plugins/jenkins_plugin.py +77 -0
- jcli/specs/plugins/jenkins_system.py +137 -0
- jcli/specs/resources/build.yaml +160 -0
- jcli/specs/resources/credential.yaml +97 -0
- jcli/specs/resources/job.yaml +184 -0
- jcli/specs/resources/node.yaml +69 -0
- jcli/specs/resources/pipeline.yaml +62 -0
- jcli/specs/resources/plugin.yaml +65 -0
- jcli/specs/resources/system.yaml +72 -0
- jcli/specs/resources/view.yaml +75 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/METADATA +57 -12
- python_jcli-1.0.1.dist-info/RECORD +40 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/WHEEL +1 -1
- jcli/plugins/__init__.py +0 -28
- jcli/plugins/build.py +0 -197
- jcli/plugins/config.py +0 -268
- jcli/plugins/credential.py +0 -210
- jcli/plugins/job.py +0 -425
- jcli/plugins/node.py +0 -134
- jcli/plugins/pipeline.py +0 -108
- jcli/plugins/plugin.py +0 -173
- jcli/plugins/system.py +0 -173
- jcli/plugins/view.py +0 -148
- jcli/sdk/build.py +0 -182
- jcli/sdk/credential.py +0 -143
- jcli/sdk/job.py +0 -191
- jcli/sdk/job_templates.py +0 -128
- jcli/sdk/node.py +0 -139
- jcli/sdk/pipeline.py +0 -121
- jcli/sdk/plugin.py +0 -155
- jcli/sdk/system.py +0 -202
- jcli/sdk/view.py +0 -94
- jcli/skills/jcli/config/SKILL.md +0 -81
- python_jcli-0.1.0.dist-info/RECORD +0 -44
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/entry_points.txt +0 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Top-level command plugins for jcli 2.0: skills, completion.
|
|
2
|
+
|
|
3
|
+
Migrated verbatim from jcli v1:
|
|
4
|
+
|
|
5
|
+
* ``skills`` — jcli/plugins/skills.py
|
|
6
|
+
* ``completion`` — jcli/cli.py ``add_completion_command``
|
|
7
|
+
|
|
8
|
+
Each group is registered as a cliyard command plugin
|
|
9
|
+
(``@register_command``), so ``create_cli`` attaches it to the top-level
|
|
10
|
+
Click group. Command semantics and output formats are unchanged from v1.
|
|
11
|
+
"""
|
|
2
12
|
|
|
3
13
|
from __future__ import annotations
|
|
4
14
|
|
|
@@ -8,14 +18,20 @@ from typing import Any
|
|
|
8
18
|
|
|
9
19
|
import click
|
|
10
20
|
|
|
21
|
+
from cliyard.plugin import register_command
|
|
22
|
+
|
|
11
23
|
from jcli.cli_helpers import get_formatter
|
|
12
24
|
|
|
13
|
-
# ------------------------------------------------------------------
|
|
14
|
-
# Constants
|
|
15
|
-
# ------------------------------------------------------------------
|
|
16
25
|
|
|
17
|
-
#
|
|
18
|
-
|
|
26
|
+
# =====================================================================
|
|
27
|
+
# Skills commands (verbatim from jcli/plugins/skills.py)
|
|
28
|
+
# =====================================================================
|
|
29
|
+
|
|
30
|
+
# Bundled skills directory (ships with jcli package):
|
|
31
|
+
# <repo-root>/jcli/skills/jcli/* — this plugin lives in <repo-root>/jcli/specs/plugins/
|
|
32
|
+
BUNDLED_SKILLS_DIR = (
|
|
33
|
+
Path(__file__).resolve().parent.parent.parent.parent / "jcli" / "skills" / "jcli"
|
|
34
|
+
)
|
|
19
35
|
|
|
20
36
|
# Default install directory for opencode
|
|
21
37
|
DEFAULT_INSTALL_DIR = Path.home() / ".config" / "opencode" / "skills"
|
|
@@ -24,11 +40,6 @@ DEFAULT_INSTALL_DIR = Path.home() / ".config" / "opencode" / "skills"
|
|
|
24
40
|
FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
25
41
|
|
|
26
42
|
|
|
27
|
-
# ------------------------------------------------------------------
|
|
28
|
-
# Helpers
|
|
29
|
-
# ------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
|
|
32
43
|
def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
|
|
33
44
|
"""Parse SKILL.md frontmatter to extract metadata.
|
|
34
45
|
|
|
@@ -59,9 +70,12 @@ def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
|
|
|
59
70
|
frontmatter = match.group(1)
|
|
60
71
|
|
|
61
72
|
# Simple YAML parsing without pyyaml dependency
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
lines = frontmatter.split("\n")
|
|
74
|
+
i = 0
|
|
75
|
+
while i < len(lines):
|
|
76
|
+
line = lines[i].strip()
|
|
64
77
|
if not line or line.startswith("#"):
|
|
78
|
+
i += 1
|
|
65
79
|
continue
|
|
66
80
|
|
|
67
81
|
if line.startswith("name:"):
|
|
@@ -70,14 +84,45 @@ def parse_skill_metadata(skill_dir: Path) -> dict[str, Any]:
|
|
|
70
84
|
metadata["version"] = line.split(":", 1)[1].strip().strip("'\"")
|
|
71
85
|
elif line.startswith("description:"):
|
|
72
86
|
desc = line.split(":", 1)[1].strip().strip("'\"")
|
|
87
|
+
if desc in ("|", ">", ""):
|
|
88
|
+
# YAML block scalar: collect following indented lines
|
|
89
|
+
parts = []
|
|
90
|
+
j = i + 1
|
|
91
|
+
while j < len(lines) and (
|
|
92
|
+
lines[j].startswith(" ") or lines[j].startswith("\t")
|
|
93
|
+
):
|
|
94
|
+
parts.append(lines[j].strip())
|
|
95
|
+
j += 1
|
|
96
|
+
if parts:
|
|
97
|
+
desc = " ".join(parts)
|
|
98
|
+
i = j - 1 # continue after the block scalar
|
|
73
99
|
metadata["description"] = desc
|
|
74
100
|
elif line.startswith("- ") and "allowed-tools" in frontmatter:
|
|
75
101
|
tool = line[2:].strip().strip("'\"")
|
|
76
102
|
metadata["allowed_tools"].append(tool)
|
|
103
|
+
i += 1
|
|
77
104
|
|
|
78
105
|
return metadata
|
|
79
106
|
|
|
80
107
|
|
|
108
|
+
def _is_jcli_skill(metadata: dict[str, Any]) -> bool:
|
|
109
|
+
"""Return True if the skill belongs to jcli.
|
|
110
|
+
|
|
111
|
+
A skill is considered jcli's own when:
|
|
112
|
+
- it is a symlink into the bundled skills directory
|
|
113
|
+
(``source == "bundled (symlink)"``), or
|
|
114
|
+
- its name starts with the ``jcli-`` prefix, or
|
|
115
|
+
- its path contains a ``jcli`` path segment (e.g. inside BUNDLED_SKILLS_DIR).
|
|
116
|
+
"""
|
|
117
|
+
if metadata.get("source") == "bundled (symlink)":
|
|
118
|
+
return True
|
|
119
|
+
if str(metadata.get("name", "")).startswith("jcli-"):
|
|
120
|
+
return True
|
|
121
|
+
if "jcli" in Path(str(metadata.get("path", ""))).parts:
|
|
122
|
+
return True
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
81
126
|
def get_bundled_skills() -> list[dict[str, Any]]:
|
|
82
127
|
"""Get all bundled skills from the skills directory."""
|
|
83
128
|
skills = []
|
|
@@ -149,8 +194,8 @@ def install_skill(
|
|
|
149
194
|
|
|
150
195
|
skill_dest = target_dir / name
|
|
151
196
|
|
|
152
|
-
#
|
|
153
|
-
if skill_dest.exists():
|
|
197
|
+
# Path.exists() is False for a broken symlink; is_symlink() catches it
|
|
198
|
+
if skill_dest.exists() or skill_dest.is_symlink():
|
|
154
199
|
if force:
|
|
155
200
|
# Remove existing
|
|
156
201
|
if skill_dest.is_symlink():
|
|
@@ -182,7 +227,8 @@ def uninstall_skill(name: str, install_dir: Path | None = None) -> bool:
|
|
|
182
227
|
target_dir = install_dir or DEFAULT_INSTALL_DIR
|
|
183
228
|
skill_path = target_dir / name
|
|
184
229
|
|
|
185
|
-
|
|
230
|
+
# Path.exists() is False for a broken symlink; is_symlink() catches it
|
|
231
|
+
if not skill_path.exists() and not skill_path.is_symlink():
|
|
186
232
|
raise click.ClickException(f"Skill '{name}' not found in {target_dir}.")
|
|
187
233
|
|
|
188
234
|
if skill_path.is_symlink():
|
|
@@ -196,46 +242,44 @@ def uninstall_skill(name: str, install_dir: Path | None = None) -> bool:
|
|
|
196
242
|
return True
|
|
197
243
|
|
|
198
244
|
|
|
199
|
-
# ------------------------------------------------------------------
|
|
200
|
-
# Click group
|
|
201
|
-
# ------------------------------------------------------------------
|
|
202
|
-
|
|
203
|
-
|
|
204
245
|
@click.group("skills", help="Manage jcli skills (list, install, uninstall).")
|
|
205
246
|
def skills_group() -> None:
|
|
206
247
|
"""Skills management commands."""
|
|
207
248
|
|
|
208
249
|
|
|
209
|
-
# ------------------------------------------------------------------
|
|
210
|
-
# list
|
|
211
|
-
# ------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
|
|
214
250
|
@skills_group.command("list")
|
|
215
251
|
@click.option("--installed", "-i", is_flag=True, help="Show only installed skills.")
|
|
216
252
|
@click.option("--bundled", "-b", is_flag=True, help="Show only bundled skills.")
|
|
217
253
|
@click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
|
|
218
254
|
@click.pass_context
|
|
219
|
-
def
|
|
255
|
+
def skills_list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: str | None) -> None:
|
|
220
256
|
"""List all available skills."""
|
|
221
257
|
fmt = get_formatter(ctx)
|
|
222
258
|
target_dir = Path(install_dir) if install_dir else DEFAULT_INSTALL_DIR
|
|
223
259
|
|
|
224
260
|
skills_to_show = []
|
|
225
261
|
|
|
226
|
-
if
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# Avoid duplicates if both bundled and installed
|
|
235
|
-
existing_names = {s["name"] for s in skills_to_show}
|
|
236
|
-
for skill in installed_skills:
|
|
237
|
-
if skill["name"] not in existing_names:
|
|
262
|
+
if installed and bundled:
|
|
263
|
+
# Both flags explicitly given: bundled + jcli installed (dedup by name)
|
|
264
|
+
seen: set[str] = set()
|
|
265
|
+
for skill in get_bundled_skills():
|
|
266
|
+
skills_to_show.append(skill)
|
|
267
|
+
seen.add(skill["name"])
|
|
268
|
+
for skill in get_installed_skills(target_dir):
|
|
269
|
+
if _is_jcli_skill(skill) and skill["name"] not in seen:
|
|
238
270
|
skills_to_show.append(skill)
|
|
271
|
+
seen.add(skill["name"])
|
|
272
|
+
elif installed:
|
|
273
|
+
# Only jcli installed skills
|
|
274
|
+
skills_to_show.extend(
|
|
275
|
+
s for s in get_installed_skills(target_dir) if _is_jcli_skill(s)
|
|
276
|
+
)
|
|
277
|
+
elif bundled:
|
|
278
|
+
# Only bundled skills
|
|
279
|
+
skills_to_show.extend(get_bundled_skills())
|
|
280
|
+
else:
|
|
281
|
+
# Default: only bundled (jcli's own skills)
|
|
282
|
+
skills_to_show.extend(get_bundled_skills())
|
|
239
283
|
|
|
240
284
|
if not skills_to_show:
|
|
241
285
|
fmt.print_info("No skills found.")
|
|
@@ -254,18 +298,13 @@ def list_cmd(ctx: click.Context, installed: bool, bundled: bool, install_dir: st
|
|
|
254
298
|
fmt.print_table(headers, rows, title="Jcli Skills")
|
|
255
299
|
|
|
256
300
|
|
|
257
|
-
# ------------------------------------------------------------------
|
|
258
|
-
# install
|
|
259
|
-
# ------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
|
|
262
301
|
@skills_group.command("install")
|
|
263
302
|
@click.argument("name", required=False)
|
|
264
303
|
@click.option("--all", "-a", "install_all", is_flag=True, help="Install all bundled skills.")
|
|
265
304
|
@click.option("--force", "-f", is_flag=True, help="Force install (overwrite existing).")
|
|
266
305
|
@click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
|
|
267
306
|
@click.pass_context
|
|
268
|
-
def
|
|
307
|
+
def skills_install_cmd(ctx: click.Context, name: str | None, install_all: bool, force: bool, install_dir: str | None) -> None:
|
|
269
308
|
"""Install a skill.
|
|
270
309
|
|
|
271
310
|
NAME is the skill name to install (from bundled skills).
|
|
@@ -311,16 +350,11 @@ def install_cmd(ctx: click.Context, name: str | None, install_all: bool, force:
|
|
|
311
350
|
fmt.print_success(f"Skill '{name}' installed to {target_dir / name}")
|
|
312
351
|
|
|
313
352
|
|
|
314
|
-
# ------------------------------------------------------------------
|
|
315
|
-
# uninstall
|
|
316
|
-
# ------------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
|
|
319
353
|
@skills_group.command("uninstall")
|
|
320
354
|
@click.argument("name")
|
|
321
355
|
@click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
|
|
322
356
|
@click.pass_context
|
|
323
|
-
def
|
|
357
|
+
def skills_uninstall_cmd(ctx: click.Context, name: str, install_dir: str | None) -> None:
|
|
324
358
|
"""Uninstall a skill.
|
|
325
359
|
|
|
326
360
|
NAME is the skill name to uninstall.
|
|
@@ -339,17 +373,12 @@ def uninstall_cmd(ctx: click.Context, name: str, install_dir: str | None) -> Non
|
|
|
339
373
|
fmt.print_success(f"Skill '{name}' uninstalled from {target_dir}")
|
|
340
374
|
|
|
341
375
|
|
|
342
|
-
# ------------------------------------------------------------------
|
|
343
|
-
# get
|
|
344
|
-
# ------------------------------------------------------------------
|
|
345
|
-
|
|
346
|
-
|
|
347
376
|
@skills_group.command("get")
|
|
348
377
|
@click.argument("name")
|
|
349
378
|
@click.option("--installed", "-i", is_flag=True, help="Get from installed skills.")
|
|
350
379
|
@click.option("--dir", "install_dir", type=click.Path(exists=False), help="Custom install directory.")
|
|
351
380
|
@click.pass_context
|
|
352
|
-
def
|
|
381
|
+
def skills_get_cmd(ctx: click.Context, name: str, installed: bool, install_dir: str | None) -> None:
|
|
353
382
|
"""Show skill details and SKILL.md content."""
|
|
354
383
|
fmt = get_formatter(ctx)
|
|
355
384
|
target_dir = Path(install_dir) if install_dir else DEFAULT_INSTALL_DIR
|
|
@@ -389,11 +418,43 @@ def get_cmd(ctx: click.Context, name: str, installed: bool, install_dir: str | N
|
|
|
389
418
|
fmt.console.print(content)
|
|
390
419
|
|
|
391
420
|
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
421
|
+
# =====================================================================
|
|
422
|
+
# Completion command (verbatim from jcli/cli.py add_completion_command)
|
|
423
|
+
# =====================================================================
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build_completion_group(cli: click.Group) -> click.Group:
|
|
427
|
+
"""Build the ``completion show`` group attached to *cli*."""
|
|
428
|
+
from click.shell_completion import BashComplete, FishComplete, ZshComplete
|
|
429
|
+
|
|
430
|
+
@click.group()
|
|
431
|
+
def completion() -> None:
|
|
432
|
+
"""Shell completion support for jcli."""
|
|
433
|
+
|
|
434
|
+
@completion.command()
|
|
435
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
|
|
436
|
+
def show(shell: str) -> None:
|
|
437
|
+
"""Output shell completion script for the specified shell."""
|
|
438
|
+
shell_cls = {"bash": BashComplete, "zsh": ZshComplete, "fish": FishComplete}
|
|
439
|
+
complete = shell_cls[shell](cli, {}, "jcli", "_JCLI_COMPLETE")
|
|
440
|
+
click.echo(complete.source(), nl=False)
|
|
441
|
+
|
|
442
|
+
cli.add_command(completion)
|
|
443
|
+
return completion
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# =====================================================================
|
|
447
|
+
# cliyard command plugin registration
|
|
448
|
+
# =====================================================================
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@register_command("skills")
|
|
452
|
+
def _register_skills(cli: click.Group, ctx: Any) -> None:
|
|
453
|
+
"""Attach the skills command group to the top-level CLI."""
|
|
454
|
+
cli.add_command(skills_group)
|
|
395
455
|
|
|
396
456
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
457
|
+
@register_command("completion")
|
|
458
|
+
def _register_completion(cli: click.Group, ctx: Any) -> None:
|
|
459
|
+
"""Attach the completion command group to the top-level CLI."""
|
|
460
|
+
build_completion_group(cli)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Jenkins auth steps: Basic Auth + Crumb (CSRF)."""
|
|
2
|
+
import base64
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cliyard.plugin import register_auth_step
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _config_path() -> str:
|
|
11
|
+
"""Resolve the jcli config file path at call time.
|
|
12
|
+
|
|
13
|
+
Prefers ``jcli.sdk.config.DEFAULT_CONFIG_FILE`` (so tests can point it
|
|
14
|
+
elsewhere via monkeypatch) and falls back to ``~/.jcli/config.yaml``
|
|
15
|
+
when the SDK is not importable.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
from jcli.sdk.config import DEFAULT_CONFIG_FILE
|
|
19
|
+
|
|
20
|
+
return str(DEFAULT_CONFIG_FILE)
|
|
21
|
+
except Exception:
|
|
22
|
+
return os.path.expanduser("~/.jcli/config.yaml")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_profile():
|
|
26
|
+
import yaml
|
|
27
|
+
config_path = _config_path()
|
|
28
|
+
if not os.path.exists(config_path):
|
|
29
|
+
raise RuntimeError(f"config not found: {config_path}")
|
|
30
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
31
|
+
cfg = yaml.safe_load(f) or {}
|
|
32
|
+
name = os.environ.get("JCLI_PROFILE") or cfg.get("active_profile", "default")
|
|
33
|
+
profile = dict(cfg.get("profiles", {}).get(name, {}))
|
|
34
|
+
for env_key, field in (("JCLI_URL", "url"), ("JCLI_USERNAME", "username"), ("JCLI_API_TOKEN", "api_token")):
|
|
35
|
+
if os.environ.get(env_key):
|
|
36
|
+
profile[field] = os.environ[env_key]
|
|
37
|
+
return profile
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@register_auth_step("jenkins_basic")
|
|
41
|
+
class JenkinsBasicAuth:
|
|
42
|
+
"""Set Basic Auth header from ~/.jcli/config.yaml profile."""
|
|
43
|
+
|
|
44
|
+
def execute(self, auth_state, config, http_client):
|
|
45
|
+
profile = _load_profile()
|
|
46
|
+
username = profile.get("username", "")
|
|
47
|
+
token = profile.get("api_token", "")
|
|
48
|
+
if not username or not token:
|
|
49
|
+
raise RuntimeError("missing username/api_token in profile")
|
|
50
|
+
raw = f"{username}:{token}".encode()
|
|
51
|
+
http_client.default_headers["Authorization"] = "Basic " + base64.b64encode(raw).decode()
|
|
52
|
+
auth_state["username"] = username
|
|
53
|
+
return {"username": username}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@register_auth_step("jenkins_crumb")
|
|
57
|
+
class JenkinsCrumb:
|
|
58
|
+
"""Fetch Jenkins crumb and inject as header. Tolerate 404 (CSRF disabled)."""
|
|
59
|
+
|
|
60
|
+
def execute(self, auth_state, config, http_client):
|
|
61
|
+
url = f"{http_client.base_url}/crumbIssuer/api/json"
|
|
62
|
+
try:
|
|
63
|
+
resp = http_client._session.get(url, headers=http_client.default_headers, timeout=10)
|
|
64
|
+
except requests.RequestException:
|
|
65
|
+
return {}
|
|
66
|
+
if resp.status_code == 404:
|
|
67
|
+
return {} # CSRF disabled
|
|
68
|
+
if resp.status_code != 200:
|
|
69
|
+
raise RuntimeError(f"crumb fetch failed: {resp.status_code}")
|
|
70
|
+
data = resp.json()
|
|
71
|
+
field = data.get("crumbRequestField")
|
|
72
|
+
value = data.get("crumb")
|
|
73
|
+
if field and value:
|
|
74
|
+
http_client.default_headers[field] = value
|
|
75
|
+
return {"crumb": value}
|