harnice 0.3.0__py3-none-any.whl

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 (41) hide show
  1. harnice/__init__.py +0 -0
  2. harnice/__main__.py +4 -0
  3. harnice/cli.py +234 -0
  4. harnice/fileio.py +295 -0
  5. harnice/gui/launcher.py +426 -0
  6. harnice/lists/channel_map.py +182 -0
  7. harnice/lists/circuits_list.py +302 -0
  8. harnice/lists/disconnect_map.py +237 -0
  9. harnice/lists/formboard_graph.py +63 -0
  10. harnice/lists/instances_list.py +280 -0
  11. harnice/lists/library_history.py +40 -0
  12. harnice/lists/manifest.py +93 -0
  13. harnice/lists/post_harness_instances_list.py +66 -0
  14. harnice/lists/rev_history.py +325 -0
  15. harnice/lists/signals_list.py +135 -0
  16. harnice/products/__init__.py +1 -0
  17. harnice/products/cable.py +152 -0
  18. harnice/products/chtype.py +80 -0
  19. harnice/products/device.py +844 -0
  20. harnice/products/disconnect.py +225 -0
  21. harnice/products/flagnote.py +139 -0
  22. harnice/products/harness.py +522 -0
  23. harnice/products/macro.py +10 -0
  24. harnice/products/part.py +640 -0
  25. harnice/products/system.py +125 -0
  26. harnice/products/tblock.py +270 -0
  27. harnice/state.py +57 -0
  28. harnice/utils/appearance.py +51 -0
  29. harnice/utils/circuit_utils.py +326 -0
  30. harnice/utils/feature_tree_utils.py +183 -0
  31. harnice/utils/formboard_utils.py +973 -0
  32. harnice/utils/library_utils.py +333 -0
  33. harnice/utils/note_utils.py +417 -0
  34. harnice/utils/svg_utils.py +819 -0
  35. harnice/utils/system_utils.py +563 -0
  36. harnice-0.3.0.dist-info/METADATA +32 -0
  37. harnice-0.3.0.dist-info/RECORD +41 -0
  38. harnice-0.3.0.dist-info/WHEEL +5 -0
  39. harnice-0.3.0.dist-info/entry_points.txt +3 -0
  40. harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
  41. harnice-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,326 @@
