mpflash 1.25.0__py3-none-any.whl → 1.25.0.post2__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 (52) hide show
  1. mpflash/add_firmware.py +43 -16
  2. mpflash/ask_input.py +4 -4
  3. mpflash/basicgit.py +2 -2
  4. mpflash/bootloader/manual.py +1 -1
  5. mpflash/cli_download.py +8 -5
  6. mpflash/cli_flash.py +31 -35
  7. mpflash/cli_group.py +3 -0
  8. mpflash/cli_list.py +8 -3
  9. mpflash/cli_main.py +4 -0
  10. mpflash/common.py +2 -38
  11. mpflash/config.py +21 -0
  12. mpflash/db/__init__.py +2 -0
  13. mpflash/db/core.py +61 -0
  14. mpflash/db/gather_boards.py +112 -0
  15. mpflash/db/loader.py +122 -0
  16. mpflash/db/meta.py +78 -0
  17. mpflash/db/micropython_boards.zip +0 -0
  18. mpflash/db/models.py +93 -0
  19. mpflash/db/tools.py +27 -0
  20. mpflash/download/__init__.py +46 -64
  21. mpflash/download/from_web.py +26 -36
  22. mpflash/download/fwinfo.py +41 -0
  23. mpflash/download/jid.py +56 -0
  24. mpflash/downloaded.py +79 -93
  25. mpflash/flash/__init__.py +7 -3
  26. mpflash/flash/esp.py +2 -1
  27. mpflash/flash/stm32.py +1 -1
  28. mpflash/flash/uf2/windows.py +3 -1
  29. mpflash/flash/worklist.py +16 -28
  30. mpflash/list.py +3 -3
  31. mpflash/logger.py +43 -9
  32. mpflash/mpboard_id/__init__.py +3 -9
  33. mpflash/mpboard_id/alternate.py +56 -0
  34. mpflash/mpboard_id/board_id.py +11 -94
  35. mpflash/mpboard_id/known.py +45 -73
  36. mpflash/mpboard_id/resolve.py +19 -0
  37. mpflash/mpremoteboard/__init__.py +4 -3
  38. mpflash/mpremoteboard/mpy_fw_info.py +1 -0
  39. mpflash/mpremoteboard/runner.py +5 -2
  40. mpflash/vendor/pydfu.py +33 -6
  41. mpflash/versions.py +3 -0
  42. {mpflash-1.25.0.dist-info → mpflash-1.25.0.post2.dist-info}/METADATA +49 -12
  43. mpflash-1.25.0.post2.dist-info/RECORD +69 -0
  44. mpflash/db/boards.py +0 -63
  45. mpflash/db/downloads.py +0 -87
  46. mpflash/mpboard_id/add_boards.py +0 -260
  47. mpflash/mpboard_id/board.py +0 -40
  48. mpflash/mpboard_id/store.py +0 -47
  49. mpflash-1.25.0.dist-info/RECORD +0 -62
  50. {mpflash-1.25.0.dist-info → mpflash-1.25.0.post2.dist-info}/LICENSE +0 -0
  51. {mpflash-1.25.0.dist-info → mpflash-1.25.0.post2.dist-info}/WHEEL +0 -0
  52. {mpflash-1.25.0.dist-info → mpflash-1.25.0.post2.dist-info}/entry_points.txt +0 -0
mpflash/add_firmware.py CHANGED
@@ -5,18 +5,29 @@ from typing import Union
5
5
  import jsonlines
6
6
  import requests
7
7
  from loguru import logger as log
8
-
9
8
  # re-use logic from mpremote
10
9
  from mpremote.mip import _rewrite_url as rewrite_url # type: ignore
10
+ from pytest import Session
11
11
 
12
- from mpflash.common import FWInfo
13
12
  from mpflash.config import config
13
+ from mpflash.db.core import Session
14
+ from mpflash.db.models import Firmware
14
15
  from mpflash.versions import get_preview_mp_version, get_stable_mp_version
15
16
 
