Skip to content

Commit

Permalink
Early support for config.toml, user lookup, and user add
Browse files Browse the repository at this point in the history
  • Loading branch information
synrg committed Sep 8, 2024
1 parent 35a1d06 commit 2626871
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 8 deletions.
13 changes: 5 additions & 8 deletions dronefly/core/clients/inat.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
"""Module to access iNaturalist API."""
from contextlib import contextmanager
from inspect import signature
import os
from typing import Optional

from platformdirs import user_data_dir
from pyinaturalist import (
ClientSession,
FileLockSQLiteBucket,
iNatClient as pyiNatClient,
)
from pyinaturalist.constants import RequestParams
import os

from ..constants import INAT_DEFAULTS

BASE_PATH = os.path.join(user_data_dir(), "dronefly-core")
from ..constants import INAT_DEFAULTS, USER_DATA_PATH


class iNatClient(pyiNatClient):
"""iNat client based on pyinaturalist."""

def __init__(self, *args, **kwargs):
ratelimit_path = os.path.join(BASE_PATH, "ratelimit.db")
lock_path = os.path.join(BASE_PATH, "ratelimit.lock")
cache_file = os.path.join(BASE_PATH, "api_requests.db")
ratelimit_path = os.path.join(USER_DATA_PATH, "ratelimit.db")
lock_path = os.path.join(USER_DATA_PATH, "ratelimit.lock")
cache_file = os.path.join(USER_DATA_PATH, "api_requests.db")
session = ClientSession(
bucket_class=FileLockSQLiteBucket,
cache_file=cache_file,
Expand Down
84 changes: 84 additions & 0 deletions dronefly/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from typing import Union

from attrs import define
from requests import HTTPError
from rich.markdown import Markdown

from ..clients.inat import iNatClient
from ..constants import (
CONFIG_PATH,
INAT_DEFAULTS,
INAT_USER_DEFAULT_PARAMS,
RANK_EQUIVALENTS,
Expand All @@ -22,8 +24,10 @@
ListFormatter,
ObservationFormatter,
TaxonFormatter,
UserFormatter,
p,
)
from ..models.config import Config
from ..models.user import User
from ..query.query import get_base_query_args, QueryResponse

Expand Down Expand Up @@ -101,6 +105,7 @@ class Commands:
inat_client: iNatClient = iNatClient()
parser: NaturalParser = NaturalParser()
format: Format = Format.discord_markdown
config: Config = Config()

def _parse(self, query_str):
return self.parser.parse(query_str)
Expand Down Expand Up @@ -129,6 +134,7 @@ def _get_formatted_page(
return self._format_markdown(markdown_text)

def _format_markdown(self, markdown_text: str):
"""Format Rich vs. Discord markdown."""
if self.format == Format.rich:
# Richify the markdown:
# - In Discord markdown, all newlines are rendered as line breaks
Expand All @@ -154,6 +160,22 @@ def _format_markdown(self, markdown_text: str):
response = markdown_text
return response

def _simple_format_markdown(self, markdown_text: str):
"""Simplified formatter for Rich vs. Discord markdown.
Discord vs. Rich linebreak rendering is harder than we thought, e.g.
`_format_markdown()` doesn't give correct results with point-form
or numbered lists. If special handling of newlines isn't needed, then
use this helper instead.
"""
if self.format == Format.rich:
# Richify the markdown
response = Markdown(markdown_text)
else:
# Return the literal markdown for Discord to render
response = markdown_text
return response

def life(self, ctx: Context, *args):
_args = " ".join(args) or "by me"
query = self._parse(_args)
Expand Down Expand Up @@ -456,3 +478,65 @@ def obs(self, ctx: Context, *args):
response = self._get_formatted_page(formatter)

return response

def user(self, ctx: Context, user_id: str):
with self.inat_client.set_ctx(ctx) as client:
user = None
try:
user = client.users(user_id)
except HTTPError as err:
if err.response.status_code == 404:
pass
if not user:
return "User not found."

return self._format_markdown(UserFormatter(user).format())

def user_add(self, ctx: Context, user_abbrev: str, user_id: str):
if user_abbrev != "me":
return "Only `user add me <user-id>` is supported at this time."
user_config = self.config.user(ctx.author.id)
configured_user_id = None
if user_config:
configured_user_id = user_config.get("inat_user_id")

with self.inat_client.set_ctx(ctx) as client:
user = None
try:
user = client.users(user_id)
except HTTPError as err:
if err.response.status_code == 404:
pass
if not user:
return "User not found."

response = ""
redefining = False
if configured_user_id:
if configured_user_id == user.id:
return "User already added."
configured_user = None
try:
configured_user = client.users(configured_user_id)
except HTTPError as err:
if err.response.status_code == 404:
pass
if configured_user:
configured_user_str = UserFormatter(configured_user).format()
else:
configured_user_str = f"User id not found: {configured_user_id}"
redefining = True
response += (
f"- Already defined as another user: {configured_user_str}\n"
)
response += "- To change to the specified user:\n"

response += f"1. Confirm this is you: {UserFormatter(user).format()}\n"
add_or_mod = "modify" if redefining else "add"
response += f"2. Edit `{CONFIG_PATH}` and {add_or_mod}:\n"
response += (
f"```toml\n[users.{ctx.author.id}]\ninat_user_id = {user.id}\n```\n"
)
response += "3. Restart dronefly-cli."

return self._simple_format_markdown(response)
8 changes: 8 additions & 0 deletions dronefly/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
from platformdirs import user_data_dir
import os

from pyinaturalist.constants import COMMON_RANKS, RANK_EQUIVALENTS, RANK_LEVELS


USER_DATA_PATH = os.path.join(user_data_dir(), "dronefly-core")
CONFIG_PATH = os.path.join(USER_DATA_PATH, "config.toml")


# Map Dronefly User param names to iNat API param names:
INAT_USER_DEFAULT_PARAMS = {
"inat_place_id": "preferred_place_id",
Expand Down
29 changes: 29 additions & 0 deletions dronefly/core/formatters/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1453,3 +1453,32 @@ def description(self):
if filter:
description.append(filter)
return " ".join(description)


class UserFormatter(BaseFormatter):
def __init__(
self,
user: User,
):
"""
Parameters
----------
user: User
The user to format.
"""
self.user = user

def format(self, with_link: bool = True):
"""Format the user as markdown.
with_link: bool, optional
Link to user's profile.
"""
name = self.user.name
if name:
name += f" ({self.user.login})"
else:
name = self.user.login
if with_link:
name = f"[{name}]({self.user.url})"
return name
3 changes: 3 additions & 0 deletions dronefly/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .config import * # noqa: F401,F403
from .controlled_terms import * # noqa: F401,F403
from .user import * # noqa: F401,F403
34 changes: 34 additions & 0 deletions dronefly/core/models/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from attrs import define
import tomllib
from typing import Optional, Union

from ..constants import CONFIG_PATH


@define
class Config:
"""Public class for Config model."""

data: dict = {}

def __init__(self, data_str: Optional[str] = None):
try:
self.load(data_str)
except FileNotFoundError:
pass
except (tomllib.TOMLDecodeError, OSError) as err:
print(err)

def load(self, data_str: Optional[str] = None):
self.data = {}
if data_str:
self.data = tomllib.loads(data_str)
else:
with open(CONFIG_PATH, "rb") as config_file:
self.data = tomllib.load(config_file)

def user(self, user_id: Union[str, int]):
try:
return self.data["users"][str(user_id)]
except KeyError:
return None

0 comments on commit 2626871

Please sign in to comment.