1
+ from harnice import fileio
2
+ from harnice.lists import instances_list
3
+ from harnice.utils import library_utils
4
+ import os
5
+
6
+
7
+ def end_ports_of_circuit(circuit_id):
8
+ """
9
+ Returns the instance names at the end ports (port 0 and maximum port) of a circuit.
10
+
11
+ Finds and returns the instance names connected to port 0 and the maximum port number
12
+ in the specified circuit. These represent the endpoints of the circuit.
13
+
14
+ **Args:**
15
+ - `circuit_id` (str): Circuit ID to look up. Must be a valid integer string.
16
+
17
+ **Returns:**
18
+ - `tuple`: A tuple of `(zero_port, max_port)` instance names. Either may be empty
19
+ string if not found.
20
+
21
+ **Raises:**
22
+ - `ValueError`: If `circuit_id` is not a valid integer.
23
+ """
24
+ try:
25
+ int(circuit_id)
26
+ except ValueError:
27
+ raise ValueError(f"Pass an integer circuit_id, not '{circuit_id}'")
28
+ zero_port = ""
29
+ max_port = ""
30
+ for instance in fileio.read_tsv("instances list"):
31
+ if instance.get("circuit_id") == circuit_id:
32
+ if instance.get("circuit_port_number") == 0:
33
+ zero_port = instance.get("instance_name")
34
+ if instance.get("circuit_port_number") == max_port_number_in_circuit(
35
+ circuit_id
36
+ ):
37
+ max_port = instance.get("instance_name")
38
+ return zero_port, max_port
39
+
40
+
41
+ def max_port_number_in_circuit(circuit_id):
42
+ """
43
+ Finds the maximum circuit port number used in a circuit.
44
+
45
+ Scans all instances in the circuit to find the highest port number assigned.
46
+ Circuit instances (`item_type=="circuit"`) are skipped. Blank port numbers
47
+ cause an error unless the instance is a circuit instance.
48
+
49
+ **Args:**
50
+ - `circuit_id` (str): Circuit ID to search.
51
+
52
+ **Returns:**
53
+ - `int`: The maximum port number found in the circuit (`0` if no ports found).
54
+
55
+ **Raises:**
56
+ - `ValueError`: If any non-circuit instance has a blank `circuit_port_number`.
57
+ """
58
+ max_port_number = 0
59
+ for instance in fileio.read_tsv("instances list"):
60
+ if instance.get("circuit_id") == circuit_id:
61
+ if instance.get("circuit_port_number") == "":
62
+ if instance.get("item_type") == "circuit":
63
+ continue
64
+ raise ValueError(
65
+ f"Circuit port number is blank for {instance.get('instance_name')}"
66
+ )
67
+ max_port_number = max(
68
+ max_port_number, int(instance.get("circuit_port_number"))
69
+ )
70
+ return max_port_number
71
+
72
+
73
+ def squeeze_instance_between_ports_in_circuit(
74
+ instance_name, circuit_id, new_circuit_port_number
75
+ ):
76
+ """
77
+ Inserts an instance into a circuit by shifting existing port numbers.
78
+
79
+ Assigns the specified instance to a port number in the circuit, incrementing
80
+ the port numbers of all instances that were at or after that port number.
81
+ Circuit instances (`item_type=="circuit"`) are skipped and not renumbered.
82
+
83
+ **Args:**
84
+ - `instance_name` (str): Name of the instance to insert into the circuit.
85
+ - `circuit_id` (str): Circuit ID to insert the instance into.
86
+ - `new_circuit_port_number` (int): Port number to assign to the instance. All
87
+ instances at this port number or higher will have their port numbers
88
+ incremented by 1.
89
+ """
90
+ instances = fileio.read_tsv("instances list")
91
+ for instance in instances:
92
+ if instance.get("instance_name") == instance_name:
93
+ continue
94
+ if instance.get("circuit_id") == circuit_id:
95
+ if instance.get("item_type") == "circuit":
96
+ continue
97
+ old_port_number = instance.get("circuit_port_number")
98
+ if int(instance.get("circuit_port_number")) < new_circuit_port_number:
99
+ continue
100
+ else:
101
+ instances_list.modify(
102
+ instance.get("instance_name"),
103
+ {"circuit_port_number": int(old_port_number) + 1},
104
+ )
105
+ for instance in instances:
106
+ if instance.get("instance_name") == instance_name:
107
+ instances_list.modify(
108
+ instance_name,
109
+ {
110
+ "circuit_id": circuit_id,
111
+ "circuit_port_number": new_circuit_port_number,
112
+ },
113
+ )
114
+
115
+
116
+ def instances_of_circuit(circuit_id):
117
+ """
118
+ Returns all instances in a circuit, sorted by port number.
119
+
120
+ Finds all instances (excluding circuit instances themselves) that belong to
121
+ the specified circuit and returns them sorted numerically by their `circuit_port_number`.
122
+ Instances with missing port numbers are sorted last (treated as `999999`).
123
+
124
+ **Args:**
125
+ - `circuit_id` (str): Circuit ID to search for instances.
126
+
127
+ **Returns:**
128
+ - `list`: List of instance dictionaries, sorted by `circuit_port_number` in ascending order.
129
+ """
130
+ instances = []
131
+ for instance in fileio.read_tsv("instances list"):
132
+ if instance.get("circuit_id") == circuit_id:
133
+ if instance.get("item_type") == "circuit":
134
+ continue
135
+ instances.append(instance)
136
+
137
+ # sort numerically by circuit_port_number, treating missing as large number
138
+ instances.sort(key=lambda x: int(x.get("circuit_port_number") or 999999))
139
+
140
+ return instances
141
+
142
+
143
+ def instance_of_circuit_port_number(circuit_id, circuit_port_number):
144
+ """
145
+ Finds the instance name at a specific port number in a circuit.
146
+
147
+ Searches the instances list for an instance that matches both the `circuit_id`
148
+ and `circuit_port_number`. The comparison is done after stripping whitespace
149
+ and converting to strings.
150
+
151
+ **Args:**
152
+ - `circuit_id` (str): Circuit ID to search.
153
+ - `circuit_port_number` (str or int): Port number to search for.
154
+
155
+ **Returns:**
156
+ - `str`: The `instance_name` of the instance at the specified port number.
157
+
158
+ **Raises:**
159
+ - `ValueError`: If `circuit_id` or `circuit_port_number` is blank, or if no instance
160
+ is found matching both the `circuit_id` and `circuit_port_number`.
161
+ """
162
+ if circuit_id in ["", None]:
163
+ raise ValueError("Circuit ID is blank")
164
+ if circuit_port_number in ["", None]:
165
+ raise ValueError("Circuit port number is blank")
166
+
167
+ for instance in fileio.read_tsv("instances list"):
168
+ if instance.get("circuit_id").strip() == str(circuit_id).strip():
169
+ if (
170
+ instance.get("circuit_port_number").strip()
171
+ == str(circuit_port_number).strip()
172
+ ):
173
+ return instance.get("instance_name")
174
+
175
+ raise ValueError(
176
+ f"No instance found for circuit {circuit_id} and port number {circuit_port_number}"
177
+ )
178
+
179
+
180
+ def circuit_instance_of_instance(instance_name):
181
+ """
182
+ Returns the circuit instance dictionary for an instance that belongs to a circuit.
183
+
184
+ Finds the circuit instance (`item_type=="circuit"`) that corresponds to a given
185
+ instance. The circuit instance has the same `circuit_id` as the instance's `circuit_id`.
186
+
187
+ **Args:**
188
+ - `instance_name` (str): Name of the instance to find the circuit instance for.
189
+
190
+ **Returns:**
191
+ - `dict`: The circuit instance dictionary.
192
+
193
+ **Raises:**
194
+ - `ValueError`: If the circuit instance cannot be found for the given instance.
195
+ """
196
+ circuit_instance_name = ""
197
+ instance_rows = fileio.read_tsv("instances list")
198
+ for instance in instance_rows:
199
+ if instance.get("instance_name") == instance_name:
200
+ circuit_instance_name = instance.get("circuit_id")
201
+ break
202
+ for instance in instance_rows:
203
+ if instance.get("circuit_id") == circuit_instance_name:
204
+ if instance.get("instance_name") == instance_name:
205
+ return instance
206
+ raise ValueError(
207
+ f"Circuit instance {circuit_instance_name} of instance {instance_name} not found"
208
+ )
209
+
210
+
211
+ def assign_cable_conductor(
212
+ cable_instance_name, # unique identifier for the cable in your project
213
+ cable_conductor_id, # (container, identifier) tuple identifying the conductor in the cable being imported
214
+ conductor_instance, # instance name of the conductor in your project
215
+ library_info, # dict containing library info: {lib_repo, mpn, lib_subpath, used_rev}
216
+ net, # which net this cable belongs to
217
+ ):
218
+ """
219
+ Assigns a conductor instance to a specific conductor in a cable.
220
+
221
+ Links a conductor instance in the project to a specific conductor within a cable
222
+ by importing the cable from the library (if not already imported) and updating
223
+ the conductor instance with cable assignment information, including the conductor's
224
+ appearance from the cable definition.
225
+
226
+ The `cable_conductor_id` uses the `(container, identifier)` format from the cable
227
+ conductor list. The cable is imported if it doesn't exist, and the conductor
228
+ instance is updated with parent, group, container, identifier, and appearance info.
229
+
230
+ **Args:**
231
+ - `cable_instance_name` (str): Unique identifier for the cable in the project.
232
+ - `cable_conductor_id` (tuple): Tuple of `(container, identifier)` identifying the
233
+ conductor in the cable being imported.
234
+ - `conductor_instance` (str): Instance name of the conductor in the project.
235
+ - `library_info` (dict): Dictionary containing library information with keys:
236
+ `lib_repo`, `mpn`, `lib_subpath`, and optionally `used_rev`.
237
+ - `net` (str): Net name that this cable belongs to.
238
+
239
+ **Raises:**
240
+ - `ValueError`: If the conductor has already been assigned to another instance,
241
+ or if the `conductor_instance` has already been assigned to another cable.
242
+ """
243
+ # for cable_conductor_id, see (container, identifier) from the cable conductor list.
244
+ # TODO: ensure cable_conductor_id has the right format.
245
+
246
+ instances = fileio.read_tsv("instances list")
247
+
248
+ # --- Make sure conductor of cable has not been assigned yet
249
+ for instance in instances:
250
+ if instance.get("cable_group") == cable_instance_name:
251
+ if instance.get("cable_container") == cable_conductor_id[0]:
252
+ if instance.get("cable_identifier") == cable_conductor_id[1]:
253
+ raise ValueError(
254
+ f"when assingning '{cable_conductor_id} of '{cable_instance_name}' to '{conductor_instance}', "
255
+ f"conductor '{cable_conductor_id}' of '{cable_instance_name}' has already been assigned to {instance.get('instance_name')}"
256
+ )
257
+
258
+ # --- Make sure conductor instance has not already been assigned to a cable
259
+ for instance in instances:
260
+ if instance.get("instance_name") == conductor_instance:
261
+ if (
262
+ instance.get("cable_group") not in ["", None]
263
+ or instance.get("cable_container") not in ["", None]
264
+ or instance.get("cable_identifier") not in ["", None]
265
+ ):
266
+ raise ValueError(
267
+ f"when assingning '{cable_conductor_id} of '{cable_instance_name}' to {conductor_instance}', "
268
+ f"instance '{conductor_instance}' has alredy been assigned to another cable"
269
+ f"to '{instance.get('cable_identifier')}' of cable '{instance.get('cable_group')}'"
270
+ )
271
+
272
+ cable_destination_directory = os.path.join(
273
+ fileio.dirpath(None), "instance_data", "cable", cable_instance_name
274
+ )
275
+
276
+ instances_list.new_instance(
277
+ cable_instance_name,
278
+ {
279
+ "net": net,
280
+ "item_type": "cable",
281
+ "location_type": "segment",
282
+ "cable_group": cable_instance_name,
283
+ },
284
+ ignore_duplicates=True,
285
+ )
286
+
287
+ # --- Import cable from library ---
288
+ library_utils.pull(
289
+ {
290
+ "lib_repo": library_info.get("lib_repo"),
291
+ "lib_subpath": library_info.get("lib_subpath"),
292
+ "item_type": "cable",
293
+ "mpn": library_info.get("mpn"),
294
+ "instance_name": cable_instance_name,
295
+ },
296
+ destination_directory=cable_destination_directory,
297
+ )
298
+
299
+ cable_attributes_path = os.path.join(
300
+ cable_destination_directory, f"{cable_instance_name}-conductor_list.tsv"
301
+ )
302
+ cable_attributes = fileio.read_tsv(cable_attributes_path)
303
+
304
+ # --- assign conductor
305
+ for instance in instances:
306
+ if instance.get("instance_name") == conductor_instance:
307
+ appearance = None
308
+ for row in cable_attributes:
309
+ if (
310
+ row.get("container") == cable_conductor_id[0]
311
+ and row.get("identifier") == cable_conductor_id[1]
312
+ ):
313
+ appearance = row.get("appearance")
314
+ break
315
+
316
+ instances_list.modify(
317
+ conductor_instance,
318
+ {
319
+ "parent_instance": cable_instance_name,
320
+ "cable_group": cable_instance_name,
321
+ "cable_container": cable_conductor_id[0],
322
+ "cable_identifier": cable_conductor_id[1],
323
+ "appearance": appearance,
324
+ },
325
+ )
326
+ break
@@ -0,0 +1,183 @@
1
+ import os
2
+ import runpy
3
+ import math
4
+ import json
5
+ import shutil
6
+ from harnice import fileio
7
+ from harnice.utils import library_utils
8
+
9
+
10
+ def run_macro(
11
+ macro_part_number, lib_subpath, lib_repo, artifact_id, base_directory=None, **kwargs
12
+ ):
13
+ """
14
+ Runs a macro script from the library with the given artifact ID.
15
+
16
+ Imports a macro from the library and executes its Python script. The macro
17
+ is pulled into a directory structure and then executed with the `artifact_id`
18
+ and any additional keyword arguments passed as global variables.
19
+
20
+ **Args:**
21
+ - `macro_part_number` (str): Part number of the macro to run.
22
+ - `lib_subpath` (str): Library subpath where the macro is located.
23
+ - `lib_repo` (str): Library repository URL or `"local"` for local library.
24
+ - `artifact_id` (str): Unique identifier for this macro execution (must be unique).
25
+ - `base_directory` (str, optional): Base directory for the macro output. If `None`,
26
+ defaults to `instance_data/macro/{artifact_id}`.
27
+ - `**kwargs`: Additional keyword arguments to pass as global variables to the macro script.
28
+
29
+ **Raises:**
30
+ - `ValueError`: If `artifact_id` is `None`, `macro_part_number` is `None`, `lib_repo` is `None`,
31
+ or if a macro with the given `artifact_id` already exists in library history.
32
+ """
33
+ if artifact_id is None:
34
+ raise ValueError("artifact_id is required")
35
+ if macro_part_number is None:
36
+ raise ValueError("macro_part_number is required")
37
+ if lib_repo is None:
38
+ raise ValueError("lib_repo is required")
39
+
40
+ for instance in fileio.read_tsv("library history"):
41
+ if instance.get("instance_name") == artifact_id:
42
+ raise ValueError(f"Macro with ID {artifact_id} already exists")
43
+
44
+ if base_directory is None:
45
+ base_directory = os.path.join("instance_data", "macro", artifact_id)
46
+
47
+ os.makedirs(fileio.dirpath(None, base_directory), exist_ok=True)
48
+
49
+ library_utils.pull(
50
+ {
51
+ "mpn": macro_part_number,
52
+ "lib_repo": lib_repo,
53
+ "lib_subpath": lib_subpath,
54
+ "item_type": "macro",
55
+ "instance_name": artifact_id,
56
+ },
57
+ destination_directory=fileio.dirpath(None, base_directory=base_directory),
58
+ update_instances_list=False,
59
+ )
60
+
61
+ script_path = os.path.join(
62
+ fileio.dirpath(None, base_directory=base_directory), f"{macro_part_number}.py"
63
+ )
64
+
65
+ # always pass the basics, but let kwargs override/extend
66
+ init_globals = {
67
+ "artifact_id": artifact_id,
68
+ "artifact_path": base_directory,
69
+ "base_directory": base_directory,
70
+ **kwargs, # merges/overrides
71
+ }
72
+
73
+ runpy.run_path(script_path, run_name="__main__", init_globals=init_globals)
74
+
75
+
76
+ def lookup_outputcsys_from_lib_used(instance, outputcsys, base_directory=None):
77
+ """
78
+ Looks up coordinate system transform from an instance's library attributes.
79
+
80
+ Reads the instance's attributes JSON file to find the specified output coordinate
81
+ system definition and returns its transform values. If the coordinate system is
82
+ `"origin"`, returns zero transform.
83
+
84
+ **Args:**
85
+ - `instance` (dict): Instance dictionary containing `item_type` and `instance_name`.
86
+ - `outputcsys` (str): Name of the output coordinate system to look up (`"origin"` returns zero transform).
87
+ - `base_directory` (str, optional): Base directory path. If `None`, uses current working directory.
88
+
89
+ **Returns:**
90
+ - `tuple`: A tuple of `(x, y, rotation)` representing the coordinate system transform.
91
+ Returns `(0, 0, 0)` if the coordinate system is `"origin"` or if the attributes
92
+ file is not found.
93
+
94
+ **Raises:**
95
+ - `ValueError`: If the specified output coordinate system is not defined in the
96
+ instance's attributes file.
97
+ """
98
+ if outputcsys == "origin":
99
+ return 0, 0, 0
100
+
101
+ attributes_path = os.path.join(
102
+ fileio.dirpath(None, base_directory=base_directory),
103
+ "instance_data",
104
+ instance.get("item_type"),
105
+ instance.get("instance_name"),
106
+ f"{instance.get("instance_name")}-attributes.json",
107
+ )
108
+
109
+ try:
110
+ with open(attributes_path, "r", encoding="utf-8") as f:
111
+ attributes_data = json.load(f)
112
+ except FileNotFoundError:
113
+ return 0, 0, 0
114
+
115
+ csys_children = attributes_data.get("csys_children", {})
116
+
117
+ if outputcsys not in csys_children:
118
+ raise ValueError(
119
+ f"[ERROR] Output coordinate system '{outputcsys}' not defined in {attributes_path}"
120
+ )
121
+
122
+ child_csys = csys_children[outputcsys]
123
+
124
+ # Extract values with safe numeric defaults
125
+ x = child_csys.get("x", 0)
126
+ y = child_csys.get("y", 0)
127
+ angle = child_csys.get("angle", 0)
128
+ distance = child_csys.get("distance", 0)
129
+ rotation = child_csys.get("rotation", 0)
130
+
131
+ # Convert angle to radians if it's stored in degrees
132
+ angle_rad = math.radians(angle)
133
+
134
+ # Apply translation based on distance + angle
135
+ x = x + distance * math.cos(angle_rad)
136
+ y = y + distance * math.sin(angle_rad)
137
+
138
+ return x, y, rotation
139
+
140
+
141
+ def copy_pdfs_to_cwd():
142
+ """
143
+ Copies all PDF files from `instance_data` directory to the current working directory.
144
+
145
+ Recursively searches the `instance_data` directory tree and copies all PDF files
146
+ found to the current working directory. Preserves file metadata during copy.
147
+ Prints error messages if any files cannot be copied but continues processing.
148
+ """
149
+ cwd = os.getcwd()
150
+
151
+ for root, _, files in os.walk(fileio.dirpath(None, base_directory="instance_data")):
152
+ for filename in files:
153
+ if filename.lower().endswith(".pdf"):
154
+ source_path = os.path.join(root, filename)
155
+ dest_path = os.path.join(cwd, filename)
156
+
157
+ try:
158
+ shutil.copy2(source_path, dest_path) # preserves metadata
159
+ except Exception as e:
160
+ print(f"[ERROR] Could not copy {source_path}: {e}")
161
+
162
+
163
+ def run_feature_for_relative(project_key, referenced_pn_rev, feature_tree_utils_name):
164
+ """
165
+ Runs a feature tree script from a referenced part's `features_for_relatives` directory.
166
+
167
+ Executes a Python script located in the `features_for_relatives` directory of a
168
+ referenced part. This is used to run feature scripts that are associated with
169
+ parts referenced by the current project.
170
+
171
+ **Args:**
172
+ - `project_key` (str): Key identifying the project to look up.
173
+ - `referenced_pn_rev` (tuple): Tuple of `(part_number, revision)` for the referenced part.
174
+ - `feature_tree_utils_name` (str): Filename of the feature tree script to execute.
175
+ """
176
+ project_path = fileio.get_path_to_project(project_key)
177
+ feature_tree_utils_path = os.path.join(
178
+ project_path,
179
+ f"{referenced_pn_rev[0]}-{referenced_pn_rev[1]}",
180
+ "features_for_relatives",
181
+ feature_tree_utils_name,
182
+ )
183
+ runpy.run_path(feature_tree_utils_path, run_name="__main__")