change to create an ics file instead of using google calender

This commit is contained in:
Ugric
2026-01-20 02:34:25 +00:00
parent d81730d0b4
commit b40d0a5485
4 changed files with 127 additions and 423 deletions

6
.gitignore vendored
View File

@@ -1,6 +1,6 @@
credentials.json response.txt
token.json calendar.ics
timetableurl old_calendar.ics
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

18
Pipfile
View File

@@ -1,18 +0,0 @@
[[source]]
name = "pypi"
verify_ssl = true
url = "https://pypi.org/simple"
[packages]
google-api-python-client = "*"
"oauth2client" = "*"
requests = "*"
"bs4" = "*"
pyjsparser = "*"
pytz = "*"
[dev-packages]
pylint = "*"
[requires]
python_version = "3.6"

275
Pipfile.lock generated
View File

@@ -1,275 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "ffdcc7d81db0f5c5be8a6c0e7f3d57a38eff7d66c904d23adfc53ba06e7af315"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"beautifulsoup4": {
"hashes": [
"sha256:194ec62a25438adcb3fdb06378b26559eda1ea8a747367d34c33cef9c7f48d57",
"sha256:90f8e61121d6ae58362ce3bed8cd997efb00c914eae0ff3d363c32f9a9822d10",
"sha256:f0abd31228055d698bb392a826528ea08ebb9959e6bea17c606fd9c9009db938"
],
"version": "==4.6.3"
},
"bs4": {
"hashes": [
"sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
],
"index": "pypi",
"version": "==0.0.1"
},
"cachetools": {
"hashes": [
"sha256:90f1d559512fc073483fe573ef5ceb39bf6ad3d39edc98dc55178a2b2b176fa3",
"sha256:d1c398969c478d336f767ba02040fa22617333293fb0b8968e79b16028dfee35"
],
"version": "==2.1.0"
},
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.8.24"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"google-api-python-client": {
"hashes": [
"sha256:5d5cb02c6f3112c68eed51b74891a49c0e35263380672d662f8bfe85b8114d7c",
"sha256:7cc47cf80b25ecd7f3d917ea247bb6c62587514e40604ae29c47c0e4ebd1174b"
],
"index": "pypi",
"version": "==1.7.4"
},
"google-auth": {
"hashes": [
"sha256:9ca363facbf2622d9ba828017536ccca2e0f58bd15e659b52f312172f8815530",
"sha256:a4cf9e803f2176b5de442763bd339b313d3f1ed3002e3e1eb6eec1d7c9bbc9b4"
],
"version": "==1.5.1"
},
"google-auth-httplib2": {
"hashes": [
"sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445",
"sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08"
],
"version": "==0.0.3"
},
"httplib2": {
"hashes": [
"sha256:e71daed9a0e6373642db61166fa70beecc9bf04383477f84671348c02a04cbdf"
],
"version": "==0.11.3"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"oauth2client": {
"hashes": [
"sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac",
"sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"
],
"index": "pypi",
"version": "==4.1.3"
},
"pyasn1": {
"hashes": [
"sha256:b9d3abc5031e61927c82d4d96c1cec1e55676c1a991623cfed28faea73cdd7ca",
"sha256:f58f2a3d12fd754aa123e9fa74fb7345333000a035f3921dbdaa08597aa53137"
],
"version": "==0.4.4"
},
"pyasn1-modules": {
"hashes": [
"sha256:a0cf3e1842e7c60fde97cb22d275eb6f9524f5c5250489e292529de841417547",
"sha256:a38a8811ea784c0136abfdba73963876328f66172db21a05a82f9515909bfb4e"
],
"version": "==0.2.2"
},
"pyjsparser": {
"hashes": [
"sha256:e4a659df3db42a2ff9fbc961eb6d4076a0b945e1aadfc20d48f913ad5dca011d"
],
"index": "pypi",
"version": "==2.5.2"
},
"pytz": {
"hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
],
"index": "pypi",
"version": "==2018.5"
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"index": "pypi",
"version": "==2.19.1"
},
"rsa": {
"hashes": [
"sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66",
"sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487"
],
"version": "==4.0"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"uritemplate": {
"hashes": [
"sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd",
"sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd",
"sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
],
"version": "==3.0.0"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version < '4'",
"version": "==1.23"
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
"sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
],
"version": "==2.0.4"
},
"isort": {
"hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==4.3.4"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
"sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
"sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
"sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
"sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
"sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
"sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
"sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
"sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
"sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
"sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
"sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
"sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
"sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
"sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
"sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
"sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
"sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
"sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
"sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
"sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
"sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
"sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
"sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
"sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
"sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
"sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
"sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
"sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
],
"version": "==1.3.1"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"pylint": {
"hashes": [
"sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
"sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
],
"index": "pypi",
"version": "==2.1.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"wrapt": {
"hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
],
"version": "==1.10.11"
}
}
}

239
main.py
View File

@@ -1,188 +1,185 @@
import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import datetime, timezone from datetime import datetime, timezone
from googleapiclient.discovery import build
from httplib2 import Http
from oauth2client import file, client, tools
import pytz import pytz
import hashlib
import os
# We don't store this url in the source, as it is sensitive # ------------------------------------------------------------
URL = open("timetableurl").read().strip() # Config
# ------------------------------------------------------------
RESPONSE_FILE = "response.txt"
OLD_CALENDAR = "old_calendar.ics"
NEW_CALENDAR = "calendar.ics"
LOCAL_TZ = pytz.timezone("Europe/London")
# ------------------------------------------------------------
# Parsing timetable JS
# ------------------------------------------------------------
def parse_events(events_data): def parse_events(events_data):
# Replace date objects with tuples, easier to parse # Replace JS date objects
events_data = events_data.replace("new Date", "") events_data = events_data.replace("new Date", "")
cleaned_data = "" cleaned_data = ""
# Remove comments, properties to keys
for line in events_data.split("\n"): for line in events_data.split("\n"):
comment_pos = line.find("//") comment_pos = line.find("//")
if comment_pos != -1: if comment_pos != -1:
line = line[:comment_pos] line = line[:comment_pos]
if ":" in line: if ":" in line:
line_values = line.split(":") key, val = line.split(":", 1)
line = "'" + line_values[0] + "': " + line_values[1] line = f"'{key}': {val}"
cleaned_data += line + "\n" cleaned_data += line + "\n"
# Parse the event, as if it were a dict
parsed_data = eval(cleaned_data) parsed_data = eval(cleaned_data)
# Parse the datetime info
for event in parsed_data: for event in parsed_data:
if "start" in event: if "start" in event:
event["start"] = list(event["start"]) s = list(event["start"])
event["start"][1] += 1 s[1] += 1
event["start"].append(0) s.append(0)
event["start"] = datetime(*event["start"]) event["start"] = LOCAL_TZ.localize(datetime(*s))
event["start"] = pytz.timezone("Europe/London").localize(event["start"])
if "end" in event: if "end" in event:
event["end"] = list(event["end"]) e = list(event["end"])
event["end"][1] += 1 e[1] += 1
event["end"].append(0) e.append(0)
event["end"] = datetime(*event["end"]) event["end"] = LOCAL_TZ.localize(datetime(*e))
event["end"] = pytz.timezone("Europe/London").localize(event["end"])
return parsed_data return parsed_data
def get_events_data(url): def get_events_data_from_file(path):
page_data = requests.get(url).text with open(path, "r", encoding="utf-8") as f:
page_data = f.read()
soup = BeautifulSoup(page_data, features="html.parser") soup = BeautifulSoup(page_data, features="html.parser")
source = "" for script in soup.head.find_all("script", {"type": "text/javascript"}):
for script in soup.head.findAll("script", {"type": "text/javascript"}):
if not script.has_attr("src"): if not script.has_attr("src"):
source = script.text source = script.text
break break
else:
raise RuntimeError("Could not find inline timetable script")
events_data = source.split("events:")[1].split("]")[0] +"]" return source.split("events:")[1].split("]")[0] + "]"
return events_data # ------------------------------------------------------------
# ICS helpers
# ------------------------------------------------------------
def ics_time(dt):
return dt.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
def create_google_event(event): def make_uid(event):
new_event = event.copy() key = f"{event['moduleDesc']}|{event['title']}|{event['start'].isoformat()}"
return hashlib.sha1(key.encode()).hexdigest() + "@timetable"
if not new_event:
return new_event
# Make the event the correct format
new_event["summary"] = event["moduleDesc"] + " - " + event["title"]
new_event["description"] = event["lecturer"] + " - " + event["room"]
new_event["end"] = {"dateTime": str(event["end"].isoformat()), "timeZone": "Europe/London"}
new_event["start"] = {"dateTime": str(event["start"].isoformat()), "timeZone": "Europe/London"}
new_event["reminders"] = {'useDefault': False,
'overrides': [{'method': 'popup', 'minutes': 30}]}
return new_event
# Read and write access def create_ics_event(event):
SCOPES = "https://www.googleapis.com/auth/calendar" return {
"uid": make_uid(event),
"summary": f"{event['moduleDesc']} - {event['title']}",
"description": f"{event['lecturer']} - {event['room']}",
"start": event["start"],
"end": event["end"],
}
def get_calendar_service(): # ------------------------------------------------------------
''' # Load existing calendar (UIDs only)
Connect to the google calendar service, and return the # ------------------------------------------------------------
service object
'''
store = file.Storage("token.json") def load_existing_uids(path):
creds = store.get() uids = set()
# Run prompt to get the google credentials if not os.path.exists(path):
if not creds or creds.invalid: return uids
flow = client.flow_from_clientsecrets("redentials.json", SCOPES)
creds = tools.run_flow(flow, store)
return build("calendar", "v3", http=creds.authorize(Http())) with open(path, "r", encoding="utf-8") as f:
current_uid = None
cancelled = False
for line in f:
line = line.strip()
def execute_batch(service, commands): if line == "BEGIN:VEVENT":
batch = service.new_batch_http_request() current_uid = None
batch_count = 0 cancelled = False
for command in commands: elif line.startswith("UID:"):
batch.add(command) current_uid = line[4:]
batch_count += 1
if batch_count > 999: elif line == "STATUS:CANCELLED":
batch.execute() cancelled = True
batch = service.new_batch_http_request() elif line == "END:VEVENT" and current_uid and not cancelled:
batch_count = 0 uids.add(current_uid)
if batch_count > 0: return uids
batch.execute()
# ------------------------------------------------------------
# Main
# ------------------------------------------------------------
def main(): def main():
type_to_color = {} events_js = get_events_data_from_file(RESPONSE_FILE)
# A queue of colors, where a color is removed when parsed_events = parse_events(events_js)
# when an event we have not seen before exists
color_queue = list(range(0, 12, 3))
service = get_calendar_service() new_events = [
create_ics_event(e)
for e in parsed_events
if e
]
# Get a list of all events in the future old_uids = load_existing_uids(OLD_CALENDAR)
results = service.events().list(timeMin=datetime.now().isoformat() + 'Z', calendarId='primary').execute() new_uids = {e["uid"] for e in new_events}
future_events = results.get("items", [])
cov_events = parse_events(get_events_data(URL)) now = ics_time(datetime.now(timezone.utc))
new_events = []
new_summaries = set() lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Timetable Sync//EN",
"CALSCALE:GREGORIAN",
]
for event in cov_events: # Add / update events
if not event: for ev in new_events:
continue lines.extend([
"BEGIN:VEVENT",
f"UID:{ev['uid']}",
f"DTSTAMP:{now}",
f"DTSTART:{ics_time(ev['start'])}",
f"DTEND:{ics_time(ev['end'])}",
f"SUMMARY:{ev['summary']}",
f"DESCRIPTION:{ev['description']}",
"END:VEVENT",
])
new_event = create_google_event(event) # Cancel removed events
for uid in old_uids - new_uids:
lines.extend([
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTAMP:{now}",
"STATUS:CANCELLED",
"END:VEVENT",
])
color_type = new_event["mainColor"] lines.append("END:VCALENDAR")
if color_type in type_to_color: with open(NEW_CALENDAR, "w", encoding="utf-8") as f:
colorId = type_to_color[color_type] f.write("\n".join(lines))
else:
colorId = color_queue.pop(0)
color_queue.append(colorId)
type_to_color[color_type] = colorId
new_event["colorId"] = colorId
new_events.append(new_event)
new_summaries.add(new_event["summary"])
# Make sure we remove old events so as not to create duplicates
if not future_events:
print('No existing events found')
else:
deletes = []
for existing_event in future_events:
if "summary" in existing_event and existing_event["summary"] in new_summaries:
deletes.append(service.events()
.delete(calendarId='primary',
eventId=existing_event['id']))
print(f'Removing {len(deletes)} existing events')
execute_batch(service, deletes)
inserts = []
for new_event in new_events:
inserts.append(service.events()
.insert(body=new_event,
calendarId='primary'))
print(f"Inserting {len(inserts)} new events")
execute_batch(service, inserts)
print(f"Added / updated: {len(new_events)}")
print(f"Removed: {len(old_uids - new_uids)}")
print(f"Wrote {NEW_CALENDAR}")
# ------------------------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
main() main()