The official client library for Scoreganizer, based on requests
.
Supports:
- auth
- getting lists of tournaments
- participating in tournaments
- generating and getting tournament keys
- uploading scores to tournaments
In short, this library can be used to build a client that takes full advantage of the Scoreganizer API.
It was written for use in the VSH plugin, but also with the explicit intent that it will make third-party clients much easier to create - for example, if someone wants to make a client using Arbiter instead of ViennaSweeper.
The API itself may change, but the latest version of this library should always work with the currently deployed version of Scoreganizer. An effort will be made to avoid a situation where breaking changes in this client library and the Scoreganizer API conspire to break existing code without much advance warning. More concrete guarantees/deprecation timelines will be made/published when both APIs have seen some use, and 1.0.0 of this library is released.
Scoreganizer is a web platform that allows anyone to host Minesweeper tournaments.
Users participate in tournaments. Once a tournament starts, users can generate a tournament key. This key is then embedded into replays, either automatically by a client, or manually by the user for Arbiter, and is used to validate that replays were generated after the tournament started.
Uploads are only accepted until the tournament ends, validating that the replays were also generated before the tournament ended.
This is the basic workflow. The following variations exist:
- tournaments, while open entry by default, can also be configured to require confirmation by the host for players to participate
- hosts can invite specific players to tournaments
- different tournament modes exist, and may restrict the workflow:
- entry after the tournament has already started can be prohibited (for example, if the mode needs to know the number of players for some reason). No such modes exist yet.
- a mode can limit the valid upload period per user in other ways. For example, the
sum_timelimit
mode gives every player a fixed amount of time to play within a longer timespan once they generate their key.
>>> from scoreganizer_client_lib.scoreganizer import Scoreganizer
>>> from pprint import pprint
>>>
>>> sc = Scoreganizer(host="localhost", port=8000, https=False)
>>>
>>> sc.login("ralokt", "REDACTED")
'ralokt:9cb34fba788e2d81daeb40e0f0dbaec0558c6b72f0aa89dc59bbd2548b0577be'
>>>
>>> pprint(sc.tournaments.my_active())
[Tournament(id=35,
mode='sum',
modeparams='1+1+1',
name='dj42test2',
location='here',
start=datetime.datetime(2024, 5, 20, 13, 37),
end=datetime.datetime(2025, 5, 20, 13, 37),
open_entry=True,
hide_results=False,
status='participating'),
Tournament(id=36,
mode='sum',
modeparams='1+1+1',
name='dj42test3',
location='here',
start=datetime.datetime(2024, 5, 25, 7, 23, 17, 340015),
end=datetime.datetime(2025, 5, 24, 15, 42),
open_entry=True,
hide_results=False,
status='participating')]
>>>
>>> sc.tournaments.get_key(35)
'1_35_99a8472376717bc7a676876cf0d351e3'
>>>
>>> # after setting arbiter nick to "ralokt#1_35_99a8472376717bc7a676876cf0d351e3"
>>> sc.scores.upload_filename("/path/to/arbiter/HI-SCORE Beg_8.33_3BV=22_3BVs=2.64_ralokt.avf")
>>>
from scoreganizer_client_lib.scoreganizer import Scoreganizer
from pprint import pprint
Here, we just import things we will need.
>>> sc = Scoreganizer(host="localhost", port=8000, https=False)
Here, we make an instance of our API client.
This instance will remember our authentication credentials once we have logged in, and can also be used to update a file containing saved credentials.
>>> sc.login("ralokt", "REDACTED")
'ralokt:9cb34fba788e2d81daeb40e0f0dbaec0558c6b72f0aa89dc59bbd2548b0577be'
Here, we log in with a username and password. Note that while the instance remembers our credentials, they are also returned in case we want to manage them ourselves.
>>> pprint(sc.tournaments.my_active())
[Tournament(id=35,
mode='sum',
modeparams='1+1+1',
name='dj42test2',
location='here',
start=datetime.datetime(2024, 5, 20, 13, 37),
end=datetime.datetime(2025, 5, 20, 13, 37),
open_entry=True,
hide_results=False,
status='participating'),
Tournament(id=36,
mode='sum',
modeparams='1+1+1',
name='dj42test3',
location='here',
start=datetime.datetime(2024, 5, 25, 7, 23, 17, 340015),
end=datetime.datetime(2025, 5, 24, 15, 42),
open_entry=True,
hide_results=False,
status='participating')]
Here, we get a list of our active tournaments. Active tournaments are either in the
future, or happening right now. The my_active
endpoint only returns tournaments we
are already participating in (or were invited to, or requested to join).
>>> sc.tournaments.get_key(35)
'1_35_99a8472376717bc7a676876cf0d351e3'
Here, we get our tournament key. This can only be done once a tournament has started. The key is embedded in the replay in order to prove that the replay was created/the game played after the tournament started.
- For ViennaSweeper, the nickname field needs to be set to the key
- For Arbiter, the name field needs to be set to the key. A hash (
#
) can be used to denote the start of the key, so that players can prepend their name. For example, for the game in question, the nickname was set toralokt#1_35_99a8472376717bc7a676876cf0d351e3
>>> sc.scores.upload_filename("/path/to/arbiter/HI-SCORE Beg_8.33_3BV=22_3BVs=2.64_ralokt.avf")
Here, we upload a game to the tournament. sc.scores.upload_file
also exists for
filelike objects, and the uploaded mimetype can likewise be set.
from requests.exceptions import RequestException
from scoreganizer_client_lib import Scoreganizer
from scoreganizer_client_lib.exceptions import ScoreganizerInvalidLoginData
# connects to scoreganizer.net by default
sc = Scoreganizer()
while True:
username = input("username: ")
password = input("password: ")
try:
sc.login(username, password)
except ScoreganizerInvalidLoginData:
print("invalid login data!")
except RequestException as ex:
print("something went wrong while sending the request")
raise ex
except ScoreganizerError as ex:
print(f"unexpected response from the server: {ex.error}")
raise ex
else:
break
# we are logged in!
# (... do more stuff)
ScoreganizerError
is a common base class for all application-level errors.
However, we also need to keep an eye out for RequestException
, as something could go
wrong on a lower level.
Note that we aren't retrying here in case of network failure - that's because we don't
need to. The requests
session is already configured to back off and retry for quite a
bit, more than enough to weather a bit of temporary packet loss or a few HTTP 50x
responses/disconnects during a server restart.
In general, instances of ScoreganizerError
have an error
attribute that contains
the string error code sent by the server (for example: "invalid_login_data"
,
"not_logged_in"
, etc).
from scoreganizer_client_lib.scoreganizer import Scoreganizer
sc = Scoreganizer(
host="localhost",
port=8000,
https=False,
auth_filename="scoreganizer_auth.txt",
)
if not sc.token_status_ok():
username = input("username: ")
password = input("password: ")
sc.login(username, password)
# we are logged in!
# (... do more stuff)
Note the extra auth_filename
parameter.
This will
- create the auth file if it doesn't exist yet
- if it does, it will use the credentials contained
- it will also keep the file up to date with the current credentials - for example, if the token is refreshed
We then check if we're logged in, and only prompt the user for their username and password if we don't have valid credentials saved.
Usage of this library is pretty straightforward:
- Import the
scoreganizer_client_lib.Scoreganizer
class - Instantiate it with configuration options
- Call its methods that cause requests to be sent
With the last point, it's important to note that, being based on requests
, these
methods are synchronous, meaning that they only return when the request in question is
done. Also, requests
isn't thread-safe - so all calls to its methods should happen in
the same thread.
(Side note: There is also currently neither async support nor any utilities that make working around this limitation easier; this may change in the future, and contributions would be welcome!)
Methods are namespaced in a way that reflects the actual path of the endpoints. For
example, to get a list of active tournaments, you call
scoreganizer.tournaments.active()
.
Methods will either
-
Return a result if the request succeeded. Results already have python types and are ready for use - no shenanigans like conversion, extraction from JSON, etc necessary.
-
Raise
scoreganizer_client_lib.exceptions.ScoreganizerError
(or a subclass) if the request succeeded, but the server returned an error of some sort (for example, bad login data was sent). The exception has an attributeerror
that describes what went wrong.Some values of
error
cause a specific subclass to be raised; for example, iferror
is"too_early"
, thenScoreganizerTooEarly
will be raised.-
For requests relating to tournament keys, the server will include information on how long to wait to try again in the response. In these cases, a subclass of
scoreganizer_client_lib.exceptions.ScoreganizerWait
is raised. This is a subclass ofScoreganizerError
that also has await_time
attribute (which is adatetime.timedelta
).wait_time
is NOT the wait time provided by the server - rather, it's that minus the elapsed time of the request, to compensate for ping. There is also a convenience methoddo_wait
that will calltime.sleep
forwait_time
. Note that there is NO LIMIT here - you're responsible for making sure that this doesn't hang your program.
-
-
Or raise
requests.exceptions.RequestException
(or a subclass) if the request didn't succeed (for example, the server was offline, or the network connection was down).In future versions, this might change if we switch away from requests for something more async-friendly.
There is also
scoreganizer_client_lib.exceptions.NetworkException
, which is currently an alias ofrequests.exceptions.RequestException
, and can be imported instead if you want to future-proof your code and don't care about the details of what went wrong.
Note that this library is still in very early stages of development. While anything not documented here is considered an implementation detail that may change in patch versions without warning, this API may still change, too - however, unless otherwise specified, backwards-incompatible changes will be considered breaking, and trigger a bump of the minor version.
Scoreganizer.__init__(
host="scoreganizer.net",
port=443,
https=True,
http_adapter=None,
auth_filename=None,
)
host
- the host to connect to. Default: "scoreganizer.net"
port
- the port to connect to. Default: 443
https
- whether or not to use HTTPS. Default: True
http_adapter
- an instance of requests.adapters.HTTPAdapter
. Can be used to change
how requests
works under the hood. The default is to add a retry policy that will try
hard to get a request through - for details, check the source code, and only touch this
if you know what you're doing. MAY BE CHANGED OR REMOVED AT ANY TIME.
auth_filename
- path to a file. If set, will read this file on startup, use the
credentials contained, and write any new/changed credentials back to this file.
Default: None
Scoreganizer.login(
self,
username,
password,
)
Will attempt to get authentication credentials for the scoreganizer user with username
username
and password password
.
On success, returns credentials as a string. If auth_filename
was passed in
__init__
, the new credentials will be written to that file.
scoreganizer_client_lib.exceptions.InvalidLoginData
will be raised if the server
rejects the login data.
Scoreganizer.set_auth_str(
self,
auth_str,
)
Will set the authentication credentials in auth_str
. If auth_filename
was passed in
__init__
, the new credentials will be written to that file.
Scoreganizer.token_status()
MAY BE CHANGED OR REMOVED AT ANY TIME. - might replace with an enum, use
token_status_ok
where applicable.
Will check the status of the currently remembered auth token.
On success, returns one of:
"ok"
"ok_stale"
"expired"
"nonexistent"
Will set the authentication credentials in auth_str
. If auth_filename
was passed in
__init__
, the new credentials will be written to that file.
Scoreganizer.token_status_ok()
Returns a bool
indicating whether or not the auth token we have right now is OK to
use (whether or not we are logged in).
Scoreganizer.refresh_login()
Will attempt to obtain and set new authentication credentials.
Somewhat low-level - you probably want to use refresh_login_if_stale
instead.
Returns the new credentials.
scoreganizer_client_lib.exceptions.ScoreganizerTokenTooRecent
will be raised if the
server refused to regenerate a token because this was done recently.
scoreganizer_client_lib.exceptions.ScoreganizerNotLoggedIn
will be raised if no valid
auth credentials were provided.
Scoreganizer.refresh_login_if_stale()
Will obtain and set new authentication credentials if the current ones are valid, but stale.
Returns the new credentials, if applicable, None
otherwise.
Although this class is where those methods live, as stated above - use a Scoreganizer
instance to access them. (as in: sc = Scoreganizer(); sc.tournament.<method>
).
scoreganizer.tournaments.{all, archive, in_progress, upcoming, active, my_active}()
All of these methods return a generator of Tournament
instances on success. The only
difference is what will be included in the response:
-
all
- all tournaments that were ever created. -
archive
- all tournaments that have already ended. -
in_progress
- all tournaments that are in progress right now. -
upcoming
- all tournaments that haven't started yet. -
active
- all tournaments that are either inupcoming
orin_progress
. -
my_active
- all tournaments that are inactive
where the participation workflow has started for the current user, ie:- participation is confirmed, or
- the user has requested to participate, or
- the user was invited by the host
hosts that don't participate don't show up here. REQUIRES LOGIN.
scoreganizer.tournaments.participate(tournament)
Participate (or ask to participate if entry isn't open) in tournament
.
tournament
can be a Tournament
instance returned by one of the lists, but also
anything that can be converted to int
, in which case the tournament with that id
is
used.
Returns None
on success.
scoreganizer.tournaments.player_confirm(tournament)
Confirm participation in tournament
, where the player was invited by the host.
Outside of this precondition, behavior is undefined.
tournament
can be a Tournament
instance returned by one of the lists, but also
anything that can be converted to int
, in which case the tournament with that id
is
used.
Returns None
on success.
scoreganizer.tournaments.gen_key(tournament)
Attempts to generate a key for this user and tournament. Will return the tournament key
(a str
) on success.
May raise scoreganizer_client_lib.exceptions.ScoreganizerTooEarly
if the tournament
hasn't started yet. This is a subclass of ScoreganizerWait
.
May raise scoreganizer_client_lib.exceptions.ScoreganizerKeyExists
if a key was
already generated. In this case, you need to use get_key
to get the key.
scoreganizer.tournaments.get_key(tournament)
Attempts to get the key for this user and tournament. Will return the tournament key (a
str
) on success.
May raise scoreganizer_client_lib.exceptions.ScoreganizerNeverGenerated
if the key
was never generated (and never will be).
May raise scoreganizer_client_lib.exceptions.ScoreganizerNotGenerated
if the key
wasn't generated, but could be. In this case, you need to use gen_key
to generate the
key.
May raise scoreganizer_client_lib.exceptions.ScoreganizerNotGeneratedYet
if the key
wasn't generated, but can be in the future. This is a subclass of ScoreganizerWait
.
scoreganizer.tournaments.wait_key(tournament)
Utility method to generate and/or get the key for this user and tournament, handling
exceptions that don't preclude getting a key, and waiting if necessary. Will return the
tournament key (a str
) on success.
This method calls do_wait()
if either get_key
or gen_key
raise
ScoreganizerWait
. It can take a theoretically unlimited amount of time to execute.
Although this class is where those methods live, as stated above - use a Scoreganizer
instance to access them. (as in: sc = Scoreganizer(); sc.scores.<method>
).
scoreganizer.scores.upload_file(
file,
filename,
ext=None,
mime_type=None,
tries=10,
)
Upload the replay in the filelike object file
, setting the filename to filename
.
ext
- explicitly set the extension. Will be guessed from filename
if not passed.
One of "rmv"
, "avf"
.
mime_type
- explicitly set the MIME type for the upload. Will be guessed from ext
if not passed. One of "application/x-viennasweeper"
,
"application/x-minesweeper-arbiter"
tries
- how often the client will retry if the server tells the client to retry. This
can happen very rarely despite an upload being valid. Default: 10
(this is way, way
overkill, but should therefore be a safe default). DEFAULT MAY CHANGE AT ANY TIME.
scoreganizer.scores.upload_filename(
filename,
ext=None,
mime_type=None,
tries=10,
)
Just like upload_file
, but gets the file from the filesystem at the path filename
.