mpflash 1.25.0.post1__py3-none-any.whl → 1.25.0rc2__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 (50) hide show
  1. mpflash/add_firmware.py +43 -16
  2. mpflash/ask_input.py +4 -3
  3. mpflash/basicgit.py +1 -1
  4. mpflash/bootloader/manual.py +1 -1
  5. mpflash/cli_download.py +8 -5
  6. mpflash/cli_flash.py +17 -33
  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 +1 -36
  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 +53 -0
  24. mpflash/downloaded.py +79 -93
  25. mpflash/flash/__init__.py +7 -3
  26. mpflash/flash/esp.py +2 -1
  27. mpflash/flash/worklist.py +16 -28
  28. mpflash/list.py +3 -3
  29. mpflash/logger.py +2 -2
  30. mpflash/mpboard_id/__init__.py +3 -9
  31. mpflash/mpboard_id/alternate.py +56 -0
  32. mpflash/mpboard_id/board_id.py +11 -94
  33. mpflash/mpboard_id/known.py +44 -56
  34. mpflash/mpboard_id/resolve.py +19 -0
  35. mpflash/mpremoteboard/__init__.py +1 -1
  36. mpflash/mpremoteboard/mpy_fw_info.py +1 -0
  37. mpflash/mpremoteboard/runner.py +5 -2
  38. mpflash/vendor/pydfu.py +4 -5
  39. mpflash/versions.py +3 -0
  40. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0rc2.dist-info}/METADATA +2 -2
  41. mpflash-1.25.0rc2.dist-info/RECORD +69 -0
  42. mpflash/db/boards.py +0 -63
  43. mpflash/db/downloads.py +0 -87
  44. mpflash/mpboard_id/add_boards.py +0 -260
  45. mpflash/mpboard_id/board.py +0 -40
  46. mpflash/mpboard_id/store.py +0 -47
  47. mpflash-1.25.0.post1.dist-info/RECORD +0 -62
  48. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0rc2.dist-info}/LICENSE +0 -0
  49. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0rc2.dist-info}/WHEEL +0 -0
  50. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0rc2.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,8 @@ 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,
15
+ known_stored_boards)
15
16
  from .mpremoteboard import MPRemoteBoard
16
17
  from .versions import micropython_versions
17
18
 
@@ -106,7 +107,7 @@ def filter_matching_boards(answers: dict) -> Sequence[Tuple[str, str]]:
106
107
  Returns:
107
108
  Sequence[Tuple[str, str]]: The filtered boards.
