Browse Source

NoMdns, Logs and script

log
Laurent Kislaire 8 months ago
parent
commit
6904c0baa9
9 changed files with 150 additions and 18 deletions
  1. 1
    0
      .gitignore
  2. 2
    0
      .gitlab-ci.yml
  3. 60
    6
      README.md
  4. 51
    0
      pyfbx/__main__.py
  5. 27
    9
      pyfbx/client.py
  6. 6
    2
      pyfbx/mdns.py
  7. 1
    1
      setup.cfg
  8. 1
    0
      setup.py
  9. 1
    0
      tests/sanity_test.py

+ 1
- 0
.gitignore View File

@@ -1,5 +1,6 @@
gen_api/
tests/token.txt
token.txt
.buildlog
.dock
.pypt/gh-token

+ 2
- 0
.gitlab-ci.yml View File

@@ -18,6 +18,7 @@ cache:
job_build:
stage: build
only:
- dev
- master
script:
- python setup.py build
@@ -26,6 +27,7 @@ job_build:
job_test:
stage: test
only:
- dev
- master
dependencies:
- job_build

+ 60
- 6
README.md View File

@@ -4,19 +4,71 @@

* Full API coverage
* Https and Mdns discovery
* Thin client wrapper (~150 lines)
* Thin client wrapper library (~250 lines)
* CLI script example to access any command

[![pipeline status](https://framagit.org/sun/pyfbx/badges/master/pipeline.svg)](https://framagit.org/sun/pyfbx/commits/master)
[![coverage report](https://framagit.org/sun/pyfbx/badges/master/coverage.svg)](https://framagit.org/sun/pyfbx/commits/master)


## Usage
## Script
A script called pyfbx is installed and can be used like this:

To register the application:

```shell
pyfbx SuperAppId
```
Once registration is done on the device, write down the application token.

Using this app token, acquire a session token to execute a test command

```shell
pyfbx -t '<TOKEN>' SuperAppId
```

You can also store the token in a file (example token.txt) and also execute a specific command

```shell
pyfbx -t f:/home/foo/token.txt SuperAppId -c 'Contacts.Create_a_contact(post_data={"display_name": "Sandy Kilo", "first_name": "Sandy", "last_name":"Kilo"})'
{ 'birthday': '',
'company': '',
'display_name': 'Sandy Kilo',
'first_name': 'Sandy',
'id': 17,
'last_name': 'Kilo',
'last_update': 1554378898,
'notes': '',
'photo_url': ''}
```
_Note_ : Don't forget to escape token and command.

```shell
pyfbx -h
usage: pyfbx [-h] [-c COMMAND] [-t TOKEN] [-v] [-n] app_id

positional arguments:
app_id application identifier

optional arguments:
-h, --help show this help message and exit
-c COMMAND, --command COMMAND
command, defaults to
System.Get_the_current_system_info()
-t TOKEN, --token TOKEN
token (or f:<filename>)
-v, --verbose increase verbosity to INFO, use twice for DEBUG
-n, --http Disable MDNS and use http known address
```

## Library usage

The REST API is generated at runtime with the creation of class attributes for all Freebox subsystems.

```python
>>> from pyfbx import Fbx
>>> f=Fbx()
>>> f=Fbx() # By default, this will perform MDNS discovery
>>> f=Fbx(nomdns=True) # Use faster http api discovery
>>> f.<TAB>
f.Airmedia f.Download_Config f.Lan f.Rrd f.Upnpav
f.Call f.Download_Feeds f.Lcd f.Share f.Vpn
@@ -30,9 +82,9 @@ f.Freeplug.Reset_a_Freeplug(
f.Freeplug.Get_the_current_Freeplugs_networks(

# Register application to get app token. Physical access is required.
>>> token = f.register("Id_SuperAppli", "Une superbe appli", "python")
>>> token = f.register("AppId", "My App", "PC")
# Generate session token
>>> f.mksession(app_id="Id_SupperAppli", token=token)
>>> f.mksession(app_id="AppId", token=token)

>>> help(f.Lan.Update_the_current_Lan_configuration)
Help on method Update_the_current_Lan_configuration:
@@ -60,7 +112,9 @@ Access_a_given_contact_entry(id) method of pyfbx.client.Contacts instance
>>> f.mksession()
```

## Testing
## Developpment

### Testing

You can run tests with


+ 51
- 0
pyfbx/__main__.py View File

@@ -0,0 +1,51 @@
"""
Console script
"""
import os
import logging
import argparse
import pprint
from pyfbx import Fbx

log_level = (logging.WARNING, logging.INFO, logging.DEBUG)


def console(log, level):
log.setLevel(level)
formatter = logging.Formatter('%(asctime)s - %(name)14s - %(levelname)s - %(message)s')
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
log.addHandler(stream_handler)


def main():
log = logging.getLogger("pyfbx")
parser = argparse.ArgumentParser()
parser.add_argument("app_id", type=str, help="application identifier")
parser.add_argument("-c", "--command", type=str,
help="command, defaults to System.Get_the_current_system_info()",
default="System.Get_the_current_system_info()")
parser.add_argument("-t", "--token", type=str, help="token (or f:<filename>)")
parser.add_argument("-v", "--verbose", action="count", default=0,
help="increase verbosity to INFO, use twice for DEBUG")
parser.add_argument("-n", "--http", action="store_true",
help="Disable MDNS and use http known address")
args = parser.parse_args()
console(log, log_level[min(2, args.verbose)])
try:
myfb = Fbx(nomdns=args.http)
token = args.token
if token:
if token.startswith('f:'):
with open(args.token[2:]) as tok_file:
token = tok_file.read().strip()
else:
log.warning(f"Registering app {__name__}, id {args.app_id}, Press button")
token = myfb.register(app_id=args.app_id, app_name=__name__,
device=os.uname().nodename)
log.warning(f"Save your application token: {token}")
myfb.mksession(app_id=args.app_id, token=token)
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(eval(f"myfb.{args.command}"))
except BaseException as err:
print(err)

+ 27
- 9
pyfbx/client.py View File

@@ -3,9 +3,11 @@
Fbx Client
"""
import os
import logging
import re
import hmac
import hashlib
import logging
import time
import requests
from . import api
@@ -32,15 +34,18 @@ class FbxTransport():
Transport abstraction and context handling for all methods
"""

def __init__(self, url=None, session=None):
def __init__(self, url=None, nomdns=False, session=None):
self.log = logging.getLogger("pyfbx.trans")
self._session = session or requests.session()
self._session.verify = os.path.join(os.path.dirname(__file__), 'fb.pem')
self.set_url(url)
self.set_url(url, nomdns)

def set_url(self, url):
def set_url(self, url, nomdns):
if url is None:
# Detect using MDNS or fallback
self._url = mdns.FbxMDNS().search() or self.get_local_base()
if nomdns:
self._url = self.get_local_base()
else:
self._url = mdns.FbxMDNS().search() or self.get_local_base()
else:
if re.search("https?://", url) is None:
url = "http://" + url
@@ -54,18 +59,24 @@ class FbxTransport():

def api_exec(self, http_method, endpoint, post_data=None, **kwargs):
req_response = self._session.request(
http_method, self._url + "/" + endpoint.format(**kwargs), json=post_data)
http_method,
self._url + "/" + endpoint.format(**kwargs), json=post_data)
self.log.debug(">> Sent %s %s/%s Post: %s", http_method, self._url,
endpoint.format(**kwargs), post_data)
req_response.raise_for_status()
response = req_response.json()
if response['success']:
if 'result' in response:
self.log.debug(f"<< Got {response['result']}")
return response['result']
else:
raise FbxErrorResponse(response['error_code'], response['msg'])

def get_local_base(self, url=api._DISC_HTTP_URL):
response = self._session.get(f"{url}/api_version").json()
return "%s%sv%s" % (url, response['api_base_url'], response['api_version'][0])
self.log.debug(f"<< Detected api {response['api_version']}")
return "%s%sv%s" % (url, response['api_base_url'],
response['api_version'][0])


class Fbx():
@@ -73,8 +84,9 @@ class Fbx():
Freebox object
"""

def __init__(self, url=None, session=None):
self._trn = FbxTransport(url, session=session)
def __init__(self, url=None, nomdns=False, session=None):
self.log = logging.getLogger("pyfbx.fbx")
self._trn = FbxTransport(url, nomdns=nomdns, session=session)

# Create on the fly attributes to classes
_globals = globals()
@@ -92,10 +104,12 @@ class Fbx():
res = self._trn.api_exec("POST", "login/authorize/", data)
trackid, self.token = res["track_id"], res["app_token"]
s = "pending"
self.log.info("Press Ok on the freebox to register application")
while s == "pending":
s = self._trn.api_exec("GET", f"login/authorize/{trackid}")["status"]
if s == "pending":
time.sleep(1)
self.log.debug(f"Registration returned: {s}")
return s == "granted" and self.token

def mksession(self, app_id=None, token=None):
@@ -115,6 +129,7 @@ class Fbx():
session_token = resp["session_token"]
self.app_id = app_id
self._trn.set_session_header(session_token)
self.log.info(f"Authenticated")
return resp["permissions"]


@@ -127,7 +142,10 @@ class FbxErrorResponse(Exception):
return f'{self.msg} [{self.error_code}]'


log = logging.getLogger("pyfbx")

# All FB subsystems are classes deriving from FbxClass
for _classname in api.SYSTEMS:
log.debug(f"Adding class {_classname} to locals")
locals()[_classname] = type(_classname, (FbxClass, ), {})
__all__.append(_classname)

+ 6
- 2
pyfbx/mdns.py View File

@@ -3,6 +3,7 @@ Search Freebox through MDNS
"""

import time
import logging
from zeroconf import ServiceBrowser, Zeroconf
from . import api

@@ -29,14 +30,17 @@ class FbxMDNS():

def __init__(self, timeout=1):
self.timeout = timeout
self.log = logging.getLogger("pyfbx.mdns")

def search(self, svc_name=api._DISC_MDNS_NAME, timeout=1):
self.log.debug("Searching with MDNS")
zeroconf = Zeroconf()
self._listener = FbxMDNS.MyListener()
browser = ServiceBrowser(zeroconf, svc_name, self._listener)
time.sleep(timeout)
browser.cancel() # slow
zeroconf.close() # slow
self.log.debug("Closing MDNS")
browser.cancel() # slow but required to
zeroconf.close() # stop listener thread
if self.svc_info:
prop = self.svc_prop
base = "%sv%s" % (prop['api_base_url'], prop['api_version'][0])

+ 1
- 1
setup.cfg View File

@@ -5,7 +5,7 @@ addopts = --cov --cov-report term-missing
[coverage:run]
branch = True
source = pyfbx
omit = tests/*
omit = tests/*, pyfbx/__main__.py

[wheel]
universal = 0

+ 1
- 0
setup.py View File

@@ -17,4 +17,5 @@ setup(
packages=find_packages(),
install_requires=["zeroconf", "requests"],
include_package_data=True,
entry_points={'console_scripts': ['pyfbx = pyfbx.__main__:main', ]},
)

+ 1
- 0
tests/sanity_test.py View File

@@ -21,4 +21,5 @@ def test_fbx_mdns():
with patch('pyfbx.mdns.FbxMDNS.search', return_value=None):
f = Fbx()
f = Fbx(url="http://192.168.1.254")
f = Fbx(nomdns=True)
f = Fbx(session=requests.Session())

Loading…
Cancel
Save