From 5de4d617095df7bd151be4fbe287c9de7f79a36d Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 24 Jul 2026 16:14:03 +0700 Subject: [PATCH 1/4] Revert "improve readme" This reverts commit 30f31758da3191ffedac78f2ebc4f4aa822a11d7. --- README.md | 246 ++++++++++++++++++++++++------------------------------ 1 file changed, 111 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 326a01d..372ec29 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,16 @@ # Next Commerce Theme Kit -[Theme Kit](https://github.com/NextCommerceCo/theme-kit) is a command line tool for developers to build and maintain storefront themes programmatically, allowing theme developers to: - -- Work on theme templates and assets using their local code editor or favorite IDE. -- Use git version control to work on a theme collectively with many theme collaborators. -- Use a pipeline to manage deployments of theme updates. +Theme Kit is a cross-platform command line tool to build and maintain storefront themes with [Sass Processing](#sass-processing) support on the Next Commerce platform. ## Installation -Theme Kit is a Python package available on [PyPi](https://pypi.org/project/next-theme-kit/). +Theme Kit is a python package available on [PyPi](https://pypi.org/project/next-theme-kit/) If you already have `python` and `pip`, install with the following command: -```bash -pip install next-theme-kit +``` +python -m pip install next-theme-kit ``` #### Mac OSX Requirements @@ -26,185 +22,165 @@ See how to install `python` and `pip` with [HomeBrew](https://docs.brew.sh/Homeb #### Windows Requirements -* **Option 1 (Recommended)** — Windows 10 and above feature WSL (Windows Subsystem for Linux) which provides a native Linux environment, see how to [Install WSL with Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install). Once you have installed WSL, follow the [best practice guides to configure and use with VS Code](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) and then follow the `pip` instructions above to install Theme Kit. -* **Option 2** — Installing `python` in Windows natively can be done through the [Windows App Store](https://apps.microsoft.com/store/detail/python-39/9P7QFQMJRFP7?hl=en-us&gl=us). Recommend using [Windows Powershell](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D?hl=en-us&gl=us). This route is a little more tricky and some knowledge on how to manage Python in Windows will be required. - -> [!TIP] -> **Use Python Virtual Environments** — For Mac, Windows, and Linux, it's a best practice to use a Python Virtual Environment to isolate Python packages and dependencies to reduce potential conflicts or errors, [more on creating a Python Virtual Environment](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/). - -## Setup +* **Option 1 (Recommended)** - Windows 10 and above feature WSL (Windows Subsystem for Linux) which provides a native Linux environment, see how to [Install WSL with Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install). Once you have installed WSL, follow the [best practice guides to configure and use with VS Code](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) and then follow the `pip` instructions above to install Theme Kit. -Connect `ntk` to a store in three steps. +* **Option 2** - Installing `python` in Windows natively can be done with through the [Windows App Store](https://apps.microsoft.com/store/detail/python-39/9P7QFQMJRFP7?hl=en-us&gl=us). Recommend using [Windows Powershell](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D?hl=en-us&gl=us). This route is a little more tricky and some knowledge on how to manage python in windows will be required. -### 1. Create the API Key +> **Use Python Virtual Environments** - For Mac, Windows, and Linux, it's a best practice to use a Python Virtual Environment to isolate python packages and dependecies to reduce potential conflicts or errors, [more on creating a Python Virutal Environment](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/). -Store authentication uses [OAuth 2.0](https://auth0.com/intro-to-iam/what-is-oauth-2/) and requires creating a store OAuth App with the `themes:read` and `themes:write` permissions. -1. In the Storefront admin, go to **Settings > API Access**. -2. Click **Create App**. -3. Give the app a name and assign a user. -4. In the **Permissions** tab, enable `themes:read` and `themes:write`. -5. **Save**. Copy the generated API key — you will need it in the next step. +#### Updating Theme Kit -### 2. Configure Theme Kit - -`ntk` reads its connection settings from two places: command flags (`--apikey`, `--store`, `--theme_id`) and the `config.yml` file in your theme directory. You do not need to create `config.yml` by hand — `ntk checkout` and `ntk init` write it for you, and after that commands run without flags: - -```yaml -development: - apikey: - store: https://{store}.29next.store - theme_id: +Update to the latest version of Theme Kit with the following command: ``` - -> [!WARNING] -> Keep the API key out of source control. Do not commit `config.yml` to git if it contains the key. - -> [!NOTE] -> `config.yml` supports multiple environments. Commands use the `development` entry by default; pass `-e` / `--env` to target another environment (for example `ntk push --env=production`). The `[development]` prefix in command output is the active environment. - -### 3. Connect to a Theme - -Work from a copy of an existing theme rather than an empty directory — a complete theme is the reference for the required directories, templates, and settings. - -**Work on a theme already on the store** — `ntk checkout` downloads the theme into your current directory and writes `config.yml`: - -```bash -ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store" +python -m pip install next-theme-kit --upgrade ``` -**Add a new theme to the store** — start from a copy of an existing theme, such as the [Intro Bootstrap](https://github.com/NextCommerceCo/intro-bootstrap) starter theme, then register it as a new theme with `ntk init` and upload the files with `ntk push`: - -```bash -ntk init --name="" --apikey="" --store="https://{store}.29next.store" -ntk push -``` +> :warning: **Important** +> +> As of version 1.0.2, the store authentication uses Oauth and requires creating a store **Oauth App** with `themes:read` and `themes:write` permissions. ## Usage - With the package installed, you can now use the commands inside your theme directory and work on a storefront theme. -| Command | Description | -| -------------- | ----------- | -| `ntk init` | Initialize a new theme | -| `ntk list` | List all available themes | -| `ntk checkout` | Checkout an existing theme | -| `ntk pull` | Download existing theme or theme file | -| `ntk push` | Push current theme state to store | -| `ntk watch` | Watch for local changes and automatically push changes to store | -| `ntk sass` | Process sass to css, see [Sass Processing](#sass-processing) | +**Available Commands** +* `ntk init` - initialize a new theme +* `ntk list` - list all available themes +* `ntk checkout` - checkout an existing theme +* `ntk pull` - download existing theme or theme file +* `ntk push` - push current theme state to store +* `ntk watch` - watch for local changes and automatically push changes to store +* `ntk sass` - process sass to css, see [Sass Processing](#sass-processing) -### Browse Store Themes +**Important** - You must pass the `apikey` and `store` parameters for all commands **if** there is not an existing `config.yml` file in your current directory. -To see what themes exist on the store, run `ntk list` to print the theme ID and name of each, with the active theme marked. +#### Init +Initialize a new theme which will create the theme on a store and create an initial config.yml file -```bash -ntk list ``` +ntk init --name="" --apikey="" --store="" +``` +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | +| -n | --name | Name of the new theme | -Output looks like: +#### List +List all themes installed on the theme. ``` -[development] Available themes: -[development] [42] Spring Launch -[development] [43] Holiday Promo (Active) +ntk list --apikey="" --store="" ``` +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | -If you do not have a `config.yml`, also pass `--apikey` and `--store`. - -### Work on an Existing Theme -To start working on a theme that already exists on the store, `ntk checkout` downloads it into your directory and writes `config.yml` with the theme ID. - -```bash -ntk checkout --theme_id= +#### Checkout +Checkout a theme from your store to pull it into your directory. ``` - -`--theme_id` / `-t` is required. If you do not have a `config.yml`, also pass `--apikey` and `--store`: - -```bash -ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store" +ntk checkout --theme_id= --apikey="" --store="" ``` - -`ntk checkout` differs from `ntk pull` in one way: `checkout` writes `config.yml` so the directory is ready for subsequent `ntk push` / `ntk watch` runs; `pull` downloads the same files without writing `config.yml`. - -### Add a New Theme to the Store - -`ntk init` registers your current directory as a new theme on the store and writes a `config.yml`. It does not download or scaffold any files — run it inside an existing theme codebase, then `ntk push` to upload the files. - -> [!WARNING] -> Building a theme from an empty directory is not advised. Start from a copy of a complete theme — the [Intro Bootstrap](https://github.com/NextCommerceCo/intro-bootstrap) starter theme or an existing theme from your store via [`ntk checkout`](#work-on-an-existing-theme). - -```bash -ntk init --name="" +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | +| -t | --theme_id | ID of the theme. | + +#### Pull +Pull a theme from your store to into your directory. ``` - -`--name` / `-n` is required. If you do not have a `config.yml` yet, also pass `--apikey` and `--store`: - -```bash -ntk init --name="" --apikey="" --store="https://{store}.29next.store" +ntk pull --theme_id= --apikey="" --store="" ``` +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | +| -t | --theme_id | ID of the theme. | -On success, `ntk init` logs the new theme ID and name, and persists the theme ID into `config.yml` so subsequent commands can omit `--theme_id`. - -### Sync Files to the Store - -To sync files between your local directory and the store, use `ntk push` to upload and `ntk pull` to download. Both upload or download the whole theme by default, and both accept file paths as positional arguments to limit the operation to specific files. - -> [!NOTE] -> File paths are relative to the theme root. `ntk push` only uploads files inside the theme directories (`assets`, `checkout`, `configs`, `layouts`, `locales`, `partials`, `sass`, `templates`) with valid theme file extensions — a path outside of them is skipped silently, not reported as an error. -| Example | Command | -| ------- | ------- | -| Push a single file | `ntk push templates/index.html` | -| Push a subset of files | `ntk push templates/index.html assets/main.css` | -| Pull a single file | `ntk pull templates/index.html` | -| Pull a subset of files | `ntk pull templates/index.html assets/main.css` | - -### Watch for File Changes +#### Push +Push all theme files from your local directory to the store. +``` +ntk push --theme_id= --apikey="" --store="" +``` +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | +| -t | --theme_id | ID of the theme. | -`ntk watch` monitors your theme directory and automatically pushes changed files to the store. Use it while you develop — save a file and the change is uploaded moments later. -```bash -ntk watch +#### Watch +Watch for file changes and additions in your local directory and automatically push them to the store. ``` +ntk watch --theme_id= --apikey="" --store="" +``` +##### Required flags without config.yml +| Short | Long | Description| +|--- | --- | --- | +| -a | --apikey | API Key used to connect to the store.| +| -s | --store | Full domain of the store. | +| -t | --theme_id | ID of the theme. | -On start, `ntk watch` logs the store, theme ID, a preview-theme URL, and the directory it is watching. Press `Ctrl + C` to stop. +#### Sass +Process `sass` files to CSS files for inclusion in your storefront. See [Sass Processing](#sass-processing) for more details. -> [!WARNING] -> Deletes sync too — deleting a local file while `ntk watch` is running deletes that file from the theme on the store. +``` +ntk sass +``` +##### Optional flags +| Short | Long | Description| +|--- | --- | --- | +| -sos | --sass_output_style | Options are nested, expanded, compact, or compressed| -> [!NOTE] -> `ntk watch` only uploads files with valid theme extensions. It does not accept file arguments. To scope changes to specific files, run `ntk push` with file paths instead. -### Sass Processing +## Sass Processing Theme kit includes support for Sass processing via [Python Libsass](https://sass.github.io/libsass-python/). Sass processing includes support for variables, imports, nesting, mixins, inheritance, custom functions, and more. -> [!WARNING] -> Sass processing is only supported on local, files in the `sass` directory are uploaded to your store for storage but cannot be edited in the store theme editor. - **How it works** - 1. Put `scss` files in top level `sass` directory. 2. Run `ntk sass` or `ntk watch` to process theme `sass` files. 3. Top level `scss` files will be processed to `css` files in the asset directory with the same name. **Example Theme with Sass Structure** - ``` ├── assets -│ ├── main.css // reference this asset file in templates +│   ├── styles.css // reference this asset file in templates ├── sass -│ ├── _base.scss -│ ├── _variables.scss -│ └── main.scss // processed to assets/main.css +│   ├── _base.scss +│   ├── _variables.scss +│   └── styles.scss // processed to assets/main.css ``` +**Important** - Sass processing is only supported on local, files in the `sass` directory are uploaded to your store for storage but cannot be edited in the store theme editor. + +**Configure Default Output Style** + +Change the default sass output style in `config.yml`, example below. + +``` +development: + apikey: + sass: + output_style: compressed // options: nested, expanded, compact, or compressed + store: + theme_id: +``` + + [codecov-image]: https://codecov.io/gh/29next/theme-kit/branch/master/graph/badge.svg?token=LPUOTZ5MZ5 [codecov-link]: https://codecov.io/gh/29next/theme-kit [pypi-v-image]: https://img.shields.io/pypi/v/next-theme-kit.svg [pypi-v-link]: https://pypi.org/project/next-theme-kit/ [GHAction-image]: https://github.com/NextCommerceCo/theme-kit/actions/workflows/test.yml/badge.svg?branch=master -[GHAction-link]: https://github.com/NextCommerceCo/theme-kit/actions?query=event%3appush+branch%3amaster +[GHAction-link]: https://github.com/NextCommerceCo/theme-kit/actions?query=event%3Apush+branch%3Amaster From 55f4effe0ad8bdaae1caab3955a2a800f8559b0b Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 24 Jul 2026 17:46:59 +0700 Subject: [PATCH 2/4] improve error handling --- ntk/decorator.py | 10 ++++++++++ ntk/gateway.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ntk/decorator.py b/ntk/decorator.py index 711960a..f87a116 100644 --- a/ntk/decorator.py +++ b/ntk/decorator.py @@ -1,5 +1,6 @@ import functools import logging +import sys logging.basicConfig( format='%(asctime)s %(levelname)s %(message)s', @@ -35,6 +36,15 @@ def _decorator(func): @functools.wraps(func) def _wrapper(self, *func_args, **func_kwargs): response = func(self, *func_args, **func_kwargs) + + if response.status_code == 401: + logging.error(f'Invalid API key for {self.store}.') + sys.exit(1) + + if response.status_code == 404: + logging.error(f'Theme or template not found for {self.store}.') + sys.exit(1) + error_default = f'{func.__name__.capitalize().replace("_", " ")} of {self.store} failed.' error_msg = "" content_type = response.headers.get('content-type', '').lower() diff --git a/ntk/gateway.py b/ntk/gateway.py index 2edfdd3..bda5149 100644 --- a/ntk/gateway.py +++ b/ntk/gateway.py @@ -14,7 +14,10 @@ def _request(self, request_type, url, apikey=None, payload={}, files={}): if apikey: headers = {'Authorization': f'Bearer {apikey}'} - response = requests.request(request_type, url, headers=headers, data=payload, files=files) + try: + response = requests.request(request_type, url, headers=headers, data=payload, files=files) + except requests.exceptions.ConnectionError: + return self._request(request_type, url, apikey, payload, files) if response.status_code == 429 and "throttled" in response.content.decode(): return self._request(request_type, url, apikey, payload, files) return response From cd3bf7df9f382e7c6cd268a9bf63fb7b07f3454f Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 24 Jul 2026 17:52:34 +0700 Subject: [PATCH 3/4] revert readme changes --- README.md | 246 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 135 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 372ec29..326a01d 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,20 @@ # Next Commerce Theme Kit -Theme Kit is a cross-platform command line tool to build and maintain storefront themes with [Sass Processing](#sass-processing) support on the Next Commerce platform. +[Theme Kit](https://github.com/NextCommerceCo/theme-kit) is a command line tool for developers to build and maintain storefront themes programmatically, allowing theme developers to: + +- Work on theme templates and assets using their local code editor or favorite IDE. +- Use git version control to work on a theme collectively with many theme collaborators. +- Use a pipeline to manage deployments of theme updates. ## Installation -Theme Kit is a python package available on [PyPi](https://pypi.org/project/next-theme-kit/) +Theme Kit is a Python package available on [PyPi](https://pypi.org/project/next-theme-kit/). If you already have `python` and `pip`, install with the following command: -``` -python -m pip install next-theme-kit +```bash +pip install next-theme-kit ``` #### Mac OSX Requirements @@ -22,165 +26,185 @@ See how to install `python` and `pip` with [HomeBrew](https://docs.brew.sh/Homeb #### Windows Requirements -* **Option 1 (Recommended)** - Windows 10 and above feature WSL (Windows Subsystem for Linux) which provides a native Linux environment, see how to [Install WSL with Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install). Once you have installed WSL, follow the [best practice guides to configure and use with VS Code](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) and then follow the `pip` instructions above to install Theme Kit. +* **Option 1 (Recommended)** — Windows 10 and above feature WSL (Windows Subsystem for Linux) which provides a native Linux environment, see how to [Install WSL with Ubuntu](https://docs.microsoft.com/en-us/windows/wsl/install). Once you have installed WSL, follow the [best practice guides to configure and use with VS Code](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) and then follow the `pip` instructions above to install Theme Kit. +* **Option 2** — Installing `python` in Windows natively can be done through the [Windows App Store](https://apps.microsoft.com/store/detail/python-39/9P7QFQMJRFP7?hl=en-us&gl=us). Recommend using [Windows Powershell](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D?hl=en-us&gl=us). This route is a little more tricky and some knowledge on how to manage Python in Windows will be required. + +> [!TIP] +> **Use Python Virtual Environments** — For Mac, Windows, and Linux, it's a best practice to use a Python Virtual Environment to isolate Python packages and dependencies to reduce potential conflicts or errors, [more on creating a Python Virtual Environment](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/). + +## Setup -* **Option 2** - Installing `python` in Windows natively can be done with through the [Windows App Store](https://apps.microsoft.com/store/detail/python-39/9P7QFQMJRFP7?hl=en-us&gl=us). Recommend using [Windows Powershell](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D?hl=en-us&gl=us). This route is a little more tricky and some knowledge on how to manage python in windows will be required. +Connect `ntk` to a store in three steps. -> **Use Python Virtual Environments** - For Mac, Windows, and Linux, it's a best practice to use a Python Virtual Environment to isolate python packages and dependecies to reduce potential conflicts or errors, [more on creating a Python Virutal Environment](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/). +### 1. Create the API Key +Store authentication uses [OAuth 2.0](https://auth0.com/intro-to-iam/what-is-oauth-2/) and requires creating a store OAuth App with the `themes:read` and `themes:write` permissions. -#### Updating Theme Kit +1. In the Storefront admin, go to **Settings > API Access**. +2. Click **Create App**. +3. Give the app a name and assign a user. +4. In the **Permissions** tab, enable `themes:read` and `themes:write`. +5. **Save**. Copy the generated API key — you will need it in the next step. -Update to the latest version of Theme Kit with the following command: +### 2. Configure Theme Kit + +`ntk` reads its connection settings from two places: command flags (`--apikey`, `--store`, `--theme_id`) and the `config.yml` file in your theme directory. You do not need to create `config.yml` by hand — `ntk checkout` and `ntk init` write it for you, and after that commands run without flags: + +```yaml +development: + apikey: + store: https://{store}.29next.store + theme_id: ``` -python -m pip install next-theme-kit --upgrade + +> [!WARNING] +> Keep the API key out of source control. Do not commit `config.yml` to git if it contains the key. + +> [!NOTE] +> `config.yml` supports multiple environments. Commands use the `development` entry by default; pass `-e` / `--env` to target another environment (for example `ntk push --env=production`). The `[development]` prefix in command output is the active environment. + +### 3. Connect to a Theme + +Work from a copy of an existing theme rather than an empty directory — a complete theme is the reference for the required directories, templates, and settings. + +**Work on a theme already on the store** — `ntk checkout` downloads the theme into your current directory and writes `config.yml`: + +```bash +ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store" ``` -> :warning: **Important** -> -> As of version 1.0.2, the store authentication uses Oauth and requires creating a store **Oauth App** with `themes:read` and `themes:write` permissions. +**Add a new theme to the store** — start from a copy of an existing theme, such as the [Intro Bootstrap](https://github.com/NextCommerceCo/intro-bootstrap) starter theme, then register it as a new theme with `ntk init` and upload the files with `ntk push`: + +```bash +ntk init --name="" --apikey="" --store="https://{store}.29next.store" +ntk push +``` ## Usage + With the package installed, you can now use the commands inside your theme directory and work on a storefront theme. -**Available Commands** -* `ntk init` - initialize a new theme -* `ntk list` - list all available themes -* `ntk checkout` - checkout an existing theme -* `ntk pull` - download existing theme or theme file -* `ntk push` - push current theme state to store -* `ntk watch` - watch for local changes and automatically push changes to store -* `ntk sass` - process sass to css, see [Sass Processing](#sass-processing) +| Command | Description | +| -------------- | ----------- | +| `ntk init` | Initialize a new theme | +| `ntk list` | List all available themes | +| `ntk checkout` | Checkout an existing theme | +| `ntk pull` | Download existing theme or theme file | +| `ntk push` | Push current theme state to store | +| `ntk watch` | Watch for local changes and automatically push changes to store | +| `ntk sass` | Process sass to css, see [Sass Processing](#sass-processing) | -**Important** - You must pass the `apikey` and `store` parameters for all commands **if** there is not an existing `config.yml` file in your current directory. +### Browse Store Themes -#### Init -Initialize a new theme which will create the theme on a store and create an initial config.yml file +To see what themes exist on the store, run `ntk list` to print the theme ID and name of each, with the active theme marked. +```bash +ntk list ``` -ntk init --name="" --apikey="" --store="" -``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | -| -n | --name | Name of the new theme | +Output looks like: -#### List -List all themes installed on the theme. ``` -ntk list --apikey="" --store="" +[development] Available themes: +[development] [42] Spring Launch +[development] [43] Holiday Promo (Active) ``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | +If you do not have a `config.yml`, also pass `--apikey` and `--store`. -#### Checkout -Checkout a theme from your store to pull it into your directory. -``` -ntk checkout --theme_id= --apikey="" --store="" -``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | -| -t | --theme_id | ID of the theme. | - -#### Pull -Pull a theme from your store to into your directory. -``` -ntk pull --theme_id= --apikey="" --store="" -``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | -| -t | --theme_id | ID of the theme. | +### Work on an Existing Theme +To start working on a theme that already exists on the store, `ntk checkout` downloads it into your directory and writes `config.yml` with the theme ID. -#### Push -Push all theme files from your local directory to the store. +```bash +ntk checkout --theme_id= ``` -ntk push --theme_id= --apikey="" --store="" -``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | -| -t | --theme_id | ID of the theme. | +`--theme_id` / `-t` is required. If you do not have a `config.yml`, also pass `--apikey` and `--store`: -#### Watch -Watch for file changes and additions in your local directory and automatically push them to the store. +```bash +ntk checkout --theme_id= --apikey="" --store="https://{store}.29next.store" ``` -ntk watch --theme_id= --apikey="" --store="" + +`ntk checkout` differs from `ntk pull` in one way: `checkout` writes `config.yml` so the directory is ready for subsequent `ntk push` / `ntk watch` runs; `pull` downloads the same files without writing `config.yml`. + +### Add a New Theme to the Store + +`ntk init` registers your current directory as a new theme on the store and writes a `config.yml`. It does not download or scaffold any files — run it inside an existing theme codebase, then `ntk push` to upload the files. + +> [!WARNING] +> Building a theme from an empty directory is not advised. Start from a copy of a complete theme — the [Intro Bootstrap](https://github.com/NextCommerceCo/intro-bootstrap) starter theme or an existing theme from your store via [`ntk checkout`](#work-on-an-existing-theme). + +```bash +ntk init --name="" ``` -##### Required flags without config.yml -| Short | Long | Description| -|--- | --- | --- | -| -a | --apikey | API Key used to connect to the store.| -| -s | --store | Full domain of the store. | -| -t | --theme_id | ID of the theme. | -#### Sass -Process `sass` files to CSS files for inclusion in your storefront. See [Sass Processing](#sass-processing) for more details. +`--name` / `-n` is required. If you do not have a `config.yml` yet, also pass `--apikey` and `--store`: +```bash +ntk init --name="" --apikey="" --store="https://{store}.29next.store" ``` -ntk sass + +On success, `ntk init` logs the new theme ID and name, and persists the theme ID into `config.yml` so subsequent commands can omit `--theme_id`. + +### Sync Files to the Store + +To sync files between your local directory and the store, use `ntk push` to upload and `ntk pull` to download. Both upload or download the whole theme by default, and both accept file paths as positional arguments to limit the operation to specific files. + +> [!NOTE] +> File paths are relative to the theme root. `ntk push` only uploads files inside the theme directories (`assets`, `checkout`, `configs`, `layouts`, `locales`, `partials`, `sass`, `templates`) with valid theme file extensions — a path outside of them is skipped silently, not reported as an error. + +| Example | Command | +| ------- | ------- | +| Push a single file | `ntk push templates/index.html` | +| Push a subset of files | `ntk push templates/index.html assets/main.css` | +| Pull a single file | `ntk pull templates/index.html` | +| Pull a subset of files | `ntk pull templates/index.html assets/main.css` | + +### Watch for File Changes + +`ntk watch` monitors your theme directory and automatically pushes changed files to the store. Use it while you develop — save a file and the change is uploaded moments later. + +```bash +ntk watch ``` -##### Optional flags -| Short | Long | Description| -|--- | --- | --- | -| -sos | --sass_output_style | Options are nested, expanded, compact, or compressed| +On start, `ntk watch` logs the store, theme ID, a preview-theme URL, and the directory it is watching. Press `Ctrl + C` to stop. + +> [!WARNING] +> Deletes sync too — deleting a local file while `ntk watch` is running deletes that file from the theme on the store. + +> [!NOTE] +> `ntk watch` only uploads files with valid theme extensions. It does not accept file arguments. To scope changes to specific files, run `ntk push` with file paths instead. +### Sass Processing -## Sass Processing Theme kit includes support for Sass processing via [Python Libsass](https://sass.github.io/libsass-python/). Sass processing includes support for variables, imports, nesting, mixins, inheritance, custom functions, and more. +> [!WARNING] +> Sass processing is only supported on local, files in the `sass` directory are uploaded to your store for storage but cannot be edited in the store theme editor. + **How it works** + 1. Put `scss` files in top level `sass` directory. 2. Run `ntk sass` or `ntk watch` to process theme `sass` files. 3. Top level `scss` files will be processed to `css` files in the asset directory with the same name. **Example Theme with Sass Structure** + ``` ├── assets -│   ├── styles.css // reference this asset file in templates +│ ├── main.css // reference this asset file in templates ├── sass -│   ├── _base.scss -│   ├── _variables.scss -│   └── styles.scss // processed to assets/main.css +│ ├── _base.scss +│ ├── _variables.scss +│ └── main.scss // processed to assets/main.css ``` -**Important** - Sass processing is only supported on local, files in the `sass` directory are uploaded to your store for storage but cannot be edited in the store theme editor. - -**Configure Default Output Style** - -Change the default sass output style in `config.yml`, example below. - -``` -development: - apikey: - sass: - output_style: compressed // options: nested, expanded, compact, or compressed - store: - theme_id: -``` - - [codecov-image]: https://codecov.io/gh/29next/theme-kit/branch/master/graph/badge.svg?token=LPUOTZ5MZ5 [codecov-link]: https://codecov.io/gh/29next/theme-kit [pypi-v-image]: https://img.shields.io/pypi/v/next-theme-kit.svg [pypi-v-link]: https://pypi.org/project/next-theme-kit/ [GHAction-image]: https://github.com/NextCommerceCo/theme-kit/actions/workflows/test.yml/badge.svg?branch=master -[GHAction-link]: https://github.com/NextCommerceCo/theme-kit/actions?query=event%3Apush+branch%3Amaster +[GHAction-link]: https://github.com/NextCommerceCo/theme-kit/actions?query=event%3appush+branch%3amaster From 1314048296bc97118e6224af2feb1fc10113018a Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 24 Jul 2026 18:11:49 +0700 Subject: [PATCH 4/4] improve approach --- ntk/__main__.py | 7 +++++ ntk/command.py | 17 +++++++---- ntk/decorator.py | 10 +++---- ntk/exceptions.py | 14 +++++++++ ntk/gateway.py | 35 +++++++++++++++++----- tests/test_command.py | 15 ++++++++++ tests/test_gateway.py | 68 +++++++++++++++++++++++++++++++++++++++++-- tests/test_main.py | 34 ++++++++++++++++++++++ 8 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 ntk/exceptions.py create mode 100644 tests/test_main.py diff --git a/ntk/__main__.py b/ntk/__main__.py index ec73b1d..5da23e2 100644 --- a/ntk/__main__.py +++ b/ntk/__main__.py @@ -1,8 +1,10 @@ #!/usr/bin/env python import logging +import sys from requests.exceptions import HTTPError +from ntk.exceptions import NTKError from ntk.ntk_parser import Parser logging.basicConfig( @@ -19,6 +21,11 @@ def main(): args.func(args) except AttributeError: print('Use ntk -h or --help to see available commands') + except NTKError as e: + # print new line for support error on process progress bar + print() + logging.error(e) + sys.exit(1) except (TypeError, HTTPError) as e: # print new line for support error on process progress bar print() diff --git a/ntk/command.py b/ntk/command.py index 991f388..8a79690 100644 --- a/ntk/command.py +++ b/ntk/command.py @@ -12,6 +12,7 @@ SASS_EXTENSIONS, ) from ntk.decorator import parser_config +from ntk.exceptions import NTKError from ntk.gateway import Gateway from ntk.utils import get_template_name, progress_bar @@ -49,12 +50,16 @@ def _handle_files_change(self, changes): if not pathfile.endswith(valid_extensions): continue template_name = get_template_name(pathfile) - if event_type in [Change.added, Change.modified]: - logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') - self._push_templates([template_name], compile_sass=True) - elif event_type == Change.deleted: - logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') - self._delete_templates([template_name]) + try: + if event_type in [Change.added, Change.modified]: + logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') + self._push_templates([template_name], compile_sass=True) + elif event_type == Change.deleted: + logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}') + self._delete_templates([template_name]) + except NTKError as error: + # Keep watching on a transient failure instead of killing the watcher. + logging.error(f'[{self.config.env}] {error}') def _push_templates(self, template_names, compile_sass=False): template_names = self._get_accept_files(template_names) diff --git a/ntk/decorator.py b/ntk/decorator.py index f87a116..a814e38 100644 --- a/ntk/decorator.py +++ b/ntk/decorator.py @@ -1,6 +1,7 @@ import functools import logging -import sys + +from ntk.exceptions import NTKAuthError, NTKNotFoundError logging.basicConfig( format='%(asctime)s %(levelname)s %(message)s', @@ -38,12 +39,11 @@ def _wrapper(self, *func_args, **func_kwargs): response = func(self, *func_args, **func_kwargs) if response.status_code == 401: - logging.error(f'Invalid API key for {self.store}.') - sys.exit(1) + raise NTKAuthError(f'Invalid API key for {self.store}.') if response.status_code == 404: - logging.error(f'Theme or template not found for {self.store}.') - sys.exit(1) + raise NTKNotFoundError( + f'Not found: {response.url} — check the store URL and theme id.') error_default = f'{func.__name__.capitalize().replace("_", " ")} of {self.store} failed.' error_msg = "" diff --git a/ntk/exceptions.py b/ntk/exceptions.py new file mode 100644 index 0000000..ad67e43 --- /dev/null +++ b/ntk/exceptions.py @@ -0,0 +1,14 @@ +class NTKError(Exception): + """Base class for ntk errors surfaced to the CLI entry point.""" + + +class NTKAuthError(NTKError): + """Raised on a 401 response (invalid or missing API key).""" + + +class NTKNotFoundError(NTKError): + """Raised on a 404 response (store, theme, or template not found).""" + + +class NTKConnectionError(NTKError): + """Raised when a request cannot reach the store after retries.""" diff --git a/ntk/gateway.py b/ntk/gateway.py index bda5149..44678ed 100644 --- a/ntk/gateway.py +++ b/ntk/gateway.py @@ -1,7 +1,20 @@ +import time import requests from urllib.parse import urljoin from ntk.decorator import check_error +from ntk.exceptions import NTKConnectionError + +MAX_RETRIES = 3 +RETRY_BACKOFF_SECONDS = 1 + +# Transport-level failures that are worth retrying rather than surfacing as a raw stack trace. +TRANSIENT_EXCEPTIONS = ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.SSLError, +) class Gateway: @@ -14,13 +27,21 @@ def _request(self, request_type, url, apikey=None, payload={}, files={}): if apikey: headers = {'Authorization': f'Bearer {apikey}'} - try: - response = requests.request(request_type, url, headers=headers, data=payload, files=files) - except requests.exceptions.ConnectionError: - return self._request(request_type, url, apikey, payload, files) - if response.status_code == 429 and "throttled" in response.content.decode(): - return self._request(request_type, url, apikey, payload, files) - return response + for attempt in range(MAX_RETRIES): + try: + response = requests.request(request_type, url, headers=headers, data=payload, files=files) + except TRANSIENT_EXCEPTIONS: + if attempt == MAX_RETRIES - 1: + raise NTKConnectionError(f'Unable to reach {self.store} after {MAX_RETRIES} attempts.') + time.sleep(RETRY_BACKOFF_SECONDS * (2 ** attempt)) + continue + + throttled = response.status_code == 429 and "throttled" in response.content.decode() + if throttled and attempt < MAX_RETRIES - 1: + time.sleep(RETRY_BACKOFF_SECONDS * (2 ** attempt)) + continue + + return response @check_error(error_format='Missing Themes in {store}') def get_themes(self): diff --git a/tests/test_command.py b/tests/test_command.py index b6e5766..e7cf86a 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -487,6 +487,21 @@ def test_watch_command_with_create_image_file_should_call_gateway_with_correct_a ) self.assertIn(expected_call_added, self.mock_gateway.mock_calls) + @patch("ntk.command.Command._push_templates", autospec=True) + def test_watch_logs_and_continues_when_push_raises_ntk_error(self, mock_push_templates): + """A transient NTKError during watch should be logged, not kill the watcher.""" + from ntk.exceptions import NTKAuthError + mock_push_templates.side_effect = NTKAuthError('Invalid API key for http://development.com.') + self.command.config.parser_config(self.parser) + changes = [ + (Change.modified, './templates/index.html'), + ] + # Should not raise — the error is caught inside _handle_files_change. + with self.assertLogs(level='ERROR') as log: + self.command._handle_files_change(changes) + self.assertTrue( + any('Invalid API key for http://development.com.' in line for line in log.output)) + @patch("ntk.command.Command._get_accept_files", autospec=True) @patch("ntk.command.Command._compile_sass", autospec=True) def test_watch_command_with_sass_directory_should_call_compile_sass( diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 0165d24..73faba6 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -1,7 +1,10 @@ import unittest from unittest.mock import call, MagicMock, patch -from ntk.gateway import Gateway +import requests + +from ntk.exceptions import NTKAuthError, NTKConnectionError, NTKNotFoundError +from ntk.gateway import Gateway, MAX_RETRIES class TestGateway(unittest.TestCase): @@ -42,8 +45,9 @@ def test_request(self, mock_request): ] assert mock_request.mock_calls == expected_calls + @patch('ntk.gateway.time.sleep', autospec=True) @patch('ntk.gateway.requests.request', autospec=True) - def test_request_with_rate_limit_should_retry(self, mock_request): + def test_request_with_rate_limit_should_retry(self, mock_request, mock_sleep): mock_response_429 = MagicMock() mock_response_429.status_code = 429 mock_response_429.content.decode.return_value = "throttled" @@ -82,6 +86,66 @@ def test_request_with_rate_limit_should_retry(self, mock_request): ] assert mock_request.mock_calls == expected_calls + @patch('ntk.gateway.time.sleep', autospec=True) + @patch('ntk.gateway.requests.request', autospec=True) + def test_request_rate_limit_gives_up_after_max_retries(self, mock_request, mock_sleep): + mock_response_429 = MagicMock() + mock_response_429.status_code = 429 + mock_response_429.content.decode.return_value = "throttled" + + mock_request.return_value = mock_response_429 + + response = self.gateway._request('GET', 'http://simple.com/api/admin/themes/', apikey=self.apikey) + + assert mock_request.call_count == MAX_RETRIES + assert response is mock_response_429 + + @patch('ntk.gateway.time.sleep', autospec=True) + @patch('ntk.gateway.requests.request', autospec=True) + def test_request_retries_transient_error_then_succeeds(self, mock_request, mock_sleep): + mock_response_200 = MagicMock() + mock_response_200.status_code = 200 + + mock_request.side_effect = [requests.exceptions.ConnectionError(), mock_response_200] + + response = self.gateway._request('GET', 'http://simple.com/api/admin/themes/', apikey=self.apikey) + + assert mock_request.call_count == 2 + assert response is mock_response_200 + + @patch('ntk.gateway.time.sleep', autospec=True) + @patch('ntk.gateway.requests.request', autospec=True) + def test_request_raises_after_persistent_transient_error(self, mock_request, mock_sleep): + # A Timeout is one of the broadened transient exceptions that should be retried, not surfaced raw. + mock_request.side_effect = requests.exceptions.Timeout() + + with self.assertRaises(NTKConnectionError): + self.gateway._request('GET', 'http://simple.com/api/admin/themes/', apikey=self.apikey) + + assert mock_request.call_count == MAX_RETRIES + + ##### + # 401 / 404 handling (check_error decorator) + ##### + @patch('ntk.gateway.requests.request', autospec=True) + def test_request_401_raises_auth_error(self, mock_request): + mock_request.return_value.status_code = 401 + + with self.assertRaises(NTKAuthError) as error: + self.gateway.get_themes() + + self.assertEqual(str(error.exception), 'Invalid API key for http://simple.com.') + + @patch('ntk.gateway.requests.request', autospec=True) + def test_request_404_raises_not_found_error_with_url(self, mock_request): + mock_request.return_value.status_code = 404 + mock_request.return_value.url = 'http://simple.com/api/admin/themes/6/templates/' + + with self.assertRaises(NTKNotFoundError) as error: + self.gateway.get_templates(theme_id=6) + + self.assertIn('http://simple.com/api/admin/themes/6/templates/', str(error.exception)) + ##### # get_themes ##### diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..19b88e7 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import MagicMock, patch + +from ntk.__main__ import main +from ntk.exceptions import NTKNotFoundError + + +class TestMain(unittest.TestCase): + @patch('ntk.__main__.Parser', autospec=True) + def test_main_exits_1_on_ntk_error(self, mock_parser): + args = MagicMock() + args.func.side_effect = NTKNotFoundError( + 'Not found: http://simple.com/api/admin/themes/6/templates/ — check the store URL and theme id.') + mock_parser.return_value.create_parser.return_value.parse_args.return_value = args + + with self.assertRaises(SystemExit) as exit_context: + with self.assertLogs(level='ERROR') as log: + main() + + self.assertEqual(exit_context.exception.code, 1) + self.assertTrue(any('Not found' in line for line in log.output)) + + @patch('ntk.__main__.Parser', autospec=True) + def test_main_prints_help_hint_on_attribute_error(self, mock_parser): + args = MagicMock() + args.func.side_effect = AttributeError() + mock_parser.return_value.create_parser.return_value.parse_args.return_value = args + + # No command supplied — should print the help hint and not raise. + main() + + +if __name__ == '__main__': + unittest.main()