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,333 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ import filecmp
5
+ import json
6
+ from harnice import fileio
7
+ from harnice.lists import instances_list, library_history, rev_history
8
+ from harnice.cli import print_import_status
9
+
10
+ """
11
+ where a part lands in a project after it's been imported:
12
+
13
+ instance_data
14
+ item_type
15
+ destination_directory
16
+ lib_used
17
+ lib_used_rev
18
+
19
+ """
20
+
21
+
22
+ def pull(input_dict, update_instances_list=True, destination_directory=None):
23
+ """
24
+ Imports a part from the library into the project.
25
+
26
+ Copies a part (device, connector, cable, etc.) from the library repository into
27
+ the project's `instance_data` directory. Handles revision selection, file copying,
28
+ and updating the instances list and library history. The function:
29
+
30
+ 1. Validates required fields (`lib_repo`, `mpn`, `item_type`)
31
+ 2. Determines which revision to use (specified or latest available)
32
+ 3. Copies the library revision to `library_used_do_not_edit`
33
+ 4. Copies editable files to the instance directory (only if not already present)
34
+ 5. Updates the instances list with library metadata
35
+ 6. Records the import in library history
36
+
37
+ **Args:**
38
+ - `input_dict` (dict): Dictionary containing part information with required keys:
39
+ - `instance_name` (str): Name for this instance in the project
40
+ - `lib_repo` (str): Library repository URL or `"local"` for local library
41
+ - `mpn` (str): Manufacturer part number
42
+ - `item_type` (str): Type of item (device, connector, cable, etc.)
43
+ - `lib_subpath` (str, optional): Subpath within the library
44
+ - `lib_rev_used_here` (str, optional): Specific revision to use (e.g., `"1"` or `"rev1"`)
45
+ - `update_instances_list` (bool, optional): If `True`, updates the instances list with
46
+ library metadata. Defaults to `True`.
47
+ - `destination_directory` (str, optional): Custom destination directory. If `None`,
48
+ defaults to `instance_data/{item_type}/{instance_name}`.
49
+
50
+ **Returns:**
51
+ - `str`: Path to the destination directory where the part was imported.
52
+
53
+ **Raises:**
54
+ - `ValueError`: If required fields (`lib_repo`, `mpn`, `item_type`) are blank.
55
+ - `FileNotFoundError`: If no revision folders are found for the part number in the library.
56
+ """
57
+ # throw errors if required fields are blank
58
+ if input_dict.get("lib_repo") in [None, ""]:
59
+ raise ValueError(
60
+ f"when importing {input_dict.get('instance_name')} 'lib_repo' is required but blank"
61
+ )
62
+ if input_dict.get("mpn") in [None, ""]:
63
+ raise ValueError(
64
+ f"when importing {input_dict.get('instance_name')} 'mpn' is required but blank"
65
+ )
66
+ if input_dict.get("item_type") in [None, ""]:
67
+ raise ValueError(
68
+ f"when importing {input_dict.get('instance_name')} 'item_type' is required but blank"
69
+ )
70
+
71
+ # determine destination directory
72
+ if destination_directory is None:
73
+ destination_directory = os.path.join(
74
+ fileio.dirpath(None),
75
+ "instance_data",
76
+ input_dict.get("item_type"),
77
+ input_dict.get("instance_name"),
78
+ )
79
+ os.makedirs(destination_directory, exist_ok=True)
80
+
81
+ lib_repo = None
82
+ if input_dict.get("lib_repo") == "local":
83
+ lib_repo = os.path.join(fileio.part_directory(), "library")
84
+ else:
85
+ lib_repo = get_local_path(input_dict.get("lib_repo"))
86
+
87
+ # determine source library path
88
+ source_lib_path = os.path.join(
89
+ lib_repo,
90
+ input_dict.get("item_type"),
91
+ input_dict.get("lib_subpath", ""),
92
+ input_dict.get("mpn"),
93
+ )
94
+
95
+ # === Find highest rev in library
96
+ source_revision_folders = [
97
+ name
98
+ for name in os.listdir(source_lib_path)
99
+ if os.path.isdir(os.path.join(source_lib_path, name))
100
+ and re.fullmatch(
101
+ rf"{re.escape(input_dict.get('mpn').lower())}-rev(\d+)", name.lower()
102
+ )
103
+ ]
104
+ if not source_revision_folders:
105
+ raise FileNotFoundError(
106
+ f"No revision folders found for {input_dict.get('mpn')} in {source_lib_path}"
107
+ )
108
+ highest_source_rev = str(
109
+ max(
110
+ int(re.search(r"rev(\d+)", name).group(1))
111
+ for name in source_revision_folders
112
+ )
113
+ )
114
+ # === Decide which rev to use
115
+ if input_dict.get("lib_rev_used_here"):
116
+ rev_to_use = int(
117
+ input_dict.get("lib_rev_used_here").strip().lower().replace("rev", "")
118
+ )
119
+ if int(highest_source_rev) > int(rev_to_use):
120
+ import_state = f"newer rev exists (rev{rev_to_use} used, rev{highest_source_rev} available)"
121
+ else:
122
+ import_state = f"library up to date (rev{rev_to_use})"
123
+ else:
124
+ rev_to_use = highest_source_rev
125
+ import_state = f"imported latest (rev{rev_to_use})"
126
+
127
+ # === Import library contents freshly every time
128
+ lib_used_path = os.path.join(destination_directory, "library_used_do_not_edit")
129
+ os.makedirs(lib_used_path, exist_ok=True)
130
+
131
+ lib_used_rev_path = os.path.join(
132
+ lib_used_path, f"{input_dict.get('mpn')}-rev{rev_to_use}"
133
+ )
134
+ if os.path.exists(lib_used_rev_path):
135
+ shutil.rmtree(lib_used_rev_path)
136
+
137
+ source_lib_rev_path = os.path.join(
138
+ source_lib_path, f"{input_dict.get('mpn')}-rev{rev_to_use}"
139
+ )
140
+
141
+ shutil.copytree(source_lib_rev_path, lib_used_rev_path)
142
+
143
+ # === Copy editable files into the editable directory only if not already present
144
+ rename_suffixes = [
145
+ "-drawing.svg",
146
+ "-params.json",
147
+ "-attributes.json",
148
+ "-signals_list.tsv",
149
+ "-feature_tree.py",
150
+ "-conductor_list.tsv",
151
+ ]
152
+ Modified = False
153
+ for filename in os.listdir(lib_used_rev_path):
154
+ lib_used_do_not_edit_file = os.path.join(lib_used_rev_path, filename)
155
+
156
+ new_name = filename
157
+ for suffix in rename_suffixes:
158
+ if filename.endswith(suffix):
159
+ new_name = f"{input_dict.get('instance_name')}{suffix}"
160
+ break
161
+
162
+ editable_file_path = os.path.join(destination_directory, new_name)
163
+ if not os.path.exists(editable_file_path):
164
+ shutil.copy2(lib_used_do_not_edit_file, editable_file_path)
165
+
166
+ # special rules for copying svg
167
+ if new_name.endswith(".svg"):
168
+ with open(editable_file_path, "r", encoding="utf-8") as f:
169
+ content = f.read()
170
+ content = content.replace(
171
+ f"{input_dict.get('mpn')}-drawing-contents-start",
172
+ f"{input_dict.get('instance_name')}-contents-start",
173
+ ).replace(
174
+ f"{input_dict.get('mpn')}-drawing-contents-end",
175
+ f"{input_dict.get('instance_name')}-contents-end",
176
+ )
177
+ with open(editable_file_path, "w", encoding="utf-8") as f:
178
+ f.write(content)
179
+
180
+ else:
181
+ # Compare the existing editable file and the library version
182
+ if not filecmp.cmp(
183
+ lib_used_do_not_edit_file, editable_file_path, shallow=False
184
+ ):
185
+ Modified = Modified or True
186
+
187
+ if Modified:
188
+ import_state = f"modified in this project (rev{rev_to_use})"
189
+ else:
190
+ import_state = f"up to date (rev{rev_to_use})"
191
+
192
+ # === Load revision row from revision history TSV in source library ===
193
+ revhistory_path = os.path.join(
194
+ source_lib_path, f"{input_dict.get('mpn')}-revision_history.tsv"
195
+ )
196
+ revhistory_row = rev_history.info(rev=rev_to_use, path=revhistory_path)
197
+
198
+ try:
199
+ with open(
200
+ os.path.join(
201
+ destination_directory,
202
+ f"{input_dict.get('instance_name')}-attributes.json",
203
+ ),
204
+ "r",
205
+ encoding="utf-8",
206
+ ) as f:
207
+ attributes_data = json.load(f)
208
+
209
+ csys_children = attributes_data.get("csys_children") or {}
210
+ tools = attributes_data.get("tools") or []
211
+ build_notes = attributes_data.get("build_notes") or []
212
+
213
+ except Exception:
214
+ csys_children = {}
215
+ tools = []
216
+ build_notes = []
217
+
218
+ update_contents = {
219
+ "mpn": input_dict.get("mpn"),
220
+ "item_type": input_dict.get("item_type"),
221
+ "csys_children": csys_children,
222
+ "lib_repo": lib_repo,
223
+ "lib_subpath": input_dict.get("lib_subpath"),
224
+ "lib_desc": revhistory_row.get("desc"),
225
+ "lib_latest_rev": highest_source_rev,
226
+ "lib_rev_used_here": rev_to_use,
227
+ "lib_status": revhistory_row.get("status"),
228
+ "lib_releaseticket": revhistory_row.get("releaseticket"),
229
+ "lib_datestarted": revhistory_row.get("datestarted"),
230
+ "lib_datemodified": revhistory_row.get("datemodified"),
231
+ "lib_datereleased": revhistory_row.get("datereleased"),
232
+ "lib_drawnby": revhistory_row.get("drawnby"),
233
+ "lib_checkedby": revhistory_row.get("checkedby"),
234
+ "lib_tools": tools,
235
+ "lib_build_notes": build_notes,
236
+ "project_editable_lib_modified": Modified,
237
+ }
238
+
239
+ if update_instances_list:
240
+ try:
241
+ instances_list.modify(input_dict.get("instance_name"), update_contents)
242
+ except ValueError:
243
+ instances_list.new_instance(
244
+ input_dict.get("instance_name"), update_contents
245
+ )
246
+
247
+ library_history.append(input_dict.get("instance_name"), update_contents)
248
+
249
+ print_import_status(
250
+ input_dict.get("instance_name"),
251
+ input_dict.get("item_type"),
252
+ update_contents.get("lib_status"),
253
+ import_state,
254
+ os.path.basename(
255
+ os.path.dirname(os.path.dirname(os.path.dirname(destination_directory)))
256
+ ),
257
+ )
258
+ return destination_directory
259
+
260
+
261
+ def get_local_path(lib_repo):
262
+ """
263
+ Looks up the local filesystem path for a library repository URL.
264
+
265
+ Reads the `library_locations.csv` file to find the mapping between a library
266
+ repository URL and its local filesystem path. If the CSV file doesn't exist,
267
+ it creates one with a default entry for the `harnice-library-public` repository.
268
+
269
+ The lookup is case-insensitive. The local path is expanded (e.g., `~` is expanded
270
+ to the user's home directory).
271
+
272
+ **Args:**
273
+ - `lib_repo` (str): Library repository URL to look up (e.g.,
274
+ `"https://github.com/harnice/harnice"`).
275
+
276
+ **Returns:**
277
+ - `str`: Local filesystem path to the library repository.
278
+
279
+ **Raises:**
280
+ - `ValueError`: If the library repository URL is not found in the CSV file,
281
+ or if no local path is specified for the repository.
282
+ """
283
+ csv_path = fileio.path("library locations") # path to library_locations.csv
284
+
285
+ # ----------------------------------------------------
286
+ # If file does not exist, auto-generate with default
287
+ # ----------------------------------------------------
288
+ if not os.path.exists(csv_path):
289
+ # Determine base directory of the Harnice repo
290
+ # (__file__) → .../harnice/utils/library_utils.py
291
+ # dirname 3 times → repo root
292
+ repo_root = os.path.dirname(
293
+ os.path.dirname(
294
+ os.path.dirname(
295
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
296
+ )
297
+ )
298
+ )
299
+
300
+ default_local_path = os.path.join(repo_root, "harnice-library-public")
301
+
302
+ # Ensure the directory exists for the CSV
303
+ os.makedirs(os.path.dirname(csv_path), exist_ok=True)
304
+
305
+ with open(csv_path, "w", encoding="utf-8") as f:
306
+ f.write(f"https://github.com/harnice/harnice,{default_local_path}\n")
307
+
308
+ print(f"[harnice] Created '{csv_path}'")
309
+ print(f"[harnice] Default library-public location: {default_local_path}")
310
+
311
+ # ----------------------------------------------------
312
+ # Normal lookup (with BOM-safe decoding)
313
+ # ----------------------------------------------------
314
+ with open(csv_path, newline="", encoding="utf-8-sig") as f:
315
+ for line in f:
316
+ line = line.strip()
317
+ if not line or line.startswith("#"):
318
+ continue
319
+
320
+ parts = line.split(",")
321
+ if len(parts) < 2:
322
+ continue
323
+
324
+ url = parts[0].strip()
325
+ local = parts[1].strip()
326
+
327
+ # Case-insensitive match
328
+ if url.lower() == lib_repo.lower().strip():
329
+ if not local:
330
+ raise ValueError(f"No local path found for '{lib_repo}'")
331
+ return os.path.expanduser(local)
332
+
333
+ raise ValueError(f"'{lib_repo}' not found in library locations")