cv-frames 0.0.3__tar.gz → 0.0.4__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 (27) hide show
  1. {cv_frames-0.0.3 → cv_frames-0.0.4}/.github/workflows/tests.yml +3 -3
  2. cv_frames-0.0.4/.pre-commit-config.yaml +24 -0
  3. {cv_frames-0.0.3/cv_frames.egg-info → cv_frames-0.0.4}/PKG-INFO +20 -2
  4. cv_frames-0.0.3/PKG-INFO → cv_frames-0.0.4/README.rst +10 -11
  5. cv_frames-0.0.3/README.rst → cv_frames-0.0.4/cv_frames.egg-info/PKG-INFO +29 -0
  6. {cv_frames-0.0.3 → cv_frames-0.0.4}/cv_frames.egg-info/SOURCES.txt +2 -0
  7. cv_frames-0.0.4/cv_frames.egg-info/requires.txt +17 -0
  8. cv_frames-0.0.4/cv_frames.egg-info/scm_file_list.json +19 -0
  9. cv_frames-0.0.4/cv_frames.egg-info/scm_version.json +8 -0
  10. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/cli.py +4 -3
  11. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/iterate.py +54 -20
  12. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/web/server.py +12 -9
  13. {cv_frames-0.0.3 → cv_frames-0.0.4}/pyproject.toml +33 -12
  14. {cv_frames-0.0.3 → cv_frames-0.0.4}/requirements.txt +1 -1
  15. {cv_frames-0.0.3 → cv_frames-0.0.4}/tests/test_iterate.py +2 -3
  16. cv_frames-0.0.3/.pre-commit-config.yaml +0 -39
  17. cv_frames-0.0.3/cv_frames.egg-info/requires.txt +0 -6
  18. {cv_frames-0.0.3 → cv_frames-0.0.4}/.github/workflows/publish.yml +0 -0
  19. {cv_frames-0.0.3 → cv_frames-0.0.4}/.gitignore +0 -0
  20. {cv_frames-0.0.3 → cv_frames-0.0.4}/cv_frames.egg-info/dependency_links.txt +0 -0
  21. {cv_frames-0.0.3 → cv_frames-0.0.4}/cv_frames.egg-info/entry_points.txt +0 -0
  22. {cv_frames-0.0.3 → cv_frames-0.0.4}/cv_frames.egg-info/top_level.txt +0 -0
  23. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/__init__.py +0 -0
  24. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/web/__init__.py +0 -0
  25. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/web/client.py +0 -0
  26. {cv_frames-0.0.3 → cv_frames-0.0.4}/cvframes/web/static/index.html +0 -0
  27. {cv_frames-0.0.3 → cv_frames-0.0.4}/setup.cfg +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
 
12
12
  strategy:
13
13
  matrix:
14
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
14
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v3
@@ -24,11 +24,11 @@ jobs:
24
24
  - name: Install dependencies
25
25
  run: |
26
26
  pip install -r requirements.txt
27
- pip install -e .
27
+ pip install -e .[opencv]
28
28
 
29
29
  - name: Run the tests
30
30
  run: |
31
- PYTHONFAULTHANDLER=2 pytest --cov=cvframes --cov-report term-missing tests/
31
+ PYTHONFAULTHANDLER=2 pytest tests/
32
32
 
33
33
  - name: Run pre-commit checks
34
34
  uses: pre-commit/action@v3.0.1
@@ -0,0 +1,24 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-added-large-files
6
+ args: ["--maxkb=1000"]
7
+ - id: check-json
8
+ - id: check-docstring-first
9
+ - id: end-of-file-fixer
10
+ - id: trailing-whitespace
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.16.3
14
+ hooks:
15
+ - id: ruff-check
16
+ args: ["--fix"]
17
+ - id: ruff-format
18
+
19
+ - repo: https://github.com/pre-commit/mirrors-mypy
20
+ rev: v1.14.1
21
+ hooks:
22
+ - id: mypy
23
+ additional_dependencies: ["types-PyYAML"]
24
+ args: ["--config-file", "pyproject.toml"]
@@ -1,13 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cv-frames
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Read frames from OpenCV like humans
5
+ Requires-Python: >=3.10
5
6
  Description-Content-Type: text/x-rst
6
- Requires-Dist: opencv-python
7
7
  Requires-Dist: numpy
8
8
  Requires-Dist: click
9
9
  Provides-Extra: web
10
10
  Requires-Dist: aiohttp; extra == "web"
