File: frigcal-products/unzipped/searchcals.py

#!/usr/bin/env python3
"""
=====================================================================================
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.

-------------------------------------------------------------------------------------
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 os   # for chdir

# [2.0] for frozen app/exes, fix module+resource visibility (sys.path)
import fixfrozenpaths

# [2.0] make relative calendar paths map to frigcal's install folder
os.chdir(fixfrozenpaths.fetchMyInstallDir(__file__))   # absolute


import sys, 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__':
    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)



[Home page] Books Code Blog Python Author Train Find ©M.Lutz