Adds a Note to a Notefile, creating the Notefile if it doesn't yet exist.
+
If a Notefile name is specified, the file must either be a DB Notefile or
+outbound queue file (see file argument below).
+Arguments
file
string (Default: data.qo)
The name of the Notefile.
On Notecard LoRa this argument is required. On all other Notecards this field is
+optional and defaults to data.qo if not provided.
When using this request on the Notecard the Notefile name must end in one of:
.qo for a queue outgoing (Notecard to Notehub) with plaintext transport
.qos for a queue outgoing with encrypted transport
.db for a bidirectionally synchronized database with plaintext transport
.dbs for a bidirectionally synchronized database with encrypted transport
.dbx for a local-only database
note
string (optional)
If the Notefile has a .db/.dbs/.dbx extension, specifies a unique Note ID.
If note string is "?", then a random unique Note ID is generated and returned
+as {"note":"xxx"}.
If this argument is provided for a .qo Notefile, an error is returned.
body
JSON object (optional)
A JSON object to be enqueued. A note must have either a body or a payload,
+and can have both.
payload
base64 string (optional)
A base64-encoded binary payload. A note must have either a body or a
+payload, and can have both. If a
+Note template is not
+in use, payloads are limited to 250 bytes.
sync
boolean (optional)
Set to true to sync immediately. Only applies to outgoing Notecard
+requests, and only guarantees syncing the specified Notefile. Auto-syncing
+incoming Notes from Notehub is set on the Notecard with
+{"req": "hub.set", "mode":"continuous", "sync": true}.
key
string (optional)
The name of an environment variable in your Notehub.io project that contains
+the contents of a public key. Used when
+encrypting the Note body for transport.
verify
boolean (optional)
If set to true and using a templated Notefile, the Notefile will be written to
+flash immediately, rather than being cached in RAM and written to flash later.
binary
boolean (optional)
If true, the Notecard will send all the data in the binary buffer to Notehub.
If true, bypasses saving the Note to flash on the Notecard. Required to be set
+to true if also using "binary":true.
full
boolean (optional)
If set to true, and the Note is using a
+Notefile Template,
+the Note will bypass usage of
+omitempty
+and retain null, 0, false, and empty string "" values.
limit
boolean (optional)
If set to true, the Note will not be created if Notecard is in a
+penalty box.
max
integer (optional)
Defines the maximum number of queued Notes permitted in the specified Notefile
+("file"). Any Notes added after this value will be rejected. When used with
+"sync":true, a sync will be triggered when the number of pending Notes matches
+the max value.
Used to incrementally retrieve changes within a specific Notefile.
+Arguments
file
string
The Notefile ID.
tracker
string (optional)
The change tracker ID. This value is developer-defined and can be used
+across both the note.changes and file.changes requests.
max
integer (optional)
The maximum number of Notes to return in the request.
start
boolean (optional)
true to reset the tracker to the beginning.
stop
boolean (optional)
true to delete the tracker.
deleted
boolean (optional)
true to return deleted Notes with this request. Deleted notes are only
+persisted in a database notefile (.db/.dbs) between the time of note deletion
+on the Notecard and the time that a sync with notehub takes place. As such, this
+boolean will have no effect after a sync or on queue notefiles (.q*).
An object with a key for each Note (the Note ID in a DB Notefile or an
+internally-generated ID for .qo and .qi Notes) and value object with the
+body of each Note and the time the Note was added.
Deletes a Note from a DB Notefile by its Note ID. To delete
+Notes from a .qi Notefile, use note.get or note.changes with
+delete:true.
+Arguments
file
string
The Notefile from which to delete a Note. Must be a Notefile with
+a .db or .dbx extension.
note
string
The Note ID of the Note to delete.
verify
boolean (optional)
If set to true and using a templated Notefile, the Notefile will be written to
+flash immediately, rather than being cached in RAM and written to flash later.
Updates a Note in a DB Notefile by its ID, replacing the existing body
+and/or payload.
+Arguments
file
string
The name of the DB Notefile that contains the Note to update.
note
string
The unique Note ID.
body
JSON object
A JSON object to add to the Note. A Note must have either a body or payload,
+and can have both.
payload
base64 string
A base64-encoded binary payload. A Note must have either a body or payload,
+and can have both.
verify
boolean (optional)
If set to true and using a templated Notefile, the Notefile will be written to
+flash immediately, rather than being cached in RAM and written to flash later.
By using the note.template request with any .qo/.qos Notefile,
+developers can provide the Notecard with a schema of sorts to apply to
+future Notes added to the Notefile. This template acts as a hint to
+the Notecard that allows it to internally store data as fixed-length binary
+records rather than as flexible JSON objects which require much more
+memory. Using templated Notes in place of regular Notes increases the storage
+and sync capability of the Notecard by an order of magnitude.
The name of the Notefile to which the template will be applied.
body
JSON object (optional)
A sample JSON body that specifies field names and values as
+"hints" for the data type. Possible data types are: boolean, integer, float,
+and string.
+See Understanding Template Data Types
+for an explanation of type hints and explanations.
length
integer (optional)
The maximum length of a payload (in bytes) that can be sent in Notes for the
+template Notefile. As of v3.2.1 length is not required, and
+payloads can be added to any template-based Note without specifying the payload
+length.
verify
boolean (optional)
If true, returns the current template set on a given Notefile.
format
string (optional)
By default all Note templates automatically include metadata, including a
+timestamp for when the Note was created, various fields about a device's
+location, as well as a timestamp for when the device's location was determined.
By providing a format of "compact" you tell the Notecard to omit this
+additional metadata to save on storage and bandwidth. The use of
+format: "compact" is required for Notecard LoRa and a Notecard paired with
+Starnote.
When using "compact" templates, you may include the following keywords in
+your template to add in fields that would otherwise be omitted: _lat, _lon,
+_ltime, _time. See
+Creating Compact Templates
+to learn more.
port
integer
This argument is required on Notecard LoRa and a Notecard paired with Starnote,
+but ignored on all other Notecards.
A port is a unique integer in the range 1–100, where each unique number represents
+one Notefile. This argument allows the Notecard to send a numerical reference to
+the Notefile over the air, rather than the full Notefile name.
The port you provide is also used in the “frame port” field on LoRaWAN gateways.
delete
boolean
Set to true to delete all pending Notes using the template if one of the
+following scenarios is also true:
Connecting via non-NTN (e.g. cellular or Wi-Fi) communications, but attempting
+to sync NTN-compatible Notefiles.
or
Connecting via NTN (e.g. satellite) communications, but attempting to sync
+non-NTN-compatible Notefiles.
\ No newline at end of file
From 94f49ee24309bb0674ffcd83b5397dde7d6ae0ca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 29 Jul 2025 19:13:55 +0000
Subject: [PATCH 3/5] Comprehensive improvement of note.template schemas with
API documentation content
Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com>
---
note.template.req.notecard.api.json | 92 ++++++++++++-
note.template.rsp.notecard.api.json | 41 +++++-
tests/test_note_template_req.py | 202 ++++++++++++++++++++++++++++
tests/test_note_template_rsp.py | 149 ++++++++++++++++++++
4 files changed, 476 insertions(+), 8 deletions(-)
create mode 100644 tests/test_note_template_req.py
create mode 100644 tests/test_note_template_rsp.py
diff --git a/note.template.req.notecard.api.json b/note.template.req.notecard.api.json
index 943e554..900be6b 100644
--- a/note.template.req.notecard.api.json
+++ b/note.template.req.notecard.api.json
@@ -4,19 +4,48 @@
"title": "note.template Request Application Programming Interface (API) Schema",
"description": "By using the note.template request with any .qo/.qos Notefile, developers can provide the Notecard with a schema of sorts to apply to future Notes added to the Notefile. This template acts as a hint to the Notecard that allows it to internally store data as fixed-length binary records rather than as flexible JSON objects which require much more memory. Using templated Notes in place of regular Notes increases the storage and sync capability of the Notecard by an order of magnitude.",
"type": "object",
+ "skus": ["CELL", "CELL+WIFI", "LORA", "WIFI"],
"properties": {
"file": {
- "description": "Name of the notefile to add the note to",
+ "description": "The name of the Notefile to which the template will be applied.",
"type": "string"
},
- "template": {
- "description": "Template for the note",
- "type": "object"
- },
"body": {
- "description": "Template for the note body",
+ "description": "A sample JSON body that specifies field names and values as \"hints\" for the data type. Possible data types are: boolean, integer, float, and string.",
"type": "object"
},
+ "length": {
+ "description": "The maximum length of a payload (in bytes) that can be sent in Notes for the template Notefile. As of v3.2.1 length is not required, and payloads can be added to any template-based Note without specifying the payload length.",
+ "type": "integer",
+ "minimum": 0
+ },
+ "verify": {
+ "description": "If `true`, returns the current template set on a given Notefile.",
+ "type": "boolean"
+ },
+ "format": {
+ "description": "By default all Note templates automatically include metadata, including a timestamp for when the Note was created, various fields about a device's location, as well as a timestamp for when the device's location was determined. By providing a format of \"compact\" you tell the Notecard to omit this additional metadata to save on storage and bandwidth. The use of format: \"compact\" is required for Notecard LoRa and a Notecard paired with Starnote.",
+ "type": "string",
+ "enum": ["compact"],
+ "sub-descriptions": [
+ {
+ "const": "compact",
+ "description": "Omit additional metadata to save on storage and bandwidth. Required for Notecard LoRa and a Notecard paired with Starnote. When using \"compact\" templates, you may include the following keywords in your template to add in fields that would otherwise be omitted: `_lat`, `_lon`, `_ltime`, `_time`.",
+ "skus": ["CELL", "CELL+WIFI", "LORA", "WIFI"]
+ }
+ ]
+ },
+ "port": {
+ "description": "A port is a unique integer in the range 1–100, where each unique number represents one Notefile. This argument allows the Notecard to send a numerical reference to the Notefile over the air, rather than the full Notefile name. The port you provide is also used in the \"frame port\" field on LoRaWAN gateways.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "skus": ["LORA"]
+ },
+ "delete": {
+ "description": "Set to `true` to delete all pending Notes using the template if one of the following scenarios is also true: Connecting via non-NTN (e.g. cellular or Wi-Fi) communications, but attempting to sync NTN-compatible Notefiles. or Connecting via NTN (e.g. satellite) communications, but attempting to sync non-NTN-compatible Notefiles.",
+ "type": "boolean"
+ },
"cmd": {
"description": "Command for the Notecard (no response)",
"const": "note.template"
@@ -48,7 +77,56 @@
}
}
],
+ "if": {
+ "properties": {
+ "port": {
+ "type": "integer"
+ }
+ },
+ "required": ["port"]
+ },
+ "then": {
+ "anyOf": [
+ {
+ "properties": {
+ "req": {
+ "const": "note.template"
+ }
+ },
+ "required": ["req"]
+ },
+ {
+ "properties": {
+ "cmd": {
+ "const": "note.template"
+ }
+ },
+ "required": ["cmd"]
+ }
+ ]
+ },
"additionalProperties": false,
- "version": "0.1.1",
+ "samples": [
+ {
+ "req": "note.template",
+ "file": "readings.qo",
+ "body": {
+ "new_vals": true,
+ "temperature": 14.1,
+ "humidity": 11,
+ "pump_state": "4"
+ }
+ },
+ {
+ "req": "note.template",
+ "file": "sensor.qo",
+ "format": "compact"
+ },
+ {
+ "cmd": "note.template",
+ "verify": true
+ }
+ ],
+ "version": "0.2.0",
"apiVersion": "9.1.1"
}
diff --git a/note.template.rsp.notecard.api.json b/note.template.rsp.notecard.api.json
index 65e6d23..fa8fbd2 100644
--- a/note.template.rsp.notecard.api.json
+++ b/note.template.rsp.notecard.api.json
@@ -2,13 +2,52 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/note.template.rsp.notecard.api.json",
"title": "note.template Response Application Programming Interface (API) Schema",
+ "description": "Response from the `note.template` request. When `verify` is set to `true`, returns the current template information. Otherwise, returns success status of the template operation.",
"type": "object",
"properties": {
"success": {
"description": "Whether the template was set successfully",
"type": "boolean"
+ },
+ "template": {
+ "description": "When `verify` is `true`, returns the current template body for the specified Notefile",
+ "type": "object"
+ },
+ "file": {
+ "description": "When `verify` is `true`, returns the Notefile name for which the template is set",
+ "type": "string"
+ },
+ "length": {
+ "description": "When `verify` is `true`, returns the maximum payload length configured for the template",
+ "type": "integer",
+ "minimum": 0
+ },
+ "format": {
+ "description": "When `verify` is `true`, returns the format setting for the template",
+ "type": "string"
+ },
+ "port": {
+ "description": "When `verify` is `true` and on LoRa/Starnote, returns the port number assigned to the Notefile",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
}
},
- "version": "0.1.1",
+ "samples": [
+ {
+ "success": true
+ },
+ {},
+ {
+ "file": "readings.qo",
+ "template": {
+ "temperature": 14.1,
+ "humidity": 11,
+ "pump_state": "4"
+ },
+ "format": "compact"
+ }
+ ],
+ "version": "0.2.0",
"apiVersion": "9.1.1"
}
diff --git a/tests/test_note_template_req.py b/tests/test_note_template_req.py
new file mode 100644
index 0000000..7de0c98
--- /dev/null
+++ b/tests/test_note_template_req.py
@@ -0,0 +1,202 @@
+import pytest
+import jsonschema
+
+SCHEMA_FILE = "note.template.req.notecard.api.json"
+
+def test_valid_req(schema):
+ """Tests a minimal valid request."""
+ instance = {
+ "req": "note.template"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_cmd(schema):
+ """Tests a minimal valid command."""
+ instance = {
+ "cmd": "note.template"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_missing_req_cmd(schema):
+ """Tests invalid request missing both req and cmd."""
+ instance = {
+ "file": "test.qo"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ # Should fail because neither 'req' nor 'cmd' is present
+
+def test_valid_with_file(schema):
+ """Tests valid request with file parameter."""
+ instance = {
+ "req": "note.template",
+ "file": "data.qo"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_with_format(schema):
+ """Tests valid request with format parameter."""
+ instance = {
+ "req": "note.template",
+ "format": "compact"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_with_body(schema):
+ """Tests valid request with body parameter."""
+ instance = {
+ "req": "note.template",
+ "body": {
+ "readings": {
+ "temp": 0,
+ "humid": 0
+ }
+ }
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_complete_request(schema):
+ """Tests valid request with all parameters."""
+ instance = {
+ "req": "note.template",
+ "file": "sensors.qo",
+ "format": "compact",
+ "body": {
+ "temperature": 0.0,
+ "humidity": 0.0,
+ "pressure": 0.0
+ },
+ "length": 100
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_file_type(schema):
+ """Tests invalid request with non-string file parameter."""
+ instance = {
+ "req": "note.template",
+ "file": 123
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "123 is not of type 'string'" in str(excinfo.value)
+
+def test_invalid_format_type(schema):
+ """Tests invalid request with non-string format parameter."""
+ instance = {
+ "req": "note.template",
+ "format": True
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "True is not of type 'string'" in str(excinfo.value)
+
+def test_invalid_body_type(schema):
+ """Tests invalid request with non-object body parameter."""
+ instance = {
+ "req": "note.template",
+ "body": []
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "[] is not of type 'object'" in str(excinfo.value)
+
+def test_cmd_instead_of_req(schema):
+ """Tests valid command instead of request."""
+ instance = {
+ "cmd": "note.template",
+ "file": "data.qo",
+ "body": {"type": "sensor"}
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_additional_properties(schema):
+ """Tests that additional properties are not allowed."""
+ instance = {
+ "req": "note.template",
+ "invalid_property": "should_fail"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "Additional properties are not allowed" in str(excinfo.value)
+
+def test_valid_verify_parameter(schema):
+ """Tests valid request with verify parameter."""
+ instance = {
+ "req": "note.template",
+ "verify": True
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_verify_type(schema):
+ """Tests invalid request with non-boolean verify parameter."""
+ instance = {
+ "req": "note.template",
+ "verify": "true"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'true' is not of type 'boolean'" in str(excinfo.value)
+
+def test_valid_length_parameter(schema):
+ """Tests valid request with length parameter."""
+ instance = {
+ "req": "note.template",
+ "length": 250
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_length_type(schema):
+ """Tests invalid request with non-integer length parameter."""
+ instance = {
+ "req": "note.template",
+ "length": "250"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'250' is not of type 'integer'" in str(excinfo.value)
+
+def test_invalid_length_negative(schema):
+ """Tests invalid request with negative length parameter."""
+ instance = {
+ "req": "note.template",
+ "length": -1
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "-1 is less than the minimum of 0" in str(excinfo.value)
+
+def test_valid_delete_parameter(schema):
+ """Tests valid request with delete parameter."""
+ instance = {
+ "req": "note.template",
+ "delete": True
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_delete_type(schema):
+ """Tests invalid request with non-boolean delete parameter."""
+ instance = {
+ "req": "note.template",
+ "delete": 1
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "1 is not of type 'boolean'" in str(excinfo.value)
+
+def test_valid_format_compact(schema):
+ """Tests valid request with format=compact parameter."""
+ instance = {
+ "req": "note.template",
+ "format": "compact"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_format_value(schema):
+ """Tests invalid request with invalid format value."""
+ instance = {
+ "req": "note.template",
+ "format": "invalid"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'invalid' is not one of ['compact']" in str(excinfo.value)
\ No newline at end of file
diff --git a/tests/test_note_template_rsp.py b/tests/test_note_template_rsp.py
new file mode 100644
index 0000000..329e328
--- /dev/null
+++ b/tests/test_note_template_rsp.py
@@ -0,0 +1,149 @@
+import pytest
+import jsonschema
+
+SCHEMA_FILE = "note.template.rsp.notecard.api.json"
+
+def test_minimal_valid_response(schema):
+ """Tests a minimal valid response (empty object)."""
+ instance = {}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_success_true(schema):
+ """Tests a valid response with success=true."""
+ instance = {"success": True}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_valid_success_false(schema):
+ """Tests a valid response with success=false."""
+ instance = {"success": False}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_success_type(schema):
+ """Tests an invalid response with a non-boolean type for success."""
+ instance = {"success": "true"}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'true' is not of type 'boolean'" in str(excinfo.value)
+
+def test_valid_with_error(schema):
+ """Tests a valid response with an error message."""
+ instance = {"err": "template validation failed"}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_error_type(schema):
+ """Tests response with potential additional fields like error."""
+ # Since additionalProperties is not false, this should be valid
+ instance = {"err": "template validation failed"}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_response_with_both_success_and_error(schema):
+ """Tests response that might contain both success and error fields."""
+ # This could be valid depending on the actual API behavior
+ instance = {
+ "success": False,
+ "err": "invalid template format"
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_response_with_additional_fields(schema):
+ """Tests response with potential additional fields."""
+ # Based on pattern from other schemas, there might be additional response fields
+ instance = {
+ "success": True
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_empty_response_valid(schema):
+ """Tests that an empty response object is valid."""
+ instance = {}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_unknown_property_type(schema):
+ """Tests response with an invalid property type if schema doesn't allow additional properties."""
+ # This test will depend on whether additionalProperties is false in the schema
+ instance = {"unknown_field": 123}
+ try:
+ jsonschema.validate(instance=instance, schema=schema)
+ # If validation passes, the schema allows additional properties
+ except jsonschema.ValidationError:
+ # If validation fails, the schema doesn't allow additional properties
+ pass
+
+def test_valid_template_response(schema):
+ """Tests valid response with template field."""
+ instance = {
+ "template": {
+ "temperature": 14.1,
+ "humidity": 11
+ }
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_template_type(schema):
+ """Tests invalid response with non-object template."""
+ instance = {"template": "invalid"}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'invalid' is not of type 'object'" in str(excinfo.value)
+
+def test_valid_file_response(schema):
+ """Tests valid response with file field."""
+ instance = {"file": "readings.qo"}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_file_type(schema):
+ """Tests invalid response with non-string file."""
+ instance = {"file": 123}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "123 is not of type 'string'" in str(excinfo.value)
+
+def test_valid_length_response(schema):
+ """Tests valid response with length field."""
+ instance = {"length": 250}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_length_type(schema):
+ """Tests invalid response with non-integer length."""
+ instance = {"length": "250"}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'250' is not of type 'integer'" in str(excinfo.value)
+
+def test_valid_format_response(schema):
+ """Tests valid response with format field."""
+ instance = {"format": "compact"}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_format_type(schema):
+ """Tests invalid response with non-string format."""
+ instance = {"format": 123}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "123 is not of type 'string'" in str(excinfo.value)
+
+def test_valid_port_response(schema):
+ """Tests valid response with port field."""
+ instance = {"port": 42}
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_port_type(schema):
+ """Tests invalid response with non-integer port."""
+ instance = {"port": "42"}
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'42' is not of type 'integer'" in str(excinfo.value)
+
+def test_valid_complete_verify_response(schema):
+ """Tests complete response from verify request."""
+ instance = {
+ "file": "sensors.qo",
+ "template": {
+ "temperature": 0.0,
+ "humidity": 0.0
+ },
+ "format": "compact",
+ "length": 100,
+ "port": 5
+ }
+ jsonschema.validate(instance=instance, schema=schema)
\ No newline at end of file
From 53d167e456f302bfd25c46634987e573b2b41e11 Mon Sep 17 00:00:00 2001
From: "Zachary J. Fields"
Date: Tue, 29 Jul 2025 16:07:48 -0500
Subject: [PATCH 4/5] fix: manually fix template
---
note.template.req.notecard.api.json | 108 ++++++++++------------------
note.template.rsp.notecard.api.json | 49 +++++--------
2 files changed, 55 insertions(+), 102 deletions(-)
diff --git a/note.template.req.notecard.api.json b/note.template.req.notecard.api.json
index 900be6b..3bb278c 100644
--- a/note.template.req.notecard.api.json
+++ b/note.template.req.notecard.api.json
@@ -2,57 +2,50 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/note.template.req.notecard.api.json",
"title": "note.template Request Application Programming Interface (API) Schema",
- "description": "By using the note.template request with any .qo/.qos Notefile, developers can provide the Notecard with a schema of sorts to apply to future Notes added to the Notefile. This template acts as a hint to the Notecard that allows it to internally store data as fixed-length binary records rather than as flexible JSON objects which require much more memory. Using templated Notes in place of regular Notes increases the storage and sync capability of the Notecard by an order of magnitude.",
+ "description": "By using the `note.template` request with any `.qo`/`.qos` Notefile, developers can provide the Notecard with a schema of sorts to apply to future Notes added to the Notefile. This template acts as a hint to the Notecard that allows it to internally store data as fixed-length binary records rather than as flexible JSON objects which require much more memory. Using templated Notes in place of regular Notes increases the storage and sync capability of the Notecard by an order of magnitude.",
"type": "object",
"skus": ["CELL", "CELL+WIFI", "LORA", "WIFI"],
+ "version": "0.2.1",
+ "apiVersion": "9.1.1",
"properties": {
+ "cmd": {
+ "description": "Command for the Notecard (no response)",
+ "const": "note.template"
+ },
+ "req": {
+ "description": "Request for the Notecard (expects response)",
+ "const": "note.template"
+ },
"file": {
"description": "The name of the Notefile to which the template will be applied.",
"type": "string"
},
"body": {
- "description": "A sample JSON body that specifies field names and values as \"hints\" for the data type. Possible data types are: boolean, integer, float, and string.",
+ "description": "A sample JSON body that specifies field names and values as \"hints\" for the data type. Possible data types are: boolean, integer, float, and string. See [Understanding Template Data Types](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#understanding-template-data-types) for an explanation of type hints and explanations.",
"type": "object"
},
"length": {
- "description": "The maximum length of a payload (in bytes) that can be sent in Notes for the template Notefile. As of v3.2.1 length is not required, and payloads can be added to any template-based Note without specifying the payload length.",
+ "description": "The maximum length of a `payload` (in bytes) that can be sent in Notes for the template Notefile. As of v3.2.1 `length` is not required, and payloads can be added to any template-based Note without specifying the payload length.",
"type": "integer",
- "minimum": 0
+ "minimum": -1
},
"verify": {
"description": "If `true`, returns the current template set on a given Notefile.",
"type": "boolean"
},
"format": {
- "description": "By default all Note templates automatically include metadata, including a timestamp for when the Note was created, various fields about a device's location, as well as a timestamp for when the device's location was determined. By providing a format of \"compact\" you tell the Notecard to omit this additional metadata to save on storage and bandwidth. The use of format: \"compact\" is required for Notecard LoRa and a Notecard paired with Starnote.",
- "type": "string",
- "enum": ["compact"],
- "sub-descriptions": [
- {
- "const": "compact",
- "description": "Omit additional metadata to save on storage and bandwidth. Required for Notecard LoRa and a Notecard paired with Starnote. When using \"compact\" templates, you may include the following keywords in your template to add in fields that would otherwise be omitted: `_lat`, `_lon`, `_ltime`, `_time`.",
- "skus": ["CELL", "CELL+WIFI", "LORA", "WIFI"]
- }
- ]
+ "description": "By default all Note templates automatically include metadata, including a timestamp for when the Note was created, various fields about a device's location, as well as a timestamp for when the device's location was determined.\n\nBy providing a `format` of `\"compact\"` you tell the Notecard to omit this additional metadata to save on storage and bandwidth. The use of `format: \"compact\"` is required for Notecard LoRa and a Notecard paired with Starnote.\n\nWhen using `\"compact\"` templates, you may include the following keywords in your template to add in fields that would otherwise be omitted: `_lat`, `_lon`, `_ltime`, `_time`. See [Creating Compact Templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#creating-compact-templates) to learn more.",
+ "type": "string"
},
"port": {
- "description": "A port is a unique integer in the range 1–100, where each unique number represents one Notefile. This argument allows the Notecard to send a numerical reference to the Notefile over the air, rather than the full Notefile name. The port you provide is also used in the \"frame port\" field on LoRaWAN gateways.",
+ "description": "This argument is required on Notecard LoRa and a Notecard paired with Starnote, but ignored on all other Notecards.\n\nA port is a unique integer in the range 1–100, where each unique number represents one Notefile. This argument allows the Notecard to send a numerical reference to the Notefile over the air, rather than the full Notefile name.\n\nThe port you provide is also used in the \"frame port\" field on LoRaWAN gateways.",
"type": "integer",
"minimum": 1,
- "maximum": 100,
- "skus": ["LORA"]
+ "maximum": 100
},
"delete": {
- "description": "Set to `true` to delete all pending Notes using the template if one of the following scenarios is also true: Connecting via non-NTN (e.g. cellular or Wi-Fi) communications, but attempting to sync NTN-compatible Notefiles. or Connecting via NTN (e.g. satellite) communications, but attempting to sync non-NTN-compatible Notefiles.",
+ "description": "Set to `true` to delete all pending Notes using the template if one of the following scenarios is also true:\n\nConnecting via non-NTN (e.g. cellular or Wi-Fi) communications, but attempting to sync NTN-compatible Notefiles.\n\nor\n\nConnecting via NTN (e.g. satellite) communications, but attempting to sync non-NTN-compatible Notefiles.\n\nRead more about this feature in [Starnote Best Practices](https://dev.blues.io/starnote/starnote-best-practices/#define-ntn-vs-non-ntn-templates).",
"type": "boolean"
- },
- "cmd": {
- "description": "Command for the Notecard (no response)",
- "const": "note.template"
- },
- "req": {
- "description": "Request for the Notecard (expects response)",
- "const": "note.template"
}
},
"oneOf": [
@@ -77,56 +70,33 @@
}
}
],
- "if": {
- "properties": {
- "port": {
- "type": "integer"
- }
- },
- "required": ["port"]
- },
- "then": {
- "anyOf": [
- {
- "properties": {
- "req": {
- "const": "note.template"
- }
- },
- "required": ["req"]
- },
- {
- "properties": {
- "cmd": {
- "const": "note.template"
- }
- },
- "required": ["cmd"]
- }
- ]
- },
"additionalProperties": false,
"samples": [
{
- "req": "note.template",
- "file": "readings.qo",
- "body": {
- "new_vals": true,
- "temperature": 14.1,
- "humidity": 11,
- "pump_state": "4"
- }
+ "title": "Provide Schema",
+ "json": "{\"req\":\"note.template\",\"file\":\"readings.qo\",\"body\":{\"new_vals\":true,\"temperature\":14.1,\"humidity\":11,\"pump_state\":\"4\"}}"
+ },
+ {
+ "title": "Compact Schema for LoRa",
+ "json": "{\"req\":\"note.template\",\"file\":\"readings.qo\",\"body\":{\"new_vals\":true,\"temperature\":14.1,\"humidity\":11,\"pump_state\":\"4\"},\"format\":\"compact\",\"port\":50}"
},
{
- "req": "note.template",
- "file": "sensor.qo",
- "format": "compact"
+ "title": "Compact Schema with Additional Fields",
+ "json": "{\"req\":\"note.template\",\"file\":\"readings.qo\",\"body\":{\"temperature\":14.1,\"_lat\":14.1,\"_lon\":14.1,\"_ltime\":14,\"_time\":14},\"format\":\"compact\",\"port\":50}"
},
{
- "cmd": "note.template",
- "verify": true
+ "title": "Request Current Template",
+ "json": "{\"req\":\"note.template\",\"file\":\"readings.qo\",\"verify\":true}"
}
],
- "version": "0.2.0",
- "apiVersion": "9.1.1"
+ "annotations": [
+ {
+ "title": "note",
+ "description": "Read about [Working with Note Templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#working-with-note-templates) for additional information."
+ },
+ {
+ "title": "note",
+ "description": "See examples of `note.template` usage in [these accelerator projects](https://dev.blues.io/accelerators/?category=&feature=templated-notefiles)."
+ }
+ ]
}
diff --git a/note.template.rsp.notecard.api.json b/note.template.rsp.notecard.api.json
index fa8fbd2..af6e258 100644
--- a/note.template.rsp.notecard.api.json
+++ b/note.template.rsp.notecard.api.json
@@ -2,52 +2,35 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/note.template.rsp.notecard.api.json",
"title": "note.template Response Application Programming Interface (API) Schema",
- "description": "Response from the `note.template` request. When `verify` is set to `true`, returns the current template information. Otherwise, returns success status of the template operation.",
"type": "object",
+ "version": "0.2.0",
+ "apiVersion": "9.1.1",
"properties": {
- "success": {
- "description": "Whether the template was set successfully",
- "type": "boolean"
+ "bytes": {
+ "description": "The number of bytes that will be transmitted to Notehub, per Note, before compression.",
+ "type": "integer"
},
"template": {
- "description": "When `verify` is `true`, returns the current template body for the specified Notefile",
- "type": "object"
+ "description": "`true` if an active template exists on the Notefile.",
+ "type": "boolean"
},
- "file": {
- "description": "When `verify` is `true`, returns the Notefile name for which the template is set",
- "type": "string"
+ "body": {
+ "description": "If the `verify` argument is provided and the Notefile has an active template, the template `body`.",
+ "type": "object"
},
"length": {
- "description": "When `verify` is `true`, returns the maximum payload length configured for the template",
- "type": "integer",
- "minimum": 0
+ "description": "If the `verify` argument is provided and the Notefile has an active template with a payload, the payload length.",
+ "type": "integer"
},
"format": {
- "description": "When `verify` is `true`, returns the format setting for the template",
+ "description": "If the `format` argument is provided, this represents the format applied to the template.",
"type": "string"
- },
- "port": {
- "description": "When `verify` is `true` and on LoRa/Starnote, returns the port number assigned to the Notefile",
- "type": "integer",
- "minimum": 1,
- "maximum": 100
}
},
"samples": [
{
- "success": true
- },
- {},
- {
- "file": "readings.qo",
- "template": {
- "temperature": 14.1,
- "humidity": 11,
- "pump_state": "4"
- },
- "format": "compact"
+ "title": "Example Response",
+ "json": "{\"bytes\":40}"
}
- ],
- "version": "0.2.0",
- "apiVersion": "9.1.1"
+ ]
}
From c85fb4770f5d6438c793b2f5f1e1e37da2965f4b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 30 Jul 2025 12:46:24 +0000
Subject: [PATCH 5/5] fix: update note.template tests to match manually updated
schema
Co-authored-by: zakoverflow <215570245+zakoverflow@users.noreply.github.com>
---
tests/test_note_template_req.py | 54 +++++++++++++++++++++++++++------
tests/test_note_template_rsp.py | 38 +++++++++++------------
2 files changed, 63 insertions(+), 29 deletions(-)
diff --git a/tests/test_note_template_req.py b/tests/test_note_template_req.py
index 7de0c98..fc130b4 100644
--- a/tests/test_note_template_req.py
+++ b/tests/test_note_template_req.py
@@ -155,15 +155,13 @@ def test_invalid_length_type(schema):
jsonschema.validate(instance=instance, schema=schema)
assert "'250' is not of type 'integer'" in str(excinfo.value)
-def test_invalid_length_negative(schema):
- """Tests invalid request with negative length parameter."""
+def test_valid_length_negative_one(schema):
+ """Tests valid request with length=-1 (now allowed in schema)."""
instance = {
"req": "note.template",
"length": -1
}
- with pytest.raises(jsonschema.ValidationError) as excinfo:
- jsonschema.validate(instance=instance, schema=schema)
- assert "-1 is less than the minimum of 0" in str(excinfo.value)
+ jsonschema.validate(instance=instance, schema=schema)
def test_valid_delete_parameter(schema):
"""Tests valid request with delete parameter."""
@@ -191,12 +189,50 @@ def test_valid_format_compact(schema):
}
jsonschema.validate(instance=instance, schema=schema)
-def test_invalid_format_value(schema):
- """Tests invalid request with invalid format value."""
+def test_invalid_length_too_negative(schema):
+ """Tests invalid request with length less than -1."""
+ instance = {
+ "req": "note.template",
+ "length": -2
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "-2 is less than the minimum of -1" in str(excinfo.value)
+
+def test_valid_port_parameter(schema):
+ """Tests valid request with port parameter."""
+ instance = {
+ "req": "note.template",
+ "port": 50
+ }
+ jsonschema.validate(instance=instance, schema=schema)
+
+def test_invalid_port_type(schema):
+ """Tests invalid request with non-integer port parameter."""
+ instance = {
+ "req": "note.template",
+ "port": "50"
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "'50' is not of type 'integer'" in str(excinfo.value)
+
+def test_invalid_port_too_low(schema):
+ """Tests invalid request with port below minimum."""
+ instance = {
+ "req": "note.template",
+ "port": 0
+ }
+ with pytest.raises(jsonschema.ValidationError) as excinfo:
+ jsonschema.validate(instance=instance, schema=schema)
+ assert "0 is less than the minimum of 1" in str(excinfo.value)
+
+def test_invalid_port_too_high(schema):
+ """Tests invalid request with port above maximum."""
instance = {
"req": "note.template",
- "format": "invalid"
+ "port": 101
}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
- assert "'invalid' is not one of ['compact']" in str(excinfo.value)
\ No newline at end of file
+ assert "101 is greater than the maximum of 100" in str(excinfo.value)
\ No newline at end of file
diff --git a/tests/test_note_template_rsp.py b/tests/test_note_template_rsp.py
index 329e328..6ad17c9 100644
--- a/tests/test_note_template_rsp.py
+++ b/tests/test_note_template_rsp.py
@@ -20,10 +20,11 @@ def test_valid_success_false(schema):
def test_invalid_success_type(schema):
"""Tests an invalid response with a non-boolean type for success."""
+ # Note: success is not defined in the current schema, so this test should validate
+ # as the schema allows additional properties
instance = {"success": "true"}
- with pytest.raises(jsonschema.ValidationError) as excinfo:
- jsonschema.validate(instance=instance, schema=schema)
- assert "'true' is not of type 'boolean'" in str(excinfo.value)
+ # This should actually pass since success is not defined in the schema
+ jsonschema.validate(instance=instance, schema=schema)
def test_valid_with_error(schema):
"""Tests a valid response with an error message."""
@@ -70,21 +71,18 @@ def test_invalid_unknown_property_type(schema):
pass
def test_valid_template_response(schema):
- """Tests valid response with template field."""
+ """Tests valid response with template field (boolean)."""
instance = {
- "template": {
- "temperature": 14.1,
- "humidity": 11
- }
+ "template": True
}
jsonschema.validate(instance=instance, schema=schema)
def test_invalid_template_type(schema):
- """Tests invalid response with non-object template."""
+ """Tests invalid response with non-boolean template."""
instance = {"template": "invalid"}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
- assert "'invalid' is not of type 'object'" in str(excinfo.value)
+ assert "'invalid' is not of type 'boolean'" in str(excinfo.value)
def test_valid_file_response(schema):
"""Tests valid response with file field."""
@@ -93,10 +91,10 @@ def test_valid_file_response(schema):
def test_invalid_file_type(schema):
"""Tests invalid response with non-string file."""
+ # Note: file is not defined in the current schema, so this should pass
+ # as the schema allows additional properties
instance = {"file": 123}
- with pytest.raises(jsonschema.ValidationError) as excinfo:
- jsonschema.validate(instance=instance, schema=schema)
- assert "123 is not of type 'string'" in str(excinfo.value)
+ jsonschema.validate(instance=instance, schema=schema)
def test_valid_length_response(schema):
"""Tests valid response with length field."""
@@ -129,21 +127,21 @@ def test_valid_port_response(schema):
def test_invalid_port_type(schema):
"""Tests invalid response with non-integer port."""
+ # Note: port is not defined in the current schema, so this should pass
+ # as the schema allows additional properties
instance = {"port": "42"}
- with pytest.raises(jsonschema.ValidationError) as excinfo:
- jsonschema.validate(instance=instance, schema=schema)
- assert "'42' is not of type 'integer'" in str(excinfo.value)
+ jsonschema.validate(instance=instance, schema=schema)
def test_valid_complete_verify_response(schema):
"""Tests complete response from verify request."""
instance = {
- "file": "sensors.qo",
- "template": {
+ "bytes": 40,
+ "template": True,
+ "body": {
"temperature": 0.0,
"humidity": 0.0
},
"format": "compact",
- "length": 100,
- "port": 5
+ "length": 100
}
jsonschema.validate(instance=instance, schema=schema)
\ No newline at end of file