imexp 0.1.0__py3-none-win_amd64.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.
imexp/__init__.py ADDED
File without changes
imexp/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Module entrypoint for python -m imexp."""
2
+
3
+ from imexp.cli.main import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ main()
Binary file
imexp/cli/__init__.py ADDED
File without changes
imexp/cli/config.py ADDED
@@ -0,0 +1,180 @@
1
+ """Configuration loading and management."""
2
+
3
+ import os
4
+ import logging
5
+ import configparser
6
+ from pathlib import Path
7
+ from dataclasses import dataclass
8
+
9
+ logger = logging.getLogger("imexp")
10
+
11
+ IOS_BACKUP_ROOT = Path("~/Library/Application Support/MobileSync/Backup").expanduser()
12
+ CONFIG_FILE = "cli/config.ini"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ExportDefaults:
17
+ """User-configurable export defaults from config.ini."""
18
+
19
+ platform: str
20
+ format: str
21
+ copy_method: str
22
+ conversation_filter: str
23
+ use_caller_id: bool
24
+ output_dir: str
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CLIConfig:
29
+ """Resolved CLI configuration."""
30
+
31
+ export: ExportDefaults
32
+ path: Path
33
+
34
+
35
+ def _get_project_root() -> Path:
36
+ """Find project root by looking for pyproject.toml or .git."""
37
+ current = Path(__file__).resolve()
38
+ for parent in current.parents:
39
+ if (parent / "pyproject.toml").exists():
40
+ return parent
41
+ if (parent / ".git").exists():
42
+ return parent
43
+ return Path.cwd()
44
+
45
+
46
+ def _get_data_dir() -> Path:
47
+ """Get the data directory."""
48
+ env_value = os.getenv("IMEXP_DATA_DIR")
49
+ if env_value:
50
+ return Path(env_value)
51
+ return _get_project_root() / "data"
52
+
53
+
54
+ def _get_config_dir() -> Path:
55
+ """Get the config directory."""
56
+ env_value = os.getenv("IMEXP_CONFIG_DIR")
57
+ if env_value:
58
+ return Path(env_value)
59
+ return _get_data_dir() / "config"
60
+
61
+
62
+ def _resolve_config_path(config_path: Path | None = None) -> Path:
63
+ """Resolve the config file path."""
64
+ if config_path is not None:
65
+ return config_path.expanduser().resolve()
66
+
67
+ env_path = os.getenv("IMEXP_CONFIG_FILE")
68
+ if env_path:
69
+ return Path(env_path).expanduser().resolve()
70
+
71
+ return _get_config_dir() / CONFIG_FILE
72
+
73
+
74
+ def _ensure_config_file(path: Path) -> None:
75
+ """Create the config file with defaults if it doesn't exist."""
76
+ if path.exists():
77
+ return
78
+
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ path.write_text(_default_config_template(), encoding="utf-8")
81
+
82
+
83
+ def _get_value(
84
+ parser: configparser.ConfigParser,
85
+ section: str,
86
+ key: str,
87
+ ) -> str | None:
88
+ """Read a string value from the config parser."""
89
+ if not parser.has_section(section):
90
+ return None
91
+
92
+ if not parser.has_option(section, key):
93
+ return None
94
+
95
+ value = parser.get(section, key).strip()
96
+ if not value:
97
+ return None
98
+
99
+ return value
100
+
101
+
102
+ def _get_bool_value(
103
+ parser: configparser.ConfigParser,
104
+ section: str,
105
+ key: str,
106
+ ) -> bool | None:
107
+ """Read a boolean value from the config parser."""
108
+ if not parser.has_section(section):
109
+ return None
110
+
111
+ if not parser.has_option(section, key):
112
+ return None
113
+
114
+ raw_value = parser.get(section, key).strip()
115
+ if not raw_value:
116
+ return None
117
+
118
+ return parser.getboolean(section, key)
119
+
120
+
121
+ def load_config(config_path: Path | None = None) -> CLIConfig:
122
+ """Load configuration from the config.ini file."""
123
+ parser = configparser.ConfigParser()
124
+ resolved_path = _resolve_config_path(config_path)
125
+ _ensure_config_file(resolved_path)
126
+ parser.read(resolved_path)
127
+
128
+ output_dir = _get_value(parser, "export", "output_dir") or ""
129
+ output_dir = os.environ.get("IMEXP_BASE_OUTPUT_DIR", output_dir) or "./data/messages/sms"
130
+
131
+ return CLIConfig(
132
+ export=ExportDefaults(
133
+ platform=_get_value(parser, "export", "platform") or "",
134
+ format=_get_value(parser, "export", "format") or "txt",
135
+ copy_method=_get_value(parser, "export", "copy_method") or "full",
136
+ conversation_filter=_get_value(parser, "export", "conversation_filter") or "",
137
+ use_caller_id=_get_bool_value(parser, "export", "use_caller_id") or False,
138
+ output_dir=output_dir,
139
+ ),
140
+ path=resolved_path,
141
+ )
142
+
143
+
144
+ def base_output_dir(cli_config: CLIConfig | None = None) -> Path:
145
+ """Return the base output directory for exports."""
146
+ if cli_config:
147
+ return Path(cli_config.export.output_dir)
148
+ value = os.environ.get("IMEXP_BASE_OUTPUT_DIR", "./data/messages/sms")
149
+ return Path(value)
150
+
151
+
152
+ def _default_config_template() -> str:
153
+ return """# imexp CLI configuration
154
+ # This file is auto-generated on first run.
155
+ # Values here serve as defaults; CLI flags always override.
156
+
157
+ [export]
158
+ # Source platform (macOS or iOS). Leave empty to prompt interactively.
159
+ platform =
160
+
161
+ # Output format for exported messages.
162
+ # Options: txt, html
163
+ format = txt
164
+
165
+ # Attachment copy method.
166
+ # Options: disabled, clone, basic, full
167
+ copy_method = full
168
+
169
+ # Default conversation filter (comma-separated).
170
+ # This is the filter passed to imessage-exporter --conversation-filter.
171
+ # Leave empty to export all conversations.
172
+ conversation_filter =
173
+
174
+ # Use caller ID instead of "Me" in exports.
175
+ use_caller_id = true
176
+
177
+ # Base output directory for exports.
178
+ # Can also be set via IMEXP_BASE_OUTPUT_DIR environment variable.
179
+ output_dir = ./data/messages/sms
180
+ """