{"id":18162714,"url":"https://github.com/sco1/zwom","last_synced_at":"2025-10-10T08:39:06.339Z","repository":{"id":62230421,"uuid":"548510774","full_name":"sco1/zwom","owner":"sco1","description":"Python toolkit for the ZWO minilang","archived":false,"fork":false,"pushed_at":"2025-07-08T16:20:36.000Z","size":250,"stargazers_count":1,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-16T07:09:30.964Z","etag":null,"topics":["cli","cycling","cycling-workouts","parser","python","python3","python311","zwift"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sco1.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"sco1"}},"created_at":"2022-10-09T18:09:48.000Z","updated_at":"2025-07-08T16:20:41.000Z","dependencies_parsed_at":"2023-10-03T02:57:58.812Z","dependency_job_id":"21f8fff9-f8b9-46a4-adc5-3635c326df3e","html_url":"https://github.com/sco1/zwom","commit_stats":{"total_commits":36,"total_committers":2,"mean_commits":18.0,"dds":0.2777777777777778,"last_synced_commit":"62f25dd48a80801361bed2bab254677d476db832"},"previous_names":["sco1/zwo"],"tags_count":3,"template":false,"template_full_name":"sco1/py-template","purl":"pkg:github/sco1/zwom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sco1%2Fzwom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sco1%2Fzwom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sco1%2Fzwom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sco1%2Fzwom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sco1","download_url":"https://codeload.github.com/sco1/zwom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sco1%2Fzwom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279003276,"owners_count":26083555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","cycling","cycling-workouts","parser","python","python3","python311","zwift"],"created_at":"2024-11-02T10:05:02.883Z","updated_at":"2025-10-10T08:39:06.333Z","avatar_url":"https://github.com/sco1.png","language":"Python","funding_links":["https://github.com/sponsors/sco1"],"categories":[],"sub_categories":[],"readme":"# ZWO Minilang\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zwolang/0.3.0?logo=python\u0026logoColor=FFD43B)](https://pypi.org/project/zwolang/)\n[![PyPI](https://img.shields.io/pypi/v/zwolang)](https://pypi.org/project/zwolang/)\n[![PyPI - License](https://img.shields.io/pypi/l/zwolang?color=magenta)](https://github.com/sco1/zwom/blob/master/LICENSE)\n[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/sco1/zwom/main.svg)](https://results.pre-commit.ci/latest/github/sco1/zwom/main)\n\nPython toolkit for the ZWO minilang.\n\n## Installation\nInstall from PyPi with your favorite `pip` invocation:\n\n```bash\n$ pip install zwolang\n```\n\nYou can confirm proper installation via the `zwom` CLI:\n\u003c!-- [[[cog\nimport cog\nfrom subprocess import PIPE, run\nout = run([\"zwom\", \"--help\"], stdout=PIPE, encoding=\"ascii\")\ncog.out(\n    f\"```\\n$ zwom --help\\n{out.stdout.rstrip()}\\n```\"\n)\n]]] --\u003e\n```\n$ zwom --help\nUsage: zwom [OPTIONS] COMMAND [ARGS]...\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  single  Convert the specified `*.zwom` file to Zwift's `*.zwo`.\n  batch   Discover and convert all `*.zwom` files in the given directory.\n```\n\u003c!-- [[[end]]] --\u003e\n\n## The ZWOM File Specification\nThe primary purpose of this package is to provide a simple, human-readable format for constructing Zwift workouts that can be used to generate the actual workout XML. Let's call it a `*.zwom` file, or ZWOM.\n\nZWOM files are parsed using a [Parsimonious](https://github.com/erikrose/parsimonious) grammar, as specified below:\n\u003c!-- [[[cog\nfrom textwrap import dedent\nimport cog\nfrom zwo.parser import RAW_GRAMMAR\ncog.out(\n    f\"```{dedent(RAW_GRAMMAR)}```\"\n)\n]]] --\u003e\n```\nworkout   = ((comment / block) elws*)+ / elws\nblock     = tag ws \"{\" ((comment / params) / elws)+ \"}\"\nparams    = (message / value) \",\"?\nvalue     = tag ws (string / range / rangeval)\n\nmessage   = \"@\" ws duration ws string\nrange     = rangeval ws \"-\u003e\" ws rangeval\nrangeval  = duration / numeric / zone\nduration  = number \":\" number\npercent   = number \"%\"\nzone      = (\"Z\" number) / \"SS\"\nnumeric   = percent / number\nelws      = ws / emptyline\n\ncomment   = ~r\"\\;[^\\r\\n]*\"\ntag       = ~\"[A-Z_]+\"\nstring    = ~'\"[^\\\"]+\"'\nnumber    = ~\"\\d+\"\nws        = ~\"\\s*\"\nemptyline = ws+\n```\n\u003c!-- [[[end]]] --\u003e\n\n### Syntax \u0026 Keywords\nLike Zwift's built-in workout builder, the ZWO minilang is a block-based system. Blocks are specified using a `\u003ctag\u003e {\u003cblock contents\u003e}` format supporting arbitrary whitespace.\n\nInline comments are also supported, denoted by a leading `;`.\n\n### Workout Metadata\nEach ZWO file must begin with a `META` block containing comma-separated parameters:\n\n| Keyword       | Description             | Accepted Inputs                | Optional?         |\n|---------------|-------------------------|--------------------------------|-------------------|\n| `NAME`        | Displayed workout name  | `str`                          | No                |\n| `AUTHOR`      | Workout author          | `str`                          | No                |\n| `DESCRIPTION` | Workout description     | `str`\u003csup\u003e1\u003c/sup\u003e              | No                |\n| `FTP`         | Rider's FTP             | `int`                          | Maybe\u003csup\u003e2\u003c/sup\u003e |\n| `TAGS`        | Workout tags            | String of hashtags\u003csup\u003e3\u003c/sup\u003e | Yes               |\n\n1. Multiline strings are supported\n2. Zwift's workouts are generated using FTP percentages rather than absolute watts, so your FTP is required if you want to use absolute watts in your ZWOM\n3. Tags are capped at 31 total characters, including spaces and hashtags. Zwift also provides 4 built-in tags (`#RECOVERY`, `#INTERVALS`, `#FTP`, and `#TT`) that may also be added and do not count against this total.\n\n### Workout Blocks\nFollowing the `META` block are your workout blocks:\n\n| Keyword     | Description        |\n|-------------|--------------------|\n| `FREE`      | Free ride          |\n| `COOLDOWN`  | Cooldown           |\n| `INTERVALS` | Intervals          |\n| `RAMP`      | Ramp               |\n| `SEGMENT`   | Steady segment     |\n| `WARMUP`    | Warmup             |\n\n**NOTE:** While there is no specific Ramp block in the workout building UI, some experimental observations have been made:\n  * If a Ramp is at the very beginning of the workout, Zwift serializes it as a Warmup block\n  * If there are multiple blocks in a workout and a Ramp is at the end, there are two paths:\n    * If the left power is higher than the right power, Zwift serializes it as a Cooldown block\n    * If the right power is higher than the left power, Zwift serializes it as a Ramp block\n  * If there are multiple blocks in a workout and a Ramp is not at the beginning nor the end, Zwift serializes it as a Ramp block\n\nWhen writing your `*.zwom` file, these 3 blocks can be used interchangably, and ZWOM will try to match this behavior when outputting its `*.zwo` file. Zwift may do its own normalization if edits are made in the workout UI.\n\n### Workout Block Metadata\nWorkout blocks can contain the following (optionally) comma-separated parameters:\n\n| Keyword    | Description         | Accepted Inputs                                    | Optional?                |\n|------------|---------------------|----------------------------------------------------|--------------------------|\n| `DURATION` | Block duration      | `MM:SS`, Range\u003csup\u003e1\u003c/sup\u003e                         | No                       |\n| `CADENCE`  | Target cadence      | `int`, Range\u003csup\u003e1,2\u003c/sup\u003e                         | Yes                      |\n| `REPEAT`   | Number of intervals | `int`                                              | Only valid for intervals |\n| `POWER`    | Target power        | `int`, `int%`, Zone\u003csup\u003e3\u003c/sup\u003e, Range\u003csup\u003e1\u003c/sup\u003e | Mostly no\u003csup\u003e4\u003c/sup\u003e    |\n| `@`        | Display a message   | `@ MM:SS str`\u003csup\u003e5\u003c/sup\u003e                          | Yes                      |\n\n1. For Interval \u0026 Ramp segments, the range syntax can be used to set values for the `\u003cleft\u003e -\u003e \u003cright\u003e` segments (e.g. `65% -\u003e 120%` or `Z2 -\u003e Z6`)\n2. Cadence ranges are only valid for Interval segments\n3. Zones may be specified as `Z1-7` or `SS`\n4. Power is ignored for Free segments\n5. Message timestamps are relative to their containing block\n\n### Repeating a Chunk of Blocks\nThe `START_REPEAT` and `END_REPEAT` meta blocks are provided to specify an arbitrary chunk of blocks to repeat. The `START_REPEAT` block must specify a `REPEAT` parameter; `END_REPEAT` accepts no parameters. Nested repeats are not currently supported.\n\nFor example:\n\n```\nSEGMENT {DURATION 2:00, POWER 65%}\nRAMP {\n    DURATION 2:00,\n    POWER 120% -\u003e 140%,\n    @ 0:00 \"Here goes the ramp!\",\n    @ 1:50 \"10 seconds left!\",\n}\nSEGMENT {DURATION 2:00, POWER 65%}\nRAMP {\n    DURATION 2:00,\n    POWER 120% -\u003e 140%,\n    @ 0:00 \"Here goes the ramp!\",\n    @ 1:50 \"10 seconds left!\",\n}\n```\nBecomes:\n\n```\nSTART_REPEAT {REPEAT 2}\nSEGMENT {DURATION 2:00, POWER 65%}\nRAMP {\n    DURATION 2:00,\n    POWER 120% -\u003e 140%,\n    @ 0:00 \"Here goes the ramp!\",\n    @ 1:50 \"10 seconds left!\",\n}\nEND_REPEAT {}\n```\n\n## Sample Workout\n```\n; Here is a workout-level comment!\nMETA {\n    NAME \"Sample Workout\",\n    AUTHOR \"sco1\",\n    DESCRIPTION \"Here's a description!\n\n    Descriptions may be on more than one line too!\",\n    TAGS \"#RECOVERY #super #sweet #workout\",\n    FTP 270,\n}\nFREE {DURATION 10:00}\nINTERVALS {\n    ; Here is a block-level comment!\n    REPEAT 3,\n    DURATION 1:00 -\u003e 0:30,\n    POWER 55% -\u003e 78%,\n    CADENCE 85 -\u003e 110,\n}\nSEGMENT {DURATION 2:00, POWER 65%}\nRAMP {\n    DURATION 2:00,\n    POWER 120% -\u003e 140%,\n    @ 0:00 \"Here goes the ramp!\",\n    @ 1:50 \"10 seconds left!\",\n}\nFREE {DURATION 10:00}\n```\n\n![Workout Screenshot](https://raw.githubusercontent.com/sco1/sco1.github.io/master/zwo/sample_zwift_workout.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsco1%2Fzwom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsco1%2Fzwom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsco1%2Fzwom/lists"}