API docs

lib50

exception lib50.ConnectionError(*args, **kwargs)

A lib50.Error signalling a connection has errored.

exception lib50.Error(*args, **kwargs)

A generic lib50 Error.

Variables

payload (dict) – arbitrary data

__init__(*args, **kwargs)
exception lib50.InvalidConfigError(*args, **kwargs)

A lib50.Error signalling that a config is invalid.

exception lib50.InvalidSlugError(*args, **kwargs)

A lib50.Error signalling that a slug is invalid.

exception lib50.MissingFilesError(files, dir=None)

A lib50.Error signaling that files are missing. This error’s payload has a files and dir key. MissingFilesError.payload["files"] are all the missing files in a list of strings. MissingFilesError.payload["dir"] is the current working directory (cwd) from when this error was raised.

__init__(files, dir=None)
Parameters

files (list of string(s) or Pathlib.path(s)) – the missing files that caused the error

exception lib50.MissingToolError(*args, **kwargs)

A more specific lib50.InvalidConfigError signalling that an entry for a tool is missing in the config.

class lib50.ProgressBar(message, output_stream=None)

A contextmanager that shows a progress bar starting with message.

Example usage:

from lib50 import ProgressBar
import time

with ProgressBar("uploading") as bar:
    time.sleep(5)
    bar.stop()
    time.sleep(5)
__init__(message, output_stream=None)
Parameters
  • message (str) – the message of the progress bar, what the user is waiting on

  • output_stream (a stream or file-like object) – a stream to write the progress bar to

stop()

Stop the progress bar.

class lib50.Slug(slug, offline=False, github_token=None)

A CS50 slug that uniquely identifies a location on GitHub.

A slug is formatted as follows: <org>/<repo>/<branch>/<problem> Both the branch and the problem can have an arbitrary number of slashes. lib50.Slug performs validation on the slug, by querrying GitHub, pulling in all branches, and then by finding a branch and problem that matches the slug.

Variables
  • org (str) – the GitHub organization

  • repo (str) – the GitHub repo

  • branch (str) – the branch in the repo

  • problem (str) – path to the problem, the directory containing .cs50.yml

  • slug (str) – string representation of the slug

  • offline (bool) – flag signalling whether the user is offline. If set to True, the slug is parsed locally.

  • origin (str) – GitHub url for org/repo including authentication.

Example usage:

from lib50._api import Slug

slug = Slug("cs50/problems/2019/x/hello")
print(slug.org)
print(slug.repo)
print(slug.branch)
print(slug.problem)
__init__(slug, offline=False, github_token=None)

Parse <org>/<repo>/<branch>/<problem_dir> from slug.

static normalize_case(slug)

Normalize the case of a slug in string form

exception lib50.TimeoutError(*args, **kwargs)

A lib50.Error signalling a timeout has occured.

exception lib50.TooManyFilesError(limit, dir=None)

A lib50.Error signaling that too many files were attempted to be included. The error’s payload has a dir and a limit key. TooManyFilesError.payload["dir"] is the directory in which the attempted submission occured. TooManyFilesError.payload["limit"] is the max number of files allowed

__init__(limit, dir=None)
lib50.authenticate(org, repo=None)

A contextmanager that authenticates a user with GitHub via SSH if possible, otherwise via HTTPS.

Parameters
  • org (str) – GitHub organisation to authenticate with

  • repo (str, optional) – GitHub repo (part of the org) to authenticate with. Default is the user’s GitHub login.

Returns

an authenticated user

Type

lib50.User

Example usage:

from lib50 import authenticate

with authenticate("me50") as user:
    print(user.name)
lib50.cd(dest)

A contextmanager for temporarily changing directory.

Parameters

dest (str or pathlib.Path) – the path to the directory

Returns

dest unchanged

Type

str or pathlib.Path

Example usage:

from lib50 import cd
import os

with cd("foo") as current_dir:
    print(os.getcwd())
lib50.check_github_status()

Pings the githubstatus API. Raises a ConnectionError if the Git Operations and/or API requests components show an increase in errors.

Returns

None

Type

None

Raises

lib50.ConnectionError – if the Git Operations and/or API requests components show an increase in errors.

