icpc-api 0.3.0__tar.gz → 0.4.0__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.
- {icpc_api-0.3.0/icpc_api.egg-info → icpc_api-0.4.0}/PKG-INFO +1 -1
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/__init__.py +1 -1
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/cli/main.py +104 -29
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/entities.py +19 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0/icpc_api.egg-info}/PKG-INFO +1 -1
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_institutions.py +41 -3
- {icpc_api-0.3.0 → icpc_api-0.4.0}/LICENSE +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/MANIFEST.in +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/README.md +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/THIRD-PARTY-LICENSES.md +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/common.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/contest.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/person.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/public.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/staff.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/survey.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/api/team.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/cognito.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/flows.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/provider.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/srp.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/store.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/auth/tokens.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/cli/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/cli/columns.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/cli/render.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/config.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/errors.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/facade/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/facade/client.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/facade/domain.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/_generated.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/base.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/blobs.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/common.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/countries.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/enums.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/mixins.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/models/surveys.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/py.typed +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/_generated.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/dsl.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/endpoint.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/fields.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/institutions.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/search/surveys.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/transport/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/transport/_shared.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/transport/async_client.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/transport/operation.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc/transport/sync_client.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc_api.egg-info/SOURCES.txt +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc_api.egg-info/dependency_links.txt +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc_api.egg-info/entry_points.txt +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc_api.egg-info/requires.txt +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/icpc_api.egg-info/top_level.txt +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/pyproject.toml +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/setup.cfg +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/__init__.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_auth.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_columns.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_dsl.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_join.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_models.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_paging.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_reference.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_srp.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_surveys.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_transport.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_transport_parity.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/tests/unit/test_typing.py +0 -0
- {icpc_api-0.3.0 → icpc_api-0.4.0}/uv.lock +0 -0
|
@@ -14,7 +14,7 @@ from icpc.errors import ApiError, AuthError, IcpcError, SearchError
|
|
|
14
14
|
from icpc.facade.client import AsyncIcpc, Icpc, Include
|
|
15
15
|
from icpc.facade.domain import ContestView, Member, Team
|
|
16
16
|
|
|
17
|
-
__version__ = "0.
|
|
17
|
+
__version__ = "0.4.0"
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
20
20
|
"ApiError",
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import copy
|
|
10
10
|
import os
|
|
11
|
-
import re
|
|
12
11
|
import sys
|
|
13
12
|
from collections.abc import Sequence
|
|
14
13
|
from dataclasses import dataclass
|
|
@@ -17,6 +16,7 @@ from pathlib import Path
|
|
|
17
16
|
from typing import Annotated, Any
|
|
18
17
|
|
|
19
18
|
import typer
|
|
19
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
20
20
|
|
|
21
21
|
from icpc import errors
|
|
22
22
|
from icpc.api import common as common_api
|
|
@@ -46,6 +46,7 @@ from icpc.cli.render import OutputFormat, fail, note, render, warn
|
|
|
46
46
|
from icpc.config import Settings
|
|
47
47
|
from icpc.facade.client import Icpc, Include
|
|
48
48
|
from icpc.models.countries import country
|
|
49
|
+
from icpc.models.entities import SiteSettings
|
|
49
50
|
from icpc.models.enums import ExportType, InstitutionUnitType, MemberRole, TeamStatus
|
|
50
51
|
from icpc.search import _generated as endpoints
|
|
51
52
|
from icpc.search.dsl import Filter, SortKey
|
|
@@ -739,9 +740,59 @@ def contest_set_site(
|
|
|
739
740
|
contest_api.site_settings(site_id),
|
|
740
741
|
lambda o: contest_api.update_site_settings(contest_id, o),
|
|
741
742
|
changes,
|
|
743
|
+
SiteSettings,
|
|
742
744
|
)
|
|
743
745
|
|
|
744
746
|
|
|
747
|
+
_NEW_SITE_SETTINGS: dict[str, Any] = {
|
|
748
|
+
"details": "",
|
|
749
|
+
"allowRegistration": False,
|
|
750
|
+
"invitationOnly": False,
|
|
751
|
+
"homepage": "",
|
|
752
|
+
"additionalInfo": "",
|
|
753
|
+
"enforceCapacity": False,
|
|
754
|
+
"allowTeamChanges": False,
|
|
755
|
+
}
|
|
756
|
+
_NEW_SITE_REQUIRED = ("locationName", "capacity", "siteDelivery", "siteType")
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
@contest_app.command("add-site")
|
|
760
|
+
def contest_add_site(
|
|
761
|
+
ctx: typer.Context,
|
|
762
|
+
contest_id: int,
|
|
763
|
+
fields: Annotated[list[str], typer.Argument(help="KEY=VALUE; see below.")],
|
|
764
|
+
) -> None:
|
|
765
|
+
"""Add a site to a contest.
|
|
766
|
+
|
|
767
|
+
icpc contest add-site 1235 "name=World Finals" email=manager@icpc.global \
|
|
768
|
+
locationName=Dubai capacity=150 siteDelivery=ONSITE siteType=NORMAL \
|
|
769
|
+
allowRegistration=true enforceCapacity=true
|
|
770
|
+
|
|
771
|
+
Required: name, email, locationName, capacity, siteDelivery, siteType.
|
|
772
|
+
Optional: allowRegistration, invitationOnly, enforceCapacity, allowTeamChanges,
|
|
773
|
+
details, homepage, additionalInfo.
|
|
774
|
+
"""
|
|
775
|
+
values = _assignments(fields)
|
|
776
|
+
body: dict[str, Any] = {"name": values.pop("name", None), "email": values.pop("email", None)}
|
|
777
|
+
missing = [k for k in ("name", "email") if not body[k]]
|
|
778
|
+
missing += [k for k in _NEW_SITE_REQUIRED if values.get(k) in (None, "")]
|
|
779
|
+
if missing:
|
|
780
|
+
raise typer.BadParameter(f"missing {', '.join(missing)}")
|
|
781
|
+
known = (*_NEW_SITE_REQUIRED, *_NEW_SITE_SETTINGS)
|
|
782
|
+
unknown = sorted(set(values) - set(known))
|
|
783
|
+
if unknown:
|
|
784
|
+
raise typer.BadParameter(
|
|
785
|
+
f"unknown {', '.join(unknown)}; known: name, email, {', '.join(known)}"
|
|
786
|
+
)
|
|
787
|
+
try:
|
|
788
|
+
settings = SiteSettings.model_validate({**_NEW_SITE_SETTINGS, **values})
|
|
789
|
+
except ValidationError as e:
|
|
790
|
+
raise typer.BadParameter(_validation_message(e)) from None
|
|
791
|
+
body["siteSettings"] = settings.model_dump(by_alias=True, exclude_unset=True)
|
|
792
|
+
with _client(ctx) as icpc:
|
|
793
|
+
render(icpc.send(contest_api.create_site(contest_id, body)), _ctx(ctx).output)
|
|
794
|
+
|
|
795
|
+
|
|
745
796
|
@contest_app.command("summary")
|
|
746
797
|
def contest_summary(ctx: typer.Context, contest_id: int) -> None:
|
|
747
798
|
"""Team counts by site and by status, from one joined fetch."""
|
|
@@ -869,58 +920,82 @@ def contest_load(
|
|
|
869
920
|
|
|
870
921
|
|
|
871
922
|
def _assignments(pairs: list[str]) -> dict[str, Any]:
|
|
872
|
-
"""Parse ``key=value`` arguments,
|
|
923
|
+
"""Parse ``key=value`` arguments; ``null`` becomes None, the rest stay strings.
|
|
873
924
|
|
|
874
|
-
|
|
875
|
-
|
|
925
|
+
The caller types them by its model, so ``zip=010000`` keeps its zero while
|
|
926
|
+
``capacity=50`` turns into a number.
|
|
876
927
|
"""
|
|
877
928
|
out: dict[str, Any] = {}
|
|
878
929
|
for pair in pairs:
|
|
879
930
|
key, sep, raw = pair.partition("=")
|
|
880
931
|
if not sep:
|
|
881
932
|
raise typer.BadParameter(f"expected KEY=VALUE, got {pair!r}")
|
|
882
|
-
|
|
883
|
-
if low in ("true", "false"):
|
|
884
|
-
out[key] = low == "true"
|
|
885
|
-
elif low in ("null", "none"):
|
|
886
|
-
out[key] = None
|
|
887
|
-
elif raw.lstrip("-").isdigit():
|
|
888
|
-
out[key] = int(raw)
|
|
889
|
-
elif re.fullmatch(r"-?\d+\.\d+", raw):
|
|
890
|
-
out[key] = float(raw)
|
|
891
|
-
else:
|
|
892
|
-
out[key] = raw.strip('"')
|
|
933
|
+
out[key] = None if raw.lower() in ("null", "none") else raw
|
|
893
934
|
return out
|
|
894
935
|
|
|
895
936
|
|
|
896
|
-
def
|
|
937
|
+
def _validation_message(e: ValidationError) -> str:
|
|
938
|
+
return "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in e.errors())
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _edit(
|
|
942
|
+
ctx: typer.Context,
|
|
943
|
+
label: str,
|
|
944
|
+
read: Any,
|
|
945
|
+
write: Any,
|
|
946
|
+
pairs: list[str],
|
|
947
|
+
model: type[BaseModel] | None = None,
|
|
948
|
+
) -> None:
|
|
897
949
|
"""Read a settings object, apply ``key=value`` changes, write it back.
|
|
898
950
|
|
|
899
951
|
These endpoints are full-object replaces, so the read is not optional: send
|
|
900
952
|
only the changed keys and everything else is wiped. ``a.b=value`` sets a key
|
|
901
953
|
of a nested object.
|
|
954
|
+
|
|
955
|
+
Values are typed by ``model``, by default the model ``read`` returns. A field
|
|
956
|
+
the model doesn't declare takes the type of its current value, or stays a
|
|
957
|
+
string if that is null.
|
|
902
958
|
"""
|
|
903
959
|
changes = _assignments(pairs)
|
|
904
960
|
with _client(ctx) as icpc:
|
|
905
961
|
current = icpc.send(read)
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
for
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
962
|
+
if isinstance(current, BaseModel):
|
|
963
|
+
model = model or type(current)
|
|
964
|
+
current = current.model_dump(by_alias=True)
|
|
965
|
+
obj = copy.deepcopy(current)
|
|
966
|
+
try:
|
|
967
|
+
for path, value in changes.items():
|
|
968
|
+
*parents, key = path.split(".")
|
|
969
|
+
target = obj
|
|
970
|
+
for part in parents:
|
|
971
|
+
target = target.get(part) if isinstance(target, dict) else None
|
|
972
|
+
if not isinstance(target, dict) or key not in target:
|
|
973
|
+
known = sorted(target) if isinstance(target, dict) else []
|
|
974
|
+
fail(f"{label} has no field {path!r}; known here: {', '.join(known)}")
|
|
975
|
+
raise typer.Exit(1)
|
|
976
|
+
old = target[key]
|
|
977
|
+
if value is not None and isinstance(old, bool | int | float):
|
|
978
|
+
target[key] = TypeAdapter(type(old)).validate_python(value)
|
|
979
|
+
else:
|
|
980
|
+
target[key] = value
|
|
981
|
+
if model is not None:
|
|
982
|
+
obj = model.model_validate(obj).model_dump(by_alias=True, exclude_unset=True)
|
|
983
|
+
except ValidationError as e:
|
|
984
|
+
fail(f"bad value for {label}: {_validation_message(e)}")
|
|
985
|
+
raise typer.Exit(1) from None
|
|
986
|
+
for path in changes:
|
|
987
|
+
note(f"{path}: {_at(current, path)!r} -> {_at(obj, path)!r}")
|
|
919
988
|
icpc.send(write(obj))
|
|
920
989
|
after = icpc.send(read)
|
|
921
990
|
render(after, _ctx(ctx).output)
|
|
922
991
|
|
|
923
992
|
|
|
993
|
+
def _at(obj: Any, path: str) -> Any:
|
|
994
|
+
for part in path.split("."):
|
|
995
|
+
obj = obj[part]
|
|
996
|
+
return obj
|
|
997
|
+
|
|
998
|
+
|
|
924
999
|
def _grid(name: str, entity_id: int) -> SearchEndpoint[Any, Any]:
|
|
925
1000
|
factory = GRIDS.get(name)
|
|
926
1001
|
if factory is None:
|
|
@@ -48,6 +48,7 @@ __all__ = [
|
|
|
48
48
|
"RegistrationInfo",
|
|
49
49
|
"RegistrationStatus",
|
|
50
50
|
"SiteRow",
|
|
51
|
+
"SiteSettings",
|
|
51
52
|
"SiteTreeNode",
|
|
52
53
|
"StandingRow",
|
|
53
54
|
"SuggestedInstitution",
|
|
@@ -403,6 +404,24 @@ class SiteRow(Row):
|
|
|
403
404
|
enforce_capacity: bool | None = None
|
|
404
405
|
|
|
405
406
|
|
|
407
|
+
class SiteSettings(Row):
|
|
408
|
+
"""``siteSettings`` of ``GET /contest/site/{id}``, as ``contest.site_settings`` reads it."""
|
|
409
|
+
|
|
410
|
+
id: int | None = None
|
|
411
|
+
version: int | None = None
|
|
412
|
+
location_name: str | None = None
|
|
413
|
+
capacity: int | None = None
|
|
414
|
+
site_delivery: str | None = None
|
|
415
|
+
site_type: str | None = None
|
|
416
|
+
details: str | None = None
|
|
417
|
+
homepage: str | None = None
|
|
418
|
+
additional_info: str | None = None
|
|
419
|
+
allow_registration: bool | None = None
|
|
420
|
+
invitation_only: bool | None = None
|
|
421
|
+
enforce_capacity: bool | None = None
|
|
422
|
+
allow_team_changes: bool | None = None
|
|
423
|
+
|
|
424
|
+
|
|
406
425
|
class SiteTreeNode(Row):
|
|
407
426
|
"""``GET /contest/site/tree/...`` — the contest tree, recursively."""
|
|
408
427
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Institutions: the unit's odd wire key, the grid's export path, and the CLI
|
|
1
|
+
"""Institutions: the unit's odd wire key, the grid's export path, and the CLI edits."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -75,8 +75,9 @@ def test_the_grid_exports_from_a_sibling_path():
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class _Fake:
|
|
78
|
-
def __init__(self) -> None:
|
|
78
|
+
def __init__(self, reply: Any = None) -> None:
|
|
79
79
|
self.sent: list[Any] = []
|
|
80
|
+
self.reply = reply
|
|
80
81
|
|
|
81
82
|
def __enter__(self) -> _Fake:
|
|
82
83
|
return self
|
|
@@ -88,7 +89,7 @@ class _Fake:
|
|
|
88
89
|
if op.request.method == "POST":
|
|
89
90
|
self.sent.append(op.request.json)
|
|
90
91
|
return None
|
|
91
|
-
return InstitutionUnit.model_validate(UNIT)
|
|
92
|
+
return self.reply if self.reply is not None else InstitutionUnit.model_validate(UNIT)
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
def test_set_unit_edits_nested_keys_and_sends_the_whole_object(monkeypatch):
|
|
@@ -113,6 +114,43 @@ def test_set_unit_edits_nested_keys_and_sends_the_whole_object(monkeypatch):
|
|
|
113
114
|
assert common.update_institution_unit({}).request.path == "/common/institutionunit"
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
def test_set_unit_types_values_by_the_model_not_by_their_look(monkeypatch):
|
|
118
|
+
"""`zip` is a string, so its leading zero survives; `latitude` is a float."""
|
|
119
|
+
fake = _Fake()
|
|
120
|
+
monkeypatch.setattr(main, "_client", lambda _ctx: fake)
|
|
121
|
+
result = CliRunner().invoke(
|
|
122
|
+
main.app, ["institution", "set-unit", "5695", "mailingAddress.zip=010000"]
|
|
123
|
+
)
|
|
124
|
+
assert result.exit_code == 0, result.output
|
|
125
|
+
assert fake.sent[0]["mailingAddress"]["zip"] == "010000"
|
|
126
|
+
|
|
127
|
+
result = CliRunner().invoke(main.app, ["institution", "set-unit", "5695", "latitude=north"])
|
|
128
|
+
assert result.exit_code == 1
|
|
129
|
+
assert len(fake.sent) == 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_set_site_types_a_plain_dict_by_site_settings(monkeypatch):
|
|
133
|
+
"""The read is a dict: typed by SiteSettings, an unmodeled key by its current value."""
|
|
134
|
+
current = {"id": 1, "capacity": 10, "allowRegistration": False, "futureFlag": False}
|
|
135
|
+
fake = _Fake(reply=current)
|
|
136
|
+
monkeypatch.setattr(main, "_client", lambda _ctx: fake)
|
|
137
|
+
result = CliRunner().invoke(
|
|
138
|
+
main.app,
|
|
139
|
+
[
|
|
140
|
+
"contest",
|
|
141
|
+
"set-site",
|
|
142
|
+
"1235",
|
|
143
|
+
"--site",
|
|
144
|
+
"1",
|
|
145
|
+
"capacity=50",
|
|
146
|
+
"allowRegistration=true",
|
|
147
|
+
"futureFlag=true",
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
assert result.exit_code == 0, result.output
|
|
151
|
+
assert fake.sent == [{"id": 1, "capacity": 50, "allowRegistration": True, "futureFlag": True}]
|
|
152
|
+
|
|
153
|
+
|
|
116
154
|
def test_set_unit_refuses_an_unknown_nested_key(monkeypatch):
|
|
117
155
|
fake = _Fake()
|
|
118
156
|
monkeypatch.setattr(main, "_client", lambda _ctx: fake)
|
|
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
|
|
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
|