Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/run-test-programs-single-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
python: ['3.9','3.13']
irods_server: ['4.3.4','5.0.2']
irods_server: ['4.3.5','5.0.2']

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-test-suite-multiple-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
python: ['3.9','3.13']
irods_server: ['4.3.4','5.0.2']
irods_server: ['4.3.5','5.0.2']

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-test-suite-single-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
python: ['3.9','3.13']
irods_server: ['4.3.4','5.0.2']
irods_server: ['4.3.5','5.0.2']

steps:
- name: Checkout
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,62 @@ relations:
['1636757427']
```

Listing All Tickets
-------------------

In PRC v3.3.0 and later, Ticket objects constructed using a query `result` will contain
attributes (such as `id`, `string`, `create_time`, `modify_time`, etc.) reflecting
the columns fetched from that query.

As before, of course, we can still construct the 'raw' Ticket object from a session object and
optional ticket string (leaving out the `result` parameter) and then use that object to generate
a new ticket or delete an existing ticket of the given name.

A free function also exists, irods.ticket.ticket_iterator, which by default (like `iticket ls`)
iterates over a query of the `TicketQuery.Ticket` model to enumerate the set of
ticket objects visible to the current user. Note that for a rodsadmin, this will include
not only their own tickets, but (unlike `iticket ls`) also those generated by rodsusers as well, including
possibly some tickets belonging to users no longer existing. To avoid this, we can query in
following manner:

```py
from pprint import pp
from irods.ticket import TicketQuery
pp([vars(t) for t in
ticket_iterator(session, filter_args=[TicketQuery.Owner.name != ''])
])
```

The above should produce a listing fairly close to what one would expect from `iticket ls`.

How can a rodsadmin selectively access "orphan" tickets (those no longer owned by any user)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's turn this into a subsection and adjust the wording to not be a question.

For example ...

### Removing orphan tickets


Although one cannot filter a Genquery1-type query through the negation of the above filter,
i.e. `TicketQuery.Owner.name == ''`, there are other ways to accomplish the same result.
For example, to clean the system of the orphan tickets (those from users which have been deleted),
we can do something like this:

```py
import collections, itertools
import irods.keywords as kw
from irods.models import User
from irods.ticket import ticket_iterator

def delete_ticket_and_ignore_result(ticket):
ticket.delete(**{kw.ADMIN_KW:''})
return True

existing_users = [row[User.id] for row in session.query(User.id)]

