mifpy 1.0.0__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.
mifpy/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .core import MIFPackage
2
+ from .exceptions import MIFValidationError
3
+ from .srt import parse_srt, format_srt
4
+ from .utils import ms_to_timestamp, timestamp_to_ms
5
+
6
+ __all__ = [
7
+ "MIFPackage",
8
+ "MIFValidationError",
9
+ "parse_srt",
10
+ "format_srt",
11
+ "ms_to_timestamp",
12
+ "timestamp_to_ms",
13
+ ]
mifpy/cli.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ from .core import MIFPackage
9
+
10
+
11
+ def inspect_command(path: Path) -> int:
12
+ package = MIFPackage.load(path)
13
+ print(f"MIF package: {path}")
14
+ print("Metadata:")
15
+ for key, value in package.get_metadata().items():
16
+ print(f" {key}: {value}")
17
+ print(f"Subtitles length: {len(package.get_subtitles())} characters")
18
+ print(f"Appearances: {len(package.get_appearances().get('appearances', []))}")
19
+ print(f"Content files: {len(package.list_content_files())}")
20
+ return 0
21
+
22
+
23
+ def extract_command(source: Path, destination: Path) -> int:
24
+ destination.mkdir(parents=True, exist_ok=True)
25
+ with zipfile.ZipFile(source, "r") as archive:
26
+ archive.extractall(destination)
27
+ print(f"Extracted {source} to {destination}")
28
+ return 0
29
+
30
+
31
+ def create_command(path: Path) -> int:
32
+ meta = {"title": "Example Movie", "year": 2025, "runtime_ms": 90_000}
33
+ subtitles = "1\n00:00:00,000 --> 00:00:03,000\nHello from MIF!\n"
34
+ appearances = {
35
+ "actors": {
36
+ "example_actor": {
37
+ "name": "Example Actor",
38
+ "image": "content/example.png",
39
+ }
40
+ },
41
+ "appearances": [
42
+ {"actor": "example_actor", "start_ms": 0, "end_ms": 3000}
43
+ ],
44
+ }
45
+ package = MIFPackage.create(meta=meta, subtitles=subtitles, appearances=appearances)
46
+ package.add_content_file("example.png", b"")
47
+ package.save(path)
48
+ print(f"Created example MIF file at {path}")
49
+ return 0
50
+
51
+
52
+ def main(argv: list[str] | None = None) -> int:
53
+ parser = argparse.ArgumentParser(prog="mifpy", description="Movie Information Format toolkit")
54
+ subparsers = parser.add_subparsers(dest="command", required=True)
55
+
56
+ inspect_parser = subparsers.add_parser("inspect", help="Display MIF metadata and structure")
57
+ inspect_parser.add_argument("source", type=Path, help="Path to the .mif file")
58
+
59
+ extract_parser = subparsers.add_parser("extract", help="Extract the contents of a MIF archive")
60
+ extract_parser.add_argument("source", type=Path, help="Path to the .mif file")
61
+ extract_parser.add_argument("destination", type=Path, help="Output folder")
62
+
63
+ create_parser = subparsers.add_parser("create", help="Create a template MIF file")
64
+ create_parser.add_argument("target", type=Path, help="Path to create the .mif file")
65
+
66
+ args = parser.parse_args(argv)
67
+
68
+ if args.command == "inspect":
69
+ return inspect_command(args.source)
70
+ if args.command == "extract":
71
+ return extract_command(args.source, args.destination)
72
+ if args.command == "create":
73
+ return create_command(args.target)
74
+
75
+ parser.print_help()
76
+ return 1
77
+
78
+
79
+ if __name__ == "__main__":
80
+ raise SystemExit(main())
mifpy/core.py ADDED
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import zipfile
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .exceptions import MIFValidationError
9
+ from .parser import parse_appearances, parse_manifest, parse_meta
10
+ from .srt import parse_srt
11
+ from .writer import build_manifest_text, build_meta_text, build_appearances_text
12
+
13
+
14
+ class MIFPackage:
15
+ MANIFEST_NAME = "manifest.ini"
16
+ META_NAME = "meta.ini"
17
+ SUBTITLES_NAME = "subtitles.srt"
18
+ APPEARANCES_NAME = "appearances.json"
19
+ DEFAULT_CONTENT_DIR = "content/"
20
+
21
+ def __init__(
22
+ self,
23
+ meta: dict[str, Any],
24
+ subtitles: str,
25
+ appearances: dict[str, Any],
26
+ content_files: dict[str, bytes] | None = None,
27
+ content_dir: str | None = None,
28
+ ):
29
+ self._meta = meta
30
+ self._subtitles = subtitles
31
+ self._appearances = appearances
32
+ self._content_files = content_files or {}
33
+ self._content_dir = content_dir or self.DEFAULT_CONTENT_DIR
34
+ self._validate()
35
+
36
+ @classmethod
37
+ def create(
38
+ cls,
39
+ meta: dict[str, Any],
40
+ subtitles: str,
41
+ appearances: dict[str, Any],
42
+ ) -> "MIFPackage":
43
+ return cls(meta=meta, subtitles=subtitles, appearances=appearances, content_files={})
44
+
45
+ @classmethod
46
+ def load(cls, path: str | Path) -> "MIFPackage":
47
+ path_obj = Path(path)
48
+ if not path_obj.exists():
49
+ raise FileNotFoundError(f"MIF file not found: {path}")
50
+
51
+ with zipfile.ZipFile(path_obj, "r") as archive:
52
+ namelist = archive.namelist()
53
+ if cls.MANIFEST_NAME not in namelist:
54
+ raise MIFValidationError("manifest.ini is required in a MIF archive")
55
+
56
+ manifest_text = archive.read(cls.MANIFEST_NAME).decode("utf-8")
57
+ manifest = parse_manifest(manifest_text)
58
+ files = manifest["files"]
59
+ content_dir = files["content_dir"]
60
+
61
+ def read_required(file_key: str, name: str) -> str:
62
+ if name not in namelist:
63
+ raise MIFValidationError(f"{file_key} declared in manifest.ini is missing from the archive: {name}")
64
+ return archive.read(name).decode("utf-8")
65
+
66
+ meta_text = read_required("meta", files["meta"])
67
+ subtitles_text = read_required("subtitles", files["subtitles"])
68
+ appearances_text = read_required("appearances", files["appearances"])
69
+
70
+ metadata = parse_meta(meta_text)
71
+ appearances = parse_appearances(appearances_text, content_files=[name for name in namelist])
72
+
73
+ content_files: dict[str, bytes] = {}
74
+ for member in namelist:
75
+ if not member.endswith("/") and member.startswith(content_dir):
76
+ relative = member[len(content_dir) :]
77
+ if relative:
78
+ content_files[relative] = archive.read(member)
79
+
80
+ for actor_id, actor in appearances["actors"].items():
81
+ image_path = actor.get("image")
82
+ if image_path:
83
+ normalized = image_path.replace("\\", "/").lstrip("/")
84
+ if normalized not in namelist:
85
+ raise MIFValidationError(
86
+ f"actor '{actor_id}' references missing content file '{image_path}'"
87
+ )
88
+
89
+ return cls(
90
+ meta=metadata,
91
+ subtitles=subtitles_text,
92
+ appearances=appearances,
93
+ content_files=content_files,
94
+ content_dir=content_dir,
95
+ )
96
+
97
+ def save(self, path: str | Path) -> None:
98
+ self._validate_image_references()
99
+ path_obj = Path(path)
100
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
101
+
102
+ with zipfile.ZipFile(path_obj, "w", compression=zipfile.ZIP_DEFLATED) as archive:
103
+ archive.writestr(self.MANIFEST_NAME, build_manifest_text(self._content_dir))
104
+ archive.writestr(self.META_NAME, build_meta_text(self._meta))
105
+ archive.writestr(self.SUBTITLES_NAME, self._subtitles)
106
+ archive.writestr(self.APPEARANCES_NAME, build_appearances_text(self._appearances))
107
+
108
+ for relative_path, content in self._content_files.items():
109
+ normalized = relative_path.replace("\\", "/").lstrip("/")
110
+ if ".." in normalized.split("/"):
111
+ raise ValueError("Content file paths must not traverse parent directories")
112
+ archive_path = f"{self._content_dir}{normalized}"
113
+ archive.writestr(archive_path, content)
114
+
115
+ def _validate_image_references(self) -> None:
116
+ content_paths = {f"{self._content_dir}{name}" for name in self._content_files}
117
+ for actor_id, actor in self._appearances.get("actors", {}).items():
118
+ image_path = actor.get("image")
119
+ if image_path:
120
+ normalized = image_path.replace("\\", "/").lstrip("/")
121
+ if normalized not in content_paths:
122
+ raise MIFValidationError(
123
+ f"actor '{actor_id}' references missing content file '{image_path}'"
124
+ )
125
+
126
+ def get_metadata(self) -> dict[str, Any]:
127
+ return dict(self._meta)
128
+
129
+ def get_subtitles(self) -> str:
130
+ return self._subtitles
131
+
132
+ def get_appearances(self) -> dict[str, Any]:
133
+ return {
134
+ "actors": dict(self._appearances["actors"]),
135
+ "appearances": [dict(item) for item in self._appearances["appearances"]],
136
+ }
137
+
138
+ def list_content_files(self) -> list[str]:
139
+ return sorted(self._content_files.keys())
140
+
141
+ def add_content_file(self, path: str, data: bytes) -> None:
142
+ if not isinstance(path, str) or not path.strip():
143
+ raise ValueError("Content file path must be a non-empty string")
144
+ if not isinstance(data, (bytes, bytearray)):
145
+ raise ValueError("Content file data must be bytes")
146
+
147
+ normalized = path.replace("\\", "/").lstrip("/")
148
+ if ".." in normalized.split("/"):
149
+ raise ValueError("Content file paths must not traverse parent directories")
150
+ self._content_files[normalized] = bytes(data)
151
+
152
+ def _validate(self) -> None:
153
+ if not isinstance(self._meta, dict):
154
+ raise MIFValidationError("meta must be a dictionary")
155
+ if not isinstance(self._subtitles, str):
156
+ raise MIFValidationError("subtitles must be a string")
157
+ if not isinstance(self._appearances, dict):
158
+ raise MIFValidationError("appearances must be a dictionary")
159
+
160
+ _ = parse_meta(build_meta_text(self._meta))
161
+ _ = parse_appearances(build_appearances_text(self._appearances), content_files=None)
162
+
mifpy/exceptions.py ADDED
@@ -0,0 +1,9 @@
1
+ class MIFValidationError(Exception):
2
+ """Raised when a MIF package contains invalid metadata or structure."""
3
+
4
+ def __init__(self, message: str):
5
+ super().__init__(message)
6
+ self.message = message
7
+
8
+ def __str__(self) -> str:
9
+ return f"MIFValidationError: {self.message}"
mifpy/parser.py ADDED
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import configparser
4
+ import json
5
+ from typing import Any
6
+
7
+ from .exceptions import MIFValidationError
8
+ from .utils import timestamp_to_ms
9
+
10
+
11
+ def _normalize_path(path: str) -> str:
12
+ return path.replace("\\", "/").lstrip("/")
13
+
14
+
15
+ def parse_manifest(text: str) -> dict[str, Any]:
16
+ parser = configparser.ConfigParser()
17
+ parser.read_string(text)
18
+
19
+ if "mif" not in parser:
20
+ raise MIFValidationError("manifest.ini is missing the [mif] section")
21
+ if "files" not in parser:
22
+ raise MIFValidationError("manifest.ini is missing the [files] section")
23
+
24
+ try:
25
+ version = parser.getint("mif", "version")
26
+ except (configparser.NoOptionError, ValueError):
27
+ raise MIFValidationError("manifest.ini must declare an integer version")
28
+
29
+ files = {}
30
+ for key in ["meta", "subtitles", "appearances"]:
31
+ if not parser.has_option("files", key):
32
+ raise MIFValidationError(f"manifest.ini is missing the required file entry '{key}'")
33
+ files[key] = _normalize_path(parser.get("files", key))
34
+
35
+ content_dir = parser.get("files", "content_dir", fallback="content/")
36
+ content_dir = _normalize_path(content_dir)
37
+ if content_dir and not content_dir.endswith("/"):
38
+ content_dir += "/"
39
+
40
+ return {"version": version, "files": {**files, "content_dir": content_dir}}
41
+
42
+
43
+ def parse_meta(text: str) -> dict[str, Any]:
44
+ parser = configparser.ConfigParser()
45
+ parser.read_string(text)
46
+
47
+ if "movie" not in parser:
48
+ raise MIFValidationError("meta.ini is missing the [movie] section")
49
+
50
+ movie = parser["movie"]
51
+ title = movie.get("title", fallback="").strip()
52
+ year = movie.get("year", fallback="").strip()
53
+ runtime_ms = movie.get("runtime_ms", fallback="").strip()
54
+
55
+ if not title:
56
+ raise MIFValidationError("meta.ini must include a non-empty title")
57
+ if not year.isdigit():
58
+ raise MIFValidationError("meta.ini year must be numeric")
59
+ if not runtime_ms.isdigit():
60
+ raise MIFValidationError("meta.ini runtime_ms must be numeric")
61
+
62
+ return {
63
+ "title": title,
64
+ "year": int(year),
65
+ "runtime_ms": int(runtime_ms),
66
+ }
67
+
68
+
69
+ def parse_appearances(text: str, content_files: list[str] | None = None) -> dict[str, Any]:
70
+ try:
71
+ data = json.loads(text)
72
+ except json.JSONDecodeError as exc:
73
+ raise MIFValidationError(f"appearances.json is not valid JSON: {exc}") from exc
74
+
75
+ if not isinstance(data, dict):
76
+ raise MIFValidationError("appearances.json must contain a JSON object")
77
+ if "actors" not in data or "appearances" not in data:
78
+ raise MIFValidationError("appearances.json must include 'actors' and 'appearances' keys")
79
+
80
+ actors = data["actors"]
81
+ appearances = data["appearances"]
82
+
83
+ if not isinstance(actors, dict):
84
+ raise MIFValidationError("appearances.json actors section must be an object")
85
+ if not isinstance(appearances, list):
86
+ raise MIFValidationError("appearances.json appearances section must be an array")
87
+
88
+ for actor_id, actor in actors.items():
89
+ if not isinstance(actor_id, str) or not actor_id.strip():
90
+ raise MIFValidationError("actor IDs must be non-empty strings")
91
+ if not isinstance(actor, dict):
92
+ raise MIFValidationError(f"actor '{actor_id}' must be an object")
93
+ if "name" not in actor or not isinstance(actor["name"], str) or not actor["name"].strip():
94
+ raise MIFValidationError(f"actor '{actor_id}' must include a non-empty name")
95
+ if "image" in actor:
96
+ image = actor["image"]
97
+ if not isinstance(image, str) or not image.strip():
98
+ raise MIFValidationError(f"actor '{actor_id}' image path must be a non-empty string")
99
+ if content_files is not None:
100
+ normalized = _normalize_path(image)
101
+ if normalized not in content_files:
102
+ raise MIFValidationError(f"actor '{actor_id}' references missing content file '{image}'")
103
+
104
+ for index, appearance in enumerate(appearances):
105
+ if not isinstance(appearance, dict):
106
+ raise MIFValidationError("each appearance entry must be an object")
107
+ actor = appearance.get("actor")
108
+ if not isinstance(actor, str) or actor not in actors:
109
+ raise MIFValidationError(f"appearance entry at index {index} references unknown actor '{actor}'")
110
+
111
+ for field in ["start_ms", "end_ms"]:
112
+ value = appearance.get(field)
113
+ if not isinstance(value, int) or value < 0:
114
+ raise MIFValidationError(f"appearance entry at index {index} must include a non-negative integer '{field}'")
115
+
116
+ if appearance["end_ms"] < appearance["start_ms"]:
117
+ raise MIFValidationError(f"appearance entry at index {index} has end_ms before start_ms")
118
+
119
+ return {"actors": actors, "appearances": appearances}
mifpy/srt.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from .utils import timestamp_to_ms
6
+ from .exceptions import MIFValidationError
7
+
8
+
9
+ def parse_srt(text: str) -> List[dict]:
10
+ if not isinstance(text, str):
11
+ raise ValueError("SRT input must be a string")
12
+
13
+ lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
14
+ entries: List[dict] = []
15
+ current: dict | None = None
16
+ state = "index"
17
+
18
+ for raw_line in lines + [""]:
19
+ line = raw_line.strip()
20
+
21
+ if state == "index":
22
+ if not line:
23
+ continue
24
+ current = {"index": line, "start": "", "end": "", "text": []}
25
+ state = "timestamp"
26
+ continue
27
+
28
+ if state == "timestamp" and current is not None:
29
+ if "-->" not in line:
30
+ raise MIFValidationError(f"Malformed SRT timestamp line: {line!r}")
31
+ start, end = [part.strip() for part in line.split("-->", 1)]
32
+ current["start"] = start
33
+ current["end"] = end
34
+ state = "text"
35
+ continue
36
+
37
+ if state == "text" and current is not None:
38
+ if not line:
39
+ if current["text"]:
40
+ current["text"] = "\n".join(current["text"])
41
+ entries.append(current)
42
+ current = None
43
+ state = "index"
44
+ continue
45
+ current["text"].append(raw_line)
46
+ continue
47
+
48
+ if current is not None and current["text"]:
49
+ current["text"] = "\n".join(current["text"])
50
+ entries.append(current)
51
+
52
+ return entries
53
+
54
+
55
+ def format_srt(entries: List[dict]) -> str:
56
+ output_lines: List[str] = []
57
+
58
+ for entry in entries:
59
+ index = entry.get("index", "")
60
+ start = entry.get("start", "")
61
+ end = entry.get("end", "")
62
+ text = entry.get("text", "")
63
+
64
+ if not index or not start or not end or text is None:
65
+ raise ValueError("Each subtitle entry must include index, start, end, and text")
66
+
67
+ output_lines.append(str(index))
68
+ output_lines.append(f"{start} --> {end}")
69
+ if isinstance(text, str):
70
+ output_lines.extend(text.split("\n"))
71
+ else:
72
+ output_lines.extend(map(str, text))
73
+ output_lines.append("")
74
+
75
+ return "\n".join(output_lines).strip() + "\n"
mifpy/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ import re
2
+
3
+ SRT_TIMESTAMP_PATTERN = re.compile(r"^(\d{2}):(\d{2}):(\d{2}),(\d{3})$")
4
+
5
+
6
+ def ms_to_timestamp(ms: int) -> str:
7
+ if not isinstance(ms, int) or ms < 0:
8
+ raise ValueError("Milliseconds must be a non-negative integer")
9
+
10
+ hours, rest = divmod(ms, 3_600_000)
11
+ minutes, rest = divmod(rest, 60_000)
12
+ seconds, milliseconds = divmod(rest, 1_000)
13
+ return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
14
+
15
+
16
+ def timestamp_to_ms(timestamp: str) -> int:
17
+ if not isinstance(timestamp, str):
18
+ raise ValueError("Timestamp must be a string")
19
+
20
+ match = SRT_TIMESTAMP_PATTERN.match(timestamp.strip())
21
+ if not match:
22
+ raise ValueError(f"Invalid SRT timestamp format: {timestamp!r}")
23
+
24
+ hours, minutes, seconds, milliseconds = map(int, match.groups())
25
+ return ((hours * 60 + minutes) * 60 + seconds) * 1_000 + milliseconds
mifpy/writer.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import configparser
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ def build_manifest_text(content_dir: str) -> str:
9
+ if content_dir and not content_dir.endswith("/"):
10
+ content_dir = f"{content_dir}/"
11
+
12
+ parser = configparser.ConfigParser()
13
+ parser["mif"] = {"version": "1"}
14
+ parser["files"] = {
15
+ "meta": "meta.ini",
16
+ "subtitles": "subtitles.srt",
17
+ "appearances": "appearances.json",
18
+ "content_dir": content_dir or "content/",
19
+ }
20
+
21
+ from io import StringIO
22
+
23
+ buffer = StringIO()
24
+ parser.write(buffer)
25
+ return buffer.getvalue()
26
+
27
+
28
+ def build_meta_text(meta: dict[str, Any]) -> str:
29
+ parser = configparser.ConfigParser()
30
+ parser["movie"] = {
31
+ "title": str(meta.get("title", "")),
32
+ "year": str(int(meta.get("year", 0))),
33
+ "runtime_ms": str(int(meta.get("runtime_ms", 0))),
34
+ }
35
+
36
+ from io import StringIO
37
+
38
+ buffer = StringIO()
39
+ parser.write(buffer)
40
+ return buffer.getvalue()
41
+
42
+
43
+ def build_appearances_text(appearances: dict[str, Any]) -> str:
44
+ return json.dumps(appearances, indent=2, ensure_ascii=False) + "\n"
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: mifpy
3
+ Version: 1.0.0
4
+ Summary: A Python library for reading and writing Movie Information Format (.mif) containers.
5
+ License: MIT
6
+ Author: OmgRod
7
+ Author-email: rod@omgrod.me
8
+ Requires-Python: >=3.10
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Description-Content-Type: text/markdown
17
+
18
+ # MIFpy
19
+
20
+ `mifpy` is a lightweight Python library for the Movie Information Format (`.mif`), a ZIP-based container format for movie metadata, subtitles, actor appearance timelines, and associated assets.
21
+
22
+ ## Features
23
+
24
+ - Load and save `.mif` archives using Python `zipfile`
25
+ - Validate required package files and formats
26
+ - Parse `manifest.ini`, `meta.ini`, `appearances.json`, and `subtitles.srt`
27
+ - Add content assets under `content/`
28
+ - CLI support: `inspect`, `extract`, and `create`
29
+
30
+ ## Package structure
31
+
32
+ A valid `.mif` archive contains:
33
+
34
+ - `manifest.ini`
35
+ - `meta.ini`
36
+ - `subtitles.srt`
37
+ - `appearances.json`
38
+ - `content/` (optional)
39
+
40
+ ## Installation
41
+
42
+ Install with Poetry:
43
+
44
+ ```bash
45
+ poetry install
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ from mifpy import MIFPackage
52
+
53
+ pkg = MIFPackage.create(
54
+ meta={"title": "The Matrix", "year": 1999, "runtime_ms": 8160000},
55
+ subtitles="1\n00:00:00,000 --> 00:00:02,000\nWake up, Neo.\n",
56
+ appearances={
57
+ "actors": {
58
+ "keanu_reeves": {
59
+ "name": "Keanu Reeves",
60
+ "image": "content/keanu.png",
61
+ }
62
+ },
63
+ "appearances": [
64
+ {"actor": "keanu_reeves", "start_ms": 1000, "end_ms": 10000}
65
+ ],
66
+ },
67
+ )
68
+
69
+ pkg.add_content_file("keanu.png", b"<raw-image-bytes>")
70
+ pkg.save("movie.mif")
71
+ ```
72
+
73
+ ## CLI
74
+
75
+ ```bash
76
+ mifpy inspect movie.mif
77
+ mifpy extract movie.mif ./out
78
+ mifpy create sample.mif
79
+ ```
80
+
81
+ ## Testing
82
+
83
+ Run tests with:
84
+
85
+ ```bash
86
+ poetry run pytest
87
+ ```
88
+
@@ -0,0 +1,12 @@
1
+ mifpy/__init__.py,sha256=AcWg1EKYe73gpnZWpae1_S8fvd6ealUmMW557-vBPMA,316
2
+ mifpy/cli.py,sha256=7-qQ0r47_FMxiG7WvfPhtYWtgcTdc_4EdY3SsGoRkwU,2869
3
+ mifpy/core.py,sha256=44gHr2E_ScoKYm7a2JKed-IPvVUMxJBLV0-Ha2vJru8,6869
4
+ mifpy/exceptions.py,sha256=JzZuJ9a8ma66TdUq-g0PS7P2hrPlQvmjK8yMoGKnrFM,310
5
+ mifpy/parser.py,sha256=dMplVRM0No7J4RDDZOV2EzHkbmgaudAVbh74_wG_x24,5007
6
+ mifpy/srt.py,sha256=k03Aw0hmWtYAt2qySsttfYad9XNtBQGexKvMTHMJzA8,2410
7
+ mifpy/utils.py,sha256=Q3Ygpg7DNMNQXfvQ2jG3Gf9_kp7gmK2GHLFYlJUWO5U,896
8
+ mifpy/writer.py,sha256=Uog9m2L1nx_vQQ-J4pHihY_mzye9Qyr1NZiEPXZ9d-U,1177
9
+ mifpy-1.0.0.dist-info/entry_points.txt,sha256=c4sCrX46OMrppJreNbgq3wvKCYicpBd1MIWm22qiZvQ,40
10
+ mifpy-1.0.0.dist-info/METADATA,sha256=dHNUA0soon3sEMo9848yTISq8OqRUXIm-HeVJYQwHeU,2071
11
+ mifpy-1.0.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
+ mifpy-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mifpy=mifpy.cli:main
3
+