├── .gitignore ├── CHANGES.rst ├── LICENSE ├── README.rst ├── docs ├── backlog.rst ├── release.rst └── setup.rst ├── examples ├── phenodata-mellifera │ ├── dashboard.json │ ├── datasource.json │ ├── demo.py │ └── readme.rst └── sinewave-midnights │ ├── dashboard.json │ ├── datasource.json │ ├── demo.py │ └── readme.rst ├── grafana_pandas_datasource ├── __init__.py ├── config.py ├── core.py ├── logging.py ├── registry.py └── service.py ├── poetry.lock ├── poetry.toml └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .idea 4 | var/lib 5 | dist 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Changelog 3 | ********* 4 | 5 | 6 | development 7 | =========== 8 | 9 | 10 | 2022-02-06 0.3.0 11 | ================ 12 | - Improve installation documentation 13 | - Mitigate ``400 Bad Request`` responses when receiving GET requests with 14 | empty HTTP request bodies but still setting ``Content-Type: application/json``. 15 | Thanks for the report, @byteptr and @MichielKE! 16 | - Improve installation instructions for Windows. Thanks, @MichielKE! 17 | - Improve documentation 18 | - Initialize Flask logger appropriately 19 | - Add capability for request/response logging 20 | - Format the code with black and isort 21 | - Rename ``util.py`` to ``core.py`` 22 | - Mitigate pandas deprecation warning about the ``astype()`` method of datetimelike 23 | types to convert to integer dtypes 24 | 25 | 26 | 2022-01-22 0.2.0 27 | ================ 28 | - Add example for annotating phenology data within Grafana 29 | - Change license to AGPL-3.0 30 | - Modernize dependency versions. Drop support for Python 3.6. 31 | - Improve sandbox environment setup and documentation 32 | 33 | 34 | 2020-12-27 0.1.2 35 | ================ 36 | - Adjust documentation 37 | 38 | 39 | 2020-12-27 0.1.1 40 | ================ 41 | - Adjust documentation 42 | 43 | 44 | 2020-12-27 0.1.0 45 | ================ 46 | - First official release 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/pyversions/grafana-pandas-datasource.svg 2 | :target: https://pypi.org/project/grafana-pandas-datasource/ 3 | 4 | .. image:: https://img.shields.io/badge/Grafana-6.x%20--%208.x-blue.svg 5 | :target: https://github.com/grafana/grafana 6 | :alt: Supported Grafana versions 7 | 8 | .. image:: https://img.shields.io/pypi/v/grafana-pandas-datasource.svg 9 | :target: https://pypi.org/project/grafana-pandas-datasource/ 10 | 11 | .. image:: https://img.shields.io/pypi/l/grafana-pandas-datasource.svg 12 | :target: https://github.com/panodata/grafana-pandas-datasource/blob/main/LICENSE 13 | 14 | .. image:: https://img.shields.io/pypi/status/grafana-pandas-datasource.svg 15 | :target: https://pypi.org/project/grafana-pandas-datasource/ 16 | 17 | .. image:: https://img.shields.io/pypi/dm/grafana-pandas-datasource.svg 18 | :target: https://pypi.org/project/grafana-pandas-datasource/ 19 | 20 | | 21 | 22 | ######################### 23 | Grafana pandas datasource 24 | ######################### 25 | 26 | 27 | ***** 28 | About 29 | ***** 30 | 31 | A HTTP API based on Flask_ for serving pandas_ data frames to Grafana_, 32 | generated by NumPy_. The `Grafana Simple JSON Datasource`_ is used to interface 33 | Grafana with the HTTP API. 34 | 35 | This way, a pure Python application can be used to directly supply data to 36 | Grafana, both easily and powerfully. 37 | 38 | The framework supports feeding both timeseries data as well as annotations 39 | through corresponding ``/query`` and ``/annotations`` endpoints and also 40 | provides ``/search`` and ``/panels`` endpoints. 41 | 42 | 43 | *********** 44 | Screenshots 45 | *********** 46 | 47 | .. figure:: https://user-images.githubusercontent.com/453543/103137119-78dab480-46c6-11eb-829f-6aa957239804.png 48 | 49 | Image: Sinewave data and midnights annotations, both generated using NumPy_, 50 | see `Sinewave/Midnights example`_. 51 | 52 | 53 | ***** 54 | Setup 55 | ***** 56 | 57 | `Grafana pandas datasource setup`_ outlines how to install all software 58 | prerequisites needed to run this project. Please read this section carefully. 59 | 60 | :: 61 | 62 | pip install grafana-pandas-datasource 63 | 64 | 65 | ******** 66 | Synopsis 67 | ******** 68 | 69 | Test drive:: 70 | 71 | # Run Grafana pandas datasource demo. 72 | python examples/sinewave-midnights/demo.py 73 | 74 | # Submit a timeseries data request. 75 | echo '{"targets": [{"target": "sine_wave:24"}], "range": {"from": "2022-02-22T15", "to": "2022-02-22T20"}}' | http http://127.0.0.1:3003/query 76 | 77 | # Submit an annotation data request. 78 | echo '{"annotation": {"query": "midnights:xx"}, "range": {"from": "2022-02-20", "to": "2022-02-22"}}' | http http://127.0.0.1:3003/annotations 79 | 80 | When the environment has been properly configured, both requests above will 81 | yield appropriate responses. 82 | 83 | Then, configure the Grafana entities. You will need a datasource object and a 84 | dashboard object. 85 | 86 | 87 | ******** 88 | Examples 89 | ******** 90 | 91 | There are `different demo programs`_ accompanied with Grafana datasource and 92 | dashboard definition files. 93 | 94 | After confirming the sandbox environment has been installed successfully, 95 | please head over to the `Sinewave/Midnights example`_ page in order to learn 96 | how to provision Grafana with corresponding resources. 97 | 98 | 99 | ********************** 100 | Custom implementations 101 | ********************** 102 | 103 | In order to conceive your own `pandas`_-based data source, please use the 104 | `Sinewave/Midnights demo.py`_ as a blueprint. If you think it would be a 105 | valuable contribution to the community, we will be happy to add it to the 106 | repository. 107 | 108 | 109 | ******* 110 | Credits 111 | ******* 112 | 113 | Kudos to Linar, who conceived the initial version of this software the other 114 | day at https://gist.github.com/linar-jether/95ff412f9d19fdf5e51293eb0c09b850. 115 | 116 | 117 | ************** 118 | Other projects 119 | ************** 120 | 121 | Oz Tiram conceived a similar piece of software with Python. He uses the Bottle 122 | web framework. 123 | 124 | - https://gitlab.com/oz123/grafana-python-datasource 125 | - https://oz123.github.io/writings/2019-06-16-Visualize-almost-anything-with-Grafana-and-Python/index.html 126 | 127 | 128 | 129 | .. _different demo programs: https://github.com/panodata/grafana-pandas-datasource/tree/main/examples 130 | .. _Flask: https://github.com/pallets/flask 131 | .. _Grafana: https://github.com/grafana/grafana 132 | .. _Grafana pandas datasource setup: https://github.com/panodata/grafana-pandas-datasource/blob/main/docs/setup.rst 133 | .. _Grafana Simple JSON Datasource: https://grafana.com/grafana/plugins/grafana-simple-json-datasource/ 134 | .. _NumPy: https://numpy.org/ 135 | .. _pandas: https://github.com/pandas-dev/pandas 136 | .. _Sinewave/Midnights demo.py: https://github.com/panodata/grafana-pandas-datasource/blob/main/examples/sinewave-midnights/demo.py 137 | .. _Sinewave/Midnights example: https://github.com/panodata/grafana-pandas-datasource/tree/main/examples/sinewave-midnights 138 | -------------------------------------------------------------------------------- /docs/backlog.rst: -------------------------------------------------------------------------------- 1 | ################################# 2 | Grafana pandas datasource backlog 3 | ################################# 4 | 5 | 6 | ********** 7 | Priority 1 8 | ********** 9 | - [x] Adjust HTML index page 10 | - [x] Add documentation for installation on Windows 11 | - [x] Improve documentation 12 | - [x] Add logging 13 | - [x] Format code with black and isort 14 | - [x] Publish new release 15 | 16 | 17 | ********** 18 | Priority 2 19 | ********** 20 | - [o] Add an example for the "rendering HTML panels" feature, 21 | see https://github.com/panodata/grafana-pandas-datasource/issues/12 22 | - [o] Add software tests 23 | - [o] Integrate sinewave demo into codebase as "builtin:sinewave-demo" 24 | - [o] Expand sinewave example from ``sine_wave:24`` to, e.g. ``fn:sin(freq=24)``. 25 | - [o] Use ``add_panel_reader``, see https://github.com/panodata/grafana-pandas-datasource/pull/3 26 | 27 | 28 | ********** 29 | Priority 3 30 | ********** 31 | - [o] Migrate to https://github.com/marcusolsson/grafana-json-datasource 32 | - [o] Upgrade to Flask 2 33 | - [o] Migrate from Poetry to native pip 34 | -------------------------------------------------------------------------------- /docs/release.rst: -------------------------------------------------------------------------------- 1 | ################################# 2 | Grafana pandas datasource release 3 | ################################# 4 | 5 | 6 | 1. Update changelog in ``CHANGES.rst`` 7 | 2. Tag repository with ``git tag `` 8 | 3. Update repository with ``git push && git push --tags`` 9 | 4. Build packages with ``pip install build && python -m build`` 10 | 5. Upload packages with ``pip install twine && twine upload dist/*`` 11 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | ############################### 2 | Grafana pandas datasource setup 3 | ############################### 4 | 5 | 6 | ************ 7 | Introduction 8 | ************ 9 | 10 | Until this package will provide an appropriate entrypoint program, it is 11 | recommended to install it from the source repository. 12 | 13 | 14 | ******************* 15 | Sandbox environment 16 | ******************* 17 | 18 | In order to work efficiently with the resources provided by this repository, we 19 | recommend to install some programs upfront. This will optimally work on Linux 20 | and macOS. Windows users might use the WSL subsystem, and follow the 21 | instructions for Ubuntu. 22 | 23 | 24 | Install prerequisites 25 | ===================== 26 | :: 27 | 28 | # macOS / Homebrew 29 | brew install git python3 poetry httpie docker 30 | 31 | # Debian and Ubuntu 32 | apt update 33 | apt install --yes git python3 python3-pip httpie docker.io 34 | pip install poetry 35 | 36 | # Arch Linux 37 | pacman --sync --refresh 38 | pacman --noconfirm --sync git python3 poetry httpie docker 39 | 40 | # Fedora 41 | dnf install -y git python3 poetry httpie docker 42 | alternatives --install /usr/bin/python python /usr/bin/python3 1 43 | 44 | # Rocky Linux 45 | dnf install -y git python39 python39-pip docker 46 | pip3 install poetry httpie 47 | 48 | # CentOS 7 49 | yum install -y git docker 50 | yum -y install centos-release-scl-rh centos-release-scl 51 | yum --enablerepo=centos-sclo-rh -y install rh-python38 52 | scl enable rh-python38 bash 53 | pip3 install cryptography==3.3.2 poetry httpie 54 | 55 | # CentOS 8 56 | dnf install -y git python39 python39-pip docker 57 | pip3 install poetry httpie 58 | 59 | # openSUSE 60 | zypper install git python39 python39-pip httpie docker 61 | pip3.9 install poetry 62 | 63 | # Windows / Chocolatey 64 | # https://chocolatey.org/ 65 | choco install git python3 poetry httpie docker-desktop docker-cli 66 | 67 | 68 | Acquire sources 69 | =============== 70 | :: 71 | 72 | git clone https://github.com/panodata/grafana-pandas-datasource 73 | cd grafana-pandas-datasource 74 | 75 | 76 | Bootstrap sandbox 77 | ================= 78 | :: 79 | 80 | python3 -m venv .venv 81 | 82 | # Linux, *nix, macOS 83 | source .venv/bin/activate 84 | 85 | # Windows 86 | .\venv\Scripts\activate 87 | 88 | pip install poetry 89 | poetry install 90 | poetry shell 91 | -------------------------------------------------------------------------------- /examples/phenodata-mellifera/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "overwrite": true, 3 | "dashboard": { 4 | "annotations": { 5 | "list": [ 6 | { 7 | "builtIn": 1, 8 | "datasource": "-- Grafana --", 9 | "enable": true, 10 | "hide": true, 11 | "iconColor": "rgba(0, 211, 255, 1)", 12 | "limit": 100, 13 | "name": "Annotations & Alerts", 14 | "showIn": 0, 15 | "type": "dashboard" 16 | }, 17 | { 18 | "datasource": "phenodata-mellifera", 19 | "enable": true, 20 | "hide": false, 21 | "iconColor": "rgba(255, 96, 96, 1)", 22 | "limit": 100, 23 | "name": "Flowering Events Müncheberg", 24 | "query": "flowering:type=observations&station=müncheberg", 25 | "showIn": 0, 26 | "tags": [], 27 | "type": "tags" 28 | }, 29 | { 30 | "datasource": "phenodata-mellifera", 31 | "enable": true, 32 | "hide": false, 33 | "iconColor": "#5794F2", 34 | "limit": 100, 35 | "name": "Flowering Forecast Müncheberg", 36 | "query": "flowering:type=forecast&station=müncheberg", 37 | "showIn": 0, 38 | "tags": [], 39 | "type": "tags" 40 | } 41 | ] 42 | }, 43 | "editable": true, 44 | "gnetId": null, 45 | "graphTooltip": 0, 46 | "id": 2, 47 | "links": [], 48 | "panels": [ 49 | { 50 | "aliasColors": {}, 51 | "bars": false, 52 | "dashLength": 10, 53 | "dashes": false, 54 | "datasource": "phenodata-mellifera", 55 | "fieldConfig": { 56 | "defaults": { 57 | "custom": {}, 58 | "links": [] 59 | }, 60 | "overrides": [] 61 | }, 62 | "fill": 1, 63 | "fillGradient": 0, 64 | "gridPos": { 65 | "h": 9, 66 | "w": 12, 67 | "x": 0, 68 | "y": 0 69 | }, 70 | "hiddenSeries": false, 71 | "id": 2, 72 | "legend": { 73 | "avg": false, 74 | "current": false, 75 | "max": false, 76 | "min": false, 77 | "show": true, 78 | "total": false, 79 | "values": false 80 | }, 81 | "lines": true, 82 | "linewidth": 1, 83 | "nullPointMode": "null", 84 | "options": { 85 | "alertThreshold": true 86 | }, 87 | "percentage": false, 88 | "pluginVersion": "7.3.6", 89 | "pointradius": 2, 90 | "points": false, 91 | "renderer": "flot", 92 | "seriesOverrides": [], 93 | "spaceLength": 10, 94 | "stack": false, 95 | "steppedLine": false, 96 | "targets": [ 97 | { 98 | "refId": "A", 99 | "target": "select metric", 100 | "type": "timeserie" 101 | } 102 | ], 103 | "thresholds": [], 104 | "timeFrom": null, 105 | "timeRegions": [], 106 | "timeShift": null, 107 | "title": "Fruits for Apis mellifera in Müncheberg, Brandenburg, Germany", 108 | "tooltip": { 109 | "shared": true, 110 | "sort": 0, 111 | "value_type": "individual" 112 | }, 113 | "type": "graph", 114 | "xaxis": { 115 | "buckets": null, 116 | "mode": "time", 117 | "name": null, 118 | "show": true, 119 | "values": [] 120 | }, 121 | "yaxes": [ 122 | { 123 | "format": "short", 124 | "label": null, 125 | "logBase": 1, 126 | "max": null, 127 | "min": null, 128 | "show": true 129 | }, 130 | { 131 | "format": "short", 132 | "label": null, 133 | "logBase": 1, 134 | "max": null, 135 | "min": null, 136 | "show": true 137 | } 138 | ], 139 | "yaxis": { 140 | "align": false, 141 | "alignLevel": null 142 | } 143 | } 144 | ], 145 | "refresh": false, 146 | "schemaVersion": 26, 147 | "style": "dark", 148 | "tags": [], 149 | "templating": { 150 | "list": [] 151 | }, 152 | "time": { 153 | "from": "now-2y", 154 | "to": "now+1y" 155 | }, 156 | "timepicker": {}, 157 | "timezone": "", 158 | "title": "Fruits for Apis mellifera", 159 | "uid": "aoBz38xMy" 160 | } 161 | } -------------------------------------------------------------------------------- /examples/phenodata-mellifera/datasource.json: -------------------------------------------------------------------------------- 1 | { 2 | "access": "proxy", 3 | "basicAuth": false, 4 | "basicAuthPassword": "", 5 | "basicAuthUser": "", 6 | "database": "", 7 | "isDefault": false, 8 | "jsonData": {}, 9 | "name": "phenodata-mellifera", 10 | "orgId": 1, 11 | "password": "", 12 | "readOnly": true, 13 | "secureJsonFields": {}, 14 | "type": "grafana-simple-json-datasource", 15 | "typeLogoUrl": "", 16 | "url": "http://host.docker.internal:3003", 17 | "user": "", 18 | "withCredentials": false 19 | } -------------------------------------------------------------------------------- /examples/phenodata-mellifera/demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2020 Andreas Motl 3 | 4 | License: GNU Affero General Public License, Version 3 5 | """ 6 | import urllib.parse 7 | from datetime import datetime, timedelta 8 | 9 | import pandas as pd 10 | from cachetools import TTLCache, cached 11 | from phenodata.dwd.cdc import DwdCdcClient 12 | from phenodata.dwd.pheno import DwdPhenoData, DwdPhenoDataHumanizer 13 | from phenodata.ftp import FTPSession 14 | from phenodata.util import read_list 15 | 16 | from grafana_pandas_datasource import create_app 17 | from grafana_pandas_datasource.registry import data_generators as dg 18 | from grafana_pandas_datasource.service import pandas_component 19 | 20 | """ 21 | Demo for grafana-pandas-datasource. 22 | 23 | This is a demo program which generates flowering events for 24 | phenology species suitable as fruits for bees (Apis mellifera) 25 | as Grafana annotations. 26 | 27 | Setup:: 28 | 29 | pip install grafana-pandas-datasource phenodata>=0.10.0 cachetools 30 | 31 | To query the annotations, use ``:``, e.g. 32 | - ``flowering:station=müncheberg`` 33 | """ 34 | 35 | 36 | def define_and_register_data(): 37 | 38 | # Sample annotation reader. 39 | def get_mellifera_flowering(query_string, ts_range): 40 | query = dict(urllib.parse.parse_qsl(query_string)) 41 | series = phenodata_mellifera( 42 | dataset="immediate", years=tuple([2019, 2020, 2021]), phases=tuple([5, 7]), options=tuple(query.items()) 43 | ) 44 | return series 45 | 46 | # Register data generators. 47 | dg.add_annotation_reader("flowering", get_mellifera_flowering) 48 | 49 | 50 | @cached(cache=TTLCache(maxsize=65536, ttl=24 * 60 * 60)) 51 | def phenodata_mellifera(dataset: str, years: tuple[int], phases: tuple[str], options: tuple[tuple[str, str]]): 52 | 53 | options = dict(options) 54 | options["type"] = read_list(options.get("type")) or ["observations"] 55 | 56 | cdc_client = DwdCdcClient(ftp=FTPSession()) 57 | humanizer = DwdPhenoDataHumanizer(language="german", long_station=False, show_ids=False) 58 | client = DwdPhenoData(cdc=cdc_client, humanizer=humanizer, dataset=dataset) 59 | 60 | # Rewrite options. 61 | phenodata_options = { 62 | "partition": "recent", 63 | "year": years, 64 | "species": DwdPhenoData.load_preset("options", "species", "mellifera-de-primary"), 65 | "phase-id": phases, 66 | "station": read_list(options.get("station", [])), 67 | "humanize": True, 68 | } 69 | 70 | data_total = [] 71 | 72 | # Get observations 73 | if "observations" in options["type"]: 74 | data_past = client.get_observations(phenodata_options, humanize=phenodata_options["humanize"]) 75 | data_total.append(data_past) 76 | 77 | if "forecast" in options["type"]: 78 | next_year = (datetime.today() + timedelta(days=365)).year 79 | data_future = client.get_forecast( 80 | phenodata_options, forecast_year=next_year, humanize=phenodata_options["humanize"] 81 | ) 82 | data_total.append(data_future) 83 | 84 | data = pd.concat(data_total) 85 | 86 | # Create pandas Series from DataFrame. 87 | index = data.Datum.astype("datetime64") 88 | values = data.Spezies.str.cat(data.Phase, sep=" - ").str.cat(data.Station, sep=" - ") 89 | series = pd.Series(data=values.tolist(), index=index) 90 | series = series.sort_index(ascending=True) 91 | 92 | return series 93 | 94 | 95 | def main(): 96 | 97 | # Define and register data generators. 98 | define_and_register_data() 99 | 100 | # Create Flask application. 101 | app = create_app() 102 | 103 | # Register pandas component. 104 | app.register_blueprint(pandas_component, url_prefix="/") 105 | 106 | # Invoke Flask application. 107 | app.run(host="127.0.0.1", port=3003, debug=True) 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | -------------------------------------------------------------------------------- /examples/phenodata-mellifera/readme.rst: -------------------------------------------------------------------------------- 1 | ################################ 2 | Apis mellifera flowering example 3 | ################################ 4 | 5 | 6 | ***** 7 | About 8 | ***** 9 | 10 | This is a demo program which generates flowering events for 11 | phenology species suitable as fruits for bees (Apis mellifera) 12 | as Grafana annotations. 13 | 14 | .. figure:: https://user-images.githubusercontent.com/453543/103260962-fd1b8900-499f-11eb-8459-9ecaa6c55ac7.png 15 | 16 | Image: Flowering events for some species around Müncheberg, Brandenburg, Germany. 17 | 18 | 19 | ***** 20 | Setup 21 | ***** 22 | 23 | For general installation instructions, see `setup sandbox environment`_. 24 | 25 | :: 26 | 27 | pip install grafana-pandas-datasource phenodata>=0.10.0 cachetools 28 | 29 | 30 | ************** 31 | Start services 32 | ************** 33 | 34 | :: 35 | 36 | # Run Grafana. 37 | docker run --rm -it \ 38 | --publish=3000:3000 --volume="$(pwd)/var/lib/grafana":/var/lib/grafana \ 39 | --env='GF_SECURITY_ADMIN_PASSWORD=admin' --env='GF_INSTALL_PLUGINS=grafana-simple-json-datasource' \ 40 | grafana/grafana:8.3.4 41 | 42 | # Run Grafana pandas datasource demo. 43 | python examples/phenodata-mellifera/demo.py 44 | 45 | 46 | ***************** 47 | Configure Grafana 48 | ***************** 49 | 50 | 51 | Command line 52 | ============ 53 | 54 | You can have a quickstart by putting those two JSON definition files into 55 | Grafana:: 56 | 57 | # Login to Grafana. 58 | export GRAFANA_URL=http://localhost:3000 59 | http --session=grafana ${GRAFANA_URL} --auth=admin:admin 60 | 61 | # Create datasource. 62 | cat examples/phenodata-mellifera/datasource.json | \ 63 | http --session=grafana POST ${GRAFANA_URL}/api/datasources 64 | 65 | # Create dashboard. 66 | cat examples/phenodata-mellifera/dashboard.json | \ 67 | http --session=grafana POST ${GRAFANA_URL}/api/dashboards/db 68 | 69 | Then, visit the dashboard at:: 70 | 71 | open ${GRAFANA_URL} 72 | 73 | 74 | .. note:: 75 | 76 | The host where the datasource service is running can be accessed from the 77 | Grafana Docker container using the hostname ``host.docker.internal``. 78 | 79 | 80 | .. _setup sandbox environment: https://github.com/panodata/grafana-pandas-datasource/blob/main/docs/setup.rst 81 | -------------------------------------------------------------------------------- /examples/sinewave-midnights/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "overwrite": true, 3 | "dashboard": { 4 | "annotations": { 5 | "list": [ 6 | { 7 | "builtIn": 1, 8 | "datasource": "-- Grafana --", 9 | "enable": true, 10 | "hide": true, 11 | "iconColor": "rgba(0, 211, 255, 1)", 12 | "limit": 100, 13 | "name": "Annotations & Alerts", 14 | "showIn": 0, 15 | "type": "dashboard" 16 | }, 17 | { 18 | "datasource": "sinewave-midnights", 19 | "enable": true, 20 | "hide": false, 21 | "iconColor": "rgba(255, 96, 96, 1)", 22 | "limit": 100, 23 | "name": "Midnights", 24 | "query": "midnights:xx", 25 | "showIn": 0, 26 | "tags": [], 27 | "type": "tags" 28 | } 29 | ] 30 | }, 31 | "editable": true, 32 | "gnetId": null, 33 | "graphTooltip": 0, 34 | "links": [], 35 | "panels": [ 36 | { 37 | "aliasColors": {}, 38 | "bars": false, 39 | "dashLength": 10, 40 | "dashes": false, 41 | "datasource": "sinewave-midnights", 42 | "fieldConfig": { 43 | "defaults": { 44 | "custom": {}, 45 | "links": [] 46 | }, 47 | "overrides": [] 48 | }, 49 | "fill": 1, 50 | "fillGradient": 0, 51 | "gridPos": { 52 | "h": 9, 53 | "w": 12, 54 | "x": 0, 55 | "y": 0 56 | }, 57 | "hiddenSeries": false, 58 | "id": 2, 59 | "legend": { 60 | "avg": false, 61 | "current": false, 62 | "max": false, 63 | "min": false, 64 | "show": true, 65 | "total": false, 66 | "values": false 67 | }, 68 | "lines": true, 69 | "linewidth": 1, 70 | "nullPointMode": "null", 71 | "options": { 72 | "alertThreshold": true 73 | }, 74 | "percentage": false, 75 | "pluginVersion": "7.3.6", 76 | "pointradius": 2, 77 | "points": false, 78 | "renderer": "flot", 79 | "seriesOverrides": [], 80 | "spaceLength": 10, 81 | "stack": false, 82 | "steppedLine": false, 83 | "targets": [ 84 | { 85 | "rawQuery": true, 86 | "refId": "A", 87 | "target": "sine_wave:24", 88 | "type": "timeserie" 89 | } 90 | ], 91 | "thresholds": [], 92 | "timeFrom": null, 93 | "timeRegions": [], 94 | "timeShift": null, 95 | "title": "Sine 24", 96 | "tooltip": { 97 | "shared": true, 98 | "sort": 0, 99 | "value_type": "individual" 100 | }, 101 | "type": "graph", 102 | "xaxis": { 103 | "buckets": null, 104 | "mode": "time", 105 | "name": null, 106 | "show": true, 107 | "values": [] 108 | }, 109 | "yaxes": [ 110 | { 111 | "format": "short", 112 | "label": null, 113 | "logBase": 1, 114 | "max": null, 115 | "min": null, 116 | "show": true 117 | }, 118 | { 119 | "format": "short", 120 | "label": null, 121 | "logBase": 1, 122 | "max": null, 123 | "min": null, 124 | "show": true 125 | } 126 | ], 127 | "yaxis": { 128 | "align": false, 129 | "alignLevel": null 130 | } 131 | } 132 | ], 133 | "refresh": false, 134 | "schemaVersion": 26, 135 | "style": "dark", 136 | "tags": [], 137 | "templating": { 138 | "list": [] 139 | }, 140 | "time": { 141 | "from": "2020-12-24T07:18:21.710Z", 142 | "to": "2020-12-26T07:18:21.710Z" 143 | }, 144 | "timepicker": {}, 145 | "timezone": "", 146 | "title": "Sine 24", 147 | "uid": "xNbUrobGz" 148 | } 149 | } -------------------------------------------------------------------------------- /examples/sinewave-midnights/datasource.json: -------------------------------------------------------------------------------- 1 | { 2 | "access": "proxy", 3 | "basicAuth": false, 4 | "basicAuthPassword": "", 5 | "basicAuthUser": "", 6 | "database": "", 7 | "isDefault": false, 8 | "jsonData": {}, 9 | "name": "sinewave-midnights", 10 | "orgId": 1, 11 | "password": "", 12 | "readOnly": true, 13 | "secureJsonFields": {}, 14 | "type": "grafana-simple-json-datasource", 15 | "typeLogoUrl": "", 16 | "url": "http://host.docker.internal:3003", 17 | "user": "", 18 | "withCredentials": false 19 | } -------------------------------------------------------------------------------- /examples/sinewave-midnights/demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Linar 3 | Copyright 2020 Andreas Motl 4 | 5 | License: GNU Affero General Public License, Version 3 6 | """ 7 | import numpy as np 8 | import pandas as pd 9 | 10 | from grafana_pandas_datasource import create_app 11 | from grafana_pandas_datasource.registry import data_generators as dg 12 | from grafana_pandas_datasource.service import pandas_component 13 | 14 | """ 15 | Demo for grafana-pandas-datasource. 16 | 17 | This is a demo program which generates data using NumPy and pandas. 18 | It creates 19 | - a sine wave for data and 20 | - midnight times for annotations 21 | 22 | Setup:: 23 | 24 | pip install grafana-pandas-datasource 25 | 26 | To query the reader, use ``:``, e.g. 27 | - ``sine_wave:24`` 28 | - ``midnights:xx`` 29 | 30 | Synopsis:: 31 | 32 | python examples/sinewave-midnights/demo.py 33 | echo '{"targets": [{"target": "sine_wave:24"}], "range": {"from": "2022-02-22T15", "to": "2022-02-22T20"}}' | http http://127.0.0.1:3003/query 34 | echo '{"annotation": {"query": "midnights:xx"}, "range": {"from": "2022-02-20", "to": "2022-02-22"}}' | http http://127.0.0.1:3003/annotations 35 | 36 | """ 37 | 38 | 39 | def define_and_register_data(): 40 | 41 | # Sample timeseries reader. 42 | def get_sine(freq, ts_range): 43 | freq = int(freq) 44 | ts = pd.date_range(ts_range["$gt"], ts_range["$lte"], freq="H") 45 | return pd.Series(np.sin(np.arange(len(ts)) * np.pi * freq * 2 / float(len(ts))), index=ts).to_frame("value") 46 | 47 | # Sample annotation reader. 48 | def get_midnights(query_string, ts_range): 49 | return pd.Series( 50 | index=pd.date_range(ts_range["$gt"], ts_range["$lte"], freq="D", normalize=True), dtype="float64" 51 | ).fillna("Text for annotation - midnight") 52 | 53 | # Register data generators. 54 | dg.add_metric_reader("sine_wave", get_sine) 55 | dg.add_annotation_reader("midnights", get_midnights) 56 | 57 | 58 | def main(): 59 | 60 | # Define and register data generators. 61 | define_and_register_data() 62 | 63 | # Create Flask application. 64 | app = create_app() 65 | 66 | # Register pandas component. 67 | app.register_blueprint(pandas_component, url_prefix="/") 68 | 69 | # Invoke Flask application. 70 | app.run(host="127.0.0.1", port=3003, debug=True) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /examples/sinewave-midnights/readme.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | Sinewave/Midnights example 3 | ########################## 4 | 5 | 6 | ***** 7 | About 8 | ***** 9 | 10 | This is a demo program which generates a sine wave for data and 11 | annotations for designating midnight times. 12 | 13 | .. figure:: https://user-images.githubusercontent.com/453543/103137119-78dab480-46c6-11eb-829f-6aa957239804.png 14 | 15 | Image: Sinewave data and midnights annotations, both generated using NumPy_. 16 | 17 | 18 | ***** 19 | Setup 20 | ***** 21 | 22 | For general installation instructions, see `setup sandbox environment`_. 23 | 24 | 25 | ************** 26 | Start services 27 | ************** 28 | 29 | :: 30 | 31 | # Run Grafana. 32 | docker run --rm -it \ 33 | --publish=3000:3000 --volume="$(pwd)/var/lib/grafana":/var/lib/grafana \ 34 | --env='GF_SECURITY_ADMIN_PASSWORD=admin' --env='GF_INSTALL_PLUGINS=grafana-simple-json-datasource' \ 35 | grafana/grafana:8.3.4 36 | 37 | # Run Grafana pandas datasource demo. 38 | python examples/sinewave-midnights/demo.py 39 | 40 | 41 | ***************** 42 | Configure Grafana 43 | ***************** 44 | 45 | Command line 46 | ============ 47 | 48 | You can have a quickstart by putting those two JSON definition files into 49 | Grafana:: 50 | 51 | # Login to Grafana. 52 | export GRAFANA_URL=http://localhost:3000 53 | http --session=grafana ${GRAFANA_URL} --auth=admin:admin 54 | 55 | # Create datasource. 56 | cat examples/sinewave-midnights/datasource.json | \ 57 | http --session=grafana POST ${GRAFANA_URL}/api/datasources 58 | 59 | # Create dashboard. 60 | cat examples/sinewave-midnights/dashboard.json | \ 61 | http --session=grafana POST ${GRAFANA_URL}/api/dashboards/db 62 | 63 | Then, visit the dashboard at:: 64 | 65 | open http://localhost:3000/d/xNbUrobGz/sine-24 66 | 67 | GUI 68 | === 69 | 70 | This section walks you through setting up a data source and dashboard in 71 | Grafana manually, step by step with screenshots. Please follow the guidelines 72 | carefully. 73 | 74 | .. figure:: https://user-images.githubusercontent.com/453543/150621604-f9b4664c-493a-4a9d-bd46-cf59da175438.png 75 | 76 | Install "SimpleJson" plugin at http://localhost:3000/plugins/grafana-simple-json-datasource. 77 | 78 | .. figure:: https://user-images.githubusercontent.com/453543/150621516-cb8b24fa-46ee-4515-b66e-81f79a046912.png 79 | 80 | Add new data source of "SimpleJson" type at http://localhost:3000/datasources/new. 81 | Configure the URL to the Flask service serving pandas data frames. 82 | When running Grafana in Docker, use ``host.docker.internal`` to address the 83 | Docker host. 84 | 85 | .. figure:: https://user-images.githubusercontent.com/453543/150621520-f0eeb740-2c12-4a8b-908c-50893a8bd583.png 86 | 87 | Create dashboard with Timeseries or Graph panel at http://localhost:3000/dashboard/new, 88 | adjust "Data source" and "metric" values. 89 | 90 | .. figure:: https://user-images.githubusercontent.com/453543/150621869-5d226582-886c-41c4-a446-d8d75685f9d2.png 91 | 92 | At the time picker, choose an interval of "Last 2 days". 93 | 94 | .. figure:: https://user-images.githubusercontent.com/453543/150621970-3d20f11c-007a-4e6e-ad8f-abf1f3e02ed0.png 95 | 96 | Save your dashboard. 97 | 98 | 99 | .. _NumPy: https://numpy.org/ 100 | .. _setup sandbox environment: https://github.com/panodata/grafana-pandas-datasource/blob/main/docs/setup.rst 101 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Linar 3 | Copyright 2020-2022 Andreas Motl 4 | 5 | License: GNU Affero General Public License, Version 3 6 | """ 7 | from flask import Flask 8 | from flask_cors import CORS 9 | 10 | from grafana_pandas_datasource import config 11 | from grafana_pandas_datasource.logging import LoggingMiddleware, setup_logging 12 | 13 | 14 | def create_app(test_config=None) -> Flask: 15 | """ 16 | Create and configure the Flask application, with CORS. 17 | 18 | - https://flask.palletsprojects.com/en/1.1.x/tutorial/factory/ 19 | - https://flask-cors.readthedocs.io/ 20 | 21 | :param test_config: 22 | :return: Configured Flask application. 23 | """ 24 | 25 | setup_logging() 26 | 27 | # Create Flask application. 28 | app = Flask(__name__) 29 | 30 | # Load configuration. 31 | app.config.from_object(config) 32 | 33 | # Initialize Cross Origin Resource sharing support for 34 | # the application on all routes, for all origins and methods. 35 | CORS(app) 36 | app.config["CORS_HEADERS"] = "Content-Type" 37 | 38 | # Optionally enable HTTP conversation tracing. 39 | if app.config.get("TRACE_CONVERSATION"): 40 | app.wsgi_app = LoggingMiddleware(app.wsgi_app) 41 | 42 | return app 43 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/config.py: -------------------------------------------------------------------------------- 1 | # Flask configuration file. 2 | # https://flask.palletsprojects.com/en/1.0.x/config/ 3 | # https://exploreflask.com/en/latest/configuration.html 4 | 5 | # Enable HTTP conversation tracing. Dumps full HTTP request and response 6 | # content to the application log file. 7 | TRACE_CONVERSATION = False 8 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/core.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from werkzeug.exceptions import abort 4 | 5 | 6 | def dataframe_to_response(target, df, freq=None): 7 | response = [] 8 | 9 | if df.empty: 10 | return response 11 | 12 | if freq is not None: 13 | orig_tz = df.index.tz 14 | df = df.tz_convert("UTC").resample(rule=freq, label="right", closed="right").mean().tz_convert(orig_tz) 15 | 16 | if isinstance(df, pd.Series): 17 | response.append(_series_to_response(df, target)) 18 | elif isinstance(df, pd.DataFrame): 19 | for col in df: 20 | response.append(_series_to_response(df[col], target)) 21 | else: 22 | abort(404, Exception("Received object is not a pandas DataFrame or Series.")) 23 | 24 | return response 25 | 26 | 27 | def dataframe_to_json_table(target, df): 28 | response = [] 29 | 30 | if df.empty: 31 | return response 32 | 33 | if isinstance(df, pd.DataFrame): 34 | response.append( 35 | { 36 | "type": "table", 37 | "columns": df.columns.map(lambda col: {"text": col}).tolist(), 38 | "rows": df.where(pd.notnull(df), None).values.tolist(), 39 | } 40 | ) 41 | else: 42 | abort(404, Exception("Received object is not a pandas DataFrame.")) 43 | 44 | return response 45 | 46 | 47 | def annotations_to_response(target, df): 48 | response = [] 49 | 50 | # Single series with DatetimeIndex and values as text 51 | if isinstance(df, pd.Series): 52 | for timestamp, value in df.iteritems(): 53 | response.append( 54 | { 55 | "annotation": target, # The original annotation sent from Grafana. 56 | "time": timestamp.value // 10**6, # Time since UNIX Epoch in milliseconds. (required) 57 | "title": value, # The title for the annotation tooltip. (required) 58 | # "tags": tags, # Tags for the annotation. (optional) 59 | # "text": text # Text for the annotation. (optional) 60 | } 61 | ) 62 | 63 | # DataFrame with annotation text/tags for each entry 64 | elif isinstance(df, pd.DataFrame): 65 | for timestamp, row in df.iterrows(): 66 | annotation = { 67 | "annotation": target, # The original annotation sent from Grafana. 68 | "time": timestamp.value // 10**6, # Time since UNIX Epoch in milliseconds. (required) 69 | "title": row.get("title", ""), # The title for the annotation tooltip. (required) 70 | } 71 | 72 | if "text" in row: 73 | annotation["text"] = str(row.get("text")) 74 | if "tags" in row: 75 | annotation["tags"] = str(row.get("tags")) 76 | 77 | response.append(annotation) 78 | else: 79 | abort(404, Exception("Received object is not a pandas DataFrame or Series.")) 80 | 81 | return response 82 | 83 | 84 | def _series_to_annotations(df, target): 85 | if df.empty: 86 | return {"target": "%s" % (target), "datapoints": []} 87 | 88 | sorted_df = df.dropna().sort_index() 89 | timestamps = (sorted_df.index.astype(pd.np.int64) // 10**6).values.tolist() 90 | values = sorted_df.values.tolist() 91 | 92 | return {"target": "%s" % (df.name), "datapoints": list(zip(values, timestamps))} 93 | 94 | 95 | def _series_to_response(df, target): 96 | if df.empty: 97 | return {"target": "%s" % (target), "datapoints": []} 98 | 99 | sorted_df = df.dropna().sort_index() 100 | 101 | timestamps = (sorted_df.index.view(np.int64) // 10**6).tolist() 102 | 103 | values = sorted_df.values.tolist() 104 | 105 | return {"target": "%s" % (df.name), "datapoints": list(zip(values, timestamps))} 106 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/logging.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | from logging.config import dictConfig 4 | from pprint import pprint 5 | 6 | 7 | def setup_logging(level=logging.INFO): 8 | log_format = "%(asctime)-15s.%(msecs)d [%(name)-25s] %(levelname)-7s: %(message)s" 9 | dictConfig( 10 | { 11 | "version": 1, 12 | "formatters": { 13 | "default": { 14 | "format": log_format, 15 | } 16 | }, 17 | "handlers": { 18 | "wsgi": { 19 | "class": "logging.StreamHandler", 20 | "stream": "ext://flask.logging.wsgi_errors_stream", 21 | "formatter": "default", 22 | } 23 | }, 24 | "root": {"level": level, "handlers": ["wsgi"]}, 25 | } 26 | ) 27 | 28 | 29 | class LoggingMiddleware(object): 30 | """ 31 | - https://blog.caoyu.info/middleware-in-flask.html 32 | - https://gist.github.com/georgevreilly/5762777 33 | - https://stackoverflow.com/questions/67563385/how-do-i-access-response-content-in-wsgi-middleware-for-flask/67572634#67572634 34 | """ 35 | 36 | def __init__(self, app): 37 | self._app = app 38 | 39 | def __call__(self, environ, start_response): 40 | 41 | errorlog = environ["wsgi.errors"] 42 | 43 | request_body_content, request_body_length = self.get_request_body(environ) 44 | pprint(("REQUEST", environ), stream=errorlog) 45 | pprint(("REQUEST-BODY", request_body_content), stream=errorlog) 46 | 47 | def log_response(status, headers, *args): 48 | pprint(("RESPONSE", status, headers), stream=errorlog) 49 | response = start_response(status, headers, *args) 50 | return response 51 | 52 | # Capture response body. 53 | app_iter = self._app(environ, log_response) 54 | body = b"".join(app_iter) 55 | pprint(("RESPONSE-BODY:", body)) 56 | app_iter = [body] 57 | return app_iter 58 | 59 | def get_request_body(self, environ): 60 | """ 61 | Get request body from WSGI environment. 62 | """ 63 | content_length = environ.get("CONTENT_LENGTH") 64 | body = "" 65 | if content_length: 66 | if content_length == "-1": 67 | # This is a special case, where the content length is basically undetermined. 68 | body = environ["wsgi.input"].read(-1) 69 | content_length = len(body) 70 | else: 71 | content_length = int(content_length) 72 | body = environ["wsgi.input"].read(content_length) 73 | # Reset request body for the nested app. 74 | environ["wsgi.input"] = io.BytesIO(body) 75 | else: 76 | content_length = 0 77 | return body, content_length 78 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/registry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Linar 3 | Copyright 2020-2022 Andreas Motl 4 | 5 | License: GNU Affero General Public License, Version 3 6 | """ 7 | from dataclasses import dataclass, field 8 | from typing import Callable, Dict 9 | 10 | 11 | @dataclass 12 | class DataGenerators: 13 | """ 14 | Store references to data generator functions 15 | yielding pandas data frames. 16 | """ 17 | 18 | metric_readers: Dict[str, Callable] = field(default_factory=dict) 19 | metric_finders: Dict[str, Callable] = field(default_factory=dict) 20 | annotation_readers: Dict[str, Callable] = field(default_factory=dict) 21 | panel_readers: Dict[str, Callable] = field(default_factory=dict) 22 | 23 | def add_metric_reader(self, name, reader): 24 | self.metric_readers[name] = reader 25 | 26 | def add_metric_finder(self, name, finder): 27 | self.metric_finders[name] = finder 28 | 29 | def add_annotation_reader(self, name, reader): 30 | self.annotation_readers[name] = reader 31 | 32 | def add_panel_reader(self, name, reader): 33 | self.panel_readers[name] = reader 34 | 35 | 36 | """ 37 | @dataclass 38 | class DataGeneratorRegistry: 39 | generators: Dict[str, DataGenerators] = field(default_factory=dict) 40 | """ 41 | 42 | 43 | # Global reference to instance of DataGenerators. 44 | data_generators: DataGenerators = DataGenerators() 45 | -------------------------------------------------------------------------------- /grafana_pandas_datasource/service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Linar 3 | Copyright 2020-2022 Andreas Motl 4 | 5 | License: GNU Affero General Public License, Version 3 6 | """ 7 | import pandas as pd 8 | from flask import Blueprint, abort, current_app, jsonify, request 9 | from flask_cors import cross_origin 10 | 11 | from grafana_pandas_datasource.registry import data_generators as dg 12 | from grafana_pandas_datasource.core import ( 13 | annotations_to_response, 14 | dataframe_to_json_table, 15 | dataframe_to_response, 16 | ) 17 | 18 | pandas_component = Blueprint("pandas-component", __name__) 19 | methods = ("GET", "POST") 20 | 21 | 22 | @pandas_component.route("/", methods=methods) 23 | @cross_origin() 24 | def test_datasource(): 25 | current_app.logger.info('Request to "test_datasource" endpoint at /') 26 | return ( 27 | "Grafana pandas datasource: Serve NumPy data via pandas data frames to Grafana. " 28 | 'For documentation, see https://github.com/panodata/grafana-pandas-datasource.' 29 | ) 30 | 31 | 32 | @pandas_component.route("/search", methods=methods) 33 | @cross_origin() 34 | def find_metrics(): 35 | current_app.logger.info('Request to "find_metrics" endpoint at /search') 36 | req = request.get_json() 37 | 38 | target = req.get("target", "*") 39 | 40 | if ":" in target: 41 | finder, target = target.split(":", 1) 42 | else: 43 | finder = target 44 | 45 | if not target or finder not in dg.metric_finders: 46 | metrics = [] 47 | if target == "*": 48 | metrics += dg.metric_finders.keys() 49 | metrics += dg.metric_readers.keys() 50 | else: 51 | metrics.append(target) 52 | 53 | return jsonify(metrics) 54 | else: 55 | return jsonify(list(dg.metric_finders[finder](target))) 56 | 57 | 58 | @pandas_component.route("/query", methods=methods) 59 | @cross_origin(max_age=600) 60 | def query_metrics(): 61 | current_app.logger.info('Request to "query_metrics" endpoint at /query') 62 | req = request.get_json() 63 | 64 | results = [] 65 | 66 | ts_range = { 67 | "$gt": pd.Timestamp(req["range"]["from"]).to_pydatetime(), 68 | "$lte": pd.Timestamp(req["range"]["to"]).to_pydatetime(), 69 | } 70 | 71 | if "intervalMs" in req: 72 | freq = str(req.get("intervalMs")) + "ms" 73 | else: 74 | freq = None 75 | 76 | for target in req["targets"]: 77 | if ":" not in target.get("target", ""): 78 | abort(404, Exception("Target must be of type: :, got instead: " + target["target"])) 79 | 80 | req_type = target.get("type", "timeserie") 81 | 82 | finder, target = target["target"].split(":", 1) 83 | query_results = dg.metric_readers[finder](target, ts_range) 84 | 85 | if req_type == "table": 86 | results.extend(dataframe_to_json_table(target, query_results)) 87 | else: 88 | results.extend(dataframe_to_response(target, query_results, freq=freq)) 89 | 90 | return jsonify(results) 91 | 92 | 93 | @pandas_component.route("/annotations", methods=methods) 94 | @cross_origin(max_age=600) 95 | def query_annotations(): 96 | current_app.logger.info('Request to "query_annotations" endpoint at /annotations') 97 | req = request.get_json() 98 | 99 | results = [] 100 | 101 | ts_range = { 102 | "$gt": pd.Timestamp(req["range"]["from"]).to_pydatetime(), 103 | "$lte": pd.Timestamp(req["range"]["to"]).to_pydatetime(), 104 | } 105 | 106 | query = req["annotation"]["query"] 107 | 108 | if ":" not in query: 109 | abort(404, Exception("Target must be of type: :, got instead: " + query)) 110 | 111 | finder, target = query.split(":", 1) 112 | results.extend(annotations_to_response(query, dg.annotation_readers[finder](target, ts_range))) 113 | 114 | return jsonify(results) 115 | 116 | 117 | @pandas_component.route("/panels", methods=methods) 118 | @cross_origin() 119 | def get_panel(): 120 | current_app.logger.info('Request to "get_panel" endpoint at /panels') 121 | req = request.args 122 | 123 | ts_range = { 124 | "$gt": pd.Timestamp(int(req["from"]), unit="ms").to_pydatetime(), 125 | "$lte": pd.Timestamp(int(req["to"]), unit="ms").to_pydatetime(), 126 | } 127 | 128 | query = req["query"] 129 | 130 | if ":" not in query: 131 | abort(404, Exception("Target must be of type: :, got instead: " + query)) 132 | 133 | finder, target = query.split(":", 1) 134 | return dg.panel_readers[finder](target, ts_range) 135 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "click" 3 | version = "7.1.2" 4 | description = "Composable command line interface toolkit" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 8 | 9 | [[package]] 10 | name = "flask" 11 | version = "1.1.4" 12 | description = "A simple framework for building complex web applications." 13 | category = "main" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.dependencies] 18 | click = ">=5.1,<8.0" 19 | itsdangerous = ">=0.24,<2.0" 20 | Jinja2 = ">=2.10.1,<3.0" 21 | Werkzeug = ">=0.15,<2.0" 22 | 23 | [package.extras] 24 | dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] 25 | docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] 26 | dotenv = ["python-dotenv"] 27 | 28 | [[package]] 29 | name = "flask-cors" 30 | version = "3.0.10" 31 | description = "A Flask extension adding a decorator for CORS support" 32 | category = "main" 33 | optional = false 34 | python-versions = "*" 35 | 36 | [package.dependencies] 37 | Flask = ">=0.9" 38 | Six = "*" 39 | 40 | [[package]] 41 | name = "itsdangerous" 42 | version = "1.1.0" 43 | description = "Various helpers to pass data to untrusted environments and back." 44 | category = "main" 45 | optional = false 46 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 47 | 48 | [[package]] 49 | name = "jinja2" 50 | version = "2.11.2" 51 | description = "A very fast and expressive template engine." 52 | category = "main" 53 | optional = false 54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 55 | 56 | [package.dependencies] 57 | MarkupSafe = ">=0.23" 58 | 59 | [package.extras] 60 | i18n = ["Babel (>=0.8)"] 61 | 62 | [[package]] 63 | name = "markupsafe" 64 | version = "1.1.1" 65 | description = "Safely add untrusted strings to HTML/XML markup." 66 | category = "main" 67 | optional = false 68 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 69 | 70 | [[package]] 71 | name = "numpy" 72 | version = "1.22.1" 73 | description = "NumPy is the fundamental package for array computing with Python." 74 | category = "main" 75 | optional = false 76 | python-versions = ">=3.8" 77 | 78 | [[package]] 79 | name = "pandas" 80 | version = "1.3.5" 81 | description = "Powerful data structures for data analysis, time series, and statistics" 82 | category = "main" 83 | optional = false 84 | python-versions = ">=3.7.1" 85 | 86 | [package.dependencies] 87 | numpy = [ 88 | {version = ">=1.17.3", markers = "platform_machine != \"aarch64\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, 89 | {version = ">=1.19.2", markers = "platform_machine == \"aarch64\" and python_version < \"3.10\""}, 90 | {version = ">=1.20.0", markers = "platform_machine == \"arm64\" and python_version < \"3.10\""}, 91 | {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, 92 | ] 93 | python-dateutil = ">=2.7.3" 94 | pytz = ">=2017.3" 95 | 96 | [package.extras] 97 | test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] 98 | 99 | [[package]] 100 | name = "python-dateutil" 101 | version = "2.8.1" 102 | description = "Extensions to the standard Python datetime module" 103 | category = "main" 104 | optional = false 105 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 106 | 107 | [package.dependencies] 108 | six = ">=1.5" 109 | 110 | [[package]] 111 | name = "pytz" 112 | version = "2020.5" 113 | description = "World timezone definitions, modern and historical" 114 | category = "main" 115 | optional = false 116 | python-versions = "*" 117 | 118 | [[package]] 119 | name = "six" 120 | version = "1.15.0" 121 | description = "Python 2 and 3 compatibility utilities" 122 | category = "main" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 125 | 126 | [[package]] 127 | name = "werkzeug" 128 | version = "1.0.1" 129 | description = "The comprehensive WSGI web application library." 130 | category = "main" 131 | optional = false 132 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 133 | 134 | [package.extras] 135 | dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] 136 | watchdog = ["watchdog"] 137 | 138 | [metadata] 139 | lock-version = "1.1" 140 | python-versions = "^3.7.1" 141 | content-hash = "bc26d29ba5b2b750631d6acc8515e0f7656a44342008c1e8389a320cda312005" 142 | 143 | [metadata.files] 144 | click = [ 145 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 146 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 147 | ] 148 | flask = [ 149 | {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, 150 | {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"}, 151 | ] 152 | flask-cors = [ 153 | {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, 154 | {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, 155 | ] 156 | itsdangerous = [ 157 | {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, 158 | {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, 159 | ] 160 | jinja2 = [ 161 | {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, 162 | {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, 163 | ] 164 | markupsafe = [ 165 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 166 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 167 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 168 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 169 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 170 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 171 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 172 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 173 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 174 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 175 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 176 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 177 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 178 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 179 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 180 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 181 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 182 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 183 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 184 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 185 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 186 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 187 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 188 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 189 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 190 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 191 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 192 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 193 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 194 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 195 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 196 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 197 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 198 | ] 199 | numpy = [ 200 | {file = "numpy-1.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d62d6b0870b53799204515145935608cdeb4cebb95a26800b6750e48884cc5b"}, 201 | {file = "numpy-1.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831f2df87bd3afdfc77829bc94bd997a7c212663889d56518359c827d7113b1f"}, 202 | {file = "numpy-1.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d1563060e77096367952fb44fca595f2b2f477156de389ce7c0ade3aef29e21"}, 203 | {file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69958735d5e01f7b38226a6c6e7187d72b7e4d42b6b496aca5860b611ca0c193"}, 204 | {file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a7dfbf9ed8d68fd39763940591db7637cf8817c5bce1a44f7b56c97cbe211e"}, 205 | {file = "numpy-1.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e957ca8112c689b728037cea9c9567c27cf912741fabda9efc2c7d33d29dfa1"}, 206 | {file = "numpy-1.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:800dfeaffb2219d49377da1371d710d7952c9533b57f3d51b15e61c4269a1b5b"}, 207 | {file = "numpy-1.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:65f5e257987601fdfc63f1d02fca4d1c44a2b85b802f03bd6abc2b0b14648dd2"}, 208 | {file = "numpy-1.22.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:632e062569b0fe05654b15ef0e91a53c0a95d08ffe698b66f6ba0f927ad267c2"}, 209 | {file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d245a2bf79188d3f361137608c3cd12ed79076badd743dc660750a9f3074f7c"}, 210 | {file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b4018a19d2ad9606ce9089f3d52206a41b23de5dfe8dc947d2ec49ce45d015"}, 211 | {file = "numpy-1.22.1-cp38-cp38-win32.whl", hash = "sha256:f8ad59e6e341f38266f1549c7c2ec70ea0e3d1effb62a44e5c3dba41c55f0187"}, 212 | {file = "numpy-1.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:60f19c61b589d44fbbab8ff126640ae712e163299c2dd422bfe4edc7ec51aa9b"}, 213 | {file = "numpy-1.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2db01d9838a497ba2aa9a87515aeaf458f42351d72d4e7f3b8ddbd1eba9479f2"}, 214 | {file = "numpy-1.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bcd19dab43b852b03868796f533b5f5561e6c0e3048415e675bec8d2e9d286c1"}, 215 | {file = "numpy-1.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78bfbdf809fc236490e7e65715bbd98377b122f329457fffde206299e163e7f3"}, 216 | {file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c51124df17f012c3b757380782ae46eee85213a3215e51477e559739f57d9bf6"}, 217 | {file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d54b7b516f0ca38a69590557814de2dd638d7d4ed04864826acaac5ebb8f01"}, 218 | {file = "numpy-1.22.1-cp39-cp39-win32.whl", hash = "sha256:b5ec9a5eaf391761c61fd873363ef3560a3614e9b4ead17347e4deda4358bca4"}, 219 | {file = "numpy-1.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:4ac4d7c9f8ea2a79d721ebfcce81705fc3cd61a10b731354f1049eb8c99521e8"}, 220 | {file = "numpy-1.22.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e60ef82c358ded965fdd3132b5738eade055f48067ac8a5a8ac75acc00cad31f"}, 221 | {file = "numpy-1.22.1.zip", hash = "sha256:e348ccf5bc5235fc405ab19d53bec215bb373300e5523c7b476cc0da8a5e9973"}, 222 | ] 223 | pandas = [ 224 | {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62d5b5ce965bae78f12c1c0df0d387899dd4211ec0bdc52822373f13a3a022b9"}, 225 | {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:adfeb11be2d54f275142c8ba9bf67acee771b7186a5745249c7d5a06c670136b"}, 226 | {file = "pandas-1.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a8c055d58873ad81cae290d974d13dd479b82cbb975c3e1fa2cf1920715296"}, 227 | {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd541ab09e1f80a2a1760032d665f6e032d8e44055d602d65eeea6e6e85498cb"}, 228 | {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2651d75b9a167cc8cc572cf787ab512d16e316ae00ba81874b560586fa1325e0"}, 229 | {file = "pandas-1.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:aaf183a615ad790801fa3cf2fa450e5b6d23a54684fe386f7e3208f8b9bfbef6"}, 230 | {file = "pandas-1.3.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:344295811e67f8200de2390093aeb3c8309f5648951b684d8db7eee7d1c81fb7"}, 231 | {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552020bf83b7f9033b57cbae65589c01e7ef1544416122da0c79140c93288f56"}, 232 | {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cce0c6bbeb266b0e39e35176ee615ce3585233092f685b6a82362523e59e5b4"}, 233 | {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d28a3c65463fd0d0ba8bbb7696b23073efee0510783340a44b08f5e96ffce0c"}, 234 | {file = "pandas-1.3.5-cp37-cp37m-win32.whl", hash = "sha256:a62949c626dd0ef7de11de34b44c6475db76995c2064e2d99c6498c3dba7fe58"}, 235 | {file = "pandas-1.3.5-cp37-cp37m-win_amd64.whl", hash = "sha256:8025750767e138320b15ca16d70d5cdc1886e8f9cc56652d89735c016cd8aea6"}, 236 | {file = "pandas-1.3.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fe95bae4e2d579812865db2212bb733144e34d0c6785c0685329e5b60fcb85dd"}, 237 | {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f261553a1e9c65b7a310302b9dbac31cf0049a51695c14ebe04e4bfd4a96f02"}, 238 | {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6dbec5f3e6d5dc80dcfee250e0a2a652b3f28663492f7dab9a24416a48ac39"}, 239 | {file = "pandas-1.3.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3bc49af96cd6285030a64779de5b3688633a07eb75c124b0747134a63f4c05f"}, 240 | {file = "pandas-1.3.5-cp38-cp38-win32.whl", hash = "sha256:b6b87b2fb39e6383ca28e2829cddef1d9fc9e27e55ad91ca9c435572cdba51bf"}, 241 | {file = "pandas-1.3.5-cp38-cp38-win_amd64.whl", hash = "sha256:a395692046fd8ce1edb4c6295c35184ae0c2bbe787ecbe384251da609e27edcb"}, 242 | {file = "pandas-1.3.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd971a3f08b745a75a86c00b97f3007c2ea175951286cdda6abe543e687e5f2f"}, 243 | {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37f06b59e5bc05711a518aa10beaec10942188dccb48918bb5ae602ccbc9f1a0"}, 244 | {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c21778a688d3712d35710501f8001cdbf96eb70a7c587a3d5613573299fdca6"}, 245 | {file = "pandas-1.3.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3345343206546545bc26a05b4602b6a24385b5ec7c75cb6059599e3d56831da2"}, 246 | {file = "pandas-1.3.5-cp39-cp39-win32.whl", hash = "sha256:c69406a2808ba6cf580c2255bcf260b3f214d2664a3a4197d0e640f573b46fd3"}, 247 | {file = "pandas-1.3.5-cp39-cp39-win_amd64.whl", hash = "sha256:32e1a26d5ade11b547721a72f9bfc4bd113396947606e00d5b4a5b79b3dcb006"}, 248 | {file = "pandas-1.3.5.tar.gz", hash = "sha256:1e4285f5de1012de20ca46b188ccf33521bff61ba5c5ebd78b4fb28e5416a9f1"}, 249 | ] 250 | python-dateutil = [ 251 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 252 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 253 | ] 254 | pytz = [ 255 | {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, 256 | {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, 257 | ] 258 | six = [ 259 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 260 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 261 | ] 262 | werkzeug = [ 263 | {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, 264 | {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, 265 | ] 266 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "grafana-pandas-datasource" 3 | version = "0.3.0" 4 | description = "Serve NumPy data via pandas data frames to Grafana" 5 | authors = [ 6 | "Andreas Motl ", 7 | "Linar " 8 | ] 9 | license = "AGPL-3.0" 10 | readme = "README.rst" 11 | include = [ 12 | "LICENSE", 13 | "CHANGES.rst" 14 | ] 15 | homepage = "https://community.panodata.org/t/grafana-python-datasource-using-pandas-for-timeseries-and-table-data/148" 16 | repository = "https://github.com/panodata/grafana-pandas-datasource" 17 | 18 | keywords = [ 19 | "grafana", 20 | "pandas", 21 | "datasource", 22 | "grafana-plugin", 23 | "grafana-datasource", 24 | "pandas-dataframe" 25 | ] 26 | classifiers = [ 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "License :: OSI Approved :: GNU Affero General Public License v3", 34 | "Development Status :: 4 - Beta", 35 | "Environment :: Console", 36 | "Intended Audience :: Developers", 37 | "Intended Audience :: Education", 38 | "Intended Audience :: Information Technology", 39 | "Intended Audience :: Science/Research", 40 | "Intended Audience :: System Administrators", 41 | "Topic :: Communications", 42 | "Topic :: Database", 43 | "Topic :: Internet", 44 | "Topic :: Internet :: WWW/HTTP :: Indexing/Search", 45 | "Topic :: Scientific/Engineering :: Atmospheric Science", 46 | "Topic :: Scientific/Engineering :: GIS", 47 | "Topic :: Scientific/Engineering :: Human Machine Interfaces", 48 | "Topic :: Scientific/Engineering :: Information Analysis", 49 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", 50 | "Topic :: Scientific/Engineering :: Visualization", 51 | "Topic :: Software Development :: Libraries", 52 | "Topic :: System :: Archiving", 53 | "Topic :: Text Processing", 54 | "Topic :: Utilities", 55 | "Operating System :: POSIX", 56 | "Operating System :: Unix", 57 | "Operating System :: MacOS" 58 | ] 59 | 60 | [tool.poetry.urls] 61 | "Issues" = "https://github.com/panodata/grafana-pandas-datasource/issues" 62 | 63 | [tool.poetry.dependencies] 64 | python = "^3.7.1" 65 | pandas = "^1.3" 66 | flask = "^1.1.4" 67 | flask-cors = "^3.0.10" 68 | 69 | [tool.black] 70 | line-length = 120 71 | 72 | [tool.isort] 73 | profile = "black" 74 | multi_line_output = 3 75 | 76 | [build-system] 77 | requires = ["poetry_core>=1.0.0"] 78 | build-backend = "poetry.core.masonry.api" 79 | --------------------------------------------------------------------------------