Metadata-Version: 2.4
Name: gslocal
Version: 0.1.0
Summary: Local Gradescope autograder runner
Author-email: Nicolas Asanov <nicolas.a.asanov@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Nicolas Asanov
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/naasanov/gslocal
Project-URL: Repository, https://github.com/naasanov/gslocal
Project-URL: Issues, https://github.com/naasanov/gslocal/issues
Project-URL: Changelog, https://github.com/naasanov/gslocal/blob/main/CHANGELOG.md
Keywords: gradescope,autograder,cli,docker,education
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Education
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: colorama
Requires-Dist: tomli; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# gslocal

Run your [Gradescope](https://gradescope.com) autograder locally against any submission, without uploading anything to Gradescope.

gslocal mirrors Gradescope's exact container workflow: it builds your autograder zip, spins up the same Docker base image Gradescope uses, mounts the submission, runs the grader, and prints a formatted score summary. Build and image caching mean repeat runs are fast.

---

## Requirements

- Python 3.9+
- [Docker Desktop](https://docs.docker.com/get-docker/) (running)
- Git

## Installation

```bash
pip install gslocal
```

---

## Quick Start

In your autograder project directory, run:

```bash
gslocal init
```

This walks you through creating a `gslocal.toml` config file. Then test a submission:

```bash
gslocal run path/to/submission.zip
```

---

## How It Works

gslocal expects your autograder project to produce a Gradescope-compatible zip file via a build command. It is agnostic about your build tool - Maven, Make, a shell script, anything that outputs a zip works.

On each run, gslocal:

1. Prepares the submission (zip, directory, or GitHub clone)
2. Runs your build command if source files changed since the last build
3. Generates a Dockerfile and builds a Docker image if the zip is fresh
4. Runs the container with the submission mounted, waits for results
5. Prints a formatted score summary

**Build caching:** gslocal hashes the files listed in your `watch` config. If nothing changed and the output zip exists, the build is skipped entirely. The Docker image is similarly only rebuilt when the zip changes.

---

## Configuration

gslocal is configured via a `gslocal.toml` file in your autograder project root. Run `gslocal init` to generate one interactively.

```toml
[build]
cmd = "mvn clean package"       # command to produce the autograder zip
zip = "target/*.zip"            # glob pattern locating the output zip
watch = ["src/**/*", "pom.xml"] # glob patterns to watch for rebuild triggering

[docker]
setup = "src/main/resources/setup.sh"  # path to setup.sh inside the project (required)
# metadata = "src/main/resources/mock_submission_metadata.json"  # optional

[run]
timeout = 60     # autograder timeout in seconds
verbose = false  # show full build/docker output instead of spinners
```

**Field reference:**

| Field | Required | Description |
|---|---|---|
| `build.cmd` | yes | Shell command to build the autograder zip |
| `build.zip` | yes | Glob pattern to locate the output zip |
| `build.watch` | yes | Glob patterns whose content is hashed to detect source changes |
| `docker.setup` | yes | Path to `setup.sh` relative to project root |
| `docker.metadata` | no | Path to mock submission metadata JSON; a bundled default is used if omitted |
| `run.timeout` | no | Container timeout in seconds (default: 60) |
| `run.verbose` | no | Show full build and Docker output (default: false) |

gslocal locates `gslocal.toml` by walking up the directory tree from your current working directory, so you can run it from any subdirectory of your project.

---

## Submission Types

gslocal accepts submissions in three formats:

```bash
# Zip file
gslocal run test-zips/solution.zip

# Local directory
gslocal run /path/to/student/project/

# GitHub URL (SSH or HTTPS)
gslocal run git@github.com:student/repo.git
gslocal run https://github.com/student/repo
```

GitHub submissions are cloned with `--depth 1`.

---

## Commands

### `gslocal run <submission>`

Run the autograder against a submission.

| Flag | Description |
|---|---|
| `-r, --rebuild` | Force rebuild of build command and Docker image |
| `-n, --no-build` | Skip build, use existing zip |
| `-i, --interactive` | Drop into a shell inside the container |
| `-c, --clean` | Delete temp files after the run |
| `-t, --timeout SECS` | Override timeout (also: `GSLOCAL_TIMEOUT` env var) |
| `-v, --verbose` | Show full build and Docker output |

Timeout precedence: `--timeout` flag > `GSLOCAL_TIMEOUT` env var > `gslocal.toml` value > 60s default.

After each run, the following are available for inspection:

```
.gslocal/
  temp/
    submission/   # prepared submission files
    results/      # results.json
    autograder/   # extracted autograder zip contents
```

### `gslocal init`

Generate a `gslocal.toml` in the current directory. Prompts for each field with example hints. Skipping a required field writes a placeholder value that gslocal will catch and refuse to run with.

| Flag | Description |
|---|---|
| `--placeholders` | Write config with all sentinel values, no prompts |

### `gslocal clean`

Remove local gslocal state for the current project.

| Flag | Description |
|---|---|
| *(none)* | Delete `.gslocal/temp/` only |
| `--image` | Also remove the Docker image for this project |
| `--all` | Delete `.gslocal/` entirely and remove Docker image (full reset) |

---

## Project State

gslocal stores all state in a `.gslocal/` directory at the project root. Add it to your `.gitignore` (`gslocal init` offers to do this automatically):

```
.gslocal/
```

---

## Adding gslocal to an Existing Autograder

1. Install gslocal: `pip install gslocal`
2. Run `gslocal init` in your autograder project root
3. Fill in your build command, zip output path, and the files to watch for changes
4. Add `.gslocal/` to `.gitignore`
5. Run `gslocal run <submission>` to test

---

## Contributing

Contributions are welcome. To get started:

```bash
git clone https://github.com/naasanov/gslocal.git
cd gslocal
pip install -e .
```

The source lives in `src/gslocal/`. Key modules:

| Module | Responsibility |
|---|---|
| `cli.py` | Argument parsing and subcommand dispatch |
| `config.py` | Config loading, validation, project root resolution |
| `commands/run.py` | Run orchestration |
| `commands/init.py` | Interactive config generation |
| `commands/clean.py` | State cleanup |
| `build.py` | Build invocation and hash-based caching |
| `submission.py` | Submission preparation (zip, dir, GitHub) |
| `docker.py` | Dockerfile generation, image build, container execution |
| `results.py` | Terminal formatting of `results.json` |

Please open an issue before starting work on a large change.

---

## Releasing

`gslocal` is released through the GitHub Actions workflow at `.github/workflows/publish.yml`.

Before cutting a release:

1. Update `version` in `pyproject.toml`
2. Add a matching section to `CHANGELOG.md`, for example `## [0.1.1] - 2026-05-02`
3. Merge the release-ready changes to `main`

Then, in GitHub:

1. Open `Actions`
2. Select `Release and Publish`
3. Click `Run workflow`
4. Enter the version number, such as `0.1.1`

The workflow will:

- verify the run is on `main`
- verify the requested version matches `pyproject.toml`
- verify `CHANGELOG.md` contains a section for that version
- fail if that version is already published on PyPI
- build the package and run `twine check`
- create the `v<version>` tag if needed
- create the GitHub Release from the matching changelog section if needed
- publish the package to PyPI

If the GitHub tag or release is created but PyPI publishing fails, fix the issue and rerun the workflow for the same version. If PyPI already has that version, publish a new patch version instead.

---

## License

[MIT](LICENSE)
