Files
seiscomp-training/bin/capstool

1246 lines
40 KiB
Python
Executable File

#!/usr/bin/env python3
###############################################################################
# Copyright (C) 2012 by gempa GmbH #
# #
# All Rights Reserved. #
# #
# NOTICE: All information contained herein is, and remains #
# the property of gempa GmbH and its suppliers, if any. The intellectual #
# and technical concepts contained herein are proprietary to gempa GmbH #
# and its suppliers. #
# Dissemination of this information or reproduction of this material #
# is strictly forbidden unless prior written permission is obtained #
# from gempa GmbH. #
# #
# Author: Enrico Ellguth, Stephan Herrnkind #
# Email: ellguth@gempa.de, herrnkind@gempa.de #
###############################################################################
"""Usage:
capstool [options] -P
capstool [options] -Q
capstool [options] [request-file(s)]
Retrieve data and meta information from CAPS server.
Options:
-h, --help Display this help message and exit.
-H, --host=HOST[:PORT] Host and optionally port of the CAPS server
(default: localhost:18002).
-s, --ssl Use secure socket layer (SSL).
-c, --credentials=USER[:PASS] Authentication credentials. If password is
omitted, it is asked for on command-line.
-P, --ping Retrieve server version information and exit.
-Q, Print availability extents of all data streams.
-I, --info-streams=FILTER Like -Q but with a use a regular filter
expression for the requested streams, e.g.,
AM.*.
--filter-list=FILTER Identical to -I.
--mtime [start]:[end] Restrict request to record modification time
window. Time format:
%Y,%m,%d[,%H[,%M[,%S[,%f]]]]
-X, --info-server Request server statistics in JSON format
--modified-after=TIME Limit server statistics request to data
modified after specific time. Time format:
%Y,%m,%d[,%H[,%M[,%S[,%f]]]]
--force Disable any confirmation prompts.
Options (request file, no data download):
-G, --print-gaps Request list of data gaps.
-S, --print-segments Request list of continuous data segments.
--tolerance=SECONDS Threshold in seconds defining a data gap (decimal
point, microsecond precision, default: 0).
-R, --resolution=DAYS The resolution in multiple of days of the returned
data segments or gaps (default: 0). A value of 0
returns segments based on stored data records. A
value larger than zero will return the minimum and
maximum data time of one, two or more days.
Consecutive segments will be merged if end and start
time are within the tolerance.
--print-stat Request storage information with a granularity of
one day.
--purge Deletes data from CAPS archive with a granularity of
one day. Any data file intersecting with the time
window will be purged. The user requires the purge
permission.
Options (request file and data download):
-o, --output-file=FILE Output file for received data (default: -).
The file name is used as a prefix with the
extension added based on the record type (MSEED,
RAW, ANY, META, HELI). Multiple files are created
if mixed data types are received. For 'ANY' records
the file name is set to PREFIX_STREAMID_DATE.TYPE
--any-date-format Date format to use for any files, see
'man strftime' (default: %Y%m%d_%H%M%S).
-t, --temp-file=FILE Use temporary file to store data. On success
move to output-file.
--rt Enable real-time mode.
--ooo, --out-of-order Request data in order of transmission time instead
of sampling time.
-D, --heli Request down-sampled data (1Hz). The server will
taper, bandpass filter and re-sample the data.
--itaper=SECONDS Timespan in SECONDS for the one-sided cosine taper.
--bandpass=RANGE Corner frequency RANGE of the bandpass filter,
e.g., 1.0:4.0.
-M, --meta Request record meta data only.
-v, --version=VERSION Request a specific format version. Currently only
supported in meta requests.
Request file format:
Line based with each line containing a start time, end time and a stream id:
- time representation: %Y,%m,%d[,%H[,%M[,%S[,%f]]]]
- leading zeros may be omitted.
- an open end time is expressed by an underscore (_)
- the stream ID is defined as NET STA LOC CHA with
- LOC omitted in case of an empty location code
- support for * and ? wildcards
Example:
2014,03,17,12,00,00 2014,03,17,13,00,00 AM R0F05 00 SHZ
2014,03,17,12,0,0 _ GE APE BH?
Examples:
Fetch data from CAPS server on localhost and save it to a file:
echo "2014,03,17,12,00,00 2014,03,17,13,00,00 NET STA LOC CHA" | capstool -o [file]
Fetch data from a CAPS server on HOSTNAME and save it to a file. Request details
are provided through the request file req.txt:
capstool -H HOSTNAME -o /tmp[file] req.txt
Fetch stream information from a CAPS server on HOSTNAME using SSL port and
authentication:
capstool -H HOSNAME:18004 -s -c USERNAME:PASSWORD -Q
Fetch gap statistic for given stream and time window from local CAPS server
echo "2022,05,01,12,00,00 2022,05,03,00,00,00 NET * * *" | capstool -G
Fetch segment statistic for given stream and time window from local CAPS server
using a gap threshold of 0.5 seconds
echo "2022,05,01,12,00,00 2022,05,03,00,00,00 NET * * *" | capstool -G --tolerance=0.5
Fetch disk usage statistic for given stream and time window from local CAPS server
echo "2022,05,01,12,00,00 2022,05,03,00,00,00 NET * * *" | capstool --print-stat
Purge data for given stream and time window from local CAPS server
echo "2022,05,01,12,00,00 2022,05,03,00,00,00 NET * * *" | capstool --purge
"""
import getopt
import getpass
import os
import re
import socket
import ssl
import struct
import sys
import traceback
from datetime import datetime, timedelta
from io import BytesIO
from typing import BinaryIO, List, NamedTuple, TextIO, Tuple
FullTimeFormat = "%F %T.%fZ"
CAPSTimeRegex = re.compile(
r"^\d{4},\d{1,2},\d{1,2}(,\d{1,2}(,\d{1,2}(,\d{1,2}(,\d{1,6})?)?)?)?$"
)
PIPE = "-"
ERR_OK = 0
ERR_UNKNOWN = 1
ERR_USAGE = 2
ERR_INPUT = 3
ERR_CONNECTION = 4
ERR_SERVER = 5
class CAPSToolError(Exception):
def __init__(self, message: str, error_code: int):
super().__init__(message)
self.error_code: int = error_code
class CAPSToolUsageError(CAPSToolError):
def __init__(self, message: str):
super().__init__(message, ERR_USAGE)
class CAPSToolInputError(CAPSToolError):
def __init__(self, message: str):
super().__init__(message, ERR_INPUT)
class CAPSToolConnectionError(CAPSToolError):
def __init__(self, message: str):
super().__init__(message, ERR_CONNECTION)
class CAPSToolServerError(CAPSToolError):
def __init__(self, message: str):
super().__init__(message, ERR_SERVER)
def py3bstr(s: str) -> bytes:
"""string to bytes"""
return s.encode("utf-8")
def py3ustr(b: bytes) -> str:
"""bytes to bytes"""
return b.decode("utf-8", "replace")
def error(msg: str) -> None:
print(f"[error] {msg}", file=sys.stderr)
def warning(msg: str) -> None:
print(f"[warning] {msg}", file=sys.stderr)
def info(msg: str) -> None:
print(f"[info] {msg}", file=sys.stderr)
def send(sock: socket.socket, data: str) -> None:
sock.send(py3bstr(data) + b"\n")
def read_buffer(sock: socket.socket, bufsize: int) -> bytearray:
data = bytearray()
while bufsize > 0:
req: int = min(1024, bufsize)
buf: bytes = sock.recv(req)
if len(buf) == 0:
break
data += buf
bufsize -= len(buf)
return data
def dataSize(fmt: str) -> Tuple[int, str]:
if fmt == "RAW/INT8":
return (1, "c")
if fmt == "RAW/INT16":
return (2, "h")
if fmt == "RAW/INT32":
return (4, "i")
if fmt == "RAW/INT64":
return (8, "q")
if fmt == "RAW/FLOAT":
return (4, "f")
if fmt == "RAW/DOUBLE":
return (8, "d")
return 0, ""
class Session(NamedTuple):
sid: str = ""
sfreq: str = ""
uom: str = ""
fmt: str = ""
def __bool__(self) -> bool:
return bool(self.sid)
def writeRAW(out: TextIO, buf: bytearray, session: Session) -> int:
"""
Write RAW record in SLIST format to output file
"""
startTime = unpackTime(buf).strftime("%Y-%m-%dT%H:%M:%S.%f")
size, t = dataSize(session.fmt)
if not size:
error(f"unsupported datatype: {session.fmt}")
return 0
headerLen = 12
dataLen = len(buf) - headerLen
sampleCount = dataLen // size
if sampleCount <= 0:
return 0
sid = session.sid.replace(".", "_")
dataType = session.fmt.split("/")[1]
toks = session.sfreq.split("/")
freq = int(toks[0]) / int(toks[1])
if freq.is_integer():
freq = int(freq)
# TIMESERIES CX_PB11__BHZ_R, 1588 samples, 40 sps, 2019-05-19T19:10:53.225000,
# SLIST, FLOAT, M/S
print(
f"TIMESERIES {sid}_R, {sampleCount} samples, {freq} sps, {startTime}, SLIST, "
f"{dataType}, {session.uom}",
file=out,
)
for i in range(sampleCount):
ofs = i * size + headerLen
value = struct.unpack(t, buf[ofs : ofs + size])[0]
print(value, file=out)
return sampleCount
def writeHeli(out: TextIO, buf: bytearray, session: Session) -> int:
"""
Write HELI in SLIST format to output file
Each HELI sample consists of 3 double values:
start time (seconds since epoch), minimum, maximum
The expected sample rate is 1s. We double the rate and write the minimum and
aximum value as alternating values.
"""
recLen = 3 * 8
bufLen = len(buf)
sampleCount = bufLen // recLen
if sampleCount <= 0:
return 0
secs, minimum, maximum = struct.unpack("<ddd", buf[:recLen])
startTime = datetime.utcfromtimestamp(secs).strftime("%Y-%m-%dT%H:%M:%S.%f")
sid = session.sid.replace(".", "_")
# original frequency unused
# toks = session.sfreq.split("/")
# freq = int(toks[0]) / int(toks[1])
# TIMESERIES CX_PB11__BHZ_R, 12 samples, 2 sps, 2019-05-19T19:10:53.225000, SLIST,
# FLOAT, cnt
print(
f"TIMESERIES {sid}_R, {sampleCount * 2} samples, 2 sps, {startTime}, SLIST, "
f"FLOAT, {session.uom}",
file=out,
)
print(minimum, file=out)
print(maximum, file=out)
for i in range(1, sampleCount):
ofs = i * recLen
_secs, minimum, maximum = struct.unpack("<ddd", buf[ofs : ofs + recLen])
print(minimum, file=out)
print(maximum, file=out)
return sampleCount
def unpackTime(buf: bytearray, offset: int = 0) -> datetime:
secs, usecs = struct.unpack("<QI", buf[offset : offset + 12])
return datetime.utcfromtimestamp(secs + usecs / 10**6)
class SessionTable:
def __init__(self) -> None:
self.session_by_id: dict[int, Session] = {}
self.id_by_sid: dict[str, int] = {}
def _handleRequests(self, lines: List[str]) -> None:
d: dict[str, str] = {}
for line in lines:
if line == "END":
break
start = 0
pos = 0
while True:
pos = line.find(":", start)
if pos == -1:
break
key = line[start:pos]
start = pos + 1
pos = line.find(",", start)
if pos == -1:
value = line[start:]
else:
value = line[start:pos]
start = pos + 1
d[key] = value
if "ID" in d:
sessionID = int(d["ID"])
sid = d["SID"]
if sessionID == -1:
sessionID = self.id_by_sid[sid]
del self.id_by_sid[sid]
del self.session_by_id[sessionID]
else:
session = Session(sid, d["SFREQ"], d["UOM"], d["FMT"])
self.id_by_sid[sid] = sessionID
self.session_by_id[sessionID] = session
def handleResponse(self, sock: socket.socket) -> Tuple[bytearray, Session]:
# read initial response from server
buf = read_buffer(sock, 6)
if len(buf) != 6:
raise CAPSToolServerError("invalid response from server")
(sessionID, size) = struct.unpack("=HI", buf)
buf = read_buffer(sock, size)
if sessionID == 0:
resp = py3ustr(buf)
if resp.startswith("ERROR:"):
raise CAPSToolServerError(f"server responded with: {resp.strip()}")
toks = resp.split("\n")
# end of data
if toks[0] == "EOD":
return bytearray(), Session()
# session table modified
if toks[0] == "REQUESTS":
self._handleRequests(toks[1:])
elif toks[0] != "STATUS OK":
warning(f"server responded with unknown key word: {toks[0]}")
return buf, Session()
# unknown sessionID
if sessionID not in self.session_by_id:
raise CAPSToolServerError(
f"server responded with unknown session id: {sessionID}"
)
# read packet data
return buf, self.session_by_id[sessionID]
class CAPSTool:
class SEEDParams:
def __init__(self) -> None:
self.enable = False
self.resp_dict = False
def __init__(self) -> None:
self.host = "localhost"
self.port = 18002
self.useSSL = False
self.username = ""
self.password = ""
self.pingServer = False
self.infoStreams = ""
self.infoServer = False
self.mtime = ""
self.inputFiles = [PIPE]
self.output = PIPE
self.anyDateFormat = "%Y%m%d_%H%M%S"
self.tmpOutputFile = ""
self.printGaps = False
self.printSegments = False
self.printStat = False
self.tolerance = 0
self.resolution = 0
self.real_time = False
self.ooo = False
self.heli = False
self.itaper = ""
self.bandpass = ""
self.meta = False
self.formatVersion = ""
self.purge = False
self.force = False
self.SEED = self.SEEDParams()
self._stdin_dirty = False
def parse_args(self, args: List[str]) -> None:
try:
opts, args = getopt.gnu_getopt(
args,
"H:sc:PQI:XGSR:o:t:MDv:",
[
"ping",
"host=",
"ssl",
"credentials=",
"ping",
"info-streams=",
"filter-list=",
"mtime=",
"info-server",
"modified-after=",
"print-gaps",
"print-segments",
"output-file=",
"any-date-format=",
"temp-file=",
"rt",
"tolerance=",
"resolution=",
"ooo",
"out-of-order",
"heli",
"itaper=",
"bandpass=",
"meta",
"version=",
"purge",
"force",
"print-stat",
],
)
except getopt.GetoptError as err:
# will print something like "option -a not recognized"
raise CAPSToolUsageError(str(err)) from err
infoCommands = set()
downloadOpts = set()
for o, a in opts:
if o in ["-H", "--host"]:
toks = a.split(":", 1)
if toks[0]:
self.host = toks[0]
if len(toks) == 2 and toks[1]:
try:
self.port = int(toks[1])
except ValueError as e:
raise CAPSToolUsageError(
f"invalid port given: {toks[1]}"
) from e
elif o in ["-s", "--ssl"]:
self.useSSL = True
elif o in ["-c", "--credentials"]:
toks = a.split(":", 1)
if len(toks) > 1:
self.username = toks[0]
self.password = toks[1]
else:
self.username = toks[0]
# ask for password
self.password = getpass.getpass()
elif o in ["-P", "--ping"]:
self.pingServer = True
infoCommands.add("PING")
elif o in ["-Q"]:
self.infoStreams = "*"
infoCommands.add("INFO STREAMS")
elif o in ["-I", "--info-streams", "--filter-list"]:
self.infoStreams = a if a else "*"
infoCommands.add("INFO STREAMS")
elif o in ["--mtime"]:
toks = a.split(":")
if len(toks) != 2:
raise CAPSToolUsageError(
"invalid numbers of colons in --mtime parameter"
)
if toks[0] and not CAPSTimeRegex.match(toks[0]):
raise CAPSToolUsageError("invalid start time in --mtime parameter")
if toks[1] and not CAPSTimeRegex.match(toks[1]):
raise CAPSToolUsageError("invalid end time in --mtime parameter")
self.mtime = a
elif o in ["-X", "--info-server"]:
infoCommands.add("INFO SERVER")
elif o in ["--modified-after"]:
if not CAPSTimeRegex.match(a):
raise CAPSToolUsageError("invalid time in --modified-after")
self.mtime = a
elif o in ["-G", "--print-gaps"]:
self.printGaps = True
infoCommands.add("GAPS")
elif o in ["-S", "--print-segments"]:
self.printSegments = True
infoCommands.add("SEGMENTS")
elif o in ["--print-stat"]:
self.printStat = True
infoCommands.add("STATS")
elif o in ["--tolerance"]:
try:
self.tolerance = int(round(float(a) * 10**6))
except ValueError as e:
raise CAPSToolUsageError(
"invalid floating point value in --tolerance parameter"
) from e
elif o in ["-R", "--resolution"]:
if not a.isdigit():
raise CAPSToolUsageError(
"invalid integer value in --resolution parameter"
)
self.resolution = int(a)
elif o in ["-o", "--output-file"]:
self.output = a
downloadOpts.add(o)
elif o in ["--any-date-format"]:
self.anyDateFormat = a
downloadOpts.add(o)
elif o in ["-t", "--temp-file"]:
self.tmpOutputFile = a
downloadOpts.add(o)
elif o in ["--rt"]:
self.real_time = True
downloadOpts.add(o)
elif o in ["--ooo", "--out-of-order"]:
self.ooo = True
downloadOpts.add(o)
elif o in ["-D", "--heli"]:
self.heli = True
downloadOpts.add(o)
elif o in ["--itaper"]:
self.itaper = a
downloadOpts.add(o)
elif o in ["--bandpass"]:
self.bandpass = a
downloadOpts.add(o)
elif o in ["-M", "--meta"]:
self.meta = True
downloadOpts.add(o)
elif o in ["-v", "--version"]:
self.formatVersion = a
downloadOpts.add(o)
elif o in ["--purge"]:
self.purge = True
elif o in ["--force"]:
self.force = True
else:
raise CAPSToolUsageError(f"unhandled option: {o}")
# check for invalid command combinations
if len(infoCommands) > 1:
raise CAPSToolUsageError(
f"more than one info command requested: {', '.join(infoCommands)}"
)
# read request file arguments
if len(args) > 0:
self.inputFiles = args
if self.pingServer:
raise CAPSToolUsageError(
"PING command and request file(s) may not be combined"
)
if self.infoStreams:
raise CAPSToolUsageError(
"INFO STREAMS command and request file(s) may not be combined"
)
# check for info command and download option combinations
if infoCommands and downloadOpts:
raise CAPSToolUsageError(
"info commands and download options may not be combined\n"
f" info command(s) : {', '.join(infoCommands)}\n"
f" download option(s): {', '.join(downloadOpts)}"
)
# check for invalid parameter combinations
if self.tmpOutputFile and self.output is PIPE:
raise CAPSToolUsageError(
"temporary output file not supported when writing to stdout"
)
if self.tolerance and not (self.printGaps or self.printSegments):
raise CAPSToolUsageError(
"tolerance is only supported in GAPS and SEGMENTS requests"
)
if self.resolution and not (self.printGaps or self.printSegments):
raise CAPSToolUsageError(
"resolution is only supported in GAPS and SEGMENTS requests"
)
if self.formatVersion and not self.meta:
raise CAPSToolUsageError(
"format version is currently only supported in META requests"
)
if self.heli:
if self.meta:
raise CAPSToolUsageError("HELI and META requests may not be combined")
elif self.itaper:
raise CAPSToolUsageError(
"itaper parameter only supported for HELI requests"
)
elif self.bandpass:
raise CAPSToolUsageError(
"bandpass parameter only supported for HELI requests"
)
if self.mtime and self.real_time:
raise CAPSToolUsageError(
"mtime data filter not available in real-time mode"
)
def connect(self) -> socket.socket:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
if self.useSSL:
context = ssl.SSLContext()
sock = context.wrap_socket(sock)
sock.connect((self.host, self.port))
except socket.error as e:
raise CAPSToolConnectionError(
f"could not establish connection to {self.host}:{self.port}"
) from e
info(f"connected to {self.host}:{self.port}")
return sock
@staticmethod
def disconnect(sock: socket.socket) -> None:
send(sock, "BYE")
sock.close()
def authenticate(self, sock: socket.socket) -> None:
if self.username:
send(sock, f"AUTH {self.username} {self.password}")
def beginRequest(self, sock: socket.socket) -> None:
send(sock, "BEGIN REQUEST")
if self.mtime:
send(sock, f"MTIME {self.mtime}")
if self.printGaps:
send(sock, "GAPS ON")
if self.printSegments:
send(sock, "SEGMENTS ON")
if self.printStat:
send(sock, "STAT")
if self.tolerance:
send(sock, f"TOLERANCE {self.tolerance}")
if self.resolution:
send(sock, f"RESOLUTION {self.resolution}")
if not self.real_time:
send(sock, "REALTIME OFF")
if self.ooo:
send(sock, "OUTOFORDER ON")
if self.meta:
if self.formatVersion:
send(sock, f"META@{self.formatVersion} ON")
else:
send(sock, "META ON")
@staticmethod
def endRequest(sock: socket.socket) -> None:
send(sock, "END")
@staticmethod
def readDefaultResponse(sock: socket.socket) -> None:
buf = read_buffer(sock, 6)
if len(buf) != 6:
raise CAPSToolServerError("unexpected response from server (len != 6)")
(code, size) = struct.unpack("=HI", buf)
if code != 0:
raise CAPSToolServerError("unexpected response from server (id != 0)")
if size < 0:
raise CAPSToolServerError(
"unexpected response from server (negative data size)"
)
buf = read_buffer(sock, size)
print(py3ustr(buf))
def addRequestLinesFromFile(self, buffer: BinaryIO, f: TextIO) -> None:
def handleError(reason: str) -> None:
msg = f"invalid request line #{lc}, {reason}"
if f.isatty():
error(msg)
else:
print(f" > {line}", file=sys.stderr)
raise CAPSToolInputError(msg)
if self.heli:
heli_filter = ""
if self.itaper:
heli_filter += f" ITAPER {self.itaper}"
if self.bandpass:
heli_filter += f" BANDPASS {self.bandpass}"
lc = 0
for line in f:
lc += 1
line = line.strip()
if not line or line[0] == "#":
print(f" + {lc:>5}: SKIP", file=sys.stderr)
continue
toks = line.split()
if len(toks) < 5:
handleError("less than 5 columns given")
continue
if toks[0] == "_":
startTime = ""
else:
if CAPSTimeRegex.match(toks[0]):
startTime = toks[0]
else:
handleError("invalid start time in column 1")
continue
if toks[1] == "_":
endTime = ""
else:
if CAPSTimeRegex.match(toks[1]):
endTime = toks[1]
else:
handleError("invalid end time in column 2")
continue
print(f" + {lc:>5}: OK", file=sys.stderr)
# pylint: disable=consider-using-f-string
if len(toks) == 5:
streamID = "{}.{}..{}".format(*toks[2:5])
else:
streamID = "{}.{}.{}.{}".format(*toks[2:6])
if self.heli:
buffer.write(py3bstr(f"HELI ADD {streamID}{heli_filter}\n"))
else:
buffer.write(py3bstr(f"STREAM ADD {streamID}\n"))
buffer.write(py3bstr(f"TIME {startTime}:{endTime}\n"))
def prompt_for_confirmation(self, text: str) -> bool:
if self._stdin_dirty:
os.close(0)
tty = "/dev/tty"
if os.open(tty, os.O_RDONLY):
raise OSError(f"could not open {tty}")
self._stdin_dirty = False
while True:
response = input(text).lower()
if response in ["yes", "y"]:
return True
if response in ["no", "n"]:
return False
print(" Respond with 'y' or 'n'.")
def requestLines(self) -> BytesIO:
buffer = BytesIO()
# read request lines from stdin or files(s)
if PIPE in self.inputFiles:
if sys.stdin.isatty():
print(
"""Please input request lines of FORMAT followed by Ctrl+D
FORMAT: starttime endtime NET STA [LOC] CHA
Example: 2022,05,01,12,00,00 2022,05,03,00,00,00 AM R0F05 * *""",
file=sys.stderr,
)
else:
info("reading request lines from stdin:")
self.addRequestLinesFromFile(buffer, sys.stdin)
self._stdin_dirty = True
else:
for file in self.inputFiles:
info(f"reading request lines from {file}:")
try:
with open(file, "r", encoding="utf8") as f:
self.addRequestLinesFromFile(buffer, f)
except IOError as e:
raise CAPSToolInputError(
f"could not read request file {file}"
) from e
return buffer
def cmdPing(self) -> None:
sock = self.connect()
send(sock, "HELLO")
self.readDefaultResponse(sock)
self.disconnect(sock)
def cmdInfoStreams(self) -> None:
sock = self.connect()
self.authenticate(sock)
send(sock, f"INFO STREAMS {self.infoStreams}")
self.readDefaultResponse(sock)
self.disconnect(sock)
def cmdInfoServer(self) -> None:
sock = self.connect()
self.authenticate(sock)
mtime = f" MODIFIED AFTER {self.mtime}" if self.mtime else ""
send(sock, f"INFO SERVER{mtime}")
self.readDefaultResponse(sock)
self.disconnect(sock)
def cmdSegments(self) -> None:
buffer = self.requestLines()
sock = self.connect()
self.authenticate(sock)
self.beginRequest(sock)
sock.send(buffer.getvalue())
self.endRequest(sock)
# pylint: disable=consider-using-f-string
print(f"{'Stream ID': <15} {'Start': <27} {'End': <27}")
sessionTable = SessionTable()
while True:
buf, session = sessionTable.handleResponse(sock)
if not buf: # EOD
break
if not session:
continue
start = unpackTime(buf)
end = unpackTime(buf, 12)
print(
"{0: <15} {1: <27} {2: <27}".format(
session.sid,
start.strftime(FullTimeFormat),
end.strftime(FullTimeFormat),
)
)
def cmdStat(self) -> None:
buffer = self.requestLines()
sock = self.connect()
self.authenticate(sock)
self.beginRequest(sock)
sock.send(buffer.getvalue())
self.endRequest(sock)
self.readDefaultResponse(sock)
self.disconnect(sock)
def cmdPurge(self) -> None:
buffer = self.requestLines()
if not self.force and not self.prompt_for_confirmation(
"Do you really want to remove the selected streams permanently from CAPS "
"archive (y/n)? "
):
return
sock = self.connect()
self.authenticate(sock)
send(sock, "BEGIN PURGE")
sock.send(buffer.getvalue())
self.endRequest(sock)
self.readDefaultResponse(sock)
self.disconnect(sock)
def cmdDownload(self) -> None:
# pylint: disable=consider-using-with
# initialize out variables which depend on Python version used
mseed_out = any_out = raw_out = meta_out = heli_out = None
if not self.output or self.output == PIPE:
write_to_stdout = True
mseed_out = any_out = sys.stdout.buffer
raw_out = meta_out = heli_out = sys.stdout
mseed_file_name = any_file_name = raw_file_name = meta_file_name = (
heli_file_name
) = "stdout"
else:
write_to_stdout = False
if self.output[-6:].lower() == ".mseed":
mseed_file_name = self.output
else:
mseed_file_name = self.output + ".mseed"
any_time = f"_{self.anyDateFormat}" if self.anyDateFormat else ""
any_file_name = f"files of form {self.output}_STREAMID{any_time}.TYPE"
if self.output[-4:].lower() == ".raw":
raw_file_name = self.output
else:
raw_file_name = self.output + ".raw"
if self.output[-5:].lower() == ".meta":
meta_file_name = self.output
else:
meta_file_name = self.output + ".meta"
if self.output[-5:].lower() == ".heli":
heli_file_name = self.output
else:
heli_file_name = self.output + ".heli"
mseed_bytes = 0
any_bytes = 0
raw_samples = 0
heli_samples = 0
meta_records = 0
buffer = self.requestLines()
sock = self.connect()
self.authenticate(sock)
self.beginRequest(sock)
sock.send(buffer.getvalue())
self.endRequest(sock)
sessionTable = SessionTable()
while True:
buf, session = sessionTable.handleResponse(sock)
if not buf: # EOD
break
if not session:
continue
# MSEED: Write binary MSeed data to stdout or single output file
if session.fmt == "MSEED":
if not mseed_out:
mseed_out = open(mseed_file_name, "wb")
mseed_out.write(buf)
mseed_bytes += len(buf)
# ANY: Write binary data to stdout or individual output files
elif session.fmt == "ANY":
if any_out:
any_out.write(buf)
any_bytes += len(buf)
continue
type_len = 4
for i in range(0, 4):
if buf[i] == 0:
type_len = i
break
packetType = py3ustr(buf[0:type_len]).lower()
year, yday, hour, minute, second, usec = struct.unpack(
"=hHBBBI", buf[5:16]
)
filename = f"{self.output}_{session.sid}"
if self.anyDateFormat:
time = datetime(year, 1, 1, hour, minute, second, usec)
time += timedelta(days=yday)
filename += f"_{time.strftime(self.anyDateFormat)}"
ext = f".{packetType if packetType else 'any'}"
if filename[-len(ext) :].lower() != ext:
filename += ext
if self.tmpOutputFile:
with open(self.tmpOutputFile, "wb") as f:
f.write(buf[31:])
os.rename(self.tmpOutputFile, filename)
else:
with open(filename, "wb") as f:
f.write(buf[31:])
any_bytes += len(buf) - 31
# RAW: Write SLIST string data to stdout or single output file
elif session.fmt[:3] == "RAW":
if not raw_out:
raw_out = open(raw_file_name, "w", encoding="utf8")
raw_samples += writeRAW(raw_out, buf, session)
# HELI: Write SLIST string data to stdout or single output file
elif session.fmt[:4] == "HELI":
if not heli_out:
heli_out = open(heli_file_name, "w", encoding="utf8")
heli_samples = writeHeli(heli_out, buf, session)
# META: Write string data to stdout or single output file
elif session.fmt == "META" or session.fmt[:5] == "META@":
version = 1
if len(session.fmt) > 4:
try:
version = int(session.fmt[5:])
except ValueError:
error(
f"Invalid META record version number '{session.fmt[5:]}' "
f"for SID '{session.sid}'"
)
continue
if version > 2:
error(
f"Unsupported META record version number '{version}' for SID "
f"'{session.sid}'"
)
continue
if version > 1:
arrivalTime = unpackTime(buf, 24).strftime(FullTimeFormat)
else:
arrivalTime = ""
if not meta_out:
meta_out = open(meta_file_name, "w", encoding="utf8")
# print header for first record
if not meta_records:
print(
"#Stream ID|Sampling Frequency|Unit of Measurement"
"|Start Time|End Time|Arrival Time",
file=meta_out,
)
print(
f"{session.sid}|{session.sfreq}|{session.uom}|"
f"{unpackTime(buf, 0).strftime(FullTimeFormat)}|"
f"{unpackTime(buf, 12).strftime(FullTimeFormat)}|{arrivalTime}",
file=meta_out,
)
meta_records += 1
# unsupported session format
else:
error(f"Unsupported session FMT '{session.fmt}' in SID '{session.sid}'")
# cleanup: flush buffers, close files
if write_to_stdout:
sys.stdout.flush()
else:
for fd in [mseed_out, raw_out, meta_out, heli_out]:
if fd:
fd.close()
if any((mseed_bytes, any_bytes, raw_samples, heli_samples, meta_records)):
msg = "Data acquisition completed:"
if mseed_bytes:
msg += f"\n + MSEED: Wrote {mseed_bytes} bytes to {mseed_file_name}"
if any_bytes:
msg += f"\n + ANY : Wrote {any_bytes} bytes to {any_file_name}"
if raw_samples:
msg += f"\n + RAW : Wrote {raw_samples} samples to {raw_file_name}"
if heli_samples:
msg += f"\n + HELI : Wrote {heli_samples} samples to {heli_file_name}"
if meta_records:
msg += f"\n + META : Wrote {meta_records} records to {meta_file_name}"
info(msg)
else:
info("No data written. Check request.")
self.disconnect(sock)
def __call__(self) -> None:
if self.pingServer:
self.cmdPing()
elif self.infoStreams:
self.cmdInfoStreams()
elif self.infoServer:
self.cmdInfoServer()
elif self.printSegments or self.printGaps:
self.cmdSegments()
elif self.printStat:
self.cmdStat()
elif self.purge:
self.cmdPurge()
else:
self.cmdDownload()
def main(args: List[str]) -> None:
if "-h" in args or "--help" in args:
print(__doc__, file=sys.stderr)
return
app = CAPSTool()
app.parse_args(args)
app()
if __name__ == "__main__":
exit_code = ERR_OK
try:
main(sys.argv[1:])
except KeyboardInterrupt:
error("interrupted")
except EOFError:
error("end of file")
except CAPSToolError as main_e:
error(str(main_e))
exit_code = main_e.error_code
except Exception as main_e:
error(str(main_e))
traceback.print_exc(file=sys.stderr)
exit_code = ERR_UNKNOWN
sys.exit(exit_code)