Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ jobs:
python-version: 3.12

- name: Install build dependencies
run: python -m pip install build wheel
run: python -m pip install build

- name: Build distributions
shell: bash -l {0}
run: python setup.py sdist bdist_wheel
run: python -m build

- name: Publish package to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
Expand Down
79 changes: 79 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Next Commerce Theme Kit

CLI tool (`ntk`) for building and maintaining storefront themes on the Next Commerce platform. Supports Sass processing via `libsass`.

## Project Structure

```
ntk/
__main__.py # Entry point
command.py # All CLI commands (watch, push, pull, checkout, init, list, sass)
conf.py # Config loading and constants
decorator.py # @parser_config decorator for command validation
gateway.py # API client for Next Commerce store
utils.py # Helpers (get_template_name, progress_bar)
tests/
test_command.py
test_gateway.py
test_config.py
test_installer.py
```

## Development Setup

### Prerequisites

- Python 3.10 or higher — check with `python --version`
- `pip` — usually included with Python
- On macOS, Python can be installed via [Homebrew](https://brew.sh): `brew install python`
- On Windows, use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install) (recommended) or the [Windows App Store](https://apps.microsoft.com/store/detail/python-310/9PJPW5LDXLZ5)

### First-time Setup

```bash
# Clone the repo
git clone https://github.com/29next/theme-kit.git
cd theme-kit

# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate # Windows

# Install the package with test dependencies
pip install -e ".[test]"
```

### Installing the CLI Globally (for end users)

```bash
pip install next-theme-kit

# Or with pipx (recommended — keeps it isolated)
pipx install next-theme-kit
```

## Running Tests

```bash
pytest tests/ -v
pytest --cov=ntk --cov-report xml
```

## Key Dependencies

- `watchfiles` — file watching for `ntk watch` (replaced deprecated `watchgod`)
- `libsass` — Sass/SCSS processing
- `PyYAML` — config.yml parsing
- `requests` — HTTP client for store API

## Python Support

Requires Python >= 3.10. Tested against 3.10, 3.11, 3.12, 3.13, 3.14 via tox and GitHub Actions.

## Important Conventions

- Use `asyncio.run()` for async entry points — not `get_event_loop()` (broke in Python 3.12+)
- `watchfiles.Change` is an `IntEnum` — use `event_type.name.title()` for human-readable log output, not `str(event_type)`
- `watchfiles` internal logging is suppressed via `logging.getLogger('watchfiles').setLevel(logging.WARNING)`
- Test dependencies are declared as `extras_require={"test": ...}` in `setup.py`
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
[![Build Status][GHAction-image]][GHAction-link]
[![CodeCov][codecov-image]][codecov-link]

# 29 Next Theme Kit
# 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 29 Next platform.
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

Expand Down
11 changes: 5 additions & 6 deletions ntk/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import time
import sass

from watchgod import awatch
from watchgod.watcher import Change
from watchfiles import awatch, Change

from ntk.conf import (
Config, MEDIA_FILE_EXTENSIONS, GLOB_PATTERN, SASS_DESTINATION, SASS_SOURCE
Expand All @@ -21,6 +20,7 @@
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.getLogger('watchfiles').setLevel(logging.WARNING)


class Command:
Expand All @@ -46,10 +46,10 @@ def _handle_files_change(self, changes):
for event_type, pathfile in changes:
template_name = get_template_name(pathfile)
if event_type in [Change.added, Change.modified]:
logging.info(f'[{self.config.env}] {str(event_type)} {template_name}')
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}] {str(event_type)} {template_name}')
logging.info(f'[{self.config.env}] {event_type.name.title()} {template_name}')
self._delete_templates([template_name])

def _push_templates(self, template_names, compile_sass=False):
Expand Down Expand Up @@ -198,8 +198,7 @@ async def main():
async for changes in awatch('.'):
self._handle_files_change(changes)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())

@parser_config()
def compile_sass(self, parser):
Expand Down
12 changes: 7 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from setuptools import find_packages, setup

__version__ = '1.0.7'
__version__ = '1.1.0'

