shifter-converter 0.0.1__tar.gz

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.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: shifter-converter
3
+ Version: 0.0.1
4
+ Summary: A program for converting .Shifter files into .ics files
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: ics>=0.7.3
7
+ Requires-Dist: tzlocal>=5.3.1
8
+ Requires-Dist: rich>=15.0.0
9
+
10
+ # Shifter-Converter
11
+
12
+ A program to convert .Shifter files to .ics files
13
+
14
+ ## AI
15
+
16
+ AI was used for the following:
17
+
18
+ - Formatting output of database commands
19
+ - Instructions on setting up PyPI project
@@ -0,0 +1,10 @@
1
+ # Shifter-Converter
2
+
3
+ A program to convert .Shifter files to .ics files
4
+
5
+ ## AI
6
+
7
+ AI was used for the following:
8
+
9
+ - Formatting output of database commands
10
+ - Instructions on setting up PyPI project
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shifter-converter"
7
+ version = "0.0.1"
8
+ description = "A program for converting .Shifter files into .ics files"
9
+ readme = "README.md"
10
+ dependencies = [
11
+ "ics>=0.7.3",
12
+ "tzlocal>=5.3.1",
13
+ "rich>=15.0.0"
14
+ ]
15
+
16
+ [project.scripts]
17
+ shifter-converter = "shifter_converter.cli:main"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,59 @@
1
+ from rich.console import Console
2
+ from rich.prompt import Prompt
3
+ from rich.panel import Panel
4
+ import sqlite3
5
+ from .converter import main as converter_main
6
+ import questionary
7
+ import os
8
+ from .converter import validate_shifter_file
9
+
10
+ cwd = os.getcwd()
11
+ console = Console()
12
+
13
+
14
+ def get_file_path(name, folder=False):
15
+ return questionary.path(f"Give file path of {name}", default=cwd, only_directories=folder).ask()
16
+
17
+
18
+ def validate_file(file_path: str):
19
+ try:
20
+ db = sqlite3.connect(file_path)
21
+ if validate_shifter_file(db):
22
+ console.print("[green]File is a valid .Shifter file[/green]")
23
+ return db
24
+ else:
25
+ console.print("[yellow]File is a valid sqlite3 database, but doesn't contain the correct tables for a .Shifter file[/yellow]")
26
+ main()
27
+ except Exception as e:
28
+ console.print(f"[red]File is not a valid sqlite3 database: {e}[/red]")
29
+ main()
30
+
31
+
32
+ def main():
33
+ console.print(Panel.fit("Shifter Converter", style=" bold"))
34
+ console.print("Main menu:")
35
+ console.print("[1] Quick convert (.Shifter -> .ics)")
36
+ console.print("[2] Validate .Shifter")
37
+ console.print("[3] Exit")
38
+ option = Prompt.ask("Select option:", choices=["1", "2", "3", "4"], default="1")
39
+
40
+ if option == "1":
41
+ file_path = get_file_path(".Shifter file")
42
+ shifter_file = validate_file(file_path)
43
+ output_dir = get_file_path("output directory", folder=True)
44
+ converter_main(shifter_file, output_dir) # pyright: ignore[reportArgumentType]
45
+ console.print("[green]Succesfully converted .Shifter to .ics[/green]")
46
+ console.print(f"[cyan]Saved as [italic]{os.path.join(output_dir, 'output_calendar.ics')}[/italic][/cyan]")
47
+ main()
48
+ elif option == "2":
49
+ file_path = get_file_path(".Shifter file")
50
+ validate_file(file_path)
51
+ main()
52
+ elif option == "3":
53
+ quit()
54
+ else:
55
+ print("No clue how you did it, but you selected a number outside of the normal range")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,230 @@
1
+ from sqlite3 import Connection
2
+ from typing import Any, Optional
3
+ from datetime import datetime, date, time, timedelta
4
+ from dataclasses import dataclass, asdict
5
+ import ics
6
+ import re
7
+ from tzlocal import get_localzone
8
+ import os
9
+
10
+ shift_lables = ["_id", "text", "alarmTime", "alarm", "alarmOneDayBefore", "color", "textColor", "sequentialCode", "textSize", "alarmTime2", "alarm2", "alarmTwoDayBefore", "sound1", "sound2", "abbreviation", "startTime1", "endTime1", "startTime2", "endTime2", "splitShift", "performAction", "hourlyEarnings", "extraHourlyEarnings", "restTime", "shiftTime", "calculatedManualShiftDuration"]
11
+
12
+ local_tz = get_localzone()
13
+
14
+
15
+ @dataclass
16
+ class Shift():
17
+ _id: int
18
+ text: str
19
+ alarmTime: time
20
+ alarm: bool
21
+ alarmOneDayBefore: bool
22
+ color: str
23
+ textColor: str
24
+ sequentialCode: int
25
+ textSize: float
26
+ alarmTime2: time
27
+ alarm2: bool
28
+ alarmTwoDayBefore: bool
29
+ sound1: Optional[str]
30
+ sound2: Optional[str]
31
+ abbreviation: str
32
+ startTime1: time
33
+ endTime1: time
34
+ startTime2: time
35
+ endTime2: time
36
+ splitShift: bool
37
+ performAction: Optional[str]
38
+ hourlyEarnings: Optional[float]
39
+ extraHourlyEarnings: Optional[float]
40
+ restTime: Optional[float]
41
+ shiftTime: Optional[str]
42
+ calculatedManualShiftDuration: Optional[float]
43
+
44
+ @classmethod
45
+ def from_dict(cls, shift: dict[str, Any]):
46
+ # Turn boolean integers into actual python booleans
47
+ boolean_labels = ["alarm", "alarmOneDayBefore", "alarm2", "alarmTwoDayBefore", "splitShift"]
48
+ for label in boolean_labels:
49
+ if shift.get(label, 0) == 0:
50
+ shift[label] = False
51
+ else:
52
+ shift[label] = True
53
+
54
+ # Turn color 32 bit integers into hex color codes
55
+ color_labels = ["color", "textColor"]
56
+ for label in color_labels:
57
+ color_int = shift.get(label)
58
+ if color_int:
59
+ blue = color_int & 0xff
60
+ green = (color_int >> 8) & 0xff
61
+ red = (color_int >> 16) & 0xff
62
+
63
+ color_hex = ('#%02x%02x%02x' % (red, green, blue))
64
+ shift[label] = color_hex
65
+
66
+ # Turn time string into datetime.time object
67
+ time_labels = ["alarmTime", "alarmTime2", "startTime1", "endTime1", "startTime2", "endTime2"]
68
+ for label in time_labels:
69
+ time_value = shift.get(label)
70
+ if time_value:
71
+ datetime_object = datetime.strptime(time_value, '%H:%M').time()
72
+ shift[label] = datetime_object
73
+ return cls(**shift)
74
+
75
+
76
+ @dataclass
77
+ class DayWithShift(Shift):
78
+ date: date
79
+ note: str | None
80
+
81
+
82
+ @dataclass
83
+ class Day():
84
+ date: date
85
+ shift_id: Optional[int]
86
+ note: Optional[str]
87
+
88
+
89
+ def validate_shifter_file(db: Connection):
90
+ table_names: list[tuple[str]] = fetch_tables(db)
91
+
92
+ # Check if table format is correct
93
+ table_name_list = [row[0] for row in table_names]
94
+ if ('tablaTurnos' not in table_name_list) or ('dias' not in table_name_list):
95
+ return False
96
+ else:
97
+ return True
98
+
99
+
100
+ def fetch_tables(database: Connection) -> list[tuple[str]]:
101
+ """Find all tables in database and return them as a list"""
102
+ query = "SELECT name FROM sqlite_schema WHERE type = 'table'"
103
+ tables = database.execute(query).fetchall()
104
+ return tables
105
+
106
+
107
+ def fetch_shift_types(database: Connection) -> dict[int, Shift]:
108
+ """Fetch different types of shift and format them as a dict of _id and Shift object"""
109
+
110
+ # Fetch all shift types from database
111
+ query = "SELECT * FROM tablaTurnos"
112
+ sql_shifts: list[tuple[Any]] = database.execute(query).fetchall()
113
+
114
+ # Pair every shift value with a label from shift_labels
115
+ dict_shifts: list = [dict(zip(shift_lables, shift)) for shift in sql_shifts]
116
+
117
+ # Turn every shift into a Shift object
118
+ shifts: dict[int, Shift] = {}
119
+ for shift in dict_shifts:
120
+ shift_object = Shift.from_dict(shift)
121
+ shifts[shift_object._id] = shift_object
122
+
123
+ return shifts
124
+
125
+
126
+ def fetch_calendar(database: Connection) -> list[Any]:
127
+ """Fetch all notes from database and return them"""
128
+
129
+ # Fetch dates, shifts, and notes from table containing the dates
130
+ query = "SELECT fecha, turno1, notas FROM dias"
131
+ calendar: list[tuple[int, int | None, str | None]] = database.execute(query).fetchall()
132
+
133
+ formatted_calendar: list[Day] = []
134
+
135
+ for calendar_day in calendar:
136
+ # Turn SQL date into a datetime.date object
137
+ try:
138
+ insane_date_str = str(calendar_day[0]) # Format is YYYYMMDD, but the month is apperantly in integers, so january is 00, this has wasted 2 hours of my life (╯°□°)╯︵ ┻━┻
139
+ year = insane_date_str[:4]
140
+ month = int(insane_date_str[4:6]) + 1
141
+ month_str = f"{month:02d}"
142
+ day = insane_date_str[6:]
143
+
144
+ sane_date_str = f"{year}{month_str}{day}"
145
+
146
+ date_object = datetime.strptime(sane_date_str, "%Y%m%d").date()
147
+ except ValueError:
148
+ continue
149
+
150
+ # Get shift type and note of day
151
+ shift_type: int | None = calendar_day[1]
152
+ note: str | None = calendar_day[2]
153
+
154
+ # Unformated notes look like this: <p dir="ltr">TEXT_HERE</p>
155
+ # To parse this into normal text a regex match is used
156
+ if note:
157
+ note = re.findall(r'>(.*?)<', note)[0]
158
+
159
+ # Add formatted day to list as a Day object
160
+ formatted_calendar.append(Day(date_object, shift_type, note))
161
+
162
+ return formatted_calendar
163
+
164
+
165
+ def combine_calendar_and_shifts(days: list[Day], shifts: dict[int, Shift]) -> list[Day | DayWithShift]:
166
+ """Combine calendar entries with details about shifts"""
167
+ combined_calendar: list[Day | DayWithShift] = []
168
+
169
+ for day in days:
170
+ shift_id = day.shift_id
171
+ if shift_id:
172
+ shift: Shift = shifts[shift_id]
173
+ shift_day = DayWithShift(date=day.date, note=day.note, **asdict(shift))
174
+ combined_calendar.append(shift_day)
175
+ else:
176
+ combined_calendar.append(day)
177
+
178
+ return combined_calendar
179
+
180
+
181
+ def add_ics_event(calendar: ics.Calendar, name: str, start_date: date, start_time: time = time(0, 0), end_time=None, all_day=False, abreviation=None, second_part=False):
182
+ event = ics.Event()
183
+
184
+ event.name = name
185
+ event.begin = datetime.combine(start_date, start_time).replace(tzinfo=None).replace(tzinfo=local_tz)
186
+
187
+ if all_day:
188
+ event.make_all_day()
189
+ event.uid = f"note-{start_date.strftime('%Y-%m-%d')}-{name[:10].replace(' ', '')}"
190
+ elif end_time:
191
+ end_date = start_date if not (second_part or (start_time > end_time)) else start_date + timedelta(days=1)
192
+ event.end = datetime.combine(end_date, end_time).replace(tzinfo=local_tz)
193
+ event.uid = f"{abreviation if abreviation else 'shift'}-{start_date.strftime('%Y-%m-%d')}-{start_time.strftime('%H%M')}"
194
+ else:
195
+ print("ERROR: no end time found")
196
+
197
+ calendar.events.add(event)
198
+
199
+
200
+ def shifts_to_ics(shifts: list[Day | DayWithShift], output_dir):
201
+ ics_calendar = ics.Calendar()
202
+
203
+ for day in shifts:
204
+ if isinstance(day, DayWithShift):
205
+ if day.startTime1 == day.endTime1:
206
+ add_ics_event(ics_calendar, day.text, day.date, abreviation=day.abbreviation, all_day=True)
207
+ else:
208
+ add_ics_event(ics_calendar, day.text, day.date, start_time=day.startTime1, end_time=day.endTime1, abreviation=day.abbreviation)
209
+ if day.splitShift:
210
+ add_ics_event(ics_calendar, day.text, day.date, start_time=day.startTime2, end_time=day.endTime2, abreviation=f"{day.abbreviation}-2", second_part=True)
211
+ if day.note:
212
+ add_ics_event(ics_calendar, day.note, day.date, all_day=True)
213
+
214
+ with open(os.path.join(output_dir, 'output_calendar.ics'), 'w') as f:
215
+ f.write(ics_calendar.serialize())
216
+
217
+
218
+ def main(db: Connection, output_dir: str):
219
+ # Get information of shift types
220
+ shift_types: dict[int, Shift] = fetch_shift_types(db)
221
+
222
+ # Get all days and their corresponding shift_type and notes
223
+ calendar: list[Day] = fetch_calendar(db)
224
+
225
+ # Add shift details to the calendar items
226
+ combined_calendar = combine_calendar_and_shifts(calendar, shift_types)
227
+
228
+ shifts_to_ics(combined_calendar, output_dir)
229
+
230
+ db.close()
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: shifter-converter
3
+ Version: 0.0.1
4
+ Summary: A program for converting .Shifter files into .ics files
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: ics>=0.7.3
7
+ Requires-Dist: tzlocal>=5.3.1
8
+ Requires-Dist: rich>=15.0.0
9
+
10
+ # Shifter-Converter
11
+
12
+ A program to convert .Shifter files to .ics files
13
+
14
+ ## AI
15
+
16
+ AI was used for the following:
17
+
18
+ - Formatting output of database commands
19
+ - Instructions on setting up PyPI project
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/shifter_converter/__init__.py
4
+ src/shifter_converter/cli.py
5
+ src/shifter_converter/converter.py
6
+ src/shifter_converter.egg-info/PKG-INFO
7
+ src/shifter_converter.egg-info/SOURCES.txt
8
+ src/shifter_converter.egg-info/dependency_links.txt
9
+ src/shifter_converter.egg-info/entry_points.txt
10
+ src/shifter_converter.egg-info/requires.txt
11
+ src/shifter_converter.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ shifter-converter = shifter_converter.cli:main
@@ -0,0 +1,3 @@
1
+ ics>=0.7.3
2
+ tzlocal>=5.3.1
3
+ rich>=15.0.0
@@ -0,0 +1 @@
1
+ shifter_converter