Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
pip >= 20
wheel >= 0.36
setuptools >= 50

# Temporarily include SRComp development branch
git+https://github.com/PeterJCLaw/srcomp@9dfcea54f073101da73e8fb62c8e4b108c118f1b # match-release
8 changes: 8 additions & 0 deletions docs/commands/release-match.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
release-match
=============

.. argparse::
:module: sr.comp.cli.command_line
:func: argument_parser
:prog: srcomp
:path: release-match
8 changes: 8 additions & 0 deletions docs/commands/reset-match.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
reset-match
===========

.. argparse::
:module: sr.comp.cli.command_line
:func: argument_parser
:prog: srcomp
:path: reset-match
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'python-dateutil >=2.2, <3',
'Fabric >= 2.7, <4',
'invoke >= 1.7, <3',
# TODO(PR): bump srcomp version once we know what version will include match-release
'sr.comp >=1.8, <2',
'reportlab >=3.1.44, <5',
'requests >=2.5.1, <3',
Expand Down
2 changes: 1 addition & 1 deletion sr/comp/cli/add_delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def get_current_match_start(compstate_path: Path) -> datetime.datetime:
from sr.comp.comp import SRComp
compstate = SRComp(compstate_path)
now = compstate.schedule.datetime_now
current_matches = tuple(compstate.schedule.matches_at(now))
current_matches = compstate.operations.get_matches_at(now).matches
if not current_matches:
raise Exception("Not currently in a match, specify a valid time instead")

Expand Down
4 changes: 4 additions & 0 deletions sr/comp/cli/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
list_midi_ports,
match_order_teams,
print_schedule,
release_match,
reset_match,
schedule_league,
scorer,
shift_matches,
Expand Down Expand Up @@ -60,6 +62,8 @@ def argument_parser() -> argparse.ArgumentParser:
list_midi_ports.add_subparser(subparsers)
match_order_teams.add_subparser(subparsers)
print_schedule.add_subparser(subparsers)
release_match.add_subparser(subparsers)
reset_match.add_subparser(subparsers)
schedule_league.add_subparser(subparsers)
scorer.add_subparser(subparsers)
shift_matches.add_subparser(subparsers)
Expand Down
146 changes: 146 additions & 0 deletions sr/comp/cli/release_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Release a given match in preparation for running it.

Commutates can opt-in to "match releasing" by having an `operations.yaml` file
which contains configuration for the release timings. This command edits that
file to mark the given match as having been released.

