iso8583sim 1.1.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.
- iso8583sim/__init__.py +0 -0
- iso8583sim/cli/__init__.py +4 -0
- iso8583sim/cli/commands.py +314 -0
- iso8583sim/cli/config.py +65 -0
- iso8583sim/cli/formatter.py +110 -0
- iso8583sim/cli/utils.py +128 -0
- iso8583sim/core/__init__.py +57 -0
- iso8583sim/core/_bitmap.pyx +149 -0
- iso8583sim/core/_parser_fast.pyx +169 -0
- iso8583sim/core/_validator_fast.pyx +197 -0
- iso8583sim/core/builder.py +318 -0
- iso8583sim/core/emv.py +347 -0
- iso8583sim/core/parser.py +576 -0
- iso8583sim/core/pool.py +158 -0
- iso8583sim/core/types.py +1447 -0
- iso8583sim/core/validator.py +482 -0
- iso8583sim/demo.py +452 -0
- iso8583sim/llm/__init__.py +65 -0
- iso8583sim/llm/base.py +123 -0
- iso8583sim/llm/explainer.py +162 -0
- iso8583sim/llm/generator.py +219 -0
- iso8583sim/llm/prompts.py +266 -0
- iso8583sim/llm/providers/__init__.py +139 -0
- iso8583sim/llm/providers/anthropic.py +141 -0
- iso8583sim/llm/providers/google.py +138 -0
- iso8583sim/llm/providers/ollama.py +135 -0
- iso8583sim/llm/providers/openai.py +154 -0
- iso8583sim/utils/helpers.py +0 -0
- iso8583sim/web/app.py +0 -0
- iso8583sim-1.1.0.dist-info/METADATA +233 -0
- iso8583sim-1.1.0.dist-info/RECORD +33 -0
- iso8583sim-1.1.0.dist-info/WHEEL +4 -0
- iso8583sim-1.1.0.dist-info/licenses/LICENSE +0 -0
iso8583sim/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import code
|
|
2
|
+
import readline
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from ..core.builder import ISO8583Builder
|
|
11
|
+
from ..core.parser import ISO8583Parser
|
|
12
|
+
from ..core.types import (
|
|
13
|
+
CardNetwork,
|
|
14
|
+
ISO8583Message,
|
|
15
|
+
ISO8583Version,
|
|
16
|
+
)
|
|
17
|
+
from ..core.validator import ISO8583Validator
|
|
18
|
+
from .config import ConfigManager
|
|
19
|
+
from .formatter import CLIFormatter
|
|
20
|
+
from .utils import (
|
|
21
|
+
create_template_message,
|
|
22
|
+
format_amount,
|
|
23
|
+
load_json_file,
|
|
24
|
+
save_json_file,
|
|
25
|
+
validate_pan,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Initialize Typer app
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="iso8583sim",
|
|
31
|
+
help="ISO 8583 Message Simulator - Parse, Build, and Test ISO 8583 messages",
|
|
32
|
+
add_completion=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Initialize shared objects
|
|
36
|
+
console = Console()
|
|
37
|
+
formatter = CLIFormatter()
|
|
38
|
+
config_manager = ConfigManager()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ISO8583Shell(code.InteractiveConsole):
|
|
42
|
+
"""Enhanced interactive shell for ISO8583 simulation"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, locals: dict[str, Any], history_file: Path):
|
|
45
|
+
super().__init__(locals)
|
|
46
|
+
self.history_file = history_file
|
|
47
|
+
self.console = Console()
|
|
48
|
+
self._setup_readline()
|
|
49
|
+
|
|
50
|
+
def _setup_readline(self):
|
|
51
|
+
"""Setup readline with history and tab completion"""
|
|
52
|
+
# Enable tab completion
|
|
53
|
+
readline.parse_and_bind("tab: complete")
|
|
54
|
+
|
|
55
|
+
# Load history if exists
|
|
56
|
+
if self.history_file.exists():
|
|
57
|
+
readline.read_history_file(str(self.history_file))
|
|
58
|
+
|
|
59
|
+
# Set history length
|
|
60
|
+
readline.set_history_length(1000)
|
|
61
|
+
|
|
62
|
+
def interact(self, banner=None):
|
|
63
|
+
"""Start the interactive shell"""
|
|
64
|
+
if banner is None:
|
|
65
|
+
banner = self._get_default_banner()
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
super().interact(banner)
|
|
69
|
+
finally:
|
|
70
|
+
# Save history on exit
|
|
71
|
+
readline.write_history_file(str(self.history_file))
|
|
72
|
+
|
|
73
|
+
def _get_default_banner(self) -> str:
|
|
74
|
+
"""Generate default banner with help text"""
|
|
75
|
+
return """
|
|
76
|
+
[bold cyan]ISO 8583 Interactive Shell[/]
|
|
77
|
+
[green]Available objects:[/]
|
|
78
|
+
- [yellow]parser[/]: ISO8583Parser instance
|
|
79
|
+
- [yellow]builder[/]: ISO8583Builder instance
|
|
80
|
+
- [yellow]validator[/]: ISO8583Validator instance
|
|
81
|
+
- [yellow]formatter[/]: CLIFormatter instance
|
|
82
|
+
|
|
83
|
+
[green]Example usage:[/]
|
|
84
|
+
>>> msg = parser.parse("0100...")
|
|
85
|
+
>>> result = builder.build(msg)
|
|
86
|
+
>>> errors = validator.validate_message(msg)
|
|
87
|
+
>>> formatter.print_json(msg.fields)
|
|
88
|
+
|
|
89
|
+
[green]Special commands:[/]
|
|
90
|
+
- [yellow]?obj[/]: Get help about an object
|
|
91
|
+
- [yellow]dir(obj)[/]: List object attributes
|
|
92
|
+
- [yellow]help(obj)[/]: Detailed help about an object
|
|
93
|
+
|
|
94
|
+
[cyan]Type 'exit()' or press Ctrl+D to exit[/]
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def push(self, line: str) -> bool:
|
|
98
|
+
"""Process input line with error handling"""
|
|
99
|
+
try:
|
|
100
|
+
return super().push(line)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.console.print(f"[red]Error:[/] {str(e)}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def raw_input(self, prompt=""):
|
|
106
|
+
"""Override raw_input to use rich formatting"""
|
|
107
|
+
return super().raw_input(f"[cyan]{prompt}[/]")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def version():
|
|
112
|
+
"""Display version information"""
|
|
113
|
+
console.print("[cyan]ISO8583 Simulator[/] [green]v0.1.0[/]")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("parse")
|
|
117
|
+
def parse_message(
|
|
118
|
+
message: str = typer.Argument(..., help="ISO 8583 message string to parse"),
|
|
119
|
+
version: str = typer.Option("1987", "--version", "-v", help="ISO 8583 version (1987, 1993, 2003)"),
|
|
120
|
+
network: str | None = typer.Option(None, "--network", "-n", help="Card network (VISA, MASTERCARD, AMEX, etc.)"),
|
|
121
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output file for parsed message (JSON format)"),
|
|
122
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format (table, json, tree)"),
|
|
123
|
+
):
|
|
124
|
+
"""Parse an ISO 8583 message and display its contents"""
|
|
125
|
+
try:
|
|
126
|
+
# Initialize parser
|
|
127
|
+
iso_version = ISO8583Version(version)
|
|
128
|
+
parser = ISO8583Parser(version=iso_version)
|
|
129
|
+
|
|
130
|
+
# Parse message with optional network
|
|
131
|
+
card_network = CardNetwork(network.upper()) if network else None
|
|
132
|
+
parsed = parser.parse(message, network=card_network)
|
|
133
|
+
|
|
134
|
+
# Format and display result based on format option
|
|
135
|
+
if format == "table":
|
|
136
|
+
table = formatter.format_field_table(parsed.fields)
|
|
137
|
+
console.print(table)
|
|
138
|
+
elif format == "json":
|
|
139
|
+
formatter.print_json(parsed.fields)
|
|
140
|
+
elif format == "tree":
|
|
141
|
+
tree = formatter.format_tree_view(parsed.__dict__)
|
|
142
|
+
console.print(tree)
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Unknown format option: {format}")
|
|
145
|
+
|
|
146
|
+
# Save to file if requested
|
|
147
|
+
if output:
|
|
148
|
+
result = {"mti": parsed.mti, "version": version, "network": network, "fields": parsed.fields}
|
|
149
|
+
save_json_file(result, output)
|
|
150
|
+
console.print(f"\n[green]Results saved to {output}")
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
console.print(f"[red]Error parsing message: {str(e)}")
|
|
154
|
+
raise typer.Exit(1) from None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.command("build")
|
|
158
|
+
def build_message(
|
|
159
|
+
mti: str = typer.Option(..., "--mti", "-m", help="Message Type Indicator"),
|
|
160
|
+
fields_file: Path = typer.Option(..., "--fields", "-f", help="JSON file containing field values"),
|
|
161
|
+
version: str = typer.Option("1987", "--version", "-v", help="ISO 8583 version (1987, 1993, 2003)"),
|
|
162
|
+
network: str | None = typer.Option(None, "--network", "-n", help="Card network (VISA, MASTERCARD, AMEX, etc.)"),
|
|
163
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output file for built message"),
|
|
164
|
+
):
|
|
165
|
+
"""Build an ISO 8583 message from field values"""
|
|
166
|
+
try:
|
|
167
|
+
# Load fields from JSON and convert string keys to integers
|
|
168
|
+
fields_data = {int(k): v for k, v in load_json_file(fields_file).items()}
|
|
169
|
+
|
|
170
|
+
# Initialize builder
|
|
171
|
+
iso_version = ISO8583Version(version)
|
|
172
|
+
builder = ISO8583Builder(version=iso_version)
|
|
173
|
+
|
|
174
|
+
# Create message with optional network
|
|
175
|
+
card_network = CardNetwork(network.upper()) if network else None
|
|
176
|
+
message = ISO8583Message(mti=mti, fields=fields_data, version=iso_version, network=card_network)
|
|
177
|
+
|
|
178
|
+
# Build message
|
|
179
|
+
result = builder.build(message)
|
|
180
|
+
|
|
181
|
+
# Display result
|
|
182
|
+
panel = Panel(result, title="Built ISO 8583 Message", border_style="cyan")
|
|
183
|
+
console.print(panel)
|
|
184
|
+
|
|
185
|
+
# Save to file if requested
|
|
186
|
+
if output:
|
|
187
|
+
output.write_text(result)
|
|
188
|
+
console.print(f"\n[green]Message saved to {output}")
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
console.print(f"[red]Error building message: {str(e)}")
|
|
192
|
+
raise typer.Exit(1) from None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command("validate")
|
|
196
|
+
def validate_message(
|
|
197
|
+
message: str = typer.Argument(..., help="ISO 8583 message to validate"),
|
|
198
|
+
version: str = typer.Option("1987", "--version", "-v", help="ISO 8583 version (1987, 1993, 2003)"),
|
|
199
|
+
network: str | None = typer.Option(None, "--network", "-n", help="Card network (VISA, MASTERCARD, AMEX, etc.)"),
|
|
200
|
+
):
|
|
201
|
+
"""Validate an ISO 8583 message"""
|
|
202
|
+
try:
|
|
203
|
+
# Parse message first
|
|
204
|
+
iso_version = ISO8583Version(version)
|
|
205
|
+
parser = ISO8583Parser(version=iso_version)
|
|
206
|
+
card_network = CardNetwork(network.upper()) if network else None
|
|
207
|
+
parsed = parser.parse(message, network=card_network)
|
|
208
|
+
|
|
209
|
+
# Validate
|
|
210
|
+
validator = ISO8583Validator()
|
|
211
|
+
errors = validator.validate_message(parsed)
|
|
212
|
+
|
|
213
|
+
# Display results
|
|
214
|
+
panel = formatter.format_validation_results(errors)
|
|
215
|
+
console.print(panel)
|
|
216
|
+
|
|
217
|
+
if errors:
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f"[red]Error validating message: {str(e)}")
|
|
222
|
+
raise typer.Exit(1) from None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command("generate")
|
|
226
|
+
def generate_message(
|
|
227
|
+
type: str = typer.Option(..., "--type", "-t", help="Message type (auth, financial, reversal)"),
|
|
228
|
+
pan: str = typer.Option("4111111111111111", "--pan", "-p", help="Primary Account Number"),
|
|
229
|
+
amount: str = typer.Option("000000001000", "--amount", "-a", help="Transaction amount"),
|
|
230
|
+
currency: str = typer.Option("840", "--currency", "-c", help="Currency code (ISO 4217)"),
|
|
231
|
+
network: str | None = typer.Option(None, "--network", "-n", help="Card network (VISA, MASTERCARD, AMEX, etc.)"),
|
|
232
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output file for generated message"),
|
|
233
|
+
):
|
|
234
|
+
"""Generate a sample ISO 8583 message"""
|
|
235
|
+
try:
|
|
236
|
+
# Create message template
|
|
237
|
+
message = create_template_message(get_mti_for_type(type), pan=validate_pan(pan), amount=format_amount(amount))
|
|
238
|
+
|
|
239
|
+
# Add network if specified
|
|
240
|
+
if network:
|
|
241
|
+
message["network"] = CardNetwork(network.upper())
|
|
242
|
+
|
|
243
|
+
# Add currency
|
|
244
|
+
message["fields"][49] = currency
|
|
245
|
+
|
|
246
|
+
# Build message
|
|
247
|
+
builder = ISO8583Builder()
|
|
248
|
+
iso_message = ISO8583Message(**message)
|
|
249
|
+
result = builder.build(iso_message)
|
|
250
|
+
|
|
251
|
+
# Display result
|
|
252
|
+
panel = Panel(result, title=f"Generated {type.title()} Message", border_style="cyan")
|
|
253
|
+
console.print(panel)
|
|
254
|
+
|
|
255
|
+
# Save to file if requested
|
|
256
|
+
if output:
|
|
257
|
+
output.write_text(result)
|
|
258
|
+
console.print(f"\n[green]Message saved to {output}")
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
console.print(f"[red]Error generating message: {str(e)}")
|
|
262
|
+
raise typer.Exit(1) from None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.command("shell")
|
|
266
|
+
def interactive_shell():
|
|
267
|
+
"""Start an interactive ISO 8583 shell"""
|
|
268
|
+
try:
|
|
269
|
+
# Create instances of core components
|
|
270
|
+
parser = ISO8583Parser()
|
|
271
|
+
builder = ISO8583Builder()
|
|
272
|
+
validator = ISO8583Validator()
|
|
273
|
+
|
|
274
|
+
# Get history file from config
|
|
275
|
+
config = config_manager.get_config()
|
|
276
|
+
history_file = Path(config.history_file).expanduser()
|
|
277
|
+
|
|
278
|
+
# Create shell with local objects
|
|
279
|
+
locals_dict = {
|
|
280
|
+
"parser": parser,
|
|
281
|
+
"builder": builder,
|
|
282
|
+
"validator": validator,
|
|
283
|
+
"formatter": formatter,
|
|
284
|
+
"ISO8583Message": ISO8583Message,
|
|
285
|
+
"ISO8583Version": ISO8583Version,
|
|
286
|
+
"CardNetwork": CardNetwork,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Create and start shell
|
|
290
|
+
shell = ISO8583Shell(locals_dict, history_file)
|
|
291
|
+
shell.interact()
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
console.print(f"[red]Error starting interactive shell: {str(e)}")
|
|
295
|
+
raise typer.Exit(1) from None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_mti_for_type(type: str) -> str:
|
|
299
|
+
"""Get MTI for message type"""
|
|
300
|
+
mti_map = {"auth": "0100", "financial": "0200", "reversal": "0400", "network": "0800"}
|
|
301
|
+
|
|
302
|
+
if type not in mti_map:
|
|
303
|
+
raise ValueError(f"Invalid message type. Choose from: {', '.join(mti_map.keys())}")
|
|
304
|
+
|
|
305
|
+
return mti_map[type]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def main():
|
|
309
|
+
"""Entry point for the CLI"""
|
|
310
|
+
app()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
main()
|
iso8583sim/cli/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# iso8583sim/cli/config.py
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CLIConfig(BaseModel):
|
|
10
|
+
"""CLI Configuration Model"""
|
|
11
|
+
|
|
12
|
+
default_version: str = "1987"
|
|
13
|
+
output_directory: str = "./output"
|
|
14
|
+
templates_directory: str = "./templates"
|
|
15
|
+
log_level: str = "INFO"
|
|
16
|
+
color_output: bool = True
|
|
17
|
+
save_history: bool = True
|
|
18
|
+
history_file: str = "~/.iso8583sim_history"
|
|
19
|
+
default_currency: str = "840"
|
|
20
|
+
default_terminal_id: str = "TEST1234"
|
|
21
|
+
default_merchant_id: str = "MERCHANT123456789"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigManager:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.app_dir = typer.get_app_dir("iso8583sim")
|
|
27
|
+
self.config_file = Path(self.app_dir) / "config.json"
|
|
28
|
+
self.config = self._load_config()
|
|
29
|
+
|
|
30
|
+
def _load_config(self) -> CLIConfig:
|
|
31
|
+
"""Load configuration from file or create default"""
|
|
32
|
+
try:
|
|
33
|
+
if self.config_file.exists():
|
|
34
|
+
config_data = json.loads(self.config_file.read_text())
|
|
35
|
+
return CLIConfig(**config_data)
|
|
36
|
+
else:
|
|
37
|
+
return self._create_default_config()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Error loading config: {e}. Using defaults.")
|
|
40
|
+
return CLIConfig()
|
|
41
|
+
|
|
42
|
+
def _create_default_config(self) -> CLIConfig:
|
|
43
|
+
"""Create default configuration file"""
|
|
44
|
+
config = CLIConfig()
|
|
45
|
+
self._save_config(config)
|
|
46
|
+
return config
|
|
47
|
+
|
|
48
|
+
def _save_config(self, config: CLIConfig):
|
|
49
|
+
"""Save configuration to file"""
|
|
50
|
+
try:
|
|
51
|
+
Path(self.app_dir).mkdir(parents=True, exist_ok=True)
|
|
52
|
+
self.config_file.write_text(config.model_dump_json(indent=2))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Error saving config: {e}")
|
|
55
|
+
|
|
56
|
+
def update_config(self, **kwargs):
|
|
57
|
+
"""Update configuration with new values"""
|
|
58
|
+
config_data = self.config.model_dump()
|
|
59
|
+
config_data.update(kwargs)
|
|
60
|
+
self.config = CLIConfig(**config_data)
|
|
61
|
+
self._save_config(self.config)
|
|
62
|
+
|
|
63
|
+
def get_config(self) -> CLIConfig:
|
|
64
|
+
"""Get current configuration"""
|
|
65
|
+
return self.config
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# iso8583sim/cli/formatter.py
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CLIFormatter:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.console = Console()
|
|
15
|
+
|
|
16
|
+
def format_message(self, message: dict[str, Any]) -> Panel:
|
|
17
|
+
"""Format ISO message for display"""
|
|
18
|
+
content = []
|
|
19
|
+
content.append(f"[cyan]MTI:[/] {message.get('mti', 'N/A')}")
|
|
20
|
+
content.append(f"[cyan]Version:[/] {message.get('version', 'N/A')}")
|
|
21
|
+
|
|
22
|
+
fields = message.get("fields", {})
|
|
23
|
+
if fields:
|
|
24
|
+
content.append("\n[cyan]Fields:[/]")
|
|
25
|
+
for field_num, value in sorted(fields.items()):
|
|
26
|
+
content.append(f" Field {field_num}: {value}")
|
|
27
|
+
|
|
28
|
+
return Panel("\n".join(content), title="ISO 8583 Message", border_style="cyan")
|
|
29
|
+
|
|
30
|
+
def format_validation_results(self, errors: list[str]) -> Panel:
|
|
31
|
+
"""Format validation results"""
|
|
32
|
+
if not errors:
|
|
33
|
+
return Panel("[green]✓ Message is valid[/]", title="Validation Results", border_style="green")
|
|
34
|
+
|
|
35
|
+
content = ["[red]The following errors were found:[/]"]
|
|
36
|
+
for error in errors:
|
|
37
|
+
content.append(f"[red]✗[/] {error}")
|
|
38
|
+
|
|
39
|
+
return Panel("\n".join(content), title="Validation Results", border_style="red")
|
|
40
|
+
|
|
41
|
+
def format_field_table(self, fields: dict[int, str]) -> Table:
|
|
42
|
+
"""Create table of message fields"""
|
|
43
|
+
table = Table(title="Message Fields")
|
|
44
|
+
table.add_column("Field Number", style="cyan")
|
|
45
|
+
table.add_column("Description", style="green")
|
|
46
|
+
table.add_column("Value", style="yellow")
|
|
47
|
+
table.add_column("Length", style="magenta")
|
|
48
|
+
|
|
49
|
+
for field_num, value in sorted(fields.items()):
|
|
50
|
+
table.add_row(str(field_num), self.get_field_description(field_num), value, str(len(value)))
|
|
51
|
+
|
|
52
|
+
return table
|
|
53
|
+
|
|
54
|
+
def format_tree_view(self, message: dict[str, Any]) -> Tree:
|
|
55
|
+
"""Create tree view of message structure"""
|
|
56
|
+
tree = Tree("ISO 8583 Message")
|
|
57
|
+
tree.add(f"MTI: {message.get('mti', 'N/A')}")
|
|
58
|
+
tree.add(f"Version: {message.get('version', 'N/A')}")
|
|
59
|
+
|
|
60
|
+
fields_branch = tree.add("Fields")
|
|
61
|
+
for field_num, value in sorted(message.get("fields", {}).items()):
|
|
62
|
+
fields_branch.add(f"Field {field_num}: {value}")
|
|
63
|
+
|
|
64
|
+
return tree
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_field_description(field_num: int) -> str:
|
|
68
|
+
"""Get description for field number"""
|
|
69
|
+
# This could be expanded with a complete field description mapping
|
|
70
|
+
descriptions = {
|
|
71
|
+
0: "Message Type Indicator",
|
|
72
|
+
2: "Primary Account Number",
|
|
73
|
+
3: "Processing Code",
|
|
74
|
+
4: "Amount, Transaction",
|
|
75
|
+
11: "Systems Trace Audit Number",
|
|
76
|
+
12: "Time, Local Transaction",
|
|
77
|
+
13: "Date, Local Transaction",
|
|
78
|
+
37: "Retrieval Reference Number",
|
|
79
|
+
39: "Response Code",
|
|
80
|
+
41: "Card Acceptor Terminal ID",
|
|
81
|
+
42: "Card Acceptor ID Code",
|
|
82
|
+
}
|
|
83
|
+
return descriptions.get(field_num, "Field Description Not Available")
|
|
84
|
+
|
|
85
|
+
def print_success(self, message: str):
|
|
86
|
+
"""Print success message"""
|
|
87
|
+
self.console.print(f"[green]✓[/] {message}")
|
|
88
|
+
|
|
89
|
+
def print_error(self, message: str):
|
|
90
|
+
"""Print error message"""
|
|
91
|
+
self.console.print(f"[red]✗[/] {message}")
|
|
92
|
+
|
|
93
|
+
def print_warning(self, message: str):
|
|
94
|
+
"""Print warning message"""
|
|
95
|
+
self.console.print(f"[yellow]![/] {message}")
|
|
96
|
+
|
|
97
|
+
def print_info(self, message: str):
|
|
98
|
+
"""Print info message"""
|
|
99
|
+
self.console.print(f"[blue]i[/] {message}")
|
|
100
|
+
|
|
101
|
+
def format_json(self, data: dict[str, Any]) -> str:
|
|
102
|
+
"""Format data as colored JSON"""
|
|
103
|
+
# Just return the formatted JSON string
|
|
104
|
+
return json.dumps(data, indent=2)
|
|
105
|
+
|
|
106
|
+
def print_json(self, data: dict[str, Any]):
|
|
107
|
+
"""Print data as syntax-highlighted JSON"""
|
|
108
|
+
# Use Syntax for display but don't return it
|
|
109
|
+
syntax = Syntax(json.dumps(data, indent=2), "json", theme="monokai", line_numbers=True)
|
|
110
|
+
self.console.print(syntax)
|
iso8583sim/cli/utils.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# iso8583sim/cli/utils.py
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_json_file(file_path: Path) -> dict[str, Any]:
|
|
11
|
+
"""Load and validate JSON file"""
|
|
12
|
+
try:
|
|
13
|
+
with open(file_path) as f:
|
|
14
|
+
return json.load(f)
|
|
15
|
+
except json.JSONDecodeError:
|
|
16
|
+
raise typer.BadParameter(f"Invalid JSON file: {file_path}") from None
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
raise typer.BadParameter(f"File not found: {file_path}") from None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def save_json_file(data: dict[str, Any], file_path: Path):
|
|
22
|
+
"""Save data to JSON file"""
|
|
23
|
+
try:
|
|
24
|
+
with open(file_path, "w") as f:
|
|
25
|
+
json.dump(data, f, indent=2)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
raise typer.BadParameter(f"Error saving file: {e}") from None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_output_filename(prefix: str, suffix: str = "") -> str:
|
|
31
|
+
"""Generate unique output filename with timestamp"""
|
|
32
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
33
|
+
return f"{prefix}_{timestamp}{suffix}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ensure_directory(directory: Path):
|
|
37
|
+
"""Ensure directory exists"""
|
|
38
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_file_path(file_path: Path, create_dir: bool = True) -> Path:
|
|
42
|
+
"""Validate and prepare file path"""
|
|
43
|
+
if create_dir:
|
|
44
|
+
ensure_directory(file_path.parent)
|
|
45
|
+
return file_path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def format_amount(amount: str) -> str:
|
|
49
|
+
"""Format numeric amount with proper padding"""
|
|
50
|
+
try:
|
|
51
|
+
# Remove any decimal points and convert to integer
|
|
52
|
+
num = int(float(amount) * 100)
|
|
53
|
+
# Return 12-digit zero-padded string
|
|
54
|
+
return f"{num:012d}"
|
|
55
|
+
except ValueError:
|
|
56
|
+
raise typer.BadParameter("Invalid amount format") from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_pan(pan: str) -> str:
|
|
60
|
+
"""Validate PAN using Luhn algorithm"""
|
|
61
|
+
if not pan.isdigit():
|
|
62
|
+
raise typer.BadParameter("PAN must contain only digits")
|
|
63
|
+
|
|
64
|
+
# Luhn algorithm check
|
|
65
|
+
digits = [int(d) for d in pan]
|
|
66
|
+
checksum = 0
|
|
67
|
+
for i in range(len(digits) - 2, -1, -1):
|
|
68
|
+
d = digits[i]
|
|
69
|
+
if i % 2 == len(digits) % 2:
|
|
70
|
+
d *= 2
|
|
71
|
+
if d > 9:
|
|
72
|
+
d -= 9
|
|
73
|
+
checksum += d
|
|
74
|
+
|
|
75
|
+
if (checksum + digits[-1]) % 10 != 0:
|
|
76
|
+
raise typer.BadParameter("Invalid PAN (failed Luhn check)")
|
|
77
|
+
|
|
78
|
+
return pan
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_template_message(
|
|
82
|
+
mti: str, pan: str | None = None, amount: str | None = None, terminal_id: str | None = None
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Create template message with common fields"""
|
|
85
|
+
now = datetime.now()
|
|
86
|
+
|
|
87
|
+
message = {
|
|
88
|
+
"mti": mti,
|
|
89
|
+
"fields": {
|
|
90
|
+
11: now.strftime("%H%M%S"), # STAN
|
|
91
|
+
12: now.strftime("%H%M%S"), # Time
|
|
92
|
+
13: now.strftime("%m%d"), # Date
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if pan:
|
|
97
|
+
message["fields"][2] = validate_pan(pan)
|
|
98
|
+
|
|
99
|
+
if amount:
|
|
100
|
+
message["fields"][4] = format_amount(amount)
|
|
101
|
+
|
|
102
|
+
if terminal_id:
|
|
103
|
+
message["fields"][41] = terminal_id
|
|
104
|
+
|
|
105
|
+
return message
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_response_code_description(code: str) -> str:
|
|
109
|
+
"""Get description for response code"""
|
|
110
|
+
descriptions = {
|
|
111
|
+
"00": "Approved",
|
|
112
|
+
"01": "Refer to card issuer",
|
|
113
|
+
"02": "Refer to card issuer, special condition",
|
|
114
|
+
"03": "Invalid merchant",
|
|
115
|
+
"04": "Pick up card",
|
|
116
|
+
"05": "Do not honor",
|
|
117
|
+
"06": "Error",
|
|
118
|
+
"07": "Pick up card, special condition",
|
|
119
|
+
"08": "Honor with identification",
|
|
120
|
+
"09": "Request in progress",
|
|
121
|
+
"10": "Approved, partial",
|
|
122
|
+
"11": "Approved, VIP",
|
|
123
|
+
"12": "Invalid transaction",
|
|
124
|
+
"13": "Invalid amount",
|
|
125
|
+
"14": "Invalid card number",
|
|
126
|
+
"15": "No such issuer",
|
|
127
|
+
}
|
|
128
|
+
return descriptions.get(code, "Unknown response code")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Core module for ISO 8583 message handling.
|
|
2
|
+
|
|
3
|
+
This module provides the fundamental building blocks for working with
|
|
4
|
+
ISO 8583 financial transaction messages:
|
|
5
|
+
|
|
6
|
+
- **Types**: Data types, enums, and field definitions
|
|
7
|
+
- **Parser**: Parse raw message strings into structured objects
|
|
8
|
+
- **Builder**: Build raw message strings from objects
|
|
9
|
+
- **Validator**: Validate message structure and content
|
|
10
|
+
- **EMV**: Handle Field 55 chip card data (TLV format)
|
|
11
|
+
- **Pool**: Object pooling for high-throughput scenarios
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .builder import ISO8583Builder
|
|
15
|
+
from .emv import build_emv_data, parse_emv_data
|
|
16
|
+
from .parser import ISO8583Parser
|
|
17
|
+
from .pool import MessagePool
|
|
18
|
+
from .types import (
|
|
19
|
+
BuildError,
|
|
20
|
+
CardNetwork,
|
|
21
|
+
FieldDefinition,
|
|
22
|
+
FieldType,
|
|
23
|
+
ISO8583Message,
|
|
24
|
+
ISO8583Version,
|
|
25
|
+
MessageClass,
|
|
26
|
+
MessageFunction,
|
|
27
|
+
ParseError,
|
|
28
|
+
detect_network,
|
|
29
|
+
get_field_definition,
|
|
30
|
+
)
|
|
31
|
+
from .validator import ISO8583Validator
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Parser
|
|
35
|
+
"ISO8583Parser",
|
|
36
|
+
# Builder
|
|
37
|
+
"ISO8583Builder",
|
|
38
|
+
# Validator
|
|
39
|
+
"ISO8583Validator",
|
|
40
|
+
# Types
|
|
41
|
+
"ISO8583Message",
|
|
42
|
+
"ISO8583Version",
|
|
43
|
+
"FieldType",
|
|
44
|
+
"FieldDefinition",
|
|
45
|
+
"CardNetwork",
|
|
46
|
+
"MessageClass",
|
|
47
|
+
"MessageFunction",
|
|
48
|
+
"ParseError",
|
|
49
|
+
"BuildError",
|
|
50
|
+
"get_field_definition",
|
|
51
|
+
"detect_network",
|
|
52
|
+
# EMV
|
|
53
|
+
"parse_emv_data",
|
|
54
|
+
"build_emv_data",
|
|
55
|
+
# Pool
|
|
56
|
+
"MessagePool",
|
|
57
|
+
]
|