11
+ Provides-Extra: opencv
12
+ Requires-Dist: opencv-python; extra == "opencv"
13
+ Provides-Extra: opencv-contrib
14
+ Requires-Dist: opencv-contrib-python; extra == "opencv-contrib"
15
+ Provides-Extra: opencv-headless
16
+ Requires-Dist: opencv-python-headless; extra == "opencv-headless"
17
+ Provides-Extra: opencv-contrib-headless
18
+ Requires-Dist: opencv-contrib-python-headless; extra == "opencv-contrib-headless"
11
19
 
12
20
  cv-frames
13
21
  =========
@@ -34,6 +42,16 @@ Install via pip:
34
42
 
35
43
  pip install cv-frames
36
44
 
45
+ If you install ``cv-frames`` standalone, you need to specify an OpenCV backend:
46
+
47
+ .. code-block:: bash
48
+
49
+ pip install cv-frames[opencv]
50
+
51
+ If ``cv-frames`` is part of a project that already depends on OpenCV
52
+ (e.g. ``opencv-python`` or ``opencv-contrib-python`` is listed in your
53
+ ``requirements.txt``), no optional dependency is needed.
54
+
37
55
  ----
38
56
 
39
57
  Command Line Usage
@@ -1,14 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: cv-frames
3
- Version: 0.0.3
4
- Summary: Read frames from OpenCV like humans
5
- Description-Content-Type: text/x-rst
6
- Requires-Dist: opencv-python
7
- Requires-Dist: numpy
8
- Requires-Dist: click
9
- Provides-Extra: web
10
- Requires-Dist: aiohttp; extra == "web"
11
-
12
1
  cv-frames
13
2
  =========
14
3
 
@@ -34,6 +23,16 @@ Install via pip:
34
23
 
35
24
  pip install cv-frames
36
25
 
26
+ If you install ``cv-frames`` standalone, you need to specify an OpenCV backend:
27
+
28
+ .. code-block:: bash
29
+
30
+ pip install cv-frames[opencv]
31
+
32
+ If ``cv-frames`` is part of a project that already depends on OpenCV
33
+ (e.g. ``opencv-python`` or ``opencv-contrib-python`` is listed in your
34
+ ``requirements.txt``), no optional dependency is needed.
35
+
37
36
  ----
38
37
 
39
38
  Command Line Usage
@@ -1,3 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: cv-frames
3
+ Version: 0.0.4
4
+ Summary: Read frames from OpenCV like humans
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/x-rst
7
+ Requires-Dist: numpy
8
+ Requires-Dist: click
9
+ Provides-Extra: web
10
+ Requires-Dist: aiohttp; extra == "web"
11
+ Provides-Extra: opencv
12
+ Requires-Dist: opencv-python; extra == "opencv"
13
+ Provides-Extra: opencv-contrib
14
+ Requires-Dist: opencv-contrib-python; extra == "opencv-contrib"
15
+ Provides-Extra: opencv-headless
16
+ Requires-Dist: opencv-python-headless; extra == "opencv-headless"
17
+ Provides-Extra: opencv-contrib-headless
18
+ Requires-Dist: opencv-contrib-python-headless; extra == "opencv-contrib-headless"
19
+
1
20
  cv-frames
2
21
  =========
3
22
 
@@ -23,6 +42,16 @@ Install via pip:
23
42
 
24
43
  pip install cv-frames
25
44
 
45
+ If you install ``cv-frames`` standalone, you need to specify an OpenCV backend:
46
+
47
+ .. code-block:: bash
48
+
49
+ pip install cv-frames[opencv]
50
+
51
+ If ``cv-frames`` is part of a project that already depends on OpenCV
52
+ (e.g. ``opencv-python`` or ``opencv-contrib-python`` is listed in your
53
+ ``requirements.txt``), no optional dependency is needed.
54
+
26
55
  ----
27
56
 
28
57
  Command Line Usage
@@ -10,6 +10,8 @@ cv_frames.egg-info/SOURCES.txt
10
10
  cv_frames.egg-info/dependency_links.txt
11
11
  cv_frames.egg-info/entry_points.txt
12
12
  cv_frames.egg-info/requires.txt
13
+ cv_frames.egg-info/scm_file_list.json
14
+ cv_frames.egg-info/scm_version.json
13
15
  cv_frames.egg-info/top_level.txt
14
16
  cvframes/__init__.py
15
17
  cvframes/cli.py