# 'deque' will exhaust the ticket iterator without the storage overhead of 'list' or 'tuple'
collections.deque(
itertools.dropwhile(
delete_ticket_and_ignore_result,
(_ for _ in ticket_iterator(session) if _.user_id not in existing_users)
),
maxlen=0)
```

Tracking and manipulating replicas of Data Objects
--------------------------------------------------

Expand Down
5 changes: 2 additions & 3 deletions irods/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,8 @@ class Ticket(Model):
write_byte_count = Column(Integer, "TICKET_WRITE_BYTE_COUNT", 2213)
write_byte_limit = Column(Integer, "TICKET_WRITE_BYTE_LIMIT", 2214)

## For now, use of these columns raises CAT_SQL_ERR in both PRC and iquest: (irods/irods#5929)
# create_time = Column(String, 'TICKET_CREATE_TIME', 2209)
# modify_time = Column(String, 'TICKET_MODIFY_TIME', 2210)
create_time = Column(DateTime, 'TICKET_CREATE_TIME', 2209, min_version=(4, 3, 0))
modify_time = Column(DateTime, 'TICKET_MODIFY_TIME', 2210, min_version=(4, 3, 0))

class DataObject(Model):
"""For queries of R_DATA_MAIN when joining to R_TICKET_MAIN.
Expand Down
58 changes: 49 additions & 9 deletions irods/test/ticket_test.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
#! /usr/bin/env python

import calendar
import datetime
import os
import sys
import unittest
import tempfile
import time
import calendar
import unittest

import irods.test.helpers as helpers
import tempfile
from irods.session import iRODSSession
import irods.exception as ex
import irods.keywords as kw
from irods.ticket import Ticket
from irods.models import TicketQuery, DataObject, Collection

from irods.models import Collection, DataObject, TicketQuery
from irods.session import iRODSSession
from irods.test import helpers
from irods.ticket import Ticket, ticket_iterator

# As with most of the modules in this test suite, session objects created via
# make_session() are implicitly agents of a rodsadmin unless otherwise indicated.
Expand Down Expand Up @@ -48,7 +48,6 @@ def login(self, user):
user=user.name,
password=self.users[user.name],
)

@staticmethod
def irods_homedir(sess, path_only=False):
path = f"/{sess.zone}/home/{sess.username}"
Expand All @@ -73,6 +72,8 @@ def setUp(self):
u = ses.users.get(ses.username)
if u.type != "rodsadmin":
self.skipTest("""Test runnable only by rodsadmin.""")
self.rods_admin_name = ses.username

self.host = ses.host
self.port = ses.port
self.zone = ses.zone
Expand Down Expand Up @@ -358,6 +359,27 @@ def test_coll_read_ticket_between_rodsusers(self):
os.unlink(file_.name)
alice.cleanup()

def test_modify_time_and_create_time_attributes_in_tickets__issue_801(self):
# Specifically we are testing that 'modify_time' and 'create_time' attributes function as expected,

bobs_ticket = None

try:
with self.login(self.bob) as bob:
bobs_ticket = Ticket(bob).issue('write', helpers.home_collection(bob))
time.sleep(2)
bobs_ticket.modify('add', 'user', self.rods_admin_name)

# Reload the ticket, this time with the full complement of attributes present.
bobs_ticket = next(ticket_iterator(bob, filter_args=[TicketQuery.Ticket.string == bobs_ticket.string]))

self.assertGreaterEqual(
bobs_ticket.modify_time, bobs_ticket.create_time + datetime.timedelta(seconds=1)
)
finally:
if bobs_ticket:
bobs_ticket.delete()


class TestTicketOps(unittest.TestCase):

Expand Down Expand Up @@ -455,6 +477,24 @@ def test_data_ticket_write(self):
def test_coll_ticket_write(self):
self._ticket_write_helper(obj_type="coll")

def test_ticket_iterator__issue_120(self):

ses = self.sess
t = None

try:
# t first assigned as a "utility" Ticket object
t = Ticket(ses).issue('read', helpers.home_collection(ses))

# This time, t receives attributes from a query result: notably the id, which we use for the next test.
t = Ticket(ses, result=ses.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.string == t.string).one())

# Check an id attribute is present and listed in the results from list_tickets
self.assertIn(t.id, (ticket.id for ticket in ticket_iterator(ses)))
finally:
if t:
t.delete()


if __name__ == "__main__":
# let the tests find the parent irods lib
Expand Down
74 changes: 59 additions & 15 deletions irods/ticket.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from irods.api_number import api_number
from irods.message import iRODSMessage, TicketAdminRequest
from irods.models import TicketQuery

import calendar

Check failure on line 1 in irods/ticket.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D100

D100: Missing docstring in public module [pydocstyle:undocumented-public-module]
import contextlib
import datetime
import random
import string
import logging
import datetime
import calendar

from typing import Any, Optional, Type, Union # noqa: UP035

logger = logging.getLogger(__name__)
from irods.api_number import api_number
from irods.column import Column
from irods.message import TicketAdminRequest, iRODSMessage
from irods.models import TicketQuery


def get_epoch_seconds(utc_timestamp):
Expand All @@ -28,16 +27,61 @@
raise # final try at conversion, so a failure is an error


def ticket_iterator(session, filter_args=()):
"""
Enumerate the Tickets visible to the user.

Args:
session: an iRODSSession object with which to perform a query.
filter_args: optional arguments for filtering the query.

Returns:
An iterator over a range of Ticket objects.
"""
return (Ticket(session, result=row) for row in session.query(TicketQuery.Ticket).filter(*filter_args))


_COLUMN_KEY = Union[Column, Type[Column]] # noqa: UP006


class Ticket:
def __init__(self, session, ticket="", result=None, allow_punctuation=False):
def __init__(self, session, ticket="", result: Optional[dict[_COLUMN_KEY, Any]] = None, allow_punctuation=False): # noqa: FA100
"""
Initialize a Ticket object. If no 'result' or 'ticket' string is provided, then generate a new
Ticket string automatically.

Args:
session: an iRODSSession object through which API endpoints shall be called.
ticket: an optional ticket string, if a particular one is desired for ticket creation or deletion.
result: a row result from a query, containing at least the columns of irods.models.TicketQuery.Ticket.
allow_punctuation: True if punctuation characters are to be allowed in generating a Ticket string.
(By default, all characters will be digits or letters of the latin alphabet.)

Raises:
RuntimeError: if the given ticket parameter mismatches the result, or if result is of the wrong type.
"""

Check failure on line 62 in irods/ticket.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D205

D205: 1 blank line required between summary line and description [pydocstyle:missing-blank-line-after-summary]
self._session = session

# Do an initial error and sanity check on result.
try:
if result is not None:
ticket = result[TicketQuery.Ticket.string]
except TypeError:
raise RuntimeError(
"If specified, 'result' parameter must be a TicketQuery.Ticket search result"
)
_ticket = result[TicketQuery.Ticket.string]
except (TypeError, KeyError) as exc:
raise RuntimeError("If specified, 'result' parameter must be a TicketQuery.Ticket query result.") from exc

# Process query result if given, and set object attributes from it.
if result is not None:
if _ticket != ticket != "":
raise RuntimeError("A ticket name was specified but does not match the query result.")
ticket = _ticket
for attr, value in TicketQuery.Ticket.__dict__.items():
if value is TicketQuery.Ticket.string:
continue
if not attr.startswith("_"):
# backward compatibility with older schema versions
with contextlib.suppress(KeyError):
setattr(self, attr, result[value])

self._ticket = (
ticket if ticket else self._generate(allow_punctuation=allow_punctuation)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@




irods-catalog
5432
ICAT
irods
y
testpassword

y
demoResc

tempZone
1247
20000
20199
1248

rods
y
TEMPORARY_ZONE_KEY
32_byte_server_negotiation_key__
32_byte_server_control_plane_key
rods


Loading