Newer
Older
u"finished": self.finished.isoformat(),
}
@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(unicode))
Jean-Paul Calderone
committed
def should_start_redemption(self):
return True
def to_json_v1(self):
return {
u"name": u"error",
u"finished": self.finished.isoformat(),
u"details": self.details,
}
class Voucher(object):
"""
:ivar unicode number: The text 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_(
attr.validators.instance_of(unicode),
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):
if state == u"pending":
return Pending(counter=row[3])
if state == u"double-spend":
return DoubleSpend(
parse_datetime(row[0], delimiter=u" "),
)
if state == u"redeemed":
return Redeemed(
parse_datetime(row[0], delimiter=u" "),
row[1],
row[2],
)
raise ValueError("Unknown voucher state {}".format(state))
number, created, expected_tokens, state = row[:4]
number=number,
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=u" "),
state=state_from_row(state, row[4:]),
@classmethod
def from_json(cls, json):
values = loads(json)
version = values.pop(u"version")
return getattr(cls, "from_json_v{}".format(version))(values)
@classmethod
def from_json_v1(cls, values):
state_json = values[u"state"]
state_name = state_json[u"name"]
if state_name == u"pending":
state = Pending(counter=state_json[u"counter"])
elif state_name == u"redeeming":
state = Redeeming(
started=parse_datetime(state_json[u"started"]),
counter=state_json[u"counter"],
elif state_name == u"double-spend":
state = DoubleSpend(
finished=parse_datetime(state_json[u"finished"]),
)
elif state_name == u"redeemed":
state = Redeemed(
finished=parse_datetime(state_json[u"finished"]),
token_count=state_json[u"token-count"],
public_key=state_json[u"public-key"],
elif state_name == u"unpaid":
state = Unpaid(
finished=parse_datetime(state_json[u"finished"]),
)
elif state_name == u"error":
state = Error(
finished=parse_datetime(state_json[u"finished"]),
details=state_json[u"details"],
)
raise ValueError("Unrecognized state {!r}".format(state_json))
return cls(
number=values[u"number"],
expected_tokens=values[u"expected-tokens"],
created=None if values[u"created"] is None else parse_datetime(values[u"created"]),
def to_json(self):
return dumps(self.marshal())
def marshal(self):
return self.to_json_v1()
def to_json_v1(self):
return {
u"number": self.number,
u"expected-tokens": self.expected_tokens,
u"created": None if self.created is None else self.created.isoformat(),
u"version": 1,
}