This is useful during match operations in the event a match needs to be
abandoned and re-run.
"""

from __future__ import annotations

import argparse
from collections.abc import Iterable
from pathlib import Path
from typing import TypeVar

from sr.comp.cli import add_delay, deploy
from sr.comp.match_period import Match
from sr.comp.types import MatchNumber, OperationsData

T = TypeVar('T')


def first(iterable: Iterable[T]) -> T:
return next(i for i in iterable)


def release_match(compstate_path: Path, match_number: MatchNumber | None) -> MatchNumber:
from sr.comp.cli import yaml_round_trip as yaml
from sr.comp.comp import SRComp
from sr.comp.match_operations import MatchState

operations_path = compstate_path / 'operations.yaml'
if not operations_path.exists():
raise Exception("Cannot release matches in a compstate without an 'operations.yaml'")

compstate = SRComp(compstate_path)
now = compstate.schedule.datetime_now
if match_number is None:
current_matches = compstate.operations.get_matches_at(now).matches
if not current_matches:
raise Exception("Not currently in a match, specify a valid match number instead")
match_number = min(x.num for x in current_matches)

match: Match = first(compstate.schedule.matches[match_number].values())
state = compstate.operations.get_match_state(match, now)
if state == MatchState.RELEASED:
print(f"Match {match_number} already released")
exit(1)

with yaml.edit(operations_path) as operations_yaml:
when = compstate.schedule.datetime_now.replace(microsecond=0)

operations: OperationsData = operations_yaml['operations']
operations['released_match'] = {
'number': match_number,
'time': when,
}

# Add delay if needed
times = compstate.operations.get_arena_times(match)
if when > times.release_threshold:
how_long = when - times.release_threshold
how_long_seconds = int(how_long.total_seconds())
print(
f"Match {match.num} originally scheduled at {times.start} will "
f"be scheduled at {times.start + how_long}.",
)

with yaml.edit(compstate_path / 'schedule.yaml') as schedule:
add_delay.add_delay(schedule, how_long_seconds, match.start_time)

# Self check
new_state = SRComp(compstate_path)
new_match: Match = first(new_state.schedule.matches[match_number].values())
new_times = new_state.operations.get_arena_times(new_match)
assert new_times.release_threshold == when, (new_times.release_threshold, when)

return match_number


def command(args: argparse.Namespace) -> None:
from sr.comp.raw_compstate import RawCompstate

compstate = RawCompstate(args.compstate, local_only=False)

deploy.require_no_changes(compstate)

if not args.no_pull:
with deploy.exit_on_exception():
compstate.pull_fast_forward()

match_number = release_match(args.compstate, args.match_number)

deploy.require_valid(compstate)

with deploy.exit_on_exception(kind=RuntimeError):
compstate.stage('schedule.yaml')
compstate.stage('operations.yaml')
msg = f"Release match {match_number}"
compstate.commit(msg)

if args.deploy:
hosts = deploy.get_deployments(compstate)
deploy.run_deployments(args, compstate, hosts)


def add_subparser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
help_msg, *_ = __doc__.strip().splitlines()
parser = subparsers.add_parser(
'release-match',
help=help_msg,
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
'--no-deploy',
dest='deploy',
action='store_false',
default=True,
help="Disable deploying the compstate after making the edit",
)
parser.add_argument(
'--no-pull',
action='store_true',
help="Skip updating to the latest revision",
)
deploy.add_options(parser)
parser.add_argument(
'compstate',
type=Path,
help="competition state repository",
)
parser.add_argument(
'--match-number',
type=int,
default=None,
help=(
"A specific match number to release. "
"Note that releasing a match implicitly includes all previous matches. "
"Defaults to the current match."
),
)
parser.set_defaults(func=command)
141 changes: 141 additions & 0 deletions sr/comp/cli/reset_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""
Reset the compstate back to before the start of the given match.

This is achieved by inserting a delay so that the given match is in the future.
The point in time before the match is either:
* the "release threshold" if the compstate has opted-in to match releasing, or
* the start of the slot

This is useful during match operations in the event a match needs to be
abandoned and re-run.
"""

from __future__ import annotations

import argparse
from collections.abc import Iterable
from pathlib import Path
from typing import TypeVar

from sr.comp.cli import add_delay, deploy
from sr.comp.match_period import Match
from sr.comp.types import MatchNumber, OperationsData

T = TypeVar('T')


def first(iterable: Iterable[T]) -> T:
return next(i for i in iterable)


def reset_match(compstate_path: Path, match_number: MatchNumber | None) -> MatchNumber:
from sr.comp.cli import yaml_round_trip as yaml
from sr.comp.comp import SRComp

compstate = SRComp(compstate_path)
if match_number is None:
now = compstate.schedule.datetime_now
current_matches = compstate.operations.get_matches_at(now).matches
if not current_matches:
raise Exception("Not currently in a match, specify a valid match number instead")
match_number = min(x.num for x in current_matches)

def get_match(num: MatchNumber) -> Match:
return first(compstate.schedule.matches[num].values())

current_match: Match = get_match(match_number)

delay_target = current_match.start_time

operations_path = compstate_path / 'operations.yaml'
if operations_path.exists():
# If the release threshold is explicitly set, use that rather than the
# slot start time as our delay target.
delay_target = compstate.operations.get_arena_times(current_match).release_threshold

with yaml.edit(operations_path) as operations_yaml:
operations: OperationsData = operations_yaml['operations']

