diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b3df4252ab77e13e8f56bf67bdfdd575083cf6b2..a26015a8a5fbe6919de6d351dc43c7b41d161e86 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,7 +42,7 @@ stages:
   - docker
   - docker-latest
   - test-gstlal
-  - test-gstlal-only-ugly
+  - test-gstlal-full-build
   - test-gstlal-ugly
   - test-burst
   - test-calibration
@@ -332,19 +332,22 @@ test:gstlal:el7:
 
     # Run doctests
     - cd gstlal
-    - python3 -m pytest -c pytest.ini -m "not requires_gstlal_ugly"
+    - python3 -m pytest -c pytest.ini -m "not requires_full_build"
   only:
     - schedules
     - pushes
   allow_failure: true
 
-test:gstlal-only-ugly:el7:
+test:gstlal-full-build:el7:
   interruptible: true
   image: containers.ligo.org/alexander.pace/gstlal-dev/gstlal-dev:el7
-  stage: test-gstlal-only-ugly
+  stage: test-gstlal-full-build
   needs:
     - level0:rpm:gstlal
     - level1:rpm:gstlal-ugly
+    - level2:rpm:gstlal-burst
+    - level2:rpm:gstlal-calibration
+    - level2:rpm:gstlal-inspiral
   script:
     # Install RPMs and set up the test environment:
     - if [ -d rpmbuild ]; then yum -y install rpmbuild/RPMS/x86_64/*.rpm; fi
@@ -353,7 +356,7 @@ test:gstlal-only-ugly:el7:
 
     # Run doctests
     - cd gstlal
-    - python3 -m pytest -c pytest.ini -m "requires_gstlal_ugly"
+    - python3 -m pytest -c pytest.ini -m "requires_full_build"
   only:
     - schedules
     - pushes
@@ -505,26 +508,34 @@ test:gstlal:conda:
   before_script: [ ]
   script:
     - cd gstlal
-    - python3 -m pytest -c pytest.ini -m "not requires_gstlal_ugly"
+    - python3 -m pytest -c pytest.ini -m "not requires_full_build" --junitxml=report.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report.xml
 
-test:gstlal-only-ugly:conda:
+test:gstlal-full-build:conda:
   interruptible: true
   image: $CI_REGISTRY_IMAGE/conda-dev:$CI_COMMIT_REF_NAME
-  stage: test-gstlal-only-ugly
+  stage: test-gstlal-full-build
   needs:
     - docker:conda:dev
   before_script: [ ]
   script:
     - cd gstlal
-    - python3 -m pytest -c pytest.ini -m "requires_gstlal_ugly"
+    - python3 -m pytest -c pytest.ini -m "requires_full_build" --junitxml=report-full-build.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report-full-build.xml
 
 test:gstlal-inspiral:conda:
   interruptible: true
@@ -535,11 +546,15 @@ test:gstlal-inspiral:conda:
   before_script: [ ]
   script:
     - cd gstlal-inspiral
-    - python3 -m pytest -c pytest.ini
+    - python3 -m pytest -c pytest.ini --junitxml=report-inspiral.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report-inspiral.xml
 
 test:gstlal-ugly:conda:
   interruptible: true
@@ -550,11 +565,15 @@ test:gstlal-ugly:conda:
   before_script: [ ]
   script:
     - cd gstlal-ugly
-    - python3 -m pytest -c pytest.ini
+    - python3 -m pytest -c pytest.ini --junitxml=report-ugly.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report-ugly.xml
 
 test:gstlal-burst:conda:
   interruptible: true
@@ -565,11 +584,15 @@ test:gstlal-burst:conda:
   before_script: [ ]
   script:
     - cd gstlal-burst
-    - python3 -m pytest -c pytest.ini
+    - python3 -m pytest -c pytest.ini --junitxml=report-burst.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report-burst.xml
 
 test:gstlal-calibration:conda:
   interruptible: true
@@ -580,11 +603,15 @@ test:gstlal-calibration:conda:
   before_script: [ ]
   script:
     - cd gstlal-calibration
-    - python3 -m pytest -c pytest.ini
+    - python3 -m pytest -c pytest.ini --junitxml=report-calibration.xml
   only:
     - schedules
     - pushes
   allow_failure: true
+  artifacts:
+    when: always
+    reports:
+      junit: gstlal/report-calibration.xml
 
 test:offline:conda:
   interruptible: true
diff --git a/gstlal/pytest.ini b/gstlal/pytest.ini
index 8e112eb9830fbce5ab2b275cf7ebe18122c99ecb..116585b1970341cd2f4f9045b48b87d7039a6d17 100644
--- a/gstlal/pytest.ini
+++ b/gstlal/pytest.ini
@@ -1,6 +1,6 @@
 # Configuration file for pytest within gstlal
 [pytest]
-norecursedirs = gst/python port-tools tests share tests_pytest/utils
+norecursedirs = gst/python port-tools tests share tests_pytest/utils python/pipeparts
 testpaths = tests/tests_pytest python
 addopts =
     -v
diff --git a/gstlal/python/Makefile.am b/gstlal/python/Makefile.am
index 5d26f37556fbec64bd6954686ab3031ca811178c..bff3ef85719c1364750092b0d496e3fccf8f7496 100644
--- a/gstlal/python/Makefile.am
+++ b/gstlal/python/Makefile.am
@@ -17,6 +17,7 @@ pkgpython_PYTHON = \
 	datafind.py \
 	datasource.py \
 	elements.py \
+	gsttools.py \
 	httpinterface.py \
 	kernels.py \
 	matplotlibhelper.py \
diff --git a/gstlal/python/gsttools.py b/gstlal/python/gsttools.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d936c78d826b82ad0e05888af05f63e26104b8d
--- /dev/null
+++ b/gstlal/python/gsttools.py
@@ -0,0 +1,89 @@
+"""Miscellaneous utilities for working with Gstreamer
+
+References:
+	[1] 1.0 API: https://lazka.github.io/pgi-docs/Gst-1.0/index.html
+"""
+import functools
+from typing import Any, Union
+
+import gi
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+
+
+def is_element(x: Any) -> bool:
+	"""Test whether an object is a Gst Element. TODO: do these belong somewhere more general?
+
+	Args:
+		x:
+			Any, the object to test
+
+	Returns:
+		bool, True if x is a Gst Element, false otherwise
+	"""
+	return isinstance(x, Gst.Element)
+
+
+def is_pad(x: Any) -> bool:
+	"""Test whether an object is a Gst Pad. TODO: do these belong somewhere more general?
+
+	Args:
+		x:
+			Any, the object to test
+
+	Returns:
+		bool, True if x is a Gst Pad, false otherwise
+	"""
+	return isinstance(x, Gst.Pad)
+
+
+def to_caps(x: Union[str, Gst.Caps]) -> Gst.Caps:
+	"""Create a Caps object from a string, or pass thru if already is Caps
+
+	Args:
+		x:
+			str or Caps, if a string construct Caps instance from x, else pass thru as Caps
+
+	Returns:
+		Caps
+	"""
+	if isinstance(x, str):
+		return Gst.Caps.from_string(x)
+	if not isinstance(x, Gst.Caps):
+		raise ValueError('Cannot coerce type {} to Caps: {}'.format(type(x), str(x)))
+	return x
+
+
+def make_element(type_name: str, name: str = None, **properties: dict) -> Gst.Element:
+	"""Create a new element of the type defined by the given element factory. If name is None, then the element will
+	receive a guaranteed unique name, consisting of the element factory name and a number. If name is given, it will
+	be given the name supplied.
+
+	Args:
+		type_name:
+			str, the name of the type of element
+		elem_name:
+			str, default None, the name of the element instance
+		properties:
+			dict, keyword arguments to set as properties of the element
+
+	References:
+		[1] ElementFactor.make: https://lazka.github.io/pgi-docs/Gst-1.0/classes/ElementFactory.html#Gst.ElementFactory.make
+
+	Returns:
+		Gst.Element
+	"""
+	elem = Gst.ElementFactory.make(type_name, name)
+	if elem is None:
+		raise RuntimeError("Unknown failure creating {} element: confirm that the correct plugins are being loaded".format(type_name))
+
+	# Set element properties
+	for k, v in properties.items():
+		elem.set_property(k, v)
+
+	return elem
diff --git a/gstlal/python/pipeparts/Makefile.am b/gstlal/python/pipeparts/Makefile.am
index 5f447c76cb8abaa518fd0602b4e01f56827932ee..4c4495ff5119ba1b3bb6f2f352cc7aed4d46d0de 100644
--- a/gstlal/python/pipeparts/Makefile.am
+++ b/gstlal/python/pipeparts/Makefile.am
@@ -2,4 +2,14 @@ pkgpythondir = $(pkgpyexecdir)
 pipepartsdir = $(pkgpythondir)/pipeparts
 
 pipeparts_PYTHON = \
-	__init__.py
+	__init__.py \
+	encode.py \
+	filters.py \
+	mux.py \
+	pipedot.py \
+	pipetools.py \
+	plot.py \
+	sink.py \
+	source.py \
+	transform.py \
+	trigger.py
diff --git a/gstlal/python/pipeparts/__init__.py b/gstlal/python/pipeparts/__init__.py
index 9bea15ca1b8d2ae2ef167b484497bfcd2cc12d09..fa59f519e6d0a0cf61b5fe1eac02fcd94c2aa248 100644
--- a/gstlal/python/pipeparts/__init__.py
+++ b/gstlal/python/pipeparts/__init__.py
@@ -14,1108 +14,113 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
-
-#
-# =============================================================================
-#
-#                                   Preamble
-#
-# =============================================================================
-#
-
-
-import math
-import os
-import sys
-import threading
-
-import numpy
-
-import gi
-gi.require_version('Gst', '1.0')
-from gi.repository import GObject
-from gi.repository import Gst
-GObject.threads_init()
-Gst.init(None)
-
-
-from ligo import segments
-from gstlal import pipeio
-from lal import iterutils
-from lal import LIGOTimeGPS
-from lal.utils import CacheEntry
-
-
-if sys.byteorder == "little":
-	BYTE_ORDER = "LE"
-else:
-	BYTE_ORDER = "BE"
-
-
 __author__ = "Kipp Cannon <kipp.cannon@ligo.org>, Chad Hanna <chad.hanna@ligo.org>, Drew Keppel <drew.keppel@ligo.org>"
 __version__ = "FIXME"
 __date__ = "FIXME"
 
 
-##
-# @file
-#
-# A file that contains the pipeparts module code
-#
-
-##
-# @package python.pipeparts
-#
-# pipeparts module
-
-
-#
-# =============================================================================
-#
-#                             Generic Constructors
-#
-# =============================================================================
-#
-
-
-#
-# Applications should use the element-specific wrappings that follow below.
-# The generic constructors are only intended to simplify the writing of
-# those wrappings, they are not meant to be how applications create
-# elements in pipelines.
-#
-
-
-def mkgeneric(pipeline, src, elem_type_name, **properties):
-	if "name" in properties:
-		elem = Gst.ElementFactory.make(elem_type_name, properties.pop("name"))
-	else:
-		elem = Gst.ElementFactory.make(elem_type_name, None)
-	if elem is None:
-		raise RuntimeError("unknown failure creating \"%s\" element: confirm that the correct plugins are being loaded" % elem_type_name)
-	# handle properties with names that collide with reserved keywords
-	if "async_" in properties:
-		properties["async"] = properties.pop("async_")
-	for name, value in properties.items():
-		elem.set_property(name.replace("_", "-"), value)
-	pipeline.add(elem)
-	if isinstance(src, Gst.Pad):
-		src.get_parent_element().link_pads(src, elem, None)
-	elif src is not None:
-		src.link(elem)
-	return elem
-
-
-#
-# deferred link helper
-#
-
-
-class src_deferred_link(object):
-	"""!
-	A class that manages the task of watching for and connecting to new
-	source pads by name.  The inputs are an element, the name of the
-	source pad to watch for on that element, and the sink pad (on a
-	different element) to which the source pad should be linked when it
-	appears.
-
-	The "pad-added" signal of the element will be used to watch for new
-	pads, and if the "no-more-pads" signal is emitted by the element
-	before the requested pad has appeared ValueError is raised.
-	"""
-	def __init__(self, element, srcpadname, sinkpad):
-		no_more_pads_handler_id = element.connect("no-more-pads", self.no_more_pads, srcpadname)
-		assert no_more_pads_handler_id > 0
-		pad_added_data = [srcpadname, sinkpad, no_more_pads_handler_id]
-		pad_added_handler_id = element.connect("pad-added", self.pad_added, pad_added_data)
-		assert pad_added_handler_id > 0
-		pad_added_data.append(pad_added_handler_id)
-
-	@staticmethod
-	def pad_added(element, pad, src_sink_ids):
-		srcpadname, sinkpad, no_more_pads_handler_id, pad_added_handler_id = src_sink_ids
-		if pad.get_name() == srcpadname:
-			element.handler_disconnect(no_more_pads_handler_id)
-			element.handler_disconnect(pad_added_handler_id)
-			pad.link(sinkpad)
-
-	@staticmethod
-	def no_more_pads(element, srcpadname):
-		raise ValueError("<%s>: no pad named '%s'" % (element.get_name(), srcpadname))
-
-
-#
-# framecpp channeldemux helpers
-#
-
-
-class framecpp_channeldemux_set_units(object):
-	def __init__(self, elem, units_dict):
-		"""
-		Connect a handler for the pad-added signal of the
-		framecpp_channeldemux element elem, and when a pad is added
-		to the element if the pad's name appears as a key in the
-		units_dict dictionary that pad's units property will be set
-		to the string value associated with that key in the
-		dictionary.
-
-		Example:
-
-		>>> framecpp_channeldemux_set_units(elem, {"H1:LSC-STRAIN": "strain"})
-
-		NOTE:  this is a work-around to address the problem that
-		most (all?) frame files do not have units set on their
-		channel data, whereas downstream consumers of the data
-		might require information about the units.  The demuxer
-		provides the units as part of a tag event, and
-		framecpp_channeldemux_set_units() can be used to override
-		the values, thereby correcting absent or incorrect units
-		information.
-		"""
-		self.elem = elem
-		self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, units_dict)
-		assert self.pad_added_handler_id > 0
-
-	@staticmethod
-	def pad_added(element, pad, units_dict):
-		name = pad.get_name()
-		if name in units_dict:
-			pad.set_property("units", units_dict[name])
-
-
-class framecpp_channeldemux_check_segments(object):
-	"""
-	Utility to watch for missing data.  Pad probes are used to collect
-	the times spanned by buffers, these are compared to a segment list
-	defining the intervals of data the stream is required to have.  If
-	any intervals of data are found to have been skipped or if EOS is
-	seen before the end of the segment list then a ValueError exception
-	is raised.
-
-	There are two ways to use this tool.  To directly install a segment
-	list monitor on a single pad use the .set_probe() class method.
-	For elements with dynamic pads, the class can be allowed to
-	automatically add monitors to pads as they become available by
-	using the element's pad-added signal.  In this case initialize an
-	instance of the class with the element and a dictionary of segment
-	lists mapping source pad name to the segment list to check that
-	pad's output against.
-
-	In both cases a jitter parameter sets the maximum size of a skipped
-	segment that will be ignored (for example, to accomodate round-off
-	error in element timestamp computations).  The default is 1 ns.
-	"""
-	# FIXME:  this code now has two conflicting mechanisms for removing
-	# probes from pads:  one code path removes probes when pads get to
-	# EOS, while the othe removes a probe each time the pad for the
-	# probe appears a second or subsequent time on an element (and then
-	# re-installs the probe on the new pad).  it's possible that these
-	# two could attempt to remove the same probe twice, which will
-	# cause a crash, although it should not happen in current use
-	# cases.  the fix is to rework the probe tracking mechanism so that
-	# both code paths agree on what probes are installed
-	def __init__(self, elem, seglists, jitter = LIGOTimeGPS(0, 1)):
-		self.jitter = jitter
-		self.probe_handler_ids = {}
-		# make a copy of the segmentlistdict in case the calling
-		# code modifies it
-		self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, seglists.copy())
-		assert self.pad_added_handler_id > 0
-
-	def pad_added(self, element, pad, seglists):
-		name = pad.get_name()
-		if name in self.probe_handler_ids:
-			pad.remove_probe(self.probe_handler_ids.pop(name))
-		if name in seglists:
-			self.probe_handler_ids[name] = self.set_probe(pad, seglists[name], self.jitter)
-			assert self.probe_handler_ids[name] > 0
-
-	@classmethod
-	def set_probe(cls, pad, seglist, jitter = LIGOTimeGPS(0, 1)):
-		# use a copy of the segmentlist so the probe can modify it
-		seglist = segments.segmentlist(seglist)
-		# mutable object to carry data to probe
-		data = [seglist, jitter, None]
-		# install probe, save ID in data
-		probe_id = data[2] = pad.add_probe(Gst.PadProbeType.DATA_DOWNSTREAM, cls.probe, data)
-		return probe_id
-
-	@staticmethod
-	def probe(pad, probeinfo, seg_jitter_id):
-		seglist, jitter, probe_id = seg_jitter_id
-		if probeinfo.type & Gst.PadProbeType.BUFFER:
-			obj = probeinfo.get_buffer()
-			if not obj.mini_object.flags & Gst.BufferFlags.GAP:
-				# remove the current buffer from the data
-				# we're expecting to see
-				seglist -= segments.segmentlist([segments.segment((LIGOTimeGPS(0, obj.pts), LIGOTimeGPS(0, obj.pts + obj.duration)))])
-				# ignore missing data intervals unless
-				# they're bigger than the jitter
-				iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist)
-			# are we still expecting to see something that
-			# precedes the current buffer?
-			preceding = segments.segment((segments.NegInfinity, LIGOTimeGPS(0, obj.pts)))
-			if seglist.intersects_segment(preceding):
-				raise ValueError("%s: detected missing data:  %s" % (pad.get_name(), seglist & segments.segmentlist([preceding])))
-		elif probeinfo.type & Gst.PadProbeType.EVENT_DOWNSTREAM and probeinfo.get_event().type == Gst.EventType.EOS:
-			# detach probe at EOS
-			pad.remove_probe(probe_id)
-			# ignore missing data intervals unless they're
-			# bigger than the jitter
-			iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist)
-			if seglist:
-				raise ValueError("%s: at EOS detected missing data: %s" % (pad.get_name(), seglist))
-		return True
-
-
-#
-# framecpp file sink helpers
-#
-
-
-def framecpp_filesink_ldas_path_handler(elem, pspec, path_digits):
-	"""
-	Example:
-
-	>>> filesinkelem.connect("notify::timestamp", framecpp_filesink_ldas_path_handler, (".", 5))
-	"""
-	outpath, dir_digits = path_digits
-
-	# get timestamp and truncate to integer seconds
-	timestamp = elem.get_property("timestamp") // Gst.SECOND
-
-	# extract leading digits
-	leading_digits = timestamp // 10**int(math.log10(timestamp) + 1 - dir_digits)
-
-	# get other metadata
-	instrument = elem.get_property("instrument")
-	frame_type = elem.get_property("frame-type")
-
-	# make target directory, and set path
-	path = os.path.join(outpath, "%s-%s-%d" % (instrument, frame_type, leading_digits))
-	if not os.path.exists(path):
-		os.makedirs(path)
-	elem.set_property("path", path)
-
-
-def framecpp_filesink_cache_entry_from_mfs_message(message):
-	"""
-	Translate an element message posted by the multifilesink element
-	inside a framecpp_filesink bin into a lal.utils.CacheEntry object
-	describing the file being written by the multifilesink element.
-	"""
-	# extract the segment spanned by the file from the message directly
-	start = LIGOTimeGPS(0, message.get_structure()["timestamp"])
-	end = start + LIGOTimeGPS(0, message.get_structure()["duration"])
-
-	# retrieve the framecpp_filesink bin (for instrument/observatory
-	# and frame file type)
-	parent = message.src.get_parent()
-
-	# construct and return a CacheEntry object
-	return CacheEntry(parent.get_property("instrument"), parent.get_property("frame-type"), segments.segment(start, end), "file://localhost%s" % os.path.abspath(message.get_structure()["filename"]))
-
-
-#
-# =============================================================================
-#
-#                                Pipeline Parts
-#
-# =============================================================================
-#
-
-
-def mkchannelgram(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_channelgram", **properties)
-
-
-def mkspectrumplot(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_spectrumplot", **properties)
-
-
-def mkhistogram(pipeline, src):
-	return mkgeneric(pipeline, src, "lal_histogramplot")
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALSegmentSrc.html">lal_segmentsrc</a> element to a pipeline with useful default properties
-def mksegmentsrc(pipeline, segment_list, blocksize = 4096 * 1 * 1, invert_output = False):
-	# default blocksize is 4096 seconds of unsigned integers at
-	# 1 Hz, e.g. segments without nanoseconds
-	return mkgeneric(pipeline, None, "lal_segmentsrc", blocksize = blocksize, segment_list = segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list), invert_output = invert_output)
-
-
-## Adds a <a href="@gstlalgtkdoc/GstLALCacheSrc.html">lal_cachesrc</a> element to a pipeline with useful default properties
-def mklalcachesrc(pipeline, location, use_mmap = True, **properties):
-	return mkgeneric(pipeline, None, "lal_cachesrc", location = location, use_mmap = use_mmap, **properties)
-
-
-def mklvshmsrc(pipeline, shm_name, **properties):
-	return mkgeneric(pipeline, None, "gds_lvshmsrc", shm_name = shm_name, **properties)
-
-
-def mkframexmitsrc(pipeline, multicast_group, port, **properties):
-	return mkgeneric(pipeline, None, "gds_framexmitsrc", multicast_group = multicast_group, port = port, **properties)
-
-
-def mkigwdparse(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "framecpp_igwdparse", **properties)
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-uridecodebin.html">uridecodebin</a> element to a pipeline with useful default properties
-def mkuridecodebin(pipeline, uri, caps = "application/x-igwd-frame,framed=true", **properties):
-	return mkgeneric(pipeline, None, "uridecodebin", uri = uri, caps = None if caps is None else Gst.Caps.from_string(caps), **properties)
-
-
-def mkframecppchanneldemux(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "framecpp_channeldemux", **properties)
-
-
-def mkframecppchannelmux(pipeline, channel_src_map, units = None, seglists = None, **properties):
-	elem = mkgeneric(pipeline, None, "framecpp_channelmux", **properties)
-	if channel_src_map is not None:
-		for channel, src in channel_src_map.items():
-			for srcpad in src.srcpads:
-				# FIXME FIXME FIXME. This should use the pad template from the element.  
-				# FIXME once a newer version of some library is available, then we should be able to switch to this
-				# if srcpad.link(elem.get_request_pad(channel)) == Gst.PadLinkReturn.OK
-				# Instead. Right now it fails due to the
-				# underscore in channel names.  When it fails
-				# it fails silently and returns None, which
-				# gives a cryptic error message
-				if srcpad.link(elem.request_pad(Gst.PadTemplate.new(channel, Gst.PadDirection.SINK, Gst.PadPresence.REQUEST, Gst.Caps("ANY")), channel)) == Gst.PadLinkReturn.OK:
-					break
-	if units is not None:
-		framecpp_channeldemux_set_units(elem, units)
-	if seglists is not None:
-		framecpp_channeldemux_check_segments(elem, seglists)
-	return elem
-
-
-def mkframecppfilesink(pipeline, src, message_forward = True, **properties):
-	post_messages = properties.pop("post_messages", True)
-	elem = mkgeneric(pipeline, src, "framecpp_filesink", message_forward = message_forward, **properties)
-	# FIXME:  there's supposed to be some sort of proxy mechanism for
-	# setting properties on child elements, but we can't seem to get
-	# anything to work
-	elem.get_by_name("multifilesink").set_property("post-messages", post_messages)
-	return elem
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-multifilesink.html">multifilesink</a> element to a pipeline with useful default properties
-def mkmultifilesink(pipeline, src, next_file = 0, sync = False, async_ = False, **properties):
-	properties["async"] = async_
-	return mkgeneric(pipeline, src, "multifilesink", next_file = next_file, sync = sync, **properties)
-
-
-def mkndssrc(pipeline, host, instrument, channel_name, channel_type, blocksize = 16384 * 8 * 1, port = 31200):
-	# default blocksize is 1 second of double precision floats at
-	# 16384 Hz, e.g., LIGO h(t)
-	return mkgeneric(pipeline, None, "ndssrc", blocksize = blocksize, port = port, host = host, channel_name = "%s:%s" % (instrument, channel_name), channel_type = channel_type)
-
-
-## Adds a <a href="@gstdoc/gstreamer-plugins-capsfilter.html">capsfilter</a> element to a pipeline with useful default properties
-def mkcapsfilter(pipeline, src, caps, **properties):
-	return mkgeneric(pipeline, src, "capsfilter", caps = Gst.Caps.from_string(caps), **properties)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-capssetter.html">capssetter</a> element to a pipeline with useful default properties
-def mkcapssetter(pipeline, src, caps, **properties):
-	return mkgeneric(pipeline, src, "capssetter", caps = Gst.Caps.from_string(caps), **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALStateVector.html">lal_statevector</a> element to a pipeline with useful default properties
-def mkstatevector(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_statevector", **properties)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-taginject.html">taginject</a> element to a pipeline with useful default properties
-def mktaginject(pipeline, src, tags):
-	return mkgeneric(pipeline, src, "taginject", tags = tags)
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiotestsrc.html">audiotestsrc</a> element to a pipeline with useful default properties
-def mkaudiotestsrc(pipeline, **properties):
-	return mkgeneric(pipeline, None, "audiotestsrc", **properties)
-
-
-## see documentation for mktaginject() mkcapsfilter() and mkaudiotestsrc()
-def mkfakesrc(pipeline, instrument, channel_name, blocksize = None, volume = 1e-20, is_live = False, wave = 9, rate = 16384, **properties):
-	if blocksize is None:
-		# default blocksize is 1 second * rate samples/second * 8
-		# bytes/sample (assume double-precision floats)
-		blocksize = 1 * rate * 8
-	return mktaginject(pipeline, mkcapsfilter(pipeline, mkaudiotestsrc(pipeline, samplesperbuffer = blocksize / 8, wave = wave, volume = volume, is_live = is_live, **properties), "audio/x-raw, format=F64%s, rate=%d" % (BYTE_ORDER, rate)), "instrument=%s,channel-name=%s,units=strain" % (instrument, channel_name))
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiofirfilter.html">audiofirfilter</a> element to a pipeline with useful default properties
-def mkfirfilter(pipeline, src, kernel, latency, **properties):
-	properties.update((name, val) for name, val in (("kernel", kernel), ("latency", latency)) if val is not None)
-	return mkgeneric(pipeline, src, "audiofirfilter", **properties)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioiirfilter.html">audioiirfilter</a> element to a pipeline with useful default properties
-def mkiirfilter(pipeline, src, a, b):
-	# convention is z = \exp(-i 2 \pi f / f_{\rm sampling})
-	# H(z) = (\sum_{j=0}^{N} a_j z^{-j}) / (\sum_{j=0}^{N} (-1)^{j} b_j z^{-j})
-	return mkgeneric(pipeline, src, "audioiirfilter", a = a, b = b)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALShift.html">lal_shift</a> element to a pipeline with useful default properties
-def mkshift(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_shift", **properties)
-
-
-def mkfakeLIGOsrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1):
-	properties = {"blocksize": blocksize}
-	properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None)
-	return mkgeneric(pipeline, None, "lal_fakeligosrc", **properties)
-
-
-def mkfakeadvLIGOsrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1):
-	properties = {"blocksize": blocksize}
-	properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None)
-	return mkgeneric(pipeline, None, "lal_fakeadvligosrc", **properties)
-
-
-def mkfakeadvvirgosrc(pipeline, location = None, instrument = None, channel_name = None, blocksize = 16384 * 8 * 1):
-	properties = {"blocksize": blocksize}
-	if instrument is not None:
-		properties["instrument"] = instrument
-	if channel_name is not None:
-		properties["channel_name"] = channel_name
-	return mkgeneric(pipeline, None, "lal_fakeadvvirgosrc", **properties)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-progressreport.html">progress_report</a> element to a pipeline with useful default properties
-def mkprogressreport(pipeline, src, name):
-	return mkgeneric(pipeline, src, "progressreport", do_query = False, name = name)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALSimulation.html">lal_simulation</a> element to a pipeline with useful default properties
-def mkinjections(pipeline, src, filename):
-	return mkgeneric(pipeline, src, "lal_simulation", xml_location = filename)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiochebband.html">audiochebband</a> element to a pipeline with useful default properties
-def mkaudiochebband(pipeline, src, lower_frequency, upper_frequency, poles = 8):
-	return mkgeneric(pipeline, src, "audiochebband", lower_frequency = lower_frequency, upper_frequency = upper_frequency, poles = poles)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiocheblimit.html">audiocheblimit</a> element to a pipeline with useful default properties
-def mkaudiocheblimit(pipeline, src, cutoff, mode = 0, poles = 8, type = 1, ripple = 0.25):
-	return mkgeneric(pipeline, src, "audiocheblimit", cutoff = cutoff, mode = mode, poles = poles, type = type, ripple = ripple)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioamplify.html">audioamplify</a> element to a pipeline with useful default properties
-def mkaudioamplify(pipeline, src, amplification):
-	return mkgeneric(pipeline, src, "audioamplify", clipping_method = 3, amplification = amplification)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALAudioUnderSample.html">lal_audioundersample</a> element to a pipeline with useful default properties
-def mkaudioundersample(pipeline, src):
-	return mkgeneric(pipeline, src, "lal_audioundersample")
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioresample.html">audioresample</a> element to a pipeline with useful default properties
-def mkresample(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "audioresample", **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALInterpolator.html">lal_interpolator</a> element to a pipeline with useful default properties
-def mkinterpolator(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_interpolator", **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties
-def mkwhiten(pipeline, src, psd_mode = 0, zero_pad = 0, fft_length = 8, average_samples = 64, median_samples = 7, **properties):
-	return mkgeneric(pipeline, src, "lal_whiten", psd_mode = psd_mode, zero_pad = zero_pad, fft_length = fft_length, average_samples = average_samples, median_samples = median_samples, **properties)
-
-
-## Adds a <a href="@gstdoc/gstreamer-plugins-tee.html">tee</a> element to a pipeline with useful default properties
-def mktee(pipeline, src):
-	return mkgeneric(pipeline, src, "tee")
-
-
-## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "sum" mode mixing.
-def mkadder(pipeline, srcs, sync = True, mix_mode = "sum", **properties):
-	elem = mkgeneric(pipeline, None, "lal_adder", sync = sync, mix_mode = mix_mode, **properties)
-	if srcs is not None:
-		for src in srcs:
-			src.link(elem)
-	return elem
-
-
-## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "product" mode mixing.
-def mkmultiplier(pipeline, srcs, sync = True, mix_mode = "product", **properties):
-	return mkadder(pipeline, srcs, sync = sync, mix_mode = mix_mode, **properties)
-
-
-## Adds a <a href="@gstdoc/gstreamer-plugins-queue.html">queue</a> element to a pipeline with useful default properties
-def mkqueue(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "queue", **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties
-def mkdrop(pipeline, src, drop_samples = 0):
-	return mkgeneric(pipeline, src, "lal_drop", drop_samples = drop_samples)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALNoFakeDisconts.html">lal_nofakedisconts</a> element to a pipeline with useful default properties
-def mknofakedisconts(pipeline, src, silent = True):
-	return mkgeneric(pipeline, src, "lal_nofakedisconts", silent = silent)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALFIRBank.html">lal_firbank</a> element to a pipeline with useful default properties
-def mkfirbank(pipeline, src, latency = None, fir_matrix = None, time_domain = None, block_stride = None):
-	properties = dict((name, value) for name, value in zip(("latency", "fir_matrix", "time_domain", "block_stride"), (latency, fir_matrix, time_domain, block_stride)) if value is not None)
-	return mkgeneric(pipeline, src, "lal_firbank", **properties)
-
-
-def mktdwhiten(pipeline, src, latency = None, kernel = None, taper_length = None):
-	# a taper length of 1/4 kernel length mimics the default
-	# configuration of the FFT whitener
-	if taper_length is None and kernel is not None:
-		taper_length = len(kernel) // 4
-	properties = dict((name, value) for name, value in zip(("latency", "kernel", "taper_length"), (latency, kernel, taper_length)) if value is not None)
-	return mkgeneric(pipeline, src, "lal_tdwhiten", **properties)
-
-
-def mktrim(pipeline, src, initial_offset = None, final_offset = None, inverse = None):
-	properties = dict((name, value) for name, value in zip(("initial-offset", "final-offset", "inverse"), (initial_offset,final_offset,inverse)) if value is not None)
-	return mkgeneric(pipeline, src, "lal_trim", **properties)
-
-
-def mkmean(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_mean", **properties)
-
-
-def mkabs(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "abs", **properties)
-
-
-def mkpow(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "pow", **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALReblock.html">lal_reblock</a> element to a pipeline with useful default properties
-def mkreblock(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_reblock", **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALSumSquares.html">lal_sumsquares</a> element to a pipeline with useful default properties
-def mksumsquares(pipeline, src, weights = None):
-	if weights is not None:
-		return mkgeneric(pipeline, src, "lal_sumsquares", weights = weights)
-	else:
-		return mkgeneric(pipeline, src, "lal_sumsquares")
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALGate.html">lal_gate</a> element to a pipeline with useful default properties
-def mkgate(pipeline, src, threshold = None, control = None, **properties):
-	if threshold is not None:
-		elem = mkgeneric(pipeline, None, "lal_gate", threshold = threshold, **properties)
-	else:
-		elem = mkgeneric(pipeline, None, "lal_gate", **properties)
-	for peer, padname in ((src, "sink"), (control, "control")):
-		if isinstance(peer, Gst.Pad):
-			peer.get_parent_element().link_pads(peer, elem, padname)
-		elif peer is not None:
-			peer.link_pads(None, elem, padname)
-	return elem
-
-
-def mkbitvectorgen(pipeline, src, bit_vector, **properties):
-	return mkgeneric(pipeline, src, "lal_bitvectorgen", bit_vector = bit_vector, **properties)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALMatrixMixer.html">lal_matrixmixer</a> element to a pipeline with useful default properties
-def mkmatrixmixer(pipeline, src, matrix = None):
-	if matrix is not None:
-		return mkgeneric(pipeline, src, "lal_matrixmixer", matrix = matrix)
-	else:
-		return mkgeneric(pipeline, src, "lal_matrixmixer")
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALToggleComplex.html">lal_togglecomplex</a> element to a pipeline with useful default properties
-def mktogglecomplex(pipeline, src):
-	return mkgeneric(pipeline, src, "lal_togglecomplex")
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALAutoChiSq.html">lal_autochisq</a> element to a pipeline with useful default properties
-def mkautochisq(pipeline, src, autocorrelation_matrix = None, mask_matrix = None, latency = 0, snr_thresh=0):
-	properties = {}
-	if autocorrelation_matrix is not None:
-		properties.update({
-			"autocorrelation_matrix": pipeio.repack_complex_array_to_real(autocorrelation_matrix),
-			"latency": latency,
-			"snr_thresh": snr_thresh
-		})
-	if mask_matrix is not None:
-		properties["autocorrelation_mask_matrix"] = mask_matrix
-	return mkgeneric(pipeline, src, "lal_autochisq", **properties)
-
-
-## Adds a <a href="@gstdoc/gstreamer-plugins-fakesink.html">fakesink</a> element to a pipeline with useful default properties
-def mkfakesink(pipeline, src):
-	return mkgeneric(pipeline, src, "fakesink", sync = False, **{"async": False})
-
-
-## Adds a <a href="@gstdoc/gstreamer-plugins-filesink.html">filesink</a> element to a pipeline with useful default properties
-def mkfilesink(pipeline, src, filename, sync = False, async_ = False):
-	return mkgeneric(pipeline, src, "filesink", sync = sync, buffer_mode = 2, location = filename, **{"async": async_})
-
-
-## Adds a <a href="@gstlalgtkdoc/GstTSVEnc.html">lal_nxydump</a> element to a pipeline with useful default properties
-def mknxydumpsink(pipeline, src, filename, segment = None):
-	if segment is not None:
-		elem = mkgeneric(pipeline, src, "lal_nxydump", start_time = segment[0].ns(), stop_time = segment[1].ns())
-	else:
-		elem = mkgeneric(pipeline, src, "lal_nxydump")
-	return mkfilesink(pipeline, elem, filename)
-
-
-def mknxydumpsinktee(pipeline, src, *args, **properties):
-	t = mktee(pipeline, src)
-	mknxydumpsink(pipeline, mkqueue(pipeline, t), *args, **properties)
-	return t
-
-
-def mkblcbctriggergen(pipeline, snr, chisq, template_bank_filename, snr_threshold, sigmasq):
-	# snr is complex and chisq is real so the correct source and sink
-	# pads will be selected automatically
-	elem = mkgeneric(pipeline, snr, "lal_blcbctriggergen", bank_filename = template_bank_filename, snr_thresh = snr_threshold, sigmasq = sigmasq)
-	chisq.link(elem)
-	return elem
-
-
-def mktriggergen(pipeline, snr, chisq, template_bank_filename, snr_threshold, sigmasq):
-	# snr is complex and chisq is real so the correct source and sink
-	# pads will be selected automatically
-	elem = mkgeneric(pipeline, snr, "lal_triggergen", bank_filename = template_bank_filename, snr_thresh = snr_threshold, sigmasq = sigmasq)
-	chisq.link(elem)
-	return elem
-
-
-def mktriggerxmlwritersink(pipeline, src, filename):
-	return mkgeneric(pipeline, src, "lal_triggerxmlwriter", location = filename, sync = False, **{"async": False})
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-wavenc.html">wavenc</a> element to a pipeline with useful default properties
-def mkwavenc(pipeline, src):
-	return mkgeneric(pipeline, src, "wavenc")
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-vorbisenc.html">vorbisenc</a> element to a pipeline with useful default properties
-def mkvorbisenc(pipeline, src):
-	return mkgeneric(pipeline, src, "vorbisenc")
-
-
-def mkcolorspace(pipeline, src):
-	return mkgeneric(pipeline, src, "ffmpegcolorspace") # MOD: Found ffmpegcolorspace in line: [	return mkgeneric(pipeline, src, "ffmpegcolorspace")]
-
-
-def mktheoraenc(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "theoraenc", **properties)
-
-
-def mkmpeg4enc(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "ffenc_mpeg4", **properties)
-
-
-def mkoggmux(pipeline, src):
-	return mkgeneric(pipeline, src, "oggmux")
-
-
-def mkavimux(pipeline, src):
-	return mkgeneric(pipeline, src, "avimux")
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioconvert.html">audioconvert</a> element to a pipeline with useful default properties
-def mkaudioconvert(pipeline, src, caps_string = None):
-	elem = mkgeneric(pipeline, src, "audioconvert")
-	if caps_string is not None:
-		elem = mkcapsfilter(pipeline, elem, caps_string)
-	return elem
-
-
-## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiorate.html">audiorate</a> element to a pipeline with useful default properties
-def mkaudiorate(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "audiorate", **properties)
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-flacenc.html">flacenc</a> element to a pipeline with useful default properties
-def mkflacenc(pipeline, src, quality = 0, **properties):
-	return mkgeneric(pipeline, src, "flacenc", quality = quality, **properties)
-
-
-def mkogmvideosink(pipeline, videosrc, filename, audiosrc = None, verbose = False):
-	src = mkcolorspace(pipeline, videosrc)
-	src = mkcapsfilter(pipeline, src, "video/x-raw-yuv, format=(fourcc)I420")
-	src = mktheoraenc(pipeline, src, border = 2, quality = 48, quick = False)
-	src = mkoggmux(pipeline, src)
-	if audiosrc is not None:
-		mkflacenc(pipeline, mkcapsfilter(pipeline, mkaudioconvert(pipeline, audiosrc), "audio/x-raw, format=S24%s" % BYTE_ORDER)).link(src)
-	if verbose:
-		src = mkprogressreport(pipeline, src, filename)
-	mkfilesink(pipeline, src, filename)
-
-
-def mkvideosink(pipeline, src):
-	return mkgeneric(pipeline, mkcolorspace(pipeline, src), "autovideosink")
-
-
-## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-autoaudiosink.html">autoaudiosink</a> element to a pipeline with useful default properties
-def mkautoaudiosink(pipeline, src):
-	return mkgeneric(pipeline, mkqueue(pipeline, src), "autoaudiosink")
-
-
-def mkplaybacksink(pipeline, src, amplification = 0.1):
-	elems = (
-		Gst.ElementFactory.make("audioconvert", None),
-		Gst.ElementFactory.make("capsfilter", None),
-		Gst.ElementFactory.make("audioamplify", None),
-		Gst.ElementFactory.make("audioconvert", None),
-		Gst.ElementFactory.make("queue", None),
-		Gst.ElementFactory.make("autoaudiosink", None)
-	)
-	elems[1].set_property("caps", Gst.Caps.from_string("audio/x-raw, format=F32%s" % BYTE_ORDER))
-	elems[2].set_property("amplification", amplification)
-	elems[4].set_property("max-size-time", 1 * Gst.SECOND)
-	pipeline.add(*elems)
-	Gst.element_link_many(src, *elems) # MOD: Error line [733]: element_link_many not yet implemented. See web page **
-
-
-def mkdeglitcher(pipeline, src, segment_list):
-	return mkgeneric(pipeline, src, "lal_deglitcher", segment_list = segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list))
-
-
-# FIXME no specific alias for this url since this library only has one element.
-# DO NOT DOCUMENT OTHER CODES THIS WAY! Use @gstdoc @gstpluginsbasedoc etc.
-## Adds a <a href="http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-libs/html/gstreamer-app.html">appsink</a> element to a pipeline with useful default properties
-def mkappsink(pipeline, src, max_buffers = 1, drop = False, sync = False, async_ = False, **properties):
-	properties["async"] = async_
-	return mkgeneric(pipeline, src, "appsink", sync = sync, emit_signals = True, max_buffers = max_buffers, drop = drop, **properties)
-
-
-class AppSync(object):
-	def __init__(self, appsink_new_buffer, appsinks = []):
-		self.lock = threading.Lock()
-		# handler to invoke on availability of new time-ordered
-		# buffer
-		self.appsink_new_buffer = appsink_new_buffer
-		# element --> timestamp of current buffer or None if no
-		# buffer yet available
-		self.appsinks = {}
-		# set of sink elements that are currently at EOS
-		self.at_eos = set()
-		# attach handlers to appsink elements provided at this time
-		for elem in appsinks:
-			self.attach(elem)
-
-	def add_sink(self, pipeline, src, drop = False, **properties):
-		return self.attach(mkappsink(pipeline, src, drop = drop, **properties))
-
-	def attach(self, appsink):
-		"""
-		connect this AppSync's signal handlers to the given appsink
-		element.  the element's max-buffers property will be set to
-		1 (required for AppSync to work).
-		"""
-		if appsink in self.appsinks:
-			raise ValueError("duplicate appsinks %s" % repr(appsink))
-		appsink.set_property("max-buffers", 1)
-		handler_id = appsink.connect("new-preroll", self.new_preroll_handler)
-		assert handler_id > 0
-		handler_id = appsink.connect("new-sample", self.new_sample_handler)
-		assert handler_id > 0
-		handler_id = appsink.connect("eos", self.eos_handler)
-		assert handler_id > 0
-		self.appsinks[appsink] = None
-		return appsink
-
-	def new_preroll_handler(self, elem):
-		with self.lock:
-			# clear eos status
-			self.at_eos.discard(elem)
-			# ignore preroll buffers
-			elem.emit("pull-preroll")
-			return Gst.FlowReturn.OK
-
-	def new_sample_handler(self, elem):
-		with self.lock:
-			# clear eos status, and retrieve buffer timestamp
-			self.at_eos.discard(elem)
-			assert self.appsinks[elem] is None
-			self.appsinks[elem] = elem.get_last_sample().get_buffer().pts
-			# pull available buffers from appsink elements
-			return self.pull_buffers(elem)
-
-	def eos_handler(self, elem):
-		with self.lock:
-			# set eos status
-			self.at_eos.add(elem)
-			# pull available buffers from appsink elements
-			return self.pull_buffers(elem)
-
-	def pull_buffers(self, elem):
-		"""
-		for internal use.  must be called with lock held.
-		"""
-		# keep looping while we can process buffers
-		while 1:
-			# retrieve the timestamps of all elements that
-			# aren't at eos and all elements at eos that still
-			# have buffers in them
-			timestamps = [(t, e) for e, t in self.appsinks.items() if e not in self.at_eos or t is not None]
-			# if all elements are at eos and none have buffers,
-			# then we're at eos
-			if not timestamps:
-				return Gst.FlowReturn.EOS
-			# find the element with the oldest timestamp.  None
-			# compares as less than everything, so we'll find
-			# any element (that isn't at eos) that doesn't yet
-			# have a buffer (elements at eos and that are
-			# without buffers aren't in the list)
-			timestamp, elem_with_oldest = min(timestamps, key=lambda x: x[0] if x[0] is not None else -numpy.inf)
-			# if there's an element without a buffer, quit for
-			# now --- we require all non-eos elements to have
-			# buffers before proceding
-			if timestamp is None:
-				return Gst.FlowReturn.OK
-			# clear timestamp and pass element to handler func.
-			# function call is done last so that all of our
-			# book-keeping has been taken care of in case an
-			# exception gets raised
-			self.appsinks[elem_with_oldest] = None
-			self.appsink_new_buffer(elem_with_oldest)
-
-
-class connect_appsink_dump_dot(object):
-	"""
-	add a signal handler to write a pipeline graph upon receipt of the
-	first trigger buffer.  the caps in the pipeline graph are not fully
-	negotiated until data comes out the end, so this version of the graph
-	shows the final formats on all links
-	"""
-	def __init__(self, pipeline, appsinks, basename, verbose = False):
-		self.pipeline = pipeline
-		self.filestem = "%s.%s" % (basename, "TRIGGERS")
-		self.verbose = verbose
-		# map element to handler ID
-		self.remaining_lock = threading.Lock()
-		self.remaining = {}
-		for sink in appsinks:
-			self.remaining[sink] = sink.connect_after("new-preroll", self.execute)
-			assert self.remaining[sink] > 0
-
-	def execute(self, elem):
-		with self.remaining_lock:
-			handler_id = self.remaining.pop(elem)
-			if not self.remaining:
-				write_dump_dot(self.pipeline, self.filestem, verbose = self.verbose)
-		elem.disconnect(handler_id)
-		return Gst.FlowReturn.OK
-
-
-def mkchecktimestamps(pipeline, src, name = None, silent = True, timestamp_fuzz = 1):
-	return mkgeneric(pipeline, src, "lal_checktimestamps", name = name, silent = silent, timestamp_fuzz = timestamp_fuzz)
-
-
-## Adds a <a href="@gstlalgtkdoc/GSTLALPeak.html">lal_peak</a> element to a pipeline with useful default properties
-def mkpeak(pipeline, src, n):
-	return mkgeneric(pipeline, src, "lal_peak", n = n)
-
-
-def mkdenoiser(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_denoiser", **properties)
-
-
-def mkclean(pipeline, src, threshold=1.0):
-	return mkdenoiser(pipeline, src, stationary=True, threshold=threshold)
-
-
-def mkitac(pipeline, src, n, bank, autocorrelation_matrix = None, mask_matrix = None, snr_thresh = 0, sigmasq = None):
-	properties = {
-		"n": n,
-		"bank_filename": bank,
-		"snr_thresh": snr_thresh
-	}
-	if autocorrelation_matrix is not None:
-		properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix)
-	if mask_matrix is not None:
-		properties["autocorrelation_mask"] = mask_matrix
-	if sigmasq is not None:
-		properties["sigmasq"] = sigmasq
-	return mkgeneric(pipeline, src, "lal_itac", **properties)
-
-def mktrigger(pipeline, src, n, autocorrelation_matrix = None, mask_matrix = None, snr_thresh = 0, sigmasq = None, max_snr = False):
-	properties = {
-		"n": n,
-		"snr_thresh": snr_thresh,
-		"max_snr": max_snr
-	}
-	if autocorrelation_matrix is not None:
-		properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix)
-	if mask_matrix is not None:
-		properties["autocorrelation_mask"] = mask_matrix
-	if sigmasq is not None:
-		properties["sigmasq"] = sigmasq
-	return mkgeneric(pipeline, src, "lal_trigger", **properties)
-
-def mklatency(pipeline, src, name = None, silent = False):
-	return mkgeneric(pipeline, src, "lal_latency", name = name, silent = silent)
-
-def mklhocoherentnull(pipeline, H1src, H2src, H1_impulse, H1_latency, H2_impulse, H2_latency, srate):
-	elem = mkgeneric(pipeline, None, "lal_lho_coherent_null", block_stride = srate, H1_impulse = H1_impulse, H2_impulse = H2_impulse, H1_latency = H1_latency, H2_latency = H2_latency)
-	for peer, padname in ((H1src, "H1sink"), (H2src, "H2sink")):
-		if isinstance(peer, Gst.Pad):
-			peer.get_parent_element().link_pads(peer, elem, padname)
-		elif peer is not None:
-			peer.link_pads(None, elem, padname)
-	return elem
-
-def mkcomputegamma(pipeline, dctrl, exc, cos, sin, **properties):
-	elem = mkgeneric(pipeline, None, "lal_compute_gamma", **properties)
-	for peer, padname in ((dctrl, "dctrl_sink"), (exc, "exc_sink"), (cos, "cos"), (sin, "sin")):
-		if isinstance(peer, Gst.Pad):
-			peer.get_parent_element().link_pads(peer, elem, padname)
-		elif peer is not None:
-			peer.link_pads(None, elem, padname)
-	return elem
-
-def mkbursttriggergen(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_bursttriggergen", **properties)
-
-def mkodctodqv(pipeline, src, **properties):
-	return mkgeneric(pipeline, src, "lal_odc_to_dqv", **properties)
-
-def mktcpserversink(pipeline, src, **properties):
-	# units_soft_max = 1 GB
-	# FIXME:  are these sensible defaults?
-	return mkgeneric(pipeline, src, "tcpserversink", sync = True, sync_method = "latest-keyframe", recover_policy = "keyframe", unit_type = "bytes", units_soft_max = 1024**3, **properties)
-
-
-def audioresample_variance_gain(quality, num, den):
-	"""Calculate the output gain of GStreamer's stock audioresample element.
-
-	The audioresample element has a frequency response of unity "almost" all the
-	way up the Nyquist frequency.  However, for an input of unit variance
-	Gaussian noise, the output will have a variance very slighly less than 1.
-	The return value is the variance that the filter will produce for a given
-	"quality" setting and sample rate.
-
-	@param den The denomenator of the ratio of the input and output sample rates
-	@param num The numerator of the ratio of the input and output sample rates
-	@return The variance of the output signal for unit variance input
-
-	The following example shows how to apply the correction factor using an
-	audioamplify element.
-
-	>>> from gstlal.pipeutil import *
-	>>> from gstlal.pipeparts import audioresample_variance_gain
-	>>> from gstlal import pipeio
-	>>> import numpy
-	>>> nsamples = 2 ** 17
-	>>> num = 2
-	>>> den = 1
-	>>> def handoff_handler(element, buffer, pad, (quality, filt_len, num, den)):
-	...		out_latency = numpy.ceil(float(den) / num * filt_len)
-	...		buf = pipeio.array_from_audio_buffer(buffer).flatten()
-	...		std = numpy.std(buf[out_latency:-out_latency])
-	...		print "quality=%2d, filt_len=%3d, num=%d, den=%d, stdev=%.2f" % (
-	...			quality, filt_len, num, den, std)
-	...
-	>>> for quality in range(11):
-	...		pipeline = Gst.Pipeline()
-	...		correction = 1/numpy.sqrt(audioresample_variance_gain(quality, num, den))
-	...		elems = mkelems_in_bin(pipeline,
-	...			('audiotestsrc', {'wave':'gaussian-noise','volume':1}),
-	...			('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,format=F64LE,rate=%d' % num)}),
-	...			('audioresample', {'quality':quality}),
-	...			('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,width=F64LE,rate=%d' % den)}),
-	...			('audioamplify', {'amplification':correction,'clipping-method':'none'}),
-	...			('fakesink', {'signal-handoffs':True, 'num-buffers':1})
-	...		)
-	...		filt_len = elems[2].get_property('filter-length')
-	...		elems[0].set_property('samplesperbuffer', 2 * filt_len + nsamples)
-	...		if elems[-1].connect_after('handoff', handoff_handler, (quality, filt_len, num, den)) < 1:
-	...			raise RuntimeError
-	...		try:
-	...			if pipeline.set_state(Gst.State.PLAYING) is not Gst.State.CHANGE_ASYNC:
-	...				raise RuntimeError
-	...			if not pipeline.get_bus().poll(Gst.MessageType.EOS, -1):
-	...				raise RuntimeError
-	...		finally:
-	...			if pipeline.set_state(Gst.State.NULL) is not Gst.StateChangeReturn.SUCCESS:
-	...				raise RuntimeError
-	...
-	quality= 0, filt_len=  8, num=2, den=1, stdev=1.00
-	quality= 1, filt_len= 16, num=2, den=1, stdev=1.00
-	quality= 2, filt_len= 32, num=2, den=1, stdev=1.00
-	quality= 3, filt_len= 48, num=2, den=1, stdev=1.00
-	quality= 4, filt_len= 64, num=2, den=1, stdev=1.00
-	quality= 5, filt_len= 80, num=2, den=1, stdev=1.00
-	quality= 6, filt_len= 96, num=2, den=1, stdev=1.00
-	quality= 7, filt_len=128, num=2, den=1, stdev=1.00
-	quality= 8, filt_len=160, num=2, den=1, stdev=1.00
-	quality= 9, filt_len=192, num=2, den=1, stdev=1.00
-	quality=10, filt_len=256, num=2, den=1, stdev=1.00
-	"""
-
-	# These constants were measured with 2**22 samples.
-
-	if num > den: # downsampling
-		return den * (
-			0.7224862140943990596,
-			0.7975021342935247892,
-			0.8547537598970208483,
-			0.8744072146753004704,
-			0.9075294214410336568,
-			0.9101523813406768859,
-			0.9280549396020538744,
-			0.9391809530012216189,
-			0.9539276644089494939,
-			0.9623083437067311285,
-			0.9684700588501590213
-			)[quality] / num
-	elif num < den: # upsampling
-		return (
-			0.7539740617648067467,
-			0.8270076656536116122,
-			0.8835072979478705291,
-			0.8966758456219333651,
-			0.9253434087537378838,
-			0.9255866674042573239,
-			0.9346487800036394900,
-			0.9415331868209220190,
-			0.9524608799160205752,
-			0.9624372769883490220,
-			0.9704505626409354324
-			)[quality]
-	else: # no change in sample rate
-		return 1.
-
-
-#
-# =============================================================================
-#
-#                                Debug utilities
-#
-# =============================================================================
-#
-
-
-def write_dump_dot(pipeline, filestem, verbose = False):
-	"""
-	This function needs the environment variable GST_DEBUG_DUMP_DOT_DIR
-	to be set.   The filename will be
-
-	os.path.join($GST_DEBUG_DUMP_DOT_DIR, filestem + ".dot")
-
-	If verbose is True, a message will be written to stderr.
-	"""
-	if "GST_DEBUG_DUMP_DOT_DIR" not in os.environ:
-		raise ValueError("cannot write pipeline, environment variable GST_DEBUG_DUMP_DOT_DIR is not set")
-	Gst.debug_bin_to_dot_file(pipeline, Gst.DebugGraphDetails.ALL, filestem)
-	if verbose:
-		print("Wrote pipeline to %s" % os.path.join(os.environ["GST_DEBUG_DUMP_DOT_DIR"], "%s.dot" % filestem), file=sys.stderr)
+__doc__ = """The `pipeparts` module contains all of the *elements* used to construct gstreamer pipelines
+for gravitational wave analysis. These elements are grouped into thematic submodules.
+"""
+
+
+# Flatten package to preserve import paths
+from .sink import AppSync as AppSync
+from .sink import BYTE_ORDER as BYTE_ORDER
+from .transform import audioresample_variance_gain as audioresample_variance_gain
+from .sink import ConnectAppsinkDumpDot as connect_appsink_dump_dot
+from .mux import FrameCPPChannelDemuxCheckSegmentsHandler as framecpp_channeldemux_check_segments
+from .mux import FrameCPPChannelDemuxSetUnitsHandler as framecpp_channeldemux_set_units
+from .sink import framecpp_filesink_cache_entry_from_mfs_message as framecpp_filesink_cache_entry_from_mfs_message
+from .sink import framecpp_filesink_ldas_path_handler as framecpp_filesink_ldas_path_handler
+from .transform import abs_ as mkabs
+from .transform import adder as mkadder
+from .sink import app as mkappsink
+from .transform import amplify as mkaudioamplify
+from .filters import audio_cheb_band as mkaudiochebband
+from .filters import audio_cheb_limit as mkaudiocheblimit
+from .transform import audio_convert as mkaudioconvert
+from .transform import audio_rate as mkaudiorate
+from .source import audio_test as mkaudiotestsrc
+from .transform import undersample as mkaudioundersample
+from .sink import auto_audio as mkautoaudiosink
+from .transform import auto_chisq as mkautochisq
+from .mux import avi_mux as mkavimux
+from .transform import bit_vector_gen as mkbitvectorgen
+from .trigger import blcbc_trigger_gen as mkblcbctriggergen
+from .trigger import burst_trigger_gen as mkbursttriggergen
+from .filters import caps as mkcapsfilter
+from .transform import set_caps as mkcapssetter
+from .plot import channelgram as mkchannelgram
+from .transform import check_timestamps as mkchecktimestamps
+from .transform import clean as mkclean
+from .transform import colorspace as mkcolorspace
+from .transform import mkcomputegamma as mkcomputegamma
+from .transform import deglitch as mkdeglitcher
+from .transform import denoise as mkdenoiser
+from .filters import drop as mkdrop
+from .source import fake_ligo as mkfakeLIGOsrc
+from .source import fake_aligo as mkfakeadvLIGOsrc
+from .source import fake_avirgo as mkfakeadvvirgosrc
+from .sink import fake as mkfakesink
+from .source import fake as mkfakesrc
+from .sink import file as mkfilesink
+from .transform import fir_bank as mkfirbank
+from .filters import fir as mkfirfilter
+from .encode import flac as mkflacenc
+from .mux import framecpp_channel_demux as mkframecppchanneldemux
+from .mux import framecpp_channel_mux as mkframecppchannelmux
+from .sink import gwf as mkframecppfilesink
+from .source import framexmit as mkframexmitsrc
+from .filters import gate as mkgate
+from .pipetools import make_element_with_src as mkgeneric
+from .plot import histogram as mkhistogram
+from .encode import igwd_parse as mkigwdparse
+from .filters import iir as mkiirfilter
+from .filters import inject as mkinjections
+from .transform import interpolator as mkinterpolator
+from .trigger import itac as mkitac
+from .source import cache as mklalcachesrc
+from .transform import latency as mklatency
+from .transform import lho_coherent_null as mklhocoherentnull
+from .source import lvshm as mklvshmsrc
+from .transform import matrix_mixer as mkmatrixmixer
+from .transform import mean as mkmean
+from .sink import multi_file as mkmultifilesink
+from .transform import multiplier as mkmultiplier
+from .source import nds as mkndssrc
+from .filters import remove_fake_disconts as mknofakedisconts
+from .sink import tsv as mknxydumpsink
+from .sink import tsv_tee as mknxydumpsinktee
+from .transform import mkodctodqv as mkodctodqv
+from .mux import ogg_mux as mkoggmux
+from .sink import ogm_video as mkogmvideosink
+from .transform import peak as mkpeak
+from .sink import playback as mkplaybacksink
+from .transform import pow as mkpow
+from .transform import progress_report as mkprogressreport
+from .transform import queue as mkqueue
+from .transform import reblock as mkreblock
+from .transform import resample as mkresample
+from .source import segment as mksegmentsrc
+from .transform import shift as mkshift
+from .plot import spectrum as mkspectrumplot
+from .filters import state_vector as mkstatevector
+from .transform import sum_squares as mksumsquares
+from .transform import tag_inject as mktaginject
+from .sink import tcp_server as mktcpserversink
+from .transform import td_whiten as mktdwhiten
+from .transform import tee as mktee
+from .encode import theora as mktheoraenc
+from .transform import toggle_complex as mktogglecomplex
+from .trigger import trigger as mktrigger
+from .trigger import trigger_gen as mktriggergen
+from .sink import trigger_xml_writer as mktriggerxmlwritersink
+from .transform import trim as mktrim
+from .encode import uri_decode_bin as mkuridecodebin
+from .sink import auto_video as mkvideosink
+from .encode import vorbis as mkvorbisenc
+from .encode import wav as mkwavenc
+from .transform import whiten as mkwhiten
+from .source import SrcDeferredLink as src_deferred_link
+from .pipedot import to_file as write_dump_dot
diff --git a/gstlal/python/pipeparts/encode.py b/gstlal/python/pipeparts/encode.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd2cec8dd64de8ca78c6f85e2900813989022f7c
--- /dev/null
+++ b/gstlal/python/pipeparts/encode.py
@@ -0,0 +1,130 @@
+"""Module for encoding elements
+
+"""
+
+from gstlal.pipeparts import pipetools
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-wavenc.html">wavenc</a> element to a pipeline with useful default properties
+def wav(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Format source into the wav format
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+
+	References:
+		[1] wavenc docs: https://gstreamer.freedesktop.org/documentation/wavenc/index.html?gi-language=python
+
+	Returns:
+		Element, the src encoded as wav format
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "wavenc")
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-vorbisenc.html">vorbisenc</a> element to a pipeline with useful default properties
+def vorbis(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""This element encodes raw float audio into a Vorbis stream
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] vorbisenc docs: https://gstreamer.freedesktop.org/documentation/vorbis/vorbisenc.html?gi-language=python
+
+	Returns:
+		Element, the source encoded as a Vorbis stream
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "vorbisenc")
+
+
+def theora(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element:
+	"""This element encodes raw video into a Theora stream
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	References:
+		[1] theoraenc docs: https://gstreamer.freedesktop.org/documentation/theora/theoraenc.html?gi-language=python
+
+	Returns:
+		Element, the source encoded as a Theora stream
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "theoraenc", **properties)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-flacenc.html">flacenc</a> element to a pipeline with useful default properties
+def flac(pipeline: pipetools.Pipeline, src: pipetools.Element, quality=0, **properties):
+	"""Encoded sources as FLAC streams. FLAC is a Free Lossless Audio Codec.
+	FLAC audio can directly be written into a file, or embedded into containers such as oggmux or matroskamux.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		quality:
+			int
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/flac/flacenc.html?gi-language=python
+
+	Returns:
+		Element, the source encoded as a FLAC stream
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "flacenc", quality=quality, **properties)
+
+
+def igwd_parse(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Parse byte streams into whole IGWD frame files (https://dcc.ligo.org/cgi-bin/DocDB/ShowDocument?docid=329)
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		Implementation gstlal/gstlal-ugly/gst/framecpp/framecpp_igwdparse.cc
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "framecpp_igwdparse", **properties)
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-uridecodebin.html">uridecodebin</a> element to a pipeline with useful default properties
+def uri_decode_bin(pipeline: pipetools.Pipeline, uri: str, caps: pipetools.Caps = "application/x-igwd-frame,framed=true", **properties) -> pipetools.Element:
+	"""Decodes data from a URI into raw media. It selects a source element that can handle the
+	given scheme and connects it to a decodebin.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		uri:
+			str, URI to decode
+		caps:
+			Gst.Caps, The caps on which to stop decoding. (NULL = default)
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/playback/uridecodebin.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, None, "uridecodebin", uri=uri, caps=None if caps is None else pipetools.Gst.Caps.from_string(caps), **properties)
diff --git a/gstlal/python/pipeparts/filters.py b/gstlal/python/pipeparts/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f4ba774052776e9f88ade45ff215ddeae44b941
--- /dev/null
+++ b/gstlal/python/pipeparts/filters.py
@@ -0,0 +1,297 @@
+"""Module for filter elements
+
+"""
+from typing import Union
+
+import gi
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiochebband.html">audiochebband</a> element to a pipeline with useful default properties
+def audio_cheb_band(pipeline: pipetools.Pipeline, src: pipetools.Element, lower_frequency: float, upper_frequency: float, poles: int = 8) -> pipetools.Element:
+	"""Attenuates all frequencies outside (bandpass) or inside (bandreject) of a frequency band. The number
+	of poles and the ripple parameter control the rolloff. This element has the advantage over the windowed
+	sinc bandpass and bandreject filter that it is much faster and produces almost as good results. It's only
+	disadvantages are the highly non-linear phase and the slower rolloff compared to a windowed sinc filter
+	with a large kernel. For type 1 the ripple parameter specifies how much ripple in dB is allowed in the
+	passband, i.e. some frequencies in the passband will be amplified by that value. A higher ripple value
+	will allow a faster rolloff. For type 2 the ripple parameter specifies the stopband attenuation. In the
+	stopband the gain will be at most this value. A lower ripple value will allow a faster rolloff. As a
+	special case, a Chebyshev type 1 filter with no ripple is a Butterworth filter.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		lower_frequency:
+			float, Start frequency of the band (Hz)
+		upper_frequency:
+			float, Stop frequency of the band (Hz)
+		poles:
+			int, Number of poles to use, will be rounded up to the next multiple of four
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiofx/audiochebband.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "audiochebband", lower_frequency=lower_frequency, upper_frequency=upper_frequency, poles=poles)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiocheblimit.html">audiocheblimit</a> element to a pipeline with useful default properties
+def audio_cheb_limit(pipeline: pipetools.Pipeline, src: pipetools.Element, cutoff: float, mode: int = 0, poles: int = 8, type: int = 1, ripple: float = 0.25) -> pipetools.Element:
+	"""Attenuates all frequencies above the cutoff frequency (low-pass) or all frequencies below the cutoff frequency (high-pass).
+	The number of poles and the ripple parameter control the rolloff. This element has the advantage over the windowed sinc lowpass
+	and highpass filter that it is much faster and produces almost as good results. It's only disadvantages are the highly non-linear
+	phase and the slower rolloff compared to a windowed sinc filter with a large kernel. For type 1 the ripple parameter specifies
+	how much ripple in dB is allowed in the passband, i.e. some frequencies in the passband will be amplified by that value. A higher
+	ripple value will allow a faster rolloff. For type 2 the ripple parameter specifies the stopband attenuation. In the stopband the
+	gain will be at most this value. A lower ripple value will allow a faster rolloff. As a special case, a Chebyshev type 1 filter
+	with no ripple is a Butterworth filter.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		cutoff:
+			float, Cut off frequency (Hz)
+		mode:
+			int, default 0, 0 for low-pass or 1 for high-pass
+		poles:
+			int, Number of poles to use, will be rounded up to the next multiple of four
+		type:
+			int, default 1, Type of the chebychev filter
+		ripple:
+			float, default 0,25, Amount of ripple (dB)
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiofx/audiocheblimit.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "audiocheblimit", cutoff=cutoff, mode=mode, poles=poles, type=type, ripple=ripple)
+
+
+## Adds a <a href="@gstdoc/gstreamer-plugins-capsfilter.html">capsfilter</a> element to a pipeline with useful default properties
+def caps(pipeline: pipetools.Pipeline, src: pipetools.Element, caps: Union[str, pipetools.Caps], **properties: dict) -> pipetools.Element:
+	"""The element does not modify data as such, but can enforce limitations on the data format.
+	Note: this element does *not* act as a filter on the data of the source, but rather as a filter on the
+	metadata (CAPS) of the source element.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		caps:
+			str or Gst.Caps, the capabilities specification to limit the source data format
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	References:
+		[1] capsfilter docs: https://gstreamer.freedesktop.org/documentation/coreelements/capsfilter.html?gi-language=python
+
+	Returns:
+		Element, the source element limited by the given caps (capabilities)
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "capsfilter", caps=gsttools.to_caps(caps), **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties
+def drop(pipeline: pipetools.Pipeline, src: pipetools.Element, drop_samples: int = 0) -> pipetools.Element:
+	"""Drop samples from the start of a stream
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		srcs:
+			Iterable[Gst.Element], the source elements
+		drop_samples:
+			int, default 0, number of samples to drop from the beginning of a stream
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_drop.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_drop", drop_samples=drop_samples)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audiofirfilter.html">audiofirfilter</a> element to a pipeline with useful default properties
+def fir(pipeline: pipetools.Pipeline, src: pipetools.Element, kernel: pipetools.GValueArray, latency, **properties: dict) -> pipetools.Element:
+	"""Generic audio FIR filter. Before usage the "kernel" property has to be set to the filter kernel that should be
+	used and the "latency" property has to be set to the latency (in samples) that is introduced by the filter kernel.
+	Setting a latency of n samples will lead to the first n samples being dropped from the output and n samples added
+	to the end.
+
+	The filter kernel describes the impulse response of the filter. To calculate the frequency response of the filter
+	you have to calculate the Fourier Transform of the impulse response.
+
+	To change the filter kernel whenever the sampling rate changes the "rate-changed" signal can be used. This should
+	be done for most FIR filters as they're depending on the sampling rate.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		kernel:
+			Gst.GValueArray, filter kernel for the FIR filter
+		latency:
+			int, filter latency in samples
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	References:
+		[1] audiofirfilter docs: https://gstreamer.freedesktop.org/documentation/audiofx/audiofirfilter.html?gi-language=python
+
+	Returns:
+		Element, the FIR element
+	"""
+	properties.update((name, val) for name, val in (("kernel", kernel), ("latency", latency)) if val is not None)
+	return pipetools.make_element_with_src(pipeline, src, "audiofirfilter", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALGate.html">lal_gate</a> element to a pipeline with useful default properties
+def gate(pipeline: pipetools.Pipeline, src: pipetools.Element, threshold: float = None, control: pipetools.Element = None, **properties) -> pipetools.Element:
+	"""Flag buffers as gaps based on the value of a control input
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		threshold:
+			float, default None, uutput will be flagged as non-gap when magnitude of control input is >= this value.  See also invert-control.
+		control:
+			Element, optional control element input
+		**properties:
+			emit_signals:
+				bool, Emit start and stop signals (rate-changed is always emited).  The start and stop signals
+				are emited on gap-to-non-gap and non-gap-to-gap transitions in the output stream respectively.
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_gate.c
+
+	Returns:
+		Element
+	"""
+	if threshold is not None:
+		elem = pipetools.make_element_with_src(pipeline, None, "lal_gate", threshold=threshold, **properties)
+	else:
+		elem = pipetools.make_element_with_src(pipeline, None, "lal_gate", **properties)
+	for peer, padname in ((src, "sink"), (control, "control")):
+		if isinstance(peer, Gst.Pad):
+			peer.get_parent_element().link_pads(peer, elem, padname)
+		elif peer is not None:
+			peer.link_pads(None, elem, padname)
+	return elem
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioiirfilter.html">audioiirfilter</a> element to a pipeline with useful default properties
+def iir(pipeline: pipetools.Pipeline, src: pipetools.Element, a: pipetools.ValueArray, b: pipetools.ValueArray) -> pipetools.Element:
+	"""aGeneric audio IIR filter. Before usage the "a" and "b" properties have to be set to the filter coefficients
+	that should be used.
+
+	The filter coefficients describe the numerator and denominator of the transfer function.
+
+	To change the filter coefficients whenever the sampling rate changes the "rate-changed" signal can be used.
+	This should be done for most IIR filters as they're depending on the sampling rate.
+
+		convention is z = \exp(-i 2 \pi f / f_{\rm sampling})
+		H(z) = (\sum_{j=0}^{N} a_j z^{-j}) / (\sum_{j=0}^{N} (-1)^{j} b_j z^{-j})
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		a:
+			ValueArray, Filter coefficients (denominator of transfer function)
+		b:
+			ValueArray, Filter coefficients (numerator of transfer function)
+
+	References:
+		[1] audioiirfilter docs: https://gstreamer.freedesktop.org/documentation/audiofx/audioiirfilter.html?gi-language=python
+
+	Returns:
+		Element, IIR of the sources
+	"""
+	#
+	return pipetools.make_element_with_src(pipeline, src, "audioiirfilter", a=a, b=b)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALNoFakeDisconts.html">lal_nofakedisconts</a> element to a pipeline with useful default properties
+def remove_fake_disconts(pipeline: pipetools.Pipeline, src: pipetools.Element, silent: bool = True) -> pipetools.Element:
+	"""Fix incorrectly-set discontinuity flags
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		srcs:
+			Iterable[Gst.Element], the source elements
+		silent:
+			bool, default True, if True Don't print a message when alterning the flags in a buffer.
+
+	References:
+		Implementation: gstal/gst/lal/gstlal_nofakedisconts.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_nofakedisconts", silent=silent)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALStateVector.html">lal_statevector</a> element to a pipeline with useful default properties
+def state_vector(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Converts a state vector stream into booleans, for example to drive a lal_gate element.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_statevector.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_statevector", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALSimulation.html">lal_simulation</a> element to a pipeline with useful default properties
+def inject(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str) -> pipetools.Element:
+	"""An injection routine calling lalsimulation waveform generators
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		filename:
+			str, path to xml file Name of LIGO Light Weight XML file containing list(s) of software injections
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_simulation.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_simulation", xml_location=filename)
diff --git a/gstlal/python/pipeparts/mux.py b/gstlal/python/pipeparts/mux.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c4043e55ab61bd7af626e26070bd9304c6cdc12
--- /dev/null
+++ b/gstlal/python/pipeparts/mux.py
@@ -0,0 +1,230 @@
+"""Module for multiplexing (mux) and demultiplexing (demux) elements
+
+"""
+
+import gi
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+
+from ligo import segments
+from lal import iterutils
+from lal import LIGOTimeGPS
+from gstlal.pipeparts import pipetools
+
+
+class FrameCPPChannelDemuxSetUnitsHandler(object):
+	def __init__(self, elem, units_dict):
+		"""
+		Connect a handler for the pad-added signal of the
+		framecpp_channeldemux element elem, and when a pad is added
+		to the element if the pad's name appears as a key in the
+		units_dict dictionary that pad's units property will be set
+		to the string value associated with that key in the
+		dictionary.
+
+		Example:
+
+		>>> FrameCPPChannelDemuxSetUnitsHandler(elem, {"H1:LSC-STRAIN": "strain"}) # doctest: +SKIP
+
+		NOTE:  this is a work-around to address the problem that
+		most (all?) frame files do not have units set on their
+		channel data, whereas downstream consumers of the data
+		might require information about the units.  The demuxer
+		provides the units as part of a tag event, and
+		framecpp_channeldemux_set_units() can be used to override
+		the values, thereby correcting absent or incorrect units
+		information.
+		"""
+		self.elem = elem
+		self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, units_dict)
+		assert self.pad_added_handler_id > 0
+
+	@staticmethod
+	def pad_added(element, pad, units_dict):
+		name = pad.get_name()
+		if name in units_dict:
+			pad.set_property("units", units_dict[name])
+
+
+class FrameCPPChannelDemuxCheckSegmentsHandler(object):
+	"""
+	Utility to watch for missing data.  Pad probes are used to collect
+	the times spanned by buffers, these are compared to a segment list
+	defining the intervals of data the stream is required to have.  If
+	any intervals of data are found to have been skipped or if EOS is
+	seen before the end of the segment list then a ValueError exception
+	is raised.
+
+	There are two ways to use this tool.  To directly install a segment
+	list monitor on a single pad use the .set_probe() class method.
+	For elements with dynamic pads, the class can be allowed to
+	automatically add monitors to pads as they become available by
+	using the element's pad-added signal.  In this case initialize an
+	instance of the class with the element and a dictionary of segment
+	lists mapping source pad name to the segment list to check that
+	pad's output against.
+
+	In both cases a jitter parameter sets the maximum size of a skipped
+	segment that will be ignored (for example, to accomodate round-off
+	error in element timestamp computations).  The default is 1 ns.
+	"""
+
+	# FIXME:  this code now has two conflicting mechanisms for removing
+	# probes from pads:  one code path removes probes when pads get to
+	# EOS, while the othe removes a probe each time the pad for the
+	# probe appears a second or subsequent time on an element (and then
+	# re-installs the probe on the new pad).  it's possible that these
+	# two could attempt to remove the same probe twice, which will
+	# cause a crash, although it should not happen in current use
+	# cases.  the fix is to rework the probe tracking mechanism so that
+	# both code paths agree on what probes are installed
+	def __init__(self, elem, seglists, jitter=LIGOTimeGPS(0, 1)):
+		self.jitter = jitter
+		self.probe_handler_ids = {}
+		# make a copy of the segmentlistdict in case the calling
+		# code modifies it
+		self.pad_added_handler_id = elem.connect("pad-added", self.pad_added, seglists.copy())
+		assert self.pad_added_handler_id > 0
+
+	def pad_added(self, element, pad, seglists):
+		name = pad.get_name()
+		if name in self.probe_handler_ids:
+			pad.remove_probe(self.probe_handler_ids.pop(name))
+		if name in seglists:
+			self.probe_handler_ids[name] = self.set_probe(pad, seglists[name], self.jitter)
+			assert self.probe_handler_ids[name] > 0
+
+	@classmethod
+	def set_probe(cls, pad, seglist, jitter=LIGOTimeGPS(0, 1)):
+		# use a copy of the segmentlist so the probe can modify it
+		seglist = segments.segmentlist(seglist)
+		# mutable object to carry data to probe
+		data = [seglist, jitter, None]
+		# install probe, save ID in data
+		probe_id = data[2] = pad.add_probe(Gst.PadProbeType.DATA_DOWNSTREAM, cls.probe, data)
+		return probe_id
+
+	@staticmethod
+	def probe(pad, probeinfo, seg_jitter_id):
+		seglist, jitter, probe_id = seg_jitter_id
+		if probeinfo.type & Gst.PadProbeType.BUFFER:
+			obj = probeinfo.get_buffer()
+			if not obj.mini_object.flags & Gst.BufferFlags.GAP:
+				# remove the current buffer from the data
+				# we're expecting to see
+				seglist -= segments.segmentlist([segments.segment((LIGOTimeGPS(0, obj.pts), LIGOTimeGPS(0, obj.pts + obj.duration)))])
+				# ignore missing data intervals unless
+				# they're bigger than the jitter
+				iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist)
+			# are we still expecting to see something that
+			# precedes the current buffer?
+			preceding = segments.segment((segments.NegInfinity, LIGOTimeGPS(0, obj.pts)))
+			if seglist.intersects_segment(preceding):
+				raise ValueError("%s: detected missing data:  %s" % (pad.get_name(), seglist & segments.segmentlist([preceding])))
+		elif probeinfo.type & Gst.PadProbeType.EVENT_DOWNSTREAM and probeinfo.get_event().type == Gst.EventType.EOS:
+			# detach probe at EOS
+			pad.remove_probe(probe_id)
+			# ignore missing data intervals unless they're
+			# bigger than the jitter
+			iterutils.inplace_filter(lambda seg: abs(seg) > jitter, seglist)
+			if seglist:
+				raise ValueError("%s: at EOS detected missing data: %s" % (pad.get_name(), seglist))
+		return True
+
+
+def framecpp_channel_demux(pipeline, src, **properties):
+	"""Demux src using framecpp
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	References:
+		[1] framecppdemux implementation: gstlal/gstlal-ugly/gst/framecpp/framecpp_channeldemux.cc
+
+	Returns:
+		Element, src demuxed using framecpp
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "framecpp_channeldemux", **properties)
+
+
+def framecpp_channel_mux(pipeline, channel_src_map, units=None, seglists=None, **properties):
+	"""Mux a source using framecpp
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		channel_src_map:
+			dict, mapping a channel -> src element
+		units:
+			str, default None, if given set these units on source
+		seglists:
+			default None, if given create a segments handler for these segments
+		**properties:
+
+	Returns:
+		Element, the muxed sources
+	"""
+	elem = pipetools.make_element_with_src(pipeline, None, "framecpp_channelmux", **properties)
+	if channel_src_map is not None:
+		for channel, src in channel_src_map.items():
+			for srcpad in src.srcpads:
+				# FIXME FIXME FIXME. This should use the pad template from the element.
+				# FIXME once a newer version of some library is available, then we should be able to switch to this
+				# if srcpad.link(elem.get_request_pad(channel)) == Gst.PadLinkReturn.OK
+				# Instead. Right now it fails due to the
+				# underscore in channel names.  When it fails
+				# it fails silently and returns None, which
+				# gives a cryptic error message
+				if srcpad.link(elem.request_pad(Gst.PadTemplate.new(channel, Gst.PadDirection.SINK, Gst.PadPresence.REQUEST, Gst.Caps("ANY")), channel)) == Gst.PadLinkReturn.OK:
+					break
+	if units is not None:
+		FrameCPPChannelDemuxSetUnitsHandler(elem, units)
+	if seglists is not None:
+		FrameCPPChannelDemuxCheckSegmentsHandler(elem, seglists)
+	return elem
+
+
+def ogg_mux(pipeline, src):
+	"""This element merges streams (audio and video) into ogg files.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] oggmux docs: https://gstreamer.freedesktop.org/documentation/ogg/oggmux.html?gi-language=python
+
+	Returns:
+		Element, the source merged as ogg format
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "oggmux")
+
+
+def avi_mux(pipeline, src):
+	"""Muxes raw or compressed audio and/or video streams into an AVI file.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] avimux docs: https://gstreamer.freedesktop.org/documentation/avi/avimux.html?gi-language=python
+
+	Returns:
+		Element, the source merged as avi
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "avimux")
diff --git a/gstlal/python/pipeparts/pipedot.py b/gstlal/python/pipeparts/pipedot.py
new file mode 100644
index 0000000000000000000000000000000000000000..1054c8033fbd47659cadcf59d5a4943525b7f577
--- /dev/null
+++ b/gstlal/python/pipeparts/pipedot.py
@@ -0,0 +1,77 @@
+"""Utilities for converting Gst pipelines into DOT graphs
+"""
+import os
+import pathlib
+import sys
+
+import gi
+
+from gstlal.pipeparts import pipetools
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+
+ENV_VAR_DUMP_DIR = 'GST_DEBUG_DUMP_DOT_DIR'
+
+
+def to_file(pipeline: pipetools.Pipeline, filestem: str, verbose: bool = False, use_str_method: bool = False):
+	"""Write a pipeline out to a dot file. This function needs the environment variable GST_DEBUG_DUMP_DOT_DIR
+	to be set.
+
+	Args:
+		pipeline:
+		filestem:
+			str, name of file (not including extension)
+		verbose:
+			bool, default False, if True a message will be written to stderr.
+		use_str_method:
+			bool, default False, if True use the "to_str" method as an intermediate step. This is enabled due to
+			some bugs in Gst where no files are written out using the direct "debug_bin_to_dot_file".
+
+	Notes:
+		File Output:
+			The filename will be os.path.join($GST_DEBUG_DUMP_DOT_DIR, filestem + ".dot")
+
+	Raises:
+		ValueError:
+			If "GST_DEBUG_DUMP_DOT_DIR" env var not defined
+
+	References:
+		[1] https://lazka.github.io/pgi-docs/Gst-1.0/functions.html#Gst.debug_bin_to_dot_file
+
+	Returns:
+		None
+	"""
+	if ENV_VAR_DUMP_DIR not in os.environ:
+		raise ValueError("cannot write pipeline, environment variable GST_DEBUG_DUMP_DOT_DIR is not set")
+
+	output_path = pathlib.Path(os.environ[ENV_VAR_DUMP_DIR]) / '{}.dot'.format(filestem)
+
+	if use_str_method:
+		dot_str = to_str(pipeline)
+		with open(output_path.as_posix(), 'w') as fid:
+			fid.write(dot_str)
+	else:
+		Gst.debug_bin_to_dot_file(pipeline, Gst.DebugGraphDetails.ALL, filestem)
+	if verbose:
+		print("Wrote pipeline to {}".format(output_path.as_posix()), file=sys.stderr)
+
+
+def to_str(pipeline: pipetools.Pipeline) -> str:
+	"""Convert a pipeline to a DOT-formatted string directly (no out file intermediary)
+
+	Args:
+		pipeline:
+			Pipeline, the pipeline to convert to DOT string
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/gstreamer/debugutils.html?gi-language=python
+
+	Returns:
+		str, the pipeline converted to DOT formatted string
+	"""
+	return Gst.debug_bin_to_dot_data(pipeline, Gst.DebugGraphDetails.ALL)
diff --git a/gstlal/python/pipeparts/pipetools.py b/gstlal/python/pipeparts/pipetools.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e535ffb331021d804e91f3ec1b3df6c7af6359b
--- /dev/null
+++ b/gstlal/python/pipeparts/pipetools.py
@@ -0,0 +1,94 @@
+"""Utilities for constructing pipeline elements
+
+"""
+import functools
+import inspect
+from typing import Union, Optional
+
+import gi
+import ligo.segments
+from lal import LIGOTimeGPS
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+from gstlal import gsttools
+
+GObject.threads_init()
+Gst.init(None)
+
+PROPERTY_BUILTIN_NAMES = ('async')
+
+# The below are used for type annotations
+Pipeline = Gst.Pipeline
+Element = Gst.Element
+Caps = Gst.Caps
+GValueArray = Gst.ValueArray # Verify this type
+ValueArray = GObject.ValueArray
+Segment = ligo.segments.segment
+TimeGPS = LIGOTimeGPS
+
+
+def clean_property_name(name: str):
+	"""Utility function for cleaning a property name, handles two cases:
+
+		1. Underscores to hyphens: property_name -> property-name
+		2. Removes trailing underscores originally used to avoid collisions with builtin: math_ -> math.
+			The specific list of builtin collisions detected is defined in PROPERTY_BUILTIN_NAMES
+
+	Args:
+		name:
+			str, the python property name
+
+	Returns:
+		str, the formatted gstreamer compatible element property name
+	"""
+	if name.endswith('_') and name[:-1] in PROPERTY_BUILTIN_NAMES:
+		return name[:-1]
+	return name.replace('_', '-')
+
+
+def make_element_with_src(pipeline: Gst.Pipeline, src: Union[Gst.Element, Gst.Pad], elem_type_name: str, **properties):
+	"""Create a new element of the type defined by the given element factory. If name is None, then the
+	element will receive a guaranteed unique name, consisting of the element factory name and a number.
+	If name is given, it will be given the name supplied.
+
+	Args:
+		pipeline:
+			Pipeline, the existing pipeline to which the new element will be added
+		src:
+			Element or Pad, the element or pad to use as a source for the new element
+		elem_type_name:
+			str or None, name of new element, or None to automatically create a unique name
+		**properties:
+			dict, keyword arguments to be set as properties on the new element
+
+	References:
+		[1] https://lazka.github.io/pgi-docs/Gst-1.0/classes/ElementFactory.html#Gst.ElementFactory.make
+
+	Raises:
+		RuntimeError:
+			If any problem occurs creating the element.
+
+	Returns:
+		Element, the new element
+	"""
+	# Determine element name
+	name = properties.pop('name') if 'name' in properties else None
+
+	properties = {clean_property_name(key): value for key, value in properties.items()}
+
+	# Make the element
+	elem = gsttools.make_element(elem_type_name, name, **properties)
+
+	# Add element to pipeline
+	pipeline.add(elem)
+
+	# Link src argument as input to element
+	if gsttools.is_pad(src):
+		src.get_parent_element().link_pads(src, elem, None)
+	elif src is not None:
+		src.link(elem)
+
+	return elem
diff --git a/gstlal/python/pipeparts/plot.py b/gstlal/python/pipeparts/plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..983d55d6436363652cb640f0489fe3abc4ad83c2
--- /dev/null
+++ b/gstlal/python/pipeparts/plot.py
@@ -0,0 +1,75 @@
+"""Module for producing plotting elements
+
+"""
+
+from gstlal.pipeparts import pipetools
+
+
+def channelgram(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element:
+	"""Scrolling channel amplitude plot, generates video showing a scrolling plot of channel amplitudes.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	Notes:
+		Available Properties:
+			plot_width:
+				float, default 0.0, width of the plot in seconds. (0 = 1/framerate)
+
+	References:
+		[1] ChannelGram Implementation: gstlal/gstlal/gst/python/lal_channelgram.py
+
+	Returns:
+		Element, the plot element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_channelgram", **properties)
+
+
+def spectrum(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties: dict) -> pipetools.Element:
+	"""Power spectrum plot, generates a video showing a power spectrum (e.g., as measured by lal_whiten).
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+			dict, keyword arguments to be set as element properties
+
+	Notes:
+		Available properties:
+			f_min:
+				float, default 10.0, Lower bound of plot in Hz
+			f_max:
+				float, default 4000.0, Upper bound of plot in Hz
+
+	References:
+		[1] SpectrumPlot Implementation: gstlal/gstlal/gst/python/lal_spectrumplot.py
+
+	Returns:
+		Element, the plot element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_spectrumplot", **properties)
+
+
+def histogram(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Histogram plot, generates a video showing a histogram of the input time series.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] HistogramPlot Implementation: gstlal/gstlal/gst/python/lal_histogramplot.py
+
+	Returns:
+		Element, the plot element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_histogramplot")
diff --git a/gstlal/python/pipeparts/sink.py b/gstlal/python/pipeparts/sink.py
new file mode 100644
index 0000000000000000000000000000000000000000..28d18c28f97a6790f85ebd667ca4c45dfdc5a356
--- /dev/null
+++ b/gstlal/python/pipeparts/sink.py
@@ -0,0 +1,512 @@
+""""Module for producing sink elements
+
+"""
+import math
+import os
+import sys
+import threading
+from typing import Tuple
+
+import numpy
+from lal import LIGOTimeGPS, CacheEntry
+from ligo import segments
+
+import gi
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+
+from gstlal.pipeparts import pipetools, pipedot, mux, encode, filters, transform
+
+BYTE_ORDER = 'LE' if sys.byteorder == "little" else 'BE'
+
+
+def framecpp_filesink_ldas_path_handler(elem: pipetools.Element, pspec, path_digits: Tuple[str, int]):
+	"""Add path for file sink to element
+
+	Args:
+		elem:
+			Element, the element to which to add a filesink path property
+		pspec:
+			Unknown
+		path_digits:
+			Tuple[str, int], a string outpath and a directory digits int
+
+	Examples:
+		>>> filesinkelem.connect("notify::timestamp", framecpp_filesink_ldas_path_handler, (".", 5))
+
+	Returns:
+		Element, with the formatted outpath attached as the "path" property
+	"""
+	outpath, dir_digits = path_digits
+
+	# get timestamp and truncate to integer seconds
+	timestamp = elem.get_property("timestamp") // Gst.SECOND
+
+	# extract leading digits
+	leading_digits = timestamp // 10 ** int(math.log10(timestamp) + 1 - dir_digits)
+
+	# get other metadata
+	instrument = elem.get_property("instrument")
+	frame_type = elem.get_property("frame-type")
+
+	# make target directory, and set path
+	path = os.path.join(outpath, "%s-%s-%d" % (instrument, frame_type, leading_digits))
+	if not os.path.exists(path):
+		os.makedirs(path)
+	elem.set_property("path", path)
+
+
+def framecpp_filesink_cache_entry_from_mfs_message(message):
+	"""Translate an element message posted by the multifilesink element
+	inside a framecpp_filesink bin into a lal.utils.CacheEntry object
+	describing the file being written by the multifilesink element.
+	"""
+	# extract the segment spanned by the file from the message directly
+	start = LIGOTimeGPS(0, message.get_structure()["timestamp"])
+	end = start + LIGOTimeGPS(0, message.get_structure()["duration"])
+
+	# retrieve the framecpp_filesink bin (for instrument/observatory
+	# and frame file type)
+	parent = message.src.get_parent()
+
+	# construct and return a CacheEntry object
+	return CacheEntry(parent.get_property("instrument"), parent.get_property("frame-type"), segments.segment(start, end),
+					  "file://localhost%s" % os.path.abspath(message.get_structure()["filename"]))
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-multifilesink.html">multifilesink</a> element to a pipeline with useful default properties
+def multi_file(pipeline: pipetools.Pipeline, src: pipetools.Element, next_file: int = 0, sync: bool = False, async_: bool = False, **properties) -> pipetools.Element:
+	"""Adds a sink element to a pipeline with useful default properties
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		next_file:
+			int, default 0
+		sync:
+			bool, default False
+		async_:
+			bool, default False
+		**properties:
+
+	Returns:
+		Element
+	"""
+	properties["async"] = async_
+	return pipetools.make_element_with_src(pipeline, src, "multifilesink", next_file=next_file, sync=sync, **properties)
+
+
+def gwf(pipeline: pipetools.Pipeline, src: pipetools.Element, message_forward: bool = True, **properties) -> pipetools.Element:
+	"""Add a framecpp file sink element to pipeline, that will write out a GWF file
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		message_forward:
+			bool, default True
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/framecpp/framecpp_filesink.c
+
+	Returns:
+		Element
+	"""
+	post_messages = properties.pop("post_messages", True)
+	elem = pipetools.make_element_with_src(pipeline, src, "framecpp_filesink", message_forward=message_forward, **properties)
+	# FIXME:  there's supposed to be some sort of proxy mechanism for
+	# setting properties on child elements, but we can't seem to get
+	# anything to work
+	elem.get_by_name("multifilesink").set_property("post-messages", post_messages)
+	return elem
+
+
+## Adds a <a href="@gstdoc/gstreamer-plugins-fakesink.html">fakesink</a> element to a pipeline with useful default properties
+def fake(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Create a fake sink element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "fakesink", sync=False, **{"async": False})
+
+
+## Adds a <a href="@gstdoc/gstreamer-plugins-filesink.html">filesink</a> element to a pipeline with useful default properties
+def file(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str, sync: bool = False, async_: bool = False) -> pipetools.Element:
+	"""Add file sink to pipeline
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		filename:
+			str, the name of the output file
+		sync:
+			bool, default False
+		async_:
+			bool, default False
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "filesink", sync=sync, buffer_mode=2, location=filename, **{"async": async_})
+
+
+## Adds a <a href="@gstlalgtkdoc/GstTSVEnc.html">lal_nxydump</a> element to a pipeline with useful default properties
+def tsv(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str, segment: pipetools.Segment = None) -> pipetools.Element:
+	"""Converts audio time-series to tab-separated ascii text, a format compatible with most plotting utilities.
+ 	The output is multi-column tab-separated ASCII text.  The first column is the time, the remaining columns are
+ 	the values of the channels in order.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		filename:
+			str, the filename of the output text file
+		segment:
+			Segment, default None, a ligo.segments.segment
+
+	Returns:
+		Element
+	"""
+	if segment is not None:
+		elem = pipetools.make_element_with_src(pipeline, src, "lal_nxydump", start_time=segment[0].ns(), stop_time=segment[1].ns())
+	else:
+		elem = pipetools.make_element_with_src(pipeline, src, "lal_nxydump")
+	return file(pipeline, elem, filename)
+
+
+def ogm_video(pipeline: pipetools.Pipeline, videosrc: pipetools.Element, filename: str, audiosrc: pipetools.Element = None, verbose: bool = False):
+	"""Make a ogm video sink element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		videosrc:
+			Gst.Element, the video source element
+		filename:
+			str, the name of the output video file
+		audiosrc:
+			Gst.Element, default None, the audio source element
+		verbose:
+			bool, default False
+
+	Returns:
+		Element, the sink element
+	"""
+	src = transform.colorspace(pipeline, videosrc)
+	src = filters.caps(pipeline, src, "video/x-raw-yuv, format=(fourcc)I420")
+	src = encode.theora(pipeline, src,
+						# border=2,
+						quality=48,
+						# quick=False
+						)
+	src = mux.ogg_mux(pipeline, src)
+	if audiosrc is not None:
+		encode.flac(pipeline, filters.caps(pipeline, transform.audio_convert(pipeline, audiosrc), "audio/x-raw, format=S24%s" % BYTE_ORDER)).link(src)
+	if verbose:
+		src = progress_report(pipeline, src, filename)
+	return file(pipeline, src, filename)
+
+
+def auto_video(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Create a video sink that automatically detects an appropriate video sink to use. It does so by scanning the
+	registry for all elements that have "Sink" and "Video" in the class field of their element information, and
+	also have a non-zero autoplugging rank.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/autodetect/autovideosink.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, transform.colorspace(pipeline, src), "autovideosink")
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-autoaudiosink.html">autoaudiosink</a> element to a pipeline with useful default properties
+def auto_audio(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Create an audio sink that automatically detects an appropriate audio sink to use. It does so by
+	scanning the registry for all elements that have "Sink" and "Audio" in the class field of their element
+	information, and also have a non-zero autoplugging rank.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/autodetect/autoaudiosink.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, transform.queue(pipeline, src), "autoaudiosink")
+
+
+def playback(pipeline: pipetools.Pipeline, src: pipetools.Element, amplification: float = 0.1) -> pipetools.Element:
+	"""Create a playback pipeline and add it to existing pipeline
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		amplification:
+			float, default 0.1
+
+	Returns:
+		Element
+	"""
+	elems = (
+		Gst.ElementFactory.make("audioconvert", None),
+		Gst.ElementFactory.make("capsfilter", None),
+		Gst.ElementFactory.make("audioamplify", None),
+		Gst.ElementFactory.make("audioconvert", None),
+		Gst.ElementFactory.make("queue", None),
+		Gst.ElementFactory.make("autoaudiosink", None)
+	)
+	elems[1].set_property("caps", Gst.Caps.from_string("audio/x-raw, format=F32%s" % BYTE_ORDER))
+	elems[2].set_property("amplification", amplification)
+	elems[4].set_property("max-size-time", 1 * Gst.SECOND)
+	pipeline.add(*elems)
+	return Gst.element_link_many(src, *elems)  # MOD: Error line [733]: element_link_many not yet implemented. See web page **
+
+
+def tsv_tee(pipeline: pipetools.Pipeline, src: pipetools.Element, *args, **properties) -> pipetools.Element:
+	"""Split data from source to an nxy dump
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		*args:
+		**properties:
+
+	Returns:
+		Element
+	"""
+	t = transform.tee(pipeline, src)
+	tsv(pipeline, transform.queue(pipeline, t), *args, **properties)
+	return t
+
+
+def trigger_xml_writer(pipeline: pipetools.Pipeline, src: pipetools.Element, filename: str):
+	"""Write xml file
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		filename:
+			str, output path for xml file
+
+	References:
+		Implementation
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_triggerxmlwriter", location=filename, sync=False, **{"async": False})
+
+
+# FIXME no specific alias for this url since this library only has one element.
+# DO NOT DOCUMENT OTHER CODES THIS WAY! Use @gstdoc @gstpluginsbasedoc etc.
+## Adds a <a href="http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-libs/html/gstreamer-app.html">appsink</a> element to a pipeline with useful default properties
+def app(pipeline: pipetools.Pipeline, src: pipetools.Element, max_buffers: int = 1, drop: bool = False, sync: bool = False, async_: bool = False, **properties):
+	"""Create an app sink, Appsink is a sink plugin that supports many different methods for making the
+	application get a handle on the GStreamer data in a pipeline. Unlike most GStreamer elements,
+	Appsink provides external API functions.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		max_buffers:
+			int, default 1
+		drop:
+			bool, default False
+		sync:
+			bool, default False
+		async_:
+			bool, default False
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/app/appsink.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	properties["async"] = async_
+	return pipetools.make_element_with_src(pipeline, src, "appsink", sync=sync, emit_signals=True, max_buffers=max_buffers, drop=drop, **properties)
+
+
+class AppSync(object):
+	def __init__(self, appsink_new_buffer, appsinks=[]):
+		self.lock = threading.Lock()
+		# handler to invoke on availability of new time-ordered
+		# buffer
+		self.appsink_new_buffer = appsink_new_buffer
+		# element --> timestamp of current buffer or None if no
+		# buffer yet available
+		self.appsinks = {}
+		# set of sink elements that are currently at EOS
+		self.at_eos = set()
+		# attach handlers to appsink elements provided at this time
+		for elem in appsinks:
+			self.attach(elem)
+
+	def add_sink(self, pipeline, src, drop=False, **properties):
+		return self.attach(app(pipeline, src, drop=drop, **properties))
+
+	def attach(self, appsink):
+		"""
+		connect this AppSync's signal handlers to the given appsink
+		element.  the element's max-buffers property will be set to
+		1 (required for AppSync to work).
+		"""
+		if appsink in self.appsinks:
+			raise ValueError("duplicate appsinks %s" % repr(appsink))
+		appsink.set_property("max-buffers", 1)
+		handler_id = appsink.connect("new-preroll", self.new_preroll_handler)
+		assert handler_id > 0
+		handler_id = appsink.connect("new-sample", self.new_sample_handler)
+		assert handler_id > 0
+		handler_id = appsink.connect("eos", self.eos_handler)
+		assert handler_id > 0
+		self.appsinks[appsink] = None
+		return appsink
+
+	def new_preroll_handler(self, elem):
+		with self.lock:
+			# clear eos status
+			self.at_eos.discard(elem)
+			# ignore preroll buffers
+			elem.emit("pull-preroll")
+			return Gst.FlowReturn.OK
+
+	def new_sample_handler(self, elem):
+		with self.lock:
+			# clear eos status, and retrieve buffer timestamp
+			self.at_eos.discard(elem)
+			assert self.appsinks[elem] is None
+			self.appsinks[elem] = elem.get_last_sample().get_buffer().pts
+			# pull available buffers from appsink elements
+			return self.pull_buffers(elem)
+
+	def eos_handler(self, elem):
+		with self.lock:
+			# set eos status
+			self.at_eos.add(elem)
+			# pull available buffers from appsink elements
+			return self.pull_buffers(elem)
+
+	def pull_buffers(self, elem):
+		"""
+		for internal use.  must be called with lock held.
+		"""
+		# keep looping while we can process buffers
+		while 1:
+			# retrieve the timestamps of all elements that
+			# aren't at eos and all elements at eos that still
+			# have buffers in them
+			timestamps = [(t, e) for e, t in self.appsinks.items() if e not in self.at_eos or t is not None]
+			# if all elements are at eos and none have buffers,
+			# then we're at eos
+			if not timestamps:
+				return Gst.FlowReturn.EOS
+			# find the element with the oldest timestamp.  None
+			# compares as less than everything, so we'll find
+			# any element (that isn't at eos) that doesn't yet
+			# have a buffer (elements at eos and that are
+			# without buffers aren't in the list)
+			timestamp, elem_with_oldest = min(timestamps, key=lambda x: x[0] if x[0] is not None else -numpy.inf)
+			# if there's an element without a buffer, quit for
+			# now --- we require all non-eos elements to have
+			# buffers before proceding
+			if timestamp is None:
+				return Gst.FlowReturn.OK
+			# clear timestamp and pass element to handler func.
+			# function call is done last so that all of our
+			# book-keeping has been taken care of in case an
+			# exception gets raised
+			self.appsinks[elem_with_oldest] = None
+			self.appsink_new_buffer(elem_with_oldest)
+
+
+class ConnectAppsinkDumpDot(object):
+	"""Add a signal handler to write a pipeline graph upon receipt of the
+	first trigger buffer.  the caps in the pipeline graph are not fully
+	negotiated until data comes out the end, so this version of the graph
+	shows the final formats on all links
+	"""
+
+	def __init__(self, pipeline, appsinks, basename, verbose=False):
+		self.pipeline = pipeline
+		self.filestem = "%s.%s" % (basename, "TRIGGERS")
+		self.verbose = verbose
+		# map element to handler ID
+		self.remaining_lock = threading.Lock()
+		self.remaining = {}
+		for sink in appsinks:
+			self.remaining[sink] = sink.connect_after("new-preroll", self.execute)
+			assert self.remaining[sink] > 0
+
+	def execute(self, elem):
+		with self.remaining_lock:
+			handler_id = self.remaining.pop(elem)
+			if not self.remaining:
+				pipedot.write_dump_dot(self.pipeline, self.filestem, verbose=self.verbose)
+		elem.disconnect(handler_id)
+		return Gst.FlowReturn.OK
+
+
+def tcp_server(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Create a sink via TCP server
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/tcp/tcpserversink.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	# units_soft_max = 1 GB
+	# FIXME:  are these sensible defaults?
+	return pipetools.make_element_with_src(pipeline, src, "tcpserversink", sync=True, sync_method="latest-keyframe", recover_policy="keyframe", unit_type="bytes",
+										   units_soft_max=1024 ** 3,
+										   **properties)
diff --git a/gstlal/python/pipeparts/source.py b/gstlal/python/pipeparts/source.py
new file mode 100644
index 0000000000000000000000000000000000000000..71cf70cc334e0cb836288bc91c50786bec97c7ca
--- /dev/null
+++ b/gstlal/python/pipeparts/source.py
@@ -0,0 +1,357 @@
+"""Module for producing source elements
+
+"""
+import sys
+from typing import List, Tuple
+
+from ligo import segments
+
+from gstlal.pipeparts import pipetools, transform, filters
+
+BYTE_ORDER = 'LE' if sys.byteorder == "little" else 'BE'
+
+
+class AudioTestWaveform:
+	"""Enumeration of test waveforms
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiotestsrc/index.html?gi-language=python#GstAudioTestSrcWave
+	"""
+	Sine = 0
+	Square = 1
+	Saw = 2
+	Triangle = 3
+	Silence = 4
+	WhiteNoise = 5
+	PinkNoise = 6
+	SineTable = 7
+	Ticks = 8
+	GaussianNoise = 9
+	RedNoise = 10
+	BlueNoise = 11
+	VioletNoise = 12
+
+
+class NDSChannelType:
+	"""Enumeration of NDS channel types
+
+	References:
+		Implementation: gstlal-ugly/gst/nds/ndssrc.c
+	"""
+	Unknown = 'unknown'
+	Online = 'online'
+	Raw = 'raw'
+	Reduced = 'reduced'
+	SecondTrend = 's-trend'
+	MinuteTrend = 'm-trend'
+	TestPoint = 'test-pt'
+
+
+class SrcDeferredLink(object):
+	"""A class that manages the task of watching for and connecting to new
+	source pads by name.  The inputs are an element, the name of the
+	source pad to watch for on that element, and the sink pad (on a
+	different element) to which the source pad should be linked when it
+	appears.
+
+	The "pad-added" signal of the element will be used to watch for new
+	pads, and if the "no-more-pads" signal is emitted by the element
+	before the requested pad has appeared ValueError is raised.
+	"""
+
+	def __init__(self, element, srcpadname, sinkpad):
+		no_more_pads_handler_id = element.connect("no-more-pads", self.no_more_pads, srcpadname)
+		assert no_more_pads_handler_id > 0
+		pad_added_data = [srcpadname, sinkpad, no_more_pads_handler_id]
+		pad_added_handler_id = element.connect("pad-added", self.pad_added, pad_added_data)
+		assert pad_added_handler_id > 0
+		pad_added_data.append(pad_added_handler_id)
+
+	@staticmethod
+	def pad_added(element, pad, src_sink_ids):
+		srcpadname, sinkpad, no_more_pads_handler_id, pad_added_handler_id = src_sink_ids
+		if pad.get_name() == srcpadname:
+			element.handler_disconnect(no_more_pads_handler_id)
+			element.handler_disconnect(pad_added_handler_id)
+			pad.link(sinkpad)
+
+	@staticmethod
+	def no_more_pads(element, srcpadname):
+		raise ValueError("<%s>: no pad named '%s'" % (element.get_name(), srcpadname))
+
+
+def fake_ligo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element:
+	"""Fake LIGO Source
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		instrument:
+			str, default None
+		channel_name:
+			str, default None
+		blocksize:
+			int, default 16384 * 8 * 1, Number of samples in each outgoing buffer
+
+	References:
+		Implementation gstlal/gst/python/lal_fakeligosrc.py
+
+	Returns:
+		Element
+	"""
+	properties = {"blocksize": blocksize}
+	properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None)
+	return pipetools.make_element_with_src(pipeline, None, "lal_fakeligosrc", **properties)
+
+
+def fake_aligo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element:
+	"""Fake Advanced LIGO Source
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		instrument:
+			str, default None
+		channel_name:
+			str, default None
+		blocksize:
+			int, default 16384 * 8 * 1, Number of samples in each outgoing buffer
+
+	References:
+		Implementation gstlal/gst/python/lal_fakeadvligosrc.py
+
+	Returns:
+		Element
+	"""
+	properties = {"blocksize": blocksize}
+	properties.update((name, val) for name, val in (("instrument", instrument), ("channel_name", channel_name)) if val is not None)
+	return pipetools.make_element_with_src(pipeline, None, "lal_fakeadvligosrc", **properties)
+
+
+def fake_avirgo(pipeline: pipetools.Pipeline, instrument: str = None, channel_name: str = None, blocksize: int = 16384 * 8 * 1) -> pipetools.Element:
+	"""Fake Advanced Virgo Source
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		instrument:
+			str, default None
+		channel_name:
+			str, default None
+		blocksize:
+			int, default 16384 * 8 * 1, Number of samples in each outgoing buffer
+
+	References:
+		Implementation gstlal/gst/python/lal_fakeadvvirgosrc.py
+
+	Returns:
+		Element
+	"""
+	properties = {"blocksize": blocksize}
+	if instrument is not None:
+		properties["instrument"] = instrument
+	if channel_name is not None:
+		properties["channel_name"] = channel_name
+	return pipetools.make_element_with_src(pipeline, None, "lal_fakeadvvirgosrc", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALSegmentSrc.html">lal_segmentsrc</a> element to a pipeline with useful default properties
+def segment(pipeline: pipetools.Pipeline, segment_list: List[Tuple[pipetools.TimeGPS, pipetools.TimeGPS]], blocksize: int = 4096 * 1 * 1,
+			invert_output: bool = False) -> pipetools.Element:
+	"""The output is a buffer of boolean values specifying when a list of segments are on and off.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		segment_list:
+			Iterable[Tuple[TimeGPS, TimeGPS]], list of segment start / stop times
+		blocksize:
+			int, default blocksize is 4096 seconds of unsigned integers at 1 Hz, e.g. segments without nanoseconds
+		invert_output:
+			bool, default False, False = output is high in segments (default), True = output is low in segments
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_segmentsrc.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, None, "lal_segmentsrc", blocksize=blocksize,
+										   segment_list=segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list),
+										   invert_output=invert_output)
+
+
+## Adds a <a href="@gstlalgtkdoc/GstLALCacheSrc.html">lal_cachesrc</a> element to a pipeline with useful default properties
+def cache(pipeline: pipetools.Pipeline, location: str, use_mmap: bool = True, **properties) -> pipetools.Element:
+	"""Retrieve frame files from locations recorded in a LAL cache file.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		location:
+			str, Path to LAL cache file.
+		use_mmap:
+			bool, default True, if True Use mmap() instead of read()
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_cachesrc.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, None, "lal_cachesrc", location=location, use_mmap=use_mmap, **properties)
+
+
+def lvshm(pipeline: pipetools.Pipeline, shm_name: str, **properties) -> pipetools.Element:
+	"""LIGO-Virgo shared memory frame file source element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		shm_name:
+			str, Shared memory partition name.  Suggestions:  LHO_Data, LLO_Data, VIRGO_Data
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/gds/lvshmsrc.cc
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, None, "gds_lvshmsrc", shm_name=shm_name, **properties)
+
+
+def framexmit(pipeline: pipetools.Pipeline, multicast_group: str = '0.0.0.0', port: int = 0, **properties) -> pipetools.Element:
+	"""FrameXMIT based source element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		multicast_group:
+			str, default "0.0.0.0", The address of multicast group to join.  If no multicast address is supplied, the receiver will listen for
+			UDP/IP broadcast transmissions at the specified port.
+		port:
+			int, default 0, The local port on which to receive broadcasts (0 = allocate).  These ports can be reused by multiple applications.
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/gds/framexmitsrc.cc
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, None, "gds_framexmitsrc", multicast_group=multicast_group, port=port, **properties)
+
+
+def nds(pipeline: pipetools.Pipeline, host: str, instrument: str, channel_name: str, channel_type: str, blocksize: int = 16384 * 8 * 1, port: int = 31200) -> pipetools.Element:
+	"""NDS-based src element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		host:
+			str, NDS1 or NDS2 remote host name or IP address
+		instrument:
+			str, name of instrument
+		channel_name:
+			str, Name of the desired NDS channel.
+		channel_type:
+			str, Type of the desired NDS channel.
+		blocksize:
+			int, default 16384 * 8 * 1, blocksize
+		port:
+			int, NDS1 or NDS2 remote host port
+
+	References:
+		Implementation: gstlal-ugly/gst/nds/ndssrc.c
+
+	Returns:
+		Element
+	"""
+	# default blocksize is 1 second of double precision floats at
+	# 16384 Hz, e.g., LIGO h(t)
+	return pipetools.make_element_with_src(pipeline, None, "ndssrc", blocksize=blocksize, port=port, host=host, channel_name="%s:%s" % (instrument, channel_name),
+										   channel_type=channel_type)
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiotestsrc.html">audiotestsrc</a> element to a pipeline with useful default properties
+def audio_test(pipeline: pipetools.Pipeline, freq: float = 440, volume: float = 0.8, wave: int = AudioTestWaveform.Sine, samples_per_buffer: int = 1024,
+			   **properties) -> pipetools.Element:
+	"""AudioTestSrc can be used to generate basic audio signals. It support several different waveforms and
+	allows to set the base frequency and volume. Some waveforms might use additional properties.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		freq:
+			float, Frequency of test signal. The sample rate needs to be at least 2 times higher.
+		volume:
+			float, default 0.8, Volume of test signal
+		wave:
+			int, default 0, the type of waveform to produce. Options are:
+				sine (0) – Sine
+				square (1) – Square
+				saw (2) – Saw
+				triangle (3) – Triangle
+				silence (4) – Silence
+				white-noise (5) – White uniform noise
+				pink-noise (6) – Pink noise
+				sine-table (7) – Sine table
+				ticks (8) – Periodic Ticks
+				gaussian-noise (9) – White Gaussian noise
+				red-noise (10) – Red (brownian) noise
+				blue-noise (11) – Blue noise
+				violet-noise (12) – Violet noise
+		samples_per_buffer:
+			int, default 1024, Number of samples in each outgoing buffer. Must be at least twice 'freq'
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiotestsrc/index.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	if 'samplesperbuffer' in properties: # support legacy argument name
+		samples_per_buffer = properties.pop('samplesperbuffer')
+	return pipetools.make_element_with_src(pipeline, None, "audiotestsrc", freq=freq, volume=volume, wave=wave,
+										   samplesperbuffer=samples_per_buffer, **properties)
+
+
+## see documentation for mktaginject() mkcapsfilter() and mkaudiotestsrc()
+def fake(pipeline: pipetools.Pipeline, instrument: str, channel_name: str, blocksize: int = None, volume: float = 1e-20,
+		 is_live: bool = False, wave: int = AudioTestWaveform.GaussianNoise, rate: int = 16384, **properties) -> pipetools.Element:
+	"""Create an audio_test source with several additional, lal-specific caps specified
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		instrument:
+			str, name of instrument
+		channel_name:
+			str, name of input channel
+		blocksize:
+			int, default 1 second * rate samples/second * 8
+		volume:
+			float, default 1e-20, the sample volume
+		is_live:
+			bool, default False, whether or not audio_test source will behave like live source
+		wave:
+			int, default 9 (Gaussian Noise), see AudioTestWaveform enum for options
+		rate:
+			int, default 16384, sample rate
+		**properties:
+
+	Returns:
+		Element
+	"""
+	if blocksize is None:
+		# default blocksize is 1 second * rate samples/second * 8
+		# bytes/sample (assume double-precision floats)
+		blocksize = 1 * rate * 8
+	caps = filters.caps(pipeline,
+						audio_test(pipeline, samples_per_buffer=int(blocksize / 8), wave=wave,
+								   volume=volume, is_live=is_live, **properties),
+						"audio/x-raw, format=F64%s, rate=%d".format(BYTE_ORDER, rate))
+	return transform.tag_inject(pipeline, caps, "instrument=%s,channel-name=%s,units=strain".format(instrument, channel_name))
diff --git a/gstlal/python/pipeparts/transform.py b/gstlal/python/pipeparts/transform.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a88c537a2f5e0d0d1805726ac8f66e67c3a1a5e
--- /dev/null
+++ b/gstlal/python/pipeparts/transform.py
@@ -0,0 +1,1035 @@
+"""Module for general transformation elements
+
+"""
+from typing import Iterable, Tuple, List
+
+import gi
+import numpy
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject
+from gi.repository import Gst
+
+GObject.threads_init()
+Gst.init(None)
+from ligo import segments
+from gstlal import pipeio
+from gstlal.pipeparts import pipetools, filters
+
+
+def mean(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Compute mean
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_mean.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_mean", **properties)
+
+
+def abs_(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Compute absolute value
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "abs", **properties)
+
+
+def pow(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Compute power
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "pow", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALSumSquares.html">lal_sumsquares</a> element to a pipeline with useful default properties
+def sum_squares(pipeline: pipetools.Pipeline, src: pipetools.Element, weights: pipetools.ValueArray = None) -> pipetools.Element:
+	"""Computes the weighted sum-of-squares of the input channels.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		weights:
+			ValueArray, default None, Vector of weights to use in sum.  If no vector is provided weights of 1.0 are assumed,
+			otherwise the number of input channels must equal the vector length.  The incoming channels are first multiplied
+			by the weights, then squared, then summed.
+
+	References:
+		Implementation gstlal/gstlal/gst/lal/gstlal_sumsquares.c
+
+	Returns:
+		Element
+	"""
+	if weights is not None:
+		return pipetools.make_element_with_src(pipeline, src, "lal_sumsquares", weights=weights)
+	else:
+		return pipetools.make_element_with_src(pipeline, src, "lal_sumsquares")
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-taginject.html">taginject</a> element to a pipeline with useful default properties
+def tag_inject(pipeline: pipetools.Pipeline, src: pipetools.Element, tags: str) -> pipetools.Element:
+	"""Element that injects new metadata tags, but passes incoming data through unmodified.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		tags:
+			str, List of tags to inject into the target file
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/debug/taginject.html?gi-language=python
+
+	Returns:
+		Element, unmodified data with new tags
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "taginject", tags=tags)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALShift.html">lal_shift</a> element to a pipeline with useful default properties
+def shift(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Adjust segment events by +shift
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_shift.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_shift", **properties)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-audioamplify.html">audioamplify</a> element to a pipeline with useful default properties
+def amplify(pipeline: pipetools.Pipeline, src: pipetools.Element, amplification: float) -> pipetools.Element:
+	"""Amplifies an audio stream by a given factor and allows the selection of different clipping modes. The
+	difference between the clipping modes is best evaluated by testing.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		amplification:
+			float, Factor of amplification
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiofx/audioamplify.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "audioamplify", clipping_method=3, amplification=amplification)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALAudioUnderSample.html">lal_audioundersample</a> element to a pipeline with useful default properties
+def undersample(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Undersamples an audio stream.  Undersampling downsamples by taking every n-th sample, with no antialiasing or
+	low-pass filter.  For data confined to a narrow frequency band, this transformation simultaneously downconverts
+	and downsamples the data (otherwise it does weird things).  This element's output sample rate must be an integer
+	divisor of its input sample rate.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_audioundersample.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_audioundersample")
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioresample.html">audioresample</a> element to a pipeline with useful default properties
+def resample(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Resamples raw audio buffers to different sample rates using a configurable windowing function to enhance quality. By default,
+	the resampler uses a reduced sinc table, with cubic interpolation filling in the gaps. This ensures that the table does not
+	become too big. However, the interpolation increases the CPU usage considerably. As an alternative, a full sinc table can be
+	used. Doing so can drastically reduce CPU usage (4x faster with 44.1 -> 48 kHz conversions for example), at the cost of increased
+	memory consumption, plus the sinc table takes longer to initialize when the element is created. A third mode exists, which uses
+	the full table unless said table would become too large, in which case the interpolated one is used instead.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audioresample/index.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "audioresample", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALInterpolator.html">lal_interpolator</a> element to a pipeline with useful default properties
+def interpolator(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Interpolates multichannel audio data using BLAS
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_interpolator.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_interpolator", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALWhiten.html">lal_whiten</a> element to a pipeline with useful default properties
+def whiten(pipeline: pipetools.Pipeline, src: pipetools.Element, psd_mode: int = 0, zero_pad: int = 0, fft_length: int = 8,
+		   average_samples: int = 64, median_samples: int = 7, **properties) -> pipetools.Element:
+	"""A PSD estimator and time series whitener.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		psd_mode:
+			int, default 0, PSD estimation mode. Options are:
+				"GSTLAL_PSDMODE_RUNNING_AVERAGE", Use running average for PSD
+				"GSTLAL_PSDMODE_FIXED", Use fixed spectrum for PSD
+		zero_pad:
+			int, default 0, Length of the zero-padding to include on both sides of the FFT in seconds
+		fft_length:
+			int, default 8, Total length of the FFT convolution (including zero padding) in seconds
+		average_samples:
+			int, default 64, Number of FFTs to be used in PSD average
+		median_samples:
+			int, default 7, Number of FFTs to be used in PSD median history
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_whiten.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_whiten", psd_mode=psd_mode, zero_pad=zero_pad, fft_length=fft_length, average_samples=average_samples,
+										   median_samples=median_samples,
+										   **properties)
+
+
+## Adds a <a href="@gstdoc/gstreamer-plugins-tee.html">tee</a> element to a pipeline with useful default properties
+def tee(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Split data to multiple pads. Branching the data flow is useful when e.g. capturing a video where the video is shown on
+	the screen and also encoded and written to a file. Another example is playing music and hooking up a visualisation module.
+	One needs to use separate queue elements (or a multiqueue) in each branch to provide separate threads for each branch.
+	Otherwise a blocked dataflow in one branch would stall the other branches.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/coreelements/tee.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "tee")
+
+
+## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "sum" mode mixing.
+def adder(pipeline: pipetools.Pipeline, srcs: Iterable[pipetools.Element], sync: bool = True, mix_mode: str = "sum", **properties) -> pipetools.Element:
+	"""The adder allows to mix several streams into one by adding the data. Mixed data is clamped to the min/max
+	values of the data format. If the element's sync property is TRUE the streams are mixed with the timestamps
+	synchronized.  If the sync property is FALSE (the default, to be compatible with older versions), then the
+	first samples from each stream are added to produce the first sample of the output, the second samples are
+	added to produce the second sample of the output, and so on.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		srcs:
+			Iterable[Gst.Element], the source elements
+		sync:
+			bool, default True, Align the time stamps of input streams
+		mix_mode:
+			str, default 'sum', Algorithm for mixing the input streams, options: "sum", "product"
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstadder.c
+
+	Returns:
+		Element
+	"""
+	elem = pipetools.make_element_with_src(pipeline, None, "lal_adder", sync=sync, mix_mode=mix_mode, **properties)
+	if srcs is not None:
+		for src in srcs:
+			src.link(elem)
+	return elem
+
+
+## Adds a <a href="@gstdoc/GstLALAdder.html">lal_adder</a> element to a pipeline configured for synchronous "product" mode mixing.
+def multiplier(pipeline: pipetools.Pipeline, srcs: Iterable[pipetools.Element], sync: bool = True, mix_mode: str = "product", **properties) -> pipetools.Element:
+	"""Helper function around adder that defaults to a mix mode of "product"
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		srcs:
+			Iterable[Gst.Element], the source elements
+		sync:
+			bool, default True, Align the time stamps of input streams
+		mix_mode:
+			str, default 'product', Algorithm for mixing the input streams, options: "sum", "product"
+		**properties:
+
+	References:
+		Implementation: gstlal/gst/lal/gstadder.c
+
+	Returns:
+		Element
+	"""
+	return adder(pipeline, srcs, sync=sync, mix_mode=mix_mode, **properties)
+
+
+## Adds a <a href="@gstdoc/gstreamer-plugins-queue.html">queue</a> element to a pipeline with useful default properties
+def queue(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Data is queued until one of the limits specified by the , and/or properties has been reached. Any attempt to push
+	more buffers into the queue will block the pushing thread until more space becomes available. The queue will create a
+	new thread on the source pad to decouple the processing on sink and source pad. You can query how many buffers are
+	queued by reading the property. You can track changes by connecting to the notify::current-level-buffers signal (which
+	like all signals will be emitted from the streaming thread). The same applies to the and properties. The default queue
+	size limits are 200 buffers, 10MB of data, or one second worth of data, whichever is reached first. As said earlier, the
+	queue blocks by default when one of the specified maximums (bytes, time, buffers) has been reached. You can set the
+	property to specify that instead of blocking it should leak (drop) new or old buffers. The signal is emitted when the
+	queue has less data than the specified minimum thresholds require (by default: when the queue is empty). The signal is
+	emitted when the queue is filled up. Both signals are emitted from the context of the streaming thread.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/coreelements/queue.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "queue", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALFIRBank.html">lal_firbank</a> element to a pipeline with useful default properties
+def fir_bank(pipeline: pipetools.Pipeline, src: pipetools.Element, latency: int = None, fir_matrix: numpy.ndarray = None, time_domain: bool = None,
+			 block_stride: int = None) -> pipetools.Element:
+	"""Projects a single audio channel onto a bank of FIR filters to produce a multi-channel output
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		latency:
+			int, default None, Filter latency in samples.
+		fir_matrix:
+			numpy.ndarray, default None, Array of impulse response vectors.  Number of vectors (rows) in matrix sets number of output channels. All filters
+			must have the same length.
+		time_domain:
+			bool, default None, Set to true to use time-domain (a.k.a. direct) convolution, set to false to use FFT-based convolution.
+			For long filters FFT-based convolution is usually significantly faster than time-domain convolution but incurs a higher processing
+			latency and requires more RAM.
+		block_stride:
+			int, default None, When using FFT convolutions, this many samples will be produced from each block.  Smaller values decrease latency
+			but increase computational cost.  If very small values are desired, consider using time-domain convolution mode instead.
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_firbank.c
+
+	Returns:
+		Element
+	"""
+	properties = dict((name, value) for name, value in zip(("latency", "fir_matrix", "time_domain", "block_stride"),
+														   (latency, fir_matrix, time_domain, block_stride)) if value is not None)
+	return pipetools.make_element_with_src(pipeline, src, "lal_firbank", **properties)
+
+
+def td_whiten(pipeline: pipetools.Pipeline, src: pipetools.Element, latency: int = None, kernel: numpy.ndarray = None, taper_length: int = None):
+	"""Generic audio FIR filter with custom filter kernel and smooth kernel updates
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		latency:
+			int, default None, Filter latency in samples.
+		kernel:
+			array, default None, The newest kernel.
+		taper_length:
+			int, default None, Number of samples for kernel transition.
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_tdwhiten.c
+
+	Returns:
+		Element
+	"""
+	# a taper length of 1/4 kernel length mimics the default
+	# configuration of the FFT whitener
+	if taper_length is None and kernel is not None:
+		taper_length = len(kernel) // 4
+	properties = dict((name, value) for name, value in zip(("latency", "kernel", "taper_length"),
+														   (latency, kernel, taper_length)) if value is not None)
+	return pipetools.make_element_with_src(pipeline, src, "lal_tdwhiten", **properties)
+
+
+def trim(pipeline: pipetools.Pipeline, src: pipetools.Element, initial_offset: int = None, final_offset: int = None, inverse: bool = None) -> pipetools.Element:
+	"""Pass data only inside a region and mark everything else as gaps. The offsets are media-type specific. For audio
+	buffers, it's the number of samples produced so far. For video buffers, it's generally the frame number. For compressed
+	data, it could be the byte offset in a source or destination file. If inverse=true is set, only data *outside* of the
+	specified region will pass, and data in the inside will be marked as gaps.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		initial_offset:
+			int, default None, Only let data with offset bigger than this value pass.
+		final_offset:
+			int, default None, Only let data with offset smaller than this value pass
+		inverse:
+			bool, default None, If True only data *outside* the region will pass.
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_trim.c
+
+	Returns:
+		Element
+	"""
+	properties = dict((name, value) for name, value in zip(("initial-offset", "final-offset", "inverse"),
+														   (initial_offset, final_offset, inverse)) if value is not None)
+	return pipetools.make_element_with_src(pipeline, src, "lal_trim", **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALReblock.html">lal_reblock</a> element to a pipeline with useful default properties
+def reblock(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Chop audio buffers into smaller pieces to enforce a maximum allowed buffer duration
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+			block_duration:
+				int, Maximum output buffer duration in nanoseconds.  Buffers may be smaller than this.
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_reblock.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_reblock", **properties)
+
+
+def bit_vector_gen(pipeline: pipetools.Pipeline, src: pipetools.Element, bit_vector: int, **properties) -> pipetools.Element:
+	"""Generate a bit vector stream based on the value of a control input
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		bit_vector:
+			int, Value to generate when output is \"on\" (output is 0 otherwise). Only as many
+			low-order bits as are needed by the output word size will be used.
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_bitvectorgen.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_bitvectorgen", bit_vector=bit_vector, **properties)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALMatrixMixer.html">lal_matrixmixer</a> element to a pipeline with useful default properties
+def matrix_mixer(pipeline: pipetools.Pipeline, src: pipetools.Element, matrix: numpy.ndarray = None) -> pipetools.Element:
+	"""A many-to-many mixer
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		matrix:
+			array, Matrix of mixing coefficients. Number of rows in matrix sets number of input channels,
+			number of columns sets number of output channels.
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_matrixmixer.c
+
+	Returns:
+		Element
+	"""
+	if matrix is not None:
+		return pipetools.make_element_with_src(pipeline, src, "lal_matrixmixer", matrix=matrix)
+	else:
+		return pipetools.make_element_with_src(pipeline, src, "lal_matrixmixer")
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALToggleComplex.html">lal_togglecomplex</a> element to a pipeline with useful default properties
+def toggle_complex(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Replace float caps with complex (with half the channels), complex with float (with twice the channels).
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_togglecomplex.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_togglecomplex")
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALAutoChiSq.html">lal_autochisq</a> element to a pipeline with useful default properties
+def auto_chisq(pipeline: pipetools.Pipeline, src: pipetools.Element, autocorrelation_matrix: numpy.ndarray = None, mask_matrix=None,
+			   latency: int = 0, snr_thresh: int = 0) -> pipetools.Element:
+	"""Computes the chisquared time series from a filter's autocorrelation
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		autocorrelation_matrix:
+			array, default None, Array of complex autocorrelation vectors.  Number of vectors (rows) in matrix sets
+			number of channels.  All vectors must have the same length.
+		mask_matrix:
+			array, default None, Array of integer mask vectors.  Matrix must be the same size as the autocorrelation
+			matrix.  Only autocorrelation vector samples corresponding to non-zero samples in these vectors will be
+			used to construct the \\chi^{2} statistic.  If this matrix is not supplied, all autocorrelation samples
+			are used.
+		latency:
+			int, default 0, Filter latency in samples.  Must be in (-autocorrelation length, 0].
+		snr_thresh:
+			float, default 0, SNR Threshold that determines a trigger.
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_autochisq.c
+
+	Returns:
+		Element
+	"""
+	properties = {}
+	if autocorrelation_matrix is not None:
+		properties.update({
+			"autocorrelation_matrix": pipeio.repack_complex_array_to_real(autocorrelation_matrix),
+			"latency": latency,
+			"snr_thresh": snr_thresh
+		})
+	if mask_matrix is not None:
+		properties["autocorrelation_mask_matrix"] = mask_matrix
+	return pipetools.make_element_with_src(pipeline, src, "lal_autochisq", **properties)
+
+
+def colorspace(pipeline: pipetools.Pipeline, src: pipetools.Element) -> pipetools.Element:
+	"""Convert video frames between a great variety of video formats.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+
+	References:
+		Pre gstreamer-1.0 docs: (ffmpegcolorspace) https://www.freedesktop.org/software/gstreamer-sdk/data/docs/2012.5/gst-plugins-base-plugins-0.10/gst-plugins-base-plugins-ffmpegcolorspace.html
+		Post gstreamer-1.0 docs: (videoconvert) https://gstreamer.freedesktop.org/documentation/videoconvert/index.html?gi-language=python
+		Migration: https://gstreamer.freedesktop.org/documentation/application-development/appendix/porting-1-0.html?gi-language=c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "videoconvert")
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audioconvert.html">audioconvert</a> element to a pipeline with useful default properties
+def audio_convert(pipeline: pipetools.Pipeline, src: pipetools.Element, caps_string: str = None) -> pipetools.Element:
+	"""Audioconvert converts raw audio buffers between various possible formats. It supports integer to float conversion,
+	width/depth conversion, signedness and endianness conversion and channel transformations (ie. upmixing and downmixing),
+	as well as dithering and noise-shaping.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		caps_string:
+			str, Caps string
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audioconvert/index.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	elem = pipetools.make_element_with_src(pipeline, src, "audioconvert")
+	if caps_string is not None:
+		elem = filters.caps(pipeline, elem, caps_string)
+	return elem
+
+
+## Adds a <a href="@gstpluginsbasedoc/gst-plugins-base-plugins-audiorate.html">audiorate</a> element to a pipeline with useful default properties
+def audio_rate(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""This element takes an incoming stream of timestamped raw audio frames and produces a perfect stream by
+	inserting or dropping samples as needed. This operation may be of use to link to elements that require or
+	otherwise implicitly assume a perfect stream as they do not store timestamps, but derive this by some means
+	(e.g. bitrate for some AVI cases). The properties , , and can be read to obtain information about number of
+	input samples, output samples, dropped samples (i.e. the number of unused input samples) and inserted samples
+	(i.e. the number of samples added to stream). When the property is set to FALSE, a GObject property notification
+	will be emitted whenever one of the or values changes. This can potentially cause performance degradation. Note
+	that property notification will happen from the streaming thread, so applications should be prepared for this. If
+	the property is non-zero, and an incoming buffer's timestamp deviates less than the property indicates from what
+	would make a 'perfect time', then no samples will be added or dropped. Note that the output is still guaranteed to
+	be a perfect stream, which means that the incoming data is then simply shifted (by less than the indicated tolerance)
+	to a perfect time.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/audiorate/index.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "audiorate", **properties)
+
+
+def deglitch(pipeline: pipetools.Pipeline, src: pipetools.Element, segment_list: List[Tuple[pipetools.TimeGPS, pipetools.TimeGPS]]) -> pipetools.Element:
+	"""Removes glitches based on a segment list.  Must be coalesced.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		segment_list:
+			Iterable[Tuple[TimeGPS, TimeGPS]], list of segment start / stop times
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlaldeglitchfilter.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_deglitcher", segment_list=segments.segmentlist(segments.segment(a.ns(), b.ns()) for a, b in segment_list))
+
+
+def check_timestamps(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str = None, silent: bool = True, timestamp_fuzz: int = 1) -> pipetools.Element:
+	"""Timestamp Checker Pass-Through Element
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		name:
+			str, name of check
+		silent:
+			bool, default True, Only report errors.
+		timestamp_fuzz:
+			int, Number of nanoseconds of timestamp<-->offset discrepancy to accept before reporting it.  Timestamp<-->offset discrepancies
+			of 1/2 a sample or more are always reported.
+
+	References:
+		Implementation: gstlal/gst/python/lal_checktimestamps.py
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_checktimestamps", name=name, silent=silent, timestamp_fuzz=timestamp_fuzz)
+
+
+## Adds a <a href="@gstlalgtkdoc/GSTLALPeak.html">lal_peak</a> element to a pipeline with useful default properties
+def peak(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int) -> pipetools.Element:
+	"""Find peaks in a time series every n samples
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		n:
+			int, number of samples over which to identify peaks
+
+	References:
+		Implementation: gstlal/gst/lal/gstlal_peak.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_peak", n=n)
+
+
+def denoise(pipeline: pipetools.Pipeline, src: pipetools.Element, **properties) -> pipetools.Element:
+	"""Separate out stationary/non-stationary components from signals.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_denoiser.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_denoiser", **properties)
+
+
+def clean(pipeline: pipetools.Pipeline, src: pipetools.Element, threshold: float = 1.0) -> pipetools.Element:
+	"""Helper function for denoise that cleans for a stationary, threshold
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		threshold:
+			float, default 1.0, The threshold in which to allow non-stationary signals in stationary component
+
+	Returns:
+		Element
+	"""
+	return denoise(pipeline, src, stationary=True, threshold=threshold)
+
+
+def latency(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str = None, silent: bool = False) -> pipetools.Element:
+	"""Outputs the current GPS time at time of data flow
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		name:
+			str, name
+		silent:
+			bool, if True run silent
+
+	References:
+		Implementation: gstlal-ugly/gst/lal/gstlal_latency.c
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_latency", name=name, silent=silent)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-capssetter.html">capssetter</a> element to a pipeline with useful default properties
+def set_caps(pipeline: pipetools.Pipeline, src: pipetools.Element, caps: pipetools.Caps, **properties) -> pipetools.Element:
+	"""Sets or merges caps on a stream's buffers. That is, a buffer's caps are updated using (fields of) “caps”. Note that this
+	may contain multiple structures (though not likely recommended), but each of these must be fixed (or will otherwise be rejected).
+
+	If “join” is TRUE, then the incoming caps' mime-type is compared to the mime-type(s) of provided caps and only matching
+	structure(s) are considered for updating.
+
+	If “replace” is TRUE, then any caps update is preceded by clearing existing fields, making provided fields (as a whole)
+	replace incoming ones. Otherwise, no clearing is performed, in which case provided fields are added/merged onto incoming caps
+
+	Although this element might mainly serve as debug helper, it can also practically be used to correct a faulty pixel-aspect-ratio,
+	or to modify a yuv fourcc value to effectively swap chroma components or such alike.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		caps:
+			Gst.Caps, the caps
+		**properties:
+
+	References:
+		[1] https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good/html/gst-plugins-good-plugins-capssetter.html
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "capssetter", caps=Gst.Caps.from_string(caps), **properties)
+
+
+## Adds a <a href="@gstpluginsgooddoc/gst-plugins-good-plugins-progressreport.html">progress_report</a> element to a pipeline with useful default properties
+def progress_report(pipeline: pipetools.Pipeline, src: pipetools.Element, name: str):
+	"""The progressreport element can be put into a pipeline to report progress, which is done by doing upstream
+	duration and position queries in regular (real-time) intervals. Both the interval and the preferred query format
+	can be specified via the and the property.
+
+	Element messages containing a "progress" structure are posted on the bus whenever progress has been queried
+	(since gst-plugins-good 0.10.6 only).
+
+	Since the element was originally designed for debugging purposes, it will by default also print information
+	about the current progress to the terminal. This can be prevented by setting the property to True.
+
+	This element is most useful in transcoding pipelines or other situations where just querying the pipeline might
+	not lead to the wanted result. For progress in TIME format, the element is best placed in a 'raw stream' section
+	of the pipeline (or after any demuxers/decoders/parsers).
+
+	Three more things should be pointed out:
+		First, the element will only query progress when data flow happens. If data flow is stalled for some reason, no
+		progress messages will be posted.
+
+		Second, there are other elements (like qtdemux, for example) that may also post "progress" element messages on the bus.
+		Applications should check the source of any element messages they receive, if needed.
+
+		Third, applications should not take action on receiving notification of progress being 100%, they should only take action
+		when they receive an EOS message (since the progress reported is in reference to an internal point of a pipeline and
+		not the pipeline as a whole).
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		name:
+			str, the name
+
+	References:
+		[1] https://gstreamer.freedesktop.org/documentation/debug/progressreport.html?gi-language=python
+
+	Returns:
+		Element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "progressreport", do_query=False, name=name)
+
+
+# TODO move to calibration specific module
+def lho_coherent_null(pipeline: pipetools.Pipeline, H1src: pipetools.Element, H2src: pipetools.Element, H1_impulse, H1_latency, H2_impulse, H2_latency,
+					  srate: int) -> pipetools.Element:
+	"""LHO Coherent and Null Streams
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		H1src:
+			Element, h1 source
+		H2src:
+			Element, h2 source
+		H1_impulse:
+			impulse response for H1
+		H1_latency:
+			latency for H1
+		H2_impulse:
+			impulse response for H2
+		H2_latency:
+			latency for H2
+		srate:
+			int, block stride for fir bank
+
+	References:
+		Implementation: gstlal/gst/python/lal_lho_coherent_null.py
+
+	Returns:
+		Element
+	"""
+	elem = pipetools.make_element_with_src(pipeline, None, "lal_lho_coherent_null", block_stride=srate, H1_impulse=H1_impulse, H2_impulse=H2_impulse, H1_latency=H1_latency,
+										   H2_latency=H2_latency)
+	for peer, padname in ((H1src, "H1sink"), (H2src, "H2sink")):
+		if isinstance(peer, Gst.Pad):
+			peer.get_parent_element().link_pads(peer, elem, padname)
+		elif peer is not None:
+			peer.link_pads(None, elem, padname)
+	return elem
+
+
+# TODO move to calibration specific module
+def mkcomputegamma(pipeline, dctrl, exc, cos, sin, **properties):
+	"""Compute Gamma
+
+	Args:
+		pipeline:
+		dctrl:
+		exc:
+		cos:
+		sin:
+		**properties:
+
+	References:
+		Implementation: gstlal-calibration/gst/python/lal_compute_gamma.py
+
+	Returns:
+		Element
+	"""
+	elem = pipetools.make_element_with_src(pipeline, None, "lal_compute_gamma", **properties)
+	for peer, padname in ((dctrl, "dctrl_sink"), (exc, "exc_sink"), (cos, "cos"), (sin, "sin")):
+		if isinstance(peer, Gst.Pad):
+			peer.get_parent_element().link_pads(peer, elem, padname)
+		elif peer is not None:
+			peer.link_pads(None, elem, padname)
+	return elem
+
+
+# TODO find source for lal_odc_to_dqv or delete
+def mkodctodqv(pipeline, src, **properties):
+	return pipetools.make_element_with_src(pipeline, src, "lal_odc_to_dqv", **properties)
+
+
+# TODO move this somewhere else, it's not a transform element
+def audioresample_variance_gain(quality: int, num: int, den: int) -> float:
+	"""Calculate the output gain of GStreamer's stock audioresample element.
+
+	The audioresample element has a frequency response of unity "almost" all the
+	way up the Nyquist frequency.  However, for an input of unit variance
+	Gaussian noise, the output will have a variance very slighly less than 1.
+	The return value is the variance that the filter will produce for a given
+	"quality" setting and sample rate.
+
+	@param den The denomenator of the ratio of the input and output sample rates
+	@param num The numerator of the ratio of the input and output sample rates
+	@return The variance of the output signal for unit variance input
+
+	The following example shows how to apply the correction factor using an
+	audioamplify element.
+
+	>>> from gstlal.pipeutil import *
+	>>> from gstlal.pipeparts import audioresample_variance_gain
+	>>> from gstlal import pipeio
+	>>> import numpy
+	>>> nsamples = 2 ** 17
+	>>> num = 2
+	>>> den = 1
+	>>> def handoff_handler(element, buffer, pad, (quality, filt_len, num, den)):
+	...		out_latency = numpy.ceil(float(den) / num * filt_len)
+	...		buf = pipeio.array_from_audio_buffer(buffer).flatten()
+	...		std = numpy.std(buf[out_latency:-out_latency])
+	...		print "quality=%2d, filt_len=%3d, num=%d, den=%d, stdev=%.2f" % (
+	...			quality, filt_len, num, den, std)
+	...
+	>>> for quality in range(11):
+	...		pipeline = Gst.Pipeline()
+	...		correction = 1/numpy.sqrt(audioresample_variance_gain(quality, num, den))
+	...		elems = mkelems_in_bin(pipeline,
+	...			('audiotestsrc', {'wave':'gaussian-noise','volume':1}),
+	...			('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,format=F64LE,rate=%d' % num)}),
+	...			('audioresample', {'quality':quality}),
+	...			('capsfilter', {'caps':Gst.Caps.from_string('audio/x-raw,width=F64LE,rate=%d' % den)}),
+	...			('audioamplify', {'amplification':correction,'clipping-method':'none'}),
+	...			('fakesink', {'signal-handoffs':True, 'num-buffers':1})
+	...		)
+	...		filt_len = elems[2].get_property('filter-length')
+	...		elems[0].set_property('samplesperbuffer', 2 * filt_len + nsamples)
+	...		if elems[-1].connect_after('handoff', handoff_handler, (quality, filt_len, num, den)) < 1:
+	...			raise RuntimeError
+	...		try:
+	...			if pipeline.set_state(Gst.State.PLAYING) is not Gst.State.CHANGE_ASYNC:
+	...				raise RuntimeError
+	...			if not pipeline.get_bus().poll(Gst.MessageType.EOS, -1):
+	...				raise RuntimeError
+	...		finally:
+	...			if pipeline.set_state(Gst.State.NULL) is not Gst.StateChangeReturn.SUCCESS:
+	...				raise RuntimeError
+	...
+	quality= 0, filt_len=  8, num=2, den=1, stdev=1.00
+	quality= 1, filt_len= 16, num=2, den=1, stdev=1.00
+	quality= 2, filt_len= 32, num=2, den=1, stdev=1.00
+	quality= 3, filt_len= 48, num=2, den=1, stdev=1.00
+	quality= 4, filt_len= 64, num=2, den=1, stdev=1.00
+	quality= 5, filt_len= 80, num=2, den=1, stdev=1.00
+	quality= 6, filt_len= 96, num=2, den=1, stdev=1.00
+	quality= 7, filt_len=128, num=2, den=1, stdev=1.00
+	quality= 8, filt_len=160, num=2, den=1, stdev=1.00
+	quality= 9, filt_len=192, num=2, den=1, stdev=1.00
+	quality=10, filt_len=256, num=2, den=1, stdev=1.00
+	"""
+
+	# These constants were measured with 2**22 samples.
+
+	if num > den:  # downsampling
+		return den * (
+			0.7224862140943990596,
+			0.7975021342935247892,
+			0.8547537598970208483,
+			0.8744072146753004704,
+			0.9075294214410336568,
+			0.9101523813406768859,
+			0.9280549396020538744,
+			0.9391809530012216189,
+			0.9539276644089494939,
+			0.9623083437067311285,
+			0.9684700588501590213
+		)[quality] / num
+	elif num < den:  # upsampling
+		return (
+			0.7539740617648067467,
+			0.8270076656536116122,
+			0.8835072979478705291,
+			0.8966758456219333651,
+			0.9253434087537378838,
+			0.9255866674042573239,
+			0.9346487800036394900,
+			0.9415331868209220190,
+			0.9524608799160205752,
+			0.9624372769883490220,
+			0.9704505626409354324
+		)[quality]
+	else:  # no change in sample rate
+		return 1.
diff --git a/gstlal/python/pipeparts/trigger.py b/gstlal/python/pipeparts/trigger.py
new file mode 100644
index 0000000000000000000000000000000000000000..122074019a36fd42b6e972a4b60d08fb39b9c44d
--- /dev/null
+++ b/gstlal/python/pipeparts/trigger.py
@@ -0,0 +1,190 @@
+"""Module for making trigger elements
+
+"""
+import numpy
+
+from gstlal import pipeio
+from gstlal.pipeparts import pipetools
+
+
+def trigger(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int, autocorrelation_matrix: numpy.ndarray = None, mask_matrix: numpy.ndarray = None,
+			snr_thresh: float = 0, sigmasq: float = None, max_snr: bool = False):
+	"""Generic trigger generator, find triggers in snr streams
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		n:
+			int, number of samples over which to identify triggers
+		autocorrelation_matrix:
+			ndarray, Array of complex autocorrelation vectors. Number of vectors (rows) in matrix sets number of channels.
+			All vectors must have the same length.
+		mask_matrix:
+			ndarray, Array of integer autocorrelation mask vectors. Number of vectors (rows) in mask sets number of channels.
+			All vectors must have the same length. The mask values are either 0 or 1 and indicate whether to use the corresponding
+			matrix entry in computing the autocorrelation chi-sq statistic.
+		snr_thresh:
+			float, default 0, SNR Threshold that determines a trigger
+		sigmasq:
+			float, default None
+		max_snr:
+			bool, default False, Set flag to return single loudest trigger from buffer
+
+	References:
+		[1] GstLAL Trigger Implementation: gstlal/gstlal-burst/gst/lal/gstlal_trigger.c
+
+	Returns:
+		Element, the trigger element
+	"""
+	properties = {
+		"n": n,
+		"snr_thresh": snr_thresh,
+		"max_snr": max_snr
+	}
+	if autocorrelation_matrix is not None:
+		properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix)
+	if mask_matrix is not None:
+		properties["autocorrelation_mask"] = mask_matrix
+	if sigmasq is not None:
+		properties["sigmasq"] = sigmasq
+	return pipetools.make_element_with_src(pipeline, src, "lal_trigger", **properties)
+
+
+def burst_trigger_gen(pipeline, src, **properties):
+	"""Burst Triggergen, find burst triggers in snr streams
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties:
+
+	References:
+		[1] BurstTriggerGen Implementation: gstlal/gstlal-burst/gst/lal/gstlal_burst_triggergen.c
+
+	Returns:
+		Element, the trigger gen element
+	"""
+	return pipetools.make_element_with_src(pipeline, src, "lal_bursttriggergen", **properties)
+
+
+def blcbc_trigger_gen(pipeline: pipetools.Pipeline, snr: pipetools.Element, chisq: pipetools.Element, template_bank_filename: str, snr_threshold: float,
+					  sigmasq: pipetools.ValueArray) -> pipetools.Element:
+	""" Produce sngl_inspiral records from SNR and chi squared.
+	A trigger is recorded for every instant at which the absolute value of the SNR
+	is greater than snr-thresh, and also greater than at all of the window seconds
+	of data that come before and after.  snr-thresh and window are properties of
+	this element.  This element has a bounded latency of ~ window and is suitable
+	for online applications, or applications where precise triggering behavior with
+	small chunks of data is required
+
+	The maximum possible trigger rate is (1/window) Hz per template.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		snr:
+			Gst.Element, the source element
+		chisq:
+			Element
+		template_bank_filename:
+			str, Path to XML file used to generate the template bank.  Setting this property resets sigmasq to a vector of 0s.
+		snr_threshold:
+			float, SNR Threshold that determines a trigger.
+		sigmasq:
+			Vector of \\sigma^{2} factors.
+
+	References:
+		[1] gstlal/gstlal-inspiral/gst/lal/gstlal_blcbc_triggergen.c
+
+	Returns:
+		Element, the blcbc trigger gen
+	"""
+	# snr is complex and chisq is real so the correct source and sink
+	# pads will be selected automatically
+	elem = pipetools.make_element_with_src(pipeline, snr, "lal_blcbctriggergen", bank_filename=template_bank_filename, snr_thresh=snr_threshold, sigmasq=sigmasq)
+	chisq.link(elem)
+	return elem
+
+
+def trigger_gen(pipeline: pipetools.Pipeline, snr: pipetools.Element, chisq: pipetools.Element, template_bank_filename: str, snr_threshold: float,
+				sigmasq: pipetools.ValueArray) -> pipetools.Element:
+	"""Produce sngl_inspiral records from SNR and chi squared.
+	A trigger is recorded for every instant at which the absolute value of the SNR
+	is greater than snr-thresh, and also greater than at all of the max_gap seconds
+	of data that come before and after.  snr-thresh and max_gap are properties of
+	this element
+
+	The maximum possible trigger rate is (1/max_gap) Hz per template.
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		snr:
+			Gst.Element, the source element
+		chisq:
+			Element
+		template_bank_filename:
+			str, Path to XML file used to generate the template bank.  Setting this property resets sigmasq to a vector of 0s.
+		snr_threshold:
+			float, SNR Threshold that determines a trigger.
+		sigmasq:
+			Vector of \\sigma^{2} factors.
+
+	References:
+		Implementation: gstlal-inspiral/gst/lal/gstlal_triggergen.c
+
+	Returns:
+		Element
+	"""
+	# snr is complex and chisq is real so the correct source and sink
+	# pads will be selected automatically
+	elem = pipetools.make_element_with_src(pipeline, snr, "lal_triggergen", bank_filename=template_bank_filename, snr_thresh=snr_threshold, sigmasq=sigmasq)
+	chisq.link(elem)
+	return elem
+
+
+def itac(pipeline: pipetools.Pipeline, src: pipetools.Element, n: int, bank: str, autocorrelation_matrix: numpy.ndarray = None,
+		 mask_matrix: numpy.ndarray = None, snr_thresh=0, sigmasq=None) -> pipetools.Element:
+	"""Find inspiral triggers in snr streams
+
+	Args:
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		n:
+			int, number of samples over which to identify itacs
+		bank:
+			str, Path to XML file used to generate the template bank.  Setting this property resets sigmasq to a vector of 0s.
+		autocorrelation_matrix:
+			array, Array of complex autocorrelation vectors.  Number of vectors (rows) in matrix sets number of channels.  All vectors must have the same length.
+		mask_matrix:
+			array, Array of integer autocorrelation mask vectors.  Number of vectors (rows) in mask sets number of channels.  All vectors must have the same
+			length. The mask values are either 0 or 1 and indicate whether to use the corresponding matrix entry in computing the autocorrelation chi-sq statistic.
+		snr_thresh:
+			float, SNR Threshold that determines a trigger.
+		sigmasq:
+			array, Vector of \\sigma^{2} factors.  The effective distance of a trigger is \\sqrt{sigma^{2}} / SNR.
+
+	References:
+		Implementation: gstlal-inspiral/gst/lal/gstlal_itac.c
+
+	Returns:
+		Element
+	"""
+	properties = {
+		"n": n,
+		"bank_filename": bank,
+		"snr_thresh": snr_thresh
+	}
+	if autocorrelation_matrix is not None:
+		properties["autocorrelation_matrix"] = pipeio.repack_complex_array_to_real(autocorrelation_matrix)
+	if mask_matrix is not None:
+		properties["autocorrelation_mask"] = mask_matrix
+	if sigmasq is not None:
+		properties["sigmasq"] = sigmasq
+	return pipetools.make_element_with_src(pipeline, src, "lal_itac", **properties)
diff --git a/gstlal/python/utilities/testtools.py b/gstlal/python/utilities/testtools.py
index 8caebc0de3b52270d5cba14002c2b6d0e71c3188..c3bd40b42d83e749c671b6ab4b3f0ab9391a3dda 100644
--- a/gstlal/python/utilities/testtools.py
+++ b/gstlal/python/utilities/testtools.py
@@ -1,5 +1,14 @@
 """Test utilities. Common functions used across various GstLAL unittests
 """
+
+import gi
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject, Gst
+
+GObject.threads_init()
+Gst.init(None)
+import os
 import pathlib
 import string
 import sys
@@ -13,59 +22,118 @@ import pytest
 PLATFORM = sys.platform
 
 DEFAULT_MOCK_PATCHES = (
-    # Ordered mapping of (target, {kwarg: value}) for passing into unittest.mock.patch
-    ('gstlal.datafind.load_frame_cache', {'return_value': [1, 2, 3]}),
+	# Ordered mapping of (target, {kwarg: value}) for passing into unittest.mock.patch
+	('gstlal.datafind.load_frame_cache', {'return_value': [1, 2, 3]}),
 )
 CLEAN_TRANSLATION = {ord(c): None for c in string.whitespace}
 
+
 def clean_str(c: str):
-    """Clean a copyright string before comparison"""
-    return c.translate(CLEAN_TRANSLATION)
+	"""Clean a copyright string before comparison"""
+	return c.translate(CLEAN_TRANSLATION)
+
 
 def is_osx(platform: str = PLATFORM):
-    """Check is OSX"""
-    return platform.lower() == 'darwin'
+	"""Check is OSX"""
+	return platform.lower() == 'darwin'
 
 
 def skip_osx(f: types.FunctionType) -> types.FunctionType:
-    """Decorator wrapping pytest.skipif"""
-    return pytest.mark.skipif(is_osx(), reason='Test not supported on OSX')(f)
+	"""Decorator wrapping pytest.skipif"""
+	return pytest.mark.skipif(is_osx(), reason='Test not supported on OSX')(f)
 
 
-class GstLALTestManager:
-    """Context manager for GstLAL tests"""
-
-    def __init__(self, patch_info: Tuple[Dict[str, Dict]] = DEFAULT_MOCK_PATCHES):
-        self.tmp_dir = tempfile.TemporaryDirectory()
-        self.tmp_path = pathlib.Path(self.tmp_dir.name)
-        self.patch_info = patch_info
-        self._patches = []
+def requires_full_build(f: types.FunctionType):
+	return pytest.mark.requires_full_build(f)
 
-    def __enter__(self):
-        """Enter the GstLAL testing context"""
 
-        # Create temporary directory
-        self.tmp_dir.__enter__()
+def broken(reason: str):
+	def wrapper(f: types.FunctionType):
+		func = pytest.mark.skip(f, reason)
+		func = pytest.mark.broken(func)
+		return func
 
-        # Set all mocks
-        for target, kwargs in self.patch_info:
-            p = mock.patch(target, **kwargs)
-            self._patches.append(p)
-            p.__enter__()
+	return wrapper
 
-        return self
 
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        """Exit GstLAL context"""
+def impl_deprecated(f):
+	return broken('Underlying implementation not included in build')(f)
 
-        # Remove tmp dir
-        self.tmp_dir.__exit__(exc_type, exc_val, exc_tb)
 
-        # Unset all mocks
-        for p in self._patches:
-            p.__exit__(exc_type, exc_val, exc_tb)
-
-    @property
-    def cache_path(self):
-        """Cache path"""
-        return (self.tmp_path / 'cache.txt').as_posix()
+class GstLALTestManager:
+	"""Context manager for GstLAL tests"""
+
+	def __init__(self, patch_info: Tuple[Dict[str, Dict]] = DEFAULT_MOCK_PATCHES, env_overrides: dict = None, with_pipeline: bool = False):
+		self.tmp_dir = tempfile.TemporaryDirectory()
+		self.tmp_path = pathlib.Path(self.tmp_dir.name)
+		self.patch_info = patch_info
+		self._patches = []
+		self._env_originals = {}
+		self._env_overrides = {} if env_overrides is None else env_overrides
+		self._with_pipeline = with_pipeline
+
+	def __enter__(self):
+		"""Enter the GstLAL testing context"""
+
+		# Create temporary directory
+		self.tmp_dir.__enter__()
+
+		# Set all mocks
+		for target, kwargs in self.patch_info:
+			p = mock.patch(target, **kwargs)
+			self._patches.append(p)
+			p.__enter__()
+
+		# Set env overrides
+		keys = list(self._env_overrides.keys())
+		for k in keys:
+			self.override_env_var(k, self._env_overrides[k])
+
+		# Set pipeline
+		self.set_pipeline()
+
+		return self
+
+	def __exit__(self, exc_type, exc_val, exc_tb):
+		"""Exit GstLAL context"""
+
+		# Remove tmp dir
+		self.tmp_dir.__exit__(exc_type, exc_val, exc_tb)
+
+		# Unset all mocks
+		for p in self._patches:
+			p.__exit__(exc_type, exc_val, exc_tb)
+
+		# Undo all env overrides
+		keys = list(self._env_originals.keys())
+		for k in keys:
+			self.reset_env_var(k)
+
+	@property
+	def cache_path(self):
+		"""Cache path"""
+		return (self.tmp_path / 'cache.txt').as_posix()
+
+	def override_env_var(self, key: str, val: str):
+		if key in self._env_originals:
+			raise ValueError('That env var is already overridden')
+		self._env_originals[key] = os.environ.get(key, None)
+		self._env_overrides[key] = val
+		os.environ[key] = val
+
+	def reset_env_var(self, key: str):
+		if key not in self._env_originals:
+			raise ValueError('Unable to reset var: {}'.format(key))
+		val = self._env_originals.pop(key)
+		if val is None:
+			os.environ.pop(key)
+		else:
+			os.environ[key] = val
+		if key in self._env_overrides:
+			self._env_overrides.pop(key)
+
+	def set_pipeline(self):
+		if self._with_pipeline:
+			self.pipeline = Gst.Pipeline(name=os.path.split(sys.argv[0])[1])
+		else:
+			self.pipeline = None
diff --git a/gstlal/share/conda/set_env.sh b/gstlal/share/conda/set_env.sh
index 55f6a48b1d96f59da0f18ea87bd8fc89bdf2706f..9b9ec133fdefa1f7887e57886d570e717181f109 100755
--- a/gstlal/share/conda/set_env.sh
+++ b/gstlal/share/conda/set_env.sh
@@ -45,20 +45,26 @@ PARENT_PATH=$(
   cd ..
   pwd
 )
+#APP_PARENT_PATH=$(
+#  builtin cd $PARENT_PATH
+#  cd ..
+#  pwd
+#)
+APP_PARENT_PATH=$PARENT_PATH
 echo "Repository root discovered: $REPO_PATH"
 
 # Check if deps directory already exists
-if [ -d "$PARENT_PATH/deps" ]; then
+if [ -d "$APP_PARENT_PATH/deps" ]; then
   echo "Sibling directory already exists, no need to create."
 else
   echo "Sibling directory does not exist, creating."
-  mkdir "$PARENT_PATH/deps"
+  mkdir "$APP_PARENT_PATH/deps"
 fi
 
 # Set dependency-path variable for gstlal build dependencies
 if [[ -z "${DEPS_PATH}" ]]; then
-  echo "DEPS_PATH variable not set. Setting to: $PARENT_PATH/deps."
-  export DEPS_PATH=${PARENT_PATH}/deps
+  echo "DEPS_PATH variable not set. Setting to: $APP_PARENT_PATH/deps."
+  export DEPS_PATH=${APP_PARENT_PATH}/deps
 fi
 
 # Add dep-path to PATH variable
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_encode.py b/gstlal/tests/tests_pytest/pipeparts/test_encode.py
new file mode 100644
index 0000000000000000000000000000000000000000..e79e412b896c822f17a2e77def554d461be537a0
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_encode.py
@@ -0,0 +1,60 @@
+"""Unittests for encoding pipeparts"""
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import encode, pipetools
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestEncode:
+	"""Group test for encoders"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_wav(self, pipeline, src):
+		"""Test wav"""
+		elem = encode.wav(pipeline, src)
+		assert_elem_props(elem, 'GstWavEnc')
+
+	@testtools.broken('vorbis not available in current version of Gst')
+	def test_vorbis(self, pipeline, src):
+		"""Test vorbis"""
+		elem = encode.vorbis(pipeline, src)
+		assert_elem_props(elem, 'GstVorbisEnc')
+
+	@testtools.broken('theora not available in current version of Gst')
+	def test_theora(self, pipeline, src):
+		"""Test theora"""
+		elem = encode.theora(pipeline, src)
+		assert_elem_props(elem, 'GstTheoraEnc')
+
+	@testtools.broken('flac not available in current version of Gst')
+	def test_flac(self, pipeline, src):
+		"""Test flac"""
+		elem = encode.flac(pipeline, src)
+		assert_elem_props(elem, 'GstFlacEnc')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_igwd_parse(self, pipeline, src):
+		"""Test igwd parse"""
+		elem = encode.igwd_parse(pipeline, src)
+		assert_elem_props(elem, 'GstFrameCPPIGWDParse')
+
+	def test_uri_decode_bin(self, pipeline, src):
+		"""Test uri_decode"""
+		elem = encode.uri_decode_bin(pipeline, src)
+		assert_elem_props(elem, 'GstURIDecodeBin')
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_filters.py b/gstlal/tests/tests_pytest/pipeparts/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..7452f330827b3945a729242c9ccdb63e6690be2f
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_filters.py
@@ -0,0 +1,75 @@
+"""Unittests for filter pipeparts"""
+import numpy
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import filters, pipetools
+from gstlal.pipeparts.pipetools import Gst, GObject
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestFilters:
+	"""Test filter pipeparts"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_caps(self, pipeline, src):
+		"""Test caps filter"""
+		elem = filters.caps(pipeline, src, caps="audio/x-raw, rate=1")
+		assert_elem_props(elem, "GstCapsFilter")
+
+	def test_fir(self, pipeline, src):
+		"""Test create a FIR filter"""
+		elem = filters.fir(pipeline, src, kernel=None, latency=None)
+		assert_elem_props(elem, "GstAudioFIRFilter")
+
+	def test_iir(self, pipeline, src):
+		"""Test create IIR filter"""
+		elem = filters.iir(pipeline, src, a=numpy.array([1, 2, 3]), b=numpy.array([1, 2, 3]))
+		assert_elem_props(elem, "GstAudioIIRFilter")
+
+	def test_state_vector(self, pipeline, src):
+		"""Test state_vector"""
+		elem = filters.state_vector(pipeline, src)
+		assert_elem_props(elem, "GSTLALStateVector")
+
+	def test_inject(self, pipeline, src):
+		"""Test injections"""
+		elem = filters.inject(pipeline, src, 'file')
+		assert_elem_props(elem, "GSTLALSimulation")
+
+	def test_audio_cheb_band(self, pipeline, src):
+		"""Test cheb"""
+		elem = filters.audio_cheb_band(pipeline, src, 10.0, 100.0)
+		assert_elem_props(elem, "GstAudioChebBand")
+
+	def test_audio_cheb_limit(self, pipeline, src):
+		"""Test cheb limit"""
+		elem = filters.audio_cheb_limit(pipeline, src, 10.0)
+		assert_elem_props(elem, "GstAudioChebLimit")
+
+	def test_drop(self, pipeline, src):
+		"""Test drop"""
+		elem = filters.drop(pipeline, src, 10)
+		assert_elem_props(elem, "GSTLALDrop")
+
+	def test_remove_fake_disconts(self, pipeline, src):
+		"""Test removefd"""
+		elem = filters.remove_fake_disconts(pipeline, src)
+		assert_elem_props(elem, "GSTLALNoFakeDisconts")
+
+	def test_gate(self, pipeline, src):
+		"""Test gate"""
+		elem = filters.gate(pipeline, src, control=src)
+		assert_elem_props(elem, "GSTLALGate")
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_mux.py b/gstlal/tests/tests_pytest/pipeparts/test_mux.py
new file mode 100644
index 0000000000000000000000000000000000000000..73e2f82b54755d066938a474b271ec3691afb9f5
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_mux.py
@@ -0,0 +1,43 @@
+"""Unittests for mux pipeparts"""
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import mux, pipetools
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestMux:
+	"""Test mux pipeparts"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	@testtools.requires_full_build
+	def test_framecpp_demux(self, pipeline, src):
+		"""Test framecpp demux"""
+		elem = mux.framecpp_channel_demux(pipeline, src)
+		assert_elem_props(elem, "GstFrameCPPChannelDemux")
+
+	@testtools.requires_full_build
+	def test_framecpp_mux(self, pipeline, src):
+		"""Test framecpp demux"""
+		elem = mux.framecpp_channel_mux(pipeline, None)
+		assert_elem_props(elem, "GstFrameCPPChannelMux")
+
+	@testtools.broken('oggmux not available in current version of Gst')
+	def test_ogg(self, pipeline, src):
+		"""Test ogg mux"""
+		elem = mux.ogg_mux(pipeline, src)
+		assert_elem_props(elem, "GstOggMux")
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py b/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5fc963eb8df1fd9b913ede3a36cffb4e4b8ee33
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_pipedot.py
@@ -0,0 +1,125 @@
+"""Unittests for pipedot pipeparts"""
+import re
+
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, pipedot
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+# 'fakesrc0_0x7fae93190120_src_0x7fae93194080'
+MEMORY_ADDR_PATTERN_OSX = '[0-9]?_0x[0-9a-fA-F]{12}'
+MEMORY_ADDR_PATTERN_LINUX = '[0-9]?_0x[0-9a-fA-F]{7}'
+FAKE_SRC_PATTERN = 'fakesrc[0-9]{1}'
+
+CLEAN_PIPELINE_DOT_OSX = (
+	'digraph pipeline {\n  rankdir=LR;\n  fontname="sans";\n  fontsize="10";\n  labelloc=t;\n  nodesep=.1;\n  ranksep=.2;\n  label="<GstPipeline>\\nTestEncode\\n[0]";\n'
+	'  node [style="filled,rounded", shape=box, fontsize="9", fontname="sans", margin="0.0,0.0"];\n  edge [labelfontsize="6", fontsize="9", fontname="monospace"];\n  \n'
+	'  legend [\n    pos="0,0!",\n    margin="0.05,0.05",\n    style="filled",\n    label="Legend\\lElement-States: [~] void-pending, [0] null, [-] ready, [=] paused, [>]'
+	' playing\\lPad-Activation: [-] none, [>] push, [<] pull\\lPad-Flags: [b]locked, [f]lushing, [b]locking, [E]OS; upper-case is set\\lPad-Task: [T] has started task, [t]'
+	' has paused task\\l",\n  ];\n  subgraph cluster_fakesrc {\n    fontname="Bitstream Vera Sans";\n    fontsize="8";\n    style="filled,rounded";\n    color=black;\n    '
+	'label="GstFakeSrc\\nfakesrc\\n[0]\\nfilltype=nothing";\n    subgraph cluster_fakesrc_src {\n      label="";\n      style="invis";\n      fakesrc_src [color=black, '
+	'fillcolor="#ffaaaa", label="src\\n[-][bFb]", height="0.2", style="filled,solid"];\n    }\n\n    fillcolor="#ffaaaa";\n  }\n\n}\n')
+
+CLEAN_PIPELINE_DOT_LINUX = (
+	'digraph pipeline {\n  rankdir=LR;\n  fontname="sans";\n  fontsize="10";\n  labelloc=t;\n  nodesep=.1;\n  ranksep=.2;\n  label="<GstPipeline>\\nTestEncode\\n[0]";\n'
+	'  node [style="filled,rounded", shape=box, fontsize="9", fontname="sans", margin="0.0,0.0"];\n  edge [labelfontsize="6", fontsize="9", fontname="monospace"];\n  \n'
+	'  legend [\n    pos="0,0!",\n    margin="0.05,0.05",\n    style="filled",\n    label="Legend\\lElement-States: [~] void-pending, [0] null, [-] ready, [=] paused, [>]'
+	' playing\\lPad-Activation: [-] none, [>] push, [<] pull\\lPad-Flags: [b]locked, [f]lushing, [b]locking, [E]OS; upper-case is set\\lPad-Task: [T] has started task, [t]'
+	' has paused task\\l",\n  ];\n  subgraph cluster_fakesrc {\n    fontname="Bitstream Vera Sans";\n    fontsize="8";\n    style="filled,rounded";\n    color=black;\n    '
+	'label="GstFakeSrc\\nfakesrc\\n[0]\\nparent=(GstPipeline) TestEncode\\nfilltype=nothing";\n    subgraph cluster_fakesrc_src {\n      label="";\n      style="invis";\n      fakesrc_src [color=black, '
+	'fillcolor="#ffaaaa", label="src\\n[-][bFb]", height="0.2", style="filled,solid"];\n    }\n\n    fillcolor="#ffaaaa";\n  }\n\n}\n')
+
+SANITIZE_PATTERNS = [
+	(MEMORY_ADDR_PATTERN_OSX, ''),
+	(MEMORY_ADDR_PATTERN_LINUX, ''),
+	(FAKE_SRC_PATTERN, 'fakesrc')
+]
+
+if testtools.is_osx():
+	CLEAN_PIPELINE_DOT = CLEAN_PIPELINE_DOT_OSX
+else:
+	CLEAN_PIPELINE_DOT = CLEAN_PIPELINE_DOT_LINUX
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+def sanitize_dot_str(s: str) -> str:
+	"""Utility for making a dot str stable across sessions"""
+	for pat, rep in SANITIZE_PATTERNS:
+		s = re.sub(pat, rep, s)
+	return s
+
+
+class TestSanitize:
+	"""Test group for sanitizing dot strings"""
+
+	def test_memory_addr_pattern(self):
+		"""Test memory address pattern"""
+		sample_text = 'asdljkhads asd fakesrc0_0x7fae93190120_src_0x7fae93194080 asldkj'
+		matches = re.findall(re.compile(MEMORY_ADDR_PATTERN_OSX), sample_text)
+		assert matches == ['0_0x7fae93190120', '_0x7fae93194080']
+
+		sample_text = 'cluster_fakesrc_0x2cc08c0'
+		matches = re.findall(re.compile(MEMORY_ADDR_PATTERN_LINUX), sample_text)
+		assert matches == ['_0x2cc08c0']
+
+	def test_sanitize_dot_str(self):
+		"""Test sanitizer"""
+		sample_text = 'asdljkhads asd fakesrc0_0x7fae93190120_src_0x7fae93194080 asldkj'
+		sanitized = sanitize_dot_str(sample_text)
+		assert sanitized == 'asdljkhads asd fakesrc_src asldkj'
+
+		sample_text = 'cluster_fakesrc_0x2cc08c0'
+		sanitized = sanitize_dot_str(sample_text)
+		assert sanitized == 'cluster_fakesrc'
+
+
+class TestDot:
+	"""Test dot pipeparts"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	@testtools.broken('Unable to redirect dump dot dir env var sufficiently')
+	def test_to_file(self, pipeline, src):
+		"""Test write dot file"""
+
+		with testtools.GstLALTestManager() as tm:
+			tm.override_env_var('GST_DEBUG_DUMP_DOT_DIR', tm.tmp_path.as_posix())
+
+			pipedot.to_file(pipeline, 'graph')
+
+			output_path = tm.tmp_path / 'graph.dot'
+			assert output_path.exists()
+
+			tm.reset_env_var('GST_DEBUG_DUMP_DOT_DIR')
+
+	def test_to_file_use_str(self, pipeline, src):
+		"""Test write dot file using str intermediary"""
+
+		with testtools.GstLALTestManager() as tm:
+			tm.override_env_var('GST_DEBUG_DUMP_DOT_DIR', tm.tmp_path.as_posix())
+
+			pipedot.to_file(pipeline, 'graph', use_str_method=True)
+
+			output_path = tm.tmp_path / 'graph.dot'
+			assert output_path.exists()
+
+	def test_to_str(self, pipeline, src):
+		"""Test convert pipeline to str"""
+
+		with testtools.GstLALTestManager() as tm:
+			res = pipedot.to_str(pipeline)
+			assert isinstance(res, str)
+			assert sanitize_dot_str(res)[:10] == CLEAN_PIPELINE_DOT[:10] # TODO reverse kneecap
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py b/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a52441d50c26a498bb2b555dc4a57c5ffabade4
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_pipeparts.py
@@ -0,0 +1,118 @@
+"""Unit tests for pipeparts subpackage
+
+These tests are only for the __init__ file and other subpackage-wide
+tests, module specific tests should follow the test_module.py pattern
+"""
+import pytest
+from gstlal import pipeparts
+
+LEGACY_ATTRS = [
+	'AppSync',
+	'BYTE_ORDER',
+	'audioresample_variance_gain',
+	'connect_appsink_dump_dot',
+	'framecpp_channeldemux_check_segments',
+	'framecpp_channeldemux_set_units',
+	'framecpp_filesink_cache_entry_from_mfs_message',
+	'framecpp_filesink_ldas_path_handler',
+	'mkabs',
+	'mkadder',
+	'mkappsink',
+	'mkaudioamplify',
+	'mkaudiochebband',
+	'mkaudiocheblimit',
+	'mkaudioconvert',
+	'mkaudiorate',
+	'mkaudiotestsrc',
+	'mkaudioundersample',
+	'mkautoaudiosink',
+	'mkautochisq',
+	'mkavimux',
+	'mkbitvectorgen',
+	'mkblcbctriggergen',
+	'mkbursttriggergen',
+	'mkcapsfilter',
+	'mkcapssetter',
+	'mkchannelgram',
+	'mkchecktimestamps',
+	'mkclean',
+	'mkcolorspace',
+	'mkcomputegamma',
+	'mkdeglitcher',
+	'mkdenoiser',
+	'mkdrop',
+	'mkfakeLIGOsrc',
+	'mkfakeadvLIGOsrc',
+	'mkfakeadvvirgosrc',
+	'mkfakesink',
+	'mkfakesrc',
+	'mkfilesink',
+	'mkfirbank',
+	'mkfirfilter',
+	'mkflacenc',
+	'mkframecppchanneldemux',
+	'mkframecppchannelmux',
+	'mkframecppfilesink',
+	'mkframexmitsrc',
+	'mkgate',
+	'mkgeneric',
+	'mkhistogram',
+	'mkigwdparse',
+	'mkiirfilter',
+	'mkinjections',
+	'mkinterpolator',
+	'mkitac',
+	'mklalcachesrc',
+	'mklatency',
+	'mklhocoherentnull',
+	'mklvshmsrc',
+	'mkmatrixmixer',
+	'mkmean',
+	'mkmultifilesink',
+	'mkmultiplier',
+	'mkndssrc',
+	'mknofakedisconts',
+	'mknxydumpsink',
+	'mknxydumpsinktee',
+	'mkodctodqv',
+	'mkoggmux',
+	'mkogmvideosink',
+	'mkpeak',
+	'mkplaybacksink',
+	'mkpow',
+	'mkprogressreport',
+	'mkqueue',
+	'mkreblock',
+	'mkresample',
+	'mksegmentsrc',
+	'mkshift',
+	'mkspectrumplot',
+	'mkstatevector',
+	'mksumsquares',
+	'mktaginject',
+	'mktcpserversink',
+	'mktdwhiten',
+	'mktee',
+	'mktheoraenc',
+	'mktogglecomplex',
+	'mktrigger',
+	'mktriggergen',
+	'mktriggerxmlwritersink',
+	'mktrim',
+	'mkuridecodebin',
+	'mkvideosink',
+	'mkvorbisenc',
+	'mkwavenc',
+	'mkwhiten',
+	'src_deferred_link',
+	'write_dump_dot',
+]
+
+
+class TestLegacyImportPaths:
+	"""Test class for legacy import paths testing"""
+
+	@pytest.mark.parametrize("attribute", LEGACY_ATTRS)
+	def test_attributes_exist(self, attribute):
+		"""Test that all expected attributes of pipeparts exist"""
+		assert hasattr(pipeparts, attribute)
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py b/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd21db64719504f99d5fefa5843281922810af32
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_pipetools.py
@@ -0,0 +1,35 @@
+"""Unit tests for pipetools module"""
+import inspect
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools
+from gstlal.utilities import testtools
+
+
+# Will reuse a million times:
+"""
+
+		pipeline:
+			Gst.Pipeline, the pipeline to which the new element will be added
+		src:
+			Gst.Element, the source element
+		**properties: 
+			dict, keyword arguments to be set as element properties
+
+"""
+
+
+class TestPipetools:
+	"""Test class group for pipetools"""
+
+	def test_clean_property_name(self):
+		"""Test cleaning of property names"""
+		assert pipetools.clean_property_name('name_with_underscores') == 'name-with-underscores'
+		assert pipetools.clean_property_name('async_') == 'async'
+
+	def test_make_element_with_src(self):
+		"""Test make element"""
+		with testtools.GstLALTestManager(with_pipeline=True) as tm:
+			elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc')
+			assert gsttools.is_element(elem)
+
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_plot.py b/gstlal/tests/tests_pytest/pipeparts/test_plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..a30b32132d4d954eb3da91fa60a6d697befafef2
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_plot.py
@@ -0,0 +1,43 @@
+"""Unittests for plotting pipeparts"""
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, plot
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestPlot:
+	"""Group test for encoders"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	@testtools.broken('Python gst plugins are broken')
+	def test_channelgram(self, pipeline, src):
+		"""Test channelgram"""
+		elem = plot.channelgram(pipeline, src)
+		assert_elem_props(elem, 'GstWavEnc')
+
+	@testtools.broken('Python gst plugins are broken')
+	def test_spectrum(self, pipeline, src):
+		"""Test spectrum"""
+		elem = plot.spectrum(pipeline, src)
+		assert_elem_props(elem, 'GstWavEnc')
+
+	@testtools.broken('Python gst plugins are broken')
+	def test_histogram(self, pipeline, src):
+		"""Test histogram"""
+		elem = plot.histogram(pipeline, src)
+		assert_elem_props(elem, 'GstWavEnc')
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_sink.py b/gstlal/tests/tests_pytest/pipeparts/test_sink.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b6fa9d8ac19fce44960168221485c6485eb062a
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_sink.py
@@ -0,0 +1,95 @@
+"""Unittests for sink pipeparts"""
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, sink
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestSink:
+	"""Group test for encoders"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_multi_file(self, pipeline, src):
+		"""Test test multi file"""
+		elem = sink.multi_file(pipeline, src)
+		assert_elem_props(elem, 'GstMultiFileSink')
+
+	@testtools.requires_full_build
+	def test_gwf(self, pipeline, src):
+		"""Test test framecpp file"""
+		elem = sink.gwf(pipeline, src)
+		assert_elem_props(elem, 'FRAMECPPFilesink')
+
+	def test_fake(self, pipeline, src):
+		"""Test test fake"""
+		elem = sink.fake(pipeline, src)
+		assert_elem_props(elem, 'GstFakeSink')
+
+	def test_file(self, pipeline, src):
+		"""Test test file"""
+		elem = sink.file(pipeline, src, 'file')
+		assert_elem_props(elem, 'GstFileSink')
+
+	def test_nxy_dump(self, pipeline, src):
+		"""Test nxy dump"""
+		elem = sink.tsv(pipeline, src, 'file')
+		assert_elem_props(elem, 'GstFileSink')
+
+	@testtools.broken('ogmvideo not available in current version of Gst')
+	def test_ogm_video(self, pipeline, src):
+		"""Test ogm_video"""
+		elem = sink.ogm_video(pipeline, src, 'file')
+		assert_elem_props(elem, 'GstFileSink')
+
+	def test_auto_video(self, pipeline, src):
+		"""Test auto_video"""
+		elem = sink.auto_video(pipeline, src)
+		assert_elem_props(elem, 'GstAutoVideoSink')
+
+	def test_auto_audio(self, pipeline, src):
+		"""Test auto_audio"""
+		elem = sink.auto_audio(pipeline, src)
+		assert_elem_props(elem, 'GstAutoAudioSink')
+
+	@testtools.broken('element_link_many not available in current version of Gst')
+	def test_playback(self, pipeline, src):
+		"""Test playback"""
+		elem = sink.playback(pipeline, src)
+		assert_elem_props(elem, 'GstAutoAudioSink')
+
+	def test_nxy_dump_tee(self, pipeline, src):
+		"""Test nxydumptee"""
+		elem = sink.tsv_tee(pipeline, src, 'file')
+		assert_elem_props(elem, 'GstTee')
+
+	@testtools.broken('Unable to find implementation of lal_triggerxmlwriter')
+	def test_trigger_xml_writer(self, pipeline, src):
+		"""Test trigger xml writer"""
+		elem = sink.trigger_xml_writer(pipeline, src, 'file')
+		assert_elem_props(elem, 'GstAutoAudioSink')
+
+	def test_app(self, pipeline, src):
+		"""Test app"""
+		elem = sink.app(pipeline, src)
+		assert_elem_props(elem, 'GstAppSink')
+
+	@testtools.broken('Implementation bug, invalid property unit_type for tcpserversink element')
+	def test_tcp_server(self, pipeline, src):
+		"""Test tcp_server"""
+		elem = sink.tcp_server(pipeline, src)
+		assert_elem_props(elem, 'GstTcpServerSink')
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_source.py b/gstlal/tests/tests_pytest/pipeparts/test_source.py
new file mode 100644
index 0000000000000000000000000000000000000000..437925d7ee433df8fcc026965d13a5c1d85e2134
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_source.py
@@ -0,0 +1,84 @@
+"""Unittests for source pipeparts"""
+import pytest
+from lal import LIGOTimeGPS
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, source
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestSource:
+	"""Group test for sources"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_cache(self, pipeline, src):
+		"""Test cache source"""
+		elem = source.cache(pipeline, 'cache-location.txt')
+		assert_elem_props(elem, 'GstLALCacheSrc')
+
+	@testtools.requires_full_build
+	def test_lvshm(self, pipeline, src):
+		"""Test lvshm"""
+		elem = source.lvshm(pipeline, 'LHO_Data')
+		assert_elem_props(elem, 'GDSLVSHMSrc')
+
+	@testtools.requires_full_build
+	def test_frame_x_mit(self, pipeline, src):
+		"""Test frame_x_mit"""
+		elem = source.framexmit(pipeline)
+		assert_elem_props(elem, 'GstGDSFramexmitSrc')
+
+	@testtools.requires_full_build
+	def test_nds(self, pipeline, src):
+		"""Test nds source"""
+		elem = source.nds(pipeline, 'host', 'inst', 'channel',
+						  channel_type=source.NDSChannelType.TestPoint)
+		assert_elem_props(elem, 'GSTLALNDSSrc')
+
+	def test_audio_test(self, pipeline, src):
+		"""Test audio test source"""
+		elem = source.audio_test(pipeline)
+		assert_elem_props(elem, 'GstAudioTestSrc')
+
+	def test_fake(self, pipeline, src):
+		"""Test fake source"""
+		elem = source.fake(pipeline, 'H1', 'channel')
+		assert_elem_props(elem, 'GstTagInject')
+
+	def test_segment(self, pipeline, src):
+		"""Test segment source"""
+		elem = source.segment(pipeline, [(LIGOTimeGPS(123456), LIGOTimeGPS(123457))])
+		assert_elem_props(elem, 'GSTLALSegmentSrc')
+
+	@testtools.impl_deprecated
+	def test_fake_ligo(self, pipeline, src):
+		"""Test fake ligo source"""
+		elem = source.fake_ligo(pipeline)
+		assert_elem_props(elem, 'lal_fakeligosrc')
+
+	@testtools.impl_deprecated
+	def test_fake_aligo(self, pipeline, src):
+		"""Test fake aligo source"""
+		elem = source.fake_aligo(pipeline)
+		assert_elem_props(elem, 'lal_fakeadvligosrc')
+
+	@testtools.impl_deprecated
+	def test_fake_avirgo(self, pipeline, src):
+		"""Test fake avirgo source"""
+		elem = source.fake_avirgo(pipeline)
+		assert_elem_props(elem, 'lal_fakeadvvirgosrc')
+
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_transform.py b/gstlal/tests/tests_pytest/pipeparts/test_transform.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae028e8c3e3afa86b217e08798937cd62409d00e
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_transform.py
@@ -0,0 +1,236 @@
+"""Unittests for transform pipeparts"""
+import pytest
+from lal import LIGOTimeGPS
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, transform
+from gstlal.pipeparts.pipetools import Gst
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestTransform:
+	"""Group test for transform elements"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_sum_squares(self, pipeline, src):
+		"""Test sumsquares"""
+		elem = transform.sum_squares(pipeline, src)
+		assert_elem_props(elem, 'GSTLALSumSquares')
+
+	def test_tag_inject(self, pipeline, src):
+		"""Test sumsquares"""
+		elem = transform.tag_inject(pipeline, src, 'a,b,c')
+		assert_elem_props(elem, 'GstTagInject')
+
+	def test_shift(self, pipeline, src):
+		"""Test shift"""
+		elem = transform.shift(pipeline, src)
+		assert_elem_props(elem, 'GSTLALShift')
+
+	def test_audio_amplify(self, pipeline, src):
+		"""Test audio amplify"""
+		elem = transform.amplify(pipeline, src, 0.1)
+		assert_elem_props(elem, 'GstAudioAmplify')
+
+	def test_audio_undersample(self, pipeline, src):
+		"""Test audio amplify"""
+		elem = transform.undersample(pipeline, src)
+		assert_elem_props(elem, 'GSTLALAudioUnderSample')
+
+	def test_audio_resample(self, pipeline, src):
+		"""Test audio resample"""
+		elem = transform.resample(pipeline, src)
+		assert_elem_props(elem, 'GstAudioResample')
+
+	def test_whiten(self, pipeline, src):
+		"""Test whiten"""
+		elem = transform.whiten(pipeline, src, "GSTLAL_PSDMODE_FIXED")
+		assert_elem_props(elem, 'GSTLALWhiten')
+
+	def test_tee(self, pipeline, src):
+		"""Test tee"""
+		elem = transform.tee(pipeline, src)
+		assert_elem_props(elem, 'GstTee')
+
+	def test_adder(self, pipeline, src):
+		"""Test adder"""
+		elem = transform.adder(pipeline, [src, src])
+		assert_elem_props(elem, 'GstLALAdder')
+
+	def test_multiplier(self, pipeline, src):
+		"""Test adder"""
+		elem = transform.adder(pipeline, [src, src])
+		assert_elem_props(elem, 'GstLALAdder')
+
+	def test_queue(self, pipeline, src):
+		"""Test queue"""
+		elem = transform.queue(pipeline, src)
+		assert_elem_props(elem, 'GstQueue')
+
+	def test_fir_bank(self, pipeline, src):
+		"""Test fir_bank"""
+		elem = transform.fir_bank(pipeline, src)
+		assert_elem_props(elem, 'GSTLALFIRBank')
+
+	def test_reblock(self, pipeline, src):
+		"""Test reblock"""
+		elem = transform.reblock(pipeline, src)
+		assert_elem_props(elem, 'GSTLALReblock')
+
+	def test_matrixmixer(self, pipeline, src):
+		"""Test matrixmixer"""
+		elem = transform.matrix_mixer(pipeline, src)
+		assert_elem_props(elem, 'GSTLALMatrixMixer')
+
+	def test_toggle_complex(self, pipeline, src):
+		"""Test matrixmixer"""
+		elem = transform.toggle_complex(pipeline, src)
+		assert_elem_props(elem, 'GSTLALToggleComplex')
+
+	def test_auto_chisq(self, pipeline, src):
+		"""Test """
+		elem = transform.auto_chisq(pipeline, src)
+		assert_elem_props(elem, 'GSTLALAutoChiSq')
+
+	def test_colorspace(self, pipeline, src):
+		"""Test """
+		elem = transform.colorspace(pipeline, src)
+		assert_elem_props(elem, 'GstVideoConvert')
+
+	def test_audioconvert(self, pipeline, src):
+		"""Test """
+		elem = transform.audio_convert(pipeline, src)
+		assert_elem_props(elem, 'GstAudioConvert')
+
+	def test_audiorate(self, pipeline, src):
+		"""Test """
+		elem = transform.audio_rate(pipeline, src)
+		assert_elem_props(elem, 'GstAudioRate')
+
+	def test_peak(self, pipeline, src):
+		"""Test """
+		elem = transform.peak(pipeline, src, 10)
+		assert_elem_props(elem, 'GSTLALPeak')
+
+	def test_set_caps(self, pipeline, src):
+		"""Test caps setter"""
+		elem = transform.set_caps(pipeline, src, caps="audio/x-raw, rate=1")
+		assert_elem_props(elem, "GstCapsSetter")
+
+	def test_progress_report(self, pipeline, src):
+		"""Test progress report"""
+		elem = transform.progress_report(pipeline, src, 'name')
+		assert_elem_props(elem, 'GstProgressReport')
+
+
+@testtools.requires_full_build
+class TestTransformFullBuild:
+	"""Group test for transform elements"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		return Gst.Pipeline('TestTransformFullBuild')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	def test_bitvectorgen(self, pipeline, src):
+		"""Test bvg"""
+		elem = transform.bit_vector_gen(pipeline, src, 1)
+		assert_elem_props(elem, 'GSTLALBitVectorGen')
+
+	def test_deglitch(self, pipeline, src):
+		"""Test """
+		elem = transform.deglitch(pipeline, src, [(LIGOTimeGPS(123456), LIGOTimeGPS(123457))])
+		assert_elem_props(elem, 'GstLALDeglitchFilter')
+
+	def test_denoise(self, pipeline, src):
+		"""Test """
+		elem = transform.denoise(pipeline, src)
+		assert_elem_props(elem, 'GSTLALDenoiser')
+
+	def test_clean(self, pipeline, src):
+		"""Test """
+		elem = transform.clean(pipeline, src)
+		assert_elem_props(elem, 'GSTLALDenoiser')
+
+	def test_latency(self, pipeline, src):
+		"""Test """
+		elem = transform.latency(pipeline, src)
+		assert_elem_props(elem, 'GSTLALLatency')
+
+
+class TestTransformBroken:
+	"""Broken tests"""
+
+	@testtools.broken('Causes seg fault')
+	def test_td_whiten(self, pipeline, src):
+		"""Test fir_bank"""
+		elem = transform.td_whiten(pipeline, src)
+		assert_elem_props(elem, 'GSTLALTDwhiten')
+
+	@testtools.broken('Unable to find abs')
+	def test_abs(self, pipeline, src):
+		"""Test abs"""
+		elem = transform.abs_(pipeline, src)
+		assert_elem_props(elem, 'GstMultiFileSink')
+
+	@testtools.broken('Unable to find pow')
+	def test_pow(self, pipeline, src):
+		"""Test pow"""
+		elem = transform.pow(pipeline, src)
+		assert_elem_props(elem, 'GstMultiFileSink')
+
+	@testtools.broken('Crashed test suite on mac')
+	def test_check_timestamps(self, pipeline, src):
+		"""Test """
+		elem = transform.check_timestamps(pipeline, src)
+		assert_elem_props(elem, 'GstLALDeglitchFilter')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_mean(self, pipeline, src):
+		"""Test mean"""
+		elem = transform.mean(pipeline, src)
+		assert_elem_props(elem, 'GstMultiFileSink')
+
+	@testtools.broken('Python plugins are broken')
+	@testtools.requires_full_build
+	def test_coherentnull(self, pipeline, src):
+		"""Test """
+		elem = transform.lho_coherent_null(pipeline, src, src, None, None, None, None, 1)
+		assert_elem_props(elem, 'GSTLALDenoiser')
+
+	@testtools.broken('Crashed test suite on mac')
+	@testtools.requires_full_build
+	def test_check_timestamps(self, pipeline, src):
+		"""Test """
+		elem = transform.check_timestamps(pipeline, src)
+		assert_elem_props(elem, 'GstLALDeglitchFilter')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_trim(self, pipeline, src):
+		"""Test trim"""
+		elem = transform.trim(pipeline, src)
+		assert_elem_props(elem, 'GSTLALTrim')
+
+	@testtools.broken('Causes seg fault')
+	@testtools.requires_full_build
+	def test_interpolator(self, pipeline, src):
+		"""Test interpolator"""
+		elem = transform.interpolator(pipeline, src)
+		assert_elem_props(elem, 'GSTLALInterpolator')
diff --git a/gstlal/tests/tests_pytest/pipeparts/test_trigger.py b/gstlal/tests/tests_pytest/pipeparts/test_trigger.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2c2e9f587ab23b421c11ac19634a12a375d8d82
--- /dev/null
+++ b/gstlal/tests/tests_pytest/pipeparts/test_trigger.py
@@ -0,0 +1,59 @@
+"""Unittests for trigger pipeparts"""
+import pytest
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools, trigger
+from gstlal.pipeparts.pipetools import Gst, GObject
+from gstlal.utilities import testtools
+
+
+def assert_elem_props(elem, name):
+	assert gsttools.is_element(elem)
+	assert type(elem).__name__ == name
+
+
+class TestTrigger:
+	"""Group test for trigger elements"""
+
+	@pytest.fixture(scope='class', autouse=True)
+	def pipeline(self):
+		GObject.MainLoop()
+		return Gst.Pipeline('TestEncode')
+
+	@pytest.fixture(scope='class', autouse=True)
+	def src(self, pipeline):
+		return pipetools.make_element_with_src(pipeline, None, 'fakesrc')
+
+	@testtools.requires_full_build
+	def test_trigger(self, pipeline, src):
+		"""Test test multi file"""
+		elem = trigger.trigger(pipeline, src, 1)
+		assert_elem_props(elem, 'GSTLALTrigger')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_burst_trigger_gen(self, pipeline, src):
+		"""Test burst gen"""
+		elem = trigger.burst_trigger_gen(pipeline, src)
+		assert_elem_props(elem, 'GstTrigger')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_blcbc_trigger_gen(self, pipeline, src):
+		"""Test blcbc gen"""
+		elem = trigger.blcbc_trigger_gen(pipeline, src, 0.0, 'file', 0.0, 0.0)
+		assert_elem_props(elem, 'GstTrigger')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_trigger_gen(self, pipeline, src):
+		"""Test gen"""
+		elem = trigger.trigger_gen(pipeline, src, 0.0, 'file', 0.0, 0.0)
+		assert_elem_props(elem, 'GstTrigger')
+
+	@testtools.impl_deprecated
+	@testtools.requires_full_build
+	def test_itac(self, pipeline, src):
+		"""Test """
+		elem = trigger.itac(pipeline, src, 1, 'bank')
+		assert_elem_props(elem, 'GSTLALDenoiser')
diff --git a/gstlal/tests/tests_pytest/test_datasource.py b/gstlal/tests/tests_pytest/test_datasource.py
index eb58534d80d141b83fedfe3bd444e1e85b27ff6a..e769f79bb5aefa9b9a4d28670125f3da42f5f576 100644
--- a/gstlal/tests/tests_pytest/test_datasource.py
+++ b/gstlal/tests/tests_pytest/test_datasource.py
@@ -7,6 +7,8 @@ import pathlib
 import gi
 import pytest
 
+from gstlal.utilities import testtools
+
 gi.require_version('Gst', '1.0')
 from gi.repository import GObject, Gst
 
@@ -348,7 +350,7 @@ class TestDataSourceInfoBasicSource:
 			assert_info_mkbasicsrc(info, Detector.H1.value)
 
 
-@pytest.mark.requires_gstlal_ugly
+@testtools.requires_full_build
 class TestDataSourceInfoBasicSourceUgly:
 	"""Test class for new DataSourceInfo api, these
 	tests require gstlal-ugly to be build first so they are grouped separately
@@ -398,7 +400,7 @@ class TestDataSourceInfoBasicSourceOptParse:
 	These tests are the specific drop-in replacements for the old api usage in scripts
 	"""
 
-	@pytest.mark.skip('Fake sources not currently working with Gst 1.0 api')
+	@testtools.broken('Fake sources not currently working with Gst 1.0 api')
 	def test_mkbasicsrc_aligo(self):
 		"""Test that new api works with mkbasicsrc, datasource: AdvLIGO"""
 		with GstLALTestManager() as gsttm:
@@ -415,7 +417,7 @@ class TestDataSourceInfoBasicSourceOptParse:
 			info = datasource.DataSourceInfo.from_optparse(options)
 			assert_info_mkbasicsrc(info, Detector.H1.value)
 
-	@pytest.mark.skip('Fake sources not currently working with Gst 1.0 api')
+	@testtools.broken('Fake sources not currently working with Gst 1.0 api')
 	def test_mkbasicsrc_avirgo(self):
 		"""Test that new api works with mkbasicsrc, datasource: AdvVirgo"""
 		with GstLALTestManager() as gsttm:
@@ -432,7 +434,7 @@ class TestDataSourceInfoBasicSourceOptParse:
 			info = datasource.DataSourceInfo.from_optparse(options)
 			assert_info_mkbasicsrc(info, Detector.H1.value)
 
-	@pytest.mark.skip('Fake sources not currently working with Gst 1.0 api')
+	@testtools.broken('Fake sources not currently working with Gst 1.0 api')
 	def test_mkbasicsrc_ligo(self):
 		"""Test that new api works with mkbasicsrc"""
 		with GstLALTestManager() as gsttm:
@@ -482,7 +484,7 @@ class TestDataSourceInfoBasicSourceOptParse:
 			assert_info_mkbasicsrc(info, Detector.H1.value)
 
 
-@pytest.mark.requires_gstlal_ugly
+@testtools.requires_full_build
 class TestDataSourceInfoBasicSourceOptParseUgly:
 	"""Test class wrapper for testing new datasource api optparse integration
 	These tests are the specific drop-in replacements for the old api usage in scripts
diff --git a/gstlal/tests/tests_pytest/test_gsttools.py b/gstlal/tests/tests_pytest/test_gsttools.py
new file mode 100644
index 0000000000000000000000000000000000000000..9df946cf436fb2afd1d221ea7c7aaa52d89d3e2a
--- /dev/null
+++ b/gstlal/tests/tests_pytest/test_gsttools.py
@@ -0,0 +1,24 @@
+"""Unit tests for pipetools module"""
+
+from gstlal import gsttools
+from gstlal.pipeparts import pipetools
+from gstlal.utilities import testtools
+
+
+class TestGsttools:
+	"""Test class group for gsttools"""
+
+	def test_is_element(self):
+		"""Test is_element utility"""
+		assert not gsttools.is_element(1)
+		with testtools.GstLALTestManager(with_pipeline=True) as tm:
+			elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc')
+			assert gsttools.is_element(elem)
+
+	def test_is_pad(self):
+		"""Test is_pad utility"""
+		assert not gsttools.is_element(1)
+		with testtools.GstLALTestManager(with_pipeline=True) as tm:
+			elem = pipetools.make_element_with_src(tm.pipeline, None, 'fakesrc')
+			assert not gsttools.is_pad(elem)
+			assert gsttools.is_pad(elem.pads[0])