subscriptions-to-csv 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.
- subscriptions_to_csv/__init__.py +26 -0
- subscriptions_to_csv/cli.py +100 -0
- subscriptions_to_csv/converter.py +209 -0
- subscriptions_to_csv-1.0.0.dist-info/METADATA +331 -0
- subscriptions_to_csv-1.0.0.dist-info/RECORD +8 -0
- subscriptions_to_csv-1.0.0.dist-info/WHEEL +5 -0
- subscriptions_to_csv-1.0.0.dist-info/entry_points.txt +2 -0
- subscriptions_to_csv-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Convert subscription lists to CSV with EUR conversion.
|
|
2
|
+
|
|
3
|
+
This package provides both a command-line tool and a Python library
|
|
4
|
+
for converting subscription data to CSV format with EUR conversion.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .converter import (
|
|
8
|
+
convert_subscriptions,
|
|
9
|
+
fetch_exchange_rate,
|
|
10
|
+
parse_subscription_data,
|
|
11
|
+
write_csv_file,
|
|
12
|
+
SubscriptionConverter,
|
|
13
|
+
SubscriptionParseError,
|
|
14
|
+
ExchangeRateError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"convert_subscriptions",
|
|
20
|
+
"fetch_exchange_rate",
|
|
21
|
+
"parse_subscription_data",
|
|
22
|
+
"write_csv_file",
|
|
23
|
+
"SubscriptionConverter",
|
|
24
|
+
"SubscriptionParseError",
|
|
25
|
+
"ExchangeRateError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Command-line interface for subscription-to-csv converter."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from .converter import convert_subscriptions, ExchangeRateError, SubscriptionParseError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_arguments():
|
|
10
|
+
"""Parse command line arguments."""
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description='Convert subscription list to CSV with EUR conversion',
|
|
13
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
14
|
+
epilog="""
|
|
15
|
+
Input File Format:
|
|
16
|
+
Each subscription consists of 2 lines:
|
|
17
|
+
Line 1: Service name (e.g., "Netflix")
|
|
18
|
+
Line 2: Price with currency, optionally preceded by tabs/spaces (e.g., "$15.99 USD" or "€9.99")
|
|
19
|
+
|
|
20
|
+
Example input file format:
|
|
21
|
+
Netflix
|
|
22
|
+
\t12.99 €
|
|
23
|
+
Spotify
|
|
24
|
+
\t9.99 €
|
|
25
|
+
|
|
26
|
+
Supported currencies: USD ($), EUR (€)
|
|
27
|
+
All prices are converted to EUR in the output CSV.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
subscriptions-to-csv subscriptions.txt output.csv
|
|
31
|
+
subscriptions-to-csv --input subscriptions.txt --output output.csv
|
|
32
|
+
subscriptions-to-csv # uses default files
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument('input_pos', nargs='?', help='Input file containing subscriptions')
|
|
36
|
+
parser.add_argument('output_pos', nargs='?', help='Output CSV file')
|
|
37
|
+
parser.add_argument('--input', '-i', help='Input file containing subscriptions')
|
|
38
|
+
parser.add_argument('--output', '-o', help='Output CSV file')
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
# Use optional args if provided, otherwise positional, otherwise defaults
|
|
42
|
+
args.input = args.input or args.input_pos or 'subscriptions.txt'
|
|
43
|
+
args.output = args.output or args.output_pos or 'subscriptions.csv'
|
|
44
|
+
|
|
45
|
+
return args
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_summary(output_file: str, subscriptions: list, total_eur: float):
|
|
49
|
+
"""Print summary of the conversion."""
|
|
50
|
+
print(f'Created {output_file}')
|
|
51
|
+
print('First few lines:')
|
|
52
|
+
|
|
53
|
+
# Show header and first few rows
|
|
54
|
+
if subscriptions:
|
|
55
|
+
print('Service,Price,Currency,PriceEUR')
|
|
56
|
+
for sub in subscriptions[:3]: # Show first 3 subscriptions
|
|
57
|
+
print(f"{sub['Service']},{sub['Price']},{sub['Currency']},{sub['PriceEUR']}")
|
|
58
|
+
if len(subscriptions) > 3:
|
|
59
|
+
print('...')
|
|
60
|
+
|
|
61
|
+
print(f'Total in EUR: {total_eur:.2f}')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
"""Main function to run the subscription converter CLI."""
|
|
66
|
+
args = parse_arguments()
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Check if input file exists
|
|
70
|
+
input_path = Path(args.input)
|
|
71
|
+
if not input_path.exists():
|
|
72
|
+
print(f"Error: Input file '{args.input}' does not exist", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
# Read input file
|
|
76
|
+
with open(args.input, 'r', encoding='utf-8') as f:
|
|
77
|
+
content = f.read()
|
|
78
|
+
|
|
79
|
+
# Convert subscriptions
|
|
80
|
+
subscriptions, total_eur = convert_subscriptions(content, args.output)
|
|
81
|
+
|
|
82
|
+
# Print summary
|
|
83
|
+
print_summary(args.output, subscriptions, total_eur)
|
|
84
|
+
|
|
85
|
+
except ExchangeRateError as e:
|
|
86
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
except SubscriptionParseError as e:
|
|
89
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except FileNotFoundError as e:
|
|
92
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
main()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Convert subscription lists to CSV with EUR conversion."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import csv
|
|
6
|
+
from typing import List, Dict, Union, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SubscriptionParseError(Exception):
|
|
10
|
+
"""Exception raised when subscription data cannot be parsed."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExchangeRateError(Exception):
|
|
15
|
+
"""Exception raised when exchange rate cannot be fetched."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fetch_exchange_rate() -> float:
|
|
20
|
+
"""Fetch USD to EUR exchange rate from API.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Exchange rate as float
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
Falls back to 1.0 if the API request fails
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
with urllib.request.urlopen('https://api.exchangerate-api.com/v4/latest/USD') as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
return data['rates']['EUR']
|
|
32
|
+
except Exception:
|
|
33
|
+
return 1.0 # fallback
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_subscription_data(content: Union[str, List[str]], rate: float) -> List[Dict[str, str]]:
|
|
37
|
+
"""Parse subscription data from string or list of lines into list of dictionaries.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
content: Subscription data as string or list of lines
|
|
41
|
+
rate: USD to EUR exchange rate
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of subscription dictionaries with Service, Price, Currency, and PriceEUR keys
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
SubscriptionParseError: If the content format is invalid
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(content, str):
|
|
50
|
+
lines = [line.strip() for line in content.strip().split('\n') if line.strip()]
|
|
51
|
+
else:
|
|
52
|
+
lines = [line.strip() for line in content if line.strip()]
|
|
53
|
+
|
|
54
|
+
subscriptions = []
|
|
55
|
+
for i in range(0, len(lines), 2):
|
|
56
|
+
if i + 1 >= len(lines):
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
service = lines[i]
|
|
60
|
+
price_line = lines[i + 1].lstrip('\t')
|
|
61
|
+
parts = price_line.split()
|
|
62
|
+
|
|
63
|
+
if not parts:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
price_str = parts[0].lstrip('$').lstrip('€')
|
|
67
|
+
try:
|
|
68
|
+
price = float(price_str)
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue # Skip invalid prices
|
|
71
|
+
|
|
72
|
+
# Determine currency from the price string
|
|
73
|
+
if parts[0].startswith('€') or (len(parts) > 1 and parts[1].upper() in ('EUR', '€')):
|
|
74
|
+
currency = 'EUR'
|
|
75
|
+
elif parts[0].startswith('$') or (len(parts) > 1 and parts[1].upper() == 'USD'):
|
|
76
|
+
currency = 'USD'
|
|
77
|
+
else:
|
|
78
|
+
currency = parts[1] if len(parts) > 1 else 'EUR'
|
|
79
|
+
|
|
80
|
+
if currency.upper() == 'USD':
|
|
81
|
+
eur_price = price * rate
|
|
82
|
+
elif currency.upper() in ('EUR', '€'):
|
|
83
|
+
eur_price = price
|
|
84
|
+
else:
|
|
85
|
+
raise SubscriptionParseError(f"Unsupported currency '{currency}' for service '{service}'")
|
|
86
|
+
|
|
87
|
+
subscriptions.append({
|
|
88
|
+
'Service': service,
|
|
89
|
+
'Price': f'{price:.2f}',
|
|
90
|
+
'Currency': currency.upper(),
|
|
91
|
+
'PriceEUR': f'{eur_price:.2f}'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return subscriptions
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def write_csv_file(subscriptions: List[Dict[str, str]], output_file: str) -> float:
|
|
98
|
+
"""Write subscriptions to CSV file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
subscriptions: List of subscription dictionaries
|
|
102
|
+
output_file: Path to output CSV file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Total EUR amount
|
|
106
|
+
"""
|
|
107
|
+
total_eur = sum(float(sub['PriceEUR']) for sub in subscriptions)
|
|
108
|
+
with open(output_file, 'w', newline='') as csvfile:
|
|
109
|
+
fieldnames = ['Service', 'Price', 'Currency', 'PriceEUR']
|
|
110
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
111
|
+
writer.writeheader()
|
|
112
|
+
writer.writerows(subscriptions)
|
|
113
|
+
return total_eur
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def convert_subscriptions(
|
|
117
|
+
content: Union[str, List[str]],
|
|
118
|
+
output_file: Optional[str] = None,
|
|
119
|
+
exchange_rate: Optional[float] = None
|
|
120
|
+
) -> Tuple[List[Dict[str, str]], float]:
|
|
121
|
+
"""Convert subscription data to CSV format with EUR conversion.
|
|
122
|
+
|
|
123
|
+
This is the main library function for converting subscription data.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: Subscription data as string or list of lines
|
|
127
|
+
output_file: Optional path to write CSV file to
|
|
128
|
+
exchange_rate: Optional exchange rate (will fetch if not provided)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (subscriptions_list, total_eur_amount)
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ExchangeRateError: If exchange rate cannot be fetched
|
|
135
|
+
SubscriptionParseError: If subscription data is malformed
|
|
136
|
+
"""
|
|
137
|
+
rate = exchange_rate if exchange_rate is not None else fetch_exchange_rate()
|
|
138
|
+
subscriptions = parse_subscription_data(content, rate)
|
|
139
|
+
|
|
140
|
+
if output_file:
|
|
141
|
+
total_eur = write_csv_file(subscriptions, output_file)
|
|
142
|
+
else:
|
|
143
|
+
total_eur = sum(float(sub['PriceEUR']) for sub in subscriptions)
|
|
144
|
+
|
|
145
|
+
return subscriptions, total_eur
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SubscriptionConverter:
|
|
149
|
+
"""Advanced converter class for subscription data processing.
|
|
150
|
+
|
|
151
|
+
This class provides more control over the conversion process,
|
|
152
|
+
allowing you to reuse exchange rates and customize behavior.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, exchange_rate: Optional[float] = None):
|
|
156
|
+
"""Initialize the converter.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
exchange_rate: Optional exchange rate to use (will fetch if not provided)
|
|
160
|
+
"""
|
|
161
|
+
self.exchange_rate = exchange_rate
|
|
162
|
+
|
|
163
|
+
def set_exchange_rate(self, rate: float):
|
|
164
|
+
"""Set a custom exchange rate."""
|
|
165
|
+
self.exchange_rate = rate
|
|
166
|
+
|
|
167
|
+
def get_exchange_rate(self) -> float:
|
|
168
|
+
"""Get the current exchange rate, fetching if necessary."""
|
|
169
|
+
if self.exchange_rate is None:
|
|
170
|
+
self.exchange_rate = fetch_exchange_rate()
|
|
171
|
+
return self.exchange_rate
|
|
172
|
+
|
|
173
|
+
def convert(self, content: Union[str, List[str]]) -> List[Dict[str, str]]:
|
|
174
|
+
"""Convert subscription content to list of dictionaries.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
content: Subscription data as string or list of lines
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of subscription dictionaries
|
|
181
|
+
"""
|
|
182
|
+
rate = self.get_exchange_rate()
|
|
183
|
+
return parse_subscription_data(content, rate)
|
|
184
|
+
|
|
185
|
+
def convert_to_csv(self, content: Union[str, List[str]], output_file: str) -> float:
|
|
186
|
+
"""Convert subscription content and write to CSV file.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
content: Subscription data as string or list of lines
|
|
190
|
+
output_file: Path to output CSV file
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Total EUR amount
|
|
194
|
+
"""
|
|
195
|
+
subscriptions = self.convert(content)
|
|
196
|
+
return write_csv_file(subscriptions, output_file)
|
|
197
|
+
|
|
198
|
+
def convert_with_total(self, content: Union[str, List[str]]) -> Tuple[List[Dict[str, str]], float]:
|
|
199
|
+
"""Convert subscription content and return data with total.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
content: Subscription data as string or list of lines
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (subscriptions_list, total_eur_amount)
|
|
206
|
+
"""
|
|
207
|
+
subscriptions = self.convert(content)
|
|
208
|
+
total_eur = sum(float(sub['PriceEUR']) for sub in subscriptions)
|
|
209
|
+
return subscriptions, total_eur
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: subscriptions-to-csv
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Convert subscription lists to CSV with EUR conversion
|
|
5
|
+
Author: Subscription Converter Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/MBanucu/subscriptions-to-csv
|
|
8
|
+
Keywords: csv,subscriptions,eur,conversion,finance
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
23
|
+
Requires-Python: >=3.6
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Subscriptions to CSV
|
|
27
|
+
|
|
28
|
+
A Python package built as a Nix flake utility that provides both CLI and library functionality to convert subscription lists into CSV files with EUR conversions and totals. Includes comprehensive type hints, error handling, and a full test suite.
|
|
29
|
+
|
|
30
|
+
## Description
|
|
31
|
+
|
|
32
|
+
This tool processes subscription data (from files or strings) containing service names and prices, generates CSV output with columns for Service, Price, Currency, and Price in EUR (with automatic USD to EUR conversion), and calculates total sums in EUR.
|
|
33
|
+
|
|
34
|
+
Available as both:
|
|
35
|
+
- **Command-line tool**: Process files directly from the terminal
|
|
36
|
+
- **Python library**: Import and use programmatically in your applications
|
|
37
|
+
|
|
38
|
+
The project includes comprehensive unit tests covering all major functionality and supports PyPI distribution.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Option 1: PyPI (Library + CLI)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install subscriptions-to-csv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This installs both the command-line tool and Python library.
|
|
49
|
+
|
|
50
|
+
### Option 2: Nix Flake (Development/Direct Usage)
|
|
51
|
+
|
|
52
|
+
Ensure you have Nix installed with flakes enabled.
|
|
53
|
+
|
|
54
|
+
#### Clone the Repository
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/MBanucu/subscriptions-to-csv.git
|
|
58
|
+
cd subscriptions-to-csv
|
|
59
|
+
|
|
60
|
+
# Allow direnv to load the .envrc file (one-time setup)
|
|
61
|
+
direnv allow
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The project uses [direnv](https://direnv.net/) for automatic development environment loading. After running `direnv allow`, the Nix devShell will be automatically activated whenever you enter the directory.
|
|
65
|
+
|
|
66
|
+
#### Direct from GitHub
|
|
67
|
+
|
|
68
|
+
You can also use this flake directly from GitHub without cloning:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Run with default files
|
|
72
|
+
nix run github:MBanucu/subscriptions-to-csv#subscriptions-to-csv
|
|
73
|
+
|
|
74
|
+
# Specify input and output files
|
|
75
|
+
nix run github:MBanucu/subscriptions-to-csv#subscriptions-to-csv path/to/input.txt path/to/output.csv
|
|
76
|
+
|
|
77
|
+
# Show help
|
|
78
|
+
nix run github:MBanucu/subscriptions-to-csv#subscriptions-to-csv -- --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This approach allows you to use the tool immediately without downloading the source code.
|
|
82
|
+
|
|
83
|
+
**Note**: When using `nix run` directly from GitHub, use positional arguments for input/output files or the `--` separator before option flags. Both approaches work the same way. Options work normally when running locally after cloning.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### CLI Usage
|
|
88
|
+
|
|
89
|
+
#### Basic Usage
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Enter the development shell (or use direnv for automatic loading)
|
|
93
|
+
nix develop
|
|
94
|
+
|
|
95
|
+
# Run the converter
|
|
96
|
+
subscriptions-to-csv
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This will read `subscriptions.txt` and output `subscriptions.csv`.
|
|
100
|
+
|
|
101
|
+
**Note**: If you have direnv installed, the development shell will be automatically activated when you enter the directory, making the `nix develop` step unnecessary.
|
|
102
|
+
|
|
103
|
+
### Custom Files
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Specify input and output files (positional)
|
|
107
|
+
nix run .#subscriptions-to-csv path/to/input.txt path/to/output.csv
|
|
108
|
+
|
|
109
|
+
# Or using options
|
|
110
|
+
nix run .#subscriptions-to-csv --input path/to/input.txt --output path/to/output.csv
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Direct Run
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
nix run .#subscriptions-to-csv
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Help
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Show usage information
|
|
123
|
+
nix run .#subscriptions-to-csv -- --help
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Note: The `--` separates nix arguments from application arguments.
|
|
127
|
+
|
|
128
|
+
## Library Usage
|
|
129
|
+
|
|
130
|
+
When installed via pip, you can use the package as a Python library:
|
|
131
|
+
|
|
132
|
+
### Basic Usage
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from subscriptions_to_csv import convert_subscriptions
|
|
136
|
+
|
|
137
|
+
# Convert from string data
|
|
138
|
+
data = """Netflix
|
|
139
|
+
$15.99 USD
|
|
140
|
+
Spotify
|
|
141
|
+
€9.99"""
|
|
142
|
+
|
|
143
|
+
subscriptions, total = convert_subscriptions(data)
|
|
144
|
+
print(f"Total: €{total:.2f}")
|
|
145
|
+
for sub in subscriptions:
|
|
146
|
+
print(f"{sub['Service']}: {sub['Price']} {sub['Currency']} = €{sub['PriceEUR']}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Advanced Usage
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from subscriptions_to_csv import SubscriptionConverter, fetch_exchange_rate
|
|
153
|
+
|
|
154
|
+
# Manual control over exchange rates
|
|
155
|
+
converter = SubscriptionConverter()
|
|
156
|
+
converter.set_exchange_rate(0.85) # Set custom rate
|
|
157
|
+
|
|
158
|
+
# Convert and get data
|
|
159
|
+
subscriptions = converter.convert("Netflix\n$15.99 USD")
|
|
160
|
+
total, count = converter.convert_with_total("Netflix\n$15.99 USD")
|
|
161
|
+
|
|
162
|
+
# Write to CSV file
|
|
163
|
+
converter.convert_to_csv("Netflix\n$15.99 USD", "output.csv")
|
|
164
|
+
|
|
165
|
+
# Individual functions
|
|
166
|
+
rate = fetch_exchange_rate()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Input Format
|
|
170
|
+
|
|
171
|
+
The input file should contain subscription data in the following format:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Service Name
|
|
175
|
+
Price Currency
|
|
176
|
+
Service Name
|
|
177
|
+
Price Currency
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Spotify
|
|
184
|
+
12.99 €
|
|
185
|
+
Netflix
|
|
186
|
+
19.99 €
|
|
187
|
+
GutHub Copilot Pro
|
|
188
|
+
$10.00 USD
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Supported currencies: € (Euro), USD (automatically converted to EUR).
|
|
192
|
+
|
|
193
|
+
## Output
|
|
194
|
+
|
|
195
|
+
The output CSV contains:
|
|
196
|
+
|
|
197
|
+
- **Service**: The subscription name
|
|
198
|
+
- **Price**: The original price
|
|
199
|
+
- **Currency**: The original currency
|
|
200
|
+
- **PriceEUR**: The price in EUR (converted if necessary)
|
|
201
|
+
|
|
202
|
+
Plus a total sum in EUR printed to the console.
|
|
203
|
+
|
|
204
|
+
Example output:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Service,Price,Currency,PriceEUR
|
|
208
|
+
Spotify,12.99,€,12.99
|
|
209
|
+
Netflix,19.99,€,19.99
|
|
210
|
+
GutHub Copilot Pro,10.00,USD,8.62
|
|
211
|
+
Total in EUR: 41.60
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Configuration
|
|
215
|
+
|
|
216
|
+
- **Input file**: Default `subscriptions.txt`, can be overridden with `--input` or positional argument
|
|
217
|
+
- **Output file**: Default `subscriptions.csv`, can be overridden with `--output` or positional argument
|
|
218
|
+
- **Exchange rate**: Automatically fetched from exchangerate-api.com
|
|
219
|
+
- **Fallback**: If API fails, uses rate 1.0
|
|
220
|
+
|
|
221
|
+
## Test Coverage
|
|
222
|
+
|
|
223
|
+
The project includes comprehensive unit tests covering:
|
|
224
|
+
- Command-line argument parsing (default, positional, optional)
|
|
225
|
+
- Exchange rate API fetching with fallback behavior
|
|
226
|
+
- Subscription data parsing and currency conversion
|
|
227
|
+
- CSV file generation and total calculations
|
|
228
|
+
- Integration testing of the full workflow
|
|
229
|
+
|
|
230
|
+
## Requirements
|
|
231
|
+
|
|
232
|
+
### CLI Usage
|
|
233
|
+
- Nix with flakes support (for nix-based installation)
|
|
234
|
+
- Internet connection for exchange rate fetching
|
|
235
|
+
|
|
236
|
+
### Library Usage
|
|
237
|
+
- Python 3.6+ (3.13 recommended)
|
|
238
|
+
- pip for installation
|
|
239
|
+
- Internet connection for exchange rate fetching
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
### Project Structure
|
|
244
|
+
|
|
245
|
+
The project is structured as a proper Python package:
|
|
246
|
+
|
|
247
|
+
- `flake.nix`: Nix flake configuration for multi-platform builds
|
|
248
|
+
- `flake.lock`: Nix flake lock file
|
|
249
|
+
- `.envrc`: Direnv configuration for automatic devShell loading
|
|
250
|
+
- `pyproject.toml`: Python package configuration and build system
|
|
251
|
+
- `subscriptions_to_csv/`: Main Python package
|
|
252
|
+
- `__init__.py`: Package initialization and exports
|
|
253
|
+
- `converter.py`: Core conversion functions and classes
|
|
254
|
+
- `cli.py`: Command-line interface
|
|
255
|
+
- `tests/test_main.py`: Comprehensive unit test suite
|
|
256
|
+
|
|
257
|
+
### Building
|
|
258
|
+
|
|
259
|
+
Build the Python package:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
nix build
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
This creates a proper Python package using `buildPythonPackage` that can be installed and distributed.
|
|
266
|
+
|
|
267
|
+
### Testing
|
|
268
|
+
|
|
269
|
+
Run the comprehensive test suite including CLI integration tests:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Run unit tests (direnv automatically loads environment)
|
|
273
|
+
pytest
|
|
274
|
+
|
|
275
|
+
# Or manually enter devShell and run tests
|
|
276
|
+
nix develop --command pytest
|
|
277
|
+
|
|
278
|
+
# Run flake checks (includes CLI functionality tests)
|
|
279
|
+
nix flake check
|
|
280
|
+
|
|
281
|
+
# Run specific flake checks
|
|
282
|
+
nix build .#checks.x86_64-linux.help-test
|
|
283
|
+
nix build .#checks.x86_64-linux.basic-test
|
|
284
|
+
nix build .#checks.x86_64-linux.named-args-test
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The flake checks verify that:
|
|
288
|
+
- The `--help` command works correctly
|
|
289
|
+
- Basic functionality with sample data works
|
|
290
|
+
- Positional and named arguments function properly
|
|
291
|
+
|
|
292
|
+
### Testing
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Run the test suite (environment loads automatically with direnv)
|
|
296
|
+
pytest
|
|
297
|
+
|
|
298
|
+
# Or enter devShell manually
|
|
299
|
+
nix develop
|
|
300
|
+
pytest
|
|
301
|
+
|
|
302
|
+
# Run specific tests
|
|
303
|
+
pytest tests/test_main.py
|
|
304
|
+
pytest -k "parse" # Run tests matching pattern
|
|
305
|
+
|
|
306
|
+
# Manual testing - Run with defaults
|
|
307
|
+
nix run .#subscriptions-to-csv
|
|
308
|
+
|
|
309
|
+
# Test CLI options
|
|
310
|
+
nix run .#subscriptions-to-csv -- --help
|
|
311
|
+
|
|
312
|
+
# Check the output CSV and total
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Code Style
|
|
316
|
+
|
|
317
|
+
See AGENTS.md for detailed coding guidelines.
|
|
318
|
+
|
|
319
|
+
## Contributing
|
|
320
|
+
|
|
321
|
+
1. Fork the repository
|
|
322
|
+
2. Create a feature branch
|
|
323
|
+
3. Make changes
|
|
324
|
+
4. Run tests: `pytest` (direnv automatically loads the environment)
|
|
325
|
+
5. Test CLI: `nix run .#subscriptions-to-csv -- --help`
|
|
326
|
+
6. Test library: `python3 -c "from subscriptions_to_csv import convert_subscriptions; print('Library works')"`
|
|
327
|
+
7. Submit a pull request
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
This project is open source. Please check the license file if present.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
subscriptions_to_csv/__init__.py,sha256=aZiEm8F7jC9z_0KN3hJdhsH6lgA2Y58aermSenJJEY0,635
|
|
2
|
+
subscriptions_to_csv/cli.py,sha256=etEm9R9BXlywSKD8YMRIMKGpOOroVF6SPbkhNRQOjbk,3267
|
|
3
|
+
subscriptions_to_csv/converter.py,sha256=EZ8ziBxGFkJYyzOvtQEFLP5uP-SevPYa34-6X--x9JU,6739
|
|
4
|
+
subscriptions_to_csv-1.0.0.dist-info/METADATA,sha256=kimiPYwOK1dRK3datpRgNY_5v7_2QgaI4XwwpDRyW8Y,9068
|
|
5
|
+
subscriptions_to_csv-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
subscriptions_to_csv-1.0.0.dist-info/entry_points.txt,sha256=euesE9jMcLXw3mocwe4eGn0EVLoIurCg_nPc6lNaXkA,71
|
|
7
|
+
subscriptions_to_csv-1.0.0.dist-info/top_level.txt,sha256=AxP-dG7UH18VV3vIIXuwPM9VWz3Lwxy4rhUNhsrEqN8,21
|
|
8
|
+
subscriptions_to_csv-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
subscriptions_to_csv
|