appintel 1.0.2__tar.gz → 1.0.4__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.
- appintel-1.0.4/.gitignore +105 -0
- {appintel-1.0.2 → appintel-1.0.4}/PKG-INFO +26 -19
- appintel-1.0.4/appintel/__init__.py +3 -0
- appintel-1.0.4/appintel/appintel.py +206 -0
- appintel-1.0.4/classes.dot +4 -0
- appintel-1.0.4/pylintrc +528 -0
- appintel-1.0.4/pyproject.toml +70 -0
- appintel-1.0.4/requirements.txt +11 -0
- appintel-1.0.4/tests/test_appintel.py +43 -0
- appintel-1.0.4/uv.lock +697 -0
- appintel-1.0.2/appintel/__init__.py +0 -1
- appintel-1.0.2/appintel/appintel.py +0 -218
- appintel-1.0.2/pyproject.toml +0 -31
- {appintel-1.0.2 → appintel-1.0.4}/LICENSE +0 -0
- {appintel-1.0.2 → appintel-1.0.4}/README.md +0 -0
- {appintel-1.0.2 → appintel-1.0.4}/appintel/__main__.py +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
# Usually these files are written by a python script from a template
|
|
30
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Django stuff:
|
|
54
|
+
*.log
|
|
55
|
+
local_settings.py
|
|
56
|
+
|
|
57
|
+
# Flask stuff:
|
|
58
|
+
instance/
|
|
59
|
+
.webassets-cache
|
|
60
|
+
|
|
61
|
+
# Scrapy stuff:
|
|
62
|
+
.scrapy
|
|
63
|
+
|
|
64
|
+
# Sphinx documentation
|
|
65
|
+
docs/_build/
|
|
66
|
+
|
|
67
|
+
# PyBuilder
|
|
68
|
+
target/
|
|
69
|
+
|
|
70
|
+
# Jupyter Notebook
|
|
71
|
+
.ipynb_checkpoints
|
|
72
|
+
|
|
73
|
+
# pyenv
|
|
74
|
+
.python-version
|
|
75
|
+
|
|
76
|
+
# celery beat schedule file
|
|
77
|
+
celerybeat-schedule
|
|
78
|
+
|
|
79
|
+
# SageMath parsed files
|
|
80
|
+
*.sage.py
|
|
81
|
+
|
|
82
|
+
# dotenv
|
|
83
|
+
.env
|
|
84
|
+
|
|
85
|
+
# virtualenv
|
|
86
|
+
.venv
|
|
87
|
+
venv/
|
|
88
|
+
ENV/
|
|
89
|
+
|
|
90
|
+
# Spyder project settings
|
|
91
|
+
.spyderproject
|
|
92
|
+
.spyproject
|
|
93
|
+
|
|
94
|
+
# Rope project settings
|
|
95
|
+
.ropeproject
|
|
96
|
+
|
|
97
|
+
# mkdocs documentation
|
|
98
|
+
/site
|
|
99
|
+
|
|
100
|
+
# mypy
|
|
101
|
+
.mypy_cache/
|
|
102
|
+
|
|
103
|
+
# outputfiles
|
|
104
|
+
*.json
|
|
105
|
+
/itunizer/*.json
|
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: appintel
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: A command line tool to search and compare prices of apps and music on the Apple Store
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Project-URL: Homepage, https://github.com/james-see/appintel
|
|
6
|
+
Project-URL: Repository, https://github.com/james-see/appintel
|
|
7
|
+
Project-URL: Issues, https://github.com/james-see/appintel/issues
|
|
8
|
+
Author-email: James Campbell <james@jamescampbell.us>
|
|
9
|
+
Maintainer-email: James Campbell <james@jamescampbell.us>
|
|
10
|
+
License-Expression: GPL-3.0-or-later
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,apple-store,data-analysis,itunes,market-research,pricing
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Requires-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: six (>=1.16.0,<2.0.0)
|
|
22
|
-
Requires-Dist: urllib3 (>=2.2.1,<3.0.0)
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
31
|
Description-Content-Type: text/markdown
|
|
24
32
|
|
|
25
33
|
# appintel
|
|
@@ -68,4 +76,3 @@ It is difficult to get nice structured data from Apple. This helps with that.
|
|
|
68
76
|
## Example search
|
|
69
77
|
|
|
70
78
|

|
|
71
|
-
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""A command line tool to search and compare prices of apps and music on the Apple Store."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pprint import pprint
|
|
8
|
+
from statistics import mean, median
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.4"
|
|
13
|
+
|
|
14
|
+
logo = """
|
|
15
|
+
+-------------------------------------+
|
|
16
|
+
| --\\ |
|
|
17
|
+
| / -\\ |
|
|
18
|
+
| / -\\ |
|
|
19
|
+
| / - |
|
|
20
|
+
| \\ / |
|
|
21
|
+
| \\ / |
|
|
22
|
+
| \\ -/ |
|
|
23
|
+
| -\\ / |
|
|
24
|
+
| \\ / |
|
|
25
|
+
| \\ / AppIntel |
|
|
26
|
+
| \\ / |
|
|
27
|
+
| -/ |
|
|
28
|
+
+-------------------------------------+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
ITUNES_URL_ENDPOINT = "https://itunes.apple.com/search?term={}&country={}&entity={}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_content(
|
|
35
|
+
search_term: str, store_country: str = "us", category: str = "software"
|
|
36
|
+
) -> requests.Response:
|
|
37
|
+
"""Fetch data from the iTunes search endpoint."""
|
|
38
|
+
url = (
|
|
39
|
+
f"https://itunes.apple.com/search?"
|
|
40
|
+
f"term={search_term}&country={store_country}&entity={category}"
|
|
41
|
+
)
|
|
42
|
+
return requests.get(url, timeout=10)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _prices(jsondata: dict) -> list[float]:
|
|
46
|
+
return [float(item.get("price")) for item in jsondata.get("results", []) if "price" in item]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_mean(jsondata: dict) -> float:
|
|
50
|
+
prices = _prices(jsondata)
|
|
51
|
+
if not prices:
|
|
52
|
+
return 0.0
|
|
53
|
+
if len(prices) > 1:
|
|
54
|
+
return mean(prices)
|
|
55
|
+
return prices[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_max(jsondata: dict) -> float:
|
|
59
|
+
prices = _prices(jsondata)
|
|
60
|
+
if not prices:
|
|
61
|
+
return 0.0
|
|
62
|
+
if len(prices) > 1:
|
|
63
|
+
return max(prices)
|
|
64
|
+
return prices[0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_min(jsondata: dict) -> float:
|
|
68
|
+
prices = _prices(jsondata)
|
|
69
|
+
if not prices:
|
|
70
|
+
return 0.0
|
|
71
|
+
if len(prices) > 1:
|
|
72
|
+
return min(prices)
|
|
73
|
+
return prices[0]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_median(jsondata: dict) -> float:
|
|
77
|
+
prices = _prices(jsondata)
|
|
78
|
+
if not prices:
|
|
79
|
+
return 0.0
|
|
80
|
+
if len(prices) > 1:
|
|
81
|
+
return median(prices)
|
|
82
|
+
return prices[0]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def format_table(results: list[dict]) -> str:
|
|
86
|
+
if not results:
|
|
87
|
+
return "(no results)"
|
|
88
|
+
values = [
|
|
89
|
+
(str(item.get("price")), str(item.get("artistName")), str(item.get("trackName")))
|
|
90
|
+
for item in results
|
|
91
|
+
]
|
|
92
|
+
header = ("price", "artistName", "trackName")
|
|
93
|
+
rows = [header, *values]
|
|
94
|
+
widths = [max(len(row[index]) for row in rows) for index in range(3)]
|
|
95
|
+
lines = [" | ".join(field.ljust(widths[index]) for index, field in enumerate(header))]
|
|
96
|
+
lines.append("-+-".join("-" * widths[index] for index in range(3)))
|
|
97
|
+
lines.extend(
|
|
98
|
+
" | ".join(field.ljust(widths[index]) for index, field in enumerate(row)) for row in values
|
|
99
|
+
)
|
|
100
|
+
return "\n".join(lines)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
104
|
+
parser = argparse.ArgumentParser(
|
|
105
|
+
description="Collect and summarize data from the Apple iTunes Store",
|
|
106
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"-s",
|
|
110
|
+
"--search",
|
|
111
|
+
dest="search_term",
|
|
112
|
+
default="nginx",
|
|
113
|
+
help="search term for the iTunes store",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"-c",
|
|
117
|
+
"--category",
|
|
118
|
+
dest="category_location",
|
|
119
|
+
default="software",
|
|
120
|
+
help="store category to search",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"-p",
|
|
124
|
+
"--print",
|
|
125
|
+
dest="print_me",
|
|
126
|
+
action="store_true",
|
|
127
|
+
default=False,
|
|
128
|
+
help="print raw search results",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"-n",
|
|
132
|
+
"--no-logo",
|
|
133
|
+
dest="logo_off",
|
|
134
|
+
action="store_true",
|
|
135
|
+
default=False,
|
|
136
|
+
help="disable the AppIntel logo",
|
|
137
|
+
)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"-t",
|
|
140
|
+
"--table",
|
|
141
|
+
dest="output_table",
|
|
142
|
+
action="store_true",
|
|
143
|
+
default=False,
|
|
144
|
+
help="print a compact table instead of JSON",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--country",
|
|
148
|
+
dest="store_country",
|
|
149
|
+
default="us",
|
|
150
|
+
help="store country to search",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--version",
|
|
154
|
+
"-v",
|
|
155
|
+
dest="app_version",
|
|
156
|
+
action="store_true",
|
|
157
|
+
default=False,
|
|
158
|
+
help="print the AppIntel version",
|
|
159
|
+
)
|
|
160
|
+
return parser
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main(argv: list[str] | None = None) -> int:
|
|
164
|
+
args = build_parser().parse_args(argv)
|
|
165
|
+
|
|
166
|
+
if not args.logo_off:
|
|
167
|
+
print(logo)
|
|
168
|
+
if args.app_version:
|
|
169
|
+
print(f"Current version: {__version__}")
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
response = get_content(args.search_term, args.store_country, args.category_location)
|
|
173
|
+
jsondata = response.json()
|
|
174
|
+
|
|
175
|
+
if args.print_me:
|
|
176
|
+
pprint(jsondata)
|
|
177
|
+
for key in jsondata.get("results", [{}])[0]:
|
|
178
|
+
print(f"{key}:")
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
average_price = get_mean(jsondata)
|
|
182
|
+
max_price = get_max(jsondata)
|
|
183
|
+
min_price = get_min(jsondata)
|
|
184
|
+
median_price = get_median(jsondata)
|
|
185
|
+
|
|
186
|
+
print("Results of the query")
|
|
187
|
+
print("*****" * 5)
|
|
188
|
+
print(
|
|
189
|
+
f"The average price of the \033[94m{jsondata.get('resultCount', 0)}\033[0m items matching search term "
|
|
190
|
+
f"\033[92m{args.search_term}\033[0m: ${average_price:.2f}, the median is \033[94m{median_price:.2f}\033[0m, "
|
|
191
|
+
f"the min is \033[94m{min_price:.2f}\033[0m, and the max is \033[94m{max_price:.2f}\033[0m"
|
|
192
|
+
)
|
|
193
|
+
print("")
|
|
194
|
+
|
|
195
|
+
if args.output_table:
|
|
196
|
+
print(format_table(jsondata.get("results", [])))
|
|
197
|
+
else:
|
|
198
|
+
filename = f"{args.search_term}.json"
|
|
199
|
+
with open(filename, "w", encoding="utf-8") as file:
|
|
200
|
+
json.dump(response.json(), file, indent=2)
|
|
201
|
+
print(f"file saved as {filename}")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
raise SystemExit(main())
|