Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base: 6def13b636
...
compare: c816d444d2
Checking mergeability… Don't worry, you can still create the pull request.
  • 9 commits
  • 4 files changed
  • 0 commit comments
  • 2 contributors
View
289 smarkets/uuid.py
@@ -10,128 +10,211 @@
"""
import types
-
-_CHARS = '0123456789' \
- 'abcdefghijklmnopqrstuvwxyz' \
- 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-
-_UUID_TAGS = {
- 'Account': 'acc1',
- 'ContractGroup': 'c024',
- 'Contract': 'cccc',
- 'Order': 'fff0',
- 'Comment': 'b1a4',
- 'Entity': '0444',
- 'Event': '1100',
- 'Session': '9999',
- 'User': '0f00',
- 'Referrer': '4e4e'
-}
-
-_UUID_TAGS_BY_TAG = dict(((v, k) for k, v in _UUID_TAGS.iteritems()))
-_UUID_INT_TAGS = dict((k, int(v, 16)) for k, v in _UUID_TAGS.iteritems())
-_UUID_INT_TAGS_BY_TAG = dict((int(v, 16), k) for k, v in _UUID_TAGS.iteritems())
-
-
-def _base_n(number, chars):
+from collections import namedtuple
+
+
+UuidTagBase = namedtuple('UuidTagBase', ['name', 'int_tag', 'prefix'])
+UuidBase = namedtuple('UuidBase', ['number', 'tag'])
+
+
+class UuidTag(UuidTagBase):
+ "Represents tag information"
+ __slots__ = ()
+ tag_mult = 1 << 16
+
+ @property
+ def hex_str(self):
+ "Hex tag value"
+ return '%x' % self.int_tag
+
+ def tag_number(self, number):
+ "Adds this tag to a number"
+ return number * self.tag_mult + self.int_tag
+
+ @classmethod
+ def split_int_tag(cls, number):
+ "Splits a number into the ID and tag"
+ return divmod(number, cls.tag_mult)
+
+
+class Uuid(UuidBase):
+ "Represents a UUID"
+ __slots__ = ()
+ chars = '0123456789' \
+ 'abcdefghijklmnopqrstuvwxyz' \
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ tag_list = (
+ UuidTag('Account', int('acc1', 16), 'a'),
+ UuidTag('ContractGroup', int('c024', 16), 'm'),
+ UuidTag('Contract', int('cccc', 16), 'c'),
+ UuidTag('Order', int('fff0', 16), 'o'),
+ UuidTag('Comment', int('b1a4', 16), 'b'),
+ UuidTag('Entity', int('0444', 16), 'n'),
+ UuidTag('Event', int('1100', 16), 'e'),
+ UuidTag('Session', int('9999', 16), 's'),
+ UuidTag('User', int('0f00', 16), 'u'),
+ UuidTag('Referrer', int('4e4e', 16), 'r'),
+ )
+
+ # Various indexes into uuid map
+ tags = dict((t.name, t) for t in tag_list)
+ tags_by_hex_str = dict((t.hex_str, t) for t in tag_list)
+ tags_by_prefix = dict((t.prefix, t) for t in tag_list)
+ tags_by_int_tag = dict((t.int_tag, t) for t in tag_list)
+ mask64 = (1 << 64) - 1
+
+ @property
+ def low(self):
+ "Lower 64 bits of number"
+ return self.number & self.mask64
+
+ @property
+ def high(self):
+ "Higher 64 bits of number"
+ return (self.number >> 64) & self.mask64
+
+ @property
+ def shorthex(self):
+ "Short hex representation of Uuid"
+ return '%x' % self.number
+
+ def to_slug(self, base=36, chars=None, pad=0):
+ "Convert to slug representation"
+ if chars is None:
+ chars = self.chars
+ if base < 2 or base > len(chars):
+ raise TypeError("base must be between 2 and %s" % len(chars))
+ chars = chars[:base]
+ number = self.tag.tag_number(self.number)
+ slug = self.pad_uuid(self.base_n(number, chars), pad=pad)
+ return '%s-%s' % (self.tag.prefix, slug)
+
+ def to_hex(self, pad=32):
+ "Convert to tagged hex representation"
+ hex_str = '%x%s' % (self.number, self.tag.hex_str)
+ return self.pad_uuid(hex_str, pad=pad)
+
+ @staticmethod
+ def base_n(number, chars):
+ "Recursive helper for calculating a number in base len(chars)"
+ return ((number == 0) and "0") \
+ or (Uuid.base_n(number // (len(chars)), chars).lstrip("0") \
+ + chars[number % (len(chars))])
+
+ @staticmethod
+ def pad_uuid(uuid, pad=32, padchar='0'):
+ "Pads a UUID with <pad> <padchar>s"
+ return padchar * (pad - len(uuid)) + uuid
+
+ @classmethod
+ def unsplit64(cls, high, low):
+ "Converts a high/low 64-bit integer pair into a 128-bit large integer"
+ return ((high & cls.mask64) << 64) | (low & cls.mask64)
+
+ @classmethod
+ def from_int(cls, number, ttype):
+ "Convert an integer and tag type to a Uuid"
+ if isinstance(number, tuple):
+ number = cls.unsplit64(*number)
+ if not isinstance(number, (int, long)):
+ raise TypeError("Number must be an integer: %r" % number)
+ if number < 0:
+ raise TypeError("Number cannot be negative: %r" % number)
+ tag = cls.tags.get(ttype)
+ if tag is None:
+ raise ValueError("invalid type: %r" % ttype)
+ return cls(number, tag)
+
+ @classmethod
+ def from_slug(cls, slug, base=36, chars=None):
+ "Convert a slug into a Uuid"
+ if not isinstance(slug, types.StringTypes):
+ raise TypeError("slug must be a string: %r" % slug)
+ if chars is None:
+ chars = cls.chars
+ if base < 2 or base > len(chars):
+ raise TypeError("base must be between 2 and %s" % len(chars))
+ if base <= 36:
+ slug = slug.lower()
+ chars = chars[:base]
+ index = dict(zip(chars, range(0, len(chars))))
+ if len(slug) > 1 and '-' in slug:
+ # We have a prefix
+ prefix, slug = slug.split('-')
+ number = reduce(lambda acc, val: acc + val[0] * len(index) ** val[1],
+ zip([index[x] for x in slug],
+ reversed(range(0, len(slug)))), 0)
+ number, int_tag = UuidTag.split_int_tag(number)
+ tag = cls.tags_by_int_tag.get(int_tag)
+ if tag is None:
+ raise ValueError("invalid integer tag: %r" % int_tag)
+ if prefix and tag != cls.tags_by_prefix.get(prefix):
+ raise ValueError("prefix %r did not match tag %r" % (prefix, tag))
+ return cls(number, tag)
+
+ @classmethod
+ def from_hex(cls, hex_str):
+ "Convert a hex uuid into a Uuid"
+ if not isinstance(hex_str, types.StringTypes):
+ raise TypeError("hex_str must be a string: %r" % hex_str)
+ hex_tag = hex_str[-4:]
+ number = int(hex_str[:-4], 16)
+ tag = cls.tags_by_hex_str.get(hex_tag)
+ if tag is None:
+ raise ValueError("invalid hex tag: %r" % hex_tag)
+ return cls(number, tag)
+
+
+def int_to_slug(number, ttype):
+ "Convert a large integer to a slug"
+ return Uuid.from_int(number, ttype).to_slug()
+
+
+def slug_to_int(slug, return_tag=None, split=False):
"""
- Helper method for recursing
+ Convert a slug to an integer, optionally splitting into high and
+ low 64 bit parts
"""
- return ((number == 0) and "0") \
- or (_base_n(number // (len(chars)), chars).lstrip("0") \
- + chars[number % (len(chars))])
-
-
-def pad_uuid(uuid, pad=32, padchar='0'):
- "Pads a UUID with <pad> <padchar>s"
- return padchar * (pad - len(uuid)) + uuid
+ uuid = Uuid.from_slug(slug)
+ number = (uuid.high, uuid.low) if split else uuid.number
+ if return_tag == 'type':
+ return (number, uuid.tag.name)
+ elif return_tag == 'int':
+ return (number, uuid.tag.int_tag)
+ else:
+ return number
-def int_to_slug(number, base=36, chars=_CHARS, prefix=None, pad=0):
- """
- Convert a large integer to a slug
- """
- if not isinstance(number, (int, long)):
- raise TypeError("Number must be an integer: %r" % number)
- if number < 0:
- raise TypeError("Number cannot be negative: %r" % number)
- if base < 2 or base > len(_CHARS):
- raise TypeError("base must be between 2 and %s" % len(_CHARS))
- chars = chars[:base]
- slug = pad_uuid(_base_n(number, chars), pad=pad)
- if prefix is not None:
- if not isinstance(prefix, types.StringTypes):
- raise TypeError("prefix must be a string: %r" % prefix)
- return '%s-%s' % (prefix, slug)
- return slug
-
-
-def slug_to_int(slug, base=36, chars=_CHARS, prefix=None):
- """
- Convert a slug to a large integer
- """
- if not isinstance(slug, types.StringTypes):
- raise TypeError("slug must be a string: %r" % slug)
- if base < 2 or base > len(_CHARS):
- raise TypeError("base must be between 2 and %s" % len(_CHARS))
- if base <= 36:
- slug = slug.lower()
- chars = _CHARS[:base]
- index = dict(zip(chars, range(0, len(chars))))
- if prefix is not None:
- # Prefix is simply an assertion here
- got_prefix, slug = slug.split('-')
- if (prefix or got_prefix) != got_prefix:
- raise TypeError("Got prefix '%s' when expected '%s'" \
- % (got_prefix, prefix))
- return reduce(lambda acc, val: acc + val[0] * len(index) ** val[1],
- zip([index[x] for x in slug],
- reversed(range(0, len(slug)))), 0)
-
-
-def uuid_to_slug(number, base=36, chars=_CHARS, prefix=None, pad=0):
+def uuid_to_slug(number):
"""
Convert a Smarkets UUID (128-bit hex) to a slug
"""
- if not isinstance(number, types.StringTypes):
- raise TypeError("Number must be a string: %r" % number)
- return int_to_slug(int(number, 16), base, chars, prefix, pad)
+ return Uuid.from_hex(number).to_slug()
-def slug_to_uuid(slug, base=36, chars=_CHARS, prefix=None, pad=32):
+def slug_to_uuid(slug):
"""
Convert a slug to a Smarkets UUID
"""
- if not isinstance(slug, types.StringTypes):
- raise TypeError("slug must be a string: %r" % slug)
- return pad_uuid('%x' % slug_to_int(slug, base, chars, prefix), pad=pad)
+ return Uuid.from_slug(slug).to_hex()
-def int_to_uuid(number, type):
+def int_to_uuid(number, ttype):
"Convert an untagged integer into a tagged uuid"
- if not isinstance(number, (long, int)):
- raise TypeError("number must be an integer: %r", number)
-
- tag = _UUID_TAGS.get(type)
- if tag is None:
- raise ValueError("invalid type: %r" % type)
+ return Uuid.from_int(number, ttype).to_hex()
- return pad_uuid('%x%s' % (number, tag))
-
-def uuid_to_int(uuid, return_tag=None):
+def uuid_to_int(uuid, return_tag=None, split=False):
"Convert a tagged uuid into an integer, optionally returning type"
- tagless = uuid[:-4]
- number = int(tagless, 16)
+ uuid = Uuid.from_hex(uuid)
+ number = (uuid.high, uuid.low) if split else uuid.number
if return_tag == 'type':
- tag = uuid[-4:]
- tag_type = _UUID_TAGS_BY_TAG.get(tag)
- if tag_type is None:
- raise ValueError("Invalid tagged uuid: %r" % uuid)
- return (number, tag_type)
- if return_tag == 'int':
- return (number, int(uuid[-4:], 16))
- return number
+ return (number, uuid.tag.name)
+ elif return_tag == 'int':
+ return (number, uuid.tag.int_tag)
+ else:
+ return number
+
def uuid_to_short(uuid):
"Converts a full UUID to the shortened version"
View
2  tests/__init__.py
@@ -14,6 +14,7 @@
from unit_tests import (
CallbackTestCase,
SmarketsTestCase,
+ UuidTestCase,
)
@@ -21,6 +22,7 @@ def unit_tests(suite):
"Add tests to a `unittest.TestSuite` containing only unit tests"
suite.addTest(unittest.makeSuite(CallbackTestCase))
suite.addTest(unittest.makeSuite(SmarketsTestCase))
+ suite.addTest(unittest.makeSuite(UuidTestCase))
def integration_tests(
View
22 tests/session_tests.py
@@ -191,7 +191,7 @@ def test_order_rejected(self):
order_rejected_msg = self._simple_cb(
self.clients[0], 'seto.order_rejected')
order = self._test_order()
- order.quantity = 4000000000 # should be insufficient funds
+ order.quantity = 50000000 # should be insufficient funds
order.validate_new()
self.assertEquals(
self.clients[0].order(order), 2)
@@ -207,6 +207,26 @@ def test_order_rejected(self):
order_rejected_msg.order_rejected.reason,
seto.ORDER_REJECTED_INSUFFICIENT_FUNDS)
+ def test_order_invalid(self):
+ order_invalid_msg = self._simple_cb(
+ self.clients[0], 'seto.order_invalid')
+ order = self._test_order()
+ order.quantity = 500000001 # should be invalid
+ order.validate_new()
+ self.assertEquals(
+ self.clients[0].order(order), 2)
+ self.assertEquals(self.clients[0].session.outseq, 3)
+ self.clients[0].read() # should be invalid
+ self.assertEquals(self.clients[0].session.inseq, 3)
+ self.assertEquals(
+ order_invalid_msg.type,
+ seto.PAYLOAD_ORDER_INVALID)
+ # Order create message was #2
+ self.assertEquals(order_invalid_msg.order_invalid.seq, 2)
+ self.assertEquals(
+ order_invalid_msg.order_invalid.reasons,
+ [seto.ORDER_INVALID_INVALID_QUANTITY])
+
def test_order_cancel(self):
order_accepted_msg = self._simple_cb(
self.clients[0], 'seto.order_accepted')
View
112 tests/unit_tests.py
@@ -1,11 +1,12 @@
import unittest
from contextlib import contextmanager
-from itertools import chain
+from itertools import chain, product
from mock import Mock, patch, sentinel
import smarkets.eto.piqi_pb2 as eto
import smarkets.seto.piqi_pb2 as seto
+import smarkets.uuid as uuid
from smarkets.clients import Callback, Smarkets
from smarkets.exceptions import InvalidCallbackError
@@ -279,3 +280,112 @@ def _login_response():
payload.eto_payload.login_response.session = 'session'
payload.eto_payload.login_response.reset = 2
return payload
+
+
+class UuidTestCase(unittest.TestCase):
+ "Unit tests for Uuids"
+ def test_int_roundtrip(self):
+ "Test converting an integer to a Uuid and back"
+ ttype = 'Account'
+ for i in chain(xrange(1, 1000), product(xrange(1, 10), repeat=2)):
+ u1 = uuid.int_to_uuid(i, ttype)
+ u2, test_ttype = uuid.uuid_to_int(u1, return_tag='type', split=isinstance(i, tuple))
+ self.assertEquals(i, u2)
+ self.assertEquals(test_ttype, ttype)
+ u3 = uuid.int_to_slug(i, ttype)
+ u4, test_ttype = uuid.slug_to_int(u3, return_tag='type', split=isinstance(i, tuple))
+ self.assertEquals(i, u4)
+ self.assertEquals(test_ttype, ttype)
+
+ def test_uuid_roundtrip(self):
+ "Test converting a hex string to a Uuid and back"
+ suffix = 'acc1'
+ for i in xrange(1, 1000):
+ hex_str = '%x%s' % (i, suffix)
+ hex_str = '0' * (32 - len(hex_str)) + hex_str
+ u1, ttype = uuid.uuid_to_int(hex_str, return_tag='type')
+ self.assertEquals(ttype, 'Account')
+ u2 = uuid.int_to_uuid(u1, ttype)
+ self.assertEquals(u2, hex_str)
+ u3 = uuid.uuid_to_slug(hex_str)
+ u4 = uuid.slug_to_uuid(u3)
+ self.assertEquals(u4, hex_str)
+
+ def test_uuid_tag(self):
+ "Test UuidTag class"
+ tag = uuid.Uuid.tags.get('Account')
+ self.assertEquals(tag.name, 'Account')
+ self.assertEquals(tag.hex_str, 'acc1')
+ self.assertEquals(tag.prefix, 'a')
+ self.assertEquals(tag.int_tag, 44225)
+ tagged_8 = tag.tag_number(8)
+ self.assertEquals(tagged_8, int('8acc1', 16))
+ self.assertEquals(tag.split_int_tag(tagged_8), (8, tag.int_tag))
+
+ def test_uuid_high_low(self):
+ "Test Uuid class support for high/low number"
+ tag = uuid.Uuid.tags.get('Account')
+ uuid1 = uuid.Uuid(73786976294838235846L, tag)
+ self.assertEquals(uuid1.low, 29382)
+ self.assertEquals(uuid1.high, 4)
+ uuid2 = uuid.Uuid(5, tag)
+ self.assertEquals(uuid2.low, 5)
+ self.assertEquals(uuid2.high, 0)
+
+ def test_uuid_shorthex(self):
+ "Test Uuid class support for short hex values"
+ tag = uuid.Uuid.tags.get('Account')
+ uuid1 = uuid.Uuid(73786976294838235846L, tag)
+ self.assertEquals(uuid1.shorthex, '400000000000072c6')
+ uuid2 = uuid.Uuid(5, tag)
+ self.assertEquals(uuid2.shorthex, '5')
+
+ def test_bad_base_raises(self):
+ "Test that Uuid class raises TypeError when given a bad 'base' value"
+ tag = uuid.Uuid.tags.get('Account')
+ uuid1 = uuid.Uuid(73786976294838235846L, tag)
+ self.assertRaises(TypeError, lambda: uuid1.to_slug(base=90))
+ self.assertRaises(TypeError, lambda: uuid1.to_slug(base=1))
+ self.assertRaises(TypeError, lambda: uuid1.to_slug(base=-1))
+ self.assertRaises(TypeError, lambda: uuid1.to_slug(chars='abc', base=4))
+
+ def test_slug(self):
+ "Test that Uuid can be converted to/from slugs"
+ tag = uuid.Uuid.tags.get('Account')
+ uuid1 = uuid.Uuid(73786976294838235846L, tag)
+ slug = 'a-lvgb2h48s4kweqbl'
+ self.assertEquals(uuid1.to_slug(), slug)
+ self.assertEquals(uuid1.to_slug(base=16)[2:], uuid1.to_hex().lstrip('0'))
+ uuid2 = uuid.Uuid.from_slug(slug)
+ self.assertEquals(uuid1, uuid2)
+
+ def test_hex(self):
+ "Test that Uuid can be converted to/from hex"
+ tag = uuid.Uuid.tags.get('Account')
+ uuid1 = uuid.Uuid(73786976294838235846L, tag)
+ hex_str = '00000000000400000000000072c6acc1'
+ self.assertEquals(uuid1.to_hex(), hex_str)
+ self.assertEquals(uuid1.shorthex, hex_str.lstrip('0')[:-4])
+ self.assertEquals(uuid1.to_hex(pad=0)[:-4], uuid1.shorthex)
+ uuid2 = uuid.Uuid.from_hex(hex_str)
+ self.assertEquals(uuid1, uuid2)
+ self.assertRaises(TypeError, lambda: uuid.Uuid.from_hex(10))
+ self.assertRaises(ValueError, lambda: uuid.Uuid.from_hex('aa0000'))
+
+ def test_int(self):
+ "Test that Uuid can be converted to/from integer"
+ tag = uuid.Uuid.tags.get('Account')
+ number = 73786976294838235846L
+ low = 29382
+ high = 4
+ uuid1 = uuid.Uuid(number, tag)
+ uuid2 = uuid.Uuid.from_int(number, 'Account')
+ self.assertEqual(uuid1, uuid2)
+ self.assertEqual(uuid2.number, number)
+ self.assertEqual(uuid2.high, high)
+ self.assertEqual(uuid2.low, low)
+ uuid3 = uuid.Uuid.from_int((high, low), 'Account')
+ self.assertEquals(uuid1, uuid3)
+ self.assertRaises(TypeError, lambda: uuid.Uuid.from_int('foo', 'Account'))
+ self.assertRaises(TypeError, lambda: uuid.Uuid.from_int(-100, 'Account'))
+ self.assertRaises(ValueError, lambda: uuid.Uuid.from_int(1, 'invalid-type'))

No commit comments for this range

Something went wrong with that request. Please try again.