summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2018-02-15 18:06:50 +0100
committerJustus Winter <justus@sequoia-pgp.org>2019-01-11 13:23:24 +0100
commit177835be9dba392ab10994254b67aa676be66331 (patch)
tree7682b06cd40bbec88dec091c23a18002a9bfabaa
parented7d023d5a6a2587ba218910bc1849d0d34adca7 (diff)
ffi: Add preliminary Python bindings.
- The bingings support basic manipulation of OpenPGP data, but are quite incomplete. Furthermore, the Python API is not very pythonic in some places, so expect it to break in the future.
-rw-r--r--README.md14
-rw-r--r--ffi/Makefile11
-rw-r--r--ffi/lang/python/.gitignore8
-rw-r--r--ffi/lang/python/Makefile81
-rw-r--r--ffi/lang/python/README.md5
-rw-r--r--ffi/lang/python/examples/decrypt.py43
-rw-r--r--ffi/lang/python/sequoia/__init__.py8
-rw-r--r--ffi/lang/python/sequoia/core.py131
-rw-r--r--ffi/lang/python/sequoia/error.py69
-rw-r--r--ffi/lang/python/sequoia/glue.py102
-rw-r--r--ffi/lang/python/sequoia/net.py37
-rw-r--r--ffi/lang/python/sequoia/openpgp.py361
-rw-r--r--ffi/lang/python/sequoia/prelude.py6
-rw-r--r--ffi/lang/python/sequoia/sequoia_build.py51
-rw-r--r--ffi/lang/python/sequoia/store.py236
-rw-r--r--ffi/lang/python/setup.cfg2
-rw-r--r--ffi/lang/python/setup.py41
-rw-r--r--ffi/lang/python/tests/test_armor.py71
-rw-r--r--ffi/lang/python/tests/test_fingerprint.py45
-rw-r--r--ffi/lang/python/tests/test_keyid.py63
-rw-r--r--ffi/lang/python/tests/test_packet_parser.py51
-rw-r--r--ffi/lang/python/tests/test_store.py47
-rw-r--r--ffi/lang/python/tests/test_tpk.py83
-rw-r--r--openpgp/tests/data/keys/testy.asc30
24 files changed, 1591 insertions, 5 deletions
diff --git a/README.md b/README.md
index 38a2d2f5..590d649a 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,20 @@ use `make install`. The latter target honors `PREFIX` and `DESTDIR`.
Finally, to return your source tree to its pristine state, run `make
clean`.
+Bindings
+--------
+
+### Python
+
+The FFI crate contains Python bindings. To disable building, testing,
+and installing the Python bindings, use `make PYTHON=disable`.
+
+To build the Python bindings, you will need cffi and pytest for Python3.
+
+Debian:
+
+ $ apt install python3-cffi python3-pytest
+
Getting help
============
diff --git a/ffi/Makefile b/ffi/Makefile
index 60447429..64f48235 100644
--- a/ffi/Makefile
+++ b/ffi/Makefile
@@ -23,15 +23,12 @@ all: build
.PHONY: build
build:
- sed -e 's|VERSION|$(VERSION)|g' \
- -e 's|PREFIX|$(shell pwd)|g' \
- -e 's|libdir=.*|libdir='"$(CARGO_TARGET_DIR)"'/debug|g' \
- sequoia.pc.in \
- > $(CARGO_TARGET_DIR)/debug/sequoia.pc
+ $(MAKE) -Clang/python build
# Testing and examples.
.PHONY: test check
test check:
+ $(MAKE) -Clang/python test
.PHONY: examples
examples:
@@ -40,6 +37,7 @@ examples:
# Installation.
.PHONY: build-release
build-release:
+ $(MAKE) -Clang/python build-release
.PHONY: install
install:
@@ -62,9 +60,12 @@ install:
$(DESTDIR)$(PREFIX)/lib/libsequoia_ffi.so
$(INSTALL) $(CARGO_TARGET_DIR)/release/libsequoia_ffi.a \
$(DESTDIR)$(PREFIX)/lib/libsequoia_ffi.a
+ # Now the bindings.
+ $(MAKE) -Clang/python install
# Housekeeping.
.PHONY: clean
clean:
rm -f sequoia.pc
$(MAKE) -Cexamples clean
+ $(MAKE) -Clang/python clean
diff --git a/ffi/lang/python/.gitignore b/ffi/lang/python/.gitignore
new file mode 100644
index 00000000..6d916e40
--- /dev/null
+++ b/ffi/lang/python/.gitignore
@@ -0,0 +1,8 @@
+/build
+/dist
+/sequoia.egg-info
+/.eggs
+__pycache__
+*.pyc
+.stamp*
+_sequoia.*.so
diff --git a/ffi/lang/python/Makefile b/ffi/lang/python/Makefile
new file mode 100644
index 00000000..960045ac
--- /dev/null
+++ b/ffi/lang/python/Makefile
@@ -0,0 +1,81 @@
+# Makefile for Sequoia's Python bindings.
+
+# Configuration.
+PREFIX ?= /usr/local
+DESTDIR ?=
+CFLAGS += -I../../include
+
+# Tools.
+PYTHON ?= python3
+IPYTHON ?= ipython3
+PYTEST ?= pytest-3
+INSTALL ?= install
+
+ifneq "$(PYTHON)" "disable"
+PY_VERSION = $(shell $(PYTHON) -c \
+ 'import sys; print("{0.major}.{0.minor}".format(sys.version_info))')
+endif
+
+# Make sure subprocesses pick these up.
+export CFLAGS
+
+all: build
+
+.PHONY: build
+build: .stamp-build
+.stamp-build: sequoia/* ../../include/sequoia/*
+ifneq "$(PYTHON)" "disable"
+ LDFLAGS=-L../../../target/debug $(PYTHON) setup.py build
+ touch $@
+endif
+
+# Testing and examples.
+.PHONY: test check
+test check:
+ifneq "$(PYTHON)" "disable"
+ LDFLAGS=-L../../../target/debug LD_LIBRARY_PATH=../../../target/debug \
+ $(PYTHON) setup.py test
+endif
+
+.PHONY: shell
+shell: build
+ifneq "$(PYTHON)" "disable"
+ cp build/*/_sequoia.abi*.so . # XXX can we get setuptools to do that?
+ LDFLAGS=-L../../../target/debug LD_LIBRARY_PATH=../../../target/debug \
+ $(IPYTHON) -i -c \
+'from sequoia.prelude import *; ctx = Context("org.sequoia-pgp.tests.interactive")'
+endif
+
+# Installation.
+.PHONY: build-release
+build-release: .stamp-build-release
+.stamp-build-release:
+ifneq "$(PYTHON)" "disable"
+ rm -f .stamp-build
+ $(PYTHON) setup.py clean
+ LDFLAGS=-L../../../target/release \
+ $(PYTHON) setup.py build
+ touch $@
+endif
+
+ifneq "$(DESTDIR)" ""
+ root_arg=--root=$(DESTDIR)
+endif
+
+.PHONY: install
+install: build-release
+ifneq "$(PYTHON)" "disable"
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/lib/python$(PY_VERSION)/site-packages
+
+ LDFLAGS=-L../../../target/release \
+ $(PYTHON) setup.py install $(root_arg) --prefix=$(PREFIX)
+endif
+
+# Housekeeping.
+.PHONY: clean
+clean:
+ifneq "$(PYTHON)" "disable"
+ $(PYTHON) setup.py clean
+ rm -f _sequoia.*.so
+ rm -f .stamp-build .stamp-build-release
+endif
diff --git a/ffi/lang/python/README.md b/ffi/lang/python/README.md
new file mode 100644
index 00000000..3be34691
--- /dev/null
+++ b/ffi/lang/python/README.md
@@ -0,0 +1,5 @@
+Python bindings for Sequoia
+===========================
+
+These bindings are work in progress. You are welcome to play with
+them, but we will likely break the API in the future.
diff --git a/ffi/lang/python/examples/decrypt.py b/ffi/lang/python/examples/decrypt.py
new file mode 100644
index 00000000..c65f151d
--- /dev/null
+++ b/ffi/lang/python/examples/decrypt.py
@@ -0,0 +1,43 @@
+import sys
+import os
+from getpass import getpass
+from enum import Enum, auto
+from sequoia.core import Context, NetworkPolicy
+from sequoia.openpgp import Tag, PacketParser
+
+ctx = Context("org.sequoia-pgp.examples",
+ network_policy=NetworkPolicy.Offline,
+ ephemeral=True)
+
+class State(Enum):
+ Start = auto()
+ Decrypted = auto()
+ Deciphered = auto()
+ Done = auto()
+
+state = State.Start
+algo, key = None, None
+pp = PacketParser.open(ctx, sys.argv[1])
+while pp.has_packet:
+ packet = pp.packet
+ tag = packet.tag
+
+ if state == State.Start:
+ if tag == Tag.SKESK:
+ passphrase = getpass("Enter passphrase to decrypt message: ")
+ algo, key = packet.match().decrypt(passphrase.encode())
+ state = State.Decrypted
+ elif tag == Tag.PKESK:
+ sys.stderr.write("Decryption using PKESK not yet supported.\n")
+ elif state == State.Decrypted:
+ if tag == Tag.SEIP:
+ pp.decrypt(algo, key)
+ state = State.Deciphered
+ elif state == State.Deciphered:
+ if tag == Tag.Literal:
+ body = pp.buffer_unread_content()
+ os.write(sys.stdout.fileno(), body)
+ state = State.Done
+
+ pp.recurse()
+assert state == State.Done
diff --git a/ffi/lang/python/sequoia/__init__.py b/ffi/lang/python/sequoia/__init__.py
new file mode 100644
index 00000000..4f84346e
--- /dev/null
+++ b/ffi/lang/python/sequoia/__init__.py
@@ -0,0 +1,8 @@
+from . import (
+ prelude,
+ error,
+ openpgp,
+ core,
+ net,
+ store,
+)
diff --git a/ffi/lang/python/sequoia/core.py b/ffi/lang/python/sequoia/core.py
new file mode 100644
index 00000000..9810c27b
--- /dev/null
+++ b/ffi/lang/python/sequoia/core.py
@@ -0,0 +1,131 @@
+import io
+from enum import Enum
+
+from _sequoia import ffi, lib
+from .error import Error
+from .glue import SQObject
+
+class NetworkPolicy(Enum):
+ Offline = lib.SQ_NETWORK_POLICY_OFFLINE
+ Anonymized = lib.SQ_NETWORK_POLICY_ANONYMIZED
+ Encrypted = lib.SQ_NETWORK_POLICY_ENCRYPTED
+ Insecure = lib.SQ_NETWORK_POLICY_INSECURE
+
+class IPCPolicy(Enum):
+ External = lib.SQ_IPC_POLICY_EXTERNAL
+ Internal = lib.SQ_IPC_POLICY_INTERNAL
+ Robust = lib.SQ_IPC_POLICY_ROBUST
+
+class Context(SQObject):
+ _del = lib.sq_context_free
+ def __init__(self, domain,
+ home=None,
+ network_policy=NetworkPolicy.Encrypted,
+ ipc_policy=IPCPolicy.Robust,
+ ephemeral=False):
+ cfg = lib.sq_context_configure(domain.encode())
+ if home:
+ lib.sq_config_home(cfg, home.encode())
+ lib.sq_config_network_policy(cfg, network_policy.value)
+ lib.sq_config_ipc_policy(cfg, ipc_policy.value)
+ if ephemeral:
+ lib.sq_config_ephemeral(cfg)
+ err = ffi.new("sq_error_t[1]")
+ o = lib.sq_config_build(cfg, err)
+ if o == ffi.NULL:
+ raise Error._from(err[0])
+ super(Context, self).__init__(o)
+
+class AbstractReader(SQObject, io.RawIOBase):
+ _del = lib.sq_reader_free
+
+ def readable(self):
+ return True
+ def writable(self):
+ return False
+
+ def readinto(self, buf):
+ bytes_read = lib.sq_reader_read(
+ self.context().ref(), self.ref(),
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf))
+ if bytes_read < 0:
+ raise Error._last(self.context())
+ return bytes_read
+
+ def close(self):
+ self._delete()
+
+ # Implement the context manager protocol.
+ def __enter__(self):
+ return self
+ def __exit__(self, *args):
+ self.close()
+ return False
+
+class Reader(AbstractReader):
+ @classmethod
+ def open(cls, ctx, filename):
+ return Reader(
+ lib.sq_reader_from_file(
+ ctx.ref(),
+ filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_fd(cls, ctx, fd):
+ return Reader(lib.sq_reader_from_fd(fd),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, buf):
+ return Reader(
+ lib.sq_reader_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf)),
+ context=ctx)
+
+class AbstractWriter(SQObject, io.RawIOBase):
+ _del = lib.sq_writer_free
+
+ def readable(self):
+ return False
+ def writable(self):
+ return True
+
+ def write(self, buf):
+ bytes_written = lib.sq_writer_write(
+ self.context().ref(), self.ref(),
+ ffi.cast("const uint8_t *", ffi.from_buffer(buf)), len(buf))
+ if bytes_written < 0:
+ raise Error._last(self.context())
+ return bytes_written
+
+ def close(self):
+ self._delete()
+
+ # Implement the context manager protocol.
+ def __enter__(self):
+ return self
+ def __exit__(self, *args):
+ self.close()
+ return False
+
+class Writer(AbstractWriter):
+ @classmethod
+ def open(cls, ctx, filename):
+ return Writer(
+ lib.sq_writer_from_file(
+ ctx.ref(),
+ filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_fd(cls, ctx, fd):
+ return Writer(lib.sq_writer_from_fd(fd),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, buf):
+ return Writer(
+ lib.sq_writer_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(buf)), len(buf)),
+ context=ctx)
diff --git a/ffi/lang/python/sequoia/error.py b/ffi/lang/python/sequoia/error.py
new file mode 100644
index 00000000..29eb2545
--- /dev/null
+++ b/ffi/lang/python/sequoia/error.py
@@ -0,0 +1,69 @@
+from _sequoia import ffi, lib
+from .glue import sq_str
+
+class Error(Exception):
+ @classmethod
+ def _from(cls, o):
+ if o == ffi.NULL:
+ return MalformedValue()
+
+ status = lib.sq_error_status(o)
+ return _status_map[status](o)
+
+ @classmethod
+ def _last(cls, ctx):
+ if not ctx:
+ return MalformedValue()
+ return Error._from(lib.sq_context_last_error(ctx.ref()))
+
+class MalformedValue(Error, ValueError):
+ def __init__(self, message="Malformed value"):
+ super(MalformedValue, self).__init__(message)
+
+class SQError(Error):
+ def __init__(self, o):
+ self.__o = ffi.gc(o, lib.sq_error_free)
+ super(SQError, self).__init__(sq_str(lib.sq_error_string(self.__o)))
+
+class Success(SQError):
+ pass
+
+class UnknownError(SQError):
+ pass
+
+class NetworkPolicyViolation(SQError):
+ pass
+
+class IoError(SQError):
+ pass
+
+class InvalidOperataion(SQError):
+ pass
+
+class MalformedPacket(SQError):
+ pass
+
+class UnsupportedHashAlgorithm(SQError):
+ pass
+
+class UnsupportedSymmetricAlgorithm(SQError):
+ pass
+
+class InvalidPassword(SQError):
+ pass
+
+class InvalidSessionKey(SQError):
+ pass
+
+_status_map = {
+ lib.SQ_STATUS_SUCCESS: Success,
+ lib.SQ_STATUS_UNKNOWN_ERROR: UnknownError,
+ lib.SQ_STATUS_NETWORK_POLICY_VIOLATION: NetworkPolicyViolation,
+ lib.SQ_STATUS_IO_ERROR: IoError,
+ lib.SQ_STATUS_INVALID_OPERATION: InvalidOperataion,
+ lib.SQ_STATUS_MALFORMED_PACKET: MalformedPacket,
+ lib.SQ_STATUS_UNSUPPORTED_HASH_ALGORITHM: UnsupportedHashAlgorithm,
+ lib.SQ_STATUS_UNSUPPORTED_SYMMETRIC_ALGORITHM: UnsupportedSymmetricAlgorithm,
+ lib.SQ_STATUS_INVALID_PASSWORD: InvalidPassword,
+ lib.SQ_STATUS_INVALID_SESSION_KEY: InvalidSessionKey,
+}
diff --git a/ffi/lang/python/sequoia/glue.py b/ffi/lang/python/sequoia/glue.py
new file mode 100644
index 00000000..4b74d609
--- /dev/null
+++ b/ffi/lang/python/sequoia/glue.py
@@ -0,0 +1,102 @@
+from datetime import datetime, timezone
+
+from _sequoia import ffi, lib
+from . import error
+
+class SQObject(object):
+ # These class attributes determine what features the wrapper class
+ # implements. They must be set to the relevant Sequoia functions.
+ #
+ # XXX: Once we can assume Python3.6 we can use '__init_subclass__'
+ # and reflection on the 'lib' object to set them automatically
+ # using the type name.
+ _del = None
+ _clone = None
+ _eq = None
+ _str = None
+ _hash = None
+
+ def __init__(self, o, context=None, owner=None, references=None):
+ if o == ffi.NULL:
+ raise error.Error._last(context)
+ self.__o = None
+ self.ref_replace(o, owner=owner, references=references)
+ self.__ctx = context
+ if self.__class__._hash is None and not hasattr(self.__class__, '__hash__'):
+ # Unhashable types must have '__hash__' set to None.
+ # Until we can use '__init_subclass__', we need to patch
+ # the class here. Yuck.
+ self.__class__.__hash__ = None
+
+ def ref(self):
+ return self.__o
+
+ def ref_consume(self):
+ ref = self.ref()
+ self._delete(skip_free=True)
+ return ref
+
+ def ref_replace(self, new, owner=None, references=None):
+ old = self.ref_consume()
+ if self._del and owner == None:
+ # There is a destructor and We own the referenced object
+ # new.
+ self.__o = ffi.gc(new, self._del)
+ else:
+ self.__o = new
+ self.__owner = owner
+ self.__references = references
+ return old
+
+ def _delete(self, skip_free=False):
+ if not self.__o:
+ return
+ if self._del and skip_free:
+ ffi.gc(self.__o, None)
+ self.__o = None
+ self.__owner = None
+ self.__references = None
+
+ def context(self):
+ return self.__ctx
+
+ def __str__(self):
+ if self._str:
+ return _str(self._str(self.ref()))
+ else:
+ return repr(self)
+
+ def __eq__(self, other):
+ if self._eq:
+ return (isinstance(other, self.__class__)
+ and bool(self._eq(self.ref(), other.ref())))
+ else:
+ return NotImplemented
+
+ def copy(self):
+ if self._clone:
+ return self.__class__(self._clone(self.ref()))
+ else:
+ raise NotImplementedError()
+
+ def __hash__(self):
+ return self._hash(self.ref())
+
+def sq_str(s):
+ t = ffi.string(s).decode()
+ lib.sq_string_free(s)
+ return t
+_str = sq_str
+
+def sq_iterator(iterator, next_fn, map=lambda x: x):
+ while True:
+ entry = next_fn(iterator)
+ if entry == ffi.NULL:
+ break
+ yield map(entry)
+
+def sq_time(t):
+ if t == 0:
+ return None
+ else:
+ return datetime.fromtimestamp(t, timezone.utc)
diff --git a/ffi/lang/python/sequoia/net.py b/ffi/lang/python/sequoia/net.py
new file mode 100644
index 00000000..c5f4a7f4
--- /dev/null
+++ b/ffi/lang/python/sequoia/net.py
@@ -0,0 +1,37 @@
+from _sequoia import ffi, lib
+
+from .openpgp import TPK
+from .error import Error
+from .glue import SQObject
+
+class KeyServer(SQObject):
+ _del = lib.sq_keyserver_free
+
+ @classmethod
+ def new(cls, ctx, uri, cert=None):
+ if not cert:
+ ks = lib.sq_keyserver_new(ctx.ref(), uri.encode())
+ else:
+ ks = lib.sq_keyserver_with_cert(
+ ctx.ref(), uri.encode(),
+ ffi.cast("uint8_t *", ffi.from_buffer(cert)),
+ len(cert))
+ return KeyServer(ks, context=ctx)
+
+ @classmethod
+ def sks_pool(cls, ctx):
+ return KeyServer(lib.sq_keyserver_sks_pool(ctx.ref()),
+ context=ctx)
+
+ def get(self, keyid):
+ return TPK(lib.sq_keyserver_get(self.context().ref(),
+ self.ref(),
+ keyid.ref()),
+ context=self.context())
+
+ def send(self, tpk):
+ r = lib.sq_keyserver_send(self.context().ref(),
+ self.ref(),
+ tpk.ref())
+ if r:
+ raise Error._last(self.context())
diff --git a/ffi/lang/python/sequoia/openpgp.py b/ffi/lang/python/sequoia/openpgp.py
new file mode 100644
index 00000000..aaea5262
--- /dev/null
+++ b/ffi/lang/python/sequoia/openpgp.py
@@ -0,0 +1,361 @@
+from enum import Enum
+
+from _sequoia import ffi, lib
+from .error import Error
+from .glue import _str, SQObject
+from .core import AbstractReader, AbstractWriter
+
+class KeyID(SQObject):
+ _del = lib.sq_keyid_free
+ _clone = lib.sq_keyid_clone
+ _str = lib.sq_keyid_to_string
+ _eq = lib.sq_keyid_equal
+ _hash = lib.sq_keyid_hash
+
+ @classmethod
+ def from_bytes(cls, fp):
+ if len(fp) != 8:
+ raise Error("KeyID must be of length 8")
+ return KeyID(lib.sq_keyid_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(fp))))
+
+ @classmethod
+ def from_hex(cls, fp):
+ return KeyID(lib.sq_keyid_from_hex(fp.encode()))
+
+ def hex(self):
+ return _str(lib.sq_keyid_to_hex(self.ref()))
+
+class Fingerprint(SQObject):
+ _del = lib.sq_fingerprint_free
+ _clone = lib.sq_fingerprint_clone
+ _str = lib.sq_fingerprint_to_string
+ _eq = lib.sq_fingerprint_equal
+ _hash = lib.sq_fingerprint_hash
+
+ @classmethod
+ def from_bytes(cls, fp):
+ return Fingerprint(lib.sq_fingerprint_from_bytes(
+ ffi.cast("uint8_t *", ffi.from_buffer(fp)), len(fp)))
+
+ @classmethod
+ def from_hex(cls, fp):
+ return Fingerprint(lib.sq_fingerprint_from_hex(fp.encode()))
+
+ def hex(self):
+ return _str(lib.sq_fingerprint_to_hex(self.ref()))
+
+ def keyid(self):
+ return KeyID(lib.sq_fingerprint_to_keyid(self.ref()))
+
+class PacketPile(SQObject):
+ _del = lib.sq_packet_pile_free
+ _clone = lib.sq_packet_pile_clone
+
+ @classmethod
+ def from_reader(cls, ctx, reader):
+ return PacketPile(lib.sq_packet_pile_from_reader(ctx.ref(), reader.ref()),
+ context=ctx)
+
+ @classmethod
+ def open(cls, ctx, filename):
+ return PacketPile(lib.sq_packet_pile_from_file(ctx.ref(), filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, source):
+ return PacketPile(lib.sq_packet_pile_from_bytes(ctx.ref(),
+ ffi.from_buffer(source),
+ len(source)),
+ context=ctx)
+
+ def serialize(self, writer):
+ status = lib.sq_packet_pile_serialize(self.context().ref(),
+ self.ref(),
+ writer.ref())
+ if status:
+ raise Error._last(self.context())
+
+class TPK(SQObject):
+ _del = lib.sq_tpk_free
+ _clone = lib.sq_tpk_clone
+ _eq = lib.sq_tpk_equal
+
+ @classmethod
+ def from_reader(cls, ctx, reader):
+ return TPK(lib.sq_tpk_from_reader(ctx.ref(), reader.ref()),
+ context=ctx)
+
+ @classmethod
+ def open(cls, ctx, filename):
+ return TPK(lib.sq_tpk_from_file(ctx.ref(), filename.encode()),
+ context=ctx)
+
+ @classmethod
+ def from_packet_pile(cls, ctx, packet_pile):
+ return TPK(lib.sq_tpk_from_packet_pile(ctx.ref(), packet_pile.ref_consume()),
+ context=ctx)
+
+ @classmethod
+ def from_bytes(cls, ctx, source):
+ return TPK(lib.sq_tpk_from_bytes(ctx.ref(),
+ ffi.from_buffer(source),
+ len(source)),
+ context=ctx