augplot 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {augplot-0.1.0 → augplot-0.1.1}/PKG-INFO +36 -2
  2. {augplot-0.1.0 → augplot-0.1.1}/README.md +35 -1
  3. {augplot-0.1.0 → augplot-0.1.1}/docs/api.md +3 -2
  4. {augplot-0.1.0 → augplot-0.1.1}/docs/generated-code-guardrails.md +19 -6
  5. {augplot-0.1.0 → augplot-0.1.1}/pyproject.toml +1 -1
  6. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/__init__.py +1 -1
  7. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/execution.py +203 -26
  8. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/profiling.py +6 -2
  9. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/prompts.py +21 -3
  10. {augplot-0.1.0 → augplot-0.1.1}/tests/test_execution.py +351 -0
  11. {augplot-0.1.0 → augplot-0.1.1}/tests/test_profiling.py +18 -2
  12. {augplot-0.1.0 → augplot-0.1.1}/tests/test_prompts.py +8 -1
  13. {augplot-0.1.0 → augplot-0.1.1}/tests/test_workflow.py +72 -0
  14. augplot-0.1.1/tests/titanic_fixture.py +40 -0
  15. {augplot-0.1.0 → augplot-0.1.1}/.github/dependabot.yml +0 -0
  16. {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/codeql.yml +0 -0
  17. {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/release.yml +0 -0
  18. {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/tests.yml +0 -0
  19. {augplot-0.1.0 → augplot-0.1.1}/.gitignore +0 -0
  20. {augplot-0.1.0 → augplot-0.1.1}/AGENTS.md +0 -0
  21. {augplot-0.1.0 → augplot-0.1.1}/CHANGELOG.md +0 -0
  22. {augplot-0.1.0 → augplot-0.1.1}/CODE_OF_CONDUCT.md +0 -0
  23. {augplot-0.1.0 → augplot-0.1.1}/CONTRIBUTING.md +0 -0
  24. {augplot-0.1.0 → augplot-0.1.1}/LICENSE +0 -0
  25. {augplot-0.1.0 → augplot-0.1.1}/NOTICE +0 -0
  26. {augplot-0.1.0 → augplot-0.1.1}/SECURITY.md +0 -0
  27. {augplot-0.1.0 → augplot-0.1.1}/docs/assets/augplot-workflow.png +0 -0
  28. {augplot-0.1.0 → augplot-0.1.1}/docs/visualization-history.md +0 -0
  29. {augplot-0.1.0 → augplot-0.1.1}/examples/quickstart.ipynb +0 -0
  30. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/core.py +0 -0
  31. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/datasets.py +0 -0
  32. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/errors.py +0 -0
  33. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/exporting.py +0 -0
  34. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/history.py +0 -0
  35. {augplot-0.1.0 → augplot-0.1.1}/src/augplot/provider.py +0 -0
  36. {augplot-0.1.0 → augplot-0.1.1}/tests/conftest.py +0 -0
  37. {augplot-0.1.0 → augplot-0.1.1}/tests/notebook_charts.py +0 -0
  38. {augplot-0.1.0 → augplot-0.1.1}/tests/test_datasets.py +0 -0
  39. {augplot-0.1.0 → augplot-0.1.1}/tests/test_display.py +0 -0
  40. {augplot-0.1.0 → augplot-0.1.1}/tests/test_export.py +0 -0
  41. {augplot-0.1.0 → augplot-0.1.1}/tests/test_history.py +0 -0
  42. {augplot-0.1.0 → augplot-0.1.1}/tests/test_live.py +0 -0
  43. {augplot-0.1.0 → augplot-0.1.1}/tests/test_metadata.py +0 -0
  44. {augplot-0.1.0 → augplot-0.1.1}/tests/test_notebook.py +0 -0
  45. {augplot-0.1.0 → augplot-0.1.1}/tests/test_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: augplot
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Turn notebook data into visualizations and reusable Python with an LLM.
5
5
  Project-URL: Repository, https://github.com/egpand/augplot
6
6
  Project-URL: Documentation, https://github.com/egpand/augplot/tree/main/docs
@@ -78,6 +78,40 @@ viz.to_python(function_name="plot_monthly_passengers")
78
78
  Open or download the [example notebook](examples/quickstart.ipynb) to try the complete
79
79
  workflow.
80
80
 
81
+ ## What can `data` be?
82
+
83
+ `ap.plot(data)` accepts these nonempty, in-memory Python objects:
84
+
85
+ | Input | Typical use |
86
+ | --- | --- |
87
+ | `pandas.DataFrame` | Rows and columns, including dates and categories. |
88
+ | `pandas.Series` | One labeled sequence of values. |
89
+ | `numpy.ndarray` | One- or two-dimensional numeric or mixed data. |
90
+ | `list` or `tuple` | Values such as `[2, 4, 6]`, or records such as `[{'x': 1, 'y': 2}]`. |
91
+ | `dict` | Columns such as `{'x': [1, 2], 'y': [2, 3]}`, or nested results keyed by model and metric. |
92
+
93
+ Nested lists and dictionaries can contain ordinary scalars, missing values, NumPy arrays,
94
+ and Pandas objects. Parsed JSON is therefore a valid input when its root is a nonempty
95
+ list or dictionary:
96
+
97
+ ```python
98
+ import json
99
+
100
+ records = json.loads('[{"month": "Jan", "sales": 10}, {"month": "Feb", "sales": 12}]')
101
+ viz = ap.plot(records)
102
+
103
+ results = {
104
+ "baseline": {"test_f1": [0.3, 0.4, 0.5]},
105
+ "candidate": {"test_f1": [0.4, 0.5, 0.6]},
106
+ }
107
+ viz = ap.plot(results, prompt="Compare F1 scores across models")
108
+ ```
109
+
110
+ Pass the parsed object, not a JSON string or file path. Empty inputs, cyclic containers,
111
+ custom objects, and NumPy arrays with zero or more than two dimensions raise `DataError`.
112
+ For a Pandas `Index`, another table library, or an iterator, convert to one of the types
113
+ above first.
114
+
81
115
  ## Workflow
82
116
 
83
117
  - `plot()` generates the initial visualization.
@@ -139,7 +173,7 @@ but does not train models or return predictions, forecasts, or other analytical
139
173
 
140
174
  ## Beta and security
141
175
 
142
- Augplot 0.1.0 is a beta release; APIs and saved-history formats may change before 1.0.
176
+ Augplot 0.1.1 is a beta release; APIs and saved-history formats may change before 1.0.
143
177
  Review generated code before sensitive or security-critical use, and report
144
178
  vulnerabilities through the [security policy](SECURITY.md).
145
179
 
@@ -40,6 +40,40 @@ viz.to_python(function_name="plot_monthly_passengers")
40
40
  Open or download the [example notebook](examples/quickstart.ipynb) to try the complete
41
41
  workflow.
42
42
 
43
+ ## What can `data` be?
44
+
45
+ `ap.plot(data)` accepts these nonempty, in-memory Python objects:
46
+
47
+ | Input | Typical use |
48
+ | --- | --- |
49
+ | `pandas.DataFrame` | Rows and columns, including dates and categories. |
50
+ | `pandas.Series` | One labeled sequence of values. |
51
+ | `numpy.ndarray` | One- or two-dimensional numeric or mixed data. |
52
+ | `list` or `tuple` | Values such as `[2, 4, 6]`, or records such as `[{'x': 1, 'y': 2}]`. |
53
+ | `dict` | Columns such as `{'x': [1, 2], 'y': [2, 3]}`, or nested results keyed by model and metric. |
54
+
55
+ Nested lists and dictionaries can contain ordinary scalars, missing values, NumPy arrays,
56
+ and Pandas objects. Parsed JSON is therefore a valid input when its root is a nonempty
57
+ list or dictionary:
58
+
59
+ ```python
60
+ import json
61
+
62
+ records = json.loads('[{"month": "Jan", "sales": 10}, {"month": "Feb", "sales": 12}]')
63
+ viz = ap.plot(records)
64
+
65
+ results = {
66
+ "baseline": {"test_f1": [0.3, 0.4, 0.5]},
67
+ "candidate": {"test_f1": [0.4, 0.5, 0.6]},
68
+ }
69
+ viz = ap.plot(results, prompt="Compare F1 scores across models")
70
+ ```
71
+
72
+ Pass the parsed object, not a JSON string or file path. Empty inputs, cyclic containers,
73
+ custom objects, and NumPy arrays with zero or more than two dimensions raise `DataError`.
74
+ For a Pandas `Index`, another table library, or an iterator, convert to one of the types
75
+ above first.
76
+
43
77
  ## Workflow
44
78
 
45
79
  - `plot()` generates the initial visualization.
@@ -101,7 +135,7 @@ but does not train models or return predictions, forecasts, or other analytical
101
135
 
102
136
  ## Beta and security
103
137
 
104
- Augplot 0.1.0 is a beta release; APIs and saved-history formats may change before 1.0.
138
+ Augplot 0.1.1 is a beta release; APIs and saved-history formats may change before 1.0.
105
139
  Review generated code before sensitive or security-critical use, and report
106
140
  vulnerabilities through the [security policy](SECURITY.md).
107
141
 
@@ -11,8 +11,9 @@ import augplot as ap
11
11
  plt = ap.plot(data, prompt="Plot revenue by channel")
12
12
  ```
13
13
 
14
- `ap.plot()` accepts Pandas objects, NumPy arrays, lists, and nested dictionaries. It
15
- returns the current Augplot chart object and displays its figure by default.
14
+ `ap.plot()` accepts Pandas objects, one- or two-dimensional NumPy arrays, lists, tuples,
15
+ and nested dictionaries. It returns the current Augplot chart object and displays its
16
+ figure by default.
16
17
 
17
18
  ```python
18
19
  plt = ap.plot(
@@ -10,15 +10,27 @@ Generated code must define exactly `plot_data(data, *, title=None, figsize=None)
10
10
  return a figure it created. It may use the approved, in-memory parts of NumPy, Pandas,
11
11
  Matplotlib, and Seaborn. This includes common chart types, subplot layouts, axes, legends,
12
12
  artists, ticks, date formatters, and supported data transformations.
13
+ Nested dictionaries and JSON-like records remain valid inputs. For cross-validation
14
+ results keyed by model name, generated code can derive values with a single comprehension,
15
+ such as `[np.mean(data[name]["test_f1"]) for name in data]`.
13
16
 
14
17
  Imports use fixed aliases: `np`, `pd`, `plt`, `ticker`, `dates`, and `sns`.
15
18
  Every call and attribute path is checked, and figures, axes, artists, and data-derived
16
19
  values are tracked so that a valid object cannot be substituted with an arbitrary callable.
17
20
 
18
- Loops are limited to approved Axes collections, small static sequences, or columns selected
19
- from data explicitly bounded to at most 200 rows with `head` or `tail`. This supports panel
20
- styling and per-record chart annotations without permitting unbounded or nested generated
21
- loops.
21
+ Loops are limited to approved Axes collections, small static sequences, or data explicitly
22
+ bounded to at most 200 rows with `head` or `tail`. Bounded columns and
23
+ `data.head(20).iterrows()` support per-record chart annotations. Unbounded row iteration
24
+ and nested generated loops remain rejected.
25
+ One-dimensional Axes collections from a fixed-size `subplots` call can be iterated directly.
26
+ A list slice such as `names = list(data)[:200]` also provides a bounded sequence; a
27
+ matching `np.arange(len(names))` may position its marks.
28
+ An array extent stored in a local name, such as `column_count = values.shape[1]`, may
29
+ also be used to position marks with `np.arange(column_count)`; it does not authorize an
30
+ unbounded Python annotation loop.
31
+ Simple layout values can be assigned in both branches of a conditional, including a
32
+ default `figsize`. A bounded loop over the four Matplotlib spines may hide them for
33
+ minimal timeline styling.
22
34
 
23
35
  The generation prompt summarizes the validator's main constraints so a provider is less
24
36
  likely to emit code that needs repair. This is compatibility guidance only: the prompt is
@@ -45,14 +57,15 @@ plt.imsave("chart.png", data)
45
57
 
46
58
  Fresh model output receives at most the configured repair attempt (`max_repairs=1` by
47
59
  default). The repair request contains a sanitized validation diagnostic, never a runtime
48
- exception message or data values. If it still fails, `ap.plot()` raises
60
+ exception message or data values. A recognized scatter length mismatch produces a
61
+ data-free hint so the model can repair the x and y arrays. If it still fails, `ap.plot()` raises
49
62
  `GenerationError`; rejected source is never executed, displayed, exported, or saved.
50
63
 
51
64
  ```python
52
65
  try:
53
66
  viz = ap.plot(data, prompt="...")
54
67
  except ap.GenerationError as exc:
55
- print(exc) # concise reason
68
+ print(exc) # reason, with the rejected name or method and line when known
56
69
  print(exc.violations) # structured validation failures
57
70
  print(exc.code) # inspect only when appropriate
58
71
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "augplot"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Turn notebook data into visualizations and reusable Python with an LLM."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -22,4 +22,4 @@ __all__ = [
22
22
  "ProviderError",
23
23
  "ScopeError",
24
24
  ]
25
- __version__ = "0.1.0"
25
+ __version__ = "0.1.1"
@@ -8,7 +8,7 @@ from contextlib import ExitStack
8
8
  from .errors import GenerationError, ScopeError
9
9
  from .profiling import copy_data
10
10
 
11
- API_MANIFEST_VERSION = 2
11
+ API_MANIFEST_VERSION = 8
12
12
  MAX_SOURCE_CHARS = 50_000
13
13
  MAX_AST_NODES = 4_000
14
14
  MAX_LITERAL_ITEMS = 2_000
@@ -176,6 +176,7 @@ _DATA_METHODS = {
176
176
  "idxmin",
177
177
  "infer_objects",
178
178
  "interpolate",
179
+ "iterrows",
179
180
  "isin",
180
181
  "isna",
181
182
  "items",
@@ -445,6 +446,7 @@ _AXES = {
445
446
  "secondary_yaxis",
446
447
  "set",
447
448
  "set_aspect",
449
+ "set_axisbelow",
448
450
  "set_axis_off",
449
451
  "set_axis_on",
450
452
  "set_box_aspect",
@@ -456,10 +458,12 @@ _AXES = {
456
458
  "set_xlim",
457
459
  "set_xscale",
458
460
  "set_xticks",
461
+ "set_xticklabels",
459
462
  "set_ylabel",
460
463
  "set_ylim",
461
464
  "set_yscale",
462
465
  "set_yticks",
466
+ "set_yticklabels",
463
467
  "sharex",
464
468
  "sharey",
465
469
  "specgram",
@@ -612,11 +616,18 @@ class _Validator:
612
616
 
613
617
  def merge_branches(self, before, body, otherwise):
614
618
  merged = {}
619
+ passive_kinds = {"value", "data", "bounded_sequence"}
615
620
  for name in before.keys() | body.keys() | otherwise.keys():
616
621
  body_kind = body.get(name)
617
622
  otherwise_kind = otherwise.get(name)
618
623
  if body_kind == otherwise_kind and body_kind is not None:
619
624
  merged[name] = body_kind
625
+ self.invalid_provenance.discard(name)
626
+ elif {body_kind, otherwise_kind} <= passive_kinds:
627
+ # Keep ordinary values usable after a branch, without carrying a
628
+ # bounded-iteration proof or a plotting-object capability across it.
629
+ merged[name] = "data"
630
+ self.invalid_provenance.discard(name)
620
631
  else:
621
632
  self.invalid_provenance.add(name)
622
633
  return merged
@@ -695,6 +706,108 @@ class _Validator:
695
706
  except (TypeError, ValueError, OverflowError):
696
707
  return False
697
708
 
709
+ def bounded_slice(self, node):
710
+ if not isinstance(node, ast.Slice) or node.upper is None:
711
+ return False
712
+ lower = 0 if node.lower is None else self.static_integer(node.lower)
713
+ upper = self.static_integer(node.upper)
714
+ step = 1 if node.step is None else self.static_integer(node.step)
715
+ return (
716
+ lower is not None
717
+ and upper is not None
718
+ and step is not None
719
+ and 0 <= lower <= upper
720
+ and 0 < step
721
+ and upper - lower <= MAX_BOUNDED_ITERATION
722
+ )
723
+
724
+ def bounded_arange(self, node):
725
+ if len(node.args) != 1 or node.keywords:
726
+ return False
727
+ stop = node.args[0]
728
+ return (
729
+ isinstance(stop, ast.Call)
730
+ and isinstance(stop.func, ast.Name)
731
+ and stop.func.id == "len"
732
+ and len(stop.args) == 1
733
+ and not stop.keywords
734
+ and isinstance(stop.args[0], ast.Name)
735
+ and self.env.get(stop.args[0].id)
736
+ in {"bounded_column", "bounded_data", "bounded_sequence"}
737
+ )
738
+
739
+ def data_extent(self, node):
740
+ data_kinds = {"bounded_column", "bounded_data", "bounded_sequence", "data", "sequence"}
741
+ if isinstance(node, ast.Name):
742
+ return self.env.get(node.id) == "data_extent"
743
+ if isinstance(node, ast.Call):
744
+ return (
745
+ isinstance(node.func, ast.Name)
746
+ and node.func.id == "len"
747
+ and len(node.args) == 1
748
+ and not node.keywords
749
+ and isinstance(node.args[0], ast.Name)
750
+ and self.env.get(node.args[0].id) in data_kinds
751
+ )
752
+ if isinstance(node, ast.Attribute):
753
+ return (
754
+ node.attr == "size"
755
+ and isinstance(node.value, ast.Name)
756
+ and self.env.get(node.value.id) in data_kinds
757
+ )
758
+ if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Attribute):
759
+ return (
760
+ node.value.attr == "shape"
761
+ and isinstance(node.value.value, ast.Name)
762
+ and self.env.get(node.value.value.id) in data_kinds
763
+ and isinstance(node.slice, ast.Constant)
764
+ and node.slice.value in {0, 1}
765
+ )
766
+ return False
767
+
768
+ def data_sized_stop(self, node):
769
+ if self.data_extent(node):
770
+ return True
771
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
772
+ return (
773
+ self.data_extent(node.left)
774
+ and isinstance(node.right, ast.Constant)
775
+ and isinstance(node.right.value, int)
776
+ and abs(node.right.value) <= 1
777
+ )
778
+ return False
779
+
780
+ def data_sized_range(self, node):
781
+ if node.keywords or len(node.args) not in {1, 2}:
782
+ return False
783
+ if len(node.args) == 1:
784
+ return self.data_sized_stop(node.args[0])
785
+ start = self.static_integer(node.args[0])
786
+ return start in {0, 1} and self.data_sized_stop(node.args[1])
787
+
788
+ def subplot_result(self, node):
789
+ rows = self.keyword_value(node, "nrows")
790
+ columns = self.keyword_value(node, "ncols")
791
+ if rows is None and node.args:
792
+ rows = node.args[0]
793
+ if columns is None and len(node.args) > 1:
794
+ columns = node.args[1]
795
+ rows = 1 if rows is None else self.static_integer(rows)
796
+ columns = 1 if columns is None else self.static_integer(columns)
797
+ squeeze = self.keyword_value(node, "squeeze")
798
+ is_flat_axes = (
799
+ isinstance(rows, int)
800
+ and isinstance(columns, int)
801
+ and 0 < rows * columns <= 100
802
+ and rows * columns > 1
803
+ and (rows == 1 or columns == 1)
804
+ and (
805
+ squeeze is None
806
+ or (isinstance(squeeze, ast.Constant) and squeeze.value is True)
807
+ )
808
+ )
809
+ return "axes_sequence" if is_flat_axes else "axes"
810
+
698
811
  def keyword_value(self, node, name):
699
812
  return next((item.value for item in node.keywords if item.arg == name), None)
700
813
 
@@ -822,7 +935,10 @@ class _Validator:
822
935
  )
823
936
  if self.loop_depth:
824
937
  self.fail("resource_limit", "Nested loops are not allowed.")
825
- self.assign(n.target, "axes_sequence" if kind == "axes_sequence" else "value")
938
+ target_kind = kind if kind == "axes_sequence" else "value"
939
+ if kind == "axes_sequence" and isinstance(n.target, ast.Name):
940
+ target_kind = "axes"
941
+ self.assign(n.target, target_kind)
826
942
  self.loop_depth += 1
827
943
  try:
828
944
  for s in n.body + n.orelse:
@@ -849,6 +965,7 @@ class _Validator:
849
965
  elif isinstance(target, (ast.Tuple, ast.List)):
850
966
  if kind not in {
851
967
  "figure_axes",
968
+ "figure_axes_sequence",
852
969
  "bounded_sequence",
853
970
  "sequence",
854
971
  "axes",
@@ -857,14 +974,15 @@ class _Validator:
857
974
  }:
858
975
  self.fail("assignment", "This value cannot be unpacked.")
859
976
  for i, elt in enumerate(target.elts):
860
- self.assign(
861
- elt,
862
- "figure"
863
- if i == 0 and kind == "figure_axes"
864
- else "axes"
865
- if kind in {"figure_axes", "axes"} or (kind == "axes_sequence" and i == 0)
866
- else "value",
867
- )
977
+ if i == 0 and kind in {"figure_axes", "figure_axes_sequence"}:
978
+ item_kind = "figure"
979
+ elif kind == "figure_axes_sequence":
980
+ item_kind = "axes_sequence"
981
+ elif kind in {"figure_axes", "axes"} or (kind == "axes_sequence" and i == 0):
982
+ item_kind = "axes"
983
+ else:
984
+ item_kind = "value"
985
+ self.assign(elt, item_kind)
868
986
  elif isinstance(target, ast.Subscript):
869
987
  # A local, data-derived frame may be reshaped in memory; the caller's
870
988
  # original `data` argument and all attribute/module mutation remain blocked.
@@ -875,7 +993,16 @@ class _Validator:
875
993
  ):
876
994
  self.expr(target.slice)
877
995
  return
878
- self.fail("mutation", "Attribute and subscript assignment are not allowed.")
996
+ self.fail(
997
+ "mutation",
998
+ f"Subscript assignment at generated line {target.lineno} is not allowed.",
999
+ )
1000
+ elif isinstance(target, ast.Attribute):
1001
+ self.fail(
1002
+ "mutation",
1003
+ f"Assignment to .{target.attr} at generated line {target.lineno} is not "
1004
+ "allowed. Derive a separate local value instead.",
1005
+ )
879
1006
  else:
880
1007
  self.fail("mutation", "Attribute and subscript assignment are not allowed.")
881
1008
 
@@ -892,7 +1019,11 @@ class _Validator:
892
1019
  if n.id in _BUILTINS:
893
1020
  return "builtin:" + n.id
894
1021
  if n.id in self.invalid_provenance:
895
- self.fail("provenance", "Name does not have one capability on every path.")
1022
+ self.fail(
1023
+ "provenance",
1024
+ f"Name {n.id!r} at generated line {n.lineno} does not have one "
1025
+ "capability on every path.",
1026
+ )
896
1027
  if n.id.startswith("_") or n.id not in self.env:
897
1028
  self.fail("name", "Source references an unknown capability or name.")
898
1029
  return self.env[n.id]
@@ -900,8 +1031,10 @@ class _Validator:
900
1031
  if len(n.elts) > MAX_LITERAL_ITEMS:
901
1032
  self.fail("literal_size", "Generated source contains an oversized literal.")
902
1033
  kinds = [self.expr(x) for x in n.elts]
903
- if "axes" in kinds or "axes_sequence" in kinds:
1034
+ if kinds and all(kind == "axes" for kind in kinds):
904
1035
  return "axes_sequence"
1036
+ if "axes" in kinds or "axes_sequence" in kinds:
1037
+ self.fail("provenance", "Axes collections may contain only approved Axes.")
905
1038
  return "bounded_sequence" if len(n.elts) <= MAX_BOUNDED_ITERATION else "sequence"
906
1039
  if isinstance(n, ast.Dict):
907
1040
  if len(n.keys) > MAX_LITERAL_ITEMS or any(k is None for k in n.keys):
@@ -915,6 +1048,10 @@ class _Validator:
915
1048
  if isinstance(n, ast.Subscript):
916
1049
  base = self.expr(n.value)
917
1050
  self.expr(n.slice)
1051
+ if self.data_extent(n):
1052
+ return "data_extent"
1053
+ if base == "spines":
1054
+ return "artist"
918
1055
  if base in {"axes", "axes_sequence"}:
919
1056
  return "axes"
920
1057
  if base == "bounded_data" and isinstance(n.slice, ast.Constant):
@@ -923,6 +1060,8 @@ class _Validator:
923
1060
  return "bounded_column"
924
1061
  if base == "bounded_sequence" and isinstance(n.slice, ast.Slice):
925
1062
  return "bounded_sequence"
1063
+ if base == "sequence" and self.bounded_slice(n.slice):
1064
+ return "bounded_sequence"
926
1065
  if base in {"bounded_column", "bounded_data"}:
927
1066
  return "bounded_data"
928
1067
  if base == "bounded_sequence":
@@ -1020,6 +1159,8 @@ class _Validator:
1020
1159
  if n.attr.startswith("_"):
1021
1160
  self.fail("private_access", "Private and dunder attributes are not allowed.")
1022
1161
  base = self.expr(n.value)
1162
+ if base == "module:numpy" and n.attr == "nan":
1163
+ return "value"
1023
1164
  if (
1024
1165
  base
1025
1166
  in {
@@ -1041,9 +1182,14 @@ class _Validator:
1041
1182
  )
1042
1183
  if base == "axes" and n.attr in {"xaxis", "yaxis"}:
1043
1184
  return "axis"
1185
+ if base == "axes" and n.attr == "spines":
1186
+ return "spines"
1044
1187
  if base == "artist" and n.attr in {"figure", "fig"}:
1045
1188
  return "figure"
1046
- self.fail("attribute", "Attribute access is not an approved capability.")
1189
+ self.fail(
1190
+ "attribute",
1191
+ f"Attribute .{n.attr} at generated line {n.lineno} is not an approved capability.",
1192
+ )
1047
1193
 
1048
1194
  def call(self, n):
1049
1195
  if any(isinstance(a, ast.Starred) for a in n.args):
@@ -1055,7 +1201,7 @@ class _Validator:
1055
1201
  self.expr(keyword.value)
1056
1202
  if n.func.id not in _BUILTINS:
1057
1203
  self.fail("call", "Calls must resolve to an approved capability.")
1058
- if n.func.id == "range" and (
1204
+ if n.func.id == "range" and not self.data_sized_range(n) and (
1059
1205
  len(n.args) > 3
1060
1206
  or any(
1061
1207
  not isinstance(x, ast.Constant)
@@ -1069,6 +1215,8 @@ class _Validator:
1069
1215
  return "axes_sequence"
1070
1216
  if n.func.id == "range" and self.bounded_range(n):
1071
1217
  return "bounded_sequence"
1218
+ if n.func.id == "range" and self.data_sized_range(n):
1219
+ return "sequence"
1072
1220
  bounded_kinds = {"bounded_column", "bounded_sequence"}
1073
1221
  if n.func.id == "zip" and any(kind in bounded_kinds for kind in arg_kinds):
1074
1222
  return "bounded_sequence"
@@ -1135,6 +1283,10 @@ class _Validator:
1135
1283
  self.expr(keyword.value)
1136
1284
  self.validate_resources(base, attr, n)
1137
1285
  if base == "module:numpy" and attr in _RESOURCE_NUMPY:
1286
+ if attr == "arange" and self.bounded_arange(n):
1287
+ return "bounded_sequence"
1288
+ if attr == "arange" and self.data_sized_range(n):
1289
+ return "data"
1138
1290
  self.fail(
1139
1291
  "resource_limit",
1140
1292
  "This NumPy allocation or expansion API is outside the safe plotting subset.",
@@ -1155,16 +1307,25 @@ class _Validator:
1155
1307
  if base == "module:matplotlib.dates" and attr in _DATES:
1156
1308
  return "artist"
1157
1309
  if base == "module:matplotlib.pyplot" and attr in _PYPLOT:
1158
- return (
1159
- "figure_axes"
1160
- if attr in {"subplots", "subplot_mosaic"}
1161
- else "figure"
1162
- if attr == "figure"
1163
- else "artist"
1164
- )
1310
+ if attr == "subplots":
1311
+ return (
1312
+ "figure_axes_sequence"
1313
+ if self.subplot_result(n) == "axes_sequence"
1314
+ else "figure_axes"
1315
+ )
1316
+ if attr == "subplot_mosaic":
1317
+ return "figure_axes"
1318
+ return "figure" if attr == "figure" else "artist"
1165
1319
  if base == "module:seaborn" and attr in _SEABORN:
1166
1320
  return "artist"
1167
1321
  if base in {"bounded_column", "bounded_data", "data"} and attr in _DATA_METHODS:
1322
+ if attr == "iterrows":
1323
+ if base == "bounded_data" and not n.args and not n.keywords:
1324
+ return "bounded_sequence"
1325
+ self.fail(
1326
+ "resource_limit",
1327
+ "iterrows() requires a DataFrame explicitly capped with head(N) or tail(N).",
1328
+ )
1168
1329
  if attr in {"head", "tail"} and self.bounded_head_or_tail(n):
1169
1330
  return "bounded_column" if base == "bounded_column" else "bounded_data"
1170
1331
  if base in {"bounded_column", "bounded_data"} and attr in {
@@ -1180,7 +1341,12 @@ class _Validator:
1180
1341
  return base
1181
1342
  return "data"
1182
1343
  if base == "figure" and attr in _FIGURE:
1183
- return "axes" if attr in {"add_axes", "add_subplot", "subplots"} else "artist"
1344
+ if attr == "subplots":
1345
+ return self.subplot_result(n)
1346
+ return "axes" if attr in {"add_axes", "add_subplot"} else "artist"
1347
+ if base == "value" and attr == "startswith" and len(n.args) == 1 and not n.keywords:
1348
+ if isinstance(n.args[0], ast.Constant) and isinstance(n.args[0].value, str):
1349
+ return "value"
1184
1350
  if base == "axes" and attr == "set":
1185
1351
  names = {keyword.arg for keyword in n.keywords}
1186
1352
  if n.args or None in names or not names <= _SAFE_AXES_SET_KEYWORDS:
@@ -1214,7 +1380,11 @@ class _Validator:
1214
1380
  return "artist"
1215
1381
  if base in {"bounded_sequence", "sequence"} and attr in _SEQUENCE_METHODS:
1216
1382
  return "value"
1217
- self.fail("call", "Call target is not in the approved capability manifest.")
1383
+ self.fail(
1384
+ "call",
1385
+ f"Call to .{attr}() at generated line {n.lineno} is not in the approved "
1386
+ "capability manifest.",
1387
+ )
1218
1388
 
1219
1389
 
1220
1390
  def validate_code(code: str, backend: str) -> ast.Module:
@@ -1245,7 +1415,10 @@ def execute(code, data, *, backend, title=None, figsize=None):
1245
1415
  permitted = allowed_imports(backend)
1246
1416
 
1247
1417
  def guarded_import(name, globals=None, locals=None, fromlist=(), level=0):
1248
- if level or name not in permitted:
1418
+ numpy_internal = name in {"numpy._core._methods", "numpy.core._methods"}
1419
+ # NumPy's ndarray reductions import this module lazily through the calling
1420
+ # function's builtins. The AST still forbids generated source from importing it.
1421
+ if level or name not in permitted and not numpy_internal:
1249
1422
  raise ImportError("Import is not permitted.")
1250
1423
  return builtins.__import__(name, globals, locals, fromlist, level)
1251
1424
 
@@ -1275,9 +1448,13 @@ def execute(code, data, *, backend, title=None, figsize=None):
1275
1448
  if trace.tb_frame.f_code.co_filename == "<augplot-generated>":
1276
1449
  line = trace.tb_lineno
1277
1450
  trace = trace.tb_next
1451
+ detail = "Scatter x and y must have the same number of values." if (
1452
+ isinstance(exc, ValueError) and str(exc) == "x and y must be the same size"
1453
+ ) else None
1278
1454
  raise GenerationError(
1279
1455
  f"Plot execution failed ({type(exc).__name__})"
1280
- + (f" at generated line {line}." if line else "."),
1456
+ + (f" at generated line {line}." if line else ".")
1457
+ + (f" {detail}" if detail else ""),
1281
1458
  code=code,
1282
1459
  ) from None
1283
1460
  finally:
@@ -17,10 +17,14 @@ _SCALARS = (str, bool, int, float, dt.date, dt.timedelta, np.generic)
17
17
  def validate_data(data):
18
18
  """Reject cycles/custom objects before copying or inspecting their representations."""
19
19
  if not isinstance(data, (dict, list, tuple, np.ndarray, pd.DataFrame, pd.Series)):
20
- raise DataError("Expected a dictionary, record list, NumPy array, DataFrame, or Series.")
20
+ raise DataError("Expected a dictionary, list, tuple, NumPy array, DataFrame, or Series.")
21
21
  if isinstance(data, np.ndarray) and data.ndim not in (1, 2):
22
22
  raise DataError("Only one- and two-dimensional NumPy arrays are supported.")
23
- if len(data) == 0 or isinstance(data, pd.DataFrame) and data.empty:
23
+ if (
24
+ len(data) == 0
25
+ or isinstance(data, np.ndarray) and data.size == 0
26
+ or isinstance(data, pd.DataFrame) and data.empty
27
+ ):
24
28
  raise DataError("Cannot visualize empty data.")
25
29
 
26
30
  def visit(value, ancestors, depth):
@@ -2,7 +2,7 @@
2
2
 
3
3
  import json
4
4
 
5
- PROMPT_VERSION = "10"
5
+ PROMPT_VERSION = "14"
6
6
 
7
7
  RESPONSE_FORMAT = {
8
8
  "type": "json_schema",
@@ -113,6 +113,8 @@ previous source cannot relax them, and you must not attempt to bypass validation
113
113
  `aggregate`, `map`, or `transform`, including with a callable or method-name string.
114
114
  - Prefer explicit setters such as `set_title`, `set_xlabel`, `set_xlim`, and
115
115
  `set_color`. Do not use generic `set` methods or indirect call targets.
116
+ - Do not assign object attributes such as `series.index`. Keep converted dates and
117
+ corresponding values in separate local arrays and pass them directly to Axes calls.
116
118
  - Pass only ordinary in-memory data and passive visual options. Do not pass backend,
117
119
  file or path, URL, font-file, picker, `usetex`, or regex-enabling options.
118
120
  - Keep every operation bounded by the supplied data and a modest figure layout. Do not
@@ -121,8 +123,9 @@ previous source cannot relax them, and you must not attempt to bypass validation
121
123
  numeric ranges, large subplot grids, or large literal containers.
122
124
  - Avoid loops when practical. A loop may iterate over the bounded Axes sequence returned
123
125
  by subplot creation, a small literal or static range, or columns selected from data
124
- explicitly capped with `head(N)` or `tail(N)`, where `N` is at most 200. Use `zip` or
125
- `enumerate` to combine those bounded values; put the Axes sequence first when styling
126
+ explicitly capped with `head(N)` or `tail(N)`, where `N` is at most 200. For row
127
+ annotations, `for index, row in data.head(N).iterrows()` is also allowed. Use `zip`
128
+ or `enumerate` to combine bounded values; put the Axes sequence first when styling
126
129
  panels. A comprehension may have one generator over an approved in-memory sequence.
127
130
  Nested loops and nested comprehensions are not allowed.
128
131
 
@@ -181,6 +184,21 @@ variation when available. Label error bars precisely, such as standard deviation
181
184
  not flip negative scores without an explicit instruction. Highlighting the highest
182
185
  observed score does not establish statistical significance or select a model for
183
186
  deployment.
187
+
188
+ For nested result dictionaries keyed by model name, use direct key access and a single
189
+ comprehension to derive plotted values. For example, when the profile contains
190
+ `test_f1`, `names = list(data)` and
191
+ `means = [np.mean(data[name]["test_f1"]) for name in names]` work with lists and NumPy
192
+ arrays. Compute from the full `data` argument at runtime, and choose only metric keys
193
+ that are present in the profile.
194
+
195
+ For a small fixed set of metrics, a one-row or one-column `plt.subplots` call gives an
196
+ Axes sequence that can be styled with `for ax, metric in zip(axes, metrics)`. Limit model
197
+ names with `names = list(data)[:200]`; then `np.arange(len(names))` is a bounded way to
198
+ position marks. Avoid iterating over an unbounded collection of data rows. Mean and
199
+ standard-deviation marks are sufficient for cross-validation comparisons. If adding raw
200
+ fold observations with `scatter`, supply x and y arrays of equal length; a scalar model
201
+ position cannot be paired with a multi-value fold array.
184
202
  """
185
203
 
186
204
  _CV_REQUEST_MARKERS = ("cross-validation", "cross validation", "fold", "r²")