Skip to content

Commit

Permalink
more tweaking, more testing. had to add __str__ to the dataclasses
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Philion committed Mar 4, 2024
1 parent 06a9ac4 commit 59caf17
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 30 deletions.
2 changes: 0 additions & 2 deletions cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ async def tickets(self, ctx: discord.ApplicationContext, params: str = ""):
# add groups to users.

# lookup the user
log.debug(f"looking for user mapping for {ctx}")

user = self.redmine.user_mgr.find(ctx.user.name)
log.debug(f"found user mapping for {ctx.user.name}: {user}")

Expand Down
4 changes: 2 additions & 2 deletions netbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ async def on_application_command_error(self, context: discord.ApplicationContext
if isinstance(exception, commands.CommandOnCooldown):
await context.respond("This command is currently on cooldown!")
else:
log.warning(f"{context} - {exception}", exc_info=True)
log.warning(f"{context.user}/{context.command} - {exception.__cause__}", exc_info=exception.__cause__)
#raise error # Here we raise other errors to ensure they aren't ignored
await context.respond(f"Error processing your request: {exception}")
await context.respond(f"Error processing due to: {exception.__cause__}")


def main():
Expand Down
5 changes: 4 additions & 1 deletion redmine.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def __init__(self, url: str, token: str):
self.user_mgr:UserManager = UserManager(session)
self.ticket_mgr:TicketManager = TicketManager(session)

self.user_mgr.reindex() # build the cache when starting


@classmethod
def fromenv(cls):
Expand Down Expand Up @@ -161,7 +163,8 @@ def get_team(self, teamname:str):
def update_sync_record(self, record:synctime.SyncRecord):
self.ticket_mgr.update_sync_record(record)

def get_field(self, ticket:Ticket, fieldname:str):
# mostly for formatting
def get_field(self, ticket:Ticket, fieldname:str) -> str:
match fieldname:
case "url":
return f"{self.url}/issues/{ticket.id}"
Expand Down
11 changes: 6 additions & 5 deletions session.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import os
import logging

from urllib3.exceptions import ConnectTimeoutError
import requests
from requests.exceptions import ConnectTimeout, ConnectionError


log = logging.getLogger(__name__)


TIMEOUT = 10 # seconds
TIMEOUT = 5 # seconds


class RedmineException(Exception):
Expand Down Expand Up @@ -73,11 +74,11 @@ def get(self, query_str:str, user:str=None):
return r.json()
else:
log.info(f"GET {r.reason}/{r.status_code} url={r.request.url}, reqid={r.headers['X-Request-Id']}")
except TimeoutError as toe:
except (TimeoutError, ConnectTimeoutError, ConnectTimeout, ConnectionError):
# ticket-509: Handle timeout gracefully
log.warning(f"Timeout during {query_str}: {toe}")
log.warning(f"TIMEOUT ({TIMEOUT}s) during {query_str}")
except Exception as ex:
log.exception(f"Exception during {query_str}: {ex}")
log.exception(f"{type(ex)} during {query_str}: {ex}")

return None

Expand Down
8 changes: 6 additions & 2 deletions test_netbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""NetBot Test Suite"""

import unittest
from unittest import mock
import logging

import discord
Expand Down Expand Up @@ -103,12 +104,15 @@ async def test_sync_ticket_long_message(self):

async def test_on_application_command_error(self):
ctx = self.build_context()
error = discord.DiscordException("this is exception " + self.tag)
await self.bot.on_application_command_error(ctx, error)
error = netbot.NetbotException("this is exception " + self.tag)
wrapper = discord.DiscordException("Discord Ex Wrapper")
wrapper.__cause__ = error
await self.bot.on_application_command_error(ctx, wrapper)
self.assertIn(self.tag, ctx.respond.call_args.args[0])




if __name__ == '__main__':
# when running this main, turn on DEBUG
logging.basicConfig(level=logging.DEBUG, format="{asctime} {levelname:<8s} {name:<16} {message}", style='{')
Expand Down
10 changes: 7 additions & 3 deletions test_redmine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import test_utils


logging.getLogger().setLevel(logging.ERROR)


log = logging.getLogger(__name__)


@unittest.skipUnless(load_dotenv(), "ENV settings not available")
class TestRedmine(test_utils.RedmineTestCase):
"""Test suite for Redmine client"""

def test_blocked_user(self):
def test_block_user(self):
# block
self.user_mgr.block(self.user)
self.assertTrue(self.user_mgr.is_blocked(self.user))
Expand Down Expand Up @@ -47,8 +50,9 @@ def test_client_timeout(self):
# construct an invalid client to try to get a timeout
try:
client = redmine.Client("http://192.168.1.42/", "bad-token")
log.info(client)
except TimeoutError:
self.assertIsNotNone(client)
#log.info(client)
except Exception:
self.fail("Got unexpected timeout")


Expand Down
8 changes: 5 additions & 3 deletions test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from users import User, UserManager
import session
import tickets
import redmine
from redmine import Client

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,14 +102,14 @@ def tearDownClass(cls):

class BotTestCase(unittest.IsolatedAsyncioTestCase):
"""Abstract base class for testing Bot features"""
redmine: session.RedmineSession = None
redmine: Client = None
usertag: str = None
user: User = None

@classmethod
def setUpClass(cls):
log.info("Setting up test fixtures")
cls.redmine:redmine.Client = redmine.Client.fromenv()
cls.redmine:Client = Client.fromenv()
cls.usertag:str = tagstr()
cls.user:User = create_test_user(cls.redmine.user_mgr, cls.usertag)
log.info(f"Created test user: {cls.user}")
Expand All @@ -125,6 +125,8 @@ def build_context(self) -> ApplicationContext:
ctx = mock.AsyncMock(ApplicationContext)
ctx.user = mock.AsyncMock(discord.Member)
ctx.user.name = self.discord_user
ctx.command = mock.AsyncMock(discord.ApplicationCommand)
ctx.command.name = unittest.TestCase.id(self)
log.debug(f"created ctx with {self.discord_user}: {ctx}")
return ctx

Expand Down
36 changes: 29 additions & 7 deletions tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class TicketStatus():
name: str
is_closed: bool

def __str__(self):
return self.name


@dataclass
class PropertyChange(): # https://www.redmine.org/projects/redmine/wiki/Rest_IssueJournals
Expand All @@ -39,33 +42,33 @@ class PropertyChange(): # https://www.redmine.org/projects/redmine/wiki/Rest_Iss
old_value: str
new_value: str

def __str__(self):
return f"{self.name}/{self.property} {self.old_value} -> {self.new_value}"


@dataclass
class TicketNote(): # https://www.redmine.org/projects/redmine/wiki/Rest_IssueJournals
"""a message sent to a ticket"""
id: int
user: NamedId
notes: str
created_on: dt.datetime
private_notes: bool
details: list[PropertyChange]
user: NamedId = None

def __post_init__(self):
self.user = NamedId(**self.user)
self.created_on = synctime.parse_str(self.created_on)
if self.details:
self.details = [PropertyChange(**change) for change in self.details]

def __str__(self):
return f"#{self.id} - {self.user}: {self.notes}"

@dataclass
class Ticket():
"""Encapsulates a redmine ticket"""
id: int
project: NamedId
tracker: NamedId
status: TicketStatus
priority: NamedId
author: NamedId
subject: str
description: str
done_ratio: float
Expand All @@ -77,15 +80,28 @@ class Ticket():
created_on: dt.datetime
updated_on: dt.datetime
closed_on: dt.datetime
project: NamedId = None
tracker: NamedId = None
priority: NamedId = None
author: NamedId = None
status: TicketStatus = None
parent: NamedId = None
spent_hours: float = 0.0
total_spent_hours: float = 0.0
category: str = None
assigned_to: NamedId = None
custom_fields: list[CustomField] = None
journals: list[TicketNote] = None

def __post_init__(self):
self.status = TicketStatus(**self.status)
self.author = NamedId(**self.author)
self.priority = NamedId(**self.priority)
self.project = NamedId(**self.project)
self.tracker = NamedId(**self.tracker)

if self.assigned_to:
self.assigned_to = NamedId(**self.assigned_to)
if self.created_on:
self.created_on = synctime.parse_str(self.created_on)
if self.updated_on:
Expand All @@ -108,6 +124,9 @@ def get_custom_field(self, name: str) -> str:
return field.value
return None

def __str__(self):
return f"#{self.id} {self.project} {self.status} {self.priority} {self.assigned_to}: {self.subject}"

def get_sync_record(self, expected_channel: int) -> synctime.SyncRecord:
# Parse custom_field into datetime
# lookup field by name
Expand Down Expand Up @@ -151,7 +170,9 @@ def get_notes(self, since:dt.datetime=None) -> list[TicketNote]:
return notes

def get_field(self, fieldname):
return getattr(self, fieldname)
val = getattr(self, fieldname)
#log.debug(f">>> {fieldname} = {val}, type={type(val)}")
return val



Expand Down Expand Up @@ -376,6 +397,7 @@ def my_tickets(self, user=None) -> list[Ticket]:
if not jresp:
return None

#log.debug(f"### json: {jresp}")
response = TicketsResult(**jresp)
if response.total_count > 0:
return response.issues
Expand Down
30 changes: 25 additions & 5 deletions users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,20 @@ class CustomField():
name: str
value: str

def __str__(self) -> str:
return f"field-{self.id}:{self.name}={self.value}"

@dataclass
class NamedId:
'''named ID in redmine'''
id: int
name: str
name: str = None

def __str__(self) -> str:
return self.name
if self.name:
return self.name
else:
return str(self.id)


@dataclass
Expand All @@ -46,6 +51,9 @@ def __post_init__(self):
if self.users:
self.users = [NamedId(**name) for name in self.users]

def __str__(self) -> str:
return self.name


@dataclass
class User():
Expand All @@ -71,14 +79,23 @@ def __post_init__(self):
self.custom_fields = [CustomField(**field) for field in self.custom_fields]
self.discord_id = self.get_custom_field(DISCORD_ID_FIELD)


def get_custom_field(self, name: str) -> str:
for field in self.custom_fields:
if field.name == name:
return field.value

return None

def full_name(self) -> str:
if self.firstname is None or len(self.firstname) < 2:
return self.lastname
if self.lastname is None or len(self.lastname) < 2:
return self.firstname
return self.firstname + " " + self.lastname

def __str__(self):
return f"#{self.id} {self.full_name()} login={self.login} discord={self.discord_id}"


@dataclass
class UserResult:
Expand All @@ -91,6 +108,9 @@ class UserResult:
def __post_init__(self):
self.users = [User(**user) for user in self.users]

def __str__(self):
return f"users:({[u.login + ',' for u in self.users]}), total={self.total_count}, {self.limit}/{self.offset}"


class UserCache():
"""cache of user data"""
Expand Down Expand Up @@ -486,7 +506,7 @@ def reindex_users(self):
log.debug(f"indexed {len(all_users)} users")
log.debug(f"discord users: {self.cache.discord_users}")
else:
log.error("No users to index")
log.warning("No users to index")


def reindex_teams(self):
Expand All @@ -495,7 +515,7 @@ def reindex_teams(self):
self.cache.teams = all_teams # replace all the cached teams
log.debug(f"indexed {len(all_teams)} teams")
else:
log.error("No teams to index")
log.warning("No teams to index")


def reindex(self):
Expand Down

0 comments on commit 59caf17

Please sign in to comment.