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
harnice/__init__.py ADDED
File without changes
harnice/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from harnice.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
harnice/cli.py ADDED
@@ -0,0 +1,234 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import shutil
5
+ from harnice.lists import rev_history
6
+ from harnice import state
7
+ from harnice import fileio
8
+
9
+
10
+ def print_import_status(
11
+ instance_name, item_type, library_status, import_state, called_from_base_directory
12
+ ):
13
+ print(
14
+ f"{'':<4}"
15
+ f"{instance_name:<40}"
16
+ f"{item_type:<16}"
17
+ f"{library_status:<16}"
18
+ f"{import_state:<32}"
19
+ f"{called_from_base_directory:<32}"
20
+ )
21
+
22
+
23
+ def print_import_status_headers():
24
+ print_import_status(
25
+ "INSTANCE NAME",
26
+ "ITEM TYPE",
27
+ "LIBRARY STATUS",
28
+ "IMPORT STATE",
29
+ "CALLED FROM BASE DIRECTORY",
30
+ )
31
+
32
+
33
+ def main():
34
+ # Ensure cwd exists
35
+ try:
36
+ cwd = os.getcwd()
37
+ except (FileNotFoundError, PermissionError):
38
+ sys.exit(
39
+ "Error: The current working directory is invalid "
40
+ "(it may have been deleted or you lack permission to access it)."
41
+ )
42
+
43
+ if not os.path.exists(cwd):
44
+ sys.exit(f"Error: The current working directory no longer exists: {cwd}")
45
+
46
+ # -----------------------------
47
+ # Argument parsing
48
+ # -----------------------------
49
+ parser = argparse.ArgumentParser(
50
+ prog="harnice",
51
+ description="Electrical system CAD",
52
+ )
53
+
54
+ group = parser.add_mutually_exclusive_group(required=True)
55
+
56
+ group.add_argument(
57
+ "-r", "--render", action="store_true", help="Render the product normally"
58
+ )
59
+
60
+ group.add_argument(
61
+ "-l",
62
+ "--lightweight",
63
+ action="store_true",
64
+ help="Render the product quickly without performing all checks",
65
+ )
66
+
67
+ group.add_argument(
68
+ "--newrev",
69
+ action="store_true",
70
+ help="Create a new revision in the current working directory",
71
+ )
72
+
73
+ group.add_argument(
74
+ "--gui",
75
+ action="store_true",
76
+ help="Launch the Harnice GUI launcher",
77
+ )
78
+
79
+ args = parser.parse_args()
80
+
81
+ if args.gui:
82
+ from harnice.gui.launcher import main as gui_main
83
+
84
+ gui_main()
85
+ return
86
+
87
+ # -----------------------------
88
+ # Handle new revision creation and exit
89
+ # -----------------------------
90
+ if args.newrev:
91
+ newrev()
92
+ return
93
+
94
+ # -----------------------------
95
+ # Ensure we are inside a revision folder
96
+ # May change cwd if new PN created
97
+ # -----------------------------
98
+ fileio.verify_revision_structure()
99
+ item_type = rev_history.info(field="product")
100
+
101
+ # -----------------------------
102
+ # Load product module
103
+ # -----------------------------
104
+ try:
105
+ product_module = __import__(
106
+ f"harnice.products.{item_type}", fromlist=[item_type]
107
+ )
108
+ except ModuleNotFoundError:
109
+ sys.exit(f"Unknown product: '{item_type}'")
110
+
111
+ # -----------------------------
112
+ # Set the default fileio structure dict to the product's file_structure()
113
+ # -----------------------------
114
+ if hasattr(product_module, "file_structure"):
115
+ structure = product_module.file_structure()
116
+ state.set_file_structure(structure)
117
+ else:
118
+ sys.exit(f"Product '{item_type}' must define file_structure()")
119
+
120
+ # -----------------------------
121
+ # Generate product file structure
122
+ # -----------------------------
123
+ if hasattr(product_module, "generate_structure"):
124
+ product_module.generate_structure()
125
+ else:
126
+ sys.exit(f"Product '{item_type}' must define generate_structure()")
127
+
128
+ # -----------------------------
129
+ # Execute render logic
130
+ # -----------------------------
131
+ if args.lightweight:
132
+ try:
133
+ product_module.render(lightweight=True)
134
+ except TypeError:
135
+ sys.exit(f"Product '{item_type}' does not support lightweight rendering")
136
+ else:
137
+ product_module.render()
138
+
139
+ return
140
+
141
+
142
+ def prompt(text, default=None):
143
+ p = f"{text}"
144
+ if default:
145
+ p += f" [{default}]"
146
+ p += ": "
147
+ return input(p).strip() or default
148
+
149
+
150
+ def newrev():
151
+ from harnice import fileio
152
+
153
+ """
154
+ Create a new revision directory by copying the current revision's contents
155
+ and updating filenames to reflect the new revision number.
156
+ """
157
+ # Ensure revision structure is valid and get context
158
+ fileio.verify_revision_structure()
159
+
160
+ # Prompt user for new revision number
161
+ new_rev_number = prompt(
162
+ f"Current rev number: {state.partnumber('R')}. Enter new rev number:",
163
+ default=str(int(state.partnumber("R")) + 1),
164
+ )
165
+
166
+ # Construct new revision directory path
167
+ new_rev_dir = os.path.join(
168
+ fileio.part_directory(), f"{state.partnumber('pn')}-rev{new_rev_number}"
169
+ )
170
+
171
+ # Ensure target directory does not already exist
172
+ if os.path.exists(new_rev_dir):
173
+ raise FileExistsError(f"Revision directory already exists: {new_rev_dir}")
174
+
175
+ shutil.copytree(fileio.rev_directory(), new_rev_dir)
176
+
177
+ # Walk the new directory and rename all files containing the old rev number
178
+ for root, _, files in os.walk(new_rev_dir):
179
+ for filename in files:
180
+ new_suffix = f"rev{new_rev_number}"
181
+
182
+ if state.partnumber("rev") in filename:
183
+ old_path = os.path.join(root, filename)
184
+ new_name = filename.replace(state.partnumber("rev"), new_suffix)
185
+ new_path = os.path.join(root, new_name)
186
+
187
+ os.rename(old_path, new_path)
188
+
189
+ print(
190
+ f"Successfully created new revision: {state.partnumber('pn')}-rev{new_rev_number}. Please cd into it."
191
+ )
192
+
193
+
194
+ def select_product_type():
195
+ from pathlib import Path
196
+ import harnice.products as products_pkg
197
+ from prompt_toolkit import prompt
198
+ from prompt_toolkit.completion import WordCompleter
199
+
200
+ def get_product_types():
201
+ products_dir = Path(products_pkg.__file__).parent
202
+ return sorted(
203
+ p.stem
204
+ for p in products_dir.glob("*.py")
205
+ if p.name != "__init__.py"
206
+ )
207
+
208
+ product_types = get_product_types()
209
+ product_map = {p.lower(): p for p in product_types}
210
+
211
+ completer = WordCompleter(
212
+ product_types,
213
+ ignore_case=True,
214
+ sentence=True,
215
+ )
216
+
217
+ while True:
218
+ value = prompt(
219
+ "What product type are you working on? ",
220
+ completer=completer,
221
+ default="harness",
222
+ ).strip()
223
+
224
+ if not value:
225
+ value = "harness"
226
+
227
+ key = value.lower()
228
+ if key in product_map:
229
+ return product_map[key]
230
+
231
+ print(
232
+ f"Unrecognized product type '{value}'. "
233
+ f"Valid options: {', '.join(product_types)}"
234
+ )
harnice/fileio.py ADDED
@@ -0,0 +1,295 @@
1
+ import os
2
+ import os.path
3
+ import datetime
4
+ import shutil
5
+ import re
6
+ import csv
7
+ import json
8
+ import subprocess
9
+ from harnice import state
10
+
11
+ # standard punctuation:
12
+ # . separates between name hierarchy levels
13
+ # _ means nothing, basically a space character
14
+ # - if multiple instances are found at the same hierarchy level with the same name,
15
+ # this separates name from unique instance identifier
16
+
17
+
18
+ def part_directory():
19
+ return os.path.dirname(os.getcwd())
20
+
21
+
22
+ def rev_directory():
23
+ return os.getcwd()
24
+
25
+
26
+ def silentremove(filepath):
27
+ """
28
+ Removes a file or directory and its contents.
29
+
30
+ Args:
31
+ filepath (str): The path to the file or directory to remove.
32
+ """
33
+ if os.path.exists(filepath):
34
+ if os.path.isfile(filepath) or os.path.islink(filepath):
35
+ os.remove(filepath) # remove file or symlink
36
+ elif os.path.isdir(filepath):
37
+ shutil.rmtree(filepath) # remove directory and contents
38
+
39
+
40
+ def path(target_value, structure_dict=None, base_directory=None):
41
+
42
+ # returns the filepath/filename of a filekey.
43
+ """
44
+ Recursively searches for a value in a nested JSON structure and returns the path to the element containing that value.
45
+
46
+ Args:
47
+ target_value (str): The value to search for.
48
+
49
+ Returns:
50
+ list: A list of container names leading to the element containing the target value, or None if not found.
51
+ """
52
+
53
+ # FILES NOT DEPENDENT ON PRODUCT TYPE
54
+ if target_value == "revision history":
55
+ file_path = os.path.join(
56
+ part_directory(), f"{state.partnumber('pn')}-revision_history.tsv"
57
+ )
58
+ return file_path
59
+
60
+ # FILES DEPENDENT ON HARNICE ROOT
61
+
62
+ if target_value == "library locations":
63
+ import harnice
64
+
65
+ harnice_root = os.path.dirname(
66
+ os.path.dirname(os.path.dirname(harnice.__file__))
67
+ )
68
+ return os.path.join(harnice_root, "library_locations.csv")
69
+
70
+ if target_value == "project locations":
71
+ import harnice
72
+
73
+ harnice_root = os.path.dirname(
74
+ os.path.dirname(os.path.dirname(harnice.__file__))
75
+ )
76
+ return os.path.join(harnice_root, "project_locations.csv")
77
+
78
+ if target_value == "drawnby":
79
+ import harnice
80
+
81
+ harnice_root = os.path.dirname(
82
+ os.path.dirname(os.path.dirname(harnice.__file__))
83
+ )
84
+ return os.path.join(harnice_root, "drawnby.json")
85
+
86
+ # FILES INSIDE OF A STRUCURE DEFINED BY FILEIO
87
+ # look up from default structure state if not provided
88
+ if structure_dict is None:
89
+ structure_dict = state.file_structure
90
+
91
+ def recursive_search(data, path):
92
+ if isinstance(data, dict):
93
+ for key, value in data.items():
94
+ if value == target_value:
95
+ return path + [key]
96
+ result = recursive_search(value, path + [key])
97
+ if result:
98
+ return result
99
+ elif isinstance(data, list):
100
+ for index, item in enumerate(data):
101
+ if item == target_value:
102
+ return path + [f"[{index}]"]
103
+ result = recursive_search(item, path + [f"[{index}]"])
104
+ if result:
105
+ return result
106
+ return None
107
+
108
+ path_value = recursive_search(structure_dict, [])
109
+
110
+ if not path_value:
111
+ raise TypeError(f"Could not find filepath of '{target_value}'.")
112
+ if base_directory in [None, ""]:
113
+ return os.path.join(rev_directory(), *path_value)
114
+ else:
115
+ return os.path.join(rev_directory(), base_directory, *path_value)
116
+
117
+
118
+ def dirpath(target_key, structure_dict=None, base_directory=None):
119
+ """
120
+ Returns the absolute path to a directory identified by its key
121
+ within a dict hierarchy.
122
+ """
123
+ if target_key is None:
124
+ if base_directory in [None, ""]:
125
+ return os.path.join(rev_directory())
126
+ else:
127
+ return os.path.join(rev_directory(), base_directory)
128
+
129
+ if structure_dict is None:
130
+ structure_dict = state.file_structure
131
+
132
+ def recursive_search(data, path):
133
+ if isinstance(data, dict):
134
+ for key, value in data.items():
135
+ # if the current key matches, return its path immediately
136
+ if key == target_key:
137
+ return path + [key]
138
+ # otherwise, keep descending
139
+ result = recursive_search(value, path + [key])
140
+ if result:
141
+ return result
142
+ elif isinstance(data, list):
143
+ for index, item in enumerate(data):
144
+ result = recursive_search(item, path + [f"[{index}]"])
145
+ if result:
146
+ return result
147
+ return None
148
+
149
+ path_key = recursive_search(structure_dict, [])
150
+ if not path_key:
151
+ raise TypeError(f"Could not find directory '{target_key}'.")
152
+ if base_directory in [None, ""]:
153
+ return os.path.join(rev_directory(), *path_key)
154
+ else:
155
+ return os.path.join(rev_directory(), base_directory, *path_key)
156
+
157
+
158
+ def verify_revision_structure():
159
+ from harnice import cli
160
+ from harnice.lists import rev_history
161
+
162
+ cwd = os.getcwd()
163
+ cwd_name = os.path.basename(cwd)
164
+ parent = os.path.basename(os.path.dirname(cwd))
165
+
166
+ # --- 1) Already in a <PN>-revN folder? ---
167
+ if cwd_name.startswith(f"{parent}-rev") and cwd_name.split("-rev")[-1].isdigit():
168
+ state.set_pn(parent)
169
+ state.set_rev(int(cwd_name.split("-rev")[-1]))
170
+
171
+ # --- 2) In a part folder that contains revision folders? ---
172
+ elif any(
173
+ re.fullmatch(rf"{re.escape(cwd_name)}-rev\d+", d) for d in os.listdir(cwd)
174
+ ):
175
+ print(f"This is a part folder ({cwd_name}).")
176
+ print(f"Please `cd` into a revision folder (e.g. `{cwd_name}-rev1`) and rerun.")
177
+ exit()
178
+
179
+ # --- 3) No revision structure → initialize new PN here ---
180
+ else:
181
+ answer = cli.prompt(
182
+ f"No valid Harnice file structure detected in '{cwd_name}'. Create new PN here?",
183
+ default="y",
184
+ )
185
+ if answer.lower() not in ("y", "yes", ""):
186
+ exit()
187
+
188
+ state.set_pn(cwd_name)
189
+
190
+ # inline prompt_new_rev
191
+ rev = int(cli.prompt("Enter revision number", default="1"))
192
+ state.set_rev(rev)
193
+ folder = os.path.join(cwd, f"{state.pn}-rev{state.rev}")
194
+ os.makedirs(folder, exist_ok=True)
195
+ os.chdir(folder)
196
+
197
+ # --- Ensure revision_history entry exists ---
198
+ try:
199
+ rev_history.info()
200
+ except ValueError:
201
+ rev_history.append(next_rev=state.rev)
202
+ except FileNotFoundError:
203
+ rev_history.append(next_rev=state.rev)
204
+
205
+ # --- Status must be blank to proceed ---
206
+ if rev_history.info(field="status") != "":
207
+ raise RuntimeError(
208
+ f"Revision {state.rev} status is not clear. "
209
+ f"Harnice only renders revisions with a blank status."
210
+ )
211
+
212
+ print(f"Working on PN: {state.pn}, Rev: {state.rev}")
213
+ rev_history.update_datemodified()
214
+
215
+
216
+ def today():
217
+ return datetime.date.today().strftime("%-m/%-d/%y")
218
+
219
+
220
+ def get_git_hash_of_harnice_src():
221
+ try:
222
+ # get path to harnice package directory
223
+ import harnice
224
+
225
+ repo_dir = os.path.dirname(os.path.dirname(harnice.__file__))
226
+ # ask git for commit hash
227
+ return (
228
+ subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo_dir)
229
+ .decode("utf-8")
230
+ .strip()
231
+ )
232
+ except Exception:
233
+ return "UNKNOWN"
234
+
235
+
236
+ def get_path_to_project(traceable_key):
237
+ """
238
+ Given a traceable identifier for a project (PN, URL, etc),
239
+ return the expanded local filesystem path.
240
+
241
+ Expects a CSV at the root of the repo named:
242
+ project_locations.csv
243
+
244
+ Format (no headers):
245
+ traceable_key,local_path
246
+ """
247
+ from harnice import fileio
248
+
249
+ path = fileio.path("project locations") # resolves to project_locations.csv
250
+
251
+ if not os.path.exists(path):
252
+ raise FileNotFoundError(
253
+ "Make a CSV at the root of your Harnice repo called project_locations.csv "
254
+ "with the following format (no headers):\n\n"
255
+ " traceable_key,local_path\n"
256
+ )
257
+
258
+ traceable_key = traceable_key.strip()
259
+
260
+ with open(path, newline="", encoding="utf-8") as f:
261
+ reader = csv.reader(f, delimiter=",")
262
+ for row in reader:
263
+ # skip blank or comment lines
264
+ if not row or len(row) < 2 or row[0].strip().startswith("#"):
265
+ continue
266
+
267
+ key, local = row[0].strip(), row[1].strip()
268
+
269
+ if key == traceable_key:
270
+ if not local:
271
+ raise ValueError(
272
+ f"No project local path found for '{traceable_key}'"
273
+ )
274
+ return os.path.expanduser(local)
275
+
276
+ raise ValueError(f"Could not find project traceable key '{traceable_key}'")
277
+
278
+
279
+ def read_tsv(filepath, delimiter="\t"):
280
+ try:
281
+ with open(filepath, newline="", encoding="utf-8") as f:
282
+ return list(csv.DictReader(f, delimiter=delimiter))
283
+ except FileNotFoundError:
284
+ filepath = path(filepath)
285
+ try:
286
+ with open(filepath, newline="", encoding="utf-8") as f:
287
+ return list(csv.DictReader(f, delimiter=delimiter))
288
+ except FileNotFoundError:
289
+ raise FileNotFoundError(
290
+ f"Expected csv or tsv file with delimiter '{delimiter}' at path or key {filepath}"
291
+ )
292
+
293
+
294
+ def drawnby():
295
+ return json.load(open(path("drawnby")))