lambda-watcher 0.2.0__tar.gz → 0.3.0__tar.gz

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 (55) hide show
  1. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/PKG-INFO +1 -1
  2. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/pyproject.toml +1 -1
  3. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/__init__.py +1 -1
  4. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/cli.py +30 -6
  5. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/compare.py +42 -5
  6. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/render_html.py +433 -97
  7. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/gitmirror.py +5 -3
  8. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/notify.py +2 -1
  9. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/reindex.py +8 -1
  10. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/service.py +11 -2
  11. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/store.py +53 -5
  12. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/utils.py +46 -0
  13. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/PKG-INFO +1 -1
  14. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/SOURCES.txt +1 -0
  15. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_cli.py +47 -0
  16. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_diff.py +47 -2
  17. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_ingest.py +30 -0
  18. lambda_watcher-0.3.0/tests/test_no_window.py +87 -0
  19. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_render_html.py +129 -1
  20. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/LICENSE +0 -0
  21. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/README.md +0 -0
  22. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/setup.cfg +0 -0
  23. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/__main__.py +0 -0
  24. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/__init__.py +0 -0
  25. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/deps.py +0 -0
  26. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/envvars.py +0 -0
  27. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/handler.py +0 -0
  28. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/inventory.py +0 -0
  29. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/runtime.py +0 -0
  30. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/secrets.py +0 -0
  31. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/services.py +0 -0
  32. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/config.py +0 -0
  33. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/db.py +0 -0
  34. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/__init__.py +0 -0
  35. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/build.py +0 -0
  36. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/highlight.py +0 -0
  37. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/icons.py +0 -0
  38. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/intraline.py +0 -0
  39. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/render_text.py +0 -0
  40. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/extract.py +0 -0
  41. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/identify.py +0 -0
  42. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/ingest.py +0 -0
  43. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/templates.py +0 -0
  44. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher/watcher.py +0 -0
  45. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/dependency_links.txt +0 -0
  46. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/entry_points.txt +0 -0
  47. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/requires.txt +0 -0
  48. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/top_level.txt +0 -0
  49. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_analysis.py +0 -0
  50. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_docs.py +0 -0
  51. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_extract.py +0 -0
  52. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_identify.py +0 -0
  53. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_service.py +0 -0
  54. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_utils.py +0 -0
  55. {lambda_watcher-0.2.0 → lambda_watcher-0.3.0}/tests/test_watcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-watcher
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions.
5
5
  Author: Lambda Watcher contributors
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lambda-watcher"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
1
  """Watch a downloads folder for AWS Lambda deployment packages and version them."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
  __all__ = ["__version__"]
@@ -11,7 +11,7 @@ import sys
11
11
  import webbrowser
12
12
  import zipfile
13
13
  from datetime import datetime, timezone
14
- from pathlib import Path
14
+ from pathlib import Path, PurePosixPath
15
15
  from typing import NoReturn, Optional
16
16
 
17
17
  import typer
@@ -36,7 +36,7 @@ from .service import (
36
36
  install_service,
37
37
  stop_service,
38
38
  )
39
- from .store import Store
39
+ from .store import Store, posix_stored_dir
40
40
  from .utils import format_ts, human_size, relative_ts, rmtree, setup_logging, slugify
41
41
 
42
42
  #: Index connections opened by the running command; see `_close_open_dbs`.
@@ -187,6 +187,19 @@ def _version_or_fail(db: Database, function_id: int, seq: int):
187
187
  return row
188
188
 
189
189
 
190
+ def _stored_dirname(stored_dir: str) -> str:
191
+ """The last segment of a version's stored path: ``0007-a1b2c3d4``.
192
+
193
+ Goes through :func:`~lambda_watcher.store.posix_stored_dir` first, so it
194
+ reads the same answer out of a value an older release wrote on Windows —
195
+ where ``Path(...).name`` on Linux returns the whole backslash-joined string
196
+ instead of the directory name, and the caller then builds a path to nothing.
197
+ """
198
+ # back-compat: `posix_stored_dir` is the whole point of this helper existing
199
+ # rather than callers reaching for `Path(...).name` themselves.
200
+ return PurePosixPath(posix_stored_dir(stored_dir)).name
201
+
202
+
190
203
  def _build_diff(db: Database, store: Store, cfg: Config, function_row, a_seq: int, b_seq: int,
191
204
  include_vendor: bool | None, compute_diffs: bool = True):
192
205
  """Resolve two versions and build the diff between them.
@@ -194,7 +207,10 @@ def _build_diff(db: Database, store: Store, cfg: Config, function_row, a_seq: in
194
207
  Warns, rather than fails, when a version's extracted code is missing from
195
208
  disk: the index still knows what changed at the file level, so the summary
196
209
  and dependency layers are worth printing even though the line diffs come out
197
- empty. Deleting an archive directory by hand is the usual cause.
210
+ empty. Deleting an archive directory by hand is one cause; an index pointing
211
+ somewhere the directory no longer is, after the archive was moved or copied
212
+ from another machine, is the other — which is why the warning names
213
+ ``lw reindex``, the command that rebuilds the index from what is on disk.
198
214
  """
199
215
  a = _version_or_fail(db, function_row["id"], a_seq)
200
216
  b = _version_or_fail(db, function_row["id"], b_seq)
@@ -203,7 +219,8 @@ def _build_diff(db: Database, store: Store, cfg: Config, function_row, a_seq: in
203
219
  if not path.exists():
204
220
  err_console.print(
205
221
  f"[yellow]warning:[/yellow] code for v{seq:04d} is missing at {path}; "
206
- "line diffs for it will be empty"
222
+ "line diffs for it will be empty. If the archive was moved or copied "
223
+ "here, `lw reindex` re-points the index at what is on disk."
207
224
  )
208
225
  return diff_from_index(db, store, cfg.diff, function_row["name"], a, b,
209
226
  include_vendor=include_vendor, compute_diffs=compute_diffs)
@@ -1128,8 +1145,15 @@ def rename(
1128
1145
  if new_dir.exists():
1129
1146
  _fail(f"{new_dir} already exists on disk; move it aside first")
1130
1147
  old_dir.rename(new_dir)
1148
+ # back-compat: re-point the index by rebuilding each path from where the
1149
+ # directory now is, rather than by patching the stored string. A
1150
+ # `functions/<old slug>/` replacement silently matched nothing in an
1151
+ # archive an older release wrote on Windows, where the separator is a
1152
+ # backslash — and a version left pointing at the pre-rename path reads
1153
+ # as missing from then on, which is a diff with no lines in it.
1154
+ new_versions = store.versions_dir(new_slug)
1131
1155
  for version in db.list_versions(int(row["id"])):
1132
- updated = version["dir"].replace(f"functions/{old_slug}/", f"functions/{new_slug}/", 1)
1156
+ updated = store.relative(new_versions / _stored_dirname(version["dir"]))
1133
1157
  db.conn.execute("UPDATE versions SET dir = ? WHERE id = ?", (updated, version["id"]))
1134
1158
  if old_slug != new_slug:
1135
1159
  # The mirror lives outside the function directory, so it does not move
@@ -1209,7 +1233,7 @@ def merge(
1209
1233
  stored = store.resolve_version_dir(version["dir"])
1210
1234
  if stored.exists():
1211
1235
  continue
1212
- candidate = dst_versions / Path(version["dir"]).name
1236
+ candidate = dst_versions / _stored_dirname(version["dir"])
1213
1237
  if candidate.exists():
1214
1238
  db.conn.execute(
1215
1239
  "UPDATE versions SET dir = ? WHERE id = ?",
@@ -38,7 +38,15 @@ class FileRecord:
38
38
 
39
39
  @classmethod
40
40
  def from_row(cls, row: Any) -> FileRecord:
41
- """Build a record from an index row, filling in defaults for null columns."""
41
+ """Build a record from an index row, filling in defaults for null columns.
42
+
43
+ The defaults are not decoration: a row reindexed from a manifest written
44
+ before ``lang`` and ``mode`` were recorded has NULL in those columns, and
45
+ the diff would rather call the file ``text`` at 644 than fail on it.
46
+ """
47
+ # back-compat: `or "text"` / `or 0o644` cover columns an older manifest
48
+ # cannot fill. Drop them and reindexing an old archive builds rows that
49
+ # crash the diff instead of one that is merely vaguer than it could be.
42
50
  return cls(
43
51
  path=row["path"], size=int(row["size"]), sha256=row["sha256"],
44
52
  is_text=bool(row["is_text"]), is_vendor=bool(row["is_vendor"]),
@@ -56,8 +64,9 @@ class FileChange:
56
64
 
57
65
  ``diff_lines`` may be empty even for a real modification —
58
66
  ``skipped_reason`` then says why (binary, too large, ignored by config,
59
- whitespace only), so the renderer can explain the absence rather than imply
60
- nothing changed. Two of those absences are not refusals but better answers:
67
+ whitespace only, missing from the archive), so the renderer can explain the
68
+ absence rather than imply nothing changed. Two of those absences are not
69
+ refusals but better answers:
61
70
  a whitespace-only change is fully described by its label, and a file with no
62
71
  usable lines is described by ``word_edits`` instead. See :func:`_fill_diff`.
63
72
  """
@@ -87,6 +96,12 @@ class FileChange:
87
96
  #: The changed runs of a ``long_lines`` file, with enough text either side to
88
97
  #: place them. Empty for every ordinary file.
89
98
  word_edits: list[WordEdit] = field(default_factory=list)
99
+ #: True when the index lists the file but the archived version directory does
100
+ #: not hold it, so there was nothing to diff. Unlike every other reason a
101
+ #: hunk is absent, this one is not about the file at all — the archive is
102
+ #: incomplete, or the index is pointing at the wrong place — so the
103
+ #: renderers say what to do about it rather than only what happened.
104
+ missing: bool = False
90
105
 
91
106
  @property
92
107
  def is_vendor(self) -> bool:
@@ -146,6 +161,7 @@ class FileChange:
146
161
  "is_vendor": self.is_vendor,
147
162
  "whitespace_only": self.whitespace_only,
148
163
  "long_lines": self.long_lines,
164
+ "missing": self.missing,
149
165
  "word_edits": [e.as_dict() for e in self.word_edits],
150
166
  }
151
167
 
@@ -764,6 +780,21 @@ def _whitespace_key(text: str) -> tuple[str, ...]:
764
780
  return tuple(" ".join(line.split()) for line in text.splitlines() if line.strip())
765
781
 
766
782
 
783
+ def _absent(root: Path, record: FileRecord | None) -> bool:
784
+ """True when the index lists this file but the archive no longer holds it.
785
+
786
+ :func:`~lambda_watcher.utils.read_text` answers None both for bytes that are
787
+ not text and for a file it could not open at all, and the second is by far
788
+ the likelier of the two here: the index is derived from disk, so a file it
789
+ lists was readable when it was written. A version directory that has been
790
+ deleted, moved, or restored under a path the index does not know about is
791
+ what actually produces it — see :meth:`lambda_watcher.store.Store.relative`.
792
+ Telling the reader "not decodable as text" for that sends them looking for an
793
+ encoding problem in a file that is simply not there.
794
+ """
795
+ return record is not None and not (root / record.path).is_file()
796
+
797
+
767
798
  def _fill_diff(change: FileChange, old_root: Path, new_root: Path, cfg: DiffConfig) -> None:
768
799
  """Work out what changed inside one file and write it onto the change.
769
800
 
@@ -792,7 +823,10 @@ def _fill_diff(change: FileChange, old_root: Path, new_root: Path, cfg: DiffConf
792
823
  Everything else, from :func:`difflib.unified_diff`.
793
824
 
794
825
  Before any of that, a file can be refused outright — binary, over
795
- ``max_diff_file_kb``, or not decodable — and ``skipped_reason`` says which.
826
+ ``max_diff_file_kb``, not decodable, or missing from the archive — and
827
+ ``skipped_reason`` says which. The last of those is not a property of the
828
+ file's bytes but of the archive around it, so it is told apart by
829
+ :func:`_absent` rather than lumped in with the encodings.
796
830
  Line counts stay at zero on every route but the last: the counts describe a
797
831
  hunk, and where there is no hunk ``+3/-3`` is the noise the route exists to
798
832
  remove.
@@ -813,7 +847,10 @@ def _fill_diff(change: FileChange, old_root: Path, new_root: Path, cfg: DiffConf
813
847
  old_text = read_text(old_root / old.path) if old else ""
814
848
  new_text = read_text(new_root / new.path) if new else ""
815
849
  if old_text is None or new_text is None:
816
- change.skipped_reason = "not decodable as text"
850
+ change.missing = _absent(old_root, old) or _absent(new_root, new)
851
+ change.skipped_reason = (
852
+ "missing from the archive" if change.missing else "not decodable as text"
853
+ )
817
854
  return
818
855
 
819
856
  if old_text and new_text and old_text != new_text: