Skip to content

HaystackClient

open_haystack_client

open_haystack_client(uri, username, password, ssl_context=None)

Context manager for opening and closing a session with a Project Haystack defined server application. May help prevent accidentially leaving a session with the server open.

open_haystack_client can be directly imported as follows:

from phable import open_haystack_client

Example:

from phable import open_haystack_client

uri = "http://localhost:8080/api/demo"
with open_haystack_client(uri, "su", "password") as client:
    print(client.about())

Note: This context manager uses Project Haystack's close op, which was later introduced. Therefore the context manager may not work with some servers.

Parameters:

Name Type Description Default
uri str

URI of endpoint such as "http://host/api/myProj/".

required
username str

Username for the API user.

required
password str

Password for the API user.

required
ssl_context ssl.SSLContext | None

Optional SSL context. If not provided, a SSL context with default settings is created and used.

None
Source code in phable/haystack_client.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@contextmanager
def open_haystack_client(
    uri: str, username: str, password: str, ssl_context: SSLContext | None = None
) -> Generator[HaystackClient, None, None]:
    """Context manager for opening and closing a session with a Project Haystack
    defined server application. May help prevent accidentially leaving a session with
    the server open.

    `open_haystack_client` can be directly imported as follows:

    ```python
    from phable import open_haystack_client
    ```

    **Example:**

    ```python
    from phable import open_haystack_client

    uri = "http://localhost:8080/api/demo"
    with open_haystack_client(uri, "su", "password") as client:
        print(client.about())
    ```

    **Note:** This context manager uses Project Haystack's
    [close op](https://project-haystack.org/doc/docHaystack/Ops#close), which was
    later introduced. Therefore the context manager may not work with some servers.

    Parameters:
        uri: URI of endpoint such as "http://host/api/myProj/".
        username: Username for the API user.
        password: Password for the API user.
        ssl_context:
            Optional SSL context. If not provided, a SSL context with default
            settings is created and used.
    """

    client = HaystackClient.open(uri, username, password, ssl_context=ssl_context)
    yield client
    client.close()

HaystackClient

A client interface to a Project Haystack defined server application used for authentication and operations.

HaystackClient can be directly imported as follows:

from phable import HaystackClient
Source code in phable/haystack_client.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
class HaystackClient(metaclass=NoPublicConstructor):
    """A client interface to a Project Haystack defined server application used for
    authentication and operations.

    `HaystackClient` can be directly imported as follows:

    ```python
    from phable import HaystackClient
    ```
    """

    # Note: this is intended to be an undocumented private initializer enforced by the
    #       NoPublicConstructor metaclass
    def __init__(
        self,
        uri: str,
        auth_token: str,
        ssl_context: SSLContext | None = None,
    ):
        self.uri: str = uri
        self._auth_token: str = auth_token
        self._context: SSLContext | None = ssl_context

    @classmethod
    def open(
        cls,
        uri: str,
        username: str,
        password: str,
        *,
        ssl_context: SSLContext | None = None,
    ) -> Self:
        """Opens a session with the server for the URI of the project.

        Raises:
            AuthError:
                Unable to authenticate with the server using the credentials provided.

        Parameters:
            uri: URI of endpoint such as "http://host/api/myProj/".
            username: Username for the API user.
            password: Password for the API user.
            ssl_context:
                Optional SSL context. If not provided, a SSL context with default
                settings is created and used.

        Returns:
            An instance of the class this method is used on (i.e., Client or HxClient).
        """

        try:
            scram = ScramScheme(uri, username, password, ssl_context)
            auth_token = scram.get_auth_token()
        except IncorrectHttpResponseStatus as err:
            if err.actual_status == 403:
                raise AuthError(
                    "Unable to authenticate with the server using the credentials "
                    + "provided."
                )

        return cls._create(uri, auth_token, ssl_context)

    def about(self) -> dict[str, Any]:
        """Query basic information about the server.

        Returns:
            A `dict` containing information about the server.
        """
        return self.call("about").rows[0]

    def close(self) -> Grid:
        """Close the connection to the server.

        **Note:** Project Haystack recently defined the Close operation. Some servers
        may not support this operation.

        Returns:
            An empty `Grid`.
        """

        return self.call("close")

    def read(self, filter: str, checked: bool = True) -> dict[Any, Any]:
        """Read from the database the first record which matches the
        [filter](https://project-haystack.org/doc/docHaystack/Filters).

        Raises:
            UnknownRecError: Server's response does not include requested rec.

        Parameters:
            filter:
                Project Haystack defined
                [filter](https://project-haystack.org/doc/docHaystack/Filters) for
                querying the server.
            checked:
                If `checked` is equal to false and the record cannot be found, an empty
                `dict` is returned. If `checked` is equal to true and the record cannot
                be found, an `UnknownRecError` is raised.

        Returns:
            An empty `dict` or a `dict` that describes the entity read.
        """
        response = self.read_all(filter, 1)

        if len(response.rows) == 0:
            if checked is True:
                raise UnknownRecError(
                    "Unable to locate an entity on the server that matches the filter."
                )
            else:
                response = {}
        else:
            response = response.rows[0]

        return response

    def read_all(self, filter: str, limit: int | None = None) -> Grid:
        """Read all records from the database which match the
        [filter](https://project-haystack.org/doc/docHaystack/Filters).

        Parameters:
            filter:
                Project Haystack defined
                [filter](https://project-haystack.org/doc/docHaystack/Filters) for
                querying the server.
            limit: Maximum number of entities to return in response.

        Returns:
            An empty `Grid` or a `Grid` that has a row for each entity read.
        """
        data_row = {"filter": filter}

        if limit is not None:
            data_row["limit"] = limit

        response = self.call("read", Grid.to_grid(data_row))

        return response

    def read_by_id(self, id: Ref, checked: bool = True) -> dict[Any, Any]:
        """Read an entity record using its unique identifier.

        Raises:
            UnknownRecError: Server's response does not include requested rec.

        Parameters:
            id: Unique identifier for the record being read.
            checked:
                If `checked` is equal to false and the record cannot be found, an empty
                `dict` is returned. If `checked` is equal to true and the record cannot
                be found, an `UnknownRecError` is raised.

        Returns:
            An empty `dict` or a `dict` that describes the entity read.
        """

        data_rows = [{"id": {"_kind": "ref", "val": id.val}}]
        post_data = Grid.to_grid(data_rows)
        response = self.call("read", post_data)

        if len(response.rows) == 0:
            if checked is True:
                raise UnknownRecError("Unable to locate the id on the server.")
            else:
                response = {}
        else:
            response = response.rows[0]

        return response

    def read_by_ids(self, ids: list[Ref]) -> Grid:
        """Read a set of entity records using their unique identifiers.

        **Note:** Project Haystack recently introduced batch read support, which might
        not be supported by some servers. If your server does not support the batch
        read feature, then try using the `Client.read_by_id()` method instead.

        Raises:
            UnknownRecError: Server's response does not include requested recs.

        Parameters:
            ids: Unique identifiers for the records being read.

        Returns:
            `Grid` with a row for each entity read.
        """
        ids = ids.copy()
        data_rows = [{"id": {"_kind": "ref", "val": id.val}} for id in ids]
        post_data = Grid.to_grid(data_rows)
        response = self.call("read", post_data)

        if len(response.rows) == 0:
            raise UnknownRecError("Unable to locate any of the ids on the server.")
        for row in response.rows:
            if len(row) == 0:
                raise UnknownRecError("Unable to locate one or more ids on the server.")

        return response

    def his_read_by_id(
        self,
        id: Ref,
        range: date | DateRange | DateTimeRange,
    ) -> Grid:
        """Read history data associated with `id` for the given `range`.

        When there is an existing `Grid` describing point records, it is worth
        considering to use the `Client.his_read()` method to store available
        metadata within the returned `Grid`.

        **Example:**

        ```python
        from datetime import date, timedelta

        from phable import DateRange, Ref, open_haystack_client

        # define these settings specific to your use case
        uri = "http://localhost:8080/api/demo"
        username = "<username>"
        password = "<password>"

        # update the id for your server
        id1 = Ref("2e749080-d2645928")

        end = date.today()
        start = end - timedelta(days=2)
        range = DateRange(start, end)

        with open_haystack_client(uri, username, password) as client:
            his_grid = client.his_read_by_id(id1, range)

        his_df_meta, his_df = his_grid.to_polars_all()
        ```

        Parameters:
            id:
                Unique identifier for the point record associated with the requested
                history data.
            range:
                Ranges are inclusive of start timestamp and exclusive of end timestamp.
                If a date is provided without a defined end, then the server should
                infer the range to be from midnight of the defined date to midnight of
                the day after the defined date.

        Returns:
            `Grid` with history data associated with the `id` for the given `range`.
        """
        data = _create_his_read_req_data(id, range)
        response = self.call("hisRead", data)

        return response

    def his_read_by_ids(
        self,
        ids: list[Ref],
        range: date | DateRange | DateTimeRange,
    ) -> Grid:
        """Read history data associated with `ids` for the given `range`.

        When there is an existing `Grid` describing point records, it is worth
        considering to use the `Client.his_read()` method to store available
        metadata within the returned `Grid`.

        **Note:** Project Haystack recently defined batch history read support.  Some
        Project Haystack servers may not support reading history data for more than one
        point record at a time.

        **Example:**

        ```python
        from datetime import date, timedelta

        from phable import DateRange, Ref, open_haystack_client

        # define these settings specific to your use case
        uri = "http://localhost:8080/api/demo"
        username = "<username>"
        password = "<password>"

        # update the ids for your server
        id1 = Ref("2e749080-d2645928")
        id2 = Ref("2e749080-4719193b")

        end = date.today()
        start = end - timedelta(days=2)
        range = DateRange(start, end)

        with open_haystack_client(uri, username, password) as client:
            his_grid = client.his_read_by_ids([id1, id2], range)

        his_df_meta, his_df = his_grid.to_polars_all()
        ```

        Parameters:
            ids:
                Unique identifiers for the point records associated with the requested
                history data.
            range:
                Ranges are inclusive of start timestamp and exclusive of end timestamp.
                If a date is provided without a defined end, then the server should
                infer the range to be from midnight of the defined date to midnight of
                the day after the defined date.

        Returns:
            `Grid` with history data associated with the `ids` for the given `range`.
        """
        data = _create_his_read_req_data(ids, range)
        response = self.call("hisRead", data)

        return response

    def his_write_by_id(
        self,
        id: Ref,
        his_rows: list[dict[str, Any]],
    ) -> Grid:
        """Write history data to point records on the server.

        History row key values must be valid data types defined for `Phable`.

        History row key names must be `ts` or `val`.  Values in the column named `val`
        are for the `Ref` described by the `id` parameter.

        **Additional requirements**

        1. Timestamp and value kind of `his_row` data must match the entity's (Ref)
        configured timezone and kind
        2. Numeric data must match the entity's (Ref) configured unit or status of
        being unitless

        **Recommendations for enhanced performance**

        1. Avoid posting out-of-order or duplicate data

        **Example:**

        ```python
        from datetime import datetime, timedelta
        from zoneinfo import ZoneInfo

        from phable import Number, Ref, open_haystack_client

        # define these settings specific to your use case
        uri = "http://localhost:8080/api/demo"
        username = "<username>"
        password = "<password>"

        # make sure datetime objects are timezone aware
        ts_now = datetime.now(ZoneInfo("America/New_York"))

        his_rows = [
            {
                "ts": ts_now - timedelta(seconds=30),
                "val": Number(1_000.0, "kW"),
            },
            {
                "ts": ts_now,
                "val": Number(2_000.0, "kW"),
            },
        ]

        # update the id for your server
        id1 = Ref("2e749080-3cad46c0")

        with open_haystack_client(uri, username, password) as client:
            client.his_write_by_id(id1, data_rows)
        ```

        Parameters:
            id: Unique identifier for the point record.
            his_rows: History data to be written for the `id`.

        Returns:
            An empty `Grid`.
        """
        meta = {"id": id}
        his_grid = Grid.to_grid(his_rows, meta)
        return self.call("hisWrite", his_grid)

    def his_write_by_ids(
        self,
        ids: list[Ref],
        his_rows: list[dict[str, Any]],
    ) -> Grid:
        """Write history data to point records on the server.

        History row key values must be valid data types defined for `Phable`.

        History row key names must be `ts` or `vX` where `X` is an integer equal
        to or greater than zero.  Also, `X` must not exceed the highest index of `ids`.

        The index of an id in `ids` corresponds to the column name used in `his_rows`.

        **Additional requirements**

        1. Timestamp and value kind of `his_row` data must match the entity's (Ref)
        configured timezone and kind
        2. Numeric data must match the entity's (Ref) configured unit or status of
        being unitless

        **Recommendations for enhanced performance**

        1. Avoid posting out-of-order or duplicate data

        **Batch history write support**

        Project Haystack recently defined batch history write support.  Some Project
        Haystack servers may not support writing history data to more than one point
        at a time.  For these instances it is recommended to use a `Ref` type for the
        `ids` parameter.

        **Example:**

        ```python
        from datetime import datetime, timedelta
        from zoneinfo import ZoneInfo

        from phable import Number, Ref, open_haystack_client

        # define these settings specific to your use case
        uri = "http://localhost:8080/api/demo"
        username = "<username>"
        password = "<password>"

        # make sure datetime objects are timezone aware
        ts_now = datetime.now(ZoneInfo("America/New_York"))

        his_rows = [
            {"ts": ts_now - timedelta(seconds=30), "v0": Number(1, "kW")},
            {"ts": ts_now, "v0": Number(50, "kW"), "v1": Number(20, "kW")},
        ]

        # update the ids for your server
        id1 = Ref("2e749080-3cad46c0")
        id2 = Ref("2e749080-520f621b")

        with open_haystack_client(uri, username, password) as client:
            client.his_write_by_ids([id1, id2], his_rows)
        ```

        Parameters:
            ids: Unique identifiers for the point records.
            his_rows: History data to be written for the `ids`.

        Returns:
            An empty `Grid`.
        """
        meta = {"ver": "3.0"}
        cols = [{"name": "ts"}]

        for count, id in enumerate(ids):
            cols.append({"name": f"v{count}", "meta": {"id": id}})

        his_grid = Grid(meta, cols, his_rows)

        return self.call("hisWrite", his_grid)

    def point_write(
        self,
        id: Ref,
        level: int,
        val: Number | bool | str | None = None,
        who: str | None = None,
        duration: Number | None = None,
    ) -> Grid:
        """Writes to a given level of a writable point's priority array.

        Parameters:
            id: Unique identifier of the writable point.
            level: Integer from 1 - 17 (17 is default).
            val: Current value at level or null.
            who:
                Optional username/application name performing the write. If not
                provided, the authenticated user display name is used.
            duration: Optional number with duration unit if setting level 8.

        Returns:
            `Grid` with the server's response.
        """
        row = {"id": id, "level": level}

        if val is not None:
            row["val"] = val
        if who is not None:
            row["who"] = who
        if duration is not None:
            row["duration"] = duration

        return self.call("pointWrite", Grid.to_grid(row))

    def point_write_array(self, id: Ref) -> Grid:
        """Reads the current status of a writable point's priority array.

        Parameters:
            id: Unique identifier for the record.

        Returns:
            `Grid` with the server's response.
        """
        return self.call("pointWrite", Grid.to_grid({"id": id}))

    def call(
        self,
        path: str,
        data: Grid = Grid(meta={"ver": "3.0"}, cols=[{"name": "empty"}], rows=[]),
    ) -> Grid:
        """Sends a POST request to `{uri}/{path}` using provided `data`.

        This operation is not defined by Project Haystack. However, other `Client`
        methods use this method internally.

        Parameters:
            path:
                Location on endpoint such that the complete path of the request is
                `{uri}/{path}`

                **Note:** The `uri` stored in the `Client` instance and the value
                provided as the `path` parameter of this method are used.
            data:
                Data passed in the POST request.

        Raises:
            CallError:
                Error raised by `Client` when server's `Grid` response meta has an
                `err` marker tag described
                [here](https://project-haystack.org/doc/docHaystack/HttpApi#errorGrid).

        Returns:
            HTTP response.
        """
        headers = {
            "Authorization": f"BEARER authToken={self._auth_token}",
            "Accept": "application/json",
        }

        response = post(
            url=f"{self.uri}/{path}",
            post_data=data,
            headers=headers,
            context=self._context,
        )
        _validate_response_meta(response)

        return response

about

about()

Query basic information about the server.

Returns:

Type Description
dict[str, typing.Any]

A dict containing information about the server.

Source code in phable/haystack_client.py
206
207
208
209
210
211
212
def about(self) -> dict[str, Any]:
    """Query basic information about the server.

    Returns:
        A `dict` containing information about the server.
    """
    return self.call("about").rows[0]

call

call(path, data=Grid(meta={'ver': '3.0'}, cols=[{'name': 'empty'}], rows=[]))

Sends a POST request to {uri}/{path} using provided data.

This operation is not defined by Project Haystack. However, other Client methods use this method internally.

Parameters:

Name Type Description Default
path str

Location on endpoint such that the complete path of the request is {uri}/{path}

Note: The uri stored in the Client instance and the value provided as the path parameter of this method are used.

required
data phable.kinds.Grid

Data passed in the POST request.

phable.kinds.Grid(meta={'ver': '3.0'}, cols=[{'name': 'empty'}], rows=[])

Raises:

Type Description
phable.haystack_client.CallError

Error raised by Client when server's Grid response meta has an err marker tag described here.

Returns:

Type Description
phable.kinds.Grid

HTTP response.

Source code in phable/haystack_client.py
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
def call(
    self,
    path: str,
    data: Grid = Grid(meta={"ver": "3.0"}, cols=[{"name": "empty"}], rows=[]),
) -> Grid:
    """Sends a POST request to `{uri}/{path}` using provided `data`.

    This operation is not defined by Project Haystack. However, other `Client`
    methods use this method internally.

    Parameters:
        path:
            Location on endpoint such that the complete path of the request is
            `{uri}/{path}`

            **Note:** The `uri` stored in the `Client` instance and the value
            provided as the `path` parameter of this method are used.
        data:
            Data passed in the POST request.

    Raises:
        CallError:
            Error raised by `Client` when server's `Grid` response meta has an
            `err` marker tag described
            [here](https://project-haystack.org/doc/docHaystack/HttpApi#errorGrid).

    Returns:
        HTTP response.
    """
    headers = {
        "Authorization": f"BEARER authToken={self._auth_token}",
        "Accept": "application/json",
    }

    response = post(
        url=f"{self.uri}/{path}",
        post_data=data,
        headers=headers,
        context=self._context,
    )
    _validate_response_meta(response)

    return response

close

close()

Close the connection to the server.

Note: Project Haystack recently defined the Close operation. Some servers may not support this operation.

Returns:

Type Description
phable.kinds.Grid

An empty Grid.

Source code in phable/haystack_client.py
214
215
216
217
218
219
220
221
222
223
224
def close(self) -> Grid:
    """Close the connection to the server.

    **Note:** Project Haystack recently defined the Close operation. Some servers
    may not support this operation.

    Returns:
        An empty `Grid`.
    """

    return self.call("close")

his_read_by_id

his_read_by_id(id, range)

Read history data associated with id for the given range.

When there is an existing Grid describing point records, it is worth considering to use the Client.his_read() method to store available metadata within the returned Grid.

Example:

from datetime import date, timedelta

from phable import DateRange, Ref, open_haystack_client

# define these settings specific to your use case
uri = "http://localhost:8080/api/demo"
username = "<username>"
password = "<password>"

# update the id for your server
id1 = Ref("2e749080-d2645928")

end = date.today()
start = end - timedelta(days=2)
range = DateRange(start, end)

with open_haystack_client(uri, username, password) as client:
    his_grid = client.his_read_by_id(id1, range)

his_df_meta, his_df = his_grid.to_polars_all()

Parameters:

Name Type Description Default
id phable.kinds.Ref

Unique identifier for the point record associated with the requested history data.

required
range datetime.date | phable.kinds.DateRange | phable.kinds.DateTimeRange

Ranges are inclusive of start timestamp and exclusive of end timestamp. If a date is provided without a defined end, then the server should infer the range to be from midnight of the defined date to midnight of the day after the defined date.

required

Returns:

Type Description
phable.kinds.Grid

Grid with history data associated with the id for the given range.

Source code in phable/haystack_client.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
def his_read_by_id(
    self,
    id: Ref,
    range: date | DateRange | DateTimeRange,
) -> Grid:
    """Read history data associated with `id` for the given `range`.

    When there is an existing `Grid` describing point records, it is worth
    considering to use the `Client.his_read()` method to store available
    metadata within the returned `Grid`.

    **Example:**

    ```python
    from datetime import date, timedelta

    from phable import DateRange, Ref, open_haystack_client

    # define these settings specific to your use case
    uri = "http://localhost:8080/api/demo"
    username = "<username>"
    password = "<password>"

    # update the id for your server
    id1 = Ref("2e749080-d2645928")

    end = date.today()
    start = end - timedelta(days=2)
    range = DateRange(start, end)

    with open_haystack_client(uri, username, password) as client:
        his_grid = client.his_read_by_id(id1, range)

    his_df_meta, his_df = his_grid.to_polars_all()
    ```

    Parameters:
        id:
            Unique identifier for the point record associated with the requested
            history data.
        range:
            Ranges are inclusive of start timestamp and exclusive of end timestamp.
            If a date is provided without a defined end, then the server should
            infer the range to be from midnight of the defined date to midnight of
            the day after the defined date.

    Returns:
        `Grid` with history data associated with the `id` for the given `range`.
    """
    data = _create_his_read_req_data(id, range)
    response = self.call("hisRead", data)

    return response

his_read_by_ids

his_read_by_ids(ids, range)

Read history data associated with ids for the given range.

When there is an existing Grid describing point records, it is worth considering to use the Client.his_read() method to store available metadata within the returned Grid.

Note: Project Haystack recently defined batch history read support. Some Project Haystack servers may not support reading history data for more than one point record at a time.

Example:

from datetime import date, timedelta

from phable import DateRange, Ref, open_haystack_client

# define these settings specific to your use case
uri = "http://localhost:8080/api/demo"
username = "<username>"
password = "<password>"

# update the ids for your server
id1 = Ref("2e749080-d2645928")
id2 = Ref("2e749080-4719193b")

end = date.today()
start = end - timedelta(days=2)
range = DateRange(start, end)

with open_haystack_client(uri, username, password) as client:
    his_grid = client.his_read_by_ids([id1, id2], range)

his_df_meta, his_df = his_grid.to_polars_all()

Parameters:

Name Type Description Default
ids list[phable.kinds.Ref]

Unique identifiers for the point records associated with the requested history data.

required
range datetime.date | phable.kinds.DateRange | phable.kinds.DateTimeRange

Ranges are inclusive of start timestamp and exclusive of end timestamp. If a date is provided without a defined end, then the server should infer the range to be from midnight of the defined date to midnight of the day after the defined date.

required

Returns:

Type Description
phable.kinds.Grid

Grid with history data associated with the ids for the given range.

Source code in phable/haystack_client.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def his_read_by_ids(
    self,
    ids: list[Ref],
    range: date | DateRange | DateTimeRange,
) -> Grid:
    """Read history data associated with `ids` for the given `range`.

    When there is an existing `Grid` describing point records, it is worth
    considering to use the `Client.his_read()` method to store available
    metadata within the returned `Grid`.

    **Note:** Project Haystack recently defined batch history read support.  Some
    Project Haystack servers may not support reading history data for more than one
    point record at a time.

    **Example:**

    ```python
    from datetime import date, timedelta

    from phable import DateRange, Ref, open_haystack_client

    # define these settings specific to your use case
    uri = "http://localhost:8080/api/demo"
    username = "<username>"
    password = "<password>"

    # update the ids for your server
    id1 = Ref("2e749080-d2645928")
    id2 = Ref("2e749080-4719193b")

    end = date.today()
    start = end - timedelta(days=2)
    range = DateRange(start, end)

    with open_haystack_client(uri, username, password) as client:
        his_grid = client.his_read_by_ids([id1, id2], range)

    his_df_meta, his_df = his_grid.to_polars_all()
    ```

    Parameters:
        ids:
            Unique identifiers for the point records associated with the requested
            history data.
        range:
            Ranges are inclusive of start timestamp and exclusive of end timestamp.
            If a date is provided without a defined end, then the server should
            infer the range to be from midnight of the defined date to midnight of
            the day after the defined date.

    Returns:
        `Grid` with history data associated with the `ids` for the given `range`.
    """
    data = _create_his_read_req_data(ids, range)
    response = self.call("hisRead", data)

    return response

his_write_by_id

his_write_by_id(id, his_rows)

Write history data to point records on the server.

History row key values must be valid data types defined for Phable.

History row key names must be ts or val. Values in the column named val are for the Ref described by the id parameter.

Additional requirements

  1. Timestamp and value kind of his_row data must match the entity's (Ref) configured timezone and kind
  2. Numeric data must match the entity's (Ref) configured unit or status of being unitless

Recommendations for enhanced performance

  1. Avoid posting out-of-order or duplicate data

Example:

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

from phable import Number, Ref, open_haystack_client

# define these settings specific to your use case
uri = "http://localhost:8080/api/demo"
username = "<username>"
password = "<password>"

# make sure datetime objects are timezone aware
ts_now = datetime.now(ZoneInfo("America/New_York"))

his_rows = [
    {
        "ts": ts_now - timedelta(seconds=30),
        "val": Number(1_000.0, "kW"),
    },
    {
        "ts": ts_now,
        "val": Number(2_000.0, "kW"),
    },
]

# update the id for your server
id1 = Ref("2e749080-3cad46c0")

with open_haystack_client(uri, username, password) as client:
    client.his_write_by_id(id1, data_rows)

Parameters:

Name Type Description Default
id phable.kinds.Ref

Unique identifier for the point record.

required
his_rows list[dict[str, typing.Any]]

History data to be written for the id.

required

Returns:

Type Description
phable.kinds.Grid

An empty Grid.

Source code in phable/haystack_client.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def his_write_by_id(
    self,
    id: Ref,
    his_rows: list[dict[str, Any]],
) -> Grid:
    """Write history data to point records on the server.

    History row key values must be valid data types defined for `Phable`.

    History row key names must be `ts` or `val`.  Values in the column named `val`
    are for the `Ref` described by the `id` parameter.

    **Additional requirements**

    1. Timestamp and value kind of `his_row` data must match the entity's (Ref)
    configured timezone and kind
    2. Numeric data must match the entity's (Ref) configured unit or status of
    being unitless

    **Recommendations for enhanced performance**

    1. Avoid posting out-of-order or duplicate data

    **Example:**

    ```python
    from datetime import datetime, timedelta
    from zoneinfo import ZoneInfo

    from phable import Number, Ref, open_haystack_client

    # define these settings specific to your use case
    uri = "http://localhost:8080/api/demo"
    username = "<username>"
    password = "<password>"

    # make sure datetime objects are timezone aware
    ts_now = datetime.now(ZoneInfo("America/New_York"))

    his_rows = [
        {
            "ts": ts_now - timedelta(seconds=30),
            "val": Number(1_000.0, "kW"),
        },
        {
            "ts": ts_now,
            "val": Number(2_000.0, "kW"),
        },
    ]

    # update the id for your server
    id1 = Ref("2e749080-3cad46c0")

    with open_haystack_client(uri, username, password) as client:
        client.his_write_by_id(id1, data_rows)
    ```

    Parameters:
        id: Unique identifier for the point record.
        his_rows: History data to be written for the `id`.

    Returns:
        An empty `Grid`.
    """
    meta = {"id": id}
    his_grid = Grid.to_grid(his_rows, meta)
    return self.call("hisWrite", his_grid)

his_write_by_ids

his_write_by_ids(ids, his_rows)

Write history data to point records on the server.

History row key values must be valid data types defined for Phable.

History row key names must be ts or vX where X is an integer equal to or greater than zero. Also, X must not exceed the highest index of ids.

The index of an id in ids corresponds to the column name used in his_rows.

Additional requirements

  1. Timestamp and value kind of his_row data must match the entity's (Ref) configured timezone and kind
  2. Numeric data must match the entity's (Ref) configured unit or status of being unitless

Recommendations for enhanced performance

  1. Avoid posting out-of-order or duplicate data

Batch history write support

Project Haystack recently defined batch history write support. Some Project Haystack servers may not support writing history data to more than one point at a time. For these instances it is recommended to use a Ref type for the ids parameter.

Example:

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

from phable import Number, Ref, open_haystack_client

# define these settings specific to your use case
uri = "http://localhost:8080/api/demo"
username = "<username>"
password = "<password>"

# make sure datetime objects are timezone aware
ts_now = datetime.now(ZoneInfo("America/New_York"))

his_rows = [
    {"ts": ts_now - timedelta(seconds=30), "v0": Number(1, "kW")},
    {"ts": ts_now, "v0": Number(50, "kW"), "v1": Number(20, "kW")},
]

# update the ids for your server
id1 = Ref("2e749080-3cad46c0")
id2 = Ref("2e749080-520f621b")

with open_haystack_client(uri, username, password) as client:
    client.his_write_by_ids([id1, id2], his_rows)

Parameters:

Name Type Description Default
ids list[phable.kinds.Ref]

Unique identifiers for the point records.

required
his_rows list[dict[str, typing.Any]]

History data to be written for the ids.

required

Returns:

Type Description
phable.kinds.Grid

An empty Grid.

Source code in phable/haystack_client.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
def his_write_by_ids(
    self,
    ids: list[Ref],
    his_rows: list[dict[str, Any]],
) -> Grid:
    """Write history data to point records on the server.

    History row key values must be valid data types defined for `Phable`.

    History row key names must be `ts` or `vX` where `X` is an integer equal
    to or greater than zero.  Also, `X` must not exceed the highest index of `ids`.

    The index of an id in `ids` corresponds to the column name used in `his_rows`.

    **Additional requirements**

    1. Timestamp and value kind of `his_row` data must match the entity's (Ref)
    configured timezone and kind
    2. Numeric data must match the entity's (Ref) configured unit or status of
    being unitless

    **Recommendations for enhanced performance**

    1. Avoid posting out-of-order or duplicate data

    **Batch history write support**

    Project Haystack recently defined batch history write support.  Some Project
    Haystack servers may not support writing history data to more than one point
    at a time.  For these instances it is recommended to use a `Ref` type for the
    `ids` parameter.

    **Example:**

    ```python
    from datetime import datetime, timedelta
    from zoneinfo import ZoneInfo

    from phable import Number, Ref, open_haystack_client

    # define these settings specific to your use case
    uri = "http://localhost:8080/api/demo"
    username = "<username>"
    password = "<password>"

    # make sure datetime objects are timezone aware
    ts_now = datetime.now(ZoneInfo("America/New_York"))

    his_rows = [
        {"ts": ts_now - timedelta(seconds=30), "v0": Number(1, "kW")},
        {"ts": ts_now, "v0": Number(50, "kW"), "v1": Number(20, "kW")},
    ]

    # update the ids for your server
    id1 = Ref("2e749080-3cad46c0")
    id2 = Ref("2e749080-520f621b")

    with open_haystack_client(uri, username, password) as client:
        client.his_write_by_ids([id1, id2], his_rows)
    ```

    Parameters:
        ids: Unique identifiers for the point records.
        his_rows: History data to be written for the `ids`.

    Returns:
        An empty `Grid`.
    """
    meta = {"ver": "3.0"}
    cols = [{"name": "ts"}]

    for count, id in enumerate(ids):
        cols.append({"name": f"v{count}", "meta": {"id": id}})

    his_grid = Grid(meta, cols, his_rows)

    return self.call("hisWrite", his_grid)

open classmethod

open(uri, username, password, *, ssl_context=None)

Opens a session with the server for the URI of the project.

Raises:

Type Description
phable.haystack_client.AuthError

Unable to authenticate with the server using the credentials provided.

Parameters:

Name Type Description Default
uri str

URI of endpoint such as "http://host/api/myProj/".

required
username str

Username for the API user.

required
password str

Password for the API user.

required
ssl_context ssl.SSLContext | None

Optional SSL context. If not provided, a SSL context with default settings is created and used.

None

Returns:

Type Description
typing.Self

An instance of the class this method is used on (i.e., Client or HxClient).

Source code in phable/haystack_client.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@classmethod
def open(
    cls,
    uri: str,
    username: str,
    password: str,
    *,
    ssl_context: SSLContext | None = None,
) -> Self:
    """Opens a session with the server for the URI of the project.

    Raises:
        AuthError:
            Unable to authenticate with the server using the credentials provided.

    Parameters:
        uri: URI of endpoint such as "http://host/api/myProj/".
        username: Username for the API user.
        password: Password for the API user.
        ssl_context:
            Optional SSL context. If not provided, a SSL context with default
            settings is created and used.

    Returns:
        An instance of the class this method is used on (i.e., Client or HxClient).
    """

    try:
        scram = ScramScheme(uri, username, password, ssl_context)
        auth_token = scram.get_auth_token()
    except IncorrectHttpResponseStatus as err:
        if err.actual_status == 403:
            raise AuthError(
                "Unable to authenticate with the server using the credentials "
                + "provided."
            )

    return cls._create(uri, auth_token, ssl_context)

point_write

point_write(id, level, val=None, who=None, duration=None)

Writes to a given level of a writable point's priority array.

Parameters:

Name Type Description Default
id phable.kinds.Ref

Unique identifier of the writable point.

required
level int

Integer from 1 - 17 (17 is default).

required
val phable.kinds.Number | bool | str | None

Current value at level or null.

None
who str | None

Optional username/application name performing the write. If not provided, the authenticated user display name is used.

None
duration phable.kinds.Number | None

Optional number with duration unit if setting level 8.

None

Returns:

Type Description
phable.kinds.Grid

Grid with the server's response.

Source code in phable/haystack_client.py
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def point_write(
    self,
    id: Ref,
    level: int,
    val: Number | bool | str | None = None,
    who: str | None = None,
    duration: Number | None = None,
) -> Grid:
    """Writes to a given level of a writable point's priority array.

    Parameters:
        id: Unique identifier of the writable point.
        level: Integer from 1 - 17 (17 is default).
        val: Current value at level or null.
        who:
            Optional username/application name performing the write. If not
            provided, the authenticated user display name is used.
        duration: Optional number with duration unit if setting level 8.

    Returns:
        `Grid` with the server's response.
    """
    row = {"id": id, "level": level}

    if val is not None:
        row["val"] = val
    if who is not None:
        row["who"] = who
    if duration is not None:
        row["duration"] = duration

    return self.call("pointWrite", Grid.to_grid(row))

point_write_array

point_write_array(id)

Reads the current status of a writable point's priority array.

Parameters:

Name Type Description Default
id phable.kinds.Ref

Unique identifier for the record.

required

Returns:

Type Description
phable.kinds.Grid

Grid with the server's response.

Source code in phable/haystack_client.py
635
636
637
638
639
640
641
642
643
644
def point_write_array(self, id: Ref) -> Grid:
    """Reads the current status of a writable point's priority array.

    Parameters:
        id: Unique identifier for the record.

    Returns:
        `Grid` with the server's response.
    """
    return self.call("pointWrite", Grid.to_grid({"id": id}))

read

read(filter, checked=True)

Read from the database the first record which matches the filter.

Raises:

Type Description
phable.haystack_client.UnknownRecError

Server's response does not include requested rec.

Parameters:

Name Type Description Default
filter str

Project Haystack defined filter for querying the server.

required
checked bool

If checked is equal to false and the record cannot be found, an empty dict is returned. If checked is equal to true and the record cannot be found, an UnknownRecError is raised.

True

Returns:

Type Description
dict[typing.Any, typing.Any]

An empty dict or a dict that describes the entity read.

Source code in phable/haystack_client.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def read(self, filter: str, checked: bool = True) -> dict[Any, Any]:
    """Read from the database the first record which matches the
    [filter](https://project-haystack.org/doc/docHaystack/Filters).

    Raises:
        UnknownRecError: Server's response does not include requested rec.

    Parameters:
        filter:
            Project Haystack defined
            [filter](https://project-haystack.org/doc/docHaystack/Filters) for
            querying the server.
        checked:
            If `checked` is equal to false and the record cannot be found, an empty
            `dict` is returned. If `checked` is equal to true and the record cannot
            be found, an `UnknownRecError` is raised.

    Returns:
        An empty `dict` or a `dict` that describes the entity read.
    """
    response = self.read_all(filter, 1)

    if len(response.rows) == 0:
        if checked is True:
            raise UnknownRecError(
                "Unable to locate an entity on the server that matches the filter."
            )
        else:
            response = {}
    else:
        response = response.rows[0]

    return response

read_all

read_all(filter, limit=None)

Read all records from the database which match the filter.

Parameters:

Name Type Description Default
filter str

Project Haystack defined filter for querying the server.

required
limit int | None

Maximum number of entities to return in response.

None

Returns:

Type Description
phable.kinds.Grid

An empty Grid or a Grid that has a row for each entity read.

Source code in phable/haystack_client.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def read_all(self, filter: str, limit: int | None = None) -> Grid:
    """Read all records from the database which match the
    [filter](https://project-haystack.org/doc/docHaystack/Filters).

    Parameters:
        filter:
            Project Haystack defined
            [filter](https://project-haystack.org/doc/docHaystack/Filters) for
            querying the server.
        limit: Maximum number of entities to return in response.

    Returns:
        An empty `Grid` or a `Grid` that has a row for each entity read.
    """
    data_row = {"filter": filter}

    if limit is not None:
        data_row["limit"] = limit

    response = self.call("read", Grid.to_grid(data_row))

    return response

read_by_id

read_by_id(id, checked=True)

Read an entity record using its unique identifier.

Raises:

Type Description
phable.haystack_client.UnknownRecError

Server's response does not include requested rec.

Parameters:

Name Type Description Default
id phable.kinds.Ref

Unique identifier for the record being read.

required
checked bool

If checked is equal to false and the record cannot be found, an empty dict is returned. If checked is equal to true and the record cannot be found, an UnknownRecError is raised.

True

Returns:

Type Description
dict[typing.Any, typing.Any]

An empty dict or a dict that describes the entity read.

Source code in phable/haystack_client.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def read_by_id(self, id: Ref, checked: bool = True) -> dict[Any, Any]:
    """Read an entity record using its unique identifier.

    Raises:
        UnknownRecError: Server's response does not include requested rec.

    Parameters:
        id: Unique identifier for the record being read.
        checked:
            If `checked` is equal to false and the record cannot be found, an empty
            `dict` is returned. If `checked` is equal to true and the record cannot
            be found, an `UnknownRecError` is raised.

    Returns:
        An empty `dict` or a `dict` that describes the entity read.
    """

    data_rows = [{"id": {"_kind": "ref", "val": id.val}}]
    post_data = Grid.to_grid(data_rows)
    response = self.call("read", post_data)

    if len(response.rows) == 0:
        if checked is True:
            raise UnknownRecError("Unable to locate the id on the server.")
        else:
            response = {}
    else:
        response = response.rows[0]

    return response

read_by_ids

read_by_ids(ids)

Read a set of entity records using their unique identifiers.

Note: Project Haystack recently introduced batch read support, which might not be supported by some servers. If your server does not support the batch read feature, then try using the Client.read_by_id() method instead.

Raises:

Type Description
phable.haystack_client.UnknownRecError

Server's response does not include requested recs.

Parameters:

Name Type Description Default
ids list[phable.kinds.Ref]

Unique identifiers for the records being read.

required

Returns:

Type Description
phable.kinds.Grid

Grid with a row for each entity read.

Source code in phable/haystack_client.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def read_by_ids(self, ids: list[Ref]) -> Grid:
    """Read a set of entity records using their unique identifiers.

    **Note:** Project Haystack recently introduced batch read support, which might
    not be supported by some servers. If your server does not support the batch
    read feature, then try using the `Client.read_by_id()` method instead.

    Raises:
        UnknownRecError: Server's response does not include requested recs.

    Parameters:
        ids: Unique identifiers for the records being read.

    Returns:
        `Grid` with a row for each entity read.
    """
    ids = ids.copy()
    data_rows = [{"id": {"_kind": "ref", "val": id.val}} for id in ids]
    post_data = Grid.to_grid(data_rows)
    response = self.call("read", post_data)

    if len(response.rows) == 0:
        raise UnknownRecError("Unable to locate any of the ids on the server.")
    for row in response.rows:
        if len(row) == 0:
            raise UnknownRecError("Unable to locate one or more ids on the server.")

    return response

AuthError dataclass

Bases: Exception

Error raised when the client is unable to authenticate with the server using the credentials provided.

AuthError can be directly imported as follows:

from phable import AuthError

Parameters:

Name Type Description Default
help_msg str

A display to help with troubleshooting.

required
Source code in phable/haystack_client.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass
class AuthError(Exception):
    """Error raised when the client is unable to authenticate with the server using the
    credentials provided.

    `AuthError` can be directly imported as follows:

    ```python
    from phable import AuthError
    ```

    Parameters:
        help_msg: A display to help with troubleshooting.
    """

    help_msg: str

CallError dataclass

Bases: Exception

Error raised by HaystackClient when server's Grid response meta has an err marker tag.

CallError can be directly imported as follows:

from phable import CallError

Parameters:

Name Type Description Default
help_msg phable.kinds.Grid

Grid that has err marker tag in meta described here.

required
Source code in phable/haystack_client.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@dataclass
class CallError(Exception):
    """Error raised by `HaystackClient` when server's `Grid` response meta has an `err`
    marker tag.

    `CallError` can be directly imported as follows:

    ```python
    from phable import CallError
    ```

    Parameters:
        help_msg:
            `Grid` that has `err` marker tag in meta described
            [here](https://project-haystack.org/doc/docHaystack/HttpApi#errorGrid).
    """

    help_msg: Grid

UnknownRecError dataclass

Bases: Exception

Error raised by HaystackClient when server's Grid response does not include data for one or more recs being requested.

UnknownRecError can be directly imported as follows:

from phable import UnknownRecError

Parameters:

Name Type Description Default
help_msg str

A display to help with troubleshooting.

required
Source code in phable/haystack_client.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@dataclass
class UnknownRecError(Exception):
    """Error raised by `HaystackClient` when server's `Grid` response does not include
    data for one or more recs being requested.

    `UnknownRecError` can be directly imported as follows:

    ```python
    from phable import UnknownRecError
    ```

    Parameters:
        help_msg: A display to help with troubleshooting.
    """

    help_msg: str