Skip to content

Commit 551a200

Browse files
devin-ai-integration[bot]rlauer@blues.com
andcommitted
feat: add env.template support with type hint validation
Co-Authored-By: rlauer@blues.com <rlauer@blues.com>
1 parent 1133055 commit 551a200

5 files changed

Lines changed: 262 additions & 7 deletions

File tree

docs/api.md

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,19 +210,94 @@ Perform an [env.default](#namespacenotecard_1_1env_1a6ff91175ae591e8a3a87c2a4ef9
210210
#### Returns
211211
string The result of the Notecard request.
212212

213-
#### `public def `[`get`](#namespacenotecard_1_1env_1a28ed0423d0aff1d109371427139e0a73)`(card,name)`
213+
#### `public def `[`get`](#namespacenotecard_1_1env_1a28ed0423d0aff1d109371427139e0a73)`(card,name,names,time)`
214214

215215
Perform an [env.get](#namespacenotecard_1_1env_1a28ed0423d0aff1d109371427139e0a73) request against a Notecard.
216216

217217
#### Parameters
218-
* `card` The current Notecard object.
218+
* `card` The current Notecard object.
219+
220+
* `name` (optional) The name of an environment variable to get.
219221

220-
* `name` The name of an environment variable to get.
222+
* `names` (optional) List of environment variable names to retrieve.
223+
224+
* `time` (optional) UNIX epoch time to get variables modified after.
221225

222226
#### Returns
227+
dict The result of the Notecard request containing either:
228+
* `text` Value of the requested variable if name was specified
229+
* `body` Object with name/value pairs if names was specified or if neither name nor names was specified
230+
* `time` UNIX epoch time of the last variable change
231+
232+
Example request with single variable:
233+
```json
234+
{
235+
"req": "env.get",
236+
"name": "my_var"
237+
}
238+
```
239+
240+
Example request with multiple variables:
241+
```json
242+
{
243+
"req": "env.get",
244+
"names": ["var1", "var2"]
245+
}
246+
```
247+
248+
Example response for single variable:
249+
```json
250+
{
251+
"text": "value1"
252+
}
253+
```
254+
255+
Example response for multiple variables:
256+
```json
257+
{
258+
"body": {
259+
"var1": "value1",
260+
"var2": "value2"
261+
},
262+
"time": 1609459200
263+
}
264+
```
265+
266+
#### `public def `[`template`](#namespacenotecard_1_1env_1a10f5f4667d80f47674d1876df69b8e22)`(card,body)`
267+
268+
Perform an env.template request against a Notecard.
269+
270+
#### Parameters
271+
* `card` The current Notecard object.
272+
273+
* `body` (optional) Schema with variable names and type hints.
274+
* Boolean: must be specified as true
275+
* String: numeric string for max length (pre v3.2.1) or variable-length (v3.2.1+)
276+
* Integer: 11-14, 18 for signed, 21-24 for unsigned
277+
* Float: 12.1 (2-byte), 14.1 (4-byte), 18.1 (8-byte)
223278

224279
#### Returns
225-
string The result of the Notecard request.
280+
dict The result of the Notecard request, including 'bytes' field indicating storage size.
281+
282+
Example request:
283+
```json
284+
{
285+
"req": "env.template",
286+
"body": {
287+
"active": true,
288+
"name": "32",
289+
"temperature": 14.1,
290+
"counter": 12
291+
}
292+
}
293+
```
294+
295+
Example response:
296+
```json
297+
{
298+
"bytes": 42
299+
}
300+
```
226301

227302
#### `public def `[`modified`](#namespacenotecard_1_1env_1aa672554b72786c9ec1e5f76b3e11eb34)`(card)`
228303

notecard/env.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,29 @@ def default(card, name=None, text=None):
3434

3535

3636
@validate_card_object
37-
def get(card, name=None):
37+
def get(card, name=None, names=None, time=None):
3838
"""Perform an env.get request against a Notecard.
3939
4040
Args:
4141
card (Notecard): The current Notecard object.
42-
name (string): The name of an environment variable to get.
42+
name (str, optional): The name of an environment variable to get.
43+
names (list, optional): List of environment variable names to retrieve.
44+
time (int, optional): UNIX epoch time to get variables modified after.
4345
4446
Returns:
45-
string: The result of the Notecard request.
47+
dict: The result of the Notecard request containing either:
48+
- text: Value of the requested variable if name was specified
49+
- body: Object with name/value pairs if names was specified or
50+
if neither name nor names was specified
51+
- time: UNIX epoch time of the last variable change
4652
"""
4753
req = {"req": "env.get"}
4854
if name:
4955
req["name"] = name
56+
if names:
57+
req["names"] = names
58+
if time is not None:
59+
req["time"] = time
5060
return card.Transaction(req)
5161

5262

@@ -82,3 +92,31 @@ def set(card, name=None, text=None):
8292
if text:
8393
req["text"] = text
8494
return card.Transaction(req)
95+
96+
97+
@validate_card_object
98+
def template(card, body=None):
99+
"""Perform an env.template request against a Notecard.
100+
101+
Args:
102+
card (Notecard): The current Notecard object.
103+
body (dict, optional): Schema with variable names and type hints.
104+
Supported type hints:
105+
- Boolean: true
106+
- String: numeric string for max length (pre v3.2.1)
107+
- Integer: 11-14, 18 (signed), 21-24 (unsigned)
108+
- Float: 12.1 (2-byte), 14.1 (4-byte), 18.1 (8-byte)
109+
110+
Returns:
111+
dict: The result of the Notecard request, including 'bytes' field
112+
indicating storage size.
113+
114+
Raises:
115+
ValueError: If type hints in body are invalid.
116+
"""
117+
req = {"req": "env.template"}
118+
if body is not None:
119+
from .validators import validate_template_hints
120+
validate_template_hints(body)
121+
req["body"] = body
122+
return card.Transaction(req)

notecard/validators.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,36 @@ def wrap_validator(*args, **kwargs):
3636
return func(*args, **kwargs)
3737

3838
return wrap_validator
39+
40+
41+
def validate_template_hints(body):
42+
"""Validate type hints in env.template body.
43+
44+
Args:
45+
body (dict): Schema with variable names and type hints.
46+
47+
Raises:
48+
ValueError: If type hints are invalid.
49+
"""
50+
if not isinstance(body, dict):
51+
raise ValueError("Template body must be a dictionary")
52+
53+
for key, value in body.items():
54+
if isinstance(value, bool):
55+
if value is not True:
56+
raise ValueError(f"Boolean hint for {key} must be True")
57+
elif isinstance(value, (int, float)):
58+
valid_types = [
59+
11, 12, 13, 14, 18, # signed integers
60+
21, 22, 23, 24, # unsigned integers
61+
12.1, 14.1, 18.1 # floats
62+
]
63+
if value not in valid_types:
64+
raise ValueError(f"Invalid numeric hint for {key}")
65+
elif isinstance(value, str):
66+
try:
67+
int(value) # pre v3.2.1 string length
68+
except ValueError:
69+
pass # post v3.2.1 variable length strings
70+
else:
71+
raise ValueError(f"Invalid type hint for {key}")

test/fluent_api/test_env.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
'env.get',
1616
{'name': 'my_var'}
1717
),
18+
(
19+
env.get,
20+
'env.get',
21+
{'names': ['var1', 'var2']}
22+
),
23+
(
24+
env.get,
25+
'env.get',
26+
{'time': 1609459200}
27+
),
1828
(
1929
env.modified,
2030
'env.modified',
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Tests for env.template functionality."""
2+
from notecard import env
3+
4+
5+
def test_env_template_basic(run_fluent_api_notecard_api_mapping_test):
6+
"""Test env.template with no body parameter."""
7+
run_fluent_api_notecard_api_mapping_test(
8+
env.template, 'env.template', {})
9+
10+
11+
def test_env_template_with_boolean(run_fluent_api_notecard_api_mapping_test):
12+
"""Test env.template with boolean type hint."""
13+
run_fluent_api_notecard_api_mapping_test(
14+
env.template, 'env.template', {'body': {'my_bool': True}})
15+
16+
17+
def test_env_template_with_string_pre_321(
18+
run_fluent_api_notecard_api_mapping_test):
19+
"""Test string type hint in env.template.
20+
21+
For pre v3.2.1 format."""
22+
body = {'my_string': '42'}
23+
run_fluent_api_notecard_api_mapping_test(
24+
env.template, 'env.template', {'body': body})
25+
26+
27+
def test_env_template_with_string_post_321(
28+
run_fluent_api_notecard_api_mapping_test):
29+
"""Test string type hint in env.template.
30+
31+
For post v3.2.1 format."""
32+
body = {'my_string': 'variable'}
33+
run_fluent_api_notecard_api_mapping_test(
34+
env.template, 'env.template', {'body': body})
35+
36+
37+
def test_env_template_with_signed_integers(
38+
run_fluent_api_notecard_api_mapping_test):
39+
"""Test signed integer hints.
40+
41+
Covers all supported sizes."""
42+
body = {
43+
'int8': 11, # 1 byte signed
44+
'int16': 12, # 2 byte signed
45+
'int24': 13, # 3 byte signed
46+
'int32': 14, # 4 byte signed
47+
'int64': 18 # 8 byte signed
48+
}
49+
run_fluent_api_notecard_api_mapping_test(
50+
env.template, 'env.template', {'body': body})
51+
52+
53+
def test_env_template_with_unsigned_integers(
54+
run_fluent_api_notecard_api_mapping_test):
55+
"""Test unsigned integer hints.
56+
57+
Covers all supported sizes."""
58+
body = {
59+
'uint8': 21, # 1 byte unsigned
60+
'uint16': 22, # 2 byte unsigned
61+
'uint24': 23, # 3 byte unsigned
62+
'uint32': 24 # 4 byte unsigned
63+
}
64+
run_fluent_api_notecard_api_mapping_test(
65+
env.template, 'env.template', {'body': body})
66+
67+
68+
def test_env_template_with_floats(run_fluent_api_notecard_api_mapping_test):
69+
"""Test env.template with float type hints."""
70+
body = {
71+
'float16': 12.1, # 2 byte float
72+
'float32': 14.1, # 4 byte float
73+
'float64': 18.1 # 8 byte float
74+
}
75+
run_fluent_api_notecard_api_mapping_test(
76+
env.template, 'env.template', {'body': body})
77+
78+
79+
def test_env_template_with_mixed_types(
80+
run_fluent_api_notecard_api_mapping_test):
81+
"""Test mixed type hints.
82+
83+
Tests bool, str, float, int."""
84+
body = {
85+
'active': True,
86+
'name': '32',
87+
'temperature': 14.1,
88+
'counter': 12
89+
}
90+
run_fluent_api_notecard_api_mapping_test(
91+
env.template, 'env.template', {'body': body})
92+
93+
94+
def test_env_template_response(card):
95+
"""Test env.template response contains bytes field."""
96+
card.Transaction.return_value = {'bytes': 42}
97+
response = env.template(card)
98+
assert 'bytes' in response
99+
assert isinstance(response['bytes'], int)

0 commit comments

Comments
 (0)