Newer
Older
@attr.s(frozen=True)
class Error(object):
"""
This is a non-persistent state in which a voucher exists when the database
state is **pending** but the most recent redemption attempt has failed due
to an error that is not handled by any other part of the system.
"""
finished = attr.ib(validator=attr.validators.instance_of(datetime))
details = attr.ib(validator=attr.validators.instance_of(str))
Jean-Paul Calderone
committed
def should_start_redemption(self):
return True
def to_json_v1(self):
return {
"finished": self.finished.isoformat(),
class Voucher(object):

Tom Prince
committed
:ivar bytes number: The byte string which gives this voucher its
identity.
:ivar datetime created: The time at which this voucher was added to this
node.
:ivar expected_tokens: The total number of tokens for which we expect to
be able to redeem this voucher. Tokens are redeemed in smaller
groups, progress of which is tracked in ``state``. This only gives
the total we expect to reach at completion.
:ivar state: An indication of the current state of this voucher. This is
an instance of ``Pending``, ``Redeeming``, ``Redeemed``,
``DoubleSpend``, ``Unpaid``, or ``Error``.
number = attr.ib(
validator=attr.validators.and_(

Tom Prince
committed
attr.validators.instance_of(bytes),
is_base64_encoded(urlsafe_b64decode),
has_length(44),
),
)
expected_tokens = attr.ib(
validator=attr.validators.optional(
attr.validators.and_(
attr.validators.instance_of((int, long)),
greater_than(0),
),
),
)
created = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(datetime)),
)
state = attr.ib(
default=Pending(counter=0),
validator=attr.validators.instance_of(
(
Pending,
Redeeming,
Redeemed,
DoubleSpend,
Unpaid,
Error,
)
),
@classmethod
def from_row(cls, row):
def state_from_row(state, row):
return Pending(counter=row[3])
return DoubleSpend(
row[1],
)
raise ValueError("Unknown voucher state {}".format(state))
number, created, expected_tokens, state = row[:4]

Tom Prince
committed
number=number.encode("ascii"),
expected_tokens=expected_tokens,
# All Python datetime-based date/time libraries fail to handle
# leap seconds. This parse call might raise an exception of the
# value represents a leap second. However, since we also use
# Python to generate the data in the first place, it should never
# represent a leap second... I hope.
created=parse_datetime(created, delimiter=" "),
state=state_from_row(state, row[4:]),
@classmethod
def from_json(cls, json):
values = loads(json)
return getattr(cls, "from_json_v{}".format(version))(values)
@classmethod
def from_json_v1(cls, values):
state_json = values["state"]
state_name = state_json["name"]
if state_name == "pending":
state = Pending(counter=state_json["counter"])
elif state_name == "redeeming":
started=parse_datetime(state_json["started"]),
counter=state_json["counter"],
state = DoubleSpend(
finished=parse_datetime(state_json["finished"]),
finished=parse_datetime(state_json["finished"]),
token_count=state_json["token-count"],
finished=parse_datetime(state_json["finished"]),
state = Error(
finished=parse_datetime(state_json["finished"]),
details=state_json["details"],
raise ValueError("Unrecognized state {!r}".format(state_json))
number=values["number"].encode("ascii"),
expected_tokens=values["expected-tokens"],
if values["created"] is None
else parse_datetime(values["created"]),
def to_json(self):
return dumps_utf8(self.marshal())
def marshal(self):
return self.to_json_v1()
def to_json_v1(self):
"number": self.number.decode("ascii"),
"expected-tokens": self.expected_tokens,
"created": None if self.created is None else self.created.isoformat(),