Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions compiler_admin/api/toggl.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,108 @@ def detailed_time_entries(self, start_date: datetime, end_date: datetime, **kwar

return response

def list_clients(
self, ids: list[int] | None = None, name: str | None = None, start: int | None = None
) -> requests.Response:
"""Request a filtered list of clients from Toggl Reports utils.

Args:
ids (list[int] | None): Optional client IDs to filter.
name (str | None): Optional client name filter.
start (int | None): Optional pagination cursor.

Returns:
requests.Response: The HTTP response.
"""
params: dict[str, object] = {}
if ids is not None:
params["ids"] = ids
if name is not None:
params["name"] = name
if start is not None:
params["start"] = start

url = self.make_api_url("filters/clients")
return self._post(url, **params)

def list_projects(
self,
client_ids: list[int] | None = None,
currency: str | None = None,
ids: list[int] | None = None,
is_active: bool | None = None,
is_billable: bool | None = None,
is_private: bool | None = None,
name: str | None = None,
page_size: int | None = None,
start: int | None = None,
) -> requests.Response:
"""Request a filtered list of projects from Toggl Reports utils.

Args:
client_ids (list[int] | None): Optional Toggl client IDs.
currency (str | None): Optional currency filter.
ids (list[int] | None): Optional project IDs.
is_active (bool | None): Optional archived state filter.
is_billable (bool | None): Optional billable filter.
is_private (bool | None): Optional private filter.
name (str | None): Optional project name filter.
page_size (int | None): Optional page size.
start (int | None): Optional pagination cursor.

Returns:
requests.Response: The HTTP response.
"""
params: dict[str, object] = {}
if client_ids is not None:
params["client_ids"] = client_ids
if currency is not None:
params["currency"] = currency
if ids is not None:
params["ids"] = ids
if is_active is not None:
params["is_active"] = is_active
if is_billable is not None:
params["is_billable"] = is_billable
if is_private is not None:
params["is_private"] = is_private
if name is not None:
params["name"] = name
if page_size is not None:
params["page_size"] = page_size
if start is not None:
params["start"] = start

url = self.make_api_url("filters/projects")
return self._post(url, **params)

def list_project_users(
self,
client_ids: list[int] | None = None,
project_ids: list[int] | None = None,
start_id: int | None = None,
) -> requests.Response:
"""Request a filtered list of project users from Toggl Reports utils.

Args:
client_ids (list[int] | None): Optional client IDs.
project_ids (list[int] | None): Optional project IDs.
start_id (int | None): Optional pagination cursor.

Returns:
requests.Response: The HTTP response.
"""
params: dict[str, object] = {}
if client_ids is not None:
params["client_ids"] = client_ids
if project_ids is not None:
params["project_ids"] = project_ids
if start_id is not None:
params["start_id"] = start_id

url = self.make_api_url("filters/project_users")
return self._post(url, **params)


class TogglWorkspace(TogglBase):
WORKSPACES_ID = "workspaces/{}"
Expand Down
6 changes: 6 additions & 0 deletions compiler_admin/commands/ls/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import click

from compiler_admin.commands.ls.clients import clients
from compiler_admin.commands.ls.groups import groups
from compiler_admin.commands.ls.orgs import orgs
from compiler_admin.commands.ls.project_users import project_users
from compiler_admin.commands.ls.projects import projects
from compiler_admin.commands.ls.users import users


Expand All @@ -11,6 +14,9 @@ def ls():
pass


ls.add_command(clients)
ls.add_command(groups)
ls.add_command(orgs)
ls.add_command(project_users)
ls.add_command(projects)
ls.add_command(users)
41 changes: 41 additions & 0 deletions compiler_admin/commands/ls/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import click
import pandas as pd

from compiler_admin import FORMATS, Format
from compiler_admin.services import files
from compiler_admin.services.toggl import TogglUtils


@click.command()
@click.option(
"--format",
"format_key",
help="The format of the output.",
type=click.Choice(FORMATS.keys(), case_sensitive=False),
default="basic",
)
@click.option(
"-i",
"--id",
"ids",
multiple=True,
type=int,
help="A Toggl client ID to filter by. Can be supplied more than once.",
)
@click.option("--name", help="Filter clients by name.")
def clients(format_key: str, ids: tuple[int, ...], name: str | None):
"""List Toggl clients from the Compiler workspace."""
format = FORMATS.get(format_key)
ids_list = list(ids) if ids else None

api = TogglUtils()
click.echo("Getting Toggl clients...", err=True)
items = api.get_clients(ids=ids_list, name=name)
click.echo(f"Got {len(items)} Clients", err=True)

stdout = click.get_text_stream("stdout")
if format in [Format.BASIC, Format.CSV]:
dataframe = pd.DataFrame(items)
files.write_csv(stdout, dataframe, columns=["id", "name"])
elif format == Format.JSON:
files.write_json(stdout, items)
4 changes: 2 additions & 2 deletions compiler_admin/commands/ls/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ def toggl(format: int = Format.BASIC, **kwargs):

click.echo("Getting all Toggl groups...", err=True)
groups = api.get_organization_groups()
groups_df = pd.DataFrame(groups)
click.echo(f"Got {len(groups)} Groups", err=True)

stdout = click.get_text_stream("stdout")
if format in [Format.BASIC, Format.CSV]:
files.write_csv(stdout, groups_df, columns=["group_id", "name", "at"])
dataframe = pd.DataFrame(groups)
files.write_csv(stdout, dataframe, columns=["group_id", "name", "at"])
elif format == Format.JSON:
files.write_json(stdout, groups)

Expand Down
60 changes: 60 additions & 0 deletions compiler_admin/commands/ls/project_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import click
import pandas as pd

from compiler_admin import FORMATS, Format
from compiler_admin.services import files
from compiler_admin.services.toggl import TogglUtils


@click.command(name="project-users")
@click.option(
"--format",
"format_key",
help="The format of the output.",
type=click.Choice(FORMATS.keys(), case_sensitive=False),
default="basic",
)
@click.option(
"-c",
"--client-id",
"client_ids",
multiple=True,
type=int,
help="A Toggl client ID to filter project users by. Can be supplied more than once.",
)
@click.option(
"-p",
"--project-id",
"project_ids",
multiple=True,
type=int,
help="A Toggl project ID to filter project users by. Can be supplied more than once.",
)
def project_users(
format_key: str,
client_ids: tuple[int, ...],
project_ids: tuple[int, ...],
):
"""List Toggl project users from the Compiler workspace."""
format = FORMATS.get(format_key)
client_ids_list = list(client_ids) if client_ids else None
project_ids_list = list(project_ids) if project_ids else None

api = TogglUtils()
click.echo("Getting Toggl project users...", err=True)
items = api.get_project_users(
client_ids=client_ids_list,
project_ids=project_ids_list,
)
click.echo(f"Got {len(items)} Project Users", err=True)

stdout = click.get_text_stream("stdout")
if format in [Format.BASIC, Format.CSV]:
dataframe = pd.DataFrame(items)
files.write_csv(
stdout,
dataframe,
columns=["id", "group_id", "project_id", "user_id", "hourly_rate", "labour_cost"],
)
elif format == Format.JSON:
files.write_json(stdout, items)
93 changes: 93 additions & 0 deletions compiler_admin/commands/ls/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import click
import pandas as pd

from compiler_admin import FORMATS, Format
from compiler_admin.services import files
from compiler_admin.services.toggl import TogglUtils


@click.command()
@click.option(
"--format",
"format_key",
help="The format of the output.",
type=click.Choice(FORMATS.keys(), case_sensitive=False),
default="basic",
)
@click.option(
"-c",
"--client-id",
"client_ids",
multiple=True,
type=int,
help="A Toggl client ID to filter projects by. Can be supplied more than once.",
)
@click.option(
"-i",
"--id",
"ids",
multiple=True,
type=int,
help="A Toggl project ID to filter by. Can be supplied more than once.",
)
@click.option("--name", help="Filter projects by name.")
@click.option(
"--active/--inactive",
"is_active",
default=True,
help="Filter for active projects if set to --active, archived projects if set to --inactive.",
)
@click.option(
"--billable/--internal",
"is_billable",
default=True,
help="Filter for billable projects if set to --billable, non-billable if set to --internal.",
)
@click.option(
"--private/--public",
"is_private",
default=True,
help="Filter for private projects if set to --private, public projects if set to --public.",
)
def projects(
format_key: str,
client_ids: tuple[int, ...],
ids: tuple[int, ...],
name: str | None,
is_active: bool | None,
is_billable: bool | None,
is_private: bool | None,
):
"""List Toggl projects from the Compiler workspace."""
format = FORMATS.get(format_key)
client_ids_list = list(client_ids) if client_ids else None
ids_list = list(ids) if ids else None

api = TogglUtils()
click.echo("Getting Toggl projects...", err=True)

items = api.get_projects(
client_ids=client_ids_list,
ids=ids_list,
is_active=is_active,
is_billable=is_billable,
is_private=is_private,
name=name,
)
click.echo(f"Got {len(items)} Projects", err=True)

stdout = click.get_text_stream("stdout")
if format in [Format.BASIC, Format.CSV]:
dataframe = pd.DataFrame(items)
# Ensure all requested columns exist on the dataframe to avoid KeyError
requested_columns = ["id", "name", "client_id", "active", "billable", "private"]
for col in requested_columns:
if col not in dataframe.columns:
dataframe[col] = None
files.write_csv(
stdout,
dataframe,
columns=requested_columns,
)
elif format == Format.JSON:
files.write_json(stdout, items)
10 changes: 5 additions & 5 deletions compiler_admin/commands/ls/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ def toggl(format: int = Format.BASIC, inactive: bool = False, account_type: str

click.echo("Getting all Toggl users...", err=True)
users = api.get_organization_users(inactive=inactive, groups=group_filter, **kwargs)
users_df = pd.DataFrame(users)
click.echo(f"Got {len(users)} Users", err=True)

stdout = click.get_text_stream("stdout")
columns = ["email"]
if format == Format.BASIC:
files.write_csv(stdout, users_df, columns=columns)
elif format == Format.CSV:
if format == Format.CSV:
columns += [
"name",
"id",
Expand All @@ -56,7 +53,10 @@ def toggl(format: int = Format.BASIC, inactive: bool = False, account_type: str
"2fa_enabled",
"avatar_url",
]
files.write_csv(stdout, users_df, columns=columns)

if format in [Format.BASIC, Format.CSV]:
dataframe = pd.DataFrame(users)
files.write_csv(stdout, dataframe, columns=columns)
elif format == Format.JSON:
files.write_json(stdout, users)

Expand Down
Loading
Loading