lib50.connect(slug, config_loader, file_limit=10000)

Connects to a GitHub repo indentified by slug. Then parses the .cs50.yml config file with the config_loader. If not all required files are present, per the files tag in .cs50.yml, an Error is raised.

Parameters
  • slug (str) – the slug identifying a GitHub repo.

  • config_loader (lib50.config.Loader) – a config loader that is able to parse the .cs50.yml config file for a tool.

  • file_limit (int) – The maximum number of files that are allowed to be included.

Returns

the remote configuration (org, message, callback, results), and the input for a prompt (honesty question, included files, excluded files)

Type

tuple(dict, tuple(str, set, set))

Raises
  • lib50.InvalidSlugError – if the slug is invalid for the tool

  • lib50.Error – if no files are staged. For instance the slug expects .c files, but there are only .py files present.

Example usage:

from lib50 import connect
import submit50

open("hello.c", "w").close()

remote, (honesty, included, excluded) = connect("cs50/problems/2019/x/hello", submit50.CONFIG_LOADER)
lib50.fetch_config(slug)

Fetch the config file at slug from GitHub.

Parameters

slug (str) – a slug identifying a location on GitHub to fetch the config from.

Returns

the config in the form of unparsed json

Type

str

Raises

lib50.InvalidSlugError – if there is no config file at slug.

Example usage:

from lib50 import fetch_config

config = fetch_config("cs50/problems/2019/x/hello")
print(config)
lib50.files(patterns, require_tags=('require',), include_tags=('include',), exclude_tags=('exclude',), root='.', limit=10000)

Based on a list of patterns (lib50.config.TaggedValue) determine which files should be included and excluded. Any pattern tagged with a tag:

  • from include_tags will be included

  • from require_tags can only be a file, that will then be included. MissingFilesError is raised if missing.

  • from exclude_tags will be excluded

Parameters
  • patterns (list of lib50.config.TaggedValue) – patterns that are processed in order, to determine which files should be included and excluded.

  • require_tags (list of strings, optional) – tags that mark a file as required and through that included

  • include_tags (list of strings, optional) – tags that mark a pattern as included

  • exclude_tags (list of strings, optional) – tags that mark a pattern as excluded

  • root (str or pathlib.Path, optional) – the root directory from which to look for files. Defaults to the current directory.

  • limit (int) – Maximum number of files that can be globbed.

Returns

all included files and all excluded files

Type

tuple(set of strings, set of strings)

Example usage:

from lib50 import files
from lib50.config import TaggedValue

open("foo.py", "w").close()
open("bar.c", "w").close()
open("baz.h", "w").close()

patterns = [TaggedValue("*", "exclude"),
            TaggedValue("*.c", "include"),
            TaggedValue("baz.h", "require")]

print(files(patterns)) # prints ({'bar.c', 'baz.h'}, {'foo.py'})
lib50.get_local_slugs(tool, similar_to='')

Get all slugs for tool of which lib50 has a local copy. If similar_to is given, ranks and sorts local slugs by similarity to similar_to.

Parameters
  • tool (str) – tool for which to get the local slugs

  • similar_to (str, optional) – ranks and sorts local slugs by similarity to this slug

Returns

list of slugs

Type

list of strings

Example usage:

from lib50 import get_local_slugs

slugs = get_local_slugs("check50", similar_to="cs50/problems/2019/x/hllo")
print(slugs)
lib50.local(slug, offline=False, remove_origin=False, github_token=None)

Create/update local copy of the GitHub repo indentified by slug. The local copy is shallow and single branch, it contains just the last commit on the branch identified by the slug.

Parameters
  • slug (str) – the slug identifying a GitHub repo.

  • offline (bool, optional) – a flag that indicates whether the user is offline. If so, then the local copy is only checked, but not updated.

  • remove_origin (bool, optional) – a flag, that when set to True, will remove origin as a remote of the git repo.

  • github_token (str, optional) – a GitHub authentication token used to verify the slug, only needed if the slug identifies a private repo.

Returns

path to local copy

Type

pathlib.Path

Example usage:

from lib50 import local

