Module eink.generate
Expand source code
from .client_code_generator import ClientCodeGenerator
from .client_config import ClientConfig
from .rotation import Rotation
from .server_code_generator import ServerCodeGenerator
from .status_images import StatusImages
from .transport import Transport
from .web_transport import WebTransport
__all__ = [
'ClientCodeGenerator', 'ClientConfig', 'Rotation', 'ServerCodeGenerator',
'StatusImages', 'Transport', 'WebTransport']
Sub-modules
eink.generate.client_code_generator
eink.generate.client_config
eink.generate.rotation
eink.generate.server_code_generator
eink.generate.status_images
eink.generate.transport
eink.generate.web_transport
Classes
class ClientCodeGenerator
-
Generates client-side source code for the Inkplate device.
Expand source code
class ClientCodeGenerator: """Generates client-side source code for the Inkplate device.""" # The cached return value of _str_literal_list() _str_literal_list_cache = None @staticmethod def gen(config, dir_): """Generate client-side source code files for the Inkplate device. Arguments: config (ClientConfig): The configuration for the program. dir_ (str): The directory in which to store the resulting source code files. This directory must already exist. """ ClientCodeGenerator._validate(config, dir_) ClientCodeGenerator._copy_static_files(dir_) ClientCodeGenerator._gen_dynamic_files(config, dir_) @staticmethod def _validate(config, dir_): """Raise if we detect an error in the specified arguments to ``gen``. """ if not os.path.isdir(dir_): raise OSError('No such directory {:s}'.format(dir_)) if not config._transports: raise ValueError('No Transports provided') if not config._wi_fi_networks: raise ValueError('No Wi-Fi networks provided') status_images = config._status_images images = status_images._images if status_images._initial_image_name not in images: raise ValueError( 'StatusImages is missing the initial image {:s}'.format( status_images._initial_image_name)) if status_images._low_battery_image_name not in images: raise ValueError( 'StatusImages is missing the low battery image {:s}'.format( status_images._low_battery_image_name)) for name, image in images.items(): if (image.width != status_images._width or image.height != status_images._height): raise ValueError( 'The size of the image {:s} does not match the size ' 'passed to the StatusImages constructor'.format(name)) if EinkGraphics._has_alpha(image): raise ValueError( 'Alpha channels are not supported. The image {:s} has an ' 'alpha channel.'.format(image)) @staticmethod def _copy_static_files(dir_): """Copy the static files to the specified directory. Copy the source code files for the client that are not generated programatically to the specified directory. """ client_dir = Project.client_code_dir() for subfile in os.listdir(client_dir): if subfile == 'client.ino': output_subfile = '{:s}.ino'.format( os.path.basename(os.path.abspath(dir_))) shutil.copy( os.path.join(client_dir, subfile), os.path.join(dir_, output_subfile)) elif subfile.endswith(('.cpp', '.h', '.ino')): shutil.copy( os.path.join(client_dir, subfile), os.path.join(dir_, subfile)) @staticmethod def _write_bytes_literal(file, bytes_, multiline): """Write C code for a literal byte array to the specified file. Arguments: file (file): The file object. bytes_ (bytes): The contents of the byte array. multiline (bool): Whether to format the results by writing line breaks as appropriate. """ file.write('{') if multiline: file.write('\n ') column = 4 else: column = 1 first = True for byte in bytes_: if not first: if not multiline or column < 73: file.write(', ') column += 2 else: file.write(',\n ') column = 4 first = False file.write('0x{:02x}'.format(byte)) column += 4 if multiline: file.write('\n') file.write('}') @staticmethod def _str_literal_list(): r"""Return a list of literal C string fragments for each character. The return value has 256 elements. The (i + 1)th element of the return value is for ``chr(i)``. For example, the element for the newline character may be ``'\\n'``, while the element for the dash character may be ``'-'``. The only escape sequences we use are non-numeric escape sequences and three-digit octal escape sequences such as ``'\123'``. """ if ClientCodeGenerator._str_literal_list_cache is None: literal_list = [] for i in range(0x20): literal_list.append('\\{:03o}'.format(i)) for i in range(0x20, 0x7f): literal_list.append(chr(i)) for i in range(0x7f, 0x100): literal_list.append('\\{:03o}'.format(i)) escapes = [ ('"', '\\"'), ('\\', '\\\\'), ('\a', '\\a'), ('\a', '\\a'), ('\b', '\\b'), ('\f', '\\f'), ('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t'), ('\v', '\\v'), ] for char, escape in escapes: literal_list[ord(char)] = escape ClientCodeGenerator._str_literal_list_cache = literal_list return ClientCodeGenerator._str_literal_list_cache @staticmethod def _write_str_literal(file, bytes_): """Write C code for a literal string to the specified file. The string is null-terminated. Arguments: file (file): The file object. bytes_ (bytes): The C string. """ literal_list = ClientCodeGenerator._str_literal_list() file.write('"') prev_literal = None for byte in bytes_: literal = literal_list[byte] if literal == '?' and prev_literal == '?': # Avoid the possibility of trigraphs literal = '\\?' file.write(literal) prev_literal = literal file.write('"') @staticmethod def _write_str_array(file, strs): """Write C code for a literal string array to the specified file. The strings are null-terminated. Arguments: file (file): The file object. strs (list<bytes>): The C strings. An element of ``None`` is permitted. We encode such elements as ``NULL``. """ if not strs: file.write('{}') return file.write('{\n') first = True for str_ in strs: if not first: file.write(',\n') first = False file.write(' ') if str_ is not None: ClientCodeGenerator._write_str_literal(file, str_) else: file.write('NULL') file.write('\n}') @staticmethod def _write_int_array(file, values): """Write C code for a literal ``int`` array to the specified file. Arguments: file (file): The file object. values (list<int>): The integers. """ file.write('{') first = True for value in values: if not first: file.write(', ') first = False file.write('{:d}'.format(value)) file.write('}') @staticmethod def _write_generated_message(file): """Write a C comment indicating a file was generated programatically. Arguments: file (file): The file object to write the comment to. """ file.write( '// Auto-generated by the Python class ' 'eink.generate.ClientCodeGenerator\n\n') @staticmethod def _write_secrets_cpp(file, config): """Write the contents of the secrets.cpp file. This contains all of the information that we would like to keep private. We should refrain from committing the file to a repository and from opening the file in a text editor. Arguments: file (file): The file object to write to. config (ClientConfig): The configuration for the program. """ passwords = [] for _, password in config._wi_fi_networks: if password is not None: passwords.append(password.encode()) else: passwords.append(None) ClientCodeGenerator._write_generated_message(file) if None in passwords: file.write('#include <stddef.h>\n\n') file.write('#include "secrets_constants.h"\n\n\n') file.write('const char* WI_FI_PASSWORDS[] = ') ClientCodeGenerator._write_str_array(file, passwords) file.write(';\n') @staticmethod def _write_status_image_data_h(file, status_images): """Write the contents of the status_image_data.h file. This declares the constants that are provided in status_image_data.cpp. Arguments: file (file): The file object to write to. status_images (StatusImages): The status images for the program. """ ClientCodeGenerator._write_generated_message(file) file.write( '#ifndef __STATUS_IMAGE_DATA_H__\n' '#define __STATUS_IMAGE_DATA_H__\n\n' '// The contents of each of the status image files, in the same ' 'order as\n' '// STATUS_IMAGE_DATA\n') for index in range(len(status_images._images)): file.write( 'extern const char STATUS_IMAGE_DATA{:d}[];\n'.format(index)) file.write( '\n// The number of bytes in each of the status image files, in ' 'the same order as\n' '// STATUS_IMAGE_DATA\n') for index in range(len(status_images._images)): file.write( 'extern const int STATUS_IMAGE_DATA_LENGTH{:d};\n'.format( index)) file.write('\n#endif\n') @staticmethod def _render_status_image(image, quality, palette): """Render the specified status image. Arguments: image (image): The image. quality (int): The quality, as in the ``quality`` argument to ``StatusImages.set_image``. palette (Palette): The color palette to use. Returns: bytes: The contents of the image file for the image. """ image = EinkGraphics.round(image, palette) png = ImageData.render_png(image, palette, True) if quality < 100: jpeg = ImageData.render_jpeg(image, quality) if len(jpeg) < len(png): return jpeg return png @staticmethod def _write_status_image_data_cpp(file, status_images, palette): """Write the contents of the status_image_data.cpp file. This contains the contents and sizes of the image files for the status images. We keep these in a separate file in order to improve the readability of generated.cpp. Arguments: file (file): The file object to write to. status_images (StatusImages): The status images for the program. palette (Palette): The color palette to use. """ ClientCodeGenerator._write_generated_message(file) file.write('#include "status_image_data.h"\n\n') images = [] for name, image in status_images._images.items(): quality = status_images._quality[name] images.append((ServerIO.image_id(name), image, quality)) sorted_images = sorted(images, key=lambda image: image[0]) for index, (_, image, quality) in enumerate(sorted_images): image_data = ClientCodeGenerator._render_status_image( image, quality, palette) file.write('\n') file.write( 'const int STATUS_IMAGE_DATA_LENGTH{:d} = {:d};\n'.format( index, len(image_data))) file.write('const char STATUS_IMAGE_DATA{:d}[] = '.format(index)) ClientCodeGenerator._write_bytes_literal(file, image_data, True) file.write(';\n') @staticmethod def _write_generated_h(file, config): """Write the contents of the generated.h file. This ``#includes`` the header files that declare all of the generated constants, and it defines all of the constants that are defined using ``#define``. Arguments: file (file): The file object to write to. config (ClientConfig): The configuration for the program. """ ClientCodeGenerator._write_generated_message(file) file.write( '#ifndef __GENERATED_H__\n' '#define __GENERATED_H__\n\n' '#include "generated_constants.h"\n' '#include "secrets_constants.h"\n\n\n') file.write( '// The maximum number of elements in ClientState.requestTimesDs\n' '#define MAX_REQUEST_TIMES {:d}\n\n'.format( Server._MAX_REQUEST_TIMES)) file.write( '// The number of bytes in an image ID, as in the return value of ' 'the Python\n' '// method ServerIO.image_id\n' '#define STATUS_IMAGE_ID_LENGTH {:d}\n\n'.format( ServerIO.STATUS_IMAGE_ID_LENGTH)) file.write( '// The number of elements in the return value of ' 'requestTransports()\n' '#define TRANSPORT_COUNT {:d}\n\n'.format(len(config._transports))) file.write( '// The color palette to use\n' '#define PALETTE_{:s}\n\n'.format(config._palette._name)) file.write('#endif\n') @staticmethod def _write_status_images(file, status_images): """Write the portion of the generated.cpp file for status images. Arguments: file (file): The file object to write to. status_images (StatusImages): The status images for the program. """ image_id_to_name = {} for name in status_images._images.keys(): image_id_to_name[ServerIO.image_id(name)] = name sorted_image_ids = sorted(list(image_id_to_name.keys())) image_count = len(sorted_image_ids) file.write( 'const int STATUS_IMAGE_COUNT = {:d};\n'.format(image_count)) file.write('const int STATUS_IMAGE_DATA_LENGTHS[] = {\n') for index in range(image_count): if index > 0: file.write(',\n') file.write(' STATUS_IMAGE_DATA_LENGTH{:d}'.format(index)) file.write('\n};\n') file.write('const char* STATUS_IMAGE_DATA[] = {\n') for index in range(image_count): if index > 0: file.write(',\n') file.write(' STATUS_IMAGE_DATA{:d}'.format(index)) file.write('\n};\n\n') for index, image_id in enumerate(sorted_image_ids): file.write('const char STATUS_IMAGE_ID{:d}[] = '.format(index)) ClientCodeGenerator._write_bytes_literal(file, image_id, True) file.write(';\n') file.write('const char* STATUS_IMAGE_IDS[] = {\n') for index in range(image_count): if index > 0: file.write(',\n') file.write(' STATUS_IMAGE_ID{:d}'.format(index)) file.write('\n};\n') # Compute STATUS_IMAGES_BY_TYPE image_name_to_index = {} for index, image_id in enumerate(sorted_image_ids): name = image_id_to_name[image_id] image_name_to_index[name] = index status_images_by_type = [ status_images._initial_image_name, status_images._low_battery_image_name] status_image_indices = [] for name in status_images_by_type: status_image_indices.append(image_name_to_index[name]) file.write('const int STATUS_IMAGES_BY_TYPE[] = ') ClientCodeGenerator._write_int_array(file, status_image_indices) file.write(';\n') @staticmethod def _write_transports(file, transports): """Write the portion of the generated.cpp file for transports. Arguments: file (file): The file object to write to. transports (list<Transport>): The transports for the program, in the order the client should try to connect to them. """ transport_urls = list([ transport._url.encode() for transport in transports]) file.write('const char* TRANSPORT_URLS[] = ') ClientCodeGenerator._write_str_array(file, transport_urls) file.write(';\n') @staticmethod def _write_generated_cpp(file, config): """Write the contents of the generated.cpp file. This provides all of the generated constants that don't have some special reason to be provided elsewhere. Arguments: file (file): The file object to write to. config (ClientConfig): The configuration for the program. """ ClientCodeGenerator._write_generated_message(file) file.write( '#include "generated.h"\n' '#include "status_image_data.h"\n\n\n') file.write( 'const int ROTATION = {:d};\n'.format(config._rotation.value)) file.write('const char HEADER[] = ') ClientCodeGenerator._write_bytes_literal(file, ServerIO.HEADER, True) file.write(';\n') file.write( 'const int HEADER_LENGTH = {:d};\n'.format(len(ServerIO.HEADER))) file.write('const char PROTOCOL_VERSION[] = ') ClientCodeGenerator._write_str_literal(file, ServerIO.PROTOCOL_VERSION) file.write(';\n') file.write( 'const int PROTOCOL_VERSION_LENGTH = {:d};\n\n'.format( len(ServerIO.PROTOCOL_VERSION))) ClientCodeGenerator._write_status_images(file, config._status_images) file.write('\n') ClientCodeGenerator._write_transports(file, config._transports) file.write('\n') file.write( 'const int WI_FI_NETWORK_COUNT = {:d};\n'.format( len(config._wi_fi_networks))) ssids = list([ network[0].encode() for network in config._wi_fi_networks]) file.write('const char* WI_FI_SSIDS[] = ') ClientCodeGenerator._write_str_array(file, ssids) file.write(';\n') networks_with_indices = [] for index, (ssid, _) in enumerate(config._wi_fi_networks): networks_with_indices.append((ssid.encode(), index)) sorted_networks = sorted(networks_with_indices) network_indices = list([network[1] for network in sorted_networks]) file.write('const int WI_FI_NETWORK_INDICES[] = ') ClientCodeGenerator._write_int_array(file, network_indices) file.write(';\n') @staticmethod def _gen_dynamic_files(config, dir_): """Write the contents of all of the programatically generated files. Arguments: config (ClientConfig): The configuration for the program. dir_ (str): The directory in which to store the files. """ # Contains the secret values with open(os.path.join(dir_, 'secrets.cpp'), 'w') as file: ClientCodeGenerator._write_secrets_cpp(file, config) # Declares constants for status_image_data.cpp with open(os.path.join(dir_, 'status_image_data.h'), 'w') as file: ClientCodeGenerator._write_status_image_data_h( file, config._status_images) # Contains the image files for the status images with open(os.path.join(dir_, 'status_image_data.cpp'), 'w') as file: ClientCodeGenerator._write_status_image_data_cpp( file, config._status_images, config._palette) # #includes the header files that declare the generated constants, and # defines all of the constants that use #define with open(os.path.join(dir_, 'generated.h'), 'w') as file: ClientCodeGenerator._write_generated_h(file, config) # Contains the rest of the generated constants with open(os.path.join(dir_, 'generated.cpp'), 'w') as file: ClientCodeGenerator._write_generated_cpp(file, config)
Static methods
def gen(config, dir_)
-
Generate client-side source code files for the Inkplate device.
Arguments
config
:ClientConfig
- The configuration for the program.
dir_
:str
- The directory in which to store the resulting source code files. This directory must already exist.
Expand source code
@staticmethod def gen(config, dir_): """Generate client-side source code files for the Inkplate device. Arguments: config (ClientConfig): The configuration for the program. dir_ (str): The directory in which to store the resulting source code files. This directory must already exist. """ ClientCodeGenerator._validate(config, dir_) ClientCodeGenerator._copy_static_files(dir_) ClientCodeGenerator._gen_dynamic_files(config, dir_)
class ClientConfig (transport, status_images)
-
The configuration information for generating the client's source code.
Initialize a new
ClientConfig
.Arguments
- transport (Transport|list
): The server or servers - the client should try to connect to. Each time the
- client tries to fetch updated content, it tries each of
- the transports in order until it succeeds.
eink.generate.status_images
:StatusImages
- The status images. See the
comments for
StatusImages
.
Expand source code
class ClientConfig: """The configuration information for generating the client's source code. """ # Private attributes: # # Palette _palette - The color palette to use. # Rotation _rotation - The rotation to use when drawing to the Inkplate # device. # StatusImages _status_images - The status images. See the comments for # StatusImages. # list<Transport> _transports - The servers the client should try to # connect to, in order. Each time the client tries to fetch updated # content, it tries each of the transports in order until it succeeds. # list<tuple<str, str>> _wi_fi_networks - The Wi-Fi networks the client may # connect to, in descending order of preference. Each network is # represented as a pair of the SSID and the password, if any. def __init__(self, transport, status_images): """Initialize a new ``ClientConfig``. Arguments: transport (Transport|list<Transport>): The server or servers the client should try to connect to. Each time the client tries to fetch updated content, it tries each of the transports in order until it succeeds. status_images (StatusImages): The status images. See the comments for ``StatusImages``. """ if isinstance(transport, Transport): self._transports = [transport] else: self._transports = transport self._status_images = status_images self._wi_fi_networks = [] self._palette = Palette.THREE_BIT_GRAYSCALE self._rotation = Rotation.LANDSCAPE def add_wi_fi_network(self, ssid, password): """Add the specified Wi-Fi network to the list of networks. Add the specified Wi-Fi network to the list of networks the client may connect to. The SSID must be visible. Networks from earlier calls to ``add_wi_fi_network`` are connected to in preference to networks from later calls. Arguments: ssid (str): The network's SSID. password (str): The network's password, if any. """ self._wi_fi_networks.append((ssid, password)) def set_palette(self, palette): """Set the color palette to use. The default is ``Palette.THREE_BIT_GRAYSCALE``. This must be a palette that the e-ink device supports. Arguments: palette (Palette): The palette. """ self._palette = palette def set_rotation(self, rotation): """Set the rotation to use when drawing to the Inkplate device. The default is ``Rotation.LANDSCAPE``. Arguments: rotation (Rotation): The rotation. """ self._rotation = rotation
Methods
def add_wi_fi_network(self, ssid, password)
-
Add the specified Wi-Fi network to the list of networks.
Add the specified Wi-Fi network to the list of networks the client may connect to. The SSID must be visible. Networks from earlier calls to
add_wi_fi_network
are connected to in preference to networks from later calls.Arguments
ssid
:str
- The network's SSID.
password
:str
- The network's password, if any.
Expand source code
def add_wi_fi_network(self, ssid, password): """Add the specified Wi-Fi network to the list of networks. Add the specified Wi-Fi network to the list of networks the client may connect to. The SSID must be visible. Networks from earlier calls to ``add_wi_fi_network`` are connected to in preference to networks from later calls. Arguments: ssid (str): The network's SSID. password (str): The network's password, if any. """ self._wi_fi_networks.append((ssid, password))
def set_palette(self, palette)
-
Set the color palette to use.
The default is
Palette.THREE_BIT_GRAYSCALE
. This must be a palette that the e-ink device supports.Arguments
palette
:Palette
- The palette.
Expand source code
def set_palette(self, palette): """Set the color palette to use. The default is ``Palette.THREE_BIT_GRAYSCALE``. This must be a palette that the e-ink device supports. Arguments: palette (Palette): The palette. """ self._palette = palette
def set_rotation(self, rotation)
-
Set the rotation to use when drawing to the Inkplate device.
The default is
Rotation.LANDSCAPE
.Arguments
eink.generate.rotation
:Rotation
- The rotation.
Expand source code
def set_rotation(self, rotation): """Set the rotation to use when drawing to the Inkplate device. The default is ``Rotation.LANDSCAPE``. Arguments: rotation (Rotation): The rotation. """ self._rotation = rotation
- transport (Transport|list
class Rotation (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
A rotation to use when drawing to the Inkplate device.
Expand source code
class Rotation(Enum): """A rotation to use when drawing to the Inkplate device.""" # Landscape rotation LANDSCAPE = 4 # Portrait left (bottom of device on left) PORTRAIT_LEFT = 3 # Portrait right (bottom of device on right) PORTRAIT_RIGHT = 1 # Upside down landscape rotation LANDSCAPE_UPSIDE_DOWN = 2
Ancestors
- enum.Enum
Class variables
var LANDSCAPE
var LANDSCAPE_UPSIDE_DOWN
var PORTRAIT_LEFT
var PORTRAIT_RIGHT
class ServerCodeGenerator
-
Generates skeleton code for an e-ink server.
Expand source code
class ServerCodeGenerator: """Generates skeleton code for an e-ink server.""" @staticmethod def gen_skeleton(): """Generate skeleton code for an e-ink server. We request information about the server using standard input and output. """ ServerCodeGenerator._gen_skeleton( *ServerCodeGenerator._input_skeleton_params()) @staticmethod def _input(prompt, default, validation_func, hidden=False): """Prompt the user for a string input. The basic procedure is something like this: * Display the prompt. * If nothing is entered, return the default value if there is one. If not, print an error message and repeat. * Otherwise, validate the value using ``validation_func``. * If valid, return the value entered. * If invalid, print an error message and repeat. However, the ``_input`` method is responsible for the details. Arguments: prompt (str): Text to present to the user indicating what information to enter. default (str): The value to return if the user enters the empty string. If this is ``None``, then there is no default value, and we require the user to enter a non-empty string. validation_func (callable): The function for validating the result, if any. If it raises a ``ValueError`` when we pass in the value the user entered, then that value is not permitted. Such errors should have messages that are suitable to display to the user. We do not check ``validation_func`` if the user enters the empty string. hidden (bool): Whether to hide the user's input as he enters it. This is useful for passwords. Returns: str: The value the user entered, or the default value. """ if default is not None: default_str = default else: default_str = '' if prompt.endswith('\n'): prompt_with_default = '{:s}[{:s}] '.format(prompt, default_str) else: prompt_with_default = '{:s} [{:s}] '.format(prompt, default_str) while True: if hidden: value = getpass.getpass(prompt_with_default) else: value = input(prompt_with_default) if value == '': if default is None: print() print('Please enter a value') print() continue return default elif validation_func is None: return value try: validation_func(value) except ValueError as error: print() print(error) print() else: return value @staticmethod def _input_multiple_choice(prompt, options, default): """Prompt the user to select from a list of options. The basic procedure is similar to that of ``_input``. Arguments: prompt (str): Text to present to the user indicating what information to enter. options (list<tuple<str, object>>): An array of the possible options. Each option is represented as a pair of a human-readable string identifying the option and the value for the option. default (str): The index in ``options`` of the option to select if the user enters the empty string. If this is ``None``, then there is no default option, and we require the user to enter a non-empty string. Returns: object: The option the user selected, as in the second element of the ``options`` pairs. """ prompt_components = [prompt, ':\n'] for index, option in enumerate(options): prompt_components.append(str(index + 1)) prompt_components.append(') ') prompt_components.append(option[0]) prompt_components.append('\n') if default is not None: default_value = str(default + 1) else: default_value = None value = ServerCodeGenerator._input( ''.join(prompt_components), default_value, functools.partial( ServerCodeGenerator._validate_1_to_n, len(options))) return options[int(value) - 1][1] @staticmethod def _normalize_filename(filename): """Equivalent implementation is contractually guaranteed.""" return os.path.abspath(os.path.expanduser(filename)) @staticmethod def _validate_server_dir(dir_): """Validate the server directory. Raise a ``ValueError`` if the specified user entry is not a valid server directory. Arguments: dir_ (str): The user entry. """ try: absolute_dir = ServerCodeGenerator._normalize_filename(dir_) except OSError: raise ValueError( 'Error normalizing the filename {:s}'.format(dir_)) parent = os.path.dirname(absolute_dir) if not os.path.isdir(parent): raise ValueError( 'The parent folder {:s} does not exist'.format(parent)) @staticmethod def _validate_client_dir(server_dir, dir_): """Validate the client directory. Raise a ``ValueError`` if the specified user entry is not a valid client directory. Arguments: server_dir (str): The server directory. dir_ (str): The user entry. """ try: absolute_dir = ServerCodeGenerator._normalize_filename(dir_) except OSError: raise ValueError( 'Error normalizing the filename {:s}'.format(dir_)) try: if (absolute_dir == ServerCodeGenerator._normalize_filename(server_dir)): raise ValueError( 'The client code directory may not be the same as the ' 'server code directory') except OSError: pass parent = os.path.dirname(absolute_dir) try: if parent == ServerCodeGenerator._normalize_filename(server_dir): return except OSError: pass if not os.path.isdir(parent): raise ValueError( 'The parent folder {:s} does not exist'.format(parent)) @staticmethod def _validate_url(url): """Validate the server URL. Raise a ``ValueError`` if the specified user entry is not a valid server URL. Arguments: url (str): The user entry. """ scheme = urlparse(url).scheme if scheme == 'https': raise ValueError( 'The URL must begin with http://. HTTPS is currently not ' 'supported.') elif scheme != 'http': raise ValueError('The URL must begin with http://') @staticmethod def _validate_non_empty(value): """Validate an input that may not be empty. Raise a ``ValueError`` if the specified user entry is the empty string. Arguments: value (str): The user entry. """ if not value: raise ValueError('Please enter a value') @staticmethod def _is_positive_int(value): """Return whether the specified user entry is a positive integer. Arguments: value (str): The user entry. Returns: bool: The result. """ return re.search(r'^[1-9]\d*$', value) is not None @staticmethod def _validate_positive_int(value): """Validate a positive integer. Raise a ``ValueError`` if the specified user entry is not a positive integer. Arguments: value (str): The user entry. """ if not ServerCodeGenerator._is_positive_int(value): raise ValueError('Please enter a positive integer') @staticmethod def _validate_1_to_n(n, value): """Validate an integer from 1 to ``n``. Raise a ``ValueError`` if the specified user entry is not an integer from 1 to ``n``. Arguments: n (int): The maximum value. value (str): The user entry. """ if not ServerCodeGenerator._is_positive_int(value) or int(value) > n: raise ValueError('Please enter 1 - {:d}'.format(n)) @staticmethod def _local_ip_address(): """Return the device's local IP address, e.g. ``'192.168.1.70'``.""" # Based on https://stackoverflow.com/a/28950776/10935386 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # Doesn't even have to be reachable s.connect(('10.255.255.255', 1)) return s.getsockname()[0] except InterruptedError: return '127.0.0.1' finally: s.close() @staticmethod def _ssid(): """Return the SSID for the Wi-Fi network we are connected to, if any. Return ``None`` if we were unable to determine the SSID. """ if os.name == 'nt' or sys.platform == 'darwin': if os.name == 'nt': command = ['Netsh', 'WLAN', 'show', 'interfaces'] else: command = [ '/System/Library/PrivateFrameworks/Apple80211.framework/' 'Resources/airport', '-I'] try: output = subprocess.check_output(command).decode() except ( OSError, subprocess.CalledProcessError, UnicodeDecodeError): return None for line in output.split('\n'): stripped_line = line.strip() if stripped_line.startswith('SSID'): index = stripped_line.index(':') return stripped_line[index + 2:] return None else: try: output = subprocess.check_output([ '/sbin/iwgetid', '-r']).decode() except ( OSError, subprocess.CalledProcessError, UnicodeDecodeError): return None ssid = output.rstrip('\n') if ssid: return ssid else: return None @staticmethod def _input_skeleton_params(): """Prompt the user for all of the parameters to ``_gen_skeleton``. The return value is a tuple with all of those parameters, in order. """ local_ip_address = ServerCodeGenerator._local_ip_address() connected_ssid = ServerCodeGenerator._ssid() if os.name == 'nt': default_server_dir = os.path.join( os.path.expanduser('~'), 'Documents', 'eink_server') else: default_server_dir = os.path.join('~', 'eink_server') server_dir = ServerCodeGenerator._input( 'Directory for the server code', default_server_dir, ServerCodeGenerator._validate_server_dir) client_dir = ServerCodeGenerator._input( 'Directory for the client code', os.path.join(server_dir, 'client'), functools.partial( ServerCodeGenerator._validate_client_dir, server_dir)) url = ServerCodeGenerator._input( 'Server URL', 'http://{:s}:5000/eink_server'.format(local_ip_address), ServerCodeGenerator._validate_url) ssid = ServerCodeGenerator._input( 'Wi-Fi SSID for device to connect to', connected_ssid, ServerCodeGenerator._validate_non_empty) wi_fi_password = ServerCodeGenerator._input( 'Password for Wi-Fi (leave blank if none)', '', None, True) device = ServerCodeGenerator._input_multiple_choice( 'Device', [ ('Inkplate 2', Device(212, 104, 'BLACK_WHITE_AND_RED')), ( 'Inkplate 4 TEMPERA', Device(600, 600, 'THREE_BIT_GRAYSCALE')), ('Inkplate 5', Device(960, 540, 'THREE_BIT_GRAYSCALE')), ('Inkplate 6', Device(800, 600, 'THREE_BIT_GRAYSCALE')), ('Inkplate 6COLOR', Device(600, 448, 'SEVEN_COLOR')), ('Inkplate 6MOTION', Device(1024, 758, 'FOUR_BIT_GRAYSCALE')), ('Inkplate 6PLUS', Device(1024, 758, 'THREE_BIT_GRAYSCALE')), ('Inkplate 10', Device(1200, 825, 'THREE_BIT_GRAYSCALE')), ('Other/enter parameters manually', None)], None) rotation = ServerCodeGenerator._input_multiple_choice( 'Device rotation', [ ( 'Portrait right (bottom of device on right)', Rotation.PORTRAIT_RIGHT), ('Landscape upside down', Rotation.LANDSCAPE_UPSIDE_DOWN), ( 'Portrait left (bottom of device on left)', Rotation.PORTRAIT_LEFT), ('Landscape', Rotation.LANDSCAPE)], 3) if device is not None: if (rotation == Rotation.LANDSCAPE or rotation == Rotation.LANDSCAPE_UPSIDE_DOWN): width = device.width height = device.height else: width = device.height height = device.width palette_name = device.palette_name else: if (rotation == Rotation.LANDSCAPE or rotation == Rotation.LANDSCAPE_UPSIDE_DOWN): width_prompt = 'Device width in pixels' height_prompt = 'Device height in pixels' else: width_prompt = 'Device width in pixels, after rotation' height_prompt = 'Device height in pixels, after rotation' width = int( ServerCodeGenerator._input( width_prompt, None, ServerCodeGenerator._validate_positive_int)) height = int( ServerCodeGenerator._input( height_prompt, None, ServerCodeGenerator._validate_positive_int)) palette_name = ServerCodeGenerator._input_multiple_choice( 'Color palette', [ ('3-bit grayscale', 'THREE_BIT_GRAYSCALE'), ('4-bit grayscale', 'FOUR_BIT_GRAYSCALE'), ('Black, white, and red', 'BLACK_WHITE_AND_RED'), ('7-color', 'SEVEN_COLOR')], 0) return ( server_dir, client_dir, url, ssid, wi_fi_password, rotation, width, height, palette_name) @staticmethod def _eval_template(template_filename, output_filename, params): """Evaluate a source code template. This reads the template at assets/server_skeleton/[template_filename], performs string substitutions using ``string.Template.substitute(params)``, and stores the result in ``output_filename``. Arguments: template_filename (str): The filename of the template file, excluding the directory. output_filename (str): The filename of the resulting file. params (dict<str, str>): The parameters to the template. """ input_filename = os.path.join( Project.server_skeleton_dir(), template_filename) with open(input_filename, 'r') as file: template = file.read() contents = string.Template(template).substitute(params) with open(output_filename, 'w') as file: file.write(contents) @staticmethod def _write_gen_client_code( server_dir, client_dir, url, ssid, wi_fi_password, rotation, palette_name): """Write the contents of the gen_client_code.py file. This contains code for generating the client's source code. Arguments: server_dir (str): The directory in which to store the resulting file. client_dir (str): The directory in which the client code is stored. url (str): The server's URL. ssid (str): The SSID of the Wi-Fi network that the client should connect to. wi_fi_password (str): The password of the Wi-Fi network that the client should connect to. If this is ``''``, then there is no password. rotation (Rotation): The rotation to use when drawing to the e-ink device. palette_name (str): The name of the ``Palette`` constant for the palette to use. The palette is given by ``getattr(Palette, palette_name)``. """ import_os = '' import_separator = '' if not wi_fi_password: wi_fi_password_code = 'None' import_json = '' read_secrets = '' else: wi_fi_password_code = "_read_secrets()['wiFiPassword']" import_json = '\nimport json' import_os = '\nimport os' import_separator = '\n' read_secrets = ( '\ndef _read_secrets():\n' ' """Return the JSON value stored in ' 'assets/secrets.json."""\n' ' project_dir = ' 'os.path.dirname(os.path.abspath(__file__))\n' ' secrets_filename = ' "os.path.join(project_dir, 'assets', 'secrets.json')\n" " with open(secrets_filename, 'r') as file:\n" ' return json.load(file)\n\n') if rotation == Rotation.LANDSCAPE: set_rotation = '' import_rotation = '' else: set_rotation = '\n config.set_rotation(Rotation.{:s})'.format( rotation.name) import_rotation = 'from eink.generate import Rotation\n' if palette_name == 'THREE_BIT_GRAYSCALE': set_palette = '' else: set_palette = '\n config.set_palette(MyServer.PALETTE)' if os.path.dirname(client_dir) != server_dir: set_client_dir = ' dir_ = {:s}'.format(repr(client_dir)) else: set_client_dir = ( ' project_dir = ' 'os.path.dirname(os.path.abspath(__file__))\n' ' dir_ = os.path.join(project_dir, {:s})'.format( repr(os.path.basename(client_dir)))) import_os = '\nimport os' import_separator = '\n' ServerCodeGenerator._eval_template( 'gen_client_code.py.tpl', os.path.join(server_dir, 'gen_client_code.py'), { 'import_json': import_json, 'import_os': import_os, 'import_rotation': import_rotation, 'import_separator': import_separator, 'read_secrets': read_secrets, 'set_client_dir': set_client_dir, 'set_palette': set_palette, 'set_rotation': set_rotation, 'ssid': repr(ssid), 'url': repr(url), 'wi_fi_password': wi_fi_password_code, }) @staticmethod def _gen_client_code( client_dir, url, ssid, wi_fi_password, rotation, width, height, palette_name): """Generate the client code for the skeleton. Arguments: client_dir (str): The directory in which to store the client code. url (str): The server's URL. ssid (str): The SSID of the Wi-Fi network that the client should connect to. wi_fi_password (str): The password of the Wi-Fi network that the client should connect to. If this is ``''``, then there is no password. rotation (Rotation): The rotation to use when drawing to the e-ink device. width (int): The width of the e-ink device in pixels, after applying the rotation suggested by ``rotation``. height (int): The height of the e-ink device in pixels, after applying the rotation suggested by ``rotation``. palette_name (str): The name of the ``Palette`` constant for the palette to use. The palette is given by ``getattr(Palette, palette_name)``. """ status_images = StatusImages.create_default(width, height) config = ClientConfig(WebTransport(url), status_images) if wi_fi_password: config.add_wi_fi_network(ssid, wi_fi_password) else: config.add_wi_fi_network(ssid, None) config.set_rotation(rotation) config.set_palette(getattr(Palette, palette_name)) ClientCodeGenerator.gen(config, client_dir) @staticmethod def _gen_skeleton( server_dir, client_dir, url, ssid, wi_fi_password, rotation, width, height, palette_name): """Generate a skeleton server. Arguments: server_dir (str): The directory in which to store the server. client_dir (str): The directory in which to store the client code. url (str): The server's URL. ssid (str): The SSID of the Wi-Fi network that the client should connect to. wi_fi_password (str): The password of the Wi-Fi network that the client should connect to. If this is ``''``, then there is no password. rotation (Rotation): The rotation to use when drawing to the e-ink device. width (int): The width of the e-ink device in pixels, after applying the rotation suggested by ``rotation``. height (int): The height of the e-ink device in pixels, after applying the rotation suggested by ``rotation``. palette_name (str): The name of the ``Palette`` constant for the palette to use. The palette is given by ``getattr(Palette, palette_name)``. """ server_dir = ServerCodeGenerator._normalize_filename(server_dir) assets_dir = os.path.join(server_dir, 'assets') client_dir = ServerCodeGenerator._normalize_filename(client_dir) if not os.path.exists(server_dir): os.mkdir(server_dir) if not os.path.exists(assets_dir): os.mkdir(assets_dir) if not os.path.exists(client_dir): os.mkdir(client_dir) if width >= 440 and height >= 370: template_filename = 'my_server.py.tpl' else: template_filename = 'my_server_low_res.py.tpl' if palette_name == 'THREE_BIT_GRAYSCALE': import_palette = '' palette_constant = '' palette_method = '' palette_arg = '' else: import_palette = 'from eink.image import Palette\n' palette_constant = ( '\n # The palette to use\n' ' PALETTE = Palette.{:s}\n'.format(palette_name)) palette_method = ( '\n def palette(self):\n' ' return MyServer.PALETTE\n') palette_arg = ', MyServer.PALETTE' if getattr(Palette, palette_name)._is_grayscale: image_line_break = '' image_mode = "'L'" background_color = '255' header_text_color = '0' body_text_color = '0' else: image_line_break = '\n ' image_mode = "'RGB'" background_color = '(255, 255, 255)' body_text_color = '(0, 0, 0)' if palette_name == 'SEVEN_COLOR': header_text_color = '(67, 138, 28)' elif palette_name == 'BLACK_WHITE_AND_RED': header_text_color = '(255, 0, 0)' else: header_text_color = '(0, 0, 0)' ServerCodeGenerator._eval_template( template_filename, os.path.join(server_dir, 'my_server.py'), { 'background_color': background_color, 'body_text_color': body_text_color, 'header_text_color': header_text_color, 'height': str(height), 'image_line_break': image_line_break, 'image_mode': image_mode, 'import_palette': import_palette, 'palette_arg': palette_arg, 'palette_constant': palette_constant, 'palette_method': palette_method, 'width': str(width), }) parsed_url = urlparse(url) if parsed_url.path: path = parsed_url.path else: path = '/' ServerCodeGenerator._eval_template( 'app.py.tpl', os.path.join(server_dir, 'app.py'), {'path': repr(path)}) shutil.copy( os.path.join(Project.server_skeleton_dir(), 'requirements.txt'), os.path.join(server_dir, 'requirements.txt')) shutil.copy( os.path.join(Project.server_skeleton_dir(), 'GentiumPlus-R.ttf'), os.path.join(assets_dir, 'GentiumPlus-R.ttf')) shutil.copy( os.path.join(Project.server_skeleton_dir(), 'OFL.txt'), os.path.join(assets_dir, 'OFL.txt')) if wi_fi_password: secrets = {'wiFiPassword': wi_fi_password} with open(os.path.join(assets_dir, 'secrets.json'), 'w') as file: file.write(json.dumps(secrets, indent=4)) file.write('\n') ServerCodeGenerator._write_gen_client_code( server_dir, client_dir, url, ssid, wi_fi_password, rotation, palette_name) ServerCodeGenerator._gen_client_code( client_dir, url, ssid, wi_fi_password, rotation, width, height, palette_name)
Static methods
def gen_skeleton()
-
Generate skeleton code for an e-ink server.
We request information about the server using standard input and output.
Expand source code
@staticmethod def gen_skeleton(): """Generate skeleton code for an e-ink server. We request information about the server using standard input and output. """ ServerCodeGenerator._gen_skeleton( *ServerCodeGenerator._input_skeleton_params())
class StatusImages (width, height)
-
Describes the program's "status images."
A "status image" is a special full-screen image that is not produced by
Server.render()
. It is hardcoded as part of the software on the client device, so that the client can render it without contacting the server. There are three types of status images: the initial image, the low battery image, and the screensaver. Each status image is identified by a string name.Initialize a new
StatusImages
object.Arguments
width
:int
- The width of the Inkplate display, after
rotation (as in
ClientConfig.set_rotation
). height
:int
- The height of the Inkplate display, after
rotation (as in
ClientConfig.set_rotation
).
Expand source code
class StatusImages: """Describes the program's "status images." A "status image" is a special full-screen image that is not produced by ``Server.render()``. It is hardcoded as part of the software on the client device, so that the client can render it without contacting the server. There are three types of status images: the initial image, the low battery image, and the screensaver. Each status image is identified by a string name. """ # Private attributes: # # int _height - The height of the Inkplate display, after rotation (as in # ClientConfig.set_rotation). # dict<str, Image> _images - A map from the names of the status images to # the images. # str _initial_image_name - The name of the status image to display when # the device is turned on. # str _low_battery_image_name - The name of the status image to display if # the device is low on battery. When this happens, we stop trying to # connect to the server. # dict<str, int> _quality - A map from the names of the status images to # their qualities, as in the "quality" argument to set_image. # int _width - The width of the Inkplate display, after rotation (as in # ClientConfig.set_rotation). def __init__(self, width, height): """Initialize a new ``StatusImages`` object. Arguments: width (int): The width of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). height (int): The height of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). """ self._width = width self._height = height self._images = {} self._quality = {} self._initial_image_name = 'connecting' self._low_battery_image_name = 'low_battery' def set_image(self, name, image, quality=100): """Set (or add) the status image with the specified name. We automatically reduce the image to the appropriate color palette using ``EinkGraphics.round``. Arguments: name (str): The name of the status image. image (Image): The status image. This must be the same size as the display. quality (int): The compression quality to use to store the image. This is a number from 0 to 100, as in the JPEG file format. The higher the number, the more faithfully we will be able to reproduce the image, but the more memory it will require. 100 indicates perfect quality (or lossless). Normally, a quality of 100 is recommended. But if the status images are too numerous and large, it's possible that they won't fit in the client's program memory. In that case, a lower level of quality is required. """ if image.width != self._width or image.height != self._height: raise ValueError( 'A status image must be the same size as the display') self._images[name] = image self._quality[name] = quality def set_initial_image_name(self, name): """Set the name of the initial status image. Set the name of the status image to display when the device is turned on. The default is ``'connecting'``. """ self._initial_image_name = name def set_low_battery_image_name(self, name): """Set the name of the low battery image. Set the name of the status image to display if the device is low on battery. When this happens, we stop trying to connect to the server. The default low battery image name is ``'low_battery'``. """ self._low_battery_image_name = name def default_initial_image(self): """Return the "default" initial status image. Return the "default" status image to display when the display is turned on. This is a standard image that the ``eink-server`` library provides as a default. Returns: Image: The image. """ if self._width >= 320 and self._height >= 220: return self._default_image('connecting.png') else: return self._default_image('connecting_low_res.png') def default_low_battery_image(self): """Return the "default" low battery image. Return the "default" status image to display if the device is low on battery. This is a standard image that the ``eink-server`` library provides as a default. Returns: Image: The image. """ if self._width >= 208 and self._height >= 130: return self._default_image('low_battery.png') else: return self._default_image('low_battery_low_res.png') @staticmethod def create_default(width, height): """Return an instance of ``StatusImages`` with "default" images. This uses standard images that the ``eink-server`` library provides as defaults. It uses the default image names. Arguments: width (int): The width of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). height (int): The height of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). """ images = StatusImages(width, height) images.set_image('connecting', images.default_initial_image()) images.set_image('low_battery', images.default_low_battery_image()) return images def _default_image(self, filename): """Return a default status image from the specified image file. Arguments: filename (str): The filename of the image file, excluding the directory (``Project.images_dir()``). Returns: Image: The image. """ image = Image.open(os.path.join(Project.images_dir(), filename)) output = Image.new('L', (self._width, self._height), 255) output.paste( image, ( (self._width - image.width) // 2, (self._height - image.height) // 2)) return output
Static methods
def create_default(width, height)
-
Return an instance of
StatusImages
with "default" images.This uses standard images that the
eink-server
library provides as defaults. It uses the default image names.Arguments
width
:int
- The width of the Inkplate display, after
rotation (as in
ClientConfig.set_rotation
). height
:int
- The height of the Inkplate display, after
rotation (as in
ClientConfig.set_rotation
).
Expand source code
@staticmethod def create_default(width, height): """Return an instance of ``StatusImages`` with "default" images. This uses standard images that the ``eink-server`` library provides as defaults. It uses the default image names. Arguments: width (int): The width of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). height (int): The height of the Inkplate display, after rotation (as in ``ClientConfig.set_rotation``). """ images = StatusImages(width, height) images.set_image('connecting', images.default_initial_image()) images.set_image('low_battery', images.default_low_battery_image()) return images
Methods
def default_initial_image(self)
-
Return the "default" initial status image.
Return the "default" status image to display when the display is turned on. This is a standard image that the
eink-server
library provides as a default.Returns
Image
- The image.
Expand source code
def default_initial_image(self): """Return the "default" initial status image. Return the "default" status image to display when the display is turned on. This is a standard image that the ``eink-server`` library provides as a default. Returns: Image: The image. """ if self._width >= 320 and self._height >= 220: return self._default_image('connecting.png') else: return self._default_image('connecting_low_res.png')
def default_low_battery_image(self)
-
Return the "default" low battery image.
Return the "default" status image to display if the device is low on battery. This is a standard image that the
eink-server
library provides as a default.Returns
Image
- The image.
Expand source code
def default_low_battery_image(self): """Return the "default" low battery image. Return the "default" status image to display if the device is low on battery. This is a standard image that the ``eink-server`` library provides as a default. Returns: Image: The image. """ if self._width >= 208 and self._height >= 130: return self._default_image('low_battery.png') else: return self._default_image('low_battery_low_res.png')
def set_image(self, name, image, quality=100)
-
Set (or add) the status image with the specified name.
We automatically reduce the image to the appropriate color palette using
EinkGraphics.round
.Arguments
name
:str
- The name of the status image.
image
:Image
- The status image. This must be the same size as the display.
quality
:int
-
The compression quality to use to store the image. This is a number from 0 to 100, as in the JPEG file format. The higher the number, the more faithfully we will be able to reproduce the image, but the more memory it will require. 100 indicates perfect quality (or lossless).
Normally, a quality of 100 is recommended. But if the status images are too numerous and large, it's possible that they won't fit in the client's program memory. In that case, a lower level of quality is required.
Expand source code
def set_image(self, name, image, quality=100): """Set (or add) the status image with the specified name. We automatically reduce the image to the appropriate color palette using ``EinkGraphics.round``. Arguments: name (str): The name of the status image. image (Image): The status image. This must be the same size as the display. quality (int): The compression quality to use to store the image. This is a number from 0 to 100, as in the JPEG file format. The higher the number, the more faithfully we will be able to reproduce the image, but the more memory it will require. 100 indicates perfect quality (or lossless). Normally, a quality of 100 is recommended. But if the status images are too numerous and large, it's possible that they won't fit in the client's program memory. In that case, a lower level of quality is required. """ if image.width != self._width or image.height != self._height: raise ValueError( 'A status image must be the same size as the display') self._images[name] = image self._quality[name] = quality
def set_initial_image_name(self, name)
-
Set the name of the initial status image.
Set the name of the status image to display when the device is turned on. The default is
'connecting'
.Expand source code
def set_initial_image_name(self, name): """Set the name of the initial status image. Set the name of the status image to display when the device is turned on. The default is ``'connecting'``. """ self._initial_image_name = name
def set_low_battery_image_name(self, name)
-
Set the name of the low battery image.
Set the name of the status image to display if the device is low on battery. When this happens, we stop trying to connect to the server. The default low battery image name is
'low_battery'
.Expand source code
def set_low_battery_image_name(self, name): """Set the name of the low battery image. Set the name of the status image to display if the device is low on battery. When this happens, we stop trying to connect to the server. The default low battery image name is ``'low_battery'``. """ self._low_battery_image_name = name
class Transport
-
A transport mechanism for the client to communicate with a server.
This is an abstract base class. For now, the only subclass is
WebTransport
, but we could imagine addingBluetoothTransport
orSerialTransport
in the future.Expand source code
class Transport: """A transport mechanism for the client to communicate with a server. This is an abstract base class. For now, the only subclass is ``WebTransport``, but we could imagine adding ``BluetoothTransport`` or ``SerialTransport`` in the future. """ pass
Subclasses
class WebTransport (url)
-
A
Transport
for connecting to a web server.Each request is submitted as a POST request to a given URL, with the request payload as the POST body. The response is returned in the response payload.
Initialize a new
WebTransport
.Arguments
url
:str
- The server URL.
Expand source code
class WebTransport(Transport): """A ``Transport`` for connecting to a web server. Each request is submitted as a POST request to a given URL, with the request payload as the POST body. The response is returned in the response payload. """ # Private attributes: # # str _url - The server URL. def __init__(self, url): """Initialize a new ``WebTransport``. Arguments: url (str): The server URL. """ self._url = url
Ancestors