From 9ac553cfed4ecf12db0de0db85c74c9202dd0c08 Mon Sep 17 00:00:00 2001 From: icpp Date: Wed, 18 Mar 2026 11:17:09 -0400 Subject: [PATCH 1/3] Refactor to migrate from ic-py to icp-py-core, update dependencies, and enhance release documentation --- .mypy.ini | 6 ++++- .pylintrc | 2 +- README-Release-Guide.md | 49 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 +- scripts/download.py | 20 +++++++++------- scripts/ic_py_canister.py | 28 +++++++++++++++------- scripts/upload.py | 39 +++++++++++++++++-------------- 7 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 README-Release-Guide.md diff --git a/.mypy.ini b/.mypy.ini index 9f2e60d..8bbb8ba 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -10,4 +10,8 @@ check_untyped_defs = True disallow_untyped_decorators = True show_error_codes = True warn_redundant_casts = True -warn_unused_ignores = True \ No newline at end of file +warn_unused_ignores = True + +# icp_core does not ship py.typed or type stubs +[mypy-icp_core] +ignore_missing_imports = True \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 7da9488..65a96c8 100755 --- a/.pylintrc +++ b/.pylintrc @@ -399,7 +399,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local,numpy.random,matplo # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. -ignored-modules= +ignored-modules=icp_core # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/README-Release-Guide.md b/README-Release-Guide.md new file mode 100644 index 0000000..2dab576 --- /dev/null +++ b/README-Release-Guide.md @@ -0,0 +1,49 @@ +# Release Guide + +How to create a new release of `llama_cpp_canister`. + +## Prerequisites + +- The latest `cicd-mac` workflow run on the `main` branch must have succeeded. + The release workflow checks this automatically and will fail if CI/CD is red. +- All changes intended for the release are merged into `main`. + +## Steps + +### 1. Bump the version + +Update `version.txt` in the repo root to the desired release version (e.g. `0.8.0`). +Commit and push to `main`. + +### 2. Trigger the release workflow + +1. Go to **Actions** > **Release llama_cpp_canister** in the GitHub UI. +2. Click **Run workflow**. +3. Enter the tag for the release (e.g. `v0.8.0`). +4. Click **Run workflow** to start. + +### 3. What the workflow does + +| Step | Description | +| --------------------------- | ----------------------------------------------------------------------------- | +| **check-cicd-mac-status** | Verifies the latest `cicd-mac.yml` run succeeded | +| **install & build** | Sets up miniconda, installs toolchains, builds the Wasm canister | +| **zip release files** | Packages `build/`, `scripts/`, `test/`, `dfx.json`, `version.txt`, etc. | +| **create GitHub release** | Creates a GitHub release with tag and attaches `llama_cpp_canister_.zip` | + +### 4. Post-release verification + +After the workflow completes: + +1. Download the zip artifact from the GitHub release page. +2. Unzip and verify the contents include: + - `build/llama_cpp.wasm` and `build/llama_cpp.did` + - `scripts/` with upload/download tooling + - `test/` with smoke tests + - `dfx.json`, `version.txt`, `requirements.txt` +3. Optionally deploy and run smoke tests: + ```bash + dfx start --clean --background + dfx deploy --network local + pytest -vv --network local test/ + ``` diff --git a/requirements.txt b/requirements.txt index a9da99a..b9dce00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ -r scripts/requirements.txt -r src/llama_cpp_onicai_fork/requirements.txt icpp-pro>=5.3.1 -ic-py==1.0.1 +icp-py-core==2.3.0 binaryen.py \ No newline at end of file diff --git a/scripts/download.py b/scripts/download.py index 65f7da2..07671df 100644 --- a/scripts/download.py +++ b/scripts/download.py @@ -13,7 +13,7 @@ import sys from pathlib import Path from typing import List -from .ic_py_canister import get_canister, run_dfx_command +from .ic_py_canister import extract_variant, get_canister, run_dfx_command from .parse_args_download import parse_args from .calculate_sha256 import calculate_sha256 @@ -64,13 +64,14 @@ def main() -> int: ) # --------------------------------------------------------------------------- - # get ic-py based Canister instance + # get icp-py-core based Canister instance canister_instance = get_canister(canister_name, candid_path, network, canister_id) # check health (liveness) print("--\nChecking liveness of canister (did we deploy it!)") response = canister_instance.health() - if "Ok" in response[0].keys(): + result = extract_variant(response) + if "Ok" in result: print("Ok!") else: print("Not OK, response is:") @@ -104,11 +105,12 @@ def main() -> int: } ) - if "Ok" in response[0].keys(): - r_filesize = response[0]["Ok"]["filesize"] - r_chunk: List[int] = response[0]["Ok"]["chunk"] - r_chunksize = response[0]["Ok"]["chunksize"] - r_offset = response[0]["Ok"]["offset"] + result = extract_variant(response) + if "Ok" in result: + r_filesize = result["Ok"]["filesize"] + r_chunk: List[int] = result["Ok"]["chunk"] + r_chunksize = result["Ok"]["chunksize"] + r_offset = result["Ok"]["offset"] total_received = offset + len(r_chunk) print( "--" @@ -123,7 +125,7 @@ def main() -> int: f.write(bytearray(r_chunk)) - done = response[0]["Ok"]["done"] + done = result["Ok"]["done"] else: print("Something went wrong:") print(response) diff --git a/scripts/ic_py_canister.py b/scripts/ic_py_canister.py index ec12d3e..97c9ab2 100644 --- a/scripts/ic_py_canister.py +++ b/scripts/ic_py_canister.py @@ -1,13 +1,10 @@ -"""Returns the ic-py Canister instance, for calling the endpoints.""" +"""Returns the icp-py-core Canister instance, for calling the endpoints.""" import sys import subprocess from pathlib import Path -from typing import Optional -from ic.canister import Canister # type: ignore -from ic.client import Client # type: ignore -from ic.identity import Identity # type: ignore -from ic.agent import Agent # type: ignore +from typing import Any, List, Optional +from icp_core import Agent, Identity, Client, Canister from icpp.run_shell_cmd import run_shell_cmd ROOT_PATH = Path(__file__).parent.parent @@ -16,6 +13,19 @@ DFX = "dfx" +def extract_variant(response: List[Any]) -> Any: + """Extract variant result from icp-py-core response. + + icp-py-core returns: [{'type': 'variant', 'value': {'Ok': {...}}}] + old ic-py returned: [{'Ok': {...}}] + This helper normalizes both formats to {'Ok': {...}} or {'Err': ...}. + """ + item = response[0] + if "value" in item: + return item["value"] + return item + + def run_dfx_command(cmd: str, quiet: bool = False) -> Optional[str]: """Runs dfx command as a subprocess""" try: @@ -27,7 +37,7 @@ def run_dfx_command(cmd: str, quiet: bool = False) -> Optional[str]: def get_agent(network: str = "local") -> Agent: - """Returns an ic_py Agent instance""" + """Returns an icp-py-core Agent instance""" # Check if the network is up print(f"--\nChecking if the {network} network is up...") @@ -82,7 +92,7 @@ def get_canister( network: str = "local", canister_id: Optional[str] = "", ) -> Canister: - """Returns an ic_py Canister instance""" + """Returns an icp-py-core Canister instance""" agent = get_agent(network=network) @@ -104,4 +114,4 @@ def get_canister( canister_did = f.read() # Create a Canister instance - return Canister(agent=agent, canister_id=canister_id, candid=canister_did) + return Canister(agent=agent, canister_id=canister_id, candid_str=canister_did) diff --git a/scripts/upload.py b/scripts/upload.py index b844acd..1018e66 100644 --- a/scripts/upload.py +++ b/scripts/upload.py @@ -16,7 +16,7 @@ from pathlib import Path from typing import Generator from .calculate_sha256 import calculate_sha256 -from .ic_py_canister import get_canister, run_dfx_command +from .ic_py_canister import extract_variant, get_canister, run_dfx_command from .parse_args_upload import parse_args ROOT_PATH = Path(__file__).parent.parent @@ -88,13 +88,14 @@ def main() -> int: ) # --------------------------------------------------------------------------- - # get ic-py based Canister instance + # get icp-py-core based Canister instance canister_instance = get_canister(canister_name, candid_path, network, canister_id) # check health (liveness) print("--\nChecking liveness of canister (did we deploy it!)") response = canister_instance.health() - if "Ok" in response[0].keys(): + result = extract_variant(response) + if "Ok" in result: print("Ok!") else: print("Not OK, response is:") @@ -163,7 +164,8 @@ def main() -> int: "chunk": chunk, "chunksize": chunksize, "offset": offset, - } + }, + verify_certificate=False, ) # pylint: disable=no-member else: response = canister_instance.file_upload_chunk( @@ -172,7 +174,8 @@ def main() -> int: "chunk": chunk, "chunksize": chunksize, "offset": offset, - } + }, + verify_certificate=False, ) # pylint: disable=no-member break # Exit the loop if the request is successful @@ -187,24 +190,25 @@ def main() -> int: print(f"Retrying in {retry_delay} seconds...") time.sleep(retry_delay) # Wait before retrying - if "Ok" in response[0].keys(): + result = extract_variant(response) + if "Ok" in result: if DEBUG_VERBOSE == 0: pass elif DEBUG_VERBOSE == 1: # print only every 10th chunk or if it is the last chunk if i % 10 == 0 or (offset + len(chunk)) >= len(file_bytes): print( - f"OK! filesize = {response[0]['Ok']['filesize']}, " - f"filesha256 = {response[0]['Ok']['filesha256']}" + f"OK! filesize = {result['Ok']['filesize']}, " + f"filesha256 = {result['Ok']['filesha256']}" ) else: print( - f"OK! filesize = {response[0]['Ok']['filesize']}, " - f"filesha256 = {response[0]['Ok']['filesha256']}" + f"OK! filesize = {result['Ok']['filesize']}, " + f"filesha256 = {result['Ok']['filesha256']}" ) - canister_filesize = response[0]["Ok"]["filesize"] - canister_filesha256 = response[0]["Ok"]["filesha256"] + canister_filesize = result["Ok"]["filesize"] + canister_filesha256 = result["Ok"]["filesha256"] else: print("Something went wrong:") print(response) @@ -245,14 +249,15 @@ def main() -> int: {"filename": canister_filename} ) - if "Ok" in response[0].keys(): + result = extract_variant(response) + if "Ok" in result: print( - f"OK! filesize = {response[0]['Ok']['filesize']}, " - f"filesha256 = {response[0]['Ok']['filesha256']}" + f"OK! filesize = {result['Ok']['filesize']}, " + f"filesha256 = {result['Ok']['filesha256']}" ) - canister_filesize = response[0]["Ok"]["filesize"] - canister_filesha256 = response[0]["Ok"]["filesha256"] + canister_filesize = result["Ok"]["filesize"] + canister_filesha256 = result["Ok"]["filesha256"] if (canister_filesize != local_file_size) or ( canister_filesha256 != local_file_sha256 From 96b2dcb6823e513fe8bc273aa24edb06745ff245 Mon Sep 17 00:00:00 2001 From: icpp Date: Wed, 18 Mar 2026 12:41:52 -0400 Subject: [PATCH 2/3] Update dfx version to 0.31.0 in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1ff1a40..c79587b 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VERSION_DIDC := $(shell curl --silent "https://api.github.com/repos/dfinity/cand # version to install for clang VERSION_CLANG := $(shell cat version_clang.txt) # version to install for dfx -VERSION_DFX := 0.29.2 +VERSION_DFX := 0.31.0 ########################################################################### # Use some clang tools that come with wasi-sdk From c2722f7a81419b2069ce830caafea8171e8a9ec0 Mon Sep 17 00:00:00 2001 From: icpp Date: Wed, 18 Mar 2026 12:43:07 -0400 Subject: [PATCH 3/3] Update README to require dfx version 0.31.0 or later and add verification step --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 69aa736..79a5406 100644 --- a/README.md +++ b/README.md @@ -49,15 +49,21 @@ You can just grab the latest [release](https://github.com/onicai/llama_cpp_canis # Set up -- Install dfx: +- Install dfx (version 0.31.0 or later is required): ```bash sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" # Configure your shell source "$HOME/.local/share/dfx/env" + + # Verify the version (must be >= 0.31.0) + dfx --version ``` + > **Note:** dfx 0.31+ is required because `icp-py-core` uses the `/api/v3/` + > endpoint, which is not supported by older dfx versions. + - Clone the repo and it's children: _(skip when using the [release](https://github.com/onicai/llama_cpp_canister/releases))_