@@ -15,11 +15,14 @@ | |||
"vdbd" | |||
], | |||
"cSpell.words": [ | |||
"Org's", | |||
"caat", | |||
"organisers", | |||
"outofoffice", | |||
"pattern", | |||
"seatingplan", | |||
"seatplangen", | |||
"strftime" | |||
] | |||
], | |||
"python.linting.pylintEnabled": true | |||
} |
@@ -46,8 +46,6 @@ offline_access | |||
## Installation | |||
Once the app has been created, git clone this repo, cd into its folder and install it into your user's Python PATH. | |||
```sh | |||
@@ -128,6 +126,43 @@ supper -c ~/.config/supper.yaml -o "Seating Plan {:%Y-%m-%d}.csv" | |||
This will output a file called `Seating Plan 2019-09-12.csv` | |||
### Multiple Weeks | |||
The script can output multiple weeks in advance. You can provide a number of weeks in advance with the -w or --weeks flag. | |||
```sh | |||
supper -w 2 # Creates three csv's. This weeks, and two weeks in advance. | |||
``` | |||
If datetime formatting is provided for the filename, it will give the correct datetime for that files week. Otherwise "_x" will be provided to make sure the script doesn't overwrite itself. | |||
#### Examples | |||
```sh | |||
supper -o "Seating Plan {:%Y-%m-%d}.csv" -w 2 | |||
``` | |||
Will create 3 files named | |||
``` | |||
Seating Plan 2019-10-21.csv | |||
Seating Plan 2019-10-28.csv | |||
Seating Plan 2019-11-04.csv | |||
``` | |||
--- | |||
```sh | |||
supper -o "Seating Plan.csv" -w 2 | |||
``` | |||
Will create 3 files named | |||
``` | |||
Seating Plan.csv | |||
Seating Plan_1.csv | |||
Seating Plan_2.csv | |||
``` | |||
### Debug | |||
You can enable debug output using the `-d` or `--debug` flags |
@@ -34,6 +34,6 @@ setuptools.setup( | |||
entry_points={"console_scripts": ["supper=supper.__main__:main"]}, | |||
python_requires=">=3.5", | |||
install_requires=("o365==2.0.1", "pyyaml==5.1.1"), | |||
version="1.0", | |||
version="1.1.0", | |||
license="GPL-3" | |||
) |
@@ -48,10 +48,10 @@ parser.add_argument( | |||
) | |||
parser.add_argument("-d", "--debug", action="store_true", help="Enable debug output") | |||
# TODO: Add new argument, n amount of future weeks. Then pass this onto the request which handles it | |||
parser.add_argument("-w", "--weeks", type=int, default=0, dest="weeks_extra", help="Number of weeks to add extra to the current week") | |||
def read_config_file(config_path): | |||
def read_config_file(config_path: str): | |||
""" | |||
Reads config file and sets up variables foo | |||
@@ -62,7 +62,7 @@ def read_config_file(config_path): | |||
return config | |||
def format_output_path(output_path): | |||
def format_output_path(output_path: str, date: datetime): | |||
""" | |||
Checks the string for datetime formatting and formats it if possible. | |||
@@ -70,7 +70,7 @@ def format_output_path(output_path): | |||
:return: str of the new output path | |||
""" | |||
try: | |||
new_path = output_path.format(datetime.now()) | |||
new_path = output_path.format(date) | |||
if new_path.split(".")[-1] != "csv": | |||
new_path += ".csv" | |||
LOG.info("Output path does NOT have '.csv' file extension. Adding '.csv' to end of output_path.") | |||
@@ -97,15 +97,15 @@ def parse_args(): | |||
LOG.setLevel(logging.WARNING) | |||
HANDLER.setLevel(logging.WARNING) | |||
output_path = format_output_path(args.output_path) | |||
if args.config_path: | |||
# Read the file provided and return the required config | |||
try: | |||
config = read_config_file(args.config_path) | |||
config["weeks_extra"] = args.weeks_extra | |||
config["config_path"] = args.config_path | |||
config["output_path"] = output_path | |||
config["users"] = sorted([x.lower() for x in config["users"]]) # make all names lowercase and sort alphabetically | |||
config["output_path"] = args.output_path # Needs formatting | |||
# make all names lowercase and sort alphabetically | |||
config["users"] = sorted([x.lower() for x in config["users"]]) | |||
LOG.debug("Loaded config successfully from '%s'", args.config_path) | |||
return config | |||
except FileNotFoundError: | |||
@@ -114,7 +114,7 @@ def parse_args(): | |||
exit(1) | |||
except (yaml.parser.ParserError, TypeError): | |||
# Cannot parse opened file | |||
# TypeError is sometimes raised if the metadata of the file is correct but content doesn't parse | |||
# TypeError is raised if the metadata of the file is correct but content doesn't parse | |||
LOG.error("Cannot parse config file. Make sure the provided config is a YAML file and that is is formatted correctly. Exiting...") | |||
exit(1) | |||
else: | |||
@@ -167,10 +167,25 @@ def main(): | |||
LOG.debug("Session created and authenticated. %s", session) | |||
ooo = session.get_ooo_list(email) | |||
create_ooo_csv(ooo, users, output_path) | |||
monday, friday = dates.get_week_datetime() | |||
print("\nCreated CSV seating plan for week {:%a %d/%m/%Y} to {:%a %d/%m/%Y} at {}".format(monday, friday, abspath(output_path))) | |||
filenames = [] | |||
for i in range(config["weeks_extra"] + 1): | |||
ooo = session.get_ooo_list(email, i) | |||
monday, friday = dates.get_week_datetime(i) | |||
output_filename = format_output_path(output_path, monday) | |||
# Check if filename is the same | |||
if output_filename in filenames: | |||
# Add number to end of filename to avoid overwrites | |||
path, ext = output_filename.split(".") | |||
output_filename = ".".join([path + f"_{i}", ext]) | |||
filenames.append(output_filename) | |||
create_ooo_csv(ooo, users, output_filename) | |||
print( | |||
"Created CSV seating plan for week {:%a %d/%m/%Y} to {:%a %d/%m/%Y} at {}".format( | |||
monday, friday, abspath(output_filename) | |||
) | |||
) | |||
if __name__ == "__main__": |
@@ -26,6 +26,7 @@ from . import ACCESS_TOKEN, STRFTIME, STRPTIME, LOG, dates | |||
class Account(O365.Account): | |||
"""Wrapper for the O365 Account class to add our api interactions""" | |||
@classmethod | |||
def create_session(cls, credentials, tenant_id): | |||
""" | |||
@@ -75,10 +76,9 @@ class Account(O365.Account): | |||
def get_event_range(self, beginning_of_week: datetime, email: str): | |||
""" | |||
Makes api call to grab a calender view within a 2 week window either side of the current week. | |||
Makes call to grab calender view within a 2 week window either side of the current week. | |||
:param beginning_of_week: datetime object for this weeks monday | |||
:param connection: a connection to the office365 api | |||
:return: dict of json response | |||
""" | |||
base_url = "https://outlook.office.com/api/v2.0/" | |||
@@ -97,15 +97,15 @@ class Account(O365.Account): | |||
resp = self.con.oauth_request(url, "get") | |||
return resp.json() | |||
def get_ooo_list(self, email: str): | |||
def get_ooo_list(self, email: str, week_no: int): | |||
""" | |||
Makes request and parses data into a list of users who will not be in the office | |||
:param email: string of the outofoffice email where the out of office calender is located | |||
:param connection: a connection to the office365 api | |||
:param week_no: week number. 1 = Current week, n > 1 = current week + n weeks | |||
:return: list of 5 lists representing 5 days. Each contains lowercase names of who is not in the office. | |||
""" | |||
monday, friday = dates.get_week_datetime() | |||
monday, friday = dates.get_week_datetime(week_no) | |||
try: | |||
events = self.get_event_range(monday, email) | |||
LOG.debug("Received response for two week range from week beginning with {:%Y-%m-%d} from outofoffice account with email: {}".format(monday, email)) | |||
@@ -117,7 +117,8 @@ class Account(O365.Account): | |||
outofoffice = [[], [], [], [], []] | |||
for event in events: | |||
# removes last char due to microsoft's datetime using 7 sigfigs for microseconds, python uses 6 | |||
# removes last char due to microsoft's datetime using 7 digits | |||
# for microseconds, python uses 6 | |||
start = datetime.strptime(event["Start"]["DateTime"][:-1], STRPTIME) | |||
end = datetime.strptime(event["End"]["DateTime"][:-1], STRPTIME) | |||
attendees = event["Attendees"] | |||
@@ -126,7 +127,7 @@ class Account(O365.Account): | |||
organizer = event["Organizer"] | |||
if not attendees and organizer["EmailAddress"]["Address"] != email: | |||
# Sometimes user will be the one who makes the event, not the outofoffice account. Get the organizer. | |||
# Sometimes user will be the one who makes the event. Get the organizer. | |||
attendees = [event["Organizer"]] | |||
if (end - start) <= timedelta(days=1): | |||
@@ -135,7 +136,10 @@ class Account(O365.Account): | |||
# Event is within the week we are looking at, add all attendees | |||
weekday = outofoffice[start.weekday()] | |||
if not attendees: | |||
LOG.warning("Event '%s' has no attendees. Cannot add to outofoffice list.", event["Subject"]) | |||
LOG.warning( | |||
"Event '%s' has no attendees. Cannot add to outofoffice list.", | |||
event["Subject"] | |||
) | |||
weekday = self.add_attendees_to_ooo_list(attendees, weekday) | |||
else: | |||
# Check if long events cover the days of this week |
@@ -16,7 +16,9 @@ | |||
from datetime import datetime, timedelta | |||
def get_week_datetime(start: datetime = None): | |||
from . import LOG | |||
def get_week_datetime(start: int = 0): | |||
""" | |||
Gets the current week's Monday and Friday to be used to filter a calendar. | |||
@@ -26,10 +28,10 @@ def get_week_datetime(start: datetime = None): | |||
:param start: Specifies the today variable, used for make future weeks if given. | |||
:return: Monday and Friday: Datetime objects | |||
""" | |||
if start: | |||
today = start | |||
else: | |||
today = datetime.now() | |||
today = datetime.now() + timedelta(weeks=start) | |||
if start > 5: | |||
LOG.warning("Extra weeks exceeds 5, script may run slowly.") | |||
weekday = today.weekday() | |||
extra_time = timedelta( |