Quickstart¶
This document should introduce you to how to get started with Pak, giving you a broad-strokes understanding of how the library functions.
Installation¶
To install Pak, simply install through pip:
$ pip install pak
Defining a Packet¶
A packet is a collection of values that may be marshaled to and from raw data. This concept is modeled in Pak by the Packet class.
Let’s see how to define our own packet:
import pak
class MyPacket(pak.Packet):
field: pak.Int8
First, we import pak to access the library. Then, we create the class MyPacket, which inherits from the Packet class. Within the definition of the MyPacket class, we add a class annotation with the name field and the annotation Int8. This defines that MyPacket has a field named field which is marshaled according to Int8. We’ll get more into what exactly Int8 represents later.
So, let’s see how to use our newly defined packet:
class MyPacket(pak.Packet):
field: pak.Int8
packet = MyPacket(field=1)
data_from_packet = packet.pack()
assert data_from_packet == b"\x01"
packet_from_data = MyPacket.unpack(b"\x01")
assert packet_from_data.field == 1
First, we create packet by calling the constructor and passing field=1, setting that field to the value 1. Next we call the Packet.pack() method to marshal packet to data_from_packet, and make sure that we get the expected data of b"\x01".
Separately from that, we call the Packet.unpack() class method to marshal b"\x01" to packet_from_data, and then make sure the value of packet_from_data.field is 1.
You can add additional fields by adding more annotations:
class MyPacket(pak.Packet):
field: pak.Int8
new_field: pak.Int8
packet = MyPacket.unpack(b"\x01\x02")
assert packet == MyPacket(field=1, new_field=2)
With this we have a second field in MyPacket creatively named new_field. This field will correspond to raw data directly after the raw data of the first field. So then we unpack the raw data b"\x01\x02" and assert that the resulting packet’s fields have the expected values.
It is also possible to mutate Packet objects:
class MyPacket(pak.Packet):
field: pak.Int8
packet = MyPacket(field=1)
packet.field = 2
assert packet.pack() == b"\x02"
Here we create packet with field initially set to 1. We then set packet.field to 2 and pack packet into raw data, getting b"\x02".
If it’s undesirable for you to have a mutable Packet, then you can make it immutable using the Packet.make_immutable() method:
class MyPacket(pak.Packet):
field: pak.Int8
packet = MyPacket(field=1)
packet.make_immutable()
packet.field = 2
Since we made packet immutable, this will raise an error on the last line:
Traceback (most recent call last):
...
AttributeError: This 'MyPacket' instance has been made immutable
Field Types¶
But what exactly are those Int8 annotations doing?
Int8 is referred to as the “type” of a packet field, and, as mentioned previously, they define how each field gets marshaled to and from raw bytes. This concept is modeled in Pak by the Type class. Pak comes with a healthy set of provided Types, which you can browse at the Types reference.
The main difference between a Packet and a Type is that Packets contain values, while Types only define how to marshal values to and from raw data; they don’t hold any value themselves.
Let’s see how to use a Type:
data_from_value = pak.Int8.pack(1)
assert data_from_value == b"\x01"
value_from_data = pak.Int8.unpack(b"\x01")
assert value_from_data == 1
First we call the Type.pack() method to get the raw data which corresponds to the value 1, asserting that we get the raw data b"\x01". Then we call the Type.unpack() method to get the value which corresponds to the raw data b"\x01", asserting we get the value 1.
The Type.unpack() method may also accept a binary file object in addition to plain byte data:
import io
buf = io.BytesIO(
# Two bytes '1' and '2'.
b"\x01" + b"\x02"
)
value_from_buf = pak.Int8.unpack(buf)
assert value_from_buf == 1
value_from_buf = pak.Int8.unpack(buf)
assert value_from_buf == 2
Here we construct an io.BytesIO file object filled with the data corresponding to a 1 and then a 2. Then we unpack from that binary file object a value, asserting that it equals 1, and then we unpack another value and assert it equals 2. This works because unpacking will seek forward past the corresponding raw data within the file object, allowing you to chain calls to Type.unpack(). This will also work with Packet.unpack().
Note
In certain cases it may be necessary, or otherwise desirable, to unpack a Type “asynchronously” using coroutines and the await syntax. For this case, there is the Type.unpack_async() method, which accepts an asyncio.StreamReader in place of where Type.unpack() accepts a file object.
Similarly there is also a Packet.unpack_async() method, and in general where a Pak utility involves unpacking, there will be a corresponding asynchronous version suffixed with _async.
All Types within Pak support both the synchronous and asynchronous APIs, but be aware that custom Types are permitted to only support one or the other.
Types may also have default values:
assert pak.Int8.default() == 0
Here we call the Type.default() method, getting the value 0.
This default value will be used when constructing Packets if no other value is provided for the field:
class MyPacket(pak.Packet):
field: pak.Int8
assert MyPacket() == MyPacket(field=0)
Since we supplied no value for field, the default value of 0 will be used. Keep in mind however that not all Types have default values, though many do.
Types have sizes as well, measuring how many raw bytes values get packed into:
assert pak.Int8.size(2) == 1
Here we call the Type.size() method to get the size of the raw data that corresponds to the value 2, getting a size of 1, since the corresponding raw data of the value 2 would be one byte in length.
In the worst case, determining the size of a Type will perform as badly as packing the value and getting the length of the resulting data, however this may often be optimized by the Type. In fact, in some cases you can get the size of a Type irrespective of any value:
assert pak.Int8.size() == 1
Here we omit passing any value to the Type.size() method in order to get the “static” size of the Type, getting a static size of 1 because an Int8 will always pack into a single byte of raw data.
You can also get the size of Packets, like so:
class MyPacket(pak.Packet):
field: pak.Int8
assert MyPacket().size() == 1
We call the Packet.size() method on an instance of MyPacket, which will use the sizes of its field Types and the values of its fields to calculate the total size, in this case giving us a size of 1.
You may in certain cases be able to get the static size of a Packet irrespective of the values of its fields:
class MyPacket(pak.Packet):
field: pak.Int8
assert MyPacket.size() == 1
Here we call the Packet.size() method on the MyPacket class to get its static size, again giving us a size of 1.
The Next Step¶
Now you should have a decent understanding of the basics of the library. To increase the depth of your understanding, going through Matching a Packet Protocol Using Pak may be helpful.