previous_match_number = MatchNumber(match_number - 1)
if previous_match_number < 0:
operations['released_match'] = None
else:
previous_match = get_match(previous_match_number)
operations['released_match'] = {
'number': previous_match_number,
'time': compstate.operations.get_arena_times(previous_match).release_threshold, # noqa: E501
}

# Add delay
how_long = compstate.schedule.datetime_now - delay_target
how_long_seconds = int(how_long.total_seconds())

with yaml.edit(compstate_path / 'schedule.yaml') as schedule:
add_delay.add_delay(schedule, how_long_seconds, current_match.start_time)

return match_number


def command(args: argparse.Namespace) -> None:
from sr.comp.raw_compstate import RawCompstate

compstate = RawCompstate(args.compstate, local_only=False)

deploy.require_no_changes(compstate)

if not args.no_pull:
with deploy.exit_on_exception():
compstate.pull_fast_forward()

match_number = reset_match(args.compstate, args.match_number)

deploy.require_valid(compstate)

with deploy.exit_on_exception(kind=RuntimeError):
compstate.stage('schedule.yaml')
compstate.stage('operations.yaml')
msg = f"Reset match {match_number}"
compstate.commit(msg)

if args.deploy:
hosts = deploy.get_deployments(compstate)
deploy.run_deployments(args, compstate, hosts)


def add_subparser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
help_msg, *_ = __doc__.strip().splitlines()
parser = subparsers.add_parser(
'reset-match',
help=help_msg,
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
'--no-deploy',
dest='deploy',
action='store_false',
default=True,
help="Disable deploying the compstate after making the edit",
)
parser.add_argument(
'--no-pull',
action='store_true',
help="Skip updating to the latest revision",
)
deploy.add_options(parser)
parser.add_argument(
'compstate',
type=Path,
help="competition state repository",
)
parser.add_argument(
'--match-number',
type=int,
default=None,
help=(
"A specific match number to release. "
"Note that releasing a match implicitly includes all previous matches. "
"Defaults to the current match."
),
)
parser.set_defaults(func=command)
14 changes: 11 additions & 3 deletions sr/comp/cli/show_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,22 @@ def command(settings: argparse.Namespace) -> None:
from datetime import datetime, timedelta

from sr.comp.comp import SRComp
from sr.comp.match_operations import MatchState

comp = SRComp(os.path.realpath(settings.compstate))

num_teams_per_arena = getattr(comp, 'num_teams_per_arena', len(comp.corners))

matches = comp.schedule.matches
now = datetime.now(comp.timezone)
current_matches = list(comp.schedule.matches_at(now))
current_matches = comp.operations.get_matches_at(now).matches

if not settings.all:
time = now - timedelta(minutes=10)
if current_matches:
time = min(x.start_time for x in current_matches)
else:
time = now
time -= timedelta(minutes=10)

matches = [
slot
Expand Down Expand Up @@ -93,7 +98,10 @@ def should_show_seconds() -> bool:
print_col(m.display_name.center(DISPLAY_NAME_WIDTH))

if m in current_matches:
print(" *")
marker = " *"
if comp.operations.get_match_state(m, now) == MatchState.HELD:
marker += " [held]"
print(marker)
else:
print()

Expand Down
1 change: 1 addition & 0 deletions sr/comp/cli/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def command(args: argparse.Namespace) -> None:
for period in comp.schedule.match_periods:
print(f' · {format_period(period, match_duration)}')

print("Last released match: {}".format(comp.operations.last_released_match))
print("Last scored match: {}".format(comp.scores.last_scored_match))


Expand Down
1 change: 1 addition & 0 deletions tests/snapshots/summary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ Match periods:
· 2014-04-27 Sunday, 27 April 2014, morning (09:30–12:25, matches end 12:20)
· 2014-04-27 Sunday, 27 April 2014, afternoon (13:15–15:15, matches end 15:15)
· 2014-04-27 The Knockouts, Sunday, 27 April 2014, afternoon (15:30–17:35, matches end 17:30)
Last released match: 129
Last scored match: 99