[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/llm 1f3b018dcb 03/71: Merge pull request #26 from r0man
From: |
ELPA Syncer |
Subject: |
[elpa] externals/llm 1f3b018dcb 03/71: Merge pull request #26 from r0man/plz |
Date: |
Fri, 17 May 2024 00:58:42 -0400 (EDT) |
branch: externals/llm
commit 1f3b018dcb9870667ed92c5d9c14190701cb3dfb
Merge: 9c33eb4a91 a9cd296cd8
Author: Andrew Hyatt <ahyatt@gmail.com>
Commit: GitHub <noreply@github.com>
Merge pull request #26 from r0man/plz
Add Plz
---
llm-request-plz.el | 146 +++++++++
plz-event-source.el | 458 ++++++++++++++++++++++++++
plz-media-type.el | 341 ++++++++++++++++++++
plz.el | 912 ++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1857 insertions(+)
diff --git a/llm-request-plz.el b/llm-request-plz.el
new file mode 100644
index 0000000000..d103f10448
--- /dev/null
+++ b/llm-request-plz.el
@@ -0,0 +1,146 @@
+;;; llm-request-plz.el --- Curl request handling code -*- lexical-binding: t;
package-lint-main-file: "llm.el"; -*-
+
+;; Copyright (c) 2023 Free Software Foundation, Inc.
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation; either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;; This file provides basic functions for providers who need to request data.
It
+;; assumes the server is using json.
+
+;;; Code:
+(require 'cl-macs)
+(require 'json)
+(require 'plz-event-source)
+(require 'plz-media-type)
+(require 'rx)
+(require 'url-http)
+
+(defcustom llm-request-plz-timeout 60
+ "The number of seconds to wait for a response from a HTTP server.
+
+Request timings are depending on the request. Requests that need
+more output may take more time, and there is other processing
+besides just token generation that can take a while. Sometimes
+the LLM can get stuck, and you don't want it to take too long.
+This should be balanced to be good enough for hard requests but
+not very long so that we can end stuck requests."
+ :type 'integer
+ :group 'llm)
+
+(cl-defun llm-request-plz-sync-raw-output (url &key headers data timeout)
+ "Make a request to URL. The raw text response will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+TIMEOUT is the number of seconds to wait for a response."
+ (condition-case error
+ (plz-media-type-request
+ 'post url
+ :as `(media-types ,plz-media-types)
+ :body (when data
+ (encode-coding-string (json-encode data) 'utf-8))
+ :headers (append headers '(("Content-Type" . "application/json")))
+ :timeout (or timeout llm-request-plz-timeout))
+ (plz-error
+ (seq-let [error-sym message data] error
+ (cond
+ ((eq 'plz-http-error error-sym)
+ (let ((response (plz-error-response data)))
+ (error "LLM request failed with code %d: %s (additional
information: %s)"
+ (plz-response-status response)
+ (nth 2 (assq (plz-response-status response) url-http-codes))
+ (plz-response-body response))))
+ ((and (eq 'plz-curl-error error-sym)
+ (eq 28 (car (plz-error-curl-error data))))
+ (error "LLM request timed out"))
+ (t (signal error-sym (list message data))))))))
+
+(cl-defun llm-request-plz-sync (url &key headers data timeout)
+ "Make a request to URL. The parsed response will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+TIMEOUT is the number of seconds to wait for a response."
+ (llm-request-plz-sync-raw-output url
+ :headers headers
+ :data data
+ :timeout timeout))
+
+(cl-defun llm-request-plz-async (url &key headers data on-success
on-success-raw on-error _on-partial event-stream-handlers timeout)
+ "Make a request to URL.
+Nothing will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+ON-SUCCESS will be called with the response body as a json
+object. This is optional in the case that ON-SUCCESS-DATA is set,
+and required otherwise.
+
+ON-ERROR will be called with the error code and a response-body.
+This is required.
+
+ON-PARTIAL will be called with the potentially incomplete response
+body as a string. This is an optional argument.
+
+ON-SUCCESS-RAW, if set, will be called in the buffer with the
+response body, and expect the response content. This is an
+optional argument, and mostly useful for streaming. If not set,
+the buffer is turned into JSON and passed to ON-SUCCESS."
+ (plz-media-type-request
+ 'post url
+ :as `(media-types ,(cons (cons "text/event-stream"
+ (plz-media-type:text/event-stream
+ :events event-stream-handlers))
+ plz-media-types))
+ :body (when data
+ (encode-coding-string (json-encode data) 'utf-8))
+ :headers (append headers
+ '(("Accept-encoding" . "identity")
+ ("Content-Type" . "application/json")))
+ :then (lambda (response)
+ (when on-success-raw
+ (user-error "Not supported yet: on-success-raw"))
+ (when on-success
+ (funcall on-success response)))
+ :else (lambda (error)
+ (when on-error
+ (funcall on-error error)))
+ :timeout (or timeout llm-request-plz-timeout)))
+
+;; This is a useful method for getting out of the request buffer when it's time
+;; to make callbacks.
+(defun llm-request-plz-callback-in-buffer (buf f &rest args)
+ "Run F with ARSG in the context of BUF.
+But if BUF has been killed, use a temporary buffer instead.
+If F is nil, nothing is done."
+ (when f
+ (if (buffer-live-p buf)
+ (with-current-buffer buf (apply f args))
+ (with-temp-buffer (apply f args)))))
+
+(provide 'llm-request-plz)
+;;; llm-request-plz.el ends here
diff --git a/plz-event-source.el b/plz-event-source.el
new file mode 100644
index 0000000000..c72e7bb7fa
--- /dev/null
+++ b/plz-event-source.el
@@ -0,0 +1,458 @@
+;;; plz-event-source.el --- Server Sent Event Source -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2019-2023 Free Software Foundation, Inc.
+
+;; Author: r0man <roman@burningswell.com>
+;; Maintainer: r0man <roman@burningswell.com>
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides a parser and an event source implementation
+;; for the Server Sent Event (SSE) protocol.
+
+;; See:
https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'eieio)
+(require 'pcase)
+(require 'plz)
+(require 'plz-media-type)
+(require 'rx)
+
+;; Event
+
+(defclass plz-event-source-event ()
+ ((data
+ :accessor plz-event-source-event-data
+ :initarg :data
+ :initform nil
+ :documentation "The event data.")
+ (last-event-id
+ :accessor plz-event-source-event-last-event-id
+ :initarg :last-event-id
+ :initform nil
+ :documentation "The last event id."
+ :type (or null string))
+ (origin
+ :accessor plz-event-source-event-origin
+ :initarg :origin
+ :initform nil
+ :documentation "The event origin."
+ :type (or null string))
+ (type
+ :accessor plz-event-source-event-type
+ :initarg :type
+ :initform "message"
+ :documentation "The event type."
+ :type string))
+ "The server sent event class.")
+
+;; Parser
+
+(defclass plz-event-source-parser ()
+ ((buffer
+ :documentation "The name of the buffer to read events from."
+ :initarg :buffer
+ :type string)
+ (events
+ :initarg :events
+ :initform nil
+ :documentation "The queue of events to dispatch."
+ :type (list-of plz-event-source-event))
+ (data-buffer
+ :initarg :data-buffer
+ :initform ""
+ :documentation "Data buffer."
+ :type string)
+ (event-type-buffer
+ :initarg :event-type-buffer
+ :initform ""
+ :documentation "Event type buffer."
+ :type string)
+ (last-event-id
+ :initarg :last-event-id
+ :initform ""
+ :documentation "Last event id."
+ :type string)
+ (last-event-id-buffer
+ :initarg :last-event-id-buffer
+ :initform ""
+ :documentation "Last event id buffer."
+ :type string)
+ (position
+ :initarg :position
+ :initform 0
+ :type integer
+ :documentation "The position in the buffer."
+ :type integer))
+ "The server sent event stream parser.")
+
+(defconst plz-event-source--end-of-line-regexp
+ (rx (or "\r\n" "\n" "\r"))
+ "Regular expression matching the end of a line.")
+
+(defconst plz-event-source--line-regexp
+ (rx (* not-newline) (or "\r\n" "\n" "\r"))
+ "Regular expression matching a line of the event source stream.")
+
+(defun plz-event-source--parse-bom (line)
+ "Parse the Byte Order Mark (BOM) from LINE."
+ (if (string-prefix-p "\uFEFF" line)
+ (substring line 1)
+ line))
+
+(defun plz-event-source--looking-at-line-p ()
+ "Return non-nil if the current line matches the event source line regexp."
+ (looking-at plz-event-source--line-regexp))
+
+(defun plz-event-source--parse-line ()
+ "Return non-nil if the current line matches the event source line regexp."
+ (when (looking-at plz-event-source--line-regexp)
+ (string-trim-right (delete-and-extract-region (match-beginning 0)
(match-end 0))
+ plz-event-source--end-of-line-regexp)))
+
+(defun plz-event-source--dispatch-event (parser)
+ "Dispatch an event from PARSER to registered listeners."
+ (with-slots (data-buffer event-type-buffer events last-event-id
last-event-id-buffer) parser
+ (setf last-event-id last-event-id-buffer)
+ (if (string-empty-p data-buffer)
+ (setf data-buffer ""
+ event-type-buffer "")
+ (progn
+ (setf data-buffer (string-trim-right data-buffer "\n"))
+ (let ((event (plz-event-source-event
+ :data data-buffer
+ :last-event-id (unless (string-blank-p last-event-id)
+ last-event-id)
+ :origin (buffer-name)
+ :type (if (string-blank-p event-type-buffer)
+ "message"
+ event-type-buffer))))
+ (setf data-buffer ""
+ event-type-buffer "")
+ (setf events (cons event events))
+ event)))))
+
+(defun plz-event-source--process-event (parser field value)
+ "Process the FIELD and VALUE from PARSER as a event."
+ (ignore field)
+ (with-slots (event-type-buffer) parser
+ (setf event-type-buffer value)))
+
+(defun plz-event-source--process-data (parser field value)
+ "Process the FIELD and VALUE from PARSER as data."
+ (ignore field)
+ (with-slots (data-buffer) parser
+ (setf data-buffer (concat data-buffer value "\n"))))
+
+(defun plz-event-source--process-id (parser field value)
+ "Process the FIELD and VALUE from PARSER as event id."
+ (ignore field)
+ (unless (string-match "\u0000" value)
+ (with-slots (last-event-id-buffer) parser
+ (setf last-event-id-buffer value))))
+
+(defun plz-event-source--process-retry (parser field value)
+ "Process the FIELD and VALUE from PARSER as event id."
+ (ignore parser)
+ (message "TODO: Process retry for field %s and value %s." field value))
+
+(defun plz-event-source--process-field (parser field value)
+ "Process the FIELD and VALUE from PARSER."
+ (cond ((equal "event" field)
+ (plz-event-source--process-event parser field value))
+ ((equal "data" field)
+ (plz-event-source--process-data parser field value))
+ ((equal "id" field)
+ (plz-event-source--process-id parser field value))
+ ((equal "retry" field)
+ (plz-event-source--process-retry parser field value))))
+
+(defun plz-event-source--process-line (parser line)
+ "Parse a LINE of the event stream PARSER and dispatch events."
+ (cond ((string-prefix-p ":" line))
+ ((string-blank-p line)
+ (plz-event-source--dispatch-event parser))
+ ((string-match ":" line)
+ (let ((field (substring line 0 (match-beginning 0)))
+ (value (substring line (match-end 0))))
+ (plz-event-source--process-field parser field
+ (if (string-prefix-p " " value)
+ (substring value 1)
+ value))))
+ (t (plz-event-source--process-field parser line ""))))
+
+(defun plz-event-source-parse-line (parser)
+ "Parse a line from the event stream in the PARSER buffer."
+ (with-slots (buffer position) parser
+ (with-current-buffer buffer
+ (save-excursion
+ (goto-char position)
+ (when-let (line (plz-event-source--parse-line))
+ (setf position (point))
+ (plz-event-source--process-line parser line)
+ line)))))
+
+(defun plz-event-source-parse-stream (parser)
+ "Parse the event stream in the the PARSER buffer."
+ (with-slots (buffer handlers) parser
+ (with-current-buffer (get-buffer buffer)
+ (goto-char (point-min))
+ (while (not (eobp))
+ (when-let (line (plz-event-source--parse-line))
+ (plz-event-source--process-line parser line))))))
+
+(defun plz-event-source-parser-insert (parser string)
+ "Insert STRING into the buffer of the event PARSER."
+ (with-slots (buffer events position) parser
+ (with-current-buffer (get-buffer buffer)
+ (insert string)
+ (while (plz-event-source-parse-line parser))
+ events)))
+
+;; Event Source
+
+(defclass plz-event-source ()
+ ((errors
+ :initarg :errors
+ :documentation "The errors of the event source.")
+ (handlers
+ :initarg :handlers
+ :initform nil
+ :documentation "Registered event handlers.")
+ (last-event-id
+ :initarg :last-event-id
+ :initform ""
+ :documentation "Last event id.")
+ (options
+ :initarg :options
+ :documentation "The url of the event source."
+ :type list)
+ (ready-state
+ :documentation "The ready state of the event source."
+ :initarg :ready-state
+ :initform 'closed
+ :type (member closed connecting open))
+ (url
+ :initarg :url
+ :documentation "The url of the event source."
+ :type (or null string)))
+ "The server sent event source class.")
+
+(cl-defgeneric plz-event-source-open (source)
+ "Open the event SOURCE.")
+
+(cl-defgeneric plz-event-source-close (source)
+ "Close the event SOURCE.")
+
+(cl-defgeneric plz-event-source-insert (source data)
+ "Insert DATA into the event SOURCE buffer, parse and dispatch events.")
+
+(defun plz-event-source-add-listener (source type listener)
+ "Add an event LISTENER for event TYPE to the event SOURCE."
+ (with-slots (handlers) source
+ (setf handlers (append handlers (list (cons type listener))))
+ source))
+
+(defun plz-event-source-remove-listener (source type listener)
+ "Remove an event LISTENER for event TYPE from the event SOURCE."
+ (with-slots (handlers) source
+ (setf handlers (cl-remove-if (lambda (pair)
+ (and (eq (car pair) type)
+ (eq (cdr pair) listener)))
+ handlers))
+ source))
+
+(defun plz-event-source-dispatch-event (source event)
+ "Dispatch the EVENT to the listeners of event SOURCE."
+ (with-slots (handlers) source
+ (dolist (pair handlers)
+ (when (equal (car pair) (oref event type))
+ (funcall (cdr pair) source event)))))
+
+(defun plz-event-source-dispatch-events (source events)
+ "Dispatch the EVENTS to the listeners of event SOURCE."
+ (dolist (event (reverse events))
+ (plz-event-source-dispatch-event source event)))
+
+(defun plz-event-source--response-in-buffer-p ()
+ "Return non-nil the if point is looking at a HTTP response."
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward plz-http-end-of-headers-regexp nil t)))
+
+(defun plz-event-source-parser--end-of-headers ()
+ "Return the end of headers position in the current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward plz-http-end-of-headers-regexp nil t)
+ (point)))
+
+;; Buffer event source
+
+(defclass plz-buffer-event-source (plz-event-source)
+ ((buffer
+ :initarg :buffer
+ :documentation "The event source buffer."
+ :type string)
+ (parser
+ :initarg :parser
+ :documentation "The event source parser."
+ :type (or null plz-event-source-parser)))
+ "A server sent event source using curl for HTTP.")
+
+(cl-defmethod plz-event-source-insert ((source plz-buffer-event-source) data)
+ "Insert DATA into the event SOURCE buffer, parse and dispatch events."
+ (with-slots (parser) source
+ (plz-event-source-parser-insert parser data)
+ (with-slots (events) parser
+ (plz-event-source-dispatch-events source events)
+ (setf events nil))))
+
+(defun plz-event-source--buffer-start-position ()
+ "Return the start position of the current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward plz-http-end-of-headers-regexp nil t)
+ (point)))
+
+(cl-defmethod plz-event-source-open ((source plz-buffer-event-source))
+ "Open a connection to the URL of the event SOURCE."
+ (with-slots (buffer errors options ready-state parser) source
+ (with-current-buffer (get-buffer-create buffer)
+ (let ((event (plz-event-source-event :type "open")))
+ (setf ready-state 'connecting)
+ (setf parser (plz-event-source-parser
+ :buffer buffer
+ :position (plz-event-source--buffer-start-position)))
+ (setf ready-state 'open)
+ (plz-event-source-dispatch-event source event)
+ source))))
+
+(cl-defmethod plz-event-source-close ((source plz-buffer-event-source))
+ "Close the connection of the event SOURCE."
+ (with-slots (buffer ready-state) source
+ (let ((event (plz-event-source-event :type "close")))
+ (setf ready-state 'closed)
+ (plz-event-source-dispatch-event source event)
+ source)))
+
+(defclass plz-http-event-source (plz-event-source)
+ ((process
+ :initarg :process
+ :documentation "The process of the event source."
+ :type (or null process))
+ (response
+ :initarg :response
+ :documentation "The plz HTTP response."
+ :type (or null plz-response)))
+ "A server sent event source using curl for HTTP.")
+
+(defun plz-event-source--media-types (source)
+ "Return the media types of the event SOURCE."
+ (with-slots (handlers) source
+ (let ((media-type (plz-media-type:text/event-stream :events handlers)))
+ (cons (cons "text/event-stream" media-type) plz-media-types))))
+
+(cl-defmethod plz-event-source-open ((source plz-http-event-source))
+ "Open a connection to the URL of the event SOURCE."
+ (with-slots (errors options process ready-state response url) source
+ (setf ready-state 'connecting)
+ (setf response nil)
+ (setf process (plz-media-type-request
+ (or (alist-get 'method options) 'get) url
+ :as `(media-types ,(plz-event-source--media-types source))
+ :body (alist-get 'body options)
+ :headers (alist-get 'headers options)
+ :then (lambda (object)
+ (setf response object))
+ :else (lambda (object)
+ (setf errors (push object errors))
+ (setf response (plz-error-response object)))
+ :finally (lambda ()
+ (setf ready-state 'closed))))
+ source))
+
+(cl-defmethod plz-event-source-close ((source plz-http-event-source))
+ "Close the connection of the event SOURCE."
+ (with-slots (process ready-state) source
+ (delete-process process)
+ (setf ready-state 'closed)))
+
+;; Content Type: text/event-stream
+
+(defclass plz-media-type:text/event-stream
(plz-media-type:application/octet-stream)
+ ((name :initform "text/event-stream")
+ (events :documentation "Association list from event type to handler."
+ :initarg :events)))
+
+(defun plz-media-type:text/event-stream--event-source (response)
+ "Return the event source of the RESPONSE."
+ (process-get (plz-response-process response) :plz-event-source))
+
+(cl-defmethod plz-media-type-else ((_ plz-media-type:text/event-stream) error)
+ "Transform the ERROR into a format suitable for MEDIA-TYPE."
+ (let* ((response (plz-error-response error))
+ (source (plz-media-type:text/event-stream--event-source response))
+ (event (plz-event-source-event :type "error" :data error)))
+ (plz-event-source-close source)
+ (plz-event-source-dispatch-event source event)
+ error))
+
+(cl-defmethod plz-media-type-process ((media-type
plz-media-type:text/event-stream) process chunk)
+ "Process the CHUNK according to MEDIA-TYPE using PROCESS."
+ (when (buffer-live-p (process-buffer process))
+ (with-current-buffer (process-buffer process)
+ (unless (process-get process :plz-event-source)
+ (let* ((response (make-plz-response
+ :status (plz-response-status chunk)
+ :headers (plz-response-headers chunk)))
+ (source (plz-event-source-open
+ (plz-buffer-event-source
+ :buffer (buffer-name (process-buffer process))
+ :handlers (seq-map
+ (lambda (pair)
+ (let ((type (car pair))
+ (handler (cdr pair)))
+ (cond
+ ((equal "open" type)
+ (cons type (lambda (source event)
+ (setf (oref event data)
response)
+ (funcall handler source
event))))
+ ((equal "close" type)
+ (cons type (lambda (source event)
+ (setf (oref event data)
response)
+ (funcall handler source
event))))
+ (t pair))))
+ (oref media-type events))))))
+ (process-put process :plz-event-source source)))
+ (plz-event-source-insert (process-get process :plz-event-source)
+ (plz-response-body chunk)))))
+
+(cl-defmethod plz-media-type-then ((_ plz-media-type:text/event-stream)
response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+ (let ((source (plz-media-type:text/event-stream--event-source response)))
+ (plz-event-source-close source)
+ response))
+
+(provide 'plz-event-source)
+;;; plz-event-source.el ends here
diff --git a/plz-media-type.el b/plz-media-type.el
new file mode 100644
index 0000000000..af498ce87f
--- /dev/null
+++ b/plz-media-type.el
@@ -0,0 +1,341 @@
+;;; plz-media-type.el --- plz content types -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019-2023 Free Software Foundation, Inc.
+
+;; Author: r0man <roman@burningswell.com>
+;; Maintainer: r0man <roman@burningswell.com>
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file handles content type.
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'eieio)
+(require 'plz)
+
+(defclass plz:media-type ()
+ ((name
+ :documentation "The MIME Type of the handler."
+ :initarg :name
+ :initform "application/octet-stream"
+ :type string)))
+
+(cl-defgeneric plz-media-type-else (media-type error)
+ "Transform the ERROR into a format suitable for MEDIA-TYPE.")
+
+(cl-defgeneric plz-media-type-then (media-type response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE.")
+
+(cl-defgeneric plz-media-type-process (media-type process chunk)
+ "Process the CHUNK according to MEDIA-TYPE using PROCESS.")
+
+(defun plz-media-type--content-type (response)
+ "Return the content type header of RESPONSE, or nil if it's not set."
+ (let ((headers (plz-response-headers response)))
+ (when-let (header (cdr (assoc 'content-type headers)))
+ (replace-regexp-in-string "\s*\\(;.*\\)?" "" header))))
+
+(defun plz-media--type-find (media-types media-type)
+ "Lookup the MEDIA-TYPE in MEDIA-TYPES."
+ (or (alist-get media-type media-types nil nil #'equal)
+ (alist-get t media-types)
+ (plz-media-type:application/octet-stream)))
+
+(defun plz-media-type--of-response (media-types response)
+ "Lookup the content type of RESPONSE in MEDIA-TYPES."
+ (let ((media-type (plz-media-type--content-type response)))
+ (plz-media--type-find media-types media-type)))
+
+(defun plz-media-type-process-filter (process media-types chunk)
+ "The process filter that handles different content types.
+
+PROCESS is the process.
+
+MEDIA-TYPES is an association list from media type to an
+instance of a content type class.
+
+CHUNK is a part of the HTTP body."
+ (when (buffer-live-p (process-buffer process))
+ (with-current-buffer (process-buffer process)
+ (let ((moving (= (point) (process-mark process))))
+ (if-let (media-type (process-get process :plz-media-type))
+ (let ((response (process-get process :plz-media-type-response)))
+ (setf (plz-response-body response) chunk)
+ (plz-media-type-process media-type process response))
+ (progn
+ (save-excursion
+ (goto-char (process-mark process))
+ (insert chunk)
+ (set-marker (process-mark process) (point)))
+ (goto-char (point-min))
+ (when (re-search-forward plz-http-end-of-headers-regexp nil t)
+ (let ((body-start (point)))
+ (goto-char (point-min))
+ (let* ((response (prog1 (plz--response) (widen)))
+ (media-type (plz-media-type--of-response media-types
response)))
+ (process-put process :plz-media-type media-type)
+ (when-let (body (plz-response-body response))
+ (when (> (length body) 0)
+ (delete-region body-start (point-max))
+ (set-marker (process-mark process) (point))
+ (plz-media-type-process media-type process response)))
+ (setf (plz-response-body response) nil)
+ (process-put process :plz-media-type-response response))))))
+ (when moving
+ (goto-char (process-mark process)))))))
+
+;; Content Type: application/octet-stream
+
+(defclass plz-media-type:application/octet-stream (plz:media-type)
+ ((name :initform "application/octet-stream")))
+
+(cl-defmethod plz-media-type-else ((media-type
plz-media-type:application/octet-stream) error)
+ "Transform the ERROR into a format suitable for MEDIA-TYPE."
+ (let ((response (plz-error-response error)))
+ (setf (plz-error-response error) (plz-media-type-then media-type response))
+ error))
+
+(cl-defmethod plz-media-type-then ((media-type
plz-media-type:application/octet-stream) response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+ (ignore media-type)
+ response)
+
+(cl-defmethod plz-media-type-process ((media-type
plz-media-type:application/octet-stream) process chunk)
+ "Process the CHUNK according to MEDIA-TYPE using PROCESS."
+ (ignore media-type)
+ (when (buffer-live-p (process-buffer process))
+ (with-current-buffer (process-buffer process)
+ (let ((moving (= (point) (process-mark process))))
+ (save-excursion
+ (goto-char (process-mark process))
+ (insert (plz-response-body chunk))
+ (set-marker (process-mark process) (point)))
+ (when moving
+ (goto-char (process-mark process)))))))
+
+;; Content Type: application/json
+
+(defclass plz-media-type:application/json
(plz-media-type:application/octet-stream)
+ ((name :initform "application/json")
+ (array-type :initform 'array)
+ (false-object :initform :json-false)
+ (null-object :initform nil)
+ (object-type :initform 'alist)))
+
+(cl-defmethod plz-media-type-then ((media-type
plz-media-type:application/json) response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+ (with-slots (array-type false-object null-object object-type) media-type
+ (setf (plz-response-body response)
+ (with-temp-buffer
+ (insert (plz-response-body response))
+ (goto-char (point-min))
+ (json-parse-buffer :array-type array-type
+ :false-object false-object
+ :null-object null-object
+ :object-type object-type)))
+ response))
+
+;; Content Type: application/xml
+
+(defclass plz-media-type:application/xml
(plz-media-type:application/octet-stream)
+ ((name :initform "application/xml")))
+
+(cl-defmethod plz-media-type-then ((media-type plz-media-type:application/xml)
response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+ (with-slots (array-type false-object null-object object-type) media-type
+ (setf (plz-response-body response)
+ (with-temp-buffer
+ (insert (plz-response-body response))
+ (libxml-parse-html-region)))
+ response))
+
+;; Content Type: text/html
+
+(defclass plz-media-type:text/html (plz-media-type:application/octet-stream)
+ ((name :initform "text/html")))
+
+(cl-defmethod plz-media-type-then ((media-type plz-media-type:text/html)
response)
+ "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+ (with-slots (array-type false-object null-object object-type) media-type
+ (setf (plz-response-body response)
+ (with-temp-buffer
+ (insert (plz-response-body response))
+ (libxml-parse-html-region)))
+ response))
+
+(defvar plz-media-types
+ `(("application/json" . ,(plz-media-type:application/json))
+ ("application/octet-stream" . ,(plz-media-type:application/json))
+ ("application/xml" . ,(plz-media-type:application/xml))
+ ("text/html" . ,(plz-media-type:text/html))
+ (t . ,(plz-media-type:application/octet-stream)))
+ "Alist from media type to content type.")
+
+(cl-defun plz-media-type-request
+ (method
+ url
+ &rest rest &key headers body else finally noquery
+ (as 'string)
+ (body-type 'text)
+ (connect-timeout plz-connect-timeout)
+ (decode t decode-s)
+ (then 'sync)
+ (timeout plz-timeout))
+ "Request METHOD from URL with curl.
+Return the curl process object or, for a synchronous request, the
+selected result.
+
+HEADERS may be an alist of extra headers to send with the
+request.
+
+BODY may be a string, a buffer, or a list like `(file FILENAME)'
+to upload a file from disk.
+
+BODY-TYPE may be `text' to send BODY as text, or `binary' to send
+it as binary.
+
+AS selects the kind of result to pass to the callback function
+THEN, or the kind of result to return for synchronous requests.
+It may be:
+
+- `buffer' to pass the response buffer, which will be narrowed to
+ the response body and decoded according to DECODE.
+
+- `binary' to pass the response body as an un-decoded string.
+
+- `string' to pass the response body as a decoded string.
+
+- `response' to pass a `plz-response' structure.
+
+- `file' to pass a temporary filename to which the response body
+ has been saved without decoding.
+
+- `(file FILENAME)' to pass FILENAME after having saved the
+ response body to it without decoding. FILENAME must be a
+ non-existent file; if it exists, it will not be overwritten,
+ and an error will be signaled.
+
+- `(stream :through PROCESS-FILTER)' to asynchronously stream the
+ HTTP response. PROCESS-FILTER is an Emacs process filter
+ function, and must accept two arguments: the curl process
+ sending the request and a chunk of the HTTP body, which was
+ just received.
+
+- A function, which is called in the response buffer with it
+ narrowed to the response body (suitable for, e.g. `json-read').
+
+If DECODE is non-nil, the response body is decoded automatically.
+For binary content, it should be nil. When AS is `binary',
+DECODE is automatically set to nil.
+
+THEN is a callback function, whose sole argument is selected
+above with AS; if the request fails and no ELSE function is
+given (see below), the argument will be a `plz-error' structure
+describing the error. Or THEN may be `sync' to make a
+synchronous request, in which case the result is returned
+directly from this function.
+
+ELSE is an optional callback function called when the request
+fails (i.e. if curl fails, or if the HTTP response has a non-2xx
+status code). It is called with one argument, a `plz-error'
+structure. If ELSE is nil, a `plz-curl-error' or
+`plz-http-error' is signaled when the request fails, with a
+`plz-error' structure as the error data. For synchronous
+requests, this argument is ignored.
+
+NOTE: In v0.8 of `plz', only one error will be signaled:
+`plz-error'. The existing errors, `plz-curl-error' and
+`plz-http-error', inherit from `plz-error' to allow applications
+to update their code while using v0.7 (i.e. any `condition-case'
+forms should now handle only `plz-error', not the other two).
+
+FINALLY is an optional function called without argument after
+THEN or ELSE, as appropriate. For synchronous requests, this
+argument is ignored.
+
+CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
+how long it takes to connect to a host and to receive a response
+from a host, respectively.
+
+NOQUERY is passed to `make-process', which see.
+
+When the HTTP response is streamed, the buffering in the curl
+output stream is turned off and the PROCESS-FILTER may be called
+multiple times, depending on the size of the HTTP body. It is
+the user's responsibility to understand and process each chunk,
+and to construct the finalized response if necessary. There are
+no guarantees regarding the chunk, such as being line-based or
+not.
+\(To silence checkdoc, we mention the internal argument REST.)"
+ ;; FIXME(v0.8): Remove the note about error changes from the docstring.
+ ;; FIXME(v0.8): Update error signals in docstring.
+ (declare (indent defun))
+ (ignore as) ;; TODO: Handle as?
+ (if-let (media-types (pcase as
+ (`(media-types ,media-types)
+ media-types)))
+ (let* ((plz-curl-default-args (cons "--no-buffer" plz-curl-default-args))
+ (result (plz method url
+ :as 'response
+ :body body
+ :body-type body-type
+ :connect-timeout connect-timeout
+ :decode decode
+ :else (when (functionp else)
+ (lambda (object)
+ (let* ((media-type
(plz-media-type--of-response media-types (plz-error-response object)))
+ (object (plz-media-type-else
media-type object)))
+ (funcall else object))))
+ :finally (when (functionp finally)
+ (lambda () (funcall finally)))
+ :headers headers
+ :noquery noquery
+ :process-filter (lambda (process chunk)
+ (plz-media-type-process-filter
process media-types chunk))
+ :timeout timeout
+ :then (cond
+ ((symbolp then) then)
+ ((functionp then)
+ (lambda (object)
+ (let* ((media-type
(plz-media-type--of-response media-types object))
+ (object (plz-media-type-then
media-type object)))
+ (funcall then object))))))))
+ ;; TODO: Handle sync event stream
+ (cond
+ ((processp result)
+ result)
+ ((plz-response-p result)
+ (let ((media-type (plz-media-type--of-response media-types result)))
+ (plz-media-type-then media-type result)))
+ ((plz-error-p result)
+ (let ((media-type (plz-media-type--of-response media-types
(plz-error-response result))))
+ (plz-media-type-else media-type result)))
+ (t result)))
+ (apply #'plz (append (list method url) rest))))
+
+;;;; Footer
+
+(provide 'plz-media-type)
+
+;;; plz-media-type.el ends here
diff --git a/plz.el b/plz.el
new file mode 100644
index 0000000000..0739a20b04
--- /dev/null
+++ b/plz.el
@@ -0,0 +1,912 @@
+;;; plz.el --- HTTP library -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019-2023 Free Software Foundation, Inc.
+
+;; Author: Adam Porter <adam@alphapapa.net>
+;; Maintainer: Adam Porter <adam@alphapapa.net>
+;; URL: https://github.com/alphapapa/plz.el
+;; Version: 0.8-pre
+;; Package-Requires: ((emacs "26.3"))
+;; Keywords: comm, network, http
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; An HTTP library that uses curl as a backend. Inspired by, and some
+;; code copied from, Christopher Wellons's library, elfeed-curl.el.
+;;
+;; Why this package?
+;;
+;; 1. `url' works well for many things, but it has some issues.
+;; 2. `request' works well for many things, but it has some issues.
+;; 3. Chris Wellons doesn't have time to factor his excellent
+;; elfeed-curl.el library out of Elfeed. This will have to do.
+;;
+;; Why is it called `plz'?
+;;
+;; 1. There's already a package called `http'.
+;; 2. There's already a package called `request'.
+;; 3. Naming things is hard.
+
+;;;; Usage:
+
+;; FIXME(v0.8): Remove the following note.
+
+;; NOTE: In v0.8 of plz, only one error will be signaled: `plz-error'.
+;; The existing errors, `plz-curl-error' and `plz-http-error', inherit
+;; from `plz-error' to allow applications to update their code while
+;; using v0.7 (i.e. any `condition-case' forms should now handle only
+;; `plz-error', not the other two).
+
+;; Call function `plz' to make an HTTP request. Its docstring
+;; explains its arguments. `plz' also supports other HTTP methods,
+;; uploading and downloading binary files, sending URL parameters and
+;; HTTP headers, configurable timeouts, error-handling "else" and
+;; always-called "finally" functions, and more.
+
+;; Basic usage is simple. For example, to make a synchronous request
+;; and return the HTTP response body as a string:
+;;
+;; (plz 'get "https://httpbin.org/get";)
+;;
+;; Which returns the JSON object as a string:
+;;
+;; "{
+;; \"args\": {},
+;; \"headers\": {
+;; \"Accept\": \"*/*\",
+;; \"Accept-Encoding\": \"deflate, gzip\",
+;; \"Host\": \"httpbin.org\",
+;; \"User-Agent\": \"curl/7.35.0\"
+;; },
+;; \"origin\": \"xxx.xxx.xxx.xxx\",
+;; \"url\": \"https://httpbin.org/get\";
+;; }"
+;;
+;; To make the same request asynchronously, decoding the JSON and
+;; printing a message with a value from it:
+;;
+;; (plz 'get "https://httpbin.org/get"; :as #'json-read
+;; :then (lambda (alist) (message "URL: %s" (alist-get 'url alist))))
+;;
+;; Which, after the request returns, prints:
+;;
+;; URL: https://httpbin.org/get
+
+;;;; Credits:
+
+;; Thanks to Chris Wellons for inspiration, encouragement, and advice.
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'rx)
+(require 'subr-x)
+
+;;;; Errors
+
+(define-error 'plz-error "plz error")
+(define-error 'plz-curl-error "plz: Curl error" 'plz-error)
+(define-error 'plz-http-error "plz: HTTP error" 'plz-error)
+
+;;;; Structs
+
+(cl-defstruct plz-response
+ version status headers body process)
+
+(cl-defstruct plz-error
+ curl-error response message)
+
+;;;; Constants
+
+(defconst plz-http-response-status-line-regexp
+ (rx "HTTP/" (group (or "1.0" "1.1" "2")) " "
+ ;; Status code
+ (group (1+ digit)) " "
+ ;; Reason phrase
+ (optional (group (1+ (not (any "\r\n")))))
+ (or
+ ;; HTTP 1
+ "\r\n"
+ ;; HTTP 2
+ "\n"))
+ "Regular expression matching HTTP response status line.")
+
+(defconst plz-http-end-of-headers-regexp
+ (rx (or "\r\n\r\n" "\n\n"))
+ "Regular expression matching the end of HTTP headers.
+This must work with both HTTP/1 (using CRLF) and HTTP/2 (using
+only LF).")
+
+(defconst plz-curl-errors
+ ;; Copied from elfeed-curl.el.
+ '((1 . "Unsupported protocol.")
+ (2 . "Failed to initialize.")
+ (3 . "URL malformed. The syntax was not correct.")
+ (4 . "A feature or option that was needed to perform the desired request
was not enabled or was explicitly disabled at build-time.")
+ (5 . "Couldn't resolve proxy. The given proxy host could not be resolved.")
+ (6 . "Couldn't resolve host. The given remote host was not resolved.")
+ (7 . "Failed to connect to host.")
+ (8 . "FTP weird server reply. The server sent data curl couldn't parse.")
+ (9 . "FTP access denied.")
+ (11 . "FTP weird PASS reply.")
+ (13 . "FTP weird PASV reply.")
+ (14 . "FTP weird 227 format.")
+ (15 . "FTP can't get host.")
+ (17 . "FTP couldn't set binary.")
+ (18 . "Partial file. Only a part of the file was transferred.")
+ (19 . "FTP couldn't download/access the given file, the RETR (or similar)
command failed.")
+ (21 . "FTP quote error. A quote command returned error from the server.")
+ (22 . "HTTP page not retrieved.")
+ (23 . "Write error.")
+ (25 . "FTP couldn't STOR file.")
+ (26 . "Read error. Various reading problems.")
+ (27 . "Out of memory. A memory allocation request failed.")
+ (28 . "Operation timeout.")
+ (30 . "FTP PORT failed.")
+ (31 . "FTP couldn't use REST.")
+ (33 . "HTTP range error. The range \"command\" didn't work.")
+ (34 . "HTTP post error. Internal post-request generation error.")
+ (35 . "SSL connect error. The SSL handshaking failed.")
+ (36 . "FTP bad download resume.")
+ (37 . "FILE couldn't read file.")
+ (38 . "LDAP bind operation failed.")
+ (39 . "LDAP search failed.")
+ (41 . "Function not found. A required LDAP function was not found.")
+ (42 . "Aborted by callback.")
+ (43 . "Internal error. A function was called with a bad parameter.")
+ (45 . "Interface error. A specified outgoing interface could not be used.")
+ (47 . "Too many redirects.")
+ (48 . "Unknown option specified to libcurl.")
+ (49 . "Malformed telnet option.")
+ (51 . "The peer's SSL certificate or SSH MD5 fingerprint was not OK.")
+ (52 . "The server didn't reply anything, which here is considered an
error.")
+ (53 . "SSL crypto engine not found.")
+ (54 . "Cannot set SSL crypto engine as default.")
+ (55 . "Failed sending network data.")
+ (56 . "Failure in receiving network data.")
+ (58 . "Problem with the local certificate.")
+ (59 . "Couldn't use specified SSL cipher.")
+ (60 . "Peer certificate cannot be authenticated with known CA
certificates.")
+ (61 . "Unrecognized transfer encoding.")
+ (62 . "Invalid LDAP URL.")
+ (63 . "Maximum file size exceeded.")
+ (64 . "Requested FTP SSL level failed.")
+ (65 . "Sending the data requires a rewind that failed.")
+ (66 . "Failed to initialise SSL Engine.")
+ (67 . "The user name, password, or similar was not accepted and curl
failed to log in.")
+ (68 . "File not found on TFTP server.")
+ (69 . "Permission problem on TFTP server.")
+ (70 . "Out of disk space on TFTP server.")
+ (71 . "Illegal TFTP operation.")
+ (72 . "Unknown TFTP transfer ID.")
+ (73 . "File already exists (TFTP).")
+ (74 . "No such user (TFTP).")
+ (75 . "Character conversion failed.")
+ (76 . "Character conversion functions required.")
+ (77 . "Problem with reading the SSL CA cert (path? access rights?).")
+ (78 . "The resource referenced in the URL does not exist.")
+ (79 . "An unspecified error occurred during the SSH session.")
+ (80 . "Failed to shut down the SSL connection.")
+ (82 . "Could not load CRL file, missing or wrong format (added in
7.19.0).")
+ (83 . "Issuer check failed (added in 7.19.0).")
+ (84 . "The FTP PRET command failed")
+ (85 . "RTSP: mismatch of CSeq numbers")
+ (86 . "RTSP: mismatch of Session Identifiers")
+ (87 . "unable to parse FTP file list")
+ (88 . "FTP chunk callback reported error")
+ (89 . "No connection available, the session will be queued")
+ (90 . "SSL public key does not matched pinned public key"))
+ "Alist mapping curl error code integers to helpful error messages.")
+
+;;;; Customization
+
+(defgroup plz nil
+ "Options for `plz'."
+ :group 'network
+ :link '(url-link "https://github.com/alphapapa/plz.el";))
+
+(defcustom plz-curl-program "curl"
+ "Name of curl program to call."
+ :type 'string)
+
+(defcustom plz-curl-default-args
+ '("--silent"
+ "--compressed"
+ "--location")
+ "Default arguments to curl.
+Note that these arguments are passed on the command line, which
+may be visible to other users on the local system."
+ :type '(repeat string))
+
+(defcustom plz-connect-timeout 5
+ "Default connection timeout in seconds.
+This limits how long the connection phase may last (the
+\"--connect-timeout\" argument to curl)."
+ :type 'number)
+
+(defcustom plz-timeout 60
+ "Default request timeout in seconds.
+This limits how long an entire request may take, including the
+connection phase and waiting to receive the response (the
+\"--max-time\" argument to curl)."
+ :type 'number)
+
+;;;; Functions
+
+;;;;; Public
+
+(cl-defun plz (method url &rest rest &key headers body else finally noquery
process-filter
+ (as 'string) (then 'sync)
+ (body-type 'text) (decode t decode-s)
+ (connect-timeout plz-connect-timeout) (timeout
plz-timeout))
+ "Request METHOD from URL with curl.
+Return the curl process object or, for a synchronous request, the
+selected result.
+
+HEADERS may be an alist of extra headers to send with the
+request.
+
+BODY may be a string, a buffer, or a list like `(file FILENAME)'
+to upload a file from disk.
+
+BODY-TYPE may be `text' to send BODY as text, or `binary' to send
+it as binary.
+
+AS selects the kind of result to pass to the callback function
+THEN, or the kind of result to return for synchronous requests.
+It may be:
+
+- `buffer' to pass the response buffer, which will be narrowed to
+ the response body and decoded according to DECODE.
+
+- `binary' to pass the response body as an un-decoded string.
+
+- `string' to pass the response body as a decoded string.
+
+- `response' to pass a `plz-response' structure.
+
+- `file' to pass a temporary filename to which the response body
+ has been saved without decoding.
+
+- `(file FILENAME)' to pass FILENAME after having saved the
+ response body to it without decoding. FILENAME must be a
+ non-existent file; if it exists, it will not be overwritten,
+ and an error will be signaled.
+
+- A function, which is called in the response buffer with it
+ narrowed to the response body (suitable for, e.g. `json-read').
+
+If DECODE is non-nil, the response body is decoded automatically.
+For binary content, it should be nil. When AS is `binary',
+DECODE is automatically set to nil.
+
+THEN is a callback function, whose sole argument is selected
+above with AS; if the request fails and no ELSE function is
+given (see below), the argument will be a `plz-error' structure
+describing the error. Or THEN may be `sync' to make a
+synchronous request, in which case the result is returned
+directly from this function.
+
+ELSE is an optional callback function called when the request
+fails (i.e. if curl fails, or if the HTTP response has a non-2xx
+status code). It is called with one argument, a `plz-error'
+structure. If ELSE is nil, a `plz-curl-error' or
+`plz-http-error' is signaled when the request fails, with a
+`plz-error' structure as the error data. For synchronous
+requests, this argument is ignored.
+
+NOTE: In v0.8 of `plz', only one error will be signaled:
+`plz-error'. The existing errors, `plz-curl-error' and
+`plz-http-error', inherit from `plz-error' to allow applications
+to update their code while using v0.7 (i.e. any `condition-case'
+forms should now handle only `plz-error', not the other two).
+
+FINALLY is an optional function called without argument after
+THEN or ELSE, as appropriate. For synchronous requests, this
+argument is ignored.
+
+CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
+how long it takes to connect to a host and to receive a response
+from a host, respectively.
+
+NOQUERY is passed to `make-process', which see.
+
+\(To silence checkdoc, we mention the internal argument REST.)"
+ ;; FIXME(v0.8): Remove the note about error changes from the docstring.
+ ;; FIXME(v0.8): Update error signals in docstring.
+ (declare (indent defun))
+ (setf decode (if (and decode-s (not decode))
+ nil decode))
+ ;; NOTE: By default, for PUT requests and POST requests >1KB, curl sends an
+ ;; "Expect:" header, which causes servers to send a "100 Continue" response,
which
+ ;; we don't want to have to deal with, so we disable it by setting the
header to
+ ;; the empty string. See <https://gms.tf/when-curl-sends-100-continue.html>.
+ ;; TODO: Handle "100 Continue" responses and remove this workaround.
+ (push (cons "Expect" "") headers)
+ (let* ((data-arg (pcase-exhaustive body-type
+ ('binary "--data-binary")
+ ('text "--data")))
+ (curl-command-line-args (append plz-curl-default-args
+ (list "--config" "-")))
+ (curl-config-header-args (cl-loop for (key . value) in headers
+ collect (cons "--header" (format
"%s: %s" key value))))
+ (curl-config-args (append curl-config-header-args
+ (list (cons "--url" url))
+ (when connect-timeout
+ (list (cons "--connect-timeout"
+ (number-to-string
connect-timeout))))
+ (when timeout
+ (list (cons "--max-time"
(number-to-string timeout))))
+ ;; NOTE: To make a HEAD request
+ ;; requires using the "--head"
+ ;; option rather than "--request
+ ;; HEAD", and doing so with
+ ;; "--dump-header" duplicates the
+ ;; headers, so we must instead
+ ;; specify that for each other
+ ;; method.
+ (pcase method
+ ('get
+ (list (cons "--dump-header" "-")))
+ ((or 'put 'post)
+ (list (cons "--dump-header" "-")
+ (cons "--request" (upcase
(symbol-name method)))
+ ;; It appears that this must be
the last argument
+ ;; in order to pass data on the
rest of STDIN.
+ (pcase body
+ (`(file ,filename)
+ ;; Use `expand-file-name'
because curl doesn't
+ ;; expand, e.g. "~" into
"/home/...".
+ (cons "--upload-file"
(expand-file-name filename)))
+ (_ (cons data-arg "@-")))))
+ ('delete
+ (list (cons "--dump-header" "-")
+ (cons "--request" (upcase
(symbol-name method)))))
+ ('head
+ (list (cons "--head" "")
+ (cons "--request" "HEAD"))))))
+ (curl-config (cl-loop for (key . value) in curl-config-args
+ concat (format "%s \"%s\"\n" key value)))
+ (decode (pcase as
+ ('binary nil)
+ (_ decode)))
+ (default-directory
+ ;; Avoid making process in a nonexistent directory (in case the
current
+ ;; default-directory has since been removed). It's unclear what the
best
+ ;; directory is, but this seems to make sense, and it should still
exist.
+ temporary-file-directory)
+ (process-buffer (generate-new-buffer " *plz-request-curl*"))
+ (stderr-process (make-pipe-process :name "plz-request-curl-stderr"
+ :buffer (generate-new-buffer "
*plz-request-curl-stderr*")
+ :noquery t
+ :sentinel #'plz--stderr-sentinel))
+ (process (make-process :name "plz-request-curl"
+ :buffer process-buffer
+ :coding 'binary
+ :command (append (list plz-curl-program)
curl-command-line-args)
+ :connection-type 'pipe
+ :filter process-filter
+ :sentinel #'plz--sentinel
+ :stderr stderr-process
+ :noquery noquery))
+ sync-p)
+ (when (eq 'sync then)
+ (setf sync-p t
+ then (lambda (result)
+ (process-put process :plz-result result))
+ else nil))
+ (setf
+ ;; Set the callbacks, etc. as process properties.
+ (process-get process :plz-then)
+ (pcase-exhaustive as
+ ((or 'binary 'string)
+ (lambda ()
+ (let ((coding-system (or (plz--coding-system) 'utf-8)))
+ (pcase as
+ ('binary (set-buffer-multibyte nil)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point) (point-max) coding-system))
+ (funcall then (or (buffer-string)
+ (make-plz-error :message (format "buffer-string
is nil in buffer:%S" process-buffer)))))))
+ ('buffer (progn
+ (setf (process-get process :plz-as) 'buffer)
+ (lambda ()
+ (let ((coding-system (or (plz--coding-system) 'utf-8)))
+ (pcase as
+ ('binary (set-buffer-multibyte nil)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point) (point-max)
coding-system)))
+ (funcall then (current-buffer)))))
+ ('response (lambda ()
+ (funcall then (or (plz--response :decode-p decode :process
process)
+ (make-plz-error :message (format
"response is nil for buffer:%S buffer-string:%S"
+
process-buffer (buffer-string)))))))
+ ('file (lambda ()
+ (set-buffer-multibyte nil)
+ (plz--narrow-to-body)
+ (let ((filename (make-temp-file "plz-")))
+ (condition-case err
+ (progn
+ (write-region (point-min) (point-max) filename)
+ (funcall then filename))
+ (file-already-exists
+ (funcall then (make-plz-error :message (format "error
while writing to file %S: %S" filename err))))
+ ;; In case of an error writing to the file, delete the
temp file
+ ;; and signal the error. Ignore any errors encountered
while
+ ;; deleting the file, which would obscure the original
error.
+ (error (ignore-errors
+ (delete-file filename))
+ (funcall then (make-plz-error :message (format
"error while writing to file %S: %S" filename err))))))))
+ (`(file ,(and (pred stringp) filename))
+ (lambda ()
+ (set-buffer-multibyte nil)
+ (plz--narrow-to-body)
+ (condition-case err
+ (progn
+ (write-region (point-min) (point-max) filename nil nil nil
'excl)
+ (funcall then filename))
+ (file-already-exists
+ (funcall then (make-plz-error :message (format "error while
writing to file %S: %S" filename err))))
+ ;; Since we are creating the file, it seems sensible to delete it
in case of an
+ ;; error while writing to it (e.g. a disk-full error). And we
ignore any errors
+ ;; encountered while deleting the file, which would obscure the
original error.
+ (error (ignore-errors
+ (when (file-exists-p filename)
+ (delete-file filename)))
+ (funcall then (make-plz-error :message (format "error while
writing to file %S: %S" filename err)))))))
+ ((pred functionp) (lambda ()
+ (let ((coding-system (or (plz--coding-system)
'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point) (point-max)
coding-system))
+ (funcall then (funcall as))))))
+ (process-get process :plz-else) else
+ (process-get process :plz-finally) finally
+ (process-get process :plz-sync) sync-p
+ ;; Record list of arguments for debugging purposes (e.g. when
+ ;; using Edebug in a process buffer, this allows determining
+ ;; which request the buffer is for).
+ (process-get process :plz-args) (apply #'list method url rest)
+ ;; HACK: We set the result to a sentinel value so that any other
+ ;; value, even nil, means that the response was processed, and
+ ;; the sentinel does not need to be called again (see below).
+ (process-get process :plz-result) :plz-result)
+ ;; Send --config arguments.
+ (process-send-string process curl-config)
+ (when body
+ (cl-typecase body
+ (string (process-send-string process body))
+ (buffer (with-current-buffer body
+ (process-send-region process (point-min) (point-max))))))
+ (process-send-eof process)
+ (if sync-p
+ (unwind-protect
+ (with-local-quit
+ ;; See Info node `(elisp)Accepting Output'.
+ (unless (and process stderr-process)
+ (error "Process unexpectedly nil"))
+ (while (accept-process-output process))
+ (while (accept-process-output stderr-process))
+ (when (eq :plz-result (process-get process :plz-result))
+ ;; HACK: Sentinel seems to not have been called: call it
again. (Although
+ ;; this is a hack, it seems to be a necessary one due to
Emacs's process
+ ;; handling.) See
<https://github.com/alphapapa/plz.el/issues/3> and
+ ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50166>.
+ (plz--sentinel process "finished\n")
+ (when (eq :plz-result (process-get process :plz-result))
+ (error "Plz: NO RESULT FROM PROCESS:%S ARGS:%S"
+ process rest)))
+ ;; Sentinel seems to have been called: check the result.
+ (pcase (process-get process :plz-result)
+ ((and (pred plz-error-p) data)
+ ;; The AS function signaled an error, which was collected
+ ;; into a `plz-error' struct: re-signal the error here,
+ ;; outside of the sentinel.
+ (if (plz-error-response data)
+ ;; FIXME(v0.8): Signal only plz-error.
+ (signal 'plz-http-error (list "HTTP error" data))
+ (signal 'plz-curl-error (list "Curl error" data))))
+ (else
+ ;; The AS function returned a value: return it.
+ else)))
+ (unless (eq as 'buffer)
+ (kill-buffer process-buffer))
+ (kill-buffer (process-buffer stderr-process)))
+ ;; Async request: return the process object.
+ process)))
+
+;;;;; Queue
+
+;; A simple queue system.
+
+(cl-defstruct plz-queued-request
+ "Structure representing a queued `plz' HTTP request.
+For more details on these slots, see arguments to the function
+`plz'."
+ method url headers body else finally noquery
+ as then body-type decode
+ connect-timeout timeout
+ next previous process)
+
+(cl-defstruct plz-queue
+ "Structure forming a queue for `plz' requests.
+The queue may be appended to (the default) and pre-pended to, and
+items may be removed from the front of the queue (i.e. by
+default, it's FIFO). Use functions `plz-queue', `plz-run', and
+`plz-clear' to queue, run, and clear requests, respectively."
+ (limit 1
+ :documentation "Number of simultaneous requests.")
+ (active nil
+ :documentation "Active requests.")
+ (requests nil
+ :documentation "Queued requests.")
+ (canceled-p nil
+ :documentation "Non-nil when queue has been canceled.")
+ first-active last-active
+ first-request last-request
+ (finally nil
+ :documentation "Function called with no arguments after queue has
been emptied or canceled."))
+
+(defun plz-queue (queue &rest args)
+ "Queue request for ARGS on QUEUE and return QUEUE.
+To pre-pend to QUEUE rather than append, it may be a list of the
+form (`prepend' QUEUE). QUEUE is a `plz-request' queue. ARGS
+are those passed to `plz', which see. Use `plz-run' to start
+making QUEUE's requests."
+ (declare (indent defun))
+ (cl-assert (not (equal 'sync (plist-get (cddr args) :then))) nil
+ "Only async requests may be queued")
+ (pcase-let* ((`(,method ,url . ,rest) args)
+ (args `(:method ,method :url ,url ,@rest))
+ (request (apply #'make-plz-queued-request args)))
+ (pcase queue
+ (`(prepend ,queue) (plz--queue-prepend request queue))
+ (_ (plz--queue-append request queue))))
+ queue)
+
+(defun plz--queue-append (request queue)
+ "Add REQUEST to end of QUEUE and return QUEUE."
+ (cl-check-type request plz-queued-request
+ "REQUEST must be a `plz-queued-request' structure.")
+ (cl-check-type queue plz-queue
+ "QUEUE must be a `plz-queue' structure.")
+ (when (plz-queue-last-request queue)
+ (setf (plz-queued-request-next (plz-queue-last-request queue)) request))
+ (setf (plz-queued-request-previous request) (plz-queue-last-request queue)
+ (plz-queue-last-request queue) request)
+ (unless (plz-queue-first-request queue)
+ (setf (plz-queue-first-request queue) request))
+ (unless (plz-queue-last-request queue)
+ (setf (plz-queue-last-request queue) request))
+ (push request (plz-queue-requests queue))
+ queue)
+
+(defun plz--queue-prepend (request queue)
+ "Add REQUEST to front of QUEUE and return QUEUE."
+ (cl-check-type request plz-queued-request
+ "REQUEST must be a `plz-queued-request' structure.")
+ (cl-check-type queue plz-queue
+ "QUEUE must be a `plz-queue' structure.")
+ (when (plz-queue-requests queue)
+ (setf (plz-queued-request-next request) (car (plz-queue-requests queue))
+ (plz-queued-request-previous (plz-queued-request-next request))
request))
+ (setf (plz-queue-first-request queue) request)
+ (unless (plz-queue-first-request queue)
+ (setf (plz-queue-first-request queue) request))
+ (unless (plz-queue-last-request queue)
+ (setf (plz-queue-last-request queue) request))
+ (push request (plz-queue-requests queue))
+ queue)
+
+(defun plz--queue-pop (queue)
+ "Return the first queued request on QUEUE and remove it from QUEUE."
+ (let* ((request (plz-queue-first-request queue))
+ (next (plz-queued-request-next request)))
+ (when next
+ (setf (plz-queued-request-previous next) nil))
+ (setf (plz-queue-first-request queue) next
+ (plz-queue-requests queue) (delq request (plz-queue-requests queue)))
+ (when (eq request (plz-queue-last-request queue))
+ (setf (plz-queue-last-request queue) nil))
+ request))
+
+(defun plz-run (queue)
+ "Process requests in QUEUE and return QUEUE.
+Return when QUEUE is at limit or has no more queued requests.
+
+QUEUE should be a `plz-queue' structure."
+ (cl-labels ((readyp (queue)
+ (and (not (plz-queue-canceled-p queue))
+ (plz-queue-requests queue)
+ ;; With apologies to skeeto...
+ (< (length (plz-queue-active queue)) (plz-queue-limit
queue)))))
+ (while (readyp queue)
+ (pcase-let* ((request (plz--queue-pop queue))
+ ((cl-struct plz-queued-request method url
+ headers body finally noquery as body-type
decode connect-timeout timeout
+ (else orig-else) (then orig-then))
+ request)
+ (then (lambda (response)
+ (unwind-protect
+ ;; Ensure any errors in the THEN function don't
abort the queue.
+ (funcall orig-then response)
+ (setf (plz-queue-active queue) (delq request
(plz-queue-active queue)))
+ (plz-run queue))))
+ (else (lambda (arg)
+ ;; FIXME(v0.8): This should be done in `plz-queue'
because
+ ;; `plz-clear' will call the second
queued-request's ELSE
+ ;; before it can be set by `plz-run'.
+ (unwind-protect
+ ;; Ensure any errors in the THEN function don't
abort the queue.
+ (when orig-else
+ (funcall orig-else arg))
+ (setf (plz-queue-active queue) (delq request
(plz-queue-active queue)))
+ (plz-run queue))))
+ (args (list method url
+ ;; Omit arguments for which `plz' has defaults
so as not to nil them.
+ :headers headers :body body :finally finally
:noquery noquery
+ :connect-timeout connect-timeout :timeout
timeout)))
+ ;; Add arguments which override defaults.
+ (when as
+ (setf args (plist-put args :as as)))
+ (when else
+ (setf args (plist-put args :else else)))
+ (when then
+ (setf args (plist-put args :then then)))
+ (when decode
+ (setf args (plist-put args :decode decode)))
+ (when body-type
+ (setf args (plist-put args :body-type body-type)))
+ (when connect-timeout
+ (setf args (plist-put args :connect-timeout connect-timeout)))
+ (when timeout
+ (setf args (plist-put args :timeout timeout)))
+ (setf (plz-queued-request-process request) (apply #'plz args))
+ (push request (plz-queue-active queue))))
+ (when (and (plz-queue-finally queue)
+ (zerop (length (plz-queue-active queue)))
+ (zerop (length (plz-queue-requests queue))))
+ (funcall (plz-queue-finally queue)))
+ queue))
+
+(defun plz-clear (queue)
+ "Clear QUEUE and return it.
+Cancels any active or pending requests and calls the queue's
+FINALLY function. For pending requests, their ELSE functions
+will be called with a `plz-error' structure with the message,
+\"`plz' queue cleared; request canceled.\"; active requests will
+have their curl processes killed and their ELSE functions called
+with the corresponding data."
+ (setf (plz-queue-canceled-p queue) t)
+ (dolist (request (plz-queue-active queue))
+ (when (process-live-p (plz-queued-request-process request))
+ (kill-process (plz-queued-request-process request)))
+ (setf (plz-queue-active queue) (delq request (plz-queue-active queue))))
+ (dolist (request (plz-queue-requests queue))
+ (funcall (plz-queued-request-else request)
+ (make-plz-error :message "`plz' queue cleared; request
canceled."))
+ (setf (plz-queue-requests queue) (delq request (plz-queue-requests
queue))))
+ (when (plz-queue-finally queue)
+ (funcall (plz-queue-finally queue)))
+ (setf (plz-queue-first-active queue) nil
+ (plz-queue-last-active queue) nil
+ (plz-queue-first-request queue) nil
+ (plz-queue-last-request queue) nil
+ (plz-queue-canceled-p queue) nil)
+ queue)
+
+(defun plz-length (queue)
+ "Return number of of QUEUE's outstanding requests.
+Includes active and queued requests."
+ (+ (length (plz-queue-active queue))
+ (length (plz-queue-requests queue))))
+
+;;;;; Private
+
+(defun plz--sentinel (process status)
+ "Sentinel for curl PROCESS.
+STATUS should be the process's event string (see info
+node `(elisp) Sentinels'). Calls `plz--respond' to process the
+HTTP response (directly for synchronous requests, or from a timer
+for asynchronous ones)."
+ (pcase status
+ ((or "finished\n" "killed\n" "interrupt\n"
+ (pred numberp)
+ (rx "exited abnormally with code " (group (1+ digit))))
+ (let ((buffer (process-buffer process)))
+ (if (process-get process :plz-sync)
+ (plz--respond process buffer status)
+ (run-at-time 0 nil #'plz--respond process buffer status))))))
+
+(defun plz--respond (process buffer status)
+ "Respond to HTTP response from PROCESS in BUFFER.
+Parses the response and calls the THEN/ELSE callbacks
+accordingly. To be called from `plz--sentinel'. STATUS is the
+argument passed to `plz--sentinel', which see."
+ ;; Is it silly to call this function "please respond"? Perhaps, but
+ ;; naming things is hard. The term "process" has another meaning in
+ ;; this context, and the old standby, "handle," is much overused.
+ ;; "Respond" also means "to react to something," which is what this
+ ;; does--react to receiving the HTTP response--and it's an internal
+ ;; name, so why not.
+ (unwind-protect
+ (with-current-buffer buffer
+ (pcase-exhaustive status
+ ((or 0 "finished\n")
+ ;; Curl exited normally: check HTTP status code.
+ (goto-char (point-min))
+ (plz--skip-proxy-headers)
+ (while (plz--skip-redirect-headers))
+ (pcase (plz--http-status)
+ ((and status (guard (<= 200 status 299)))
+ ;; Any 2xx response is considered successful.
+ (ignore status) ; Byte-compiling in Emacs <28 complains without
this.
+ (funcall (process-get process :plz-then)))
+ (_
+ ;; TODO: If using ":as 'response", the HTTP response
+ ;; should be passed to the THEN function, regardless
+ ;; of the status code. Only for curl errors should
+ ;; the ELSE function be called. (Maybe in v0.8.)
+
+ ;; Any other status code is considered unsuccessful
+ ;; (for now, anyway).
+ (let ((err (make-plz-error :response (plz--response :process
process))))
+ (pcase-exhaustive (process-get process :plz-else)
+ (`nil (process-put process :plz-result err))
+ ((and (pred functionp) fn) (funcall fn err)))))))
+
+ ((or (and (pred numberp) code)
+ (rx "exited abnormally with code " (let code (group (1+
digit)))))
+ ;; Curl error.
+ (let* ((curl-exit-code (cl-typecase code
+ (string (string-to-number code))
+ (number code)))
+ (curl-error-message (alist-get curl-exit-code
plz-curl-errors))
+ (err (make-plz-error :curl-error (cons curl-exit-code
curl-error-message))))
+ (pcase-exhaustive (process-get process :plz-else)
+ (`nil (process-put process :plz-result err))
+ ((and (pred functionp) fn) (funcall fn err)))))
+
+ ((and (or "killed\n" "interrupt\n") status)
+ ;; Curl process killed or interrupted.
+ (let* ((message (pcase status
+ ("killed\n" "curl process killed")
+ ("interrupt\n" "curl process interrupted")))
+ (err (make-plz-error :message message)))
+ (pcase-exhaustive (process-get process :plz-else)
+ (`nil (process-put process :plz-result err))
+ ((and (pred functionp) fn) (funcall fn err)))))))
+ (when-let ((finally (process-get process :plz-finally)))
+ (funcall finally))
+ (unless (or (process-get process :plz-sync)
+ (eq 'buffer (process-get process :plz-as)))
+ (kill-buffer buffer))))
+
+(defun plz--stderr-sentinel (process status)
+ "Sentinel for STDERR buffer.
+Arguments are PROCESS and STATUS (ok, checkdoc?)."
+ (pcase status
+ ((or "finished\n" "killed\n" "interrupt\n"
+ (pred numberp)
+ (rx "exited abnormally with code " (1+ digit)))
+ (kill-buffer (process-buffer process)))))
+
+;;;;;; HTTP Responses
+
+;; Functions for parsing HTTP responses.
+
+(defun plz--skip-proxy-headers ()
+ "Skip proxy headers in current buffer."
+ (when (looking-at plz-http-response-status-line-regexp)
+ (let* ((status-code (string-to-number (match-string 2)))
+ (reason-phrase (match-string 3)))
+ (when (and (equal 200 status-code)
+ (equal "Connection established" reason-phrase))
+ ;; Skip proxy headers (curl apparently offers no way to omit
+ ;; them).
+ (unless (re-search-forward "\r\n\r\n" nil t)
+ (signal 'plz-http-error '("plz--response: End of proxy headers not
found")))))))
+
+(defun plz--skip-redirect-headers ()
+ "Skip HTTP redirect headers in current buffer."
+ (when (and (looking-at plz-http-response-status-line-regexp)
+ (member (string-to-number (match-string 2)) '(301 302 303 307
308)))
+ ;; Skip redirect headers ("--dump-header" forces redirect headers to be
included
+ ;; even when used with "--location").
+ (or (re-search-forward "\r\n\r\n" nil t)
+ (signal 'plz-http-error '("plz--response: End of redirect headers not
found")))))
+
+(cl-defun plz--response (&key (decode-p t) process)
+ "Return response structure for HTTP response in current buffer.
+When DECODE-P is non-nil, decode the response body automatically
+according to the apparent coding system.
+
+PROCESS is the curl process object that made the request.
+
+Assumes that point is at beginning of HTTP response."
+ (save-excursion
+ ;; Parse HTTP version and status code.
+ (unless (looking-at plz-http-response-status-line-regexp)
+ (signal 'plz-http-error
+ (list "plz--response: Unable to parse HTTP response status line"
+ (buffer-substring (point) (line-end-position)))))
+ (let* ((http-version (string-to-number (match-string 1)))
+ (status-code (string-to-number (match-string 2)))
+ (headers (plz--headers))
+ (coding-system (or (plz--coding-system headers) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode-p
+ (decode-coding-region (point) (point-max) coding-system))
+ (make-plz-response
+ :version http-version
+ :status status-code
+ :headers headers
+ :body (buffer-string)
+ :process process))))
+
+(defun plz--coding-system (&optional headers)
+ "Return coding system for HTTP response in current buffer.
+HEADERS may optionally be an alist of parsed HTTP headers to
+refer to rather than the current buffer's un-parsed headers."
+ (let* ((headers (or headers (plz--headers)))
+ (content-type (alist-get 'content-type headers)))
+ (when content-type
+ (coding-system-from-name content-type))))
+
+(defun plz--http-status ()
+ "Return HTTP status code for HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+ (when (looking-at plz-http-response-status-line-regexp)
+ (string-to-number (match-string 2))))
+
+(defun plz--headers ()
+ "Return headers alist for HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+ (save-excursion
+ (forward-line 1)
+ (let ((limit (save-excursion
+ (re-search-forward plz-http-end-of-headers-regexp nil)
+ (point))))
+ (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":"))))
":" (1+ blank)
+ (group (1+ (not (in "\r\n")))))
+ limit t)
+ ;; NOTE: Some HTTP servers send all-lowercase header keys,
which means an alist
+ ;; lookup with `equal' or `string=' fails when the case
differs. We don't want
+ ;; users to have to worry about this, so for consistency, we
downcase the
+ ;; header name. And while we're at it, we might as well intern
it so we can
+ ;; use `alist-get' without having to add "nil nil #'equal"
every time.
+ collect (cons (intern (downcase (match-string 1)))
(match-string 2))))))
+
+(defun plz--narrow-to-body ()
+ "Narrow to body of HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+ (unless (re-search-forward plz-http-end-of-headers-regexp nil t)
+ (signal 'plz-http-error '("plz--narrow-to-body: Unable to find end of
headers")))
+ (narrow-to-region (point) (point-max)))
+
+;;;; Footer
+
+(provide 'plz)
+
+;;; plz.el ends here
- [elpa] externals/llm updated (efe218ac13 -> 478afbcb41), ELPA Syncer, 2024/05/17
- [elpa] externals/llm 1f3b018dcb 03/71: Merge pull request #26 from r0man/plz,
ELPA Syncer <=
- [elpa] externals/llm 829bedabe6 04/71: Support for vertex embeddings, adding callback compatibility, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 2ac956a060 05/71: Add support for the application/x-ndjson media type, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 63f2b8ffbc 10/71: Merge branch 'main' into plz, ELPA Syncer, 2024/05/17
- [elpa] externals/llm a9cd296cd8 02/71: Add llm-request-plz.el, ELPA Syncer, 2024/05/17
- [elpa] externals/llm f33475eeae 01/71: Add plz and media type and event source extensions, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 41d0889bcb 06/71: Make sync use of plz return data instead of object, converted Gemini, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 84678edfae 07/71: Merge pull request #28 from r0man/plz, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 9541d34656 08/71: Enable streaming in Open AI with plz modifications, ELPA Syncer, 2024/05/17
- [elpa] externals/llm c9ab8664ce 09/71: Support function streaming with Open AI & plz, ELPA Syncer, 2024/05/17
- [elpa] externals/llm 3988fecb53 11/71: Make separate function for event streaming, w/ client-side handlers, ELPA Syncer, 2024/05/17