pharmaradar 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pharmaradar-1.1.1/.github/workflows/ci.yml +232 -0
- pharmaradar-1.1.1/.github/workflows/test.yml +63 -0
- pharmaradar-1.1.1/.gitignore +15 -0
- pharmaradar-1.1.1/CHANGELOG.md +42 -0
- pharmaradar-1.1.1/LICENSE +21 -0
- pharmaradar-1.1.1/PKG-INFO +256 -0
- pharmaradar-1.1.1/README.md +233 -0
- pharmaradar-1.1.1/pyproject.toml +65 -0
- pharmaradar-1.1.1/scripts/local-pypi-server.sh +0 -0
- pharmaradar-1.1.1/scripts/pre-commit-check.sh +37 -0
- pharmaradar-1.1.1/setup.cfg +4 -0
- pharmaradar-1.1.1/src/pharmaradar/__init__.py +19 -0
- pharmaradar-1.1.1/src/pharmaradar/availability_level.py +25 -0
- pharmaradar-1.1.1/src/pharmaradar/database/database_interface.py +99 -0
- pharmaradar-1.1.1/src/pharmaradar/location_selector.py +313 -0
- pharmaradar-1.1.1/src/pharmaradar/medicine.py +134 -0
- pharmaradar-1.1.1/src/pharmaradar/medicine_scraper.py +421 -0
- pharmaradar-1.1.1/src/pharmaradar/pharmacy_info.py +62 -0
- pharmaradar-1.1.1/src/pharmaradar/scraping_utils.py +463 -0
- pharmaradar-1.1.1/src/pharmaradar/service/medicine_watchdog.py +306 -0
- pharmaradar-1.1.1/src/pharmaradar/text_parsers.py +886 -0
- pharmaradar-1.1.1/src/pharmaradar/webdriver_utils.py +324 -0
- pharmaradar-1.1.1/src/pharmaradar.egg-info/PKG-INFO +256 -0
- pharmaradar-1.1.1/src/pharmaradar.egg-info/SOURCES.txt +31 -0
- pharmaradar-1.1.1/src/pharmaradar.egg-info/dependency_links.txt +1 -0
- pharmaradar-1.1.1/src/pharmaradar.egg-info/requires.txt +12 -0
- pharmaradar-1.1.1/src/pharmaradar.egg-info/top_level.txt +1 -0
- pharmaradar-1.1.1/tests/__init__.py +0 -0
- pharmaradar-1.1.1/tests/test_medicine.py +211 -0
- pharmaradar-1.1.1/tests/test_medicine_feature.py +174 -0
- pharmaradar-1.1.1/tests/test_medicine_scraper.py +413 -0
- pharmaradar-1.1.1/tests/test_name_matching.py +86 -0
- pharmaradar-1.1.1/tests/test_text_parsers.py +447 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# CI/CD Pipeline with Semantic Versioning
|
|
2
|
+
#
|
|
3
|
+
# This workflow automatically versions and publishes the package based on conventional commits:
|
|
4
|
+
# - feat: adds new features → minor version bump (0.1.0 → 0.2.0)
|
|
5
|
+
# - fix: bug fixes → patch version bump (0.1.0 → 0.1.1)
|
|
6
|
+
# - BREAKING CHANGE: in commit body → major version bump (0.1.0 → 1.0.0)
|
|
7
|
+
#
|
|
8
|
+
# The pipeline:
|
|
9
|
+
# 1. Runs code quality checks (linting, formatting)
|
|
10
|
+
# 2. Runs unit tests
|
|
11
|
+
# 3. Analyzes commits and bumps version if needed (main branch only)
|
|
12
|
+
# 4. Builds package
|
|
13
|
+
# 5. Creates GitHub release (if version changed)
|
|
14
|
+
# 6. Publishes to PyPI (if version changed and in release environment)
|
|
15
|
+
#
|
|
16
|
+
# Note: Workflow skips execution for semantic-release automated commits to prevent infinite loops
|
|
17
|
+
|
|
18
|
+
name: CI/CD
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
push:
|
|
22
|
+
branches: [ main ]
|
|
23
|
+
# Don't run on tags created by semantic-release
|
|
24
|
+
tags-ignore: [ 'v*' ]
|
|
25
|
+
pull_request:
|
|
26
|
+
branches: [ main ]
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
quality:
|
|
30
|
+
name: Code Quality
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
# Skip if this is a semantic-release commit
|
|
33
|
+
if: github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
34
|
+
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Set up Python
|
|
39
|
+
uses: actions/setup-python@v4
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.13"
|
|
42
|
+
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: |
|
|
45
|
+
python -m pip install --upgrade pip
|
|
46
|
+
pip install -e ".[dev]"
|
|
47
|
+
|
|
48
|
+
- name: Lint with flake8
|
|
49
|
+
run: |
|
|
50
|
+
flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
51
|
+
flake8 src/ tests/ --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics
|
|
52
|
+
|
|
53
|
+
- name: Check formatting with black
|
|
54
|
+
run: black --check --diff src/ tests/
|
|
55
|
+
|
|
56
|
+
test:
|
|
57
|
+
name: Test Suite
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
needs: quality
|
|
60
|
+
# Skip if this is a semantic-release commit
|
|
61
|
+
if: github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
62
|
+
strategy:
|
|
63
|
+
matrix:
|
|
64
|
+
python-version: ["3.13"]
|
|
65
|
+
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
|
|
69
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
70
|
+
uses: actions/setup-python@v4
|
|
71
|
+
with:
|
|
72
|
+
python-version: ${{ matrix.python-version }}
|
|
73
|
+
|
|
74
|
+
- name: Install system dependencies
|
|
75
|
+
run: |
|
|
76
|
+
sudo apt-get update
|
|
77
|
+
sudo apt-get install -y chromium-browser chromium-chromedriver
|
|
78
|
+
|
|
79
|
+
- name: Install dependencies
|
|
80
|
+
run: |
|
|
81
|
+
python -m pip install --upgrade pip
|
|
82
|
+
pip install -e ".[dev]"
|
|
83
|
+
|
|
84
|
+
- name: Run tests
|
|
85
|
+
run: |
|
|
86
|
+
pytest tests/ --verbose --tb=short --junitxml=test-results.xml
|
|
87
|
+
env:
|
|
88
|
+
DISPLAY: :99
|
|
89
|
+
|
|
90
|
+
- name: Upload test results
|
|
91
|
+
uses: actions/upload-artifact@v4
|
|
92
|
+
if: always()
|
|
93
|
+
with:
|
|
94
|
+
name: test-results-${{ matrix.python-version }}
|
|
95
|
+
path: test-results.xml
|
|
96
|
+
|
|
97
|
+
build:
|
|
98
|
+
name: Build Package
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
needs: [quality, test]
|
|
101
|
+
# Skip if this is a semantic-release commit, and only run on main branch pushes
|
|
102
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
103
|
+
outputs:
|
|
104
|
+
version-changed: ${{ steps.semantic.outputs.version-changed }}
|
|
105
|
+
old-version: ${{ steps.semantic.outputs.old-version }}
|
|
106
|
+
new-version: ${{ steps.semantic.outputs.new-version }}
|
|
107
|
+
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
with:
|
|
111
|
+
fetch-depth: 0 # Full history for semantic versioning
|
|
112
|
+
token: ${{ secrets.TOKEN }}
|
|
113
|
+
|
|
114
|
+
- name: Set up Python
|
|
115
|
+
uses: actions/setup-python@v4
|
|
116
|
+
with:
|
|
117
|
+
python-version: "3.13"
|
|
118
|
+
|
|
119
|
+
- name: Install build dependencies
|
|
120
|
+
run: |
|
|
121
|
+
python -m pip install --upgrade pip
|
|
122
|
+
pip install build twine python-semantic-release
|
|
123
|
+
|
|
124
|
+
- name: Configure git for semantic release
|
|
125
|
+
run: |
|
|
126
|
+
git config user.name "github-actions[bot]"
|
|
127
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
128
|
+
|
|
129
|
+
- name: Run semantic release
|
|
130
|
+
id: semantic
|
|
131
|
+
env:
|
|
132
|
+
GH_TOKEN: ${{ secrets.TOKEN }}
|
|
133
|
+
run: |
|
|
134
|
+
# Check if there are changes that warrant a new version
|
|
135
|
+
echo "🔍 Analyzing commits for version bump..."
|
|
136
|
+
|
|
137
|
+
# Get current version before semantic release
|
|
138
|
+
OLD_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
139
|
+
echo "old-version=$OLD_VERSION" >> $GITHUB_OUTPUT
|
|
140
|
+
echo "📋 Current version: $OLD_VERSION"
|
|
141
|
+
|
|
142
|
+
# Run semantic release to bump version if needed
|
|
143
|
+
echo "🔄 Running semantic release..."
|
|
144
|
+
if semantic-release --noop version; then
|
|
145
|
+
echo "📦 Changes detected, proceeding with version bump"
|
|
146
|
+
semantic-release version
|
|
147
|
+
else
|
|
148
|
+
echo "ℹ️ No significant changes found, keeping current version"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Get new version after semantic release
|
|
152
|
+
NEW_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
153
|
+
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
154
|
+
|
|
155
|
+
# Check if version changed
|
|
156
|
+
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
157
|
+
echo "version-changed=true" >> $GITHUB_OUTPUT
|
|
158
|
+
echo "✅ Version bumped from $OLD_VERSION to $NEW_VERSION"
|
|
159
|
+
else
|
|
160
|
+
echo "version-changed=false" >> $GITHUB_OUTPUT
|
|
161
|
+
echo "ℹ️ No version change needed (current: $NEW_VERSION)"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
- name: Create GitHub Release
|
|
165
|
+
if: steps.semantic.outputs.version-changed == 'true'
|
|
166
|
+
env:
|
|
167
|
+
GH_TOKEN: ${{ secrets.TOKEN }}
|
|
168
|
+
run: |
|
|
169
|
+
NEW_VERSION="${{ steps.semantic.outputs.new-version }}"
|
|
170
|
+
echo "🚀 Preparing GitHub release for version $NEW_VERSION"
|
|
171
|
+
|
|
172
|
+
# Create a git tag if it doesn't exist
|
|
173
|
+
if git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" 2>/dev/null; then
|
|
174
|
+
echo "✅ Created tag v$NEW_VERSION"
|
|
175
|
+
git push origin "v$NEW_VERSION" || echo "Tag already pushed"
|
|
176
|
+
else
|
|
177
|
+
echo "ℹ️ Tag v$NEW_VERSION already exists"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Check if release already exists
|
|
181
|
+
if gh release view "v$NEW_VERSION" >/dev/null 2>&1; then
|
|
182
|
+
echo "ℹ️ Release v$NEW_VERSION already exists, skipping creation"
|
|
183
|
+
else
|
|
184
|
+
echo "📝 Creating GitHub release for v$NEW_VERSION"
|
|
185
|
+
gh release create "v$NEW_VERSION" \
|
|
186
|
+
--title "Release v$NEW_VERSION" \
|
|
187
|
+
--generate-notes \
|
|
188
|
+
--latest
|
|
189
|
+
echo "✅ GitHub release created successfully"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
- name: Build package
|
|
193
|
+
run: python -m build
|
|
194
|
+
|
|
195
|
+
- name: Check package
|
|
196
|
+
run: twine check dist/*
|
|
197
|
+
|
|
198
|
+
- name: Summary
|
|
199
|
+
run: |
|
|
200
|
+
echo "📊 Build Summary:"
|
|
201
|
+
echo "Old version: ${{ steps.semantic.outputs.old-version }}"
|
|
202
|
+
echo "New version: ${{ steps.semantic.outputs.new-version }}"
|
|
203
|
+
echo "Version changed: ${{ steps.semantic.outputs.version-changed }}"
|
|
204
|
+
echo "Build artifacts:"
|
|
205
|
+
ls -la dist/
|
|
206
|
+
|
|
207
|
+
- name: Upload build artifacts
|
|
208
|
+
uses: actions/upload-artifact@v4
|
|
209
|
+
with:
|
|
210
|
+
name: dist
|
|
211
|
+
path: dist/
|
|
212
|
+
retention-days: 90
|
|
213
|
+
|
|
214
|
+
# publish:
|
|
215
|
+
# name: Publish to PyPI
|
|
216
|
+
# runs-on: ubuntu-latest
|
|
217
|
+
# needs: build
|
|
218
|
+
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.build.outputs.version-changed == 'true'
|
|
219
|
+
# environment: release
|
|
220
|
+
|
|
221
|
+
# steps:
|
|
222
|
+
# - name: Download build artifacts
|
|
223
|
+
# uses: actions/download-artifact@v4
|
|
224
|
+
# with:
|
|
225
|
+
# name: dist
|
|
226
|
+
# path: dist/
|
|
227
|
+
|
|
228
|
+
# - name: Publish to PyPI
|
|
229
|
+
# uses: pypa/gh-action-pypi-publish@release/v1
|
|
230
|
+
# with:
|
|
231
|
+
# password: ${{ secrets.PYPI_API_TOKEN }}
|
|
232
|
+
# skip-existing: true # Skip if version already exists
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Run Unit Tests
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.13"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v4
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install system dependencies for Selenium
|
|
27
|
+
run: |
|
|
28
|
+
sudo apt-get update
|
|
29
|
+
sudo apt-get install -y chromium-browser chromium-chromedriver xvfb
|
|
30
|
+
|
|
31
|
+
- name: Cache pip dependencies
|
|
32
|
+
uses: actions/cache@v3
|
|
33
|
+
with:
|
|
34
|
+
path: ~/.cache/pip
|
|
35
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
|
36
|
+
restore-keys: |
|
|
37
|
+
${{ runner.os }}-pip-
|
|
38
|
+
|
|
39
|
+
- name: Install Python dependencies
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
|
|
44
|
+
- name: Run unit tests
|
|
45
|
+
run: |
|
|
46
|
+
# Start virtual display for headless browser tests
|
|
47
|
+
export DISPLAY=:99
|
|
48
|
+
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
|
49
|
+
|
|
50
|
+
# Run tests with verbose output
|
|
51
|
+
pytest tests/ \
|
|
52
|
+
--verbose \
|
|
53
|
+
--tb=short \
|
|
54
|
+
--durations=10 \
|
|
55
|
+
--junitxml=test-results.xml
|
|
56
|
+
|
|
57
|
+
- name: Upload test results
|
|
58
|
+
uses: actions/upload-artifact@v4
|
|
59
|
+
if: always()
|
|
60
|
+
with:
|
|
61
|
+
name: test-results-python-${{ matrix.python-version }}
|
|
62
|
+
path: test-results.xml
|
|
63
|
+
retention-days: 30
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v1.1.1 (2025-09-03)
|
|
6
|
+
|
|
7
|
+
### Enhancement
|
|
8
|
+
|
|
9
|
+
- Freeze build and twine packages
|
|
10
|
+
([`f8c01af`](https://github.com/bartekmp/pharmaradar/commit/f8c01afffd2be7064cb366635d7bc80d3bdd7419))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.1.0 (2025-08-22)
|
|
14
|
+
|
|
15
|
+
### Feature
|
|
16
|
+
|
|
17
|
+
- Add selective fields update method
|
|
18
|
+
([`55fe226`](https://github.com/bartekmp/pharmaradar/commit/55fe226d4aef26ecd6e9efe7d6231c32ff3c4fa2))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## v1.0.2 (2025-08-20)
|
|
22
|
+
|
|
23
|
+
### Enhancement
|
|
24
|
+
|
|
25
|
+
- Update readme, remove unused file
|
|
26
|
+
([`23f66fc`](https://github.com/bartekmp/pharmaradar/commit/23f66fcd73f83f3645962eb39279e18a92f7649f))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## v1.0.1 (2025-08-19)
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
- Don't run builds on tag releases
|
|
34
|
+
([`a8b9e8e`](https://github.com/bartekmp/pharmaradar/commit/a8b9e8e7ad33ae38374e866600bde332d8348fbb))
|
|
35
|
+
|
|
36
|
+
- Use right semver option
|
|
37
|
+
([`10258b5`](https://github.com/bartekmp/pharmaradar/commit/10258b5dd97b9747cd480da60de48a5350f44f1a))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## v1.0.0 (2025-08-19)
|
|
41
|
+
|
|
42
|
+
- Initial Release
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 bartekmp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pharmaradar
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Python scraping package for KtoMaLek.pl website to monitor drug availability.
|
|
5
|
+
Author: bartekmp
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ktomalek,pharmaradar,web scraping,selenium
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: selenium>=4.15.0
|
|
12
|
+
Requires-Dist: lxml>=5.0.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-mock==3.14.1; extra == "dev"
|
|
16
|
+
Requires-Dist: black==25.1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: flake8==7.3.0; extra == "dev"
|
|
18
|
+
Requires-Dist: isort==6.0.1; extra == "dev"
|
|
19
|
+
Requires-Dist: python-semantic-release==10.2.0; extra == "dev"
|
|
20
|
+
Requires-Dist: build==1.3.0; extra == "dev"
|
|
21
|
+
Requires-Dist: twine==6.1.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# PharmaRadar
|
|
25
|
+
|
|
26
|
+
[](https://github.com/bartekmp/pharmaradar/actions/workflows/test.yml)
|
|
27
|
+
[](https://github.com/bartekmp/pharmaradar/actions/workflows/ci.yml)
|
|
28
|
+
|
|
29
|
+
Python package for searching and managing pharmacy medicine availability from [KtoMaLek.pl](https://ktomalek.pl).
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
Pharmaradar requires `chromium-browser`, `chromium-chromedriver` and `xvfb` to run, as the prerequisites for Selenium used to scrape the data from the KtoMaLek.pl page, as they do not provide an open API to get the data easily.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pharmaradar
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
To work with searches use the `Medicine` object, which represents a search query including all required details about what you're looking for.
|
|
43
|
+
If you'd like to find nearest pharmacies, that have at least low availability of Euthyrox N 50 medicine, nearby the location like Złota street in Warsaw and the max radius of 10 kilometers, create it like this:
|
|
44
|
+
```python
|
|
45
|
+
import pharmaradar
|
|
46
|
+
|
|
47
|
+
medicine = pharmaradar.Medicine(
|
|
48
|
+
name="Euthyrox N 50",
|
|
49
|
+
dosage="50 mcg",
|
|
50
|
+
location="Warszawa, Złota",
|
|
51
|
+
radius_km=10.0,
|
|
52
|
+
min_availability=AvailabilityLevel.LOW,
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Now create an instance of `MedicineFinder` class:
|
|
57
|
+
```python
|
|
58
|
+
finder = pharmaradar.MedicineFinder()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then test if the connection to KtoMaLek.pl is possible and search for given medicine:
|
|
62
|
+
```python
|
|
63
|
+
if finder.test_connection():
|
|
64
|
+
pharmacies = finder.search_medicine(medicine)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the search was successful, the `pharmacies` will contain a list of `PharmacyInfo` objects, with all important data found on the page:
|
|
68
|
+
```python
|
|
69
|
+
for pharmacy in pharmacies:
|
|
70
|
+
print(f"Pharmacy Name: {pharmacy.name}")
|
|
71
|
+
print(f"Address: {pharmacy.address}")
|
|
72
|
+
print(f"Availability: {pharmacy.availability}")
|
|
73
|
+
if pharmacy.price_full:
|
|
74
|
+
print(f"Price: {pharmacy.price_full} zł")
|
|
75
|
+
if pharmacy.distance_km:
|
|
76
|
+
print(f"Distance: {pharmacy.distance_km} km")
|
|
77
|
+
if pharmacy.reservation_url:
|
|
78
|
+
print(f"Reservation URL: {pharmacy.reservation_url}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Medicine watchdog
|
|
82
|
+
|
|
83
|
+
`MedicineWatchdog` is a class useful in async and continuous tasks. It implements certain methods, like `add_medicine`, `update_medicine`, `remove_medicine`, `get_medicine`, etc. that interact with the database layer, which is responsible for operating on the actual database. It can be used to create an automated bot, which periodically will retrieve the medicine quieries using `get_all_medicines` method, and then will perform searching and notifying.
|
|
84
|
+
```python
|
|
85
|
+
import sqlite3
|
|
86
|
+
from time import sleep
|
|
87
|
+
|
|
88
|
+
sql_db_client = SqliteInterface("my_database.db")
|
|
89
|
+
watchdog = pharmaradar.MedicineWatchdog(db_client)
|
|
90
|
+
|
|
91
|
+
while True:
|
|
92
|
+
|
|
93
|
+
all_medicines: list[Medicine] = watchdog.get_all_medicines()
|
|
94
|
+
for medicine in all_medicines:
|
|
95
|
+
|
|
96
|
+
print(f"Medicine: {medicine.name}")
|
|
97
|
+
|
|
98
|
+
found_pharmacies_for_medicine: list[PharmacyInfo] = await watchdog.search_medicine(medicine)
|
|
99
|
+
|
|
100
|
+
if found_pharmacies_for_medicine:
|
|
101
|
+
|
|
102
|
+
print(f"Found {len(found_pharmacies_for_medicine)}")
|
|
103
|
+
|
|
104
|
+
for p in found_pharmacies_for_medicine:
|
|
105
|
+
print(str(p))
|
|
106
|
+
else:
|
|
107
|
+
print(f"Medicine not available in pharmacies located in {medicine.distance_km} kilometer distance")
|
|
108
|
+
|
|
109
|
+
sleep(60) # 1 minute
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Database interface
|
|
113
|
+
The database interface instance passed to `MedicineWatchdog` must implement `MedicineDatabaseInterface`, which is basically a CRUD interface. The watchdog object will use this interface to interact with the data in the table. Example for an implementation for `sqlite` database:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pharmaradar import Medicine, MedicineDatabaseInterface
|
|
117
|
+
|
|
118
|
+
class SqliteInterface(MedicineDatabaseInterface):
|
|
119
|
+
def __init__(self, db_file_path: str):
|
|
120
|
+
self.conn = sqlite3.connect(db_path)
|
|
121
|
+
self.cur = self.conn.cursor()
|
|
122
|
+
|
|
123
|
+
def _parse_row_to_medicine(self, row: tuple) -> Medicine:
|
|
124
|
+
"""Convert a database row to a Medicine object."""
|
|
125
|
+
medicine_data = {
|
|
126
|
+
"id": row[0],
|
|
127
|
+
"name": row[1],
|
|
128
|
+
"dosage": row[2],
|
|
129
|
+
"amount": row[3],
|
|
130
|
+
"location": row[4],
|
|
131
|
+
"radius_km": row[5],
|
|
132
|
+
"max_price": row[6],
|
|
133
|
+
"min_availability": row[7],
|
|
134
|
+
"title": row[8],
|
|
135
|
+
"created_at": datetime.datetime.fromisoformat(row[9]) if row[9] else None,
|
|
136
|
+
"last_search_at": datetime.datetime.fromisoformat(row[10]) if row[10] else None,
|
|
137
|
+
"active": row[11], # Default to True for existing records
|
|
138
|
+
}
|
|
139
|
+
return Medicine(**medicine_data)
|
|
140
|
+
|
|
141
|
+
def get_medicine(self, medicine_id: int) -> Medicine | None:
|
|
142
|
+
row = self.cur.execute("SELECT * FROM medicine WHERE id = ?", (medicine_id,)).fetchone()
|
|
143
|
+
if row is None:
|
|
144
|
+
return None
|
|
145
|
+
return self._parse_row_to_medicine(row)
|
|
146
|
+
|
|
147
|
+
def get_medicines(self) -> list[Medicine]:
|
|
148
|
+
rows = self.cur.execute("SELECT * FROM medicine").fetchall()
|
|
149
|
+
medicines = []
|
|
150
|
+
for medicine_row in res:
|
|
151
|
+
medicines.append(self._parse_row_to_medicine(medicine_row))
|
|
152
|
+
return medicines
|
|
153
|
+
|
|
154
|
+
def remove_medicine(self, medicine_id: int) -> bool:
|
|
155
|
+
with self.conn:
|
|
156
|
+
res = self.cur.execute("DELETE FROM medicine WHERE id = (?)", (medicine_id,))
|
|
157
|
+
return res.rowcount > 0
|
|
158
|
+
|
|
159
|
+
def save_medicine(self, medicine: Medicine) -> int:
|
|
160
|
+
with self.conn:
|
|
161
|
+
self.cur.execute(
|
|
162
|
+
"INSERT INTO medicine VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
163
|
+
(
|
|
164
|
+
medicine.name,
|
|
165
|
+
medicine.dosage,
|
|
166
|
+
medicine.amount,
|
|
167
|
+
medicine.location,
|
|
168
|
+
medicine.radius_km,
|
|
169
|
+
medicine.max_price,
|
|
170
|
+
medicine.min_availability.value,
|
|
171
|
+
medicine.title,
|
|
172
|
+
medicine.created_at.isoformat() if medicine.created_at else None,
|
|
173
|
+
medicine.last_search_at.isoformat() if medicine.last_search_at else None,
|
|
174
|
+
medicine.active,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
return self.cur.lastrowid or 0
|
|
178
|
+
|
|
179
|
+
def update_medicine(
|
|
180
|
+
self,
|
|
181
|
+
medicine_id: int,
|
|
182
|
+
*,
|
|
183
|
+
name: str | None = None,
|
|
184
|
+
dosage: str | None = None,
|
|
185
|
+
amount: str | None = None,
|
|
186
|
+
location: str | None = None,
|
|
187
|
+
radius_km: float | None = None,
|
|
188
|
+
max_price: float | None = None,
|
|
189
|
+
min_availability: str | None = None,
|
|
190
|
+
title: str | None = None,
|
|
191
|
+
last_search_at: datetime.datetime | None = None,
|
|
192
|
+
active: bool | None = None,
|
|
193
|
+
) -> bool:
|
|
194
|
+
sql = []
|
|
195
|
+
values = []
|
|
196
|
+
if name is not None:
|
|
197
|
+
sql.append("name = ?")
|
|
198
|
+
values.append(name)
|
|
199
|
+
if dosage is not None:
|
|
200
|
+
sql.append("dosage = ?")
|
|
201
|
+
values.append(dosage)
|
|
202
|
+
if amount is not None:
|
|
203
|
+
sql.append("amount = ?")
|
|
204
|
+
values.append(amount)
|
|
205
|
+
if location is not None:
|
|
206
|
+
sql.append("location = ?")
|
|
207
|
+
values.append(location)
|
|
208
|
+
if radius_km is not None:
|
|
209
|
+
sql.append("radius_km = ?")
|
|
210
|
+
values.append(radius_km)
|
|
211
|
+
if max_price is not None:
|
|
212
|
+
sql.append("max_price = ?")
|
|
213
|
+
values.append(max_price)
|
|
214
|
+
if min_availability is not None:
|
|
215
|
+
sql.append("min_availability = ?")
|
|
216
|
+
values.append(min_availability)
|
|
217
|
+
if title is not None:
|
|
218
|
+
sql.append("title = ?")
|
|
219
|
+
values.append(title)
|
|
220
|
+
if last_search_at is not None:
|
|
221
|
+
sql.append("last_search_at = ?")
|
|
222
|
+
values.append(last_search_at.isoformat())
|
|
223
|
+
if active is not None:
|
|
224
|
+
sql.append("active = ?")
|
|
225
|
+
values.append(active)
|
|
226
|
+
|
|
227
|
+
values.append(medicine_id)
|
|
228
|
+
sql = f"UPDATE medicine SET {', '.join(sql)} WHERE id = ?"
|
|
229
|
+
|
|
230
|
+
with self.conn:
|
|
231
|
+
result = self.cur.execute(sql, values)
|
|
232
|
+
return result.rowcount > 0
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Currently, the database itself must define the `medicine` table, declared as follows:
|
|
237
|
+
```sql
|
|
238
|
+
medicine(
|
|
239
|
+
id INTEGER PRIMARY KEY,
|
|
240
|
+
name TEXT NOT NULL,
|
|
241
|
+
dosage TEXT,
|
|
242
|
+
amount TEXT,
|
|
243
|
+
location TEXT NOT NULL,
|
|
244
|
+
radius_km REAL DEFAULT 10,
|
|
245
|
+
max_price REAL,
|
|
246
|
+
min_availability TEXT DEFAULT 'low',
|
|
247
|
+
title TEXT,
|
|
248
|
+
created_at TEXT,
|
|
249
|
+
last_search_at TEXT,
|
|
250
|
+
active BOOLEAN DEFAULT 1
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT License
|