108
109
  """
109
- versions = None
110
+ versions = []
110
111
  # if version is not asked ; then need to get the version from the inputs
111
112
  if "versions" in answers:
112
113
  versions = list(answers["versions"])
@@ -151,7 +152,7 @@ def ask_port_board(*, multi_select: bool, action: str):
151
152
  inquirer.List(
152
153
  "port",
153
154
  message="Which port do you want to {action} " + "to {serial} ?" if action == "flash" else "?",
154
- choices=get_known_ports(),
155
+ choices=known_ports(),
155
156
  # autocomplete=True,
156
157
  ),
157
158
  inquirer_ux(
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
@@ -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
 
@@ -31,8 +32,8 @@ from mpflash.versions import clean_version
31
32
  "-f",
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,
@@ -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,8 @@ 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
+ "--flash_mode",
129
+ "-fm",
128
130
  type=click.Choice(["keep", "qio", "qout", "dio", "dout"]),
129
131
  default="keep",
130
132
  show_default=True,
@@ -137,7 +139,7 @@ def cli_flash_board(**kwargs) -> int:
137
139
  kwargs["boards"] = []
138
140
  kwargs.pop("board")
139
141
  else:
140
- kwargs["boards"] = [kwargs.pop("board")]
142
+ kwargs["boards"] = [kwargs.pop("board")]
141
143
 
142
144
  params = FlashParams(**kwargs)
143
145
  params.versions = list(params.versions)
@@ -150,7 +152,8 @@ def cli_flash_board(**kwargs) -> int:
150
152
  # make it simple for the user to flash one board by asking for the serial port if not specified
151
153
  if params.boards == ["?"] and params.serial == "*":
152
154
  params.serial = ["?"]
153
-
155
+ if params.fw_folder:
156
+ config.firmware_folder = Path(params.fw_folder)
154
157
  # Detect connected boards if not specified,
155
158
  # and ask for input if boards cannot be detected
156
159
  all_boards: List[MPRemoteBoard] = []
@@ -167,14 +170,12 @@ def cli_flash_board(**kwargs) -> int:
167
170
  # assume manual mode if no board is detected
168
171
  params.bootloader = BootloaderMethod("manual")
169
172
  else:
170
- resolve_board_ids(params)
173
+ mpboard_id.resolve_board_ids(params)
171
174
 
172
175
  # Ask for missing input if needed
173
176
  params = ask_missing_params(params)
174
177
  if not params: # Cancelled by user
175
178
  return 2
176
- # TODO: Just in time Download of firmware
177
-
178
179
  assert isinstance(params, FlashParams)
179
180
 
180
181
  if len(params.versions) > 1:
@@ -183,6 +184,7 @@ def cli_flash_board(**kwargs) -> int:
183
184
 
184
185
  params.versions = [clean_version(v) for v in params.versions]
185
186
  worklist: WorkList = []
187
+
186
188
  # if serial port == auto and there are one or more specified/detected boards
187
189
  if params.serial == ["*"] and params.boards:
188
190
  if not all_boards:
@@ -191,37 +193,33 @@ def cli_flash_board(**kwargs) -> int:
191
193
  # if variant id provided on the cmdline, treat is as an override
192
194
  if params.variant:
193
195
  for b in all_boards:
194
- b.variant = params.variant if (params.variant.lower() not in {"-","none"}) else ""
196
+ b.variant = params.variant if (params.variant.lower() not in {"-", "none"}) else ""
195
197
 
196
198
  worklist = full_auto_worklist(
197
199
  all_boards=all_boards,
198
200
  version=params.versions[0],
199
- fw_folder=params.fw_folder,
200
201
  include=params.serial,
201
202
  ignore=params.ignore,
202
203
  )
203
204
  elif params.versions[0] and params.boards[0] and params.serial:
204
- # A one or more serial port including the board / variant
205
+ # A one or more serial port including the board / variant
205
206
  worklist = manual_worklist(
206
207
  params.serial[0],
207
208
  board_id=params.boards[0],
208
209
  version=params.versions[0],
209
- fw_folder=params.fw_folder,
210
210
  )
211
211
  else:
212
212
  # just this serial port on auto
213
213
  worklist = single_auto_worklist(
214
214
  serial=params.serial[0],
215
215
  version=params.versions[0],
216
- fw_folder=params.fw_folder,
217
216
  )
218
-
217
+ jid.ensure_firmware_downloaded(worklist, version=params.versions[0])
219
218
  if flashed := flash_list(
220
219
  worklist,
221
- params.fw_folder,
222
220
  params.erase,
223
221
  params.bootloader,
224
- flash_mode = params.flash_mode,
222
+ flash_mode=params.flash_mode,
225
223
  ):
226
224
  log.info(f"Flashed {len(flashed)} boards")
227
225
  show_mcus(flashed, title="Updated boards after flashing")
@@ -231,17 +229,3 @@ def cli_flash_board(**kwargs) -> int:
231
229
  return 1
232
230
 
233
231
 
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
@@ -28,41 +28,6 @@ PORT_FWTYPES = {
28
28
 
29
29
  UF2_PORTS = [port for port, exts in PORT_FWTYPES.items() if ".uf2" in exts]
30
30
 
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
31
 
67
32
  @dataclass
68
33
  class Params:
@@ -72,7 +37,7 @@ class Params:
72
37
  boards: List[str] = field(default_factory=list)
73
38
  variant: str = ""
74
39
  versions: List[str] = field(default_factory=list)
75
- fw_folder: Path = Path()
40
+ fw_folder: Optional[Path] = None
76
41
  serial: List[str] = field(default_factory=list)
77
42
  ignore: List[str] = field(default_factory=list)
78
43
  bluetooth: bool = False
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)