# HG changeset patch # User Goffi # Date 1663797708 -7200 # Node ID d8baf92cb92159af26aa19358e9b3f3201d969ba # Parent 92482cc80d0b0ce128b5f23f7c9da0d78459e285 cli (event): update commands following changes in events: - commands have been update to match changes in bridge method. - arguments have been completely reworked for `create` and `modify` commands, making it possible to fine tune events. - `list` has been removed in favor of `get` which can now display several events at once rel 372 diff -r 92482cc80d0b -r d8baf92cb921 sat_frontends/jp/cmd_event.py --- a/sat_frontends/jp/cmd_event.py Thu Sep 22 00:01:48 2022 +0200 +++ b/sat_frontends/jp/cmd_event.py Thu Sep 22 00:01:48 2022 +0200 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -# jp: a SàT command line tool +# libervia-cli: Libervia CLI frontend # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify @@ -18,52 +18,28 @@ # along with this program. If not, see . -from dateutil import parser as du_parser -import calendar -import time +import argparse +import sys + +from sqlalchemy import desc + +from sat.core.i18n import _ from sat.core.i18n import _ +from sat.tools.common import data_format +from sat.tools.common import data_format +from sat.tools.common import date_utils from sat.tools.common.ansi import ANSI as A +from sat.tools.common.ansi import ANSI as A +from sat_frontends.jp import common from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat.tools.common import data_format +from sat_frontends.jp.constants import Const as C + from . import base __commands__ = ["Event"] OUTPUT_OPT_TABLE = "table" -# TODO: move date parsing to base, it may be useful for other commands - - -class List(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "list", - use_output=C.OUTPUT_LIST_DICT, - use_pubsub=True, - use_verbose=True, - help=_("get list of registered events"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - events = await self.host.bridge.eventsList( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get list of events: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(events) - self.host.quit() - class Get(base.CommandBase): def __init__(self, host): @@ -71,11 +47,14 @@ self, host, "get", - use_output=C.OUTPUT_DICT, + use_output=C.OUTPUT_LIST_DICT, use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, + pubsub_flags={C.MULTI_ITEMS, C.CACHE}, use_verbose=True, - help=_("get event data"), + extra_outputs={ + "default": self.default_output, + }, + help=_("get event(s) data"), ) def add_parser_options(self): @@ -83,91 +62,281 @@ async def start(self): try: - event_tuple = await self.host.bridge.eventGet( + events_data_s = await self.host.bridge.eventsGet( self.args.service, self.args.node, - self.args.item, + self.args.items, + self.getPubsubExtra(), self.profile, ) except Exception as e: - self.disp(f"can't get event data: {e}", error=True) + self.disp(f"can't get events data: {e}", error=True) self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - event_date, event_data = event_tuple - event_data["date"] = event_date - await self.output(event_data) + events_data = data_format.deserialise(events_data_s, type_check=list) + await self.output(events_data) self.host.quit() + def default_output(self, events): + nb_events = len(events) + for idx, event in enumerate(events): + names = event["name"] + name = names.get("") or next(iter(names.values())) + start = event["start"] + start_human = date_utils.date_fmt( + start, "medium", tz_info=date_utils.TZ_LOCAL + ) + end = event["end"] + self.disp(A.color( + A.BOLD, start_human, A.RESET, " ", + f"({date_utils.delta2human(start, end)}) ", + C.A_HEADER, name + )) + if self.verbosity > 0: + descriptions = event.get("descriptions", []) + if descriptions: + self.disp(descriptions[0]["description"]) + if idx < (nb_events-1): + self.disp("") -class EventBase(object): + +class CategoryAction(argparse.Action): + + def __init__(self, option_strings, dest, nargs=None, metavar=None, **kwargs): + if nargs is not None or metavar is not None: + raise ValueError("nargs and metavar must not be used") + if metavar is not None: + metavar="TERM WIKIDATA_ID LANG" + if "--help" in sys.argv: + # FIXME: dirty workaround to have correct --help message + # argparse doesn't normally allow variable number of arguments beside "+" + # and "*", this workaround show METAVAR as 3 arguments were expected, while + # we can actuall use 1, 2 or 3. + nargs = 3 + metavar = ("TERM", "[WIKIDATA_ID]", "[LANG]") + else: + nargs = "+" + + super().__init__(option_strings, dest, metavar=metavar, nargs=nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + categories = getattr(namespace, self.dest) + if categories is None: + categories = [] + setattr(namespace, self.dest, categories) + + if not values: + parser.error("category values must be set") + + category = { + "term": values[0] + } + + if len(values) == 1: + pass + elif len(values) == 2: + value = values[1] + if value.startswith("Q"): + category["wikidata_id"] = value + else: + category["language"] = value + elif len(values) == 3: + __, wd, lang = values + category["wikidata_id"] = wd + category["language"] = lang + else: + parser.error("Category can't have more than 3 arguments") + + categories.append(category) + + +class EventBase: def add_parser_options(self): self.parser.add_argument( + "-S", "--start", type=base.date_decoder, metavar="TIME_PATTERN", + help=_("the start time of the event")) + end_group = self.parser.add_mutually_exclusive_group() + end_group.add_argument( + "-E", "--end", type=base.date_decoder, metavar="TIME_PATTERN", + help=_("the time of the end of the event")) + end_group.add_argument( + "-D", "--duration", help=_("duration of the event")) + self.parser.add_argument( + "-H", "--head-picture", help="URL to a picture to use as head-picture" + ) + self.parser.add_argument( + "-d", "--description", help="plain text description the event" + ) + self.parser.add_argument( + "-C", "--category", action=CategoryAction, dest="categories", + help="Category of the event" + ) + self.parser.add_argument( + "-l", "--location", action="append", nargs="+", metavar="[KEY] VALUE", + help="Location metadata" + ) + rsvp_group = self.parser.add_mutually_exclusive_group() + rsvp_group.add_argument( + "--rsvp", action="store_true", help=_("RSVP is requested")) + rsvp_group.add_argument( + "--rsvp_json", metavar="JSON", help=_("JSON description of RSVP form")) + for node_type in ("invitees", "comments", "blog", "schedule"): + self.parser.add_argument( + f"--{node_type}", + nargs=2, + metavar=("JID", "NODE"), + help=_("link {node_type} pubsub node").format(node_type=node_type) + ) + self.parser.add_argument( + "-a", "--attachment", action="append", dest="attachments", + help=_("attach a file") + ) + self.parser.add_argument("--website", help=_("website of the event")) + self.parser.add_argument( + "--status", choices=["confirmed", "tentative", "cancelled"], + help=_("status of the event") + ) + self.parser.add_argument( + "-T", "--language", metavar="LANG", action="append", dest="languages", + help=_("main languages spoken at the event") + ) + self.parser.add_argument( + "--wheelchair", choices=["full", "partial", "no"], + help=_("is the location accessible by wheelchair") + ) + self.parser.add_argument( + "--external", + nargs=3, + metavar=("JID", "NODE", "ITEM"), + help=_("link to an external event") + ) + + def get_event_data(self): + if self.args.duration is not None: + if self.args.start is None: + self.parser.error("--start must be send if --duration is used") + # if duration is used, we simply add it to start time to get end time + self.args.end = base.date_decoder(f"{self.args.start} + {self.args.duration}") + + event = {} + if self.args.name is not None: + event["name"] = {"": self.args.name} + + if self.args.start is not None: + event["start"] = self.args.start + + if self.args.end is not None: + event["end"] = self.args.end + + if self.args.head_picture: + event["head-picture"] = { + "sources": [{ + "url": self.args.head_picture + }] + } + if self.args.description: + event["descriptions"] = [ + { + "type": "text", + "description": self.args.description + } + ] + if self.args.categories: + event["categories"] = self.args.categories + if self.args.location is not None: + location = {} + for location_data in self.args.location: + if len(location_data) == 1: + location["description"] = location_data[0] + else: + key, *values = location_data + location[key] = " ".join(values) + event["locations"] = [location] + + if self.args.rsvp: + event["rsvp"] = [{}] + elif self.args.rsvp_json: + if isinstance(self.args.rsvp_elt, dict): + event["rsvp"] = [self.args.rsvp_json] + else: + event["rsvp"] = self.args.rsvp_json + + for node_type in ("invitees", "comments", "blog", "schedule"): + value = getattr(self.args, node_type) + if value: + service, node = value + event[node_type] = {"service": service, "node": node} + + if self.args.attachments: + attachments = event["attachments"] = [] + for attachment in self.args.attachments: + attachments.append({ + "sources": [{"url": attachment}] + }) + + extra = {} + + for arg in ("website", "status", "languages"): + value = getattr(self.args, arg) + if value is not None: + extra[arg] = value + if self.args.wheelchair is not None: + extra["accessibility"] = {"wheelchair": self.args.wheelchair} + + if extra: + event["extra"] = extra + + if self.args.external: + ext_jid, ext_node, ext_item = self.args.external + event["external"] = { + "jid": ext_jid, + "node": ext_node, + "item": ext_item + } + return event + + +class Create(EventBase, base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "create", + use_pubsub=True, + help=_("create or replace event"), + ) + + def add_parser_options(self): + super().add_parser_options() + self.parser.add_argument( "-i", "--id", default="", help=_("ID of the PubSub Item"), ) - self.parser.add_argument("-d", "--date", type=str, help=_("date of the event")) - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="fields", - metavar=("KEY", "VALUE"), - help=_("configuration field to set"), - ) - - def parseFields(self): - return dict(self.args.fields) if self.args.fields else {} - - def parseDate(self): - if self.args.date: - try: - date = int(self.args.date) - except ValueError: - try: - date_time = du_parser.parse( - self.args.date, dayfirst=not ("-" in self.args.date) - ) - except ValueError as e: - self.parser.error(_("Can't parse date: {msg}").format(msg=e)) - if date_time.tzinfo is None: - date = calendar.timegm(date_time.timetuple()) - else: - date = time.mktime(date_time.timetuple()) - else: - date = -1 - return date - - -class Create(EventBase, base.CommandBase): - def __init__(self, host): - super(Create, self).__init__( - host, - "create", - use_pubsub=True, - help=_("create or replace event"), - ) - EventBase.__init__(self) + # name is mandatory here + self.parser.add_argument("name", help=_("name of the event")) async def start(self): - fields = self.parseFields() - date = self.parseDate() + if self.args.start is None: + self.parser.error("--start must be set") + event_data = self.get_event_data() + # we check self.args.end after get_event_data because it may be set there id + # --duration is used + if self.args.end is None: + self.parser.error("--end or --duration must be set") try: - node = await self.host.bridge.eventCreate( - date, - fields, + await self.host.bridge.eventCreate( + data_format.serialise(event_data), + self.args.id, + self.args.node, self.args.service, - self.args.node, - self.args.id, self.profile, ) except Exception as e: self.disp(f"can't create event: {e}", error=True) self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - self.disp(_("Event created successfuly on node {node}").format(node=node)) + self.disp(_("Event created successfuly)")) self.host.quit() @@ -177,21 +346,24 @@ host, "modify", use_pubsub=True, - pubsub_flags={C.NODE}, + pubsub_flags={C.SINGLE_ITEM}, help=_("modify an existing event"), ) EventBase.__init__(self) + def add_parser_options(self): + super().add_parser_options() + # name is optional here + self.parser.add_argument("-N", "--name", help=_("name of the event")) + async def start(self): - fields = self.parseFields() - date = 0 if not self.args.date else self.parseDate() + event_data = self.get_event_data() try: - self.host.bridge.eventModify( + await self.host.bridge.eventModify( + data_format.serialise(event_data), + self.args.item, self.args.service, self.args.node, - self.args.id, - date, - fields, self.profile, ) except Exception as e: @@ -209,28 +381,32 @@ "get", use_output=C.OUTPUT_DICT, use_pubsub=True, - pubsub_flags={C.NODE}, + pubsub_flags={C.SINGLE_ITEM}, use_verbose=True, help=_("get event attendance"), ) def add_parser_options(self): self.parser.add_argument( - "-j", "--jid", default="", help=_("bare jid of the invitee") + "-j", "--jid", action="append", dest="jids", default=[], + help=_("only retrieve RSVP from those JIDs") ) async def start(self): try: - event_data = await self.host.bridge.eventInviteeGet( + event_data_s = await self.host.bridge.eventInviteeGet( self.args.service, self.args.node, - self.args.jid, + self.args.item, + self.args.jids, + "", self.profile, ) except Exception as e: self.disp(f"can't get event data: {e}", error=True) self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: + event_data = data_format.deserialise(event_data_s) await self.output(event_data) self.host.quit() @@ -240,9 +416,8 @@ super(InviteeSet, self).__init__( host, "set", - use_output=C.OUTPUT_DICT, use_pubsub=True, - pubsub_flags={C.NODE}, + pubsub_flags={C.SINGLE_ITEM}, help=_("set event attendance"), ) @@ -258,12 +433,14 @@ ) async def start(self): + # TODO: handle RSVP with XMLUI in a similar way as for `ad-hoc run` fields = dict(self.args.fields) if self.args.fields else {} try: self.host.bridge.eventInviteeSet( self.args.service, self.args.node, - fields, + self.args.item, + data_format.serialise(fields), self.profile, ) except Exception as e: @@ -570,7 +747,7 @@ class Event(base.CommandBase): - subcommands = (List, Get, Create, Modify, Invitee) + subcommands = (Get, Create, Modify, Invitee) def __init__(self, host): super(Event, self).__init__(