tests_require = [
"flake8==3.9.2",
"pytest==7.2.2"
"flake8",
"pytest",
"pytest-cov",
]

with open('README.md', 'r') as fh:
Expand All @@ -22,14 +23,15 @@
install_requires=[
"PyYAML>=5.4",
"requests>=2.25",
"watchgod>=0.7",
"watchfiles>=0.18",
"libsass>=0.21.0"
],
entry_points={
'console_scripts': [
'ntk = ntk.__main__:main',
],
},
extras_require={"test": tests_require},
packages=find_packages(),
python_requires='>=3.8'
python_requires='>=3.10'
)
67 changes: 66 additions & 1 deletion tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import unittest
from unittest.mock import call, MagicMock, mock_open, patch

from watchgod.watcher import Change
from watchfiles import Change

from ntk import conf
from ntk.command import Command
Expand Down Expand Up @@ -310,6 +310,62 @@ def test_pull_command_with_configs_and_filenames_should_be_download_only_file_in

mock_write_config.assert_not_called()

#####
# push
#####
def test_push_command_without_config_file_should_be_required_api_key_store_and_theme_id(self):
with self.assertRaises(TypeError) as error:
self.parser.apikey = None
self.parser.store = None
self.parser.theme_id = None
self.command.push(self.parser)
self.assertEqual(
str(error.exception), '[development] argument -a/--apikey, -s/--store, -t/--theme_id are required.')

@patch("ntk.command.Command._get_accept_files", autospec=True)
def test_push_command_with_configs_and_without_filenames_should_upload_all_files(
self, mock_get_accept_files
):
mock_get_accept_files.return_value = [
f'{os.getcwd()}/layout/base.html',
]
self.mock_gateway.return_value.create_or_update_template.return_value.ok = True
self.mock_gateway.return_value.create_or_update_template.return_value.headers = {
'content-type': 'application/json; charset=utf-8'}
self.command.config.parser_config(self.parser)
self.parser.filenames = None
with patch("builtins.open", self.mock_file):
self.command.push(self.parser)
expected_call = call().create_or_update_template(
theme_id=1234,
template_name='layout/base.html',
content='{% load i18n %}\n\n<div class="mt-2">My home page</div>',
files={}
)
self.assertIn(expected_call, self.mock_gateway.mock_calls)

@patch("ntk.command.Command._get_accept_files", autospec=True)
def test_push_command_with_filenames_should_upload_only_specified_files(
self, mock_get_accept_files
):
mock_get_accept_files.return_value = [
f'{os.getcwd()}/layout/base.html',
]
self.mock_gateway.return_value.create_or_update_template.return_value.ok = True
self.mock_gateway.return_value.create_or_update_template.return_value.headers = {
'content-type': 'application/json; charset=utf-8'}
self.command.config.parser_config(self.parser)
self.parser.filenames = ['layout/base.html']
with patch("builtins.open", self.mock_file):
self.command.push(self.parser)
expected_call = call().create_or_update_template(
theme_id=1234,
template_name='layout/base.html',
content='{% load i18n %}\n\n<div class="mt-2">My home page</div>',
files={}
)
self.assertIn(expected_call, self.mock_gateway.mock_calls)

#####
# watch (_handle_files_change)
#####
Expand Down Expand Up @@ -397,6 +453,15 @@ def test_watch_command_with_sass_directory_should_call_compile_sass(
self.command._handle_files_change(changes)
mock_compile_sass.assert_called_once()

@patch("ntk.command.asyncio.run")
@patch("ntk.command.awatch", autospec=True)
def test_watch_command_uses_asyncio_run(self, mock_awatch, mock_asyncio_run):
mock_asyncio_run.side_effect = lambda coro: coro.close()
self.command.config.parser_config(self.parser)
with patch("os.getcwd", return_value="/fake/path"):
self.command.watch(self.parser)
mock_asyncio_run.assert_called_once()

#####
# sass
#####
Expand Down
9 changes: 5 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[tox]
envlist = py37, py38, py39, py310
envlist = py310, py311, py312, py313, py314
skip_missing_interpreters = true

[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
3.14: py314

[testenv]
allowlist_externals = /usr/bin/test
Expand Down