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 +0 -0
- imexp/__main__.py +7 -0
- imexp/bin/imessage-exporter.exe +0 -0
- imexp/cli/__init__.py +0 -0
- imexp/cli/config.py +180 -0
- imexp/cli/main.py +1225 -0
- imexp/core/__init__.py +0 -0
- imexp/core/exporter_binary.py +83 -0
- imexp/core/utils/__init__.py +0 -0
- imexp/core/utils/ansi.py +33 -0
- imexp/core/utils/helpformatter.py +68 -0
- imexp-0.1.0.dist-info/METADATA +139 -0
- imexp-0.1.0.dist-info/RECORD +16 -0
- imexp-0.1.0.dist-info/WHEEL +4 -0
- imexp-0.1.0.dist-info/entry_points.txt +2 -0
- imexp-0.1.0.dist-info/licenses/LICENSE +674 -0
imexp/__init__.py
ADDED
|
File without changes
|
imexp/__main__.py
ADDED
|
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
|
+
"""
|