path = local("cs50/problems/2019/x/hello")
print(list(path.glob("**/*")))
lib50.logout()

Log out from git.

Returns

None

Type

None

lib50.prepare(tool, branch, user, included)

A contextmanager that prepares git for pushing:

  • Check that there are no permission errors

  • Add necessities to git config

  • Stage files

  • Stage files via lfs if necessary

  • Check that atleast one file is staged

Parameters
  • tool (str) – name of the tool that started the push

  • branch (str) – git branch to switch to

  • user (lib50.User) – the user who has access to the repo, and will ultimately author a commit

  • included (list of string(s) or pathlib.Path(s)) – a list of files that are to be staged in git

Returns

None

Type

None

Example usage:

from lib50 import authenticate, prepare, upload

with authenticate("me50") as user:
    tool = "submit50"
    branch = "cs50/problems/2019/x/hello"
    with prepare(tool, branch, user, ["hello.c"]):
        upload(branch, user, tool, {})
lib50.push(tool, slug, config_loader, repo=None, data=None, prompt=<function <lambda>>, file_limit=10000)

Pushes to Github in name of a tool. What should be pushed is configured by the tool and its configuration in the .cs50.yml file identified by the slug. By default, this function pushes to https://github.com/org=me50/repo=<username>/branch=<slug>.

lib50.push executes the workflow: lib50.connect, lib50.authenticate, lib50.prepare and lib50.upload.

Parameters
  • tool (str) – name of the tool that initialized the push

  • slug (str) – the slug identifying a .cs50.yml config file in a GitHub repo. This slug is also the branch in the student’s repo to which this will push.

  • config_loader (lib50.config.Loader) – a config loader for the tool that is able to parse the .cs50.yml config file for the tool.

  • repo (str, optional) – an alternative repo to push to, otherwise the default is used: github.com/me50/<github_login>

  • data (dict of strings, optional) – key value pairs that end up in the commit message. This can be used to communicate data with a backend.

  • prompt (lambda str, list, list => bool, optional) – a prompt shown just before the push. In case this prompt returns false, the push is aborted. This lambda function has access to an honesty prompt configured in .cs50,yml, and all files that will be included and excluded in the push.

  • file_limit (int) – maximum number of files to be matched by any globbing pattern.

Returns

GitHub username and the commit hash

Type

tuple(str, str)

Example usage:

from lib50 import push
import submit50

name, hash = push("submit50", "cs50/problems/2019/x/hello", submit50.CONFIG_LOADER)
print(name)
print(hash)
lib50.upload(branch, user, tool, data)

Commit + push to a branch

Parameters
  • branch (str) – git branch to commit and push to

  • user (lib50.User) – authenticated user who can push to the repo and branch

  • tool (str) – name of the tool that started the push

  • data (dict of strings) – key value pairs that end up in the commit message. This can be used to communicate data with a backend.

Returns

username and commit hash

Type

tuple(str, str)

Example usage:

from lib50 import authenticate, prepare, upload

with authenticate("me50") as user:
    tool = "submit50"
    branch = "cs50/problems/2019/x/hello"
    with prepare(tool, branch, user, ["hello.c"]):
        upload(branch, user, tool, {tool:True})
lib50.working_area(files, name='')

A contextmanager that copies all files to a temporary directory (the working area)

Parameters
  • files (list of string(s) or pathlib.Path(s)) – all files to copy to the temporary directory

  • name (str, optional) – name of the temporary directory

Returns

path to the working area

Type

pathlib.Path

Example usage:

from lib50 import working_area

with working_area(["foo.c", "bar.py"], name="baz") as area:
    print(list(area.glob("**/*")))

lib50.config

An API for retrieving and parsing .cs50.yml configs.

class lib50.config.Loader(tool, *global_tags, default=None)

A config loader (parser) that can parse a tools section of .cs50.yml config files.

The loader can be configured to parse and validate custom yaml tags. These tags can be global, in which case they can occur anywhere in a tool’s section. Or scoped to a top level key within a tool’s section, in which case these tags can only occur there.

Tags can also have defaults. In which case there does not need to be a value next to the tag in the config file.

Example usage:

from lib50 import local
from lib50.config import Loader, get_config_filepath

# Get a local config file
path = local("cs50/problems/2019/x/hello")
config_path = get_config_filepath(path)

# Create a loader for the tool submit50
loader = Loader("submit50")

# Allow the tags include/exclude/require to exist only within the files key of submit50
loader.scope("files", "include", "exclude", "require")

# Load, parse and validate the config file
with open(config_path) as f:
    config = loader.load(f.read())

print(config)
__init__(tool, *global_tags, default=None)
Parameters
  • tool (str) – the tool for which to load

  • global_tags (str) – any tags that can be applied globally (within the tool’s section)

  • default (anything, optional) – a default value for global tags

load(content, validate=True)

Parse yaml content.

Parameters
  • content (str) – the content of a config file

  • validate (bool) – if set to False, no validation will be performed. Tags can then occur anywhere.

Returns

the parsed config

Type

dict

Raises
scope(key, *tags, default=None)

Only apply tags and default for top-level key of the tool’s section. This effectively scopes the tags to just that top-level key.

Parameters
  • key (str) – the top-level key

  • tags (str) – any tags that can be applied within the top-level key

  • default (anything, optional) – a default value for these tags

Returns

None

Type

None

class lib50.config.TaggedValue(value, tag)

A value tagged in a .yml file

__init__(value, tag)
Parameters
  • value (str) – the tagged value

  • tag (str) – the yaml tag, with or without the syntactically required !

lib50.config.get_config_filepath(path)

Looks for the following files in order at path:

  • .cs50.yaml

  • .cs50.yml

If only one exists, returns path to that file (i.e. <path>/.cs50.yaml or <path>/.cs50.yml) Raises lib50.Error otherwise.

Parameters

path (str or pathlib.Path) – local path to a .cs50.yml config file

Returns

path to the config file

Type

pathlib.Path

Raises

lib50.Error – if zero or more than one config files exist

Example usage:

from lib50 import local
from lib50.config import get_config_filepath

path = local("cs50/problems/2019/x/hello")
config_path = get_config_filepath(path)
print(config_path)

lib50.crypto

An API for verifying signed payloads such as check50 results.

lib50.crypto.load_private_key(pem_str, password=None)

Load a private key from a PEM string.

“PEM is an encapsulation format, meaning keys in it can actually be any of several different key types. However these are all self-identifying, so you don’t need to worry about this detail. PEM keys are recognizable because they all begin with -----BEGIN {format}----- and end with -----END {format}-----.”

Parameters
  • pem_str (str) – the private key to load in PEM format

  • password (str, optional) – a password to decode the pem_str

Returns

a key from cryptography.hazmat

Type

One of RSAPrivateKey, DSAPrivateKey, DHPrivateKey, or EllipticCurvePrivateKey

lib50.crypto.load_public_key(pem_str)

Load a public key from a PEM string.

“PEM is an encapsulation format, meaning keys in it can actually be any of several different key types. However these are all self-identifying, so you don’t need to worry about this detail. PEM keys are recognizable because they all begin with -----BEGIN {format}----- and end with -----END {format}-----.”

Parameters

pem_str (str) – the public key to load in PEM format

Returns

a key from cryptography.hazmat

Type

One of RSAPrivateKey, DSAPrivateKey, DHPrivateKey, or EllipticCurvePrivateKey

lib50.crypto.sign(payload, private_key)

Sign a payload with a private key.

Parameters
  • payload (str) – the payload

  • private_key – a private key from lib50.crypto.load_private_key

Returns

base64 encoded signature

Type

bytes

lib50.crypto.verify(payload, signature, public_key)

Verify payload using (base64 encoded) signature and verification key. public_key should be obtained from load_public_key Uses RSA-PSS with SHA-512 and maximum salt length. The corresponding openssl command to create signatures that this function can verify is:

openssl dgst -sha512 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-2 -sign <PRIVATE_KEY> <PAYLOAD> | openssl base64 -A
Parameters
  • payload (str) – the payload

  • signature (bytes) – base64 encoded signature

  • public_key – a public key from lib50.crypto.load_public_key

Returns

True iff the payload could be verified

Type

bool