17
+ # github.com/<owner>/<repo>@<branch>#<commit>
18
+ # $remote_url = git remote get-url origin
19
+ # $branch = git rev-parse --abbrev-ref HEAD
20
+ # $commit = git rev-parse --short HEAD
21
+ # if ($remote_url -match "github.com[:/](.+)/(.+?)(\.git)?$") {
22
+ # $owner = $matches[1]
23
+ # $repo = $matches[2]
24
+ # "github.com/$owner/$repo@$branch#$commit"
25
+ # }
26
+
16
27
 
17
28
  def add_firmware(
18
29
  source: Union[Path, str],
19
- new_fw: FWInfo,
30
+ new_fw: Firmware,
20
31
  *,
21
32
  force: bool = False,
22
33
  custom: bool = False,
@@ -25,11 +36,11 @@ def add_firmware(
25
36
  """Add a firmware to the firmware folder.
26
37
 
27
38
  stored in the port folder, with the same filename as the source.
28
-
29
39
  """
30
40
  # Check minimal info needed
31
- if not new_fw.port or not new_fw.board:
32
- log.error("Port and board are required")
41
+
42
+ if not new_fw.board_id or not new_fw.board or not new_fw.port:
43
+ log.error("board_id, board and port are required")
33
44
  return False
34
45
  if not isinstance(source, Path) and not source.startswith("http"):
35
46
  log.error(f"Invalid source {source}")
@@ -37,31 +48,47 @@ def add_firmware(
37
48
 
38
49
  # use sensible defaults
39
50
  source_2 = Path(source)
40
- new_fw.ext = new_fw.ext or source_2.suffix
41
- new_fw.variant = new_fw.variant or new_fw.board
51
+ # new_fw.variant = new_fw.variant or new_fw.board
42
52
  new_fw.custom = new_fw.custom or custom
43
- new_fw.description = new_fw.description or description
44
53
  if not new_fw.version:
45
54
  # TODO: Get version from filename
46
55
  # or use the last preview version
47
- new_fw.version = get_preview_mp_version() if new_fw.preview else get_stable_mp_version()
56
+ new_fw.version = get_preview_mp_version()
48
57
 
49
58
  config.firmware_folder.mkdir(exist_ok=True)
50
59
 
51
60
  fw_filename = config.firmware_folder / new_fw.port / source_2.name
52
61
 
53
- new_fw.filename = str(fw_filename.relative_to(config.firmware_folder))
54
- new_fw.firmware = source.as_uri() if isinstance(source, Path) else source
62
+ new_fw.firmware_file = str(fw_filename.relative_to(config.firmware_folder))
63
+ new_fw.source = source.as_uri() if isinstance(source, Path) else source
55
64
 
56
65
  if not copy_firmware(source, fw_filename, force):
57
66
  log.error(f"Failed to copy {source} to {fw_filename}")
58
67
  return False
59
68
  # add to inventory
60
- with jsonlines.open(config.firmware_folder / "firmware.jsonl", "a") as writer:
61
- log.info(f"Adding {new_fw.port} {new_fw.board}")
62
- log.info(f" to {fw_filename}")
69
+ with Session() as session:
70
+ # check if the firmware already exists
71
+ existing_fw = (
72
+ session.query(Firmware)
73
+ .filter(
74
+ Firmware.board_id == new_fw.board_id,
75
+ Firmware.version == new_fw.version,
76
+ Firmware.port == new_fw.port,
77
+ )
78
+ .first()
79
+ )
80
+ if existing_fw:
81
+ log.warning(f"Firmware {existing_fw} already exists")
82
+ if not force:
83
+ return False
84
+ # update the existing firmware
85
+ existing_fw.firmware_file = new_fw.firmware_file
86
+ existing_fw.source = new_fw.source
87
+ existing_fw.custom = custom
88
+ existing_fw.description = description
89
+ else:
90
+ session.add(new_fw)
63
91
 
64
- writer.write(new_fw.to_dict())
65
92
  return True
66
93
 
67
94
 
mpflash/ask_input.py CHANGED
@@ -11,7 +11,7 @@ from loguru import logger as log
11
11
 
12
12
  from .common import DownloadParams, FlashParams, ParamType
13
13
  from .config import config
14
- from .mpboard_id import get_known_boards_for_port, get_known_ports, known_stored_boards
14
+ from .mpboard_id import get_known_boards_for_port, known_ports, known_stored_boards
15
15
  from .mpremoteboard import MPRemoteBoard
16
16
  from .versions import micropython_versions
17
17
 
@@ -106,7 +106,7 @@ def filter_matching_boards(answers: dict) -> Sequence[Tuple[str, str]]:
106
106
  Returns:
107
107
  Sequence[Tuple[str, str]]: The filtered boards.
108
108
  """
109
- versions = None
109
+ versions = []
110
110
  # if version is not asked ; then need to get the version from the inputs
111
111
  if "versions" in answers:
112
112
  versions = list(answers["versions"])
@@ -151,7 +151,7 @@ def ask_port_board(*, multi_select: bool, action: str):
151
151
  inquirer.List(
152
152
  "port",
153
153
  message="Which port do you want to {action} " + "to {serial} ?" if action == "flash" else "?",
154
- choices=get_known_ports(),
154
+ choices=known_ports(),
155
155
  # autocomplete=True,
156
156
  ),
157
157
  inquirer_ux(
@@ -226,7 +226,7 @@ def ask_serialport(*, multi_select: bool = False, bluetooth: bool = False):
226
226
  # import only when needed to reduce load time
227
227
  import inquirer
228
228
 
229
- comports = MPRemoteBoard.connected_boards(bluetooth=bluetooth, description=True)
229
+ comports = MPRemoteBoard.connected_boards(bluetooth=bluetooth, description=True) + ["auto"]
230
230
  return inquirer.List(
231
231
  "serial",
232
232
  message="Which serial port do you want to {action} ?",
mpflash/basicgit.py CHANGED
@@ -24,6 +24,7 @@ from mpflash.config import config
24
24
 
25
25
  # GH_CLIENT = None
26
26
 
27
+
27
28
  def _run_local_git(
28
29
  cmd: List[str],
29
30
  repo: Optional[Union[Path, str]] = None,
@@ -180,7 +181,6 @@ def checkout_tag(tag: str, repo: Optional[Union[str, Path]] = None) -> bool:
180
181
  return True
181
182
 
182
183
 
183
-
184
184
  def checkout_commit(commit_hash: str, repo: Optional[Union[Path, str]] = None) -> bool:
185
185
  """
186
186
  Checkout a specific commit
@@ -267,7 +267,7 @@ def pull(repo: Union[Path, str], branch: str = "main") -> bool:
267
267
  return result.returncode == 0
268
268
 
269
269
 
270
- def get_git_describe(folder: Optional[Path | str] = None):
270
+ def get_git_describe(folder: Optional[Union[Path, str]] = None):
271
271
  """
272
272
  Based on MicroPython makeversionhdr.py
273
273
  returns : current git tag, commits ,commit hash : "v1.19.1-841-g3446"
@@ -55,7 +55,7 @@ def enter_bootloader_manual(mcu: MPRemoteBoard, timeout: int = 10):
55
55
  message: str
56
56
  if mcu.port == "rp2":
57
57
  message = f"""\
58
- Please put your {" ".join([mcu.port,mcu.board])} device into bootloader mode by either:
58
+ Please put your {" ".join([mcu.port, mcu.board])} device into bootloader mode by either:
59
59
  Method 1:
60
60
  1. Unplug the USB cable,
61
61
  2. Press and hold the BOOTSEL button on the device,
mpflash/cli_download.py CHANGED
@@ -6,8 +6,10 @@ import rich_click as click
6
6
  from loguru import logger as log
7
7
 
8
8
  from mpflash.connected import connected_ports_boards
9
+ from mpflash.downloaded import clean_downloaded_firmwares
9
10
  from mpflash.errors import MPFlashError
10
11
  from mpflash.mpboard_id import find_known_board
12
+ from mpflash.mpboard_id.alternate import add_renamed_boards
11
13
  from mpflash.versions import clean_version
12
14
 
13
15
  from .ask_input import ask_missing_params
@@ -26,8 +28,8 @@ from .download import download
26
28
  "-d",
27
29
  "fw_folder",
28
30
  type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
29
- default=config.firmware_folder,
30
- show_default=True,
31
+ default=None,
32
+ show_default=False,
31
33
  help="The folder to download the firmware to.",
32
34
  )
33
35
  @click.option(
@@ -92,7 +94,8 @@ def cli_download(**kwargs) -> int:
92
94
  params.boards = list(params.boards)
93
95
  params.serial = list(params.serial)
94
96
  params.ignore = list(params.ignore)
95
-
97
+ if params.fw_folder:
98
+ config.firmware_folder = Path(params.fw_folder)
96
99
  # all_boards: List[MPRemoteBoard] = []
97
100
  if params.boards:
98
101
  if not params.ports:
@@ -116,13 +119,13 @@ def cli_download(**kwargs) -> int:
116
119
 
117
120
  try:
118
121
  download(
119
- params.fw_folder,
120
122
  params.ports,
121
- params.boards,
123
+ add_renamed_boards(params.boards),
122
124
  params.versions,
123
125
  params.force,
124
126
  params.clean,
125
127
  )
128
+ clean_downloaded_firmwares()
126
129
  return 0
127
130
  except MPFlashError as e:
128
131
  log.error(f"{e}")
mpflash/cli_flash.py CHANGED
@@ -4,6 +4,8 @@ from typing import List
4
4
  import rich_click as click
5
5
  from loguru import logger as log
6
6
 
7
+ import mpflash.download.jid as jid
8
+ import mpflash.mpboard_id as mpboard_id
7
9
  from mpflash.ask_input import ask_missing_params
8
10
  from mpflash.cli_download import connected_ports_boards
9
11
  from mpflash.cli_group import cli
@@ -13,7 +15,6 @@ from mpflash.config import config
13
15
  from mpflash.errors import MPFlashError
14
16
  from mpflash.flash import flash_list
15
17
  from mpflash.flash.worklist import WorkList, full_auto_worklist, manual_worklist, single_auto_worklist
16
- from mpflash.mpboard_id import find_known_board
17
18
  from mpflash.mpremoteboard import MPRemoteBoard
18
19
  from mpflash.versions import clean_version
19
20
 
@@ -28,11 +29,11 @@ from mpflash.versions import clean_version
28
29
  )
29
30
  @click.option(
30
31
  "--firmware",
31
- "-f",
32
+ "--ff",
32
33
  "fw_folder",
33
34
  type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
34
- default=config.firmware_folder,
35
- show_default=True,
35
+ default=None,
36
+ show_default=False,
36
37
  help="The folder to retrieve the firmware from.",
37
38
  )
38
39
  @click.option(
@@ -69,7 +70,7 @@ from mpflash.versions import clean_version
69
70
  )
70
71
  @click.option(
71
72
  "--bluetooth/--no-bluetooth",
72
- "-b/-nb",
73
+ "--bt/--no-bt",
73
74
  is_flag=True,
74
75
  default=False,
75
76
  show_default=True,
@@ -94,7 +95,7 @@ from mpflash.versions import clean_version
94
95
  )
95
96
  @click.option(
96
97
  "--variant",
97
- "-var",
98
+ "--var",
98
99
  "variant", # single board
99
100
  multiple=False,
100
101
  help="The board VARIANT to flash or '-'. If not specified will try to read the variant from the connected MCU.",
@@ -116,7 +117,7 @@ from mpflash.versions import clean_version
116
117
  )
117
118
  @click.option(
118
119
  "--bootloader",
119
- "-bl",
120
+ "--bl",
120
121
  "bootloader",
121
122
  type=click.Choice([e.value for e in BootloaderMethod]),
122
123
  default="auto",
@@ -124,7 +125,16 @@ from mpflash.versions import clean_version
124
125
  help="""How to enter the (MicroPython) bootloader before flashing.""",
125
126
  )
126
127
  @click.option(
127
- "--flash_mode", "-fm",
128
+ "--force",
129
+ "-f",
130
+ default=False,
131
+ is_flag=True,
132
+ show_default=True,
133
+ help="""Force download of firmware even if it already exists.""",
134
+ )
135
+ @click.option(
136
+ "--flash_mode",
137
+ "--fm",
128
138
  type=click.Choice(["keep", "qio", "qout", "dio", "dout"]),
129
139
  default="keep",
130
140
  show_default=True,
@@ -137,7 +147,7 @@ def cli_flash_board(**kwargs) -> int:
137
147
  kwargs["boards"] = []
138
148
  kwargs.pop("board")
139
149
  else:
140
- kwargs["boards"] = [kwargs.pop("board")]
150
+ kwargs["boards"] = [kwargs.pop("board")]
141
151
 
142
152
  params = FlashParams(**kwargs)
143
153
  params.versions = list(params.versions)
@@ -148,9 +158,14 @@ def cli_flash_board(**kwargs) -> int:
148
158
  params.bootloader = BootloaderMethod(params.bootloader)
149
159
 
150
160
  # make it simple for the user to flash one board by asking for the serial port if not specified
151
- if params.boards == ["?"] and params.serial == "*":
161
+ if params.boards == ["?"] or params.serial == "?":
152
162
  params.serial = ["?"]
163
+ if params.boards == ["*"]:
164
+ # No bard specified
165
+ params.boards = ["?"]
153
166
 
167
+ if params.fw_folder:
168
+ config.firmware_folder = Path(params.fw_folder)
154
169
  # Detect connected boards if not specified,
155
170
  # and ask for input if boards cannot be detected
156
171
  all_boards: List[MPRemoteBoard] = []
@@ -167,14 +182,12 @@ def cli_flash_board(**kwargs) -> int:
167
182
  # assume manual mode if no board is detected
168
183
  params.bootloader = BootloaderMethod("manual")
169
184
  else:
170
- resolve_board_ids(params)
185
+ mpboard_id.resolve_board_ids(params)
171
186
 
172
187
  # Ask for missing input if needed
173
188
  params = ask_missing_params(params)
174
189
  if not params: # Cancelled by user
175
190
  return 2
176
- # TODO: Just in time Download of firmware
177
-
178
191
  assert isinstance(params, FlashParams)
179
192
 
180
193
  if len(params.versions) > 1:
@@ -183,6 +196,7 @@ def cli_flash_board(**kwargs) -> int:
183
196
 
184
197
  params.versions = [clean_version(v) for v in params.versions]
185
198
  worklist: WorkList = []
199
+
186
200
  # if serial port == auto and there are one or more specified/detected boards
187
201
  if params.serial == ["*"] and params.boards:
188
202
  if not all_boards:
@@ -191,37 +205,33 @@ def cli_flash_board(**kwargs) -> int:
191
205
  # if variant id provided on the cmdline, treat is as an override
192
206
  if params.variant:
193
207
  for b in all_boards:
194
- b.variant = params.variant if (params.variant.lower() not in {"-","none"}) else ""
208
+ b.variant = params.variant if (params.variant.lower() not in {"-", "none"}) else ""
195
209
 
196
210
  worklist = full_auto_worklist(
197
211
  all_boards=all_boards,
198
212
  version=params.versions[0],
199
- fw_folder=params.fw_folder,
200
213
  include=params.serial,
201
214
  ignore=params.ignore,
202
215
  )
203
216
  elif params.versions[0] and params.boards[0] and params.serial:
204
- # A one or more serial port including the board / variant
217
+ # A one or more serial port including the board / variant
205
218
  worklist = manual_worklist(
206
219
  params.serial[0],
207
220
  board_id=params.boards[0],
208
221
  version=params.versions[0],
209
- fw_folder=params.fw_folder,
210
222
  )
211
223
  else:
212
224
  # just this serial port on auto
213
225
  worklist = single_auto_worklist(
214
226
  serial=params.serial[0],
215
227
  version=params.versions[0],
216
- fw_folder=params.fw_folder,
217
228
  )
218
-
229
+ jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
219
230
  if flashed := flash_list(
220
231
  worklist,
221
- params.fw_folder,
222
232
  params.erase,
223
233
  params.bootloader,
224
- flash_mode = params.flash_mode,
234
+ flash_mode=params.flash_mode,
225
235
  ):
226
236
  log.info(f"Flashed {len(flashed)} boards")
227
237
  show_mcus(flashed, title="Updated boards after flashing")
@@ -231,17 +241,3 @@ def cli_flash_board(**kwargs) -> int:
231
241
  return 1
232
242
 
233
243
 
234
- def resolve_board_ids(params: Params):
235
- """Resolve board descriptions to board_id, and remove empty strings from list of boards"""
236
- for board_id in params.boards:
237
- if board_id == "":
238
- params.boards.remove(board_id)
239
- continue
240
- if " " in board_id:
241
- try:
242
- if info := find_known_board(board_id):
243
- log.info(f"Resolved board description: {info.board_id}")
244
- params.boards.remove(board_id)
245
- params.boards.append(info.board_id)
246
- except Exception as e:
247
- log.warning(f"Unable to resolve board description: {e}")
mpflash/cli_group.py CHANGED
@@ -10,6 +10,9 @@ from mpflash.vendor.click_aliases import ClickAliasedGroup
10
10
  from .config import __version__, config
11
11
  from .logger import log, make_quiet, set_loglevel
12
12
 
13
+ # default log level
14
+ set_loglevel("INFO")
15
+ config.verbose = False
13
16
 
14
17
  def cb_verbose(ctx, param, value):
15
18
  """Callback to set the log level to DEBUG if verbose is set"""
mpflash/cli_list.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import time
2
3
  from typing import List
3
4
 
4
5
  import rich_click as click
@@ -48,7 +49,7 @@ from .logger import make_quiet
48
49
  )
49
50
  @click.option(
50
51
  "--bluetooth/--no-bluetooth",
51
- "-b/-nb",
52
+ "--bt/--no-bt",
52
53
  is_flag=True,
53
54
  default=False,
54
55
  show_default=True,
@@ -77,10 +78,14 @@ def cli_list_mcus(serial: List[str], ignore: List[str], bluetooth: bool, as_json
77
78
  conn_mcus = [item for item in conn_mcus if not (item.toml.get("mpflash", {}).get("ignore", False))]
78
79
  if as_json:
79
80
  print(json.dumps([mcu.to_dict() for mcu in conn_mcus], indent=4))
80
-
81
+
81
82
  if progress:
82
83
  show_mcus(conn_mcus, refresh=False)
83
84
  for mcu in conn_mcus:
84
85
  # reset the board so it can continue to whatever it was running before
85
- mcu.run_command("reset")
86
+ if mcu.family == "circuitpython":
87
+ # CircuitPython boards need a special reset command
88
+ mcu.run_command(["exec", "--no-follow", "import microcontroller,time;time.sleep(0.01);microcontroller.reset()"], resume=False)
89
+ else:
90
+ mcu.run_command("reset")
86
91
  return 0 if conn_mcus else 1
mpflash/cli_main.py CHANGED
@@ -9,9 +9,13 @@ from .cli_download import cli_download
9
9
  from .cli_flash import cli_flash_board
10
10
  from .cli_group import cli
11
11
  from .cli_list import cli_list_mcus
12
+ from .db.core import migrate_database
12
13
 
13
14
 
14
15
  def mpflash():
16
+ """Main entry point for the mpflash CLI."""
17
+ migrate_database(boards=True, firmwares=True)
18
+
15
19
  cli.add_command(cli_list_mcus)
16
20
  cli.add_command(cli_download)
17
21
  cli.add_command(cli_flash_board)
mpflash/common.py CHANGED
@@ -10,7 +10,6 @@ from typing import List, Optional, Union
10
10
  from serial.tools import list_ports
11
11
  from serial.tools.list_ports_common import ListPortInfo
12
12
 
13
-
14
13
  # from mpflash.flash.esp import FlashMode
15
14
  from .logger import log
16
15
 
@@ -28,41 +27,6 @@ PORT_FWTYPES = {
28
27
 
29
28
  UF2_PORTS = [port for port, exts in PORT_FWTYPES.items() if ".uf2" in exts]
30
29
 
31
- @dataclass
32
- class FWInfo:
33
- """
34
- Downloaded Firmware information
35
- is somewhat related to the BOARD class in the mpboard_id module
36
- """
37
-
38
- port: str # MicroPython port
39
- board: str # MicroPython board
40
- filename: str = field(default="") # relative filename of the firmware image
41
- firmware: str = field(default="") # url or path to original firmware image
42
- variant: str = field(default="") # MicroPython variant
43
- preview: bool = field(default=False) # True if the firmware is a preview version
44
- version: str = field(default="") # MicroPython version (NO v prefix)
45
- url: str = field(default="") # url to the firmware image download folder
46
- build: str = field(default="0") # The build = number of commits since the last release
47
- ext: str = field(default="") # the file extension of the firmware
48
- family: str = field(default="micropython") # The family of the firmware
49
- custom: bool = field(default=False) # True if the firmware is a custom build
50
- description: str = field(default="") # Description used by this firmware (custom only)
51
-
52
- def to_dict(self) -> dict:
53
- """Convert the object to a dictionary"""
54
- return self.__dict__
55
-
56
- @classmethod
57
- def from_dict(cls, data: dict) -> "FWInfo":
58
- """Create a FWInfo object from a dictionary"""
59
- # add missing keys
60
- if "ext" not in data:
61
- data["ext"] = Path(data["firmware"]).suffix
62
- if "family" not in data:
63
- data["family"] = "micropython"
64
- return cls(**data)
65
-
66
30
 
67
31
  @dataclass
68
32
  class Params:
@@ -72,10 +36,11 @@ class Params:
72
36
  boards: List[str] = field(default_factory=list)
73
37
  variant: str = ""
74
38
  versions: List[str] = field(default_factory=list)
75
- fw_folder: Path = Path()
39
+ fw_folder: Optional[Path] = None
76
40
  serial: List[str] = field(default_factory=list)
77
41
  ignore: List[str] = field(default_factory=list)
78
42
  bluetooth: bool = False
43
+ force: bool = False
79
44
 
80
45
 
81
46
  @dataclass
@@ -83,7 +48,6 @@ class DownloadParams(Params):
83
48
  """Parameters for downloading firmware"""
84
49
 
85
50
  clean: bool = False
86
- force: bool = False
87
51
 
88
52
 
89
53
  class BootloaderMethod(Enum):
mpflash/config.py CHANGED
@@ -31,6 +31,7 @@ class MPFlashConfig:
31
31
  # No interactions in CI
32
32
  if os.getenv("GITHUB_ACTIONS") == "true":
33
33
  from mpflash.logger import log
34
+
34
35
  log.warning("Disabling interactive mode in CI")
35
36
  return False
36
37
  return self._interactive
@@ -44,18 +45,38 @@ class MPFlashConfig:
44
45
  """The folder where firmware files are stored"""
45
46
  if not self._firmware_folder:
46
47
  self._firmware_folder = platformdirs.user_downloads_path() / "firmware"
48
+ # allow testing in CI
49
+ if Path(os.getenv("GITHUB_ACTIONS", "")).as_posix().lower() == "true":
50
+ workspace = os.getenv("GITHUB_WORKSPACE")
51
+ if workspace:
52
+ ws_path = Path(workspace) / "firmware"
53
+ ws_path.mkdir(parents=True, exist_ok=True)
54
+ print(f"Detected GitHub Actions environment. Using workspace path: {ws_path}")
55
+ self._firmware_folder = ws_path
47
56
  return self._firmware_folder
48
57
 
58
+ @firmware_folder.setter
59
+ def firmware_folder(self, value: Path):
60
+ """Set the firmware folder"""
61
+ if value.exists() and value.is_dir():
62
+ self._firmware_folder = value
63
+ else:
64
+ raise ValueError(f"Invalid firmware folder: {value}. It must be a valid directory.")
65
+
49
66
  @property
50
67
  def db_path(self) -> Path:
51
68
  """The path to the database file"""
52
69
  return self.firmware_folder / "mpflash.db"
70
+ @property
71
+ def db_version(self) -> str:
72
+ return "1.24.1"
53
73
 
54
74
  @property
55
75
  def gh_client(self):
56
76
  """The gh client to use"""
57
77
  if not self._gh_client:
58
78
  from github import Auth, Github
79
+
59
80
  # Token with no permissions to avoid throttling
60
81
  # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#getting-a-higher-rate-limit
61
82
  PAT_NO_ACCESS = "github_pat_" + "11AAHPVFQ0G4NTaQ73Bw5J" + "_fAp7K9sZ1qL8VFnI9g78eUlCdmOXHB3WzSdj2jtEYb4XF3N7PDJBl32qIxq"
mpflash/db/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+
2
+ from .models import Base, Board, Firmware
mpflash/db/core.py ADDED
@@ -0,0 +1,61 @@
1
+ from pathlib import Path
2
+ from sqlite3 import DatabaseError, OperationalError
3
+
4
+ from loguru import logger as log
5
+ from sqlalchemy import create_engine
6
+ from sqlalchemy.orm import sessionmaker
7
+
8
+ from mpflash.config import config
9
+ from mpflash.errors import MPFlashError
10
+
11
+ # TODO: lazy import to avoid slowdowns ?
12
+ from .models import Base
13
+
14
+ TRACE = False
15
+ connect_str = f"sqlite:///{config.db_path.as_posix()}"
16
+ engine = create_engine(connect_str, echo=TRACE)
17
+ Session = sessionmaker(bind=engine)
18
+
19
+
20
+ def migrate_database(boards: bool = True, firmwares: bool = True):
21
+ """Migrate from 1.24.x to 1.25.x"""
22
+ # Move import here to avoid circular import
23
+ from .loader import load_jsonl_to_db, update_boards
24
+
25
+ # get the location of the database from the session
26
+ with Session() as session:
27
+ db_location = session.get_bind().url.database # type: ignore
28
+ log.debug(f"Database location: {Path(db_location)}") # type: ignore
29
+
30
+ try:
31
+ create_database()
32
+ except (DatabaseError, OperationalError) as e:
33
+ log.error(f"Error creating database: {e}")
34
+ log.error("Database might already exist, trying to migrate.")
35
+ raise MPFlashError("Database migration failed. Please check the logs for more details.") from e
36
+ if boards:
37
+ update_boards()
38
+ if firmwares:
39
+ jsonl_file = config.firmware_folder / "firmware.jsonl"
40
+ if jsonl_file.exists():
41
+ log.info(f"Migrating JSONL data {jsonl_file} to SQLite database.")
42
+ load_jsonl_to_db(jsonl_file)
43
+ # rename the jsonl file to jsonl.bak
44
+ log.info(f"Renaming {jsonl_file} to {jsonl_file.with_suffix('.jsonl.bak')}")
45
+ try:
46
+ jsonl_file.rename(jsonl_file.with_suffix(".jsonl.bak"))
47
+ except OSError as e:
48
+ for i in range(1, 10):
49
+ try:
50
+ jsonl_file.rename(jsonl_file.with_suffix(f".jsonl.{i}.bak"))
51
+ break
52
+ except OSError:
53
+ continue
54
+
55
+
56
+ def create_database():
57
+ """
58
+ Create the SQLite database and tables if they don't exist.
59
+ """
60
+ # Create the database and tables if they don't exist
61
+ Base.metadata.create_all(engine)