micropython-stubber 1.23.1.post1__py3-none-any.whl → 1.23.2__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 (152) hide show
  1. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/LICENSE +30 -30
  2. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/METADATA +4 -4
  3. micropython_stubber-1.23.2.dist-info/RECORD +158 -0
  4. mpflash/README.md +220 -220
  5. mpflash/libusb_flash.ipynb +203 -203
  6. mpflash/mpflash/add_firmware.py +98 -98
  7. mpflash/mpflash/ask_input.py +236 -236
  8. mpflash/mpflash/basicgit.py +284 -284
  9. mpflash/mpflash/bootloader/__init__.py +2 -2
  10. mpflash/mpflash/bootloader/activate.py +60 -60
  11. mpflash/mpflash/bootloader/detect.py +82 -82
  12. mpflash/mpflash/bootloader/manual.py +101 -101
  13. mpflash/mpflash/bootloader/micropython.py +12 -12
  14. mpflash/mpflash/bootloader/touch1200.py +36 -36
  15. mpflash/mpflash/cli_download.py +129 -129
  16. mpflash/mpflash/cli_flash.py +224 -216
  17. mpflash/mpflash/cli_group.py +111 -111
  18. mpflash/mpflash/cli_list.py +87 -87
  19. mpflash/mpflash/cli_main.py +39 -39
  20. mpflash/mpflash/common.py +210 -166
  21. mpflash/mpflash/config.py +44 -44
  22. mpflash/mpflash/connected.py +96 -77
  23. mpflash/mpflash/download.py +364 -364
  24. mpflash/mpflash/downloaded.py +130 -130
  25. mpflash/mpflash/errors.py +9 -9
  26. mpflash/mpflash/flash/__init__.py +55 -55
  27. mpflash/mpflash/flash/esp.py +59 -59
  28. mpflash/mpflash/flash/stm32.py +19 -19
  29. mpflash/mpflash/flash/stm32_dfu.py +104 -104
  30. mpflash/mpflash/flash/uf2/__init__.py +88 -88
  31. mpflash/mpflash/flash/uf2/boardid.py +15 -15
  32. mpflash/mpflash/flash/uf2/linux.py +136 -130
  33. mpflash/mpflash/flash/uf2/macos.py +42 -42
  34. mpflash/mpflash/flash/uf2/uf2disk.py +12 -12
  35. mpflash/mpflash/flash/uf2/windows.py +43 -43
  36. mpflash/mpflash/flash/worklist.py +170 -170
  37. mpflash/mpflash/list.py +106 -106
  38. mpflash/mpflash/logger.py +41 -41
  39. mpflash/mpflash/mpboard_id/__init__.py +93 -93
  40. mpflash/mpflash/mpboard_id/add_boards.py +251 -251
  41. mpflash/mpflash/mpboard_id/board.py +37 -37
  42. mpflash/mpflash/mpboard_id/board_id.py +86 -86
  43. mpflash/mpflash/mpboard_id/store.py +43 -43
  44. mpflash/mpflash/mpremoteboard/__init__.py +266 -266
  45. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  46. mpflash/mpflash/mpremoteboard/runner.py +140 -140
  47. mpflash/mpflash/vendor/click_aliases.py +91 -91
  48. mpflash/mpflash/vendor/dfu.py +165 -165
  49. mpflash/mpflash/vendor/pydfu.py +605 -605
  50. mpflash/mpflash/vendor/readme.md +2 -2
  51. mpflash/mpflash/versions.py +135 -135
  52. mpflash/poetry.lock +1599 -1599
  53. mpflash/pyproject.toml +65 -65
  54. mpflash/stm32_udev_rules.md +62 -62
  55. stubber/__init__.py +3 -3
  56. stubber/board/board_info.csv +193 -193
  57. stubber/board/boot.py +34 -34
  58. stubber/board/createstubs.py +1004 -986
  59. stubber/board/createstubs_db.py +826 -825
  60. stubber/board/createstubs_db_min.py +332 -331
  61. stubber/board/createstubs_db_mpy.mpy +0 -0
  62. stubber/board/createstubs_lvgl.py +741 -741
  63. stubber/board/createstubs_lvgl_min.py +741 -741
  64. stubber/board/createstubs_mem.py +767 -766
  65. stubber/board/createstubs_mem_min.py +307 -306
  66. stubber/board/createstubs_mem_mpy.mpy +0 -0
  67. stubber/board/createstubs_min.py +295 -294
  68. stubber/board/createstubs_mpy.mpy +0 -0
  69. stubber/board/fw_info.py +141 -141
  70. stubber/board/info.py +183 -183
  71. stubber/board/main.py +19 -19
  72. stubber/board/modulelist.txt +247 -247
  73. stubber/board/pyrightconfig.json +34 -34
  74. stubber/bulk/mcu_stubber.py +437 -437
  75. stubber/codemod/_partials/__init__.py +48 -48
  76. stubber/codemod/_partials/db_main.py +147 -147
  77. stubber/codemod/_partials/lvgl_main.py +77 -77
  78. stubber/codemod/_partials/modules_reader.py +80 -80
  79. stubber/codemod/add_comment.py +53 -53
  80. stubber/codemod/add_method.py +65 -65
  81. stubber/codemod/board.py +317 -317
  82. stubber/codemod/enrich.py +151 -145
  83. stubber/codemod/merge_docstub.py +284 -284
  84. stubber/codemod/modify_list.py +54 -54
  85. stubber/codemod/utils.py +56 -56
  86. stubber/commands/build_cmd.py +94 -94
  87. stubber/commands/cli.py +49 -49
  88. stubber/commands/clone_cmd.py +78 -78
  89. stubber/commands/config_cmd.py +29 -29
  90. stubber/commands/enrich_folder_cmd.py +71 -71
  91. stubber/commands/get_core_cmd.py +71 -71
  92. stubber/commands/get_docstubs_cmd.py +92 -92
  93. stubber/commands/get_frozen_cmd.py +117 -117
  94. stubber/commands/get_mcu_cmd.py +102 -102
  95. stubber/commands/merge_cmd.py +66 -66
  96. stubber/commands/publish_cmd.py +118 -118
  97. stubber/commands/stub_cmd.py +31 -31
  98. stubber/commands/switch_cmd.py +62 -62
  99. stubber/commands/variants_cmd.py +48 -48
  100. stubber/cst_transformer.py +178 -178
  101. stubber/data/board_info.csv +193 -193
  102. stubber/data/board_info.json +1729 -1729
  103. stubber/data/micropython_tags.csv +15 -15
  104. stubber/data/requirements-core-micropython.txt +38 -38
  105. stubber/data/requirements-core-pycopy.txt +39 -39
  106. stubber/downloader.py +37 -37
  107. stubber/freeze/common.py +72 -72
  108. stubber/freeze/freeze_folder.py +69 -69
  109. stubber/freeze/freeze_manifest_2.py +126 -126
  110. stubber/freeze/get_frozen.py +131 -131
  111. stubber/get_cpython.py +112 -112
  112. stubber/get_lobo.py +59 -59
  113. stubber/minify.py +423 -423
  114. stubber/publish/bump.py +86 -86
  115. stubber/publish/candidates.py +275 -275
  116. stubber/publish/database.py +18 -18
  117. stubber/publish/defaults.py +40 -40
  118. stubber/publish/enums.py +24 -24
  119. stubber/publish/helpers.py +29 -29
  120. stubber/publish/merge_docstubs.py +136 -132
  121. stubber/publish/missing_class_methods.py +51 -51
  122. stubber/publish/package.py +150 -150
  123. stubber/publish/pathnames.py +51 -51
  124. stubber/publish/publish.py +120 -120
  125. stubber/publish/pypi.py +42 -42
  126. stubber/publish/stubpackage.py +1055 -1051
  127. stubber/rst/__init__.py +9 -9
  128. stubber/rst/classsort.py +78 -78
  129. stubber/rst/lookup.py +533 -531
  130. stubber/rst/output_dict.py +401 -401
  131. stubber/rst/reader.py +814 -814
  132. stubber/rst/report_return.py +77 -77
  133. stubber/rst/rst_utils.py +541 -541
  134. stubber/stubber.py +38 -38
  135. stubber/stubs_from_docs.py +90 -90
  136. stubber/tools/manifestfile.py +654 -654
  137. stubber/tools/readme.md +6 -6
  138. stubber/update_fallback.py +117 -117
  139. stubber/update_module_list.py +123 -123
  140. stubber/utils/__init__.py +6 -6
  141. stubber/utils/config.py +137 -137
  142. stubber/utils/makeversionhdr.py +54 -54
  143. stubber/utils/manifest.py +90 -90
  144. stubber/utils/post.py +80 -80
  145. stubber/utils/repos.py +156 -156
  146. stubber/utils/stubmaker.py +139 -139
  147. stubber/utils/typed_config_toml.py +80 -80
  148. stubber/variants.py +106 -106
  149. micropython_stubber-1.23.1.post1.dist-info/RECORD +0 -159
  150. mpflash/basicgit.py +0 -288
  151. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/WHEEL +0 -0
  152. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/entry_points.txt +0 -0
@@ -1,364 +1,364 @@
1
- """
2
- Module to download MicroPython firmware for specific boards and versions.
3
- Uses the micropython.org website to get the available versions and locations to download firmware files.
4
- """
5
-
6
- import functools
7
- import itertools
8
- import re
9
- from pathlib import Path
10
- from typing import Dict, List, Optional
11
- from urllib.parse import urljoin
12
-
13
- # #########################################################################################################
14
- # make sure that jsonlines does not mistake the MicroPython ujson for the CPython ujson
15
- import jsonlines
16
- import requests
17
- from bs4 import BeautifulSoup
18
- from loguru import logger as log
19
- from rich.progress import track
20
-
21
- from mpflash.common import PORT_FWTYPES, FWInfo
22
- from mpflash.errors import MPFlashError
23
- from mpflash.mpboard_id import get_known_ports
24
- from mpflash.versions import clean_version
25
-
26
- # avoid conflict with the ujson used by MicroPython
27
- jsonlines.ujson = None # type: ignore
28
- # #########################################################################################################
29
-
30
-
31
- MICROPYTHON_ORG_URL = "https://micropython.org/"
32
-
33
- # Regexes to remove dates and hashes in the filename that just get in the way
34
- RE_DATE = r"(-\d{8}-)"
35
- RE_HASH = r"(.g[0-9a-f]+\.)"
36
- # regex to extract the version and the build from the firmware filename
37
- # group 1 is the version, group 2 is the build
38
- RE_VERSION_PREVIEW = r"v([\d\.]+)-?(?:preview\.)?(\d+)?\."
39
- # 'RPI_PICO_W-v1.23.uf2'
40
- # 'RPI_PICO_W-v1.23.0.uf2'
41
- # 'RPI_PICO_W-v1.23.0-406.uf2'
42
- # 'RPI_PICO_W-v1.23.0-preview.406.uf2'
43
- # 'RPI_PICO_W-v1.23.0-preview.4.uf2'
44
- # 'RPI_PICO_W-v1.23.0.uf2'
45
- # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.gc1a6b95bf.uf2'
46
- # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.uf2'
47
- # 'RPI_PICO_W-v1.24.0-preview.10.gc1a6b95bf.uf2'
48
-
49
-
50
- # use functools.lru_cache to avoid needing to download pages multiple times
51
- @functools.lru_cache(maxsize=500)
52
- def get_page(page_url: str) -> str:
53
- """Get the HTML of a page and return it as a string."""
54
- response = requests.get(page_url)
55
- return response.content.decode()
56
-
57
-
58
- @functools.lru_cache(maxsize=500)
59
- def get_board_urls(page_url: str) -> List[Dict[str, str]]:
60
- """
61
- Get the urls to all the board pages listed on this page.
62
- Assumes that all links to firmware have "class": "board-card"
63
-
64
- Args:
65
- page_url (str): The url of the page to get the board urls from.
66
-
67
- Returns:
68
- List[Dict[str, str]]: A list of dictionaries containing the board name and url.
69
-
70
- """
71
- downloads_html = get_page(page_url)
72
- soup = BeautifulSoup(downloads_html, "html.parser")
73
- tags = soup.findAll("a", recursive=True, attrs={"class": "board-card"})
74
- # assumes that all links are relative to the page url
75
- boards = [tag.get("href") for tag in tags]
76
- if "?" in page_url:
77
- page_url = page_url.split("?")[0]
78
- return [{"board": board, "url": page_url + board} for board in boards]
79
-
80
-
81
- def board_firmware_urls(board_url: str, base_url: str, ext: str) -> List[str]:
82
- """
83
- Get the urls to all the firmware files for a board.
84
- Args:
85
- page_url (str): The url of the page to get the board urls from.
86
- ??? base_url (str): The base url to join the relative urls to.
87
- ext (str): The extension of the firmware files to get. (with or withouth leading .)
88
-
89
- the urls are relative urls to the site root
90
-
91
- """
92
- html = get_page(board_url)
93
- soup = BeautifulSoup(html, "html.parser")
94
- # get all the a tags:
95
- # 1. that have a url that starts with `/resources/firmware/`
96
- # 2. end with a matching extension for this port.
97
- tags = soup.findAll(
98
- "a",
99
- recursive=True,
100
- attrs={"href": re.compile(r"^/resources/firmware/.*\." + ext.lstrip(".") + "$")},
101
- )
102
- if "?" in base_url:
103
- base_url = base_url.split("?")[0]
104
- links: List = [urljoin(base_url, tag.get("href")) for tag in tags]
105
- return links
106
-
107
-
108
- # boards we are interested in ( this avoids getting a lot of boards we don't care about)
109
- # The first run takes ~60 seconds to run for 4 ports , all boards
110
- # so it makes sense to cache the results and skip boards as soon as possible
111
- def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FWInfo]:
112
- # sourcery skip: use-getitem-for-re-match-groups
113
- """
114
- Retrieves a list of firmware information for the specified ports and boards.
115
-
116
- Args:
117
- ports (List[str]): The list of ports to check for firmware.
118
- boards (List[str]): The list of boards to retrieve firmware information for.
119
- clean (bool): A flag indicating whether to perform a clean retrieval.
120
-
121
- Returns:
122
- List[FWInfo]: A list of firmware information for the specified ports and boards.
123
-
124
- """
125
- board_urls: List[FWInfo] = []
126
- if ports is None:
127
- ports = get_known_ports()
128
- for port in ports:
129
- download_page_url = f"{MICROPYTHON_ORG_URL}download/?port={port}"
130
- urls = get_board_urls(download_page_url)
131
- # filter out boards we don't care about
132
- urls = [board for board in urls if board["board"] in boards]
133
- # add the port to the board urls
134
- for board in urls:
135
- board["port"] = port
136
-
137
- for board in track(
138
- urls,
139
- description=f"Checking {port} download pages",
140
- transient=True,
141
- refresh_per_second=1,
142
- show_speed=False,
143
- ):
144
- # add a board to the list for each firmware found
145
- firmware_urls: List[str] = []
146
- for ext in PORT_FWTYPES[port]:
147
- firmware_urls += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
148
- for _url in firmware_urls:
149
- board["firmware"] = _url
150
- fname = Path(board["firmware"]).name
151
- if clean:
152
- # remove date from firmware name
153
- fname = re.sub(RE_DATE, "-", fname)
154
- # remove hash from firmware name
155
- fname = re.sub(RE_HASH, ".", fname)
156
- fw_info = FWInfo(
157
- filename=fname,
158
- port=port,
159
- board=board["board"],
160
- preview="preview" in _url,
161
- firmware=_url,
162
- version="",
163
- )
164
- # board["firmware"] = _url
165
- # board["preview"] = "preview" in _url # type: ignore
166
- if ver_match := re.search(RE_VERSION_PREVIEW, _url):
167
- fw_info.version = clean_version(ver_match.group(1))
168
- fw_info.build = ver_match.group(2) or "0"
169
- fw_info.preview = fw_info.build != "0"
170
- # # else:
171
- # # board.$1= ""
172
- # if "preview." in fw_info.version:
173
- # fw_info.build = fw_info.version.split("preview.")[-1]
174
- # else:
175
- # fw_info.build = "0"
176
-
177
- fw_info.ext = Path(fw_info.firmware).suffix
178
- fw_info.variant = fw_info.filename.split("-v")[0] if "-v" in fw_info.filename else ""
179
-
180
- board_urls.append(fw_info)
181
- return board_urls
182
-
183
-
184
- def key_fw_ver_pre_ext_bld(x: FWInfo):
185
- "sorting key for the retrieved board urls"
186
- return x.variant, x.version, x.preview, x.ext, x.build
187
-
188
-
189
- def key_fw_var_pre_ext(x: FWInfo):
190
- "Grouping key for the retrieved board urls"
191
- return x.variant, x.preview, x.ext
192
-
193
-
194
- def download_firmwares(
195
- firmware_folder: Path,
196
- ports: List[str],
197
- boards: List[str],
198
- versions: Optional[List[str]] = None,
199
- *,
200
- force: bool = False,
201
- clean: bool = True,
202
- ) -> int:
203
- """
204
- Downloads firmware files based on the specified firmware folder, ports, boards, versions, force flag, and clean flag.
205
-
206
- Args:
207
- firmware_folder : The folder to save the downloaded firmware files.
208
- ports : The list of ports to check for firmware.
209
- boards : The list of boards to download firmware for.
210
- versions : The list of versions to download firmware for.
211
- force : A flag indicating whether to force the download even if the firmware file already exists.
212
- clean : A flag indicating to clean the date from the firmware filename.
213
- """
214
- skipped = downloaded = 0
215
- versions = [] if versions is None else [clean_version(v) for v in versions]
216
- # handle renamed boards
217
- boards = add_renamed_boards(boards)
218
-
219
- unique_boards = get_firmware_list(ports, boards, versions, clean)
220
-
221
- for b in unique_boards:
222
- log.debug(b.filename)
223
- # relevant
224
-
225
- log.info(f"Found {len(unique_boards)} relevant unique firmwares")
226
- if not unique_boards:
227
- log.error("No relevant firmwares could be found on https://micropython.org/download")
228
- log.info(f"{versions=} {ports=} {boards=}")
229
- log.info("Please check the website for the latest firmware files or try the preview version.")
230
- return 0
231
-
232
- firmware_folder.mkdir(exist_ok=True)
233
-
234
- with jsonlines.open(firmware_folder / "firmware.jsonl", "a") as writer:
235
- for board in unique_boards:
236
- filename = firmware_folder / board.port / board.filename
237
- filename.parent.mkdir(exist_ok=True)
238
- if filename.exists() and not force:
239
- skipped += 1
240
- log.debug(f" {filename} already exists, skip download")
241
- continue
242
- log.info(f"Downloading {board.firmware}")
243
- log.info(f" to {filename}")
244
- try:
245
- r = requests.get(board.firmware, allow_redirects=True)
246
- with open(filename, "wb") as fw:
247
- fw.write(r.content)
248
- board.filename = str(filename.relative_to(firmware_folder))
249
- except requests.RequestException as e:
250
- log.exception(e)
251
- continue
252
- writer.write(board.to_dict())
253
- downloaded += 1
254
- # if downloaded > 0:
255
- # clean_downloaded_firmwares(firmware_folder)
256
- log.success(f"Downloaded {downloaded} firmwares, skipped {skipped} existing files.")
257
- return downloaded + skipped
258
-
259
-
260
- def get_firmware_list(ports: List[str], boards: List[str], versions: List[str], clean: bool):
261
- """
262
- Retrieves a list of unique firmware information based on the specified ports, boards, versions, and clean flag.
263
-
264
- Args:
265
- ports : The list of ports to check for firmware.
266
- boards : The list of boards to filter the firmware by.
267
- versions : The list of versions to filter the firmware by.
268
- clean : A flag indicating whether to perform a clean check.
269
-
270
- Returns:
271
- List[FWInfo]: A list of unique firmware information.
272
-
273
- """
274
-
275
- log.trace("Checking MicroPython download pages")
276
- preview = "preview" in versions
277
- board_urls = sorted(get_boards(ports, boards, clean), key=key_fw_ver_pre_ext_bld)
278
-
279
- log.debug(f"Total {len(board_urls)} firmwares")
280
-
281
- relevant = [
282
- board for board in board_urls if board.version in versions and board.build == "0" and board.board in boards and not board.preview
283
- ]
284
-
285
- if preview:
286
- relevant.extend([board for board in board_urls if board.board in boards and board.preview])
287
- log.debug(f"Matching firmwares: {len(relevant)}")
288
- # select the unique boards
289
- unique_boards: List[FWInfo] = []
290
- for _, g in itertools.groupby(relevant, key=key_fw_var_pre_ext):
291
- # list is aleady sorted by build so we can just get the last item
292
- sub_list = list(g)
293
- unique_boards.append(sub_list[-1])
294
- log.debug(f"Last preview only: {len(unique_boards)}")
295
- return unique_boards
296
-
297
-
298
- def download(
299
- destination: Path,
300
- ports: List[str],
301
- boards: List[str],
302
- versions: List[str],
303
- force: bool,
304
- clean: bool,
305
- ) -> int:
306
- """
307
- Downloads firmware files based on the specified destination, ports, boards, versions, force flag, and clean flag.
308
-
309
- Args:
310
- destination : The destination folder to save the downloaded firmware files.
311
- ports : The list of ports to check for firmware.
312
- boards : The list of boards to download firmware for.
313
- versions : The list of versions to download firmware for.
314
- force : A flag indicating whether to force the download even if the firmware file already exists.
315
- clean : A flag indicating whether to clean the date from the firmware filename.
316
-
317
- Returns:
318
- int: The number of downloaded firmware files.
319
-
320
- Raises:
321
- MPFlashError : If no boards are found or specified.
322
-
323
- """
324
- if not boards:
325
- log.critical("No boards found, please connect a board or specify boards to download firmware for.")
326
- raise MPFlashError("No boards found")
327
-
328
- try:
329
- destination.mkdir(exist_ok=True, parents=True)
330
- except (PermissionError, FileNotFoundError) as e:
331
- log.critical(f"Could not create folder {destination}")
332
- raise MPFlashError(f"Could not create folder {destination}") from e
333
- try:
334
- result = download_firmwares(destination, ports, boards, versions, force=force, clean=clean)
335
- except requests.exceptions.RequestException as e:
336
- log.exception(e)
337
- raise MPFlashError("Could not connect to micropython.org") from e
338
-
339
- return result
340
-
341
-
342
- def add_renamed_boards(boards: List[str]) -> List[str]:
343
- """
344
- Adds the renamed boards to the list of boards.
345
-
346
- Args:
347
- boards : The list of boards to add the renamed boards to.
348
-
349
- Returns:
350
- List[str]: The list of boards with the renamed boards added.
351
-
352
- """
353
- renamed = {
354
- "PICO": ["RPI_PICO"],
355
- "PICO_W": ["RPI_PICO_W"],
356
- "GENERIC": ["ESP32_GENERIC", "ESP8266_GENERIC"], # just add both of them
357
- }
358
- _boards = boards.copy()
359
- for board in boards:
360
- if board in renamed and renamed[board] not in boards:
361
- _boards.extend(renamed[board])
362
- if board != board.upper() and board.upper() not in boards:
363
- _boards.append(board.upper())
364
- return _boards
1
+ """
2
+ Module to download MicroPython firmware for specific boards and versions.
3
+ Uses the micropython.org website to get the available versions and locations to download firmware files.
4
+ """
5
+
6
+ import functools
7
+ import itertools
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+ from urllib.parse import urljoin
12
+
13
+ # #########################################################################################################
14
+ # make sure that jsonlines does not mistake the MicroPython ujson for the CPython ujson
15
+ import jsonlines
16
+ import requests
17
+ from bs4 import BeautifulSoup
18
+ from loguru import logger as log
19
+ from rich.progress import track
20
+
21
+ from mpflash.common import PORT_FWTYPES, FWInfo
22
+ from mpflash.errors import MPFlashError
23
+ from mpflash.mpboard_id import get_known_ports
24
+ from mpflash.versions import clean_version
25
+
26
+ # avoid conflict with the ujson used by MicroPython
27
+ jsonlines.ujson = None # type: ignore
28
+ # #########################################################################################################
29
+
30
+
31
+ MICROPYTHON_ORG_URL = "https://micropython.org/"
32
+
33
+ # Regexes to remove dates and hashes in the filename that just get in the way
34
+ RE_DATE = r"(-\d{8}-)"
35
+ RE_HASH = r"(.g[0-9a-f]+\.)"
36
+ # regex to extract the version and the build from the firmware filename
37
+ # group 1 is the version, group 2 is the build
38
+ RE_VERSION_PREVIEW = r"v([\d\.]+)-?(?:preview\.)?(\d+)?\."
39
+ # 'RPI_PICO_W-v1.23.uf2'
40
+ # 'RPI_PICO_W-v1.23.0.uf2'
41
+ # 'RPI_PICO_W-v1.23.0-406.uf2'
42
+ # 'RPI_PICO_W-v1.23.0-preview.406.uf2'
43
+ # 'RPI_PICO_W-v1.23.0-preview.4.uf2'
44
+ # 'RPI_PICO_W-v1.23.0.uf2'
45
+ # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.gc1a6b95bf.uf2'
46
+ # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.uf2'
47
+ # 'RPI_PICO_W-v1.24.0-preview.10.gc1a6b95bf.uf2'
48
+
49
+
50
+ # use functools.lru_cache to avoid needing to download pages multiple times
51
+ @functools.lru_cache(maxsize=500)
52
+ def get_page(page_url: str) -> str:
53
+ """Get the HTML of a page and return it as a string."""
54
+ response = requests.get(page_url)
55
+ return response.content.decode()
56
+
57
+
58
+ @functools.lru_cache(maxsize=500)
59
+ def get_board_urls(page_url: str) -> List[Dict[str, str]]:
60
+ """
61
+ Get the urls to all the board pages listed on this page.
62
+ Assumes that all links to firmware have "class": "board-card"
63
+
64
+ Args:
65
+ page_url (str): The url of the page to get the board urls from.
66
+
67
+ Returns:
68
+ List[Dict[str, str]]: A list of dictionaries containing the board name and url.
69
+
70
+ """
71
+ downloads_html = get_page(page_url)
72
+ soup = BeautifulSoup(downloads_html, "html.parser")
73
+ tags = soup.findAll("a", recursive=True, attrs={"class": "board-card"})
74
+ # assumes that all links are relative to the page url
75
+ boards = [tag.get("href") for tag in tags]
76
+ if "?" in page_url:
77
+ page_url = page_url.split("?")[0]
78
+ return [{"board": board, "url": page_url + board} for board in boards]
79
+
80
+
81
+ def board_firmware_urls(board_url: str, base_url: str, ext: str) -> List[str]:
82
+ """
83
+ Get the urls to all the firmware files for a board.
84
+ Args:
85
+ page_url (str): The url of the page to get the board urls from.
86
+ ??? base_url (str): The base url to join the relative urls to.
87
+ ext (str): The extension of the firmware files to get. (with or withouth leading .)
88
+
89
+ the urls are relative urls to the site root
90
+
91
+ """
92
+ html = get_page(board_url)
93
+ soup = BeautifulSoup(html, "html.parser")
94
+ # get all the a tags:
95
+ # 1. that have a url that starts with `/resources/firmware/`
96
+ # 2. end with a matching extension for this port.
97
+ tags = soup.findAll(
98
+ "a",
99
+ recursive=True,
100
+ attrs={"href": re.compile(r"^/resources/firmware/.*\." + ext.lstrip(".") + "$")},
101
+ )
102
+ if "?" in base_url:
103
+ base_url = base_url.split("?")[0]
104
+ links: List = [urljoin(base_url, tag.get("href")) for tag in tags]
105
+ return links
106
+
107
+
108
+ # boards we are interested in ( this avoids getting a lot of boards we don't care about)
109
+ # The first run takes ~60 seconds to run for 4 ports , all boards
110
+ # so it makes sense to cache the results and skip boards as soon as possible
111
+ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FWInfo]:
112
+ # sourcery skip: use-getitem-for-re-match-groups
113
+ """
114
+ Retrieves a list of firmware information for the specified ports and boards.
115
+
116
+ Args:
117
+ ports (List[str]): The list of ports to check for firmware.
118
+ boards (List[str]): The list of boards to retrieve firmware information for.
119
+ clean (bool): A flag indicating whether to perform a clean retrieval.
120
+
121
+ Returns:
122
+ List[FWInfo]: A list of firmware information for the specified ports and boards.
123
+
124
+ """
125
+ board_urls: List[FWInfo] = []
126
+ if ports is None:
127
+ ports = get_known_ports()
128
+ for port in ports:
129
+ download_page_url = f"{MICROPYTHON_ORG_URL}download/?port={port}"
130
+ urls = get_board_urls(download_page_url)
131
+ # filter out boards we don't care about
132
+ urls = [board for board in urls if board["board"] in boards]
133
+ # add the port to the board urls
134
+ for board in urls:
135
+ board["port"] = port
136
+
137
+ for board in track(
138
+ urls,
139
+ description=f"Checking {port} download pages",
140
+ transient=True,
141
+ refresh_per_second=1,
142
+ show_speed=False,
143
+ ):
144
+ # add a board to the list for each firmware found
145
+ firmware_urls: List[str] = []
146
+ for ext in PORT_FWTYPES[port]:
147
+ firmware_urls += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
148
+ for _url in firmware_urls:
149
+ board["firmware"] = _url
150
+ fname = Path(board["firmware"]).name
151
+ if clean:
152
+ # remove date from firmware name
153
+ fname = re.sub(RE_DATE, "-", fname)
154
+ # remove hash from firmware name
155
+ fname = re.sub(RE_HASH, ".", fname)
156
+ fw_info = FWInfo(
157
+ filename=fname,
158
+ port=port,
159
+ board=board["board"],
160
+ preview="preview" in _url,
161
+ firmware=_url,
162
+ version="",
163
+ )
164
+ # board["firmware"] = _url
165
+ # board["preview"] = "preview" in _url # type: ignore
166
+ if ver_match := re.search(RE_VERSION_PREVIEW, _url):
167
+ fw_info.version = clean_version(ver_match.group(1))
168
+ fw_info.build = ver_match.group(2) or "0"
169
+ fw_info.preview = fw_info.build != "0"
170
+ # # else:
171
+ # # board.$1= ""
172
+ # if "preview." in fw_info.version:
173
+ # fw_info.build = fw_info.version.split("preview.")[-1]
174
+ # else:
175
+ # fw_info.build = "0"
176
+
177
+ fw_info.ext = Path(fw_info.firmware).suffix
178
+ fw_info.variant = fw_info.filename.split("-v")[0] if "-v" in fw_info.filename else ""
179
+
180
+ board_urls.append(fw_info)
181
+ return board_urls
182
+
183
+
184
+ def key_fw_ver_pre_ext_bld(x: FWInfo):
185
+ "sorting key for the retrieved board urls"
186
+ return x.variant, x.version, x.preview, x.ext, x.build
187
+
188
+
189
+ def key_fw_var_pre_ext(x: FWInfo):
190
+ "Grouping key for the retrieved board urls"
191
+ return x.variant, x.preview, x.ext
192
+
193
+
194
+ def download_firmwares(
195
+ firmware_folder: Path,
196
+ ports: List[str],
197
+ boards: List[str],
198
+ versions: Optional[List[str]] = None,
199
+ *,
200
+ force: bool = False,
201
+ clean: bool = True,
202
+ ) -> int:
203
+ """
204
+ Downloads firmware files based on the specified firmware folder, ports, boards, versions, force flag, and clean flag.
205
+
206
+ Args:
207
+ firmware_folder : The folder to save the downloaded firmware files.
208
+ ports : The list of ports to check for firmware.
209
+ boards : The list of boards to download firmware for.
210
+ versions : The list of versions to download firmware for.
211
+ force : A flag indicating whether to force the download even if the firmware file already exists.
212
+ clean : A flag indicating to clean the date from the firmware filename.
213
+ """
214
+ skipped = downloaded = 0
215
+ versions = [] if versions is None else [clean_version(v) for v in versions]
216
+ # handle renamed boards
217
+ boards = add_renamed_boards(boards)
218
+
219
+ unique_boards = get_firmware_list(ports, boards, versions, clean)
220
+
221
+ for b in unique_boards:
222
+ log.debug(b.filename)
223
+ # relevant
224
+
225
+ log.info(f"Found {len(unique_boards)} relevant unique firmwares")
226
+ if not unique_boards:
227
+ log.error("No relevant firmwares could be found on https://micropython.org/download")
228
+ log.info(f"{versions=} {ports=} {boards=}")
229
+ log.info("Please check the website for the latest firmware files or try the preview version.")
230
+ return 0
231
+
232
+ firmware_folder.mkdir(exist_ok=True)
233
+
234
+ with jsonlines.open(firmware_folder / "firmware.jsonl", "a") as writer:
235
+ for board in unique_boards:
236
+ filename = firmware_folder / board.port / board.filename
237
+ filename.parent.mkdir(exist_ok=True)
238
+ if filename.exists() and not force:
239
+ skipped += 1
240
+ log.debug(f" {filename} already exists, skip download")
241
+ continue
242
+ log.info(f"Downloading {board.firmware}")
243
+ log.info(f" to {filename}")
244
+ try:
245
+ r = requests.get(board.firmware, allow_redirects=True)
246
+ with open(filename, "wb") as fw:
247
+ fw.write(r.content)
248
+ board.filename = str(filename.relative_to(firmware_folder))
249
+ except requests.RequestException as e:
250
+ log.exception(e)
251
+ continue
252
+ writer.write(board.to_dict())
253
+ downloaded += 1
254
+ # if downloaded > 0:
255
+ # clean_downloaded_firmwares(firmware_folder)
256
+ log.success(f"Downloaded {downloaded} firmwares, skipped {skipped} existing files.")
257
+ return downloaded + skipped
258
+
259
+
260
+ def get_firmware_list(ports: List[str], boards: List[str], versions: List[str], clean: bool):
261
+ """
262
+ Retrieves a list of unique firmware information based on the specified ports, boards, versions, and clean flag.
263
+
264
+ Args:
265
+ ports : The list of ports to check for firmware.
266
+ boards : The list of boards to filter the firmware by.
267
+ versions : The list of versions to filter the firmware by.
268
+ clean : A flag indicating whether to perform a clean check.
269
+
270
+ Returns:
271
+ List[FWInfo]: A list of unique firmware information.
272
+
273
+ """
274
+
275
+ log.trace("Checking MicroPython download pages")
276
+ preview = "preview" in versions
277
+ board_urls = sorted(get_boards(ports, boards, clean), key=key_fw_ver_pre_ext_bld)
278
+
279
+ log.debug(f"Total {len(board_urls)} firmwares")
280
+
281
+ relevant = [
282
+ board for board in board_urls if board.version in versions and board.build == "0" and board.board in boards and not board.preview
283
+ ]
284
+
285
+ if preview:
286
+ relevant.extend([board for board in board_urls if board.board in boards and board.preview])
287
+ log.debug(f"Matching firmwares: {len(relevant)}")
288
+ # select the unique boards
289
+ unique_boards: List[FWInfo] = []
290
+ for _, g in itertools.groupby(relevant, key=key_fw_var_pre_ext):
291
+ # list is aleady sorted by build so we can just get the last item
292
+ sub_list = list(g)
293
+ unique_boards.append(sub_list[-1])
294
+ log.debug(f"Last preview only: {len(unique_boards)}")
295
+ return unique_boards
296
+
297
+
298
+ def download(
299
+ destination: Path,
300
+ ports: List[str],
301
+ boards: List[str],
302
+ versions: List[str],
303
+ force: bool,
304
+ clean: bool,
305
+ ) -> int:
306
+ """
307
+ Downloads firmware files based on the specified destination, ports, boards, versions, force flag, and clean flag.
308
+
309
+ Args:
310
+ destination : The destination folder to save the downloaded firmware files.
311
+ ports : The list of ports to check for firmware.
312
+ boards : The list of boards to download firmware for.
313
+ versions : The list of versions to download firmware for.
314
+ force : A flag indicating whether to force the download even if the firmware file already exists.
315
+ clean : A flag indicating whether to clean the date from the firmware filename.
316
+
317
+ Returns:
318
+ int: The number of downloaded firmware files.
319
+
320
+ Raises:
321
+ MPFlashError : If no boards are found or specified.
322
+
323
+ """
324
+ if not boards:
325
+ log.critical("No boards found, please connect a board or specify boards to download firmware for.")
326
+ raise MPFlashError("No boards found")
327
+
328
+ try:
329
+ destination.mkdir(exist_ok=True, parents=True)
330
+ except (PermissionError, FileNotFoundError) as e:
331
+ log.critical(f"Could not create folder {destination}")
332
+ raise MPFlashError(f"Could not create folder {destination}") from e
333
+ try:
334
+ result = download_firmwares(destination, ports, boards, versions, force=force, clean=clean)
335
+ except requests.exceptions.RequestException as e:
336
+ log.exception(e)
337
+ raise MPFlashError("Could not connect to micropython.org") from e
338
+
339
+ return result
340
+
341
+
342
+ def add_renamed_boards(boards: List[str]) -> List[str]:
343
+ """
344
+ Adds the renamed boards to the list of boards.
345
+
346
+ Args:
347
+ boards : The list of boards to add the renamed boards to.
348
+
349
+ Returns:
350
+ List[str]: The list of boards with the renamed boards added.
351
+
352
+ """
353
+ renamed = {
354
+ "PICO": ["RPI_PICO"],
355
+ "PICO_W": ["RPI_PICO_W"],
356
+ "GENERIC": ["ESP32_GENERIC", "ESP8266_GENERIC"], # just add both of them
357
+ }
358
+ _boards = boards.copy()
359
+ for board in boards:
360
+ if board in renamed and renamed[board] not in boards:
361
+ _boards.extend(renamed[board])
362
+ if board != board.upper() and board.upper() not in boards:
363
+ _boards.append(board.upper())
364
+ return _boards