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.
- harnice/__init__.py +0 -0
- harnice/__main__.py +4 -0
- harnice/cli.py +234 -0
- harnice/fileio.py +295 -0
- harnice/gui/launcher.py +426 -0
- harnice/lists/channel_map.py +182 -0
- harnice/lists/circuits_list.py +302 -0
- harnice/lists/disconnect_map.py +237 -0
- harnice/lists/formboard_graph.py +63 -0
- harnice/lists/instances_list.py +280 -0
- harnice/lists/library_history.py +40 -0
- harnice/lists/manifest.py +93 -0
- harnice/lists/post_harness_instances_list.py +66 -0
- harnice/lists/rev_history.py +325 -0
- harnice/lists/signals_list.py +135 -0
- harnice/products/__init__.py +1 -0
- harnice/products/cable.py +152 -0
- harnice/products/chtype.py +80 -0
- harnice/products/device.py +844 -0
- harnice/products/disconnect.py +225 -0
- harnice/products/flagnote.py +139 -0
- harnice/products/harness.py +522 -0
- harnice/products/macro.py +10 -0
- harnice/products/part.py +640 -0
- harnice/products/system.py +125 -0
- harnice/products/tblock.py +270 -0
- harnice/state.py +57 -0
- harnice/utils/appearance.py +51 -0
- harnice/utils/circuit_utils.py +326 -0
- harnice/utils/feature_tree_utils.py +183 -0
- harnice/utils/formboard_utils.py +973 -0
- harnice/utils/library_utils.py +333 -0
- harnice/utils/note_utils.py +417 -0
- harnice/utils/svg_utils.py +819 -0
- harnice/utils/system_utils.py +563 -0
- harnice-0.3.0.dist-info/METADATA +32 -0
- harnice-0.3.0.dist-info/RECORD +41 -0
- harnice-0.3.0.dist-info/WHEEL +5 -0
- harnice-0.3.0.dist-info/entry_points.txt +3 -0
- harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
- 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")
|