From b351a52c8c97fe5c00023e09ac511630149f3d4f Mon Sep 17 00:00:00 2001 From: SqrtMinusOne Date: Thu, 4 Jan 2024 01:28:36 +0300 Subject: [PATCH] biome-multi: basically works --- biome-api.el | 37 ++++++++ biome-multi.el | 236 +++++++++++++++++++++++++++++++++++++++++++++++++ biome-query.el | 16 ++-- biome.el | 12 +++ 4 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 biome-multi.el diff --git a/biome-api.el b/biome-api.el index 28a9866..4766f54 100644 --- a/biome-api.el +++ b/biome-api.el @@ -128,5 +128,42 @@ called with QUERY and the data returned by the API as arguments." (cl-function (lambda (&key error-thrown response &allow-other-keys) (biome-api--show-error error-thrown response)))))) +(defun biome-api-get-multiple (queries callback) + "Get data from Open Meteo API. + +QUERIES is a list of forms as defined by `biome-query-current'. CALLBACK is +called with QUERIES and the data returned by the API as arguments." + (let (requests) + (seq-map-indexed + (lambda (query idx) + (push + (request (alist-get (alist-get :name query) + biome-api-urls nil nil #'equal) + :type "GET" + :params (biome-api--get-params query) + :parser #'json-read + :success (cl-function + (lambda (&key data &allow-other-keys) + ;; I'm not sure why, but `request-response-done-p' for + ;; the current request returns nil. I don't + ;; know how stable this is, so... + (let ((completed-count + (cl-loop for i from 0 + for request in requests + if (or (request-response-done-p request) + (= i (- (length requests) 1 idx))) + sum 1))) + (message "Completed %d/%d requests" + completed-count (length queries)) + (when (eq (length queries) completed-count) + (funcall callback (copy-tree queries) + (mapcar #'request-response-data + (reverse requests))))))) + :error + (cl-function (lambda (&key error-thrown response &allow-other-keys) + (biome-api--show-error error-thrown response)))) + requests)) + queries))) + (provide 'biome-api) ;;; biome-api.el ends here diff --git a/biome-multi.el b/biome-multi.el new file mode 100644 index 0000000..01e7e76 --- /dev/null +++ b/biome-multi.el @@ -0,0 +1,236 @@ +;;; biome-multi.el --- Do multiple queries to Open Meteo -*- lexical-binding: t -*- + +;; Copyright (C) 2024 Korytov Pavel + +;; Author: Korytov Pavel +;; Maintainer: Korytov Pavel + +;; This file is NOT part of GNU Emacs. + +;; 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 . + +;;; Commentary: + +;; TODO + +;;; Code: +(require 'biome-query) +(require 'font-lock) +(require 'transient) + +(defvar biome-multi-query-current nil + "Current query. + +This is a list of forms as defined by `biome-query-current'.") + +(defvar biome-multi--callback nil + "Call this with the selected query.") + +(defclass biome-multi--transient-report (transient-suffix) + ((transient :initform t)) + "A class to display the current report for `biome-multi'.") + +(cl-defmethod transient-init-value ((_ biome-multi--transient-report)) + "A dummy method for `biome-multi--transient-report'." + nil) + +(cl-defmethod transient-format ((_ biome-multi--transient-report)) + "TODO." + (if (seq-empty-p biome-multi-query-current) + (propertize "Add at least one query" 'face 'error) + (cl-loop for i from 0 + for query in biome-multi-query-current + concat (propertize (format "Query #%d: %s\n" i + (alist-get :name query)) + 'face 'font-lock-keyword-face) + concat (biome-query--format query) + if (not (eq i (1- (length biome-multi-query-current)))) + concat "\n"))) + +(transient-define-infix biome-multi--transient-report-infix () + :class 'biome-multi--transient-report + :key "~~1") + +(defun biome-multi-add-query () + (interactive) + (biome-query + (lambda (query) + (if (seq-empty-p biome-multi-query-current) + (setq biome-multi-query-current (list (copy-tree query))) + (nconc biome-multi-query-current (list (copy-tree query)))) + (biome-multi-query biome-multi--callback)))) + +(defun biome-multi-reset () + (interactive) + (setq biome-multi-query-current nil)) + +(defun biome-multi-edit (idx) + (interactive "nQuery number: ") + (when (or (< idx 0) + (>= idx (length biome-multi-query-current))) + (user-error "Invalid query number")) + (setq biome-query-current (nth idx biome-multi-query-current)) + (setq biome-query--callback + (lambda (query) + (setf (nth idx biome-multi-query-current) query) + (biome-multi-query biome-multi--callback))) + (biome-query--section-open (alist-get :name biome-query-current))) + +(defun biome-multi-remove (idx) + (interactive "nQuery number: ") + (when (or (< idx 0) + (>= idx (length biome-multi-query-current))) + (user-error "Invalid query number")) + (setq biome-multi-query-current + (cl-loop for query in biome-multi-query-current + for i from 0 + unless (eq i idx) + collect query))) + +(defun biome-multi-exec () + (interactive) + (when (seq-empty-p biome-multi-query-current) + (user-error "No queries to execute")) + (funcall biome-multi--callback biome-multi-query-current)) + +(transient-define-prefix biome-multi-query (callback) + ["Open Meteo Multi Query" + (biome-multi--transient-report-infix)] + ["Queries" + :class transient-row + ("a" "Add query" biome-multi-add-query) + ("e" "Edit query" biome-multi-edit :transient t) + ("d" "Delete query" biome-multi-remove :transient t)] + ["Actions" + :class transient-row + ("RET" "Run" biome-multi-exec) + ("R" "Reset" biome-multi-reset :transient t) + ("q" "Quit" transient-quit-one)] + (interactive (list nil)) + (unless callback + (error "Callback is not set. Run M-x `biome-multi' instead")) + (setq biome-multi--callback callback) + (transient-setup 'biome-multi-query)) + +(defun biome-multi--unique-names-grouped (names-by-group group-names) + (let ((name-occurences (make-hash-table :test #'equal)) + (names-mapping (make-hash-table :test #'equal))) + (cl-loop for names in names-by-group + do (cl-loop for name in names + do (puthash name + (1+ (gethash name name-occurences 0)) + name-occurences))) + (cl-loop for names in names-by-group + for group-name in group-names + do (cl-loop + for name in names + for occurences = (gethash name name-occurences) + do (puthash (format "%s--%s" group-name name) + (if (= occurences 1) + name + (format "%s_%s" name + (replace-regexp-in-string + (rx space) "_" (downcase group-name)))) + names-mapping))) + names-mapping)) + +(defun biome-multi--unique-names (names) + (let ((name-occurences (make-hash-table :test #'equal)) + (added-occurences (make-hash-table :test #'equal))) + (cl-loop for name in names + do (puthash name + (1+ (gethash name name-occurences 0)) + name-occurences)) + (cl-loop for name in names + for occurences = (gethash name name-occurences) + for added = (gethash name added-occurences) + collect (if (= occurences 1) + name + (format "%s_%d" name + (puthash + name + (1+ (or added 0)) + added-occurences)))))) + +(defun biome-multi--join-results (queries query-names vars-mapping results) + (let ((times (make-hash-table :test #'equal)) + (var-values-per-time (make-hash-table :test #'equal))) + (cl-loop for result in results + for query in queries + for query-name in query-names + for group-name = (alist-get :group query) + for vars-field = (intern group-name) + for times-vector = (thread-last + result (alist-get vars-field) (alist-get 'time)) + do (cl-loop for time across times-vector + do (puthash time t times)) + do (cl-loop for (var-name . values) in (seq-filter + (lambda (v) (not (eq 'time (car v)))) + (alist-get vars-field result)) + for mapped-var-name = + (gethash (format "%s--%s" query-name var-name) vars-mapping) + for var-values = (make-hash-table :test #'equal) + do (cl-loop for time across times-vector + for value across values + do (puthash time value var-values)) + do (puthash mapped-var-name var-values var-values-per-time))) + (let ((times-sorted (seq-sort #'string-lessp (hash-table-keys times)))) + `((time . ,(vconcat times-sorted)) + ,@(cl-loop for var-name being the hash-keys of var-values-per-time + using (hash-values var-values) + collect + (cons (intern var-name) + (vconcat + (cl-loop for time in times-sorted + collect (gethash time var-values))))))))) + +(defun biome-multi--merge (queries results) + "Merge QUERIES into one query. + +QUERIES is a list of forms as defined by `biome-query-current'." + (let* ((vars-by-group + (cl-loop for query in queries + for group = (alist-get :group query) + collect (alist-get group (alist-get :params query) + nil nil #'string-equal))) + (query-names + (biome-multi--unique-names + (cl-loop for query in queries + collect (alist-get :name query)))) + (vars-mapping (biome-multi--unique-names-grouped vars-by-group query-names))) + `(((:name . "Multi Query") + (:group . "multi") + (:params . (("multi" . + ,(cl-loop for var-name being the hash-values of vars-mapping + collect var-name))))) + ((multi_units + . ,(cons + (cons 'time "iso8601") + (cl-loop + for result in results + for query in queries + for query-name in query-names + for group-name = (alist-get :group query) + for units-field = (intern (format "%s_units" group-name)) + append (cl-loop + for (var-name . unit) in (alist-get units-field result) + unless (equal var-name 'time) + collect (cons (intern + (gethash (format "%s--%s" query-name var-name) + vars-mapping)) + unit))))) + (multi . ,(biome-multi--join-results queries query-names vars-mapping results)))))) + +(provide 'biome-multi) +;;; biome-multi.el ends here diff --git a/biome-query.el b/biome-query.el index 9d8db40..28cbef1 100644 --- a/biome-query.el +++ b/biome-query.el @@ -175,12 +175,14 @@ KEY is the api key of the variable. VAR-NAMES is the output of (capitalize (replace-regexp-in-string (regexp-quote "_") " " key)))) -(cl-defmethod transient-format ((_obj biome-query--transient-report)) - "Format the `biome-query-current'." - (let ((group (alist-get :group biome-query-current)) +(defun biome-query--format (query) + "Format QUERY for display. + +QUERY is a form as defined by `transient-define-prefix'." + (let ((group (alist-get :group query)) (var-names (biome-query--get-var-names-cache)) lat lon group-vars line-vars vars) - (dolist (item (alist-get :params biome-query-current)) + (dolist (item (alist-get :params query)) (cond ((stringp item) (push (biome-query--get-header item var-names) vars)) @@ -251,6 +253,10 @@ KEY is the api key of the variable. VAR-NAMES is the output of (when line-vars (concat (mapconcat #'identity line-vars "\n") "\n"))))) +(cl-defmethod transient-format ((_obj biome-query--transient-report)) + "Format the `biome-query-current'." + (biome-query--format biome-query-current)) + (transient-define-infix biome-query--transient-report-infix () :class 'biome-query--transient-report :key "~~1") @@ -953,7 +959,7 @@ SUFFIXES is a list of suffix definitions." "Process the query made by `biome-query'." (interactive) (unless biome-query--callback - (user-error "Biome-query--callback is not set")) + (error "Biome-query--callback is not set")) (funcall biome-query--callback biome-query-current)) (defun biome-query--generate-preset () diff --git a/biome.el b/biome.el index 576f274..ae6d5ea 100644 --- a/biome.el +++ b/biome.el @@ -40,6 +40,7 @@ ;;; Code: (require 'biome-api) +(require 'biome-multi) (require 'biome-query) (require 'biome-grid) @@ -73,6 +74,17 @@ API." ;; previous invocation of `biome' (biome-query--section-open (alist-get :name biome-query-current))) +(defun biome-multi () + "Run multiple queries to Open Meteo and join results." + (interactive) + (biome-multi-query + (lambda (query) + (biome-api-get-multiple + query + (lambda (queries results) + (let ((merged (biome-multi--merge queries results))) + (funcall biome-frontend (nth 0 merged) (nth 1 merged)))))))) + (defmacro biome-def-preset (name params) "Declare a query preset.