Skip to content

Commit 6e754e8

Browse files
authored
Initial Stable - Update documentation and perform final bug fixes (#9)
* Added Gitleaks action scan * Added definite 3.13 support * Added documentation * Added wrappers to our wrappers, so that you need less parameters * Implemented eQSL graphic retrieval * Added some logbook.QSO converters to shoot around with types * Added QRZ insert_record * Fixed delete_record * Fixed check_status * Added some QRZ error tests --------- Co-authored-by: Jacob Humble <contact@jacobhumble.com>
1 parent a8b0bc1 commit 6e754e8

File tree

14 files changed

+498
-161
lines changed

14 files changed

+498
-161
lines changed

.github/workflows/gitleaks.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: gitleaks
2+
on:
3+
pull_request:
4+
push:
5+
workflow_dispatch:
6+
schedule:
7+
- cron: "0 4 * * *" # run once a day at 4 AM
8+
jobs:
9+
scan:
10+
name: gitleaks
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- uses: gitleaks/gitleaks-action@v2
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: ["3.9", "3.10", "3.11", "3.12"]
10+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1111
steps:
1212
- uses: actions/checkout@v4
1313
- name: Set up Python ${{ matrix.python-version }}

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
QSPyLib is a bundle of API wrappers for various amateur radio-related sites, including QRZ, LOTW, eQSL, and ClubLog.
1010

11-
It is currently in development and should be considered unstable version-to-version while the version number is still 0.x.x.
11+
QSPyLib is in active development; that said, major version numbers should maintain API stability. If you need absolute stability of the API, fix your version against the major.
1212

1313
Issues and pull requests are welcome, and should be made on the [GitHub repository](https://github.com/jaytotheay/qspy).
1414

@@ -32,12 +32,14 @@ This will generate a .whl and tar.gz, which you can then install locally.
3232

3333
## What works right now?
3434

35-
As of v0.0.1:
35+
As of v1.0.0:
3636

37-
* The LotW module is, in theory, finished -- no doubt something will come up about how it's not actually practical and needs more work.
38-
* The eQSL module has most of the functionality of eQSL's API, but is incredibly unpolished and needs more work.
39-
* The QRZ module exists; the Logbook API is currently only supported for FETCH operations, and the XML Interface is not supported yet.
40-
* The ClubLog module is on-hold pending the ready-status of the other modules.
37+
* The LotW module is, in theory, finished -- you can download QSOs in bulk or by criteria, check DXCC credit, get a list of users and their date of last upload, and upload a log.
38+
* The eQSL module has most of the functionality of eQSL's API, but is a bit unpolished -- at present, you can fetch inboxes and outboxes, get AG lists, get member lists, get last upload data for users, verify an eQSL, and retrieve the eQSL card graphic for a QSL.
39+
* The QRZ module is done; for logs, we support fetching logbooks, checking logbook statuses, and inserting and deleting records. For the XML API, we support looking up a callsign's data and looking up a DXCC's data.
40+
* The ClubLog module only supports grabbing logbooks from ClubLog at the moment.
41+
42+
Everything has been tested to work when done "correctly" and simply; no doubt some edge case will pop up, or some failure state won't throw a good error. Please open an issue for *any* problems you come across, no matter how minor, even if it's just an exception that isn't descriptive.
4143

4244
## How do I use it?
4345

@@ -60,8 +62,8 @@ Other functions of APIs are generally available, like checking if an eQSL is ver
6062

6163
```py
6264
from qspylib import eqsl
63-
confirmed, raw_result = eqsl.verify_eqsl('N5UP', 'TEST', '160m', 'SSB', '01/01/2000')
65+
confirmed, raw_result = eqsl.eQSLClient.verify_eqsl('N5UP', 'TEST', '160m', 'SSB', '01/01/2000')
6466
```
65-
This will return a tuple; here, `confirmed` will be False, since this QSO is not verified on eQSL, and `raw_result` will contain any extra information eQSL provides, for instance, if it's Authenticity Guaranteed.
67+
This will return a tuple; here, `confirmed` will be False, since this QSO is not verified on eQSL, and `raw_result` will contain any extra information eQSL provides, for instance, if it's Authenticity Guaranteed. Note that verify_eqsl is a static method of the eQSLClient class, and can be called either from an eQSLClient object, or directly from the class.
6668

6769
Modules, functions, and classes are documented in-code via docstrings, and you can learn more by reading those docstrings; you can also read the [Read the Docs](http://qspylib.readthedocs.io/) listings for a visually pleasing guide on what the docstrings say.

docs/source/examples.rst

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ Pulling a LotW Logbook and Printing QSLs since Last Login, Using .adi Property
2525
.. code-block:: python
2626
2727
"""This example demonstrates logging into LOTW for a user 'CA7LSIGN' and fetching their QSLs with default parameters. By default, this will only return QSLs received since the last time a logbook was fetched from LOTW.
28-
28+
2929
This example also demonstrates using the .adi property of the Logbook; this property contains a parsed dictionary of the entire .adi log as received from LotW, and you can reference any present fields as dictionary keys.
30-
"""
31-
30+
"""
31+
3232
>>> import qspylib
3333
>>> LOTWSession = qspylib.lotw.LOTWClient("CA7LSIGN", "password")
3434
>>> lotw_logbook = LOTWSession.fetch_logbook()
@@ -42,17 +42,17 @@ Pulling a LotW Logbook and Printing QSLs since Last Login, Using .adi Property
4242
'TE5T'
4343
>>> lotw_logbook.adi[0]["BAND"]
4444
'20M'
45-
45+
4646
Pulling a LotW Logbook and Printing QSOs since Last Login, Using .log Property
4747
*********************************************************************************
4848

4949
.. code-block:: python
5050
51-
"""This example demonstrates logging into LOTW for a user 'CA7LSIGN' and fetching their QSOs since 2024-10-01, and then printing them out and grabbing the QSL index.
52-
51+
"""This example demonstrates logging into LOTW for a user 'CA7LSIGN' and fetching their QSOs since 2024-10-01, and then printing them out.
52+
5353
This example also demonstrates using the .log property of the Logbook; this property contains a list of contacts, and each contains only very limited information about each QSO (the info as seen here.) The QSL property will "unify" the QSL fields as present in ClubLog, QRZ, LoTW, and eQSL, so it is handy for comparing confirmations between sources.
5454
"""
55-
55+
5656
>>> import qspylib
5757
>>> LOTWSession = qspylib.lotw.LOTWClient('CA7LSIGN', 'password')
5858
>>> lotw_logbook = LOTWSession.fetch_logbook(qso_qsl='no', qso_qsorxsince='2024-10-01')
@@ -69,4 +69,114 @@ Pulling a LotW Logbook and Printing QSOs since Last Login, Using .log Property
6969
7070
CALL: TE5T BAND: 40M MODE: FT8 DATE: 20241003 TIME: 003700 QSL: N
7171
72-
CALL: TE6T BAND: 20M MODE: FT8 DATE: 20241003 TIME: 004500 QSL: N
72+
CALL: TE6T BAND: 20M MODE: FT8 DATE: 20241003 TIME: 004500 QSL: N
73+
74+
Note that you can also use the `fetch_qsos` method to fetch all QSOs; this has a simpler optional parameter set, but takes in a datetime object for date parameters. It also defaults to fetching as much information as LOTW will return about a QSO.
75+
76+
.. code-block:: python
77+
78+
>>> import qspylib, datetime
79+
>>> LOTWSession = qspylib.lotw.LOTWClient('CA7LSIGN', 'password')
80+
>>> qsosince_datetime = datetime.datetime(2024, 10, 1)
81+
>>> lotw_logbook = LOTWSession.fetch_qsos(qsorxsince=qsosince_datetime)
82+
>>> for contact in lotw_logbook.log:
83+
... print(contact)
84+
...
85+
CALL: TE1T BAND: 12M MODE: FT8 DATE: 20241003 TIME: 025300 QSL: Y
86+
87+
CALL: TE2T BAND: 12M MODE: FT8 DATE: 20241003 TIME: 025500 QSL: Y
88+
89+
CALL: TE3T BAND: 20M MODE: FT8 DATE: 20241003 TIME: 012100 QSL: Y
90+
91+
CALL: TE4T BAND: 12M MODE: FT8 DATE: 20241003 TIME: 012900 QSL: N
92+
93+
CALL: TE5T BAND: 40M MODE: FT8 DATE: 20241003 TIME: 003700 QSL: N
94+
95+
CALL: TE6T BAND: 20M MODE: FT8 DATE: 20241003 TIME: 004500 QSL: N
96+
97+
Pulling the inbox from eQSL, using .adi Property
98+
************************************************
99+
Pulling information from a user's eQSL inbox is relatively easy with qspylib. The `fetch_inbox` method will return a Logbook object with the inbox contents.
100+
The .adi portion of the Logbook object will contain all adif fields received from eQSL, and you can reference any present fields as dictionary keys.
101+
Note that the eQSL divides a logbook into an inbox, and an outbox; the inbox is the QSOs that the user has received, sent for confirmation by other users.
102+
103+
.. code-block:: python
104+
105+
"""This example demonstrates logging into eQSL for a user 'CA7LSIGN' and fetching their inbox since 2024-01-01 00:00, and then printing them out.
106+
107+
This example also demonstrates using the .adi property of the Logbook; this property contains a dictionary of the entire .adi log as received from eQSL,
108+
where each contact is one record. All the information received from eQSL will be present in the .adi portion of the Logbook object, unlike the .log portion.
109+
"""
110+
111+
>>> import qspylib
112+
>>> eQSLSession = qspylib.eqsl.eQSLClient('CA7LSIGN', 'password')
113+
>>> eqsl_inbox = eQSLSession.fetch_inbox(rcvd_since='202401010000')
114+
>>> for contact in eqsl_inbox.adi:
115+
... print(contact)
116+
...
117+
<QSO_DATE:8>20231105 <TIME_ON:4>1228 <CALL:5>TE3T <MODE:4>MFSK <APP_EQSL_AG:1>Y <BAND:3>40M <EQSL_QSLRDATE:8>20240120 <EQSL_QSL_RCVD:1>Y <GRIDSQUARE:6>EM17nt <QSL_SENT:1>Y <QSL_SENT_VIA:1>E <RST_SENT:3>+01 <SUBMODE:3>FT4 <TX_PWR:8>100.0000 <EOR>
118+
119+
<QSO_DATE:8>20231105 <TIME_ON:4>1230 <CALL:4>TE5T <MODE:4>MFSK <APP_EQSL_AG:1>Y <BAND:3>40M <EQSL_QSLRDATE:8>20241015 <EQSL_QSL_RCVD:1>Y <GRIDSQUARE:6>EM12qt <QSL_SENT:1>Y <QSL_SENT_VIA:1>E <RST_SENT:3>-08 <SUBMODE:3>FT4 <EOR>
120+
121+
>>>str(eqsl_inbox.adi[0])
122+
'<QSO_DATE:8>20231105 <TIME_ON:4>1228 <CALL:5>TE3T <MODE:4>MFSK <APP_EQSL_AG:1>Y <BAND:3>40M <EQSL_QSLRDATE:8>20240120 <EQSL_QSL_RCVD:1>Y <GRIDSQUARE:6>EM17nt <QSL_SENT:1>Y <QSL_SENT_VIA:1>E <RST_SENT:3>+01 <SUBMODE:3>FT4 <TX_PWR:8>100.0000 <EOR>\n'
123+
>>>str(eqsl_inbox.adi[0]['CALL'])
124+
'TE3T'
125+
>>>len(eqsl_inbox.adi)
126+
2
127+
128+
Verify a QSL with eQSL
129+
**********************
130+
eQSL provides for confirming that a QSL is confirmed -- if it was confirmed on eQSL. This can be done by *any* user, not just a logged in one, given they have the proper information.
131+
132+
.. code-block:: python
133+
134+
"""This example demonstrates confirming an eQSL took place with eQSL."""
135+
136+
>>> from qspylib import eqsl
137+
>>> confirmed, raw_result = eqsl.eQSLClient.verify_eqsl('N5UP', 'TEST', '160m', 'SSB', '01/01/2000')
138+
>>> confirmed
139+
False
140+
>>> raw_result
141+
'\r\n<HTML>\r\n<HEAD></HEAD>\r\n<BODY>\r\n\r\n\r\n\r\n Error - Result: QSO not on file\r\n </BODY>\r\n </HTML>\r\n '
142+
143+
In current versions of qspylib, parsing raw_result for additional information, such as authenticity guaranteed status or the error cause, is left as an exercise for the reader.
144+
145+
Retrieving an eQSL Graphic
146+
**************************
147+
eQSL provides for retrieving the graphic for the digital QSL card corresponding to a QSO. Note that they request you only do at most, six requests per minute.
148+
149+
.. code-block:: python
150+
151+
"This example demonstrates retrieving an eQSL graphic for a given QSO and displaying it using PIL."
152+
153+
>>> import qspylib
154+
>>> from datetime import datetime
155+
>>> from PIL import Image
156+
>>> eqsl_client = qspylib.eqsl.eQSLClient("CAL7SIGN", "notarealpassword")
157+
>>> inbox = eqsl_client.fetch_inbox_qsls()
158+
>>> str(inbox.adi[12])
159+
'<QSO_DATE:8>20230101 <TIME_ON:4>0730 <CALL:5>TE5T <MODE:3>FT8 <APP_EQSL_AG:1>Y <BAND:3>20M <EQSL_QSLRDATE:8>20230101 <EQSL_QSL_RCVD:1>Y <GRIDSQUARE:6>EM12em <QSL_SENT:1>Y <QSL_SENT_VIA:1>E <RST_SENT:3>+00 <EOR>\n'
160+
>>> qso_datetime = datetime(2023, 1, 1, 7, 30)
161+
>>> image_data = eqsl_client.retrieve_graphic("te5t", qso_datetime, "20m", "FT8")
162+
>>> image_data
163+
<_io.BytesIO object at 0x0000000000000000>
164+
>>> image = Image.open(image_data)
165+
>>> image
166+
<PIL.PngImagePlugin.PngImageFile image mode=RGB size=528x336 at 0x0000000000000000>
167+
>>> image.show() # this will open the actual image file, showing you the image.
168+
169+
Looking up a callsign on QRZ
170+
****************************
171+
QRZ allows an authenticated user to lookup certain information about a QRZ user. This information will be returned by qspylib as a dictionary that can be parsed, sharing a structure with the XML tree returned by QRZ.
172+
.. code-block:: python
173+
174+
"""This example demonstrates grabbing information about a callsign from QRZ's XML API."""
175+
176+
>>> from qspylib import qrz
177+
>>> QRZXMLSession = qrz.QRZXMLClient('TE5T', 'password', agent='sample_program/0.0.1')
178+
>>> info = QRZXMLSession.lookup_callsign('aa7bq')
179+
>>> info
180+
{'QRZDatabase': {'@version': '1.34', '@xmlns': 'http://xmldata.qrz.com', 'Callsign': {'call': 'AA7BQ', 'aliases': 'N6UFT,AA7BQ/DL1,KJ6RK,AA7BQ/HR6', 'dxcc': '291', 'attn': 'AA7BQ', 'fname': 'FRED L', 'name': 'LLOYD', 'addr1': '24 W. Camelback Rd, STE A-488', 'addr2': 'Phoenix', 'state': 'AZ', 'zip': '85013', 'country': 'United States', 'lat': '33.509665', 'lon': '-112.074142', 'grid': 'DM33xm', 'county': 'Maricopa', 'ccode': '271', 'fips': '04013', 'land': 'United States', 'efdate': '2022-04-29', 'expdate': '2030-01-20', 'class': 'E', 'codes': 'HAI', 'qslmgr': 'via QRZ', 'email': 'aa7bq@qrz.com', 'u_views': '345756', 'bio': '12804', 'biodate': '2023-02-17 17:37:29', 'image': 'https://cdn-xml.qrz.com/q/aa7bq/fred1962.jpg', 'imageinfo': '636:800:90801', 'moddate': '2022-10-09 17:32:38', 'MSA': '6200', 'AreaCode': '602', 'TimeZone': 'Mountain', 'GMTOffset': '-7', 'DST': 'N', 'eqsl': '0', 'mqsl': '0', 'cqzone': '3', 'ituzone': '6', 'born': '1953', 'lotw': '0', 'user': 'AA7BQ', 'geoloc': 'user', 'name_fmt': 'FRED L LLOYD'}, 'Session': {'Key': 'nicetrykiddo', 'Count': '539', 'SubExp': 'Mon Sep 15 02:38:30 2025', 'GMTime': 'Sun Nov 24 04:22:11 2024', 'Remark': 'cpu: 0.018s'}}}
181+
>>> info['QRZDatabase']['Callsign']['TimeZone']
182+
'Mountain'

docs/source/index.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ QSPyLib
2626
:alt: PyPI - License
2727

2828

29-
``qsyplib`` is a bundle of API wrappers for various amateur radio-related websites, including QRZ, LOTW, eQSL, and ClubLog.
29+
``qsyplib`` is a bundle of API wrappers for various amateur radio-related sites, including QRZ, LOTW, eQSL, and ClubLog.
3030

31-
It is currently in development and should be considered unstable version-to-version while the version number is still 0.x.x.
31+
QSPyLib is in active development; that said, major version numbers should maintain API stability. If you need absolute stability of the API, fix your version against the major.
3232

3333
Issues and pull requests are welcome, and should be made on the [GitHub repository](https://github.com/JayToTheAy/QSPy).
3434

3535
.. toctree::
3636
:maxdepth: 2
3737
:caption: Contents:
38-
38+
3939
modules
4040
qspylib
4141
examples

pyproject.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@ authors = [
1717
]
1818
description = "A set of API wrappers for different amateur radio websites, including LotW, QRZ, eQSL, and ClubLog"
1919
readme = "README.md"
20-
keywords = ["QRZ", "LOTW", "eQSL", "API", "amateur radio"]
20+
keywords = ["QRZ", "LOTW", "eQSL", "ClubLog", "API", "amateur radio", "ham radio"]
2121
classifiers = [
22-
"Programming Language :: Python :: 3.9",
22+
"Development Status :: 5 - Production/Stable",
23+
"Intended Audience :: Developers",
24+
"Topic :: Communications :: Ham Radio",
25+
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
26+
"Programming Language :: Python :: 3",
27+
"Programming Language :: Python :: 3.9",
2328
"Programming Language :: Python :: 3.10",
2429
"Programming Language :: Python :: 3.11",
2530
"Programming Language :: Python :: 3.12",
2631
"Programming Language :: Python :: 3.13",
27-
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
28-
"Operating System :: OS Independent",
29-
"Development Status :: 2 - Pre-Alpha"
32+
"Operating System :: OS Independent",
3033
]
3134

3235
[project.urls]
3336
Homepage = "https://github.com/JayToTheAy/QSPy"
37+
Documentation = "https://qspylib.readthedocs.io/en/"
3438
Issues = "https://github.com/JayToTheAy/QSPy/issues"
3539

3640
[tool.hatch.version]

requirements.txt

-4 Bytes
Binary file not shown.

src/qspylib/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""unified version string"""
22

3-
__version__ = "1.0.0a3"
3+
__version__ = "1.0.0"

0 commit comments

Comments
 (0)