pytrends-ng 0.1.0__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.
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytrends-ng
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in replacement for the archived pytrends: Google Trends without 429 errors, unlimited keywords on one scale.
|
|
5
|
+
Project-URL: Google Trends actor, https://apify.com/githubanassk/google-trends-scraper
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: google trends,google trends api,keyword research,pytrends,seo,trends
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Requires-Dist: pandas>=1.1
|
|
13
|
+
Requires-Dist: requests>=2.20
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pytrends-ng: pytrends without the 429 errors
|
|
17
|
+
|
|
18
|
+
[pytrends](https://github.com/GeneralMills/pytrends) was archived in April 2025 and its last release is from 2023. Google answers it with `429 Too Many Requests` most of the time. `pytrends-ng` keeps the same calls, so your code works again after changing one import:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# from pytrends.request import TrendReq
|
|
22
|
+
from pytrends_ng import TrendReq
|
|
23
|
+
|
|
24
|
+
pytrends = TrendReq() # reads APIFY_TOKEN from the environment
|
|
25
|
+
pytrends.build_payload(["chatgpt", "claude", "gemini"], timeframe="today 3-m", geo="US")
|
|
26
|
+
|
|
27
|
+
pytrends.interest_over_time() # DataFrame: date index, one column per keyword, isPartial
|
|
28
|
+
pytrends.interest_by_region() # DataFrame: one row per region
|
|
29
|
+
pytrends.related_queries() # {keyword: {"top": DataFrame, "rising": DataFrame}}
|
|
30
|
+
pytrends.trending_searches(pn="united_states")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
The requests run on a hosted [Google Trends actor](https://apify.com/githubanassk/google-trends-scraper) on Apify, which rotates IPs and retries rate limits for you. No proxies to buy, no sleep loops.
|
|
36
|
+
|
|
37
|
+
1. Create a free Apify account. It includes $5 of usage every month.
|
|
38
|
+
2. Copy your API token from [Settings > Integrations](https://console.apify.com/settings/integrations).
|
|
39
|
+
3. `pip install pytrends-ng` and `export APIFY_TOKEN=...`
|
|
40
|
+
|
|
41
|
+
Pricing is $2 per 1,000 results, where one result is one keyword in one country with its full timeline, regions and related queries. A 5-keyword payload costs $0.01, so the free credit covers about 2,500 keywords a month.
|
|
42
|
+
|
|
43
|
+
## Better than the original
|
|
44
|
+
|
|
45
|
+
- **More than 5 keywords.** `build_payload` takes any number of keywords and puts them all on one shared 0-100 scale. Google's own limit is 5.
|
|
46
|
+
- **Small keywords stay accurate.** Keywords far smaller than the biggest one keep decimal values instead of rounding to 0.
|
|
47
|
+
- **One request per payload.** `interest_over_time`, `interest_by_region` and `related_queries` all read from the same run.
|
|
48
|
+
|
|
49
|
+
## Supported calls
|
|
50
|
+
|
|
51
|
+
| pytrends call | Status |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `build_payload(kw_list, cat, timeframe, geo, gprop)` | Yes |
|
|
54
|
+
| `interest_over_time()` | Yes |
|
|
55
|
+
| `interest_by_region(resolution, inc_low_vol, inc_geo_code)` | COUNTRY, REGION, DMA (not CITY) |
|
|
56
|
+
| `related_queries()` | Yes. `value` is numeric; `formattedValue` keeps Google's text such as `Breakout` |
|
|
57
|
+
| `trending_searches(pn)`, `today_searches(pn)`, `realtime_trending_searches(pn)` | Yes |
|
|
58
|
+
| `related_topics()`, `suggestions()` | Not yet |
|
|
59
|
+
|
|
60
|
+
Constructor arguments such as `hl`, `tz`, `proxies` and `retries` are accepted and ignored, so existing code does not break.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT. Not affiliated with Google or the original pytrends authors.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# pytrends-ng: pytrends without the 429 errors
|
|
2
|
+
|
|
3
|
+
[pytrends](https://github.com/GeneralMills/pytrends) was archived in April 2025 and its last release is from 2023. Google answers it with `429 Too Many Requests` most of the time. `pytrends-ng` keeps the same calls, so your code works again after changing one import:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# from pytrends.request import TrendReq
|
|
7
|
+
from pytrends_ng import TrendReq
|
|
8
|
+
|
|
9
|
+
pytrends = TrendReq() # reads APIFY_TOKEN from the environment
|
|
10
|
+
pytrends.build_payload(["chatgpt", "claude", "gemini"], timeframe="today 3-m", geo="US")
|
|
11
|
+
|
|
12
|
+
pytrends.interest_over_time() # DataFrame: date index, one column per keyword, isPartial
|
|
13
|
+
pytrends.interest_by_region() # DataFrame: one row per region
|
|
14
|
+
pytrends.related_queries() # {keyword: {"top": DataFrame, "rising": DataFrame}}
|
|
15
|
+
pytrends.trending_searches(pn="united_states")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
The requests run on a hosted [Google Trends actor](https://apify.com/githubanassk/google-trends-scraper) on Apify, which rotates IPs and retries rate limits for you. No proxies to buy, no sleep loops.
|
|
21
|
+
|
|
22
|
+
1. Create a free Apify account. It includes $5 of usage every month.
|
|
23
|
+
2. Copy your API token from [Settings > Integrations](https://console.apify.com/settings/integrations).
|
|
24
|
+
3. `pip install pytrends-ng` and `export APIFY_TOKEN=...`
|
|
25
|
+
|
|
26
|
+
Pricing is $2 per 1,000 results, where one result is one keyword in one country with its full timeline, regions and related queries. A 5-keyword payload costs $0.01, so the free credit covers about 2,500 keywords a month.
|
|
27
|
+
|
|
28
|
+
## Better than the original
|
|
29
|
+
|
|
30
|
+
- **More than 5 keywords.** `build_payload` takes any number of keywords and puts them all on one shared 0-100 scale. Google's own limit is 5.
|
|
31
|
+
- **Small keywords stay accurate.** Keywords far smaller than the biggest one keep decimal values instead of rounding to 0.
|
|
32
|
+
- **One request per payload.** `interest_over_time`, `interest_by_region` and `related_queries` all read from the same run.
|
|
33
|
+
|
|
34
|
+
## Supported calls
|
|
35
|
+
|
|
36
|
+
| pytrends call | Status |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `build_payload(kw_list, cat, timeframe, geo, gprop)` | Yes |
|
|
39
|
+
| `interest_over_time()` | Yes |
|
|
40
|
+
| `interest_by_region(resolution, inc_low_vol, inc_geo_code)` | COUNTRY, REGION, DMA (not CITY) |
|
|
41
|
+
| `related_queries()` | Yes. `value` is numeric; `formattedValue` keeps Google's text such as `Breakout` |
|
|
42
|
+
| `trending_searches(pn)`, `today_searches(pn)`, `realtime_trending_searches(pn)` | Yes |
|
|
43
|
+
| `related_topics()`, `suggestions()` | Not yet |
|
|
44
|
+
|
|
45
|
+
Constructor arguments such as `hl`, `tz`, `proxies` and `retries` are accepted and ignored, so existing code does not break.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT. Not affiliated with Google or the original pytrends authors.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytrends-ng"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Drop-in replacement for the archived pytrends: Google Trends without 429 errors, unlimited keywords on one scale."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["pandas>=1.1", "requests>=2.20"]
|
|
13
|
+
keywords = ["pytrends", "google trends", "google trends api", "trends", "seo", "keyword research"]
|
|
14
|
+
classifiers = ["Programming Language :: Python :: 3", "Topic :: Internet :: WWW/HTTP", "Intended Audience :: Developers"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
"Google Trends actor" = "https://apify.com/githubanassk/google-trends-scraper"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.version]
|
|
20
|
+
path = "pytrends_ng/__init__.py"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["pytrends_ng"]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Drop-in replacement for the archived pytrends library.
|
|
2
|
+
|
|
3
|
+
Same calls as pytrends (TrendReq, build_payload, interest_over_time, ...), but the
|
|
4
|
+
data comes from a hosted Google Trends actor on Apify, so there are no 429 errors
|
|
5
|
+
to handle on your side. Needs an Apify API token (free account, $5 monthly credit).
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__all__ = ["TrendReq", "TrendsError"]
|
|
15
|
+
|
|
16
|
+
ACTOR = "githubanassk~google-trends-scraper"
|
|
17
|
+
API = "https://api.apify.com/v2/acts/{actor}/run-sync-get-dataset-items"
|
|
18
|
+
|
|
19
|
+
# pytrends' trending_searches(pn=...) takes country names; the actor takes ISO codes.
|
|
20
|
+
# ponytail: common countries only; any two-letter code also works.
|
|
21
|
+
COUNTRY_CODES = {
|
|
22
|
+
"united_states": "US", "united_kingdom": "GB", "canada": "CA", "australia": "AU",
|
|
23
|
+
"india": "IN", "germany": "DE", "france": "FR", "japan": "JP", "brazil": "BR",
|
|
24
|
+
"mexico": "MX", "spain": "ES", "italy": "IT", "netherlands": "NL", "south_korea": "KR",
|
|
25
|
+
"indonesia": "ID", "turkey": "TR", "poland": "PL", "south_africa": "ZA", "nigeria": "NG",
|
|
26
|
+
"philippines": "PH", "singapore": "SG", "sweden": "SE", "argentina": "AR",
|
|
27
|
+
}
|
|
28
|
+
GPROP = {"": "", "images": "images", "news": "news", "youtube": "youtube", "froogle": "froogle"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TrendsError(Exception):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _number(formatted):
|
|
36
|
+
"""'+3,800%' -> 3800, '100' -> 100, 'Breakout' -> None (see formattedValue)."""
|
|
37
|
+
digits = re.sub(r"[^\d]", "", str(formatted))
|
|
38
|
+
return int(digits) if digits else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TrendReq:
|
|
42
|
+
def __init__(self, hl="en-US", tz=360, geo="", timeout=None, proxies=None, retries=0,
|
|
43
|
+
backoff_factor=0, requests_args=None, apify_token=None, actor=ACTOR):
|
|
44
|
+
# hl, tz, timeout, proxies, retries, backoff_factor and requests_args are
|
|
45
|
+
# accepted for pytrends compatibility; the actor handles proxies and retries.
|
|
46
|
+
token = apify_token or os.environ.get("APIFY_TOKEN")
|
|
47
|
+
if not token:
|
|
48
|
+
raise TrendsError("Set APIFY_TOKEN or pass apify_token=... "
|
|
49
|
+
"(free at https://console.apify.com/settings/integrations).")
|
|
50
|
+
self._token, self._url = token, API.format(actor=actor.replace("/", "~"))
|
|
51
|
+
self._timeout = timeout if isinstance(timeout, (int, float)) else 300
|
|
52
|
+
self.geo = geo
|
|
53
|
+
self.kw_list, self.cat, self.timeframe, self.gprop = [], 0, "today 5-y", ""
|
|
54
|
+
self._runs = {}
|
|
55
|
+
|
|
56
|
+
def build_payload(self, kw_list, cat=0, timeframe="today 5-y", geo="", gprop=""):
|
|
57
|
+
if gprop not in GPROP:
|
|
58
|
+
raise ValueError("gprop must be empty or one of images, news, youtube, froogle")
|
|
59
|
+
self.kw_list, self.cat, self.timeframe, self.gprop = list(kw_list), cat, timeframe, gprop
|
|
60
|
+
self.geo = geo or self.geo
|
|
61
|
+
self._runs = {} # new payload, new data
|
|
62
|
+
|
|
63
|
+
def _run(self, run_input):
|
|
64
|
+
"""One HTTP call: start the actor, wait for it, return its rows."""
|
|
65
|
+
resp = requests.post(self._url, json=run_input, timeout=self._timeout,
|
|
66
|
+
headers={"Authorization": f"Bearer {self._token}"})
|
|
67
|
+
if resp.status_code >= 300:
|
|
68
|
+
try:
|
|
69
|
+
msg = resp.json()["error"]["message"]
|
|
70
|
+
except (ValueError, KeyError, TypeError):
|
|
71
|
+
msg = resp.text[:300]
|
|
72
|
+
raise TrendsError(f"Google Trends request failed ({resp.status_code}): {msg}")
|
|
73
|
+
return resp.json()
|
|
74
|
+
|
|
75
|
+
def _keywords(self, region_level=""):
|
|
76
|
+
"""One actor run per payload (and per region level); every method reads from it."""
|
|
77
|
+
if not self.kw_list:
|
|
78
|
+
raise TrendsError("Call build_payload(kw_list=[...]) first.")
|
|
79
|
+
if region_level not in self._runs:
|
|
80
|
+
rows = self._run({
|
|
81
|
+
"keywords": self.kw_list, "geos": [self.geo] if self.geo else [],
|
|
82
|
+
"timeframe": self.timeframe, "category": self.cat,
|
|
83
|
+
"searchProperty": self.gprop, "regionLevel": region_level,
|
|
84
|
+
"includeRelatedQueries": True,
|
|
85
|
+
})
|
|
86
|
+
self._runs[region_level] = {r["keyword"]: r for r in rows if r.get("type") == "keyword"}
|
|
87
|
+
return self._runs[region_level]
|
|
88
|
+
|
|
89
|
+
def interest_over_time(self):
|
|
90
|
+
"""DataFrame indexed by date, one column per keyword (shared 0-100 scale), plus isPartial."""
|
|
91
|
+
rows = self._keywords()
|
|
92
|
+
df = pd.DataFrame({
|
|
93
|
+
kw: pd.Series({pd.Timestamp(p["date"]).tz_localize(None): p["value"] for p in rows[kw]["timeline"]})
|
|
94
|
+
for kw in self.kw_list if kw in rows
|
|
95
|
+
})
|
|
96
|
+
if df.empty:
|
|
97
|
+
return df
|
|
98
|
+
df.index.name = "date"
|
|
99
|
+
df["isPartial"] = False
|
|
100
|
+
return df
|
|
101
|
+
|
|
102
|
+
def interest_by_region(self, resolution="COUNTRY", inc_low_vol=True, inc_geo_code=False):
|
|
103
|
+
# Worldwide + COUNTRY is the actor's automatic level; a country geo needs an explicit level.
|
|
104
|
+
level = "" if resolution == "COUNTRY" and not self.geo else resolution.upper()
|
|
105
|
+
if level == "CITY":
|
|
106
|
+
raise TrendsError("City resolution is not supported; use REGION or DMA.")
|
|
107
|
+
rows = self._keywords(level)
|
|
108
|
+
frames = {}
|
|
109
|
+
for kw in self.kw_list:
|
|
110
|
+
regions = (rows.get(kw) or {}).get("interestByRegion") or []
|
|
111
|
+
frames[kw] = pd.Series({r["geoName"]: r["value"] for r in regions})
|
|
112
|
+
df = pd.DataFrame(frames)
|
|
113
|
+
df.index.name = "geoName"
|
|
114
|
+
if not inc_low_vol:
|
|
115
|
+
df = df[(df.fillna(0) > 0).any(axis=1)]
|
|
116
|
+
if inc_geo_code:
|
|
117
|
+
codes = {r["geoName"]: r["geoCode"] for kw in self.kw_list
|
|
118
|
+
for r in (rows.get(kw) or {}).get("interestByRegion") or []}
|
|
119
|
+
df.insert(0, "geoCode", df.index.map(codes))
|
|
120
|
+
return df
|
|
121
|
+
|
|
122
|
+
def related_queries(self):
|
|
123
|
+
"""{keyword: {"top": DataFrame, "rising": DataFrame}} like pytrends."""
|
|
124
|
+
rows = self._keywords()
|
|
125
|
+
out = {}
|
|
126
|
+
for kw in self.kw_list:
|
|
127
|
+
r = rows.get(kw) or {}
|
|
128
|
+
out[kw] = {}
|
|
129
|
+
for key, field in (("top", "relatedQueriesTop"), ("rising", "relatedQueriesRising")):
|
|
130
|
+
items = r.get(field) or []
|
|
131
|
+
out[kw][key] = pd.DataFrame(
|
|
132
|
+
[{"query": q["query"], "value": _number(q["value"]), "formattedValue": q["value"]} for q in items],
|
|
133
|
+
columns=["query", "value", "formattedValue"],
|
|
134
|
+
) if items else None
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
def trending_searches(self, pn="united_states"):
|
|
138
|
+
"""DataFrame with one column (0) of trending search titles, like pytrends."""
|
|
139
|
+
df = self.realtime_trending_searches(pn)
|
|
140
|
+
return pd.DataFrame({0: df["title"]}) if not df.empty else pd.DataFrame({0: []})
|
|
141
|
+
|
|
142
|
+
def realtime_trending_searches(self, pn="US", cat="all", count=300):
|
|
143
|
+
code = pn.upper() if len(pn) == 2 else COUNTRY_CODES.get(pn.lower())
|
|
144
|
+
if not code:
|
|
145
|
+
raise ValueError(f"Unknown country '{pn}'. Use a two-letter code such as US or GB.")
|
|
146
|
+
rows = [r for r in self._run({"trendingNowGeos": [code]}) if r.get("type") == "trendingNow"]
|
|
147
|
+
return pd.DataFrame(rows[:count])
|
|
148
|
+
|
|
149
|
+
today_searches = trending_searches
|
|
150
|
+
|
|
151
|
+
def related_topics(self):
|
|
152
|
+
raise TrendsError("related_topics is not available yet; related_queries is.")
|
|
153
|
+
|
|
154
|
+
def suggestions(self, keyword):
|
|
155
|
+
raise TrendsError("suggestions is not available yet.")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Offline check: fake actor rows in, pytrends-shaped DataFrames out."""
|
|
2
|
+
import pytrends_ng
|
|
3
|
+
from pytrends_ng import TrendReq, _number
|
|
4
|
+
|
|
5
|
+
ROWS = [
|
|
6
|
+
{"type": "keyword", "keyword": "claude",
|
|
7
|
+
"timeline": [{"date": "2026-07-10T00:00:00Z", "value": 23}, {"date": "2026-07-11T00:00:00Z", "value": 20.5}],
|
|
8
|
+
"interestByRegion": [{"geoCode": "US-CA", "geoName": "California", "value": 100},
|
|
9
|
+
{"geoCode": "US-TX", "geoName": "Texas", "value": 0}],
|
|
10
|
+
"relatedQueriesTop": [{"query": "claude ai", "value": "100"}],
|
|
11
|
+
"relatedQueriesRising": [{"query": "claude 5", "value": "+3,800%"}, {"query": "claude code", "value": "Breakout"}]},
|
|
12
|
+
{"type": "keyword", "keyword": "gemini",
|
|
13
|
+
"timeline": [{"date": "2026-07-10T00:00:00Z", "value": 40}, {"date": "2026-07-11T00:00:00Z", "value": 37}],
|
|
14
|
+
"interestByRegion": [{"geoCode": "US-CA", "geoName": "California", "value": 80}],
|
|
15
|
+
"relatedQueriesTop": [], "relatedQueriesRising": []},
|
|
16
|
+
{"type": "trendingNow", "geo": "US", "title": "chicago marathon", "approxTraffic": "2000+"},
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make(calls):
|
|
21
|
+
t = TrendReq(apify_token="x")
|
|
22
|
+
|
|
23
|
+
def fake_run(run_input):
|
|
24
|
+
calls.append(run_input)
|
|
25
|
+
return ROWS
|
|
26
|
+
t._run = fake_run
|
|
27
|
+
return t
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_pytrends_shapes():
|
|
31
|
+
calls = []
|
|
32
|
+
t = make(calls)
|
|
33
|
+
t.build_payload(["claude", "gemini"], timeframe="today 3-m", geo="US")
|
|
34
|
+
|
|
35
|
+
iot = t.interest_over_time()
|
|
36
|
+
assert list(iot.columns) == ["claude", "gemini", "isPartial"]
|
|
37
|
+
assert iot.index.name == "date" and iot["claude"].iloc[1] == 20.5
|
|
38
|
+
|
|
39
|
+
rq = t.related_queries()
|
|
40
|
+
assert rq["claude"]["rising"]["value"].tolist()[0] == 3800
|
|
41
|
+
assert rq["claude"]["rising"]["formattedValue"].tolist()[1] == "Breakout"
|
|
42
|
+
assert rq["gemini"]["top"] is None
|
|
43
|
+
assert len(calls) == 1 # one run serves every method for the same payload
|
|
44
|
+
|
|
45
|
+
ibr = t.interest_by_region(resolution="REGION", inc_low_vol=False, inc_geo_code=True)
|
|
46
|
+
assert calls[-1]["regionLevel"] == "REGION" and len(calls) == 2
|
|
47
|
+
assert list(ibr.index) == ["California"] and ibr.loc["California", "geoCode"] == "US-CA"
|
|
48
|
+
|
|
49
|
+
assert t.trending_searches(pn="united_states")[0].tolist() == ["chicago marathon"]
|
|
50
|
+
assert calls[-1] == {"trendingNowGeos": ["US"]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_number():
|
|
54
|
+
assert _number("+1,650%") == 1650 and _number("Breakout") is None and _number("81") == 81
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
test_pytrends_shapes(); test_number(); print("ok", pytrends_ng.__version__)
|