loxo-cli 0.2.0__tar.gz → 0.2.2__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.
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/CHANGELOG.md +22 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/PKG-INFO +1 -1
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/pyproject.toml +1 -1
- loxo_cli-0.2.2/src/loxo_cli/__init__.py +10 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/ref.py +20 -8
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/pagination.py +15 -2
- loxo_cli-0.2.2/tests/test_cmd_ref.py +94 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_pagination.py +17 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/uv.lock +1 -1
- loxo_cli-0.2.0/src/loxo_cli/__init__.py +0 -1
- loxo_cli-0.2.0/tests/test_cmd_ref.py +0 -43
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/.github/workflows/ci.yml +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/.github/workflows/publish.yml +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/.gitignore +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/LICENSE +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/README.md +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/__main__.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/client.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/__init__.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/_app.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/_helpers.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/activities.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/api.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/candidates.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/companies.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/configure.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/deals.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/jobs.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/people.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/commands/webhooks.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/config.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/errors.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/__init__.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/base.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/candidate.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/company.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/deal.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/job.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/person.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/reference.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/models/webhook.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/src/loxo_cli/output.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/conftest.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_app_state.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_client.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_activities.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_api.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_candidates.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_companies.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_configure.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_deals.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_jobs.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_people.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_cmd_webhooks.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_config.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_error_exit_codes.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_errors.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_helpers.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_models.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_output.py +0 -0
- {loxo_cli-0.2.0 → loxo_cli-0.2.2}/tests/test_smoke.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2]
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `loxo ref custom-fields` (and `job-types`, `person-types`) no longer hammer the
|
|
8
|
+
API into rate-limiting (HTTP 429). These reference endpoints return their full
|
|
9
|
+
list in one response and ignore the `after_id` cursor, so the keyset paginator
|
|
10
|
+
looped forever. The paginator now stops when the cursor stops advancing, and
|
|
11
|
+
these endpoints are fetched in a single request.
|
|
12
|
+
- `loxo ref activity-types` no longer fails: the `activity_types` endpoint
|
|
13
|
+
rejects `after_id` with HTTP 422, so it is now fetched without a cursor.
|
|
14
|
+
- `loxo ref lists` now works. It targets the correct `person_lists` endpoint
|
|
15
|
+
(the previous `lists` path returned 404).
|
|
16
|
+
|
|
17
|
+
## [0.2.1]
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `loxo --version` now reports the actual installed version (derived from package
|
|
22
|
+
metadata) instead of a hardcoded string that could drift. (0.2.0 misreported
|
|
23
|
+
itself as 0.1.0.)
|
|
24
|
+
|
|
3
25
|
## [0.2.0]
|
|
4
26
|
|
|
5
27
|
### Added
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
# Single source of truth: the installed distribution's metadata (driven by
|
|
5
|
+
# pyproject's version), so `loxo --version` can never drift from the release.
|
|
6
|
+
__version__ = version("loxo-cli")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
8
|
+
__version__ = "0.0.0+dev"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -9,47 +9,59 @@ ref_app = typer.Typer(help="Reference data lookups. Unofficial — not affiliate
|
|
|
9
9
|
COLUMNS = ["id", "name"]
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _list_reference(ctx: typer.Context, endpoint: str) -> None:
|
|
12
|
+
def _list_reference(ctx: typer.Context, endpoint: str, *, paginated: bool = True) -> None:
|
|
13
13
|
state = ctx.obj
|
|
14
|
-
|
|
14
|
+
client = state.client()
|
|
15
|
+
if paginated:
|
|
16
|
+
rows = list(paginate(client, endpoint, scheme="after_id"))
|
|
17
|
+
else:
|
|
18
|
+
# Several reference endpoints return a fixed, complete list in one
|
|
19
|
+
# response and don't support keyset pagination: dynamic_fields/job_types/
|
|
20
|
+
# person_types ignore after_id (driving the paginator loops forever ->
|
|
21
|
+
# 429), and activity_types rejects after_id with HTTP 422. Fetch once.
|
|
22
|
+
rows = extract_items(client.get(endpoint), None)
|
|
15
23
|
state.emit(rows, columns=COLUMNS)
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
@ref_app.command("job-types")
|
|
19
27
|
def job_types(ctx: typer.Context) -> None:
|
|
20
|
-
_list_reference(ctx, "job_types")
|
|
28
|
+
_list_reference(ctx, "job_types", paginated=False)
|
|
21
29
|
|
|
22
30
|
|
|
23
31
|
@ref_app.command("activity-types")
|
|
24
32
|
def activity_types(ctx: typer.Context) -> None:
|
|
25
|
-
_list_reference(ctx, "activity_types")
|
|
33
|
+
_list_reference(ctx, "activity_types", paginated=False)
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
@ref_app.command("source-types")
|
|
29
37
|
def source_types(ctx: typer.Context) -> None:
|
|
38
|
+
# source_types genuinely keyset-paginates (honors after_id); keep paging.
|
|
30
39
|
_list_reference(ctx, "source_types")
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
@ref_app.command("person-types")
|
|
34
43
|
def person_types(ctx: typer.Context) -> None:
|
|
35
|
-
_list_reference(ctx, "person_types")
|
|
44
|
+
_list_reference(ctx, "person_types", paginated=False)
|
|
36
45
|
|
|
37
46
|
|
|
38
47
|
@ref_app.command("lists")
|
|
39
48
|
def lists(ctx: typer.Context) -> None:
|
|
40
|
-
|
|
49
|
+
"""List the agency's people lists."""
|
|
50
|
+
# The endpoint is person_lists (GET /lists is 404). It returns the full list
|
|
51
|
+
# in one response and ignores after_id, so fetch once.
|
|
52
|
+
_list_reference(ctx, "person_lists", paginated=False)
|
|
41
53
|
|
|
42
54
|
|
|
43
55
|
@ref_app.command("custom-fields")
|
|
44
56
|
def custom_fields(ctx: typer.Context) -> None:
|
|
45
57
|
"""List agency custom (dynamic) field definitions."""
|
|
46
|
-
_list_reference(ctx, "dynamic_fields")
|
|
58
|
+
_list_reference(ctx, "dynamic_fields", paginated=False)
|
|
47
59
|
|
|
48
60
|
|
|
49
61
|
@ref_app.command("dynamic-fields", hidden=True)
|
|
50
62
|
def dynamic_fields_alias(ctx: typer.Context) -> None:
|
|
51
63
|
"""Alias of custom-fields."""
|
|
52
|
-
_list_reference(ctx, "dynamic_fields")
|
|
64
|
+
_list_reference(ctx, "dynamic_fields", paginated=False)
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
@ref_app.command("hierarchies")
|
|
@@ -85,9 +85,22 @@ def paginate(
|
|
|
85
85
|
items = extract_items(data, items_key)
|
|
86
86
|
if not items:
|
|
87
87
|
return
|
|
88
|
+
next_after_id = items[-1].get("id") if isinstance(items[-1], dict) else None
|
|
89
|
+
# If the endpoint ignored after_id and returned the same page again
|
|
90
|
+
# (some reference endpoints, e.g. dynamic_fields, return a fixed list
|
|
91
|
+
# and ignore the cursor), the cursor won't advance. Stop without
|
|
92
|
+
# re-yielding the duplicate page. Without this guard those endpoints
|
|
93
|
+
# loop forever, hammering the API until it rate-limits us (429).
|
|
94
|
+
# Only applies once a cursor has actually been sent: on the first
|
|
95
|
+
# page after_id is None, and a no-id last item there is a legitimate
|
|
96
|
+
# single, complete page that must still be yielded.
|
|
97
|
+
if after_id is not None and next_after_id == after_id:
|
|
98
|
+
return
|
|
88
99
|
yield from items
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
# Last item has no id -> can't build a next cursor, so this is the
|
|
101
|
+
# final page.
|
|
102
|
+
if next_after_id is None:
|
|
91
103
|
return
|
|
104
|
+
after_id = next_after_id
|
|
92
105
|
else:
|
|
93
106
|
raise ValueError(f"Unknown pagination scheme: {scheme}")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import respx
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from loxo_cli.__main__ import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
ENV = {"LOXO_API_KEY": "k", "LOXO_API_SLUG": "acme"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@respx.mock
|
|
14
|
+
def test_ref_job_types_single_fetch():
|
|
15
|
+
# job_types ignores after_id (returns its full list every call), so the
|
|
16
|
+
# command fetches once instead of driving the keyset paginator.
|
|
17
|
+
route = respx.get("https://app.loxo.co/api/acme/job_types").mock(
|
|
18
|
+
return_value=httpx.Response(
|
|
19
|
+
200, json=[{"id": 1, "name": "Perm"}, {"id": 2, "name": "Temp"}]
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
result = runner.invoke(app, ["--json", "ref", "job-types"], env=ENV)
|
|
23
|
+
assert [x["name"] for x in json.loads(result.stdout)] == ["Perm", "Temp"]
|
|
24
|
+
assert route.call_count == 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@respx.mock
|
|
28
|
+
def test_ref_activity_types_single_fetch_no_after_id():
|
|
29
|
+
# activity_types REJECTS after_id with HTTP 422. The command must not send
|
|
30
|
+
# it: a single fetch with no after_id param, and never a second request.
|
|
31
|
+
def handler(request):
|
|
32
|
+
assert "after_id" not in request.url.params, "must not send after_id to activity_types"
|
|
33
|
+
return httpx.Response(200, json=[{"id": 9, "name": "Call"}])
|
|
34
|
+
|
|
35
|
+
route = respx.get("https://app.loxo.co/api/acme/activity_types").mock(side_effect=handler)
|
|
36
|
+
result = runner.invoke(app, ["--json", "ref", "activity-types"], env=ENV)
|
|
37
|
+
assert result.exit_code == 0
|
|
38
|
+
assert [x["name"] for x in json.loads(result.stdout)] == ["Call"]
|
|
39
|
+
assert route.call_count == 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@respx.mock
|
|
43
|
+
def test_ref_source_types_still_paginates():
|
|
44
|
+
# source_types genuinely honors after_id; the command must keep paging.
|
|
45
|
+
route = respx.get("https://app.loxo.co/api/acme/source_types").mock(
|
|
46
|
+
side_effect=[
|
|
47
|
+
httpx.Response(200, json=[{"id": 10, "name": "A"}, {"id": 11, "name": "B"}]),
|
|
48
|
+
httpx.Response(200, json=[{"id": 12, "name": "C"}]),
|
|
49
|
+
httpx.Response(200, json=[]),
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
result = runner.invoke(app, ["--json", "ref", "source-types"], env=ENV)
|
|
53
|
+
assert [x["name"] for x in json.loads(result.stdout)] == ["A", "B", "C"]
|
|
54
|
+
assert route.call_count == 3
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@respx.mock
|
|
58
|
+
def test_ref_custom_fields_single_fetch():
|
|
59
|
+
# dynamic_fields is a flat config list that ignores after_id, so the command
|
|
60
|
+
# fetches it exactly once rather than driving the keyset paginator (which the
|
|
61
|
+
# endpoint would loop forever, causing 429s). A single non-empty response is
|
|
62
|
+
# all the endpoint ever returns.
|
|
63
|
+
route = respx.get("https://app.loxo.co/api/acme/dynamic_fields").mock(
|
|
64
|
+
return_value=httpx.Response(200, json=[{"id": 1, "name": "custom_text_3"}])
|
|
65
|
+
)
|
|
66
|
+
result = runner.invoke(app, ["--json", "ref", "custom-fields"], env=ENV)
|
|
67
|
+
assert json.loads(result.stdout)[0]["name"] == "custom_text_3"
|
|
68
|
+
assert route.call_count == 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@respx.mock
|
|
72
|
+
def test_ref_lists_uses_person_lists_endpoint():
|
|
73
|
+
# `ref lists` maps to the person_lists endpoint (GET /lists 404s). It returns
|
|
74
|
+
# the full list in one response and ignores after_id -> single fetch.
|
|
75
|
+
lists_route = respx.get("https://app.loxo.co/api/acme/lists").mock(
|
|
76
|
+
return_value=httpx.Response(404, json={"status": 404, "error": "Not Found"})
|
|
77
|
+
)
|
|
78
|
+
person_lists_route = respx.get("https://app.loxo.co/api/acme/person_lists").mock(
|
|
79
|
+
return_value=httpx.Response(200, json=[{"id": 5, "name": "Hot Leads"}])
|
|
80
|
+
)
|
|
81
|
+
result = runner.invoke(app, ["--json", "ref", "lists"], env=ENV)
|
|
82
|
+
assert result.exit_code == 0
|
|
83
|
+
assert [x["name"] for x in json.loads(result.stdout)] == ["Hot Leads"]
|
|
84
|
+
assert person_lists_route.call_count == 1
|
|
85
|
+
assert lists_route.call_count == 0 # must not hit the 404 path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@respx.mock
|
|
89
|
+
def test_ref_hierarchies():
|
|
90
|
+
respx.get("https://app.loxo.co/api/acme/dynamic_fields/7/hierarchies").mock(
|
|
91
|
+
return_value=httpx.Response(200, json={"hierarchies": [{"id": 1, "name": "L1"}]})
|
|
92
|
+
)
|
|
93
|
+
result = runner.invoke(app, ["--json", "ref", "hierarchies", "7"], env=ENV)
|
|
94
|
+
assert json.loads(result.stdout)[0]["name"] == "L1"
|
|
@@ -102,3 +102,20 @@ def test_after_id_pagination():
|
|
|
102
102
|
with LoxoClient(SETTINGS) as client:
|
|
103
103
|
items = list(paginate(client, "source_types", scheme="after_id"))
|
|
104
104
|
assert [i["id"] for i in items] == [10, 11, 12]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@respx.mock
|
|
108
|
+
def test_after_id_stops_when_cursor_does_not_advance():
|
|
109
|
+
# Some reference endpoints (e.g. dynamic_fields) return a fixed, complete
|
|
110
|
+
# list and IGNORE the after_id query param — so they hand back the same
|
|
111
|
+
# non-empty page on every request. The after_id paginator must detect the
|
|
112
|
+
# non-advancing cursor and stop, instead of looping forever and getting
|
|
113
|
+
# rate-limited (429). It must also not re-yield the duplicate page.
|
|
114
|
+
base = "https://app.loxo.co/api/acme/dynamic_fields"
|
|
115
|
+
same = [{"id": 1, "name": "custom_text_3"}, {"id": 2, "name": "custom_hierarchy_5"}]
|
|
116
|
+
route = respx.get(base).mock(return_value=httpx.Response(200, json=same))
|
|
117
|
+
with LoxoClient(SETTINGS) as client:
|
|
118
|
+
items = list(paginate(client, "dynamic_fields", scheme="after_id"))
|
|
119
|
+
assert [i["id"] for i in items] == [1, 2]
|
|
120
|
+
# One real page + one probe that proves the cursor didn't advance, then stop.
|
|
121
|
+
assert route.call_count == 2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
import respx
|
|
5
|
-
from typer.testing import CliRunner
|
|
6
|
-
|
|
7
|
-
from loxo_cli.__main__ import app
|
|
8
|
-
|
|
9
|
-
runner = CliRunner()
|
|
10
|
-
ENV = {"LOXO_API_KEY": "k", "LOXO_API_SLUG": "acme"}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@respx.mock
|
|
14
|
-
def test_ref_job_types_after_id():
|
|
15
|
-
respx.get("https://app.loxo.co/api/acme/job_types").mock(
|
|
16
|
-
side_effect=[
|
|
17
|
-
httpx.Response(200, json=[{"id": 1, "name": "Perm"}, {"id": 2, "name": "Temp"}]),
|
|
18
|
-
httpx.Response(200, json=[]),
|
|
19
|
-
]
|
|
20
|
-
)
|
|
21
|
-
result = runner.invoke(app, ["--json", "ref", "job-types"], env=ENV)
|
|
22
|
-
assert [x["name"] for x in json.loads(result.stdout)] == ["Perm", "Temp"]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@respx.mock
|
|
26
|
-
def test_ref_custom_fields():
|
|
27
|
-
respx.get("https://app.loxo.co/api/acme/dynamic_fields").mock(
|
|
28
|
-
side_effect=[
|
|
29
|
-
httpx.Response(200, json=[{"id": 1, "name": "custom_text_3"}]),
|
|
30
|
-
httpx.Response(200, json=[]),
|
|
31
|
-
]
|
|
32
|
-
)
|
|
33
|
-
result = runner.invoke(app, ["--json", "ref", "custom-fields"], env=ENV)
|
|
34
|
-
assert json.loads(result.stdout)[0]["name"] == "custom_text_3"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@respx.mock
|
|
38
|
-
def test_ref_hierarchies():
|
|
39
|
-
respx.get("https://app.loxo.co/api/acme/dynamic_fields/7/hierarchies").mock(
|
|
40
|
-
return_value=httpx.Response(200, json={"hierarchies": [{"id": 1, "name": "L1"}]})
|
|
41
|
-
)
|
|
42
|
-
result = runner.invoke(app, ["--json", "ref", "hierarchies", "7"], env=ENV)
|
|
43
|
-
assert json.loads(result.stdout)[0]["name"] == "L1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|