@@ -0,0 +1,17 @@
1
+ numpy
2
+ click
3
+
4
+ [opencv]
5
+ opencv-python
6
+
7
+ [opencv-contrib]
8
+ opencv-contrib-python
9
+
10
+ [opencv-contrib-headless]
11
+ opencv-contrib-python-headless
12
+
13
+ [opencv-headless]
14
+ opencv-python-headless
15
+
16
+ [web]
17
+ aiohttp
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ ".github/workflows/publish.yml",
4
+ ".github/workflows/tests.yml",
5
+ ".gitignore",
6
+ ".pre-commit-config.yaml",
7
+ "README.rst",
8
+ "cvframes/__init__.py",
9
+ "cvframes/cli.py",
10
+ "cvframes/iterate.py",
11
+ "cvframes/web/__init__.py",
12
+ "cvframes/web/client.py",
13
+ "cvframes/web/server.py",
14
+ "cvframes/web/static/index.html",
15
+ "pyproject.toml",
16
+ "requirements.txt",
17
+ "tests/test_iterate.py"
18
+ ]
19
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.0.4",
3
+ "distance": 0,
4
+ "node": "g1a4274555c3fe3cef38d9dbff907b15a6b300752",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-24"
8
+ }
@@ -13,9 +13,10 @@ def main():
13
13
 
14
14
  @main.command(name="show")
15
15
  @click.argument("filepath", type=click.Path(exists=True))
16
- def show(filepath):
17
- for i, (_, frame) in enumerate(iterate(filepath)):
18
- print("Frame:", i)
16
+ @click.option("--start-frame", type=int, default=0)
17
+ def show(filepath, start_frame: int):
18
+ for i, (_, frame) in enumerate(iterate(filepath, start_frame=start_frame)):
19
+ click.echo(f"Frame: {i + start_frame}")
19
20
  cv2.imshow("cvframes-show", frame)
20
21
 
21
22
  # Wait for a key press and check if it's 'q' to exit
@@ -1,5 +1,6 @@
1
+ from collections.abc import Callable, Generator
1
2
  from pathlib import Path
2
- from typing import Callable, Generator, Optional, Tuple, TypeVar, Union
3
+ from typing import TypeVar
3
4
 
4
5
  import cv2
5
6
  import numpy as np
@@ -8,34 +9,67 @@ T = TypeVar("T")
8
9
 
9
10
 
10
11
  class IOCapture:
11
- def __init__(self, source: Union[str, Path], oname: Union[str, Path] = ""):
12
+ def __init__(
13
+ self,
14
+ source: str | Path,
15
+ oname: str | Path = "",
16
+ oshape: tuple[int, int] | None = None,
17
+ ):
12
18
  self.icap = cv2.VideoCapture(str(source))
13
- self.ocap = (
14
- cv2.VideoWriter(
15
- str(oname),
19
+ self.oname = str(oname) if oname else None
20
+ self.oshape = oshape
21
+ self._ocap: cv2.VideoWriter | None = None
22
+
23
+ @property
24
+ def ocap(self) -> cv2.VideoWriter | None:
25
+ if self.oname is None:
26
+ return None
27
+
28
+ if self.oshape is None:
29
+ return None
30
+
31
+ if self._ocap is None:
32
+ self._ocap = cv2.VideoWriter(
33
+ self.oname,
16
34
  cv2.VideoWriter_fourcc(*"mp4v"),
17
35
  self.icap.get(cv2.CAP_PROP_FPS),
18
- (
19
- int(self.icap.get(cv2.CAP_PROP_FRAME_WIDTH)),
20
- int(self.icap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
21
- ),
36
+ self.oshape,
22
37
  )
23
- if oname
24
- else None
25
- )
38
+
39
+ if not self._ocap.isOpened():
40
+ raise RuntimeError(
41
+ f"Cannot open output video file: {self.oname}"
42
+ )
43
+
44
+ return self._ocap
26
45
 
27
46
  def is_opened(self) -> bool:
28
47
  return self.icap.isOpened()
29
48
 
30
- def read(self) -> Tuple[bool, np.ndarray]:
49
+ def read(self) -> tuple[bool, np.ndarray]:
31
50
  return self.icap.read()
32
51
 
33
52
  def write(self, frame: np.ndarray) -> None:
53
+ height, width = frame.shape[:2]
54
+ shape = (width, height)
55
+
56
+ if self.oshape is None:
57
+ self.oshape = shape
58
+ return
59
+
60
+ if shape != self.oshape:
61
+ raise ValueError(
62
+ "All output frames must have the same resolution. "
63
+ f"Expected {self.oshape[0]}x{self.oshape[1]}, "
64
+ f"got {width}x{height}."
65
+ )
66
+
34
67
  if self.ocap is not None:
35
68
  self.ocap.write(frame)
36
69
 
37
70
  def release(self) -> None:
38
71
  self.icap.release()
72
+
39
73
  if self.ocap is not None:
40
74
  self.ocap.release()
41
75
 
@@ -45,11 +79,11 @@ class IOCapture:
45
79
 
46
80
  def iterate_generic(
47
81
  ipath: Path,
48
- opath: Optional[Path],
82
+ opath: Path | None,
49
83
  start_frame: int,
50
84
  stop_frame: int,
51
85
  process_frames: Callable[[np.ndarray], T],
52
- ) -> Generator[Tuple[IOCapture, T], None, None]:
86
+ ) -> Generator[tuple[IOCapture, T], None, None]:
53
87
  capture = IOCapture(str(ipath), oname=opath or "")
54
88
  capture.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
55
89
  count = start_frame
@@ -73,10 +107,10 @@ def iterate_generic(
73
107
 
74
108
  def iterate(
75
109
  ipath: Path,
76
- opath: Optional[Path] = None,
110
+ opath: Path | None = None,
77
111
  start_frame: int = -1,
78
112
  stop_frame: int = -1,
79
- ) -> Generator[Tuple[IOCapture, np.ndarray], None, None]:
113
+ ) -> Generator[tuple[IOCapture, np.ndarray], None, None]:
80
114
  return iterate_generic(
81
115
  ipath,
82
116
  opath,
@@ -88,11 +122,11 @@ def iterate(
88
122
 
89
123
  def iterate_sbs(
90
124
  ipath: Path,
91
- opath: Optional[Path] = None,
125
+ opath: Path | None = None,
92
126
  start_frame: int = -1,
93
127
  stop_frame: int = -1,
94
- ) -> Generator[Tuple[IOCapture, Tuple[np.ndarray, np.ndarray]], None, None]:
95
- def processor(frame: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
128
+ ) -> Generator[tuple[IOCapture, tuple[np.ndarray, np.ndarray]], None, None]:
129
+ def processor(frame: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
96
130
  _, width, _ = frame.shape
97
131
  mid = width // 2
98
132
  return frame[:, :mid, :], frame[:, mid:, :]
@@ -24,6 +24,13 @@ MAPPING = {
24
24
  }
25
25
 
26
26
 
27
+ async def send_frame(ws, frame: bytes, to_remove: set):
28
+ try:
29
+ await ws.send_bytes(frame)
30
+ except Exception: # noqa: BLE001 - a broken client must not stop the rest
31
+ to_remove.add(ws)
32
+
33
+
27
34
  async def broadcast_task(
28
35
  frame_queue: asyncio.Queue,
29
36
  clients: set,
@@ -39,13 +46,9 @@ async def broadcast_task(
39
46
  to_remove.add(ws)
40
47
  continue
41
48
 
42
- async def send(ws):
43
- try:
44
- await ws.send_bytes(frame)
45
- except Exception:
46
- to_remove.add(ws)
47
-
48
- tasks.append(asyncio.create_task(send(ws)))
49
+ tasks.append(
50
+ asyncio.create_task(send_frame(ws, frame, to_remove))
51
+ )
49
52
  await asyncio.gather(*tasks, return_exceptions=True)
50
53
  for ws in to_remove:
51
54
  clients.remove(ws)
@@ -77,7 +80,7 @@ async def handle_tcp_connection(
77
80
  buffer = buffer[end:]
78
81
  await frame_queue.put(jpg)
79
82
 
80
- except Exception as e:
83
+ except Exception as e: # noqa: BLE001 - report and close the socket
81
84
  print(f"[TCP] Error: {e}")
82
85
  finally:
83
86
  writer.close()
@@ -128,7 +131,7 @@ async def websocket_handler(request):
128
131
  print(f"[WS] Error: {ws.exception()}")
129
132
  break
130
133
 
131
- except Exception as e:
134
+ except Exception as e: # noqa: BLE001 - report and drop the client
132
135
  print(f"[WS] Receive error: {e}")
133
136
 
134
137
  finally:
@@ -1,22 +1,39 @@
1
- [tool.black]
1
+ [tool.ruff]
2
2
  line-length = 79
3
+ target-version = "py310"
3
4
 
4
- [tool.isort]
5
- profile = "black"
6
- line_length = 79
5
+ [tool.ruff.format]
6
+ quote-style = "double"
7
+ line-ending = "lf"
8
+ indent-style = "space"
9
+ skip-magic-trailing-comma = false
10
+
11
+ [tool.ruff.lint]
12
+ extend-select = [
13
+ "E", # pycodestyle
14
+ "F", # pyflakes
15
+ "I", # isort
16
+ "UP", # pyupgrade
17
+ # "B", # bugbear
18
+ # "C90", # mccabe
19
+ # "D", # pydocstyle
20
+ "ERA", # eradicate
21
+ ]
22
+ ignore = [
23
+ "E203",
24
+ "B008", # Do not perform function call
25
+ "RUF009", # Don not perform partials
26
+ # "W503",
27
+ ]
28
+
29
+ [tool.ruff.lint.isort]
30
+ combine-as-imports = true
7
31
 
8
32
  [tool.mypy]
9
33
  python_version = "3.10"
10
34
  strict = false
11
35
  ignore_missing_imports = true
12
36
 
13
- [tool.flake8]
14
- max-line-length = 79
15
- ignore = [
16
- "E203", # Whitespace before ':'
17
- "W503", # Line break before binary operator
18
- ]
19
-
20
37
  [tool.pytest.ini_options]
21
38
  addopts = "--cov=cvframes --cov-report=term-missing"
22
39
 
@@ -30,15 +47,19 @@ name = "cv-frames"
30
47
  dynamic = ["version"]
31
48
  description = "Read frames from OpenCV like humans"
32
49
  readme = "README.rst"
50
+ requires-python = ">=3.10"
33
51
 
34
52
  dependencies = [
35
- "opencv-python",
36
53
  "numpy",
37
54
  "click",
38
55
  ]
39
56
 
40
57
  [project.optional-dependencies]
41
58
  web = ["aiohttp"]
59
+ opencv = ["opencv-python"]
60
+ opencv-contrib = ["opencv-contrib-python"]
61
+ opencv-headless = ["opencv-python-headless"]
62
+ opencv-contrib-headless = ["opencv-contrib-python-headless"]
42
63
 
43
64
  [project.scripts]
44
65
  cv-frames = "cvframes.cli:main"
@@ -1,3 +1,3 @@
1
1
  pytest
2
- pytest-coverage
2
+ pytest-cov
3
3
  pre-commit
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
 
4
3
  import cv2
5
4
  import numpy as np
@@ -31,7 +30,7 @@ def video(tmp_path: Path):
31
30
  Path("output.mp4"),
32
31
  ],
33
32
  )
34
- def test_iterate(video: Path, opath: Optional[Path]):
33
+ def test_iterate(video: Path, opath: Path | None):
35
34
  # sourcery skip: no-loop-in-tests
36
35
  for capture, frame in iterate(video, opath=opath):
37
36
  capture.write(frame)
@@ -46,7 +45,7 @@ def test_iterate(video: Path, opath: Optional[Path]):
46
45
  Path("output.mp4"),
47
46
  ],
48
47
  )
49
- def test_iterate_sbs(video: Path, opath: Optional[Path]):
48
+ def test_iterate_sbs(video: Path, opath: Path | None):
50
49
  # sourcery skip: no-loop-in-tests
51
50
  for capture, (lframe, rframe) in iterate_sbs(
52
51
  Path("input.mp4"), opath=opath
@@ -1,39 +0,0 @@
1
- repos:
2
- - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v4.0.1
4
- hooks:
5
- - id: check-added-large-files
6
- args: ["--maxkb=1000"]
7
- - id: check-json
8
- - id: check-docstring-first
9
- - id: end-of-file-fixer
10
- - id: trailing-whitespace
11
-
12
- - repo: https://github.com/pycqa/isort
13
- rev: 5.12.0
14
- hooks:
15
- - id: isort
16
- name: isort (python)
17
- args: ["--settings-path", "pyproject.toml"]
18
-
19
-
20
- - repo: https://github.com/psf/black
21
- rev: 22.3.0
22
- hooks:
23
- - id: black
24
- args: ["--config", "pyproject.toml"]
25
-
26
- - repo: https://github.com/pre-commit/mirrors-mypy
27
- rev: v0.921
28
- hooks:
29
- - id: mypy
30
- additional_dependencies: ["types-PyYAML"]
31
- args: ["--config-file", "pyproject.toml"]
32
-
33
-
34
- - repo: https://github.com/pycqa/flake8
35
- rev: 7.1.2
36
- hooks:
37
- - id: flake8
38
- # args: ["--config", "pyproject.toml"]
39
- args: ["--max-line-length=79"]
@@ -1,6 +0,0 @@
1
- opencv-python
2
- numpy
3
- click
4
-
5
- [web]
6
- aiohttp
File without changes
File without changes