File: frigcal-products/unzipped/searchcals.py
#!/usr/bin/env python3
r"""[3.0] (r else py3.13 escapes error)
=====================================================================================
searchcals.py: frigcal calendar-search utility script.
[2.0] This command-line tool searches all your frigcal calendar files' events
for a keyword, in event summaries, descriptions, categories, or all three. It
prints matching events' calendars and dates (in mm/dd/yyyy format and sorted by
date), to help you access them quickly in the frigcal main script's GUI.
For instance, you can jump to a matching event listed in this script's output
by entering its date in the GUI's GoTo input field in mm/dd/yyyy format, and
pressing Enter or GoTo. The GUI will move to the event's month and highlight
its day tile. In fact, dates output here can be copy-and-pasted into the GoTo
field directly (try Ctrl-c/v on Windows and Linux, and Command-c/v on Mac OS X).
This script is launched from and prints to a command-line interface (e.g.,
Terminal on Mac and Linux, and Command Prompt on Windows), and should be run
in your frigcal install folder, because it uses your normal frigcal_configs.py
settings to locate your calendar files in the same fashion as the GUI.
See also README.txt for running scripts in frozen app/executable packages.
[3.0] Caveat: PyInstaller console exes take a long time (N seconds) to open
on macOS, but a full app seems unwarranted, and splashscreen isn't supported.
This script is also subsumed by the new GUI Find, so this is mostly moot.
------------------------------------------------------------------------------------
USAGE
Example: search for occurrences of "bread" in all fields on Windows.
(open Command Prompt)
C:\...\somedir> cd C:\...\frigcal
C:\...\frigcal> py -3 searchcals.py bread -all
Example: search for all occurrences of "french" in summaries on Mac or Linux.
(open Terminal)
/.../somedir$ cd /.../frigcal
/.../frigcal$ python3 searchcals.py french -sum
Format:
[python] searchcals.py searchfor (-sum | -des | -cat | -all)
where:
[python] is your Python's name, and optional on some installs
searchfor stands for the term you wish to find
quote multiple words: use "new york" on Windows, 'new york' on Unix
the search is always case-insensitive (e.g., 'a' == 'A')
one of the four items in the (...) is used to specify a mode
-sum finds events having searchfor in summary fields only
-des finds events having searchfor in description fields only
-cat finds events having searchfor in category fields only
-all finds events having searchfor in summary, description, or category
Search is implemented by this command-line script and is not part of the GUI
itself, because adding this to the GUI would complicate an intentionally
simplistic interface for a very rare use case. Coding note: much of this
script parrots parts of icsfiletools.py (which is too GUI-oriented to reuse).
-------------------------------------------------------------------------------------
FULL EXAMPLE RUNS
The first two of the following run on Windows, the third on Unix (Mac OS X).
C:\...\frigcal> searchcals.py nasa -all
Using Python 3.5 and Tk 8.6
Searching for 'nasa' in -all
Parsing all calendars in Calendars
Searching calendar frigcal-default-calendar.ics
FOUND on date 07/05/2015
FOUND on date 07/15/2015
FOUND on date 08/04/2015
FOUND on date 08/24/2015
FOUND on date 08/25/2015
FOUND on date 08/26/2015
FOUND on date 12/31/2015
Searching calendar Holidays.ics
Searching calendar trips.ics
Searching calendar working--Full-Calendar-From-Outlook-July1112.ics
FOUND on date 08/30/2005
FOUND on date 08/31/2005
FOUND on date 09/01/2005
FOUND on date 05/13/2011
Number matches found: 11
C:\...\frigcal> searchcals.py "las vegas" -sum
Using Python 3.5 and Tk 8.6
Searching for 'las vegas' in -sum
Parsing all calendars in Calendars
Searching calendar frigcal-default-calendar.ics
FOUND on date 12/11/2016
FOUND on date 01/08/2017
Searching calendar Holidays.ics
Searching calendar trips.ics
Searching calendar working--Full-Calendar-From-Outlook-July1112.ics
FOUND on date 09/18/2006
Number matches found: 3
/.../frigcal$ python3 searchcals.py 'new york' -sum
Using Python 3.5 and Tk 8.5
Searching for 'new york' in -all
Parsing all calendars in Calendars
Searching calendar frigcal-default-calendar.ics
Searching calendar Holidays.ics
Searching calendar trips.ics
Searching calendar working--Full-Calendar-From-Outlook-July1112.ics
FOUND on date 12/13/2005
FOUND on date 12/14/2005
FOUND on date 12/15/2005
Number matches found: 3
=====================================================================================
"""
import sys, os # os for chdir
RunningOnMacOS = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
# [2.0] for frozen app/exes, fix module+resource visibility (sys.path)
import fixfrozenpaths
# [3.0] special case (yuck): pyinstaller, but not an app (see fix file)
if RunningOnMacOS and hasattr(sys, 'frozen'):
exepath = sys.argv[0]
exedir = os.path.dirname(os.path.abspath(exepath))
os.chdir(exedir) # for any "." file refs
sys.path.append(exedir) # for configs .py import
else:
# [2.0] make relative calendar paths map to frigcal's install folder
os.chdir(fixfrozenpaths.fetchMyInstallDir(__file__)) # absolute
print('Running in CWD:', os.getcwd())
import glob, shutil, traceback, time, datetime
from sharednames import Configs, trace
import icalendar # required 3rd-party package
def parse_and_search_ics_files(searchfor, mode):
"""
load events from ics file(s), search for search term per mode arg;
on any exception here, script aborts: there is no way to proceed;
this mimics icsfiletools.parse_ics_files, which has docs cut here;
"""
nummatches = 0
trace('Parsing all calendars in', Configs.icspath)
icsfiles = glob.glob(os.path.join(Configs.icspath, '*.ics'))
# parse/index each ics file's data
for icsfile in icsfiles: # for all ics files in folder
icsfilenopath = os.path.basename(icsfile) # strip directory path prefix
trace('Searching calendar', icsfilenopath)
# read ics text: use config=ics Unicode default (bytes works too)
unicode = icalendar.parser.DEFAULT_ENCODING # changeable in source code only
calfile = open(icsfile, 'r', encoding=unicode) # [1.7] but don't hardcode 'utf8'
try:
icstext = calfile.read() # all at once: small files
finally:
calfile.close() # exc or not (not 'with': eibti)
# parse ics text: 3rd party package(s), major dependency
icsarray = icalendar.cal.Calendar.from_ical(icstext) # .cal optional but explicit
# extract data used here, find searchterm
calmatches = []
for event in icsarray.walk('VEVENT'): # for all events in parsed calendar
# icalendar.prop.vDDDTypes: .dt is datetime.{date or datetime} with .m/d/y
startdate = event['DTSTART'] # required start date
# icalendar.prop.vText (VText): .to_ical() is bytes, str() decodes auto
labeltext = event.get('SUMMARY', '') # for main label, and view/edit dialog
extratext = event.get('DESCRIPTION', '') # for footer, and view/edit dialog
category = event.get('CATEGORIES', '') # for coloring, and view/edit dialog
if isinstance(category, list):
category = category[0] # if multiple, use just one (first) for coloring
# neutralize case differences (probably a good thing)
searchfor = searchfor.lower()
labeltext = labeltext.lower()
extratext = extratext.lower()
category = category.lower()
match = (
(mode == '-sum' and searchfor in labeltext) or
(mode == '-des' and searchfor in extratext) or
(mode == '-cat' and searchfor in category) or
(mode == '-all' and searchfor in labeltext + ' ' + extratext + ' ' + category))
if match:
# order parts for later sorting here
nummatches += 1
dt = startdate.dt
dateforsort = dt.year, dt.month, dt.day
calmatches.append(dateforsort)
# report this calendar's matches
if calmatches:
# order same as GUI's GoTo input here for copy/paste
for (yyyy, mm, dd) in sorted(calmatches):
print(' FOUND on date %02d/%02d/%04d' % (mm, dd, yyyy))
# goto next icsfile calendar
return nummatches
if __name__ == '__main__':
if RunningOnWindows:
#
# [3.0] auto deblur tkinter GUIs, whether run by python.exe or standalone exe;
# this may matter for tool scripts if standalone exes (e.g., clicked to run);
#
from ctypes import windll, c_int64
windll.user32.SetProcessDpiAwarenessContext(c_int64(-4)) # per monitor aware v2
usage = 'Usage: [python] searchcals.py searchfor (-sum | -des | -cat | -all)'
if len(sys.argv) != 3:
print(usage)
elif sys.argv[2] not in ['-sum', '-des', '-cat', '-all']:
print(usage)
else:
searchfor, mode = sys.argv[1:3]
print('Searching for', repr(searchfor), 'in', mode)
nummatches = parse_and_search_ics_files(searchfor, mode)
print('Number matches found:', nummatches)
input('Press Enter to close.') # may be clicked