diff --git a/README.md b/README.md index 2419b1b..97531b0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,36 @@ -# masto-gitsocial-bridge +# masto-bridges (formerly masto-gitsocial-bridge) + +## CalDAV to Masto (and back again) + +Post directly from your (CalDAV) calendar, and follow your feeds from your calendar! + +See [config](#Config) below for how to set up for your calendar. + +Compatible with mobile too! + +![Post from your calendar, get your feed from your calendar](img/masto-caldav.png) + +### From Calendar to Masto + +Make a new calendar event with +- Event title/summary: your username `user@instance.com` or just `user`. Calendar entries that don't + match your username will not be posted (since they aren't from you!) +- Notes/Description: The content of your post! +- Time, date, duration, repetition: don't matter for now! +- The absence of a URL in the "location" field is used to make sure we don't echo posts we make from the real masto + +### From Masto to Calendar + +When running `masto_bridgebot` with `MASTOBRIDGE_ENABLE_CALDAV=true` and +`MASTOBRIDGE_STREAM_MODE="home"`, your home feed should already be posting to your calendar! + +To make it so only your posts are added to your calendar, set the stream mode to `'list'` + +## git-social bridge between mastodon and bcrypt's [git-social](https://github.com/diracdeltas/tweets) -## From git to masto +### From git to masto Post on git-social... @@ -12,7 +40,7 @@ Bridge to mastodon ![Post on mastodon, same text but with link to the git commit](img/git-to-masto_1.png) -## From masto to git +### From masto to git Post on mastodon... @@ -69,11 +97,25 @@ from environment variables, for example, make an `.env` file in the cloned repository directory like ``` -MASTOGIT_MASTO_URL="https://social.coop" -MASTOGIT_MASTO_TOKEN="" -MASTOGIT_GIT_REPO="/path/to/your/git-social/tweets" -MASTOGIT_GIT_REMOTE_URL="https://git.jon-e.net/jonny/tweets" -MASTOGIT_LOGDIR="/wherever/you/want/to/put/logs" +MASTOBRIDGE_LOGDIR="/wherever/you/want/to/put/logs" + +MASTOBRIDGE_MASTO_URL="https://social.coop" +MASTOBRIDGE_MASTO_TOKEN="" + +MASTOBRIDGE_ENABLE_GIT=false +MASTOBRIDGE_GIT_REPO="/path/to/your/git-social/tweets" +MASTOBRIDGE_GIT_REMOTE_URL="https://git.jon-e.net/jonny/tweets" + +MASTOBRIDGE_ENABLE_CALDAV=true +MASTOBRIDGE_CALDAV_URL="https://your.caldav.calendar/wherever" +MASTOBRIDGE_CALDAV_USER="" +MASTOBRIDGE_CALDAV_PASSWORD="" +MASTOBRIDGE_CALDAV_CALENDAR_NAME="" + +# "home" (for streaming your home feed to bridges) +# or "list" for streaming a list with just your account in it to bridges +MASTOBRIDGE_STREAM_MODE="home" + ``` This is assuming you don't need any sort of authentication/local password @@ -107,9 +149,9 @@ git-social (only if they are "public" or "unlisted"). poetry run masto_gitbot # or # >>> poetry shell -# >>> masto_gitbot +# >>> masto_bridgebot # or however else you run python entrypoint scripts -# hell you could do python -m masto_git_bridge.main:masto_gitbot +# hell you could do python -m masto_bridges.main:masto_gitbot # i think? ``` diff --git a/img/masto-caldav.png b/img/masto-caldav.png new file mode 100644 index 0000000..86cb431 Binary files /dev/null and b/img/masto-caldav.png differ diff --git a/masto_git_bridge/__init__.py b/masto_bridges/__init__.py similarity index 100% rename from masto_git_bridge/__init__.py rename to masto_bridges/__init__.py diff --git a/masto_bridges/bot.py b/masto_bridges/bot.py new file mode 100644 index 0000000..97b6e40 --- /dev/null +++ b/masto_bridges/bot.py @@ -0,0 +1,164 @@ +import pdb +from typing import Optional +from datetime import datetime +from masto_bridges.config import Config +from masto_bridges.models import Account, List +from masto_bridges.post import Post, Status +from masto_bridges.logger import init_logger +from masto_bridges.repo import Repo +from masto_bridges.caldav import CalDAV + +from mastodon import Mastodon, StreamListener + +class Listener(StreamListener): + def __init__(self, client: Mastodon, config: Optional[Config] = None): + super(Listener, self).__init__() + self.client = client + + if config is None: + config = Config() + self.config = config + + self.logger = init_logger('mastogit_bot-stream', basedir=self.config.LOGDIR) + + self.repo = None + if self.config.ENABLE_GIT: + self.repo = Repo(path=config.GIT_REPO) + + self.caldav = None + if self.config.ENABLE_CALDAV is not None: + self.caldav = CalDAV(config=self.config) + + + + def on_update(self, status:dict): + try: + status = Status(**status) + except: + self.logger.warning(f'Unhandled event: {status}') + return + + if status.visibility in ('private', 'direct'): + # not xposting dms + self.logger.info('Not xposting private messages') + return + + post = Post.from_status(status) + if post.text.startswith('xpost'): + self.logger.info('Not xposting an xpost') + return + + + + success = False + if self.repo: + git_success = self.repo.post(post.format_commit()) + else: + git_success = True + + if self.caldav: + caldav_success = self.caldav.post(post.format_caldav()) + else: + caldav_success = False + + success = git_success and caldav_success + + if success: + self.logger.info('Posted to bridges!') + else: + self.logger.exception('Failed to post to bridges!') + + def on_unknown_event(self, name, unknown_event = None): + """An unknown mastodon API event has been received. The name contains the event-name and unknown_event + contains the content of the unknown event. + + This function must be implemented, if unknown events should be handled without an error. + """ + self.logger.exception(f'Unknown event type: {name}') + + + + + + +class Bot: + def __init__(self, config:Optional[Config]=None, post_length=500): + self._me = None # type: Optional[Account] + self._me_list = None # type: Optional[List] + + if config is None: + config = Config() + + self.config = config + self.config.LOGDIR.mkdir(exist_ok=True) + self.post_length = post_length + self.logger = init_logger('mastogit_bot', basedir=self.config.LOGDIR) + + self.client = Mastodon( + access_token=self.config.MASTO_TOKEN, + api_base_url=self.config.MASTO_URL + ) + + self.caldav = None + if self.config.ENABLE_CALDAV: + self.caldav = CalDAV(self.config) + + def init_stream(self, run_async:bool=True): + # Listen to a stream consisting of just us. + listener = Listener(client=self.client, config=self.config) + self.logger.info('Initializing streaming') + if self.config.STREAM_MODE == 'list': + self.client.stream_list( + self.me_list.id, + listener = listener, + run_async=run_async + ) + elif self.config.STREAM_MODE == 'home': + self.client.stream_user( + listener=listener, + run_async=run_async + ) + + def post(self, post:str): + # TODO: Split long posts + if len(post)>self.post_length: + raise NotImplementedError(f"Cant split long posts yet, got post of length {len(post)} when max length is {self.post_length}") + + self.client.status_post(post) + self.logger.info(f"Posted:\n{post}") + + def poll_caldav(self): + events = self.caldav.poll() + if len(events) > 0: + for event in events: + post = Post.from_vevent(event.vobject_instance.vevent) + if post.cal_user.split('@')[0] != self.me.acct and post.cal_user != self.me.acct: + self.logger.debug(f"Not posting calendar event not from us. Got cal user: {post.cal_user} and we are {self.me.acct}") + continue + if post.cal_url: + self.logger.debug("Not posting a post we made on masto back to masto") + continue + self.post(post.format_masto()) + + @property + def me(self) -> Account: + if self._me is None: + self._me = Account(**self.client.me()) + return self._me + + def _make_me_list(self) -> List: + me_list = List(**self.client.list_create('me')) + self.client.list_accounts_add(me_list.id, [self.me.id]) + self.logger.info('Created list with just me in it!') + return me_list + + @property + def me_list(self) -> List: + if self._me_list is None: + lists = self.client.lists() + me_list = [l for l in lists if l.get('title', '') == 'me'] + if len(me_list)>0: + self._me_list = List(**me_list[0]) + else: + self._me_list = self._make_me_list() + return self._me_list diff --git a/masto_bridges/caldav.py b/masto_bridges/caldav.py new file mode 100644 index 0000000..5fc47b0 --- /dev/null +++ b/masto_bridges/caldav.py @@ -0,0 +1,99 @@ +from typing import Optional +from caldav import DAVClient, Calendar +from caldav.objects import SynchronizableCalendarObjectCollection +# from caldav.lib.error import ResponseError +from pydantic import BaseModel, Field +from datetime import datetime +from icalendar import Event as iEvent +from uuid import uuid1 + +from masto_bridges.config import Config + +class Event(BaseModel): + + summary: str # the title of the event, use for username + dtstart: datetime + dtend: datetime + uid: str = Field(default_factory=uuid1) + url: str = '' + location:str = '' + description:str = '' # post body + + + + + + def to_ical(self) -> str: + evt = iEvent() + evt.add('dtstart', self.dtstart) + evt.add('dtend', self.dtend) + evt.add('uid', self.uid) + evt.add('location', self.location) + evt.add('summary', self.summary) + evt.add('description', self.description) + evt.add('url', self.url) + return evt.to_ical() + + +class CalDAV: + def __init__(self, config:Config): + self.config = config + for value in ('CALDAV_URL', 'CALDAV_USER', 'CALDAV_PASSWORD', 'CALDAV_CALENDAR_NAME'): + if getattr(self.config, value) is None: + raise ValueError('Need all CALDAV fields to be present in config! Check your .env file!') + self._sync_token = None + self._sync = None # type: Optional[SynchronizableCalendarObjectCollection] + + + def get_calendar(self, name:str) -> Optional[Calendar]: + """ + Get a calendar by its name, creating one if none exists. + """ + with self.client as client: + cals = client.principal().calendars() + # try and find + cal = [cal for cal in cals if cal.name == name] + if len(cal) == 0: + cal = client.principal().make_calendar(name=name) + else: + cal = cal[0] + + return cal + + def post(self, event:Event) -> bool: + with self.client as client: + calendar = self.get_calendar(self.config.CALDAV_CALENDAR_NAME) + calendar.save_event( + **event.dict() + ) + + return True + + def poll(self) -> SynchronizableCalendarObjectCollection: + cal = self.get_calendar(self.config.CALDAV_CALENDAR_NAME) + if self._sync_token is None: + # we haven't polled yet. get and ignore previous entries. + # we don't want to post a bunch of junk every time lol + self._sync = cal.objects(load_objects=False) + + self._sync_token = self._sync.sync_token + + try: + self._sync = cal.objects(sync_token=self._sync_token, load_objects=True) + self._sync_token = self._sync.sync_token + except: + # deleting calendar entries makes us choke! + self._sync = cal.objects(sync_token=self._sync_token, load_objects=False) + self._sync_token = self._sync.sync_token + + return self._sync + + + + @property + def client(self) -> DAVClient: + return DAVClient( + url=self.config.CALDAV_URL, + username=self.config.CALDAV_USER, + password=self.config.CALDAV_PASSWORD + ) diff --git a/masto_bridges/config.py b/masto_bridges/config.py new file mode 100644 index 0000000..d3f46c0 --- /dev/null +++ b/masto_bridges/config.py @@ -0,0 +1,24 @@ +from pathlib import Path +from typing import Optional, Literal +from pydantic import BaseSettings, AnyHttpUrl + +class Config(BaseSettings): + MASTO_URL:AnyHttpUrl + MASTO_TOKEN:str + LOGDIR:Path=Path().home() / '.mastobridge' + LOGLEVEL:Literal['DEBUG','INFO','WARNING','ERROR','EXCEPTION']='INFO' + STREAM_MODE:Literal['list', 'home'] = 'list' + ENABLE_GIT:bool=False + ENABLE_CALDAV:bool=False + GIT_REPO:Optional[Path]=None + GIT_REMOTE_URL:Optional[AnyHttpUrl]=None + CALDAV_URL:Optional[AnyHttpUrl] = None + CALDAV_USER:Optional[str]=None + CALDAV_PASSWORD:Optional[str]=None + CALDAV_CALENDAR_NAME:Optional[str]=None + + + class Config: + env_file = '.env' + env_file_encoding = 'utf-8' + env_prefix = "MASTOBRIDGE_" diff --git a/masto_git_bridge/logger.py b/masto_bridges/logger.py similarity index 100% rename from masto_git_bridge/logger.py rename to masto_bridges/logger.py diff --git a/masto_git_bridge/main.py b/masto_bridges/main.py similarity index 62% rename from masto_git_bridge/main.py rename to masto_bridges/main.py index aace5d2..fe3c48b 100644 --- a/masto_git_bridge/main.py +++ b/masto_bridges/main.py @@ -1,9 +1,10 @@ +import pdb from typing import Optional -from masto_git_bridge.config import Config -from masto_git_bridge.repo import Repo -from masto_git_bridge.bot import Bot -from masto_git_bridge.post import Post -from masto_git_bridge.logger import init_logger +from masto_bridges.config import Config +from masto_bridges.repo import Repo +from masto_bridges.bot import Bot +from masto_bridges.post import Post +from masto_bridges.logger import init_logger from time import sleep def post_last_commit(config:Optional[Config]=None): @@ -27,7 +28,7 @@ def post_last_commit(config:Optional[Config]=None): bot = Bot(config=config) bot.post(post.format_masto()) -def masto_gitbot(config:Optional[Config]=None): +def masto_bridgebot(config:Optional[Config]=None): if config is None: config = Config() @@ -35,8 +36,17 @@ def masto_gitbot(config:Optional[Config]=None): try: bot.init_stream() while True: - sleep(60*60) - bot.logger.info('taking a breath') + + sleep(1) + if bot.caldav: + # poll for new events + bot.poll_caldav() + # pdb.set_trace() + + # print(events.objects) + bot.logger.debug('taking a breath') except KeyboardInterrupt: bot.logger.info('quitting!') + + diff --git a/masto_git_bridge/models.py b/masto_bridges/models.py similarity index 100% rename from masto_git_bridge/models.py rename to masto_bridges/models.py diff --git a/masto_bridges/post.py b/masto_bridges/post.py new file mode 100644 index 0000000..1b73f94 --- /dev/null +++ b/masto_bridges/post.py @@ -0,0 +1,116 @@ +from typing import Optional, Literal, TYPE_CHECKING +from datetime import datetime, timedelta +from pydantic import BaseModel +import re +from bs4 import BeautifulSoup + +if TYPE_CHECKING: + from vobject.icalendar import RecurringComponent + +from masto_bridges.repo import Commit +from masto_bridges.models import Account +from masto_bridges.caldav import Event + +class Status(BaseModel): + """ + Model of a toot on mastodon + + See: https://mastodonpy.readthedocs.io/en/stable/#toot-dicts + """ + id: int + url: str + account: Account + content: str + visibility: Literal['public', 'unlisted', 'private', 'direct'] + created_at:datetime + in_reply_to_id: Optional[int] = None + in_reply_to_account_id: Optional[int] = None + spoiler_text: Optional[str] = None + reblog: Optional['Status'] = None + + def __init__(self, **kwargs): + if kwargs.get('url', None) is None: + # try using the uri + kwargs['url'] = kwargs['uri'] + super(Status, self).__init__(**kwargs) + + class Config: + extra='ignore' + +class Post(BaseModel): + #timestamp: Optional[datetime] = None + text:str + status:Optional[Status] = None + commit:Optional[Commit] = None + cal_user:Optional[str] = None + cal_url:Optional[str] = None + + @classmethod + def from_commit(cls, commit:Commit) -> 'Post': + text = '\n'.join([commit.subject, commit.body]) + return Post(text=text, commit=commit) + + @classmethod + def from_status(cls, status:Status) -> 'Post': + # if this is a boost, get the original status + if status.reblog: + date = status.created_at + status = status.reblog + status.created_at = date + + soup = BeautifulSoup(status.content, 'lxml') + # replace with double line breaks + pars = [p.text for p in soup.find_all('p')] + text = '\n\n'.join(pars) + return Post(text=text, status=status) + + @classmethod + def from_vevent(cls, event:'RecurringComponent'): + user=event.summary.value + text = event.description.value + try: + url = event.location.value + except AttributeError: + url = None + return Post(text=text, cal_user=user, cal_url=url) + + + def format_masto(self) -> str: + """ + Format a post to go from git -> masto. + + Needs to have a :attr:`.commit` attribute! + + Does not split the body text into multiple toots. + That should be handled in the posting action + + Example: + + git-social: https://{repo_url}/commits/{hash} + {subject line} + {body} + """ + if self.commit is not None: + return f"xpost from git-social: {self.commit.url}\n---\n{self.text}""" + elif self.cal_user is not None: + return f"xpost from a calendar\n---\n{self.text}""" + else: + return self.text + + def format_commit(self) -> str: + """ + Add a link back to original masto post split by double lines + """ + return f"xpost from mastodon: {self.status.url}\n\n{self.text}" + + def format_caldav(self) -> Event: + """ + Format a mastodon post for posting to CALDAV + """ + return Event( + dtstart=self.status.created_at, + dtend = self.status.created_at + timedelta(minutes=5), + location = self.status.url, + summary= self.status.account.acct, + description = self.text + ) \ No newline at end of file diff --git a/masto_git_bridge/repo.py b/masto_bridges/repo.py similarity index 100% rename from masto_git_bridge/repo.py rename to masto_bridges/repo.py diff --git a/masto_git_bridge/bot.py b/masto_git_bridge/bot.py deleted file mode 100644 index c13b03b..0000000 --- a/masto_git_bridge/bot.py +++ /dev/null @@ -1,106 +0,0 @@ -import pdb -from typing import Optional -from datetime import datetime -from masto_git_bridge.config import Config -from masto_git_bridge.models import Account, List -from masto_git_bridge.post import Post, Status -from masto_git_bridge.logger import init_logger -from masto_git_bridge.repo import Repo - -from mastodon import Mastodon, StreamListener - -class Listener(StreamListener): - def __init__(self, client: Mastodon, config:Optional[Config]=None): - super(Listener, self).__init__() - self.client = client - - if config is None: - config = Config() - self.config = config - - self.logger = init_logger('mastogit_bot-stream', basedir=self.config.LOGDIR) - - self.repo = Repo(path=config.GIT_REPO) - - - - def on_update(self, status:dict): - status = Status(**status) - if status.visibility in ('private', 'direct'): - # not xposting dms - self.logger.info('Not xposting private messages') - return - - post = Post.from_status(status) - if post.text.startswith('xpost'): - self.logger.info('Not xposting an xpost') - return - - success = self.repo.post(post.format_commit()) - if success: - self.logger.info('Posted to git!') - else: - self.logger.exception('Failed to post to git!') - - - - - -class Bot: - def __init__(self, config:Optional[Config]=None, post_length=500): - self._me = None # type: Optional[Account] - self._me_list = None # type: Optional[List] - - if config is None: - config = Config() - - self.config = config - self.config.LOGDIR.mkdir(exist_ok=True) - self.post_length = post_length - self.logger = init_logger('mastogit_bot', basedir=self.config.LOGDIR) - - self.client = Mastodon( - access_token=self.config.MASTO_TOKEN, - api_base_url=self.config.MASTO_URL - ) - - def init_stream(self, run_async:bool=True): - # Listen to a stream consisting of just us. - listener = Listener(client=self.client, config=self.config) - self.logger.info('Initializing streaming') - self.client.stream_list( - self.me_list.id, - listener = listener, - run_async=run_async - ) - - def post(self, post:str): - # TODO: Split long posts - if len(post)>self.post_length: - raise NotImplementedError(f"Cant split long posts yet, got post of length {len(post)} when max length is {self.post_length}") - - self.client.status_post(post) - self.logger.info(f"Posted:\n{post}") - - @property - def me(self) -> Account: - if self._me is None: - self._me = Account(**self.client.me()) - return self._me - - def _make_me_list(self) -> List: - me_list = List(**self.client.list_create('me')) - self.client.list_accounts_add(me_list.id, [self.me.id]) - self.logger.info('Created list with just me in it!') - return me_list - - @property - def me_list(self) -> List: - if self._me_list is None: - lists = self.client.lists() - me_list = [l for l in lists if l.get('title', '') == 'me'] - if len(me_list)>0: - self._me_list = List(**me_list[0]) - else: - self._me_list = self._make_me_list() - return self._me_list diff --git a/masto_git_bridge/config.py b/masto_git_bridge/config.py deleted file mode 100644 index 7fc77f6..0000000 --- a/masto_git_bridge/config.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -from pydantic import BaseSettings, AnyHttpUrl - -class Config(BaseSettings): - MASTO_URL:AnyHttpUrl - MASTO_TOKEN:str - GIT_REPO:Path - GIT_REMOTE_URL:AnyHttpUrl - LOGDIR:Path=Path().home() / '.mastogit' - - class Config: - env_file = '.env' - env_file_encoding = 'utf-8' - env_prefix = "MASTOGIT_" - diff --git a/masto_git_bridge/post.py b/masto_git_bridge/post.py deleted file mode 100644 index c83a931..0000000 --- a/masto_git_bridge/post.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Optional, Literal -from datetime import datetime -from pydantic import BaseModel -import re -from bs4 import BeautifulSoup - -from masto_git_bridge.repo import Commit -from masto_git_bridge.models import Account - -class Status(BaseModel): - """ - Model of a toot on mastodon - - See: https://mastodonpy.readthedocs.io/en/stable/#toot-dicts - """ - id: int - url: str - account: Account - content: str - visibility: Literal['public', 'unlisted', 'private', 'direct'] - in_reply_to_id: Optional[int] = None - in_reply_to_account_id: Optional[int] = None - - class Config: - extra='ignore' - -class Post(BaseModel): - #timestamp: Optional[datetime] = None - text:str - status:Optional[Status] = None - commit:Optional[Commit] = None - - @classmethod - def from_commit(cls, commit:Commit) -> 'Post': - text = '\n'.join([commit.subject, commit.body]) - return Post(text=text, commit=commit) - - @classmethod - def from_status(cls, status:Status) -> 'Post': - # split paragraphs using bs4 - soup = BeautifulSoup(status.content, 'lxml') - # replace with double line breaks - pars = [p.text for p in soup.find_all('p')] - text = '\n\n'.join(pars) - return Post(text=text, status=status) - - def format_masto(self) -> str: - """ - Format a post to go from git -> masto. - - Needs to have a :attr:`.commit` attribute! - - Does not split the body text into multiple toots. - That should be handled in the posting action - - Example: - - git-social: https://{repo_url}/commits/{hash} - {subject line} - {body} - """ - return f"xpost from git-social: {self.commit.url}\n---\n{self.text}""" - - def format_commit(self) -> str: - """ - Add a link back to original masto post split by double lines - """ - return f"xpost from mastodon: {self.status.url}\n\n{self.text}" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index ab8093f..8f8c752 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "beautifulsoup4" version = "4.11.1" @@ -5,6 +7,10 @@ description = "Screen-scraping library" category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] [package.dependencies] soupsieve = ">1.2" @@ -20,10 +26,38 @@ description = "Pure-Python implementation of the blurhash algorithm." category = "main" optional = false python-versions = "*" +files = [ + {file = "blurhash-1.1.4-py2.py3-none-any.whl", hash = "sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d"}, + {file = "blurhash-1.1.4.tar.gz", hash = "sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"}, +] [package.extras] test = ["Pillow", "numpy", "pytest"] +[[package]] +name = "caldav" +version = "1.2.1" +description = "CalDAV (RFC4791) client library" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "caldav-1.2.1-py3-none-any.whl", hash = "sha256:6860abfab926e7a7290e94e2e179dd9ddb06ea6bc4eb60304b49ef4b14b85c6f"}, + {file = "caldav-1.2.1.tar.gz", hash = "sha256:934d7169d6e51b6e6ed4beef579b3d4008e2e6edac326a70225a32400ead86c9"}, +] + +[package.dependencies] +icalendar = "*" +lxml = "*" +pytz = "*" +recurring-ical-events = ">=2.0.0" +requests = "*" +tzlocal = "*" +vobject = "*" + +[package.extras] +test = ["coverage", "icalendar", "pytest", "pytest-coverage", "pytz", "radicale", "tzlocal", "xandikos"] + [[package]] name = "certifi" version = "2022.9.24" @@ -31,6 +65,10 @@ description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] [[package]] name = "charset-normalizer" @@ -39,6 +77,10 @@ description = "The Real First Universal Charset Detector. Open, modern and activ category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] [package.extras] unicode-backport = ["unicodedata2"] @@ -50,6 +92,10 @@ description = "Python parser for the CommonMark Markdown spec" category = "main" optional = false python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] [package.extras] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] @@ -61,6 +107,10 @@ description = "Decorators for Humans" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] [[package]] name = "dnspython" @@ -69,6 +119,10 @@ description = "DNS toolkit" category = "main" optional = false python-versions = ">=3.6,<4.0" +files = [ + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, +] [package.extras] curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] @@ -85,11 +139,31 @@ description = "A robust email address syntax and deliverability validation libra category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, + {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, +] [package.dependencies] dnspython = ">=1.15.0" idna = ">=2.0.0" +[[package]] +name = "icalendar" +version = "5.0.7" +description = "iCalendar parser/generator" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "icalendar-5.0.7-py3-none-any.whl", hash = "sha256:18ad51f9d1741d33795ddaf5c886c59f6575f287d30e5a145c2d42ef72910c7f"}, + {file = "icalendar-5.0.7.tar.gz", hash = "sha256:e306014a64dc4dcf638da0acb2487ee4ada57b871b03a62ed7b513dfc135655c"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = "*" + [[package]] name = "idna" version = "3.4" @@ -97,6 +171,10 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "lxml" @@ -105,214 +183,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] - -[[package]] -name = "mastodon-py" -version = "1.5.2" -description = "Python wrapper for the Mastodon API" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -blurhash = ">=1.1.4" -decorator = ">=4.0.0" -python-dateutil = "*" -python-magic = "*" -pytz = "*" -requests = ">=2.4.2" -six = "*" - -[package.extras] -blurhash = ["blurhash (>=1.1.4)"] -test = ["blurhash (>=1.1.4)", "cryptography (>=1.6.0)", "http-ece (>=1.0.5)", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-vcr", "requests-mock", "vcrpy"] -webpush = ["cryptography (>=1.6.0)", "http-ece (>=1.0.5)"] - -[[package]] -name = "pydantic" -version = "1.10.2" -description = "Data validation and settings management using python type hints" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} -python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pygments" -version = "2.13.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "0.21.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "python-magic" -version = "0.4.27" -description = "File type identification using libmagic" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pytz" -version = "2022.6" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rich" -version = "12.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.6.3,<4.0.0" - -[package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "soupsieve" -version = "2.3.2.post1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.12" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.9" -content-hash = "c85c922dc9bcac039fd7b4b5f12d310934ae4faa2bd23c426c2a3beb62e86721" - -[metadata.files] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, - {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, -] -blurhash = [ - {file = "blurhash-1.1.4-py2.py3-none-any.whl", hash = "sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d"}, - {file = "blurhash-1.1.4.tar.gz", hash = "sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] -dnspython = [ - {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, - {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, -] -email-validator = [ - {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, - {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -lxml = [ +files = [ {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, @@ -384,11 +255,47 @@ lxml = [ {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, ] -mastodon-py = [ + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mastodon-py" +version = "1.5.2" +description = "Python wrapper for the Mastodon API" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "Mastodon.py-1.5.2-py2.py3-none-any.whl", hash = "sha256:49afbf9f4347f355bee5638b71a5231b0e1164a58d253ccd9b4345d999c43369"}, {file = "Mastodon.py-1.5.2.tar.gz", hash = "sha256:c98fd97b7450cd02262669b80be20f53657b5540c4888a47231df11856910918"}, ] -pydantic = [ + +[package.dependencies] +blurhash = ">=1.1.4" +decorator = ">=4.0.0" +python-dateutil = "*" +python-magic = "*" +pytz = "*" +requests = ">=2.4.2" +six = "*" + +[package.extras] +blurhash = ["blurhash (>=1.1.4)"] +test = ["blurhash (>=1.1.4)", "cryptography (>=1.6.0)", "http-ece (>=1.0.5)", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-vcr", "requests-mock", "vcrpy"] +webpush = ["cryptography (>=1.6.0)", "http-ece (>=1.0.5)"] + +[[package]] +name = "pydantic" +version = "1.10.2" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, @@ -426,47 +333,258 @@ pydantic = [ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] -pygments = [ + +[package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} +python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] -python-dateutil = [ + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -python-dotenv = [ + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "0.21.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, ] -python-magic = [ + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, ] -pytz = [ + +[[package]] +name = "pytz" +version = "2022.6" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, ] -requests = [ + +[[package]] +name = "recurring-ical-events" +version = "2.0.2" +description = "A Python module which repeats ICalendar events by RRULE, RDATE and EXDATE." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "recurring_ical_events-2.0.2-py3-none-any.whl", hash = "sha256:a618140129e2ff00afa6b7afc1a154c2cb2177166621a519941b7058d6f6c339"}, + {file = "recurring_ical_events-2.0.2.tar.gz", hash = "sha256:c4bc7d0d26fcda0ac3b773c9706fd1e271b7b0e0230ad0ee02ccab55e274ea11"}, +] + +[package.dependencies] +icalendar = "*" +python-dateutil = ">=2.8.1" +pytz = "*" +x-wr-timezone = ">=0.0.5,<1.0.0" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] -rich = [ + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" +files = [ {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, ] -six = [ + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -soupsieve = [ + +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] -typing-extensions = [ + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -urllib3 = [ + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "tzlocal" +version = "5.0.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, + {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "urllib3" +version = "1.26.12" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +files = [ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "vobject" +version = "0.9.6.1" +description = "A full-featured Python package for parsing and creating iCalendar and vCard files" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "vobject-0.9.6.1.tar.gz", hash = "sha256:96512aec74b90abb71f6b53898dd7fe47300cc940104c4f79148f0671f790101"}, +] + +[package.dependencies] +python-dateutil = ">=2.4.0" + +[[package]] +name = "x-wr-timezone" +version = "0.0.5" +description = "A Python module and program to convert calendars using X-WR-TIMEZONE to standard ones." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "x_wr_timezone-0.0.5-py3-none-any.whl", hash = "sha256:e438b27b96635f5f712a4fb5dda4c82597a53a412fe834c9fe8409fddb3fc2b1"}, + {file = "x_wr_timezone-0.0.5.tar.gz", hash = "sha256:c05cb34b9b58a4607a788db086dcae5766728e4b94e0672870dc5593a6e13fe6"}, +] + +[package.dependencies] +icalendar = "*" +pytz = "*" + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "c5dd090c8750013b4ea3a1c79ede4226cc28f5ea999544d29f6021344754e459" diff --git a/pyproject.toml b/pyproject.toml index b7b1353..d7be8f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.poetry] -name = "masto-gitsocial-bridge" +name = "masto-bridges" version = "0.1.0" -description = "Crosspost between mastodon and git.social" +description = "Crosspost between mastodon and git.social and also CalDAV for some reason" authors = ["sneakers-the-rat "] license = "AGPL-3.0" readme = "README.md" -packages = [{include = "masto_git_bridge"}] +packages = [{include = "masto_bridges"}] [tool.poetry.dependencies] python = "^3.9" @@ -14,10 +14,12 @@ pydantic = {extras = ["dotenv", "email"], version = "^1.10.2"} rich = "^12.6.0" beautifulsoup4 = "^4.11.1" lxml = "^4.9.1" +caldav = "^1.2.1" +icalendar = "^5.0.7" [tool.poetry.scripts] -post_last_commit = 'masto_git_bridge.main:post_last_commit' -masto_gitbot = 'masto_git_bridge.main:masto_gitbot' +post_last_commit = 'masto_bridges.main:post_last_commit' +masto_bridgebot = 'masto_bridges.main:masto_bridgebot' [build-system] requires = ["poetry-core"]