<commit_before>from setuptools import find_packages, setup

VERSION = "8.0.0"
LONG_DESCRIPTION = """
.. image:: http://pinaxproject.com/pinax-design/patches/pinax-blog.svg
    :target: https://pypi.python.org/pypi/pinax-blog/

==========
Pinax Blog
==========

.. image:: https://img.shields.io/pypi/v/pinax-blog.svg
    :target: https://pypi.python.org/pypi/pinax-blog/

\

.. image:: https://img.shields.io/circleci/project/github/pinax/pinax-blog.svg
    :target: https://circleci.com/gh/pinax/pinax-blog
.. image:: https://img.shields.io/codecov/c/github/pinax/pinax-blog.svg
    :target: https://codecov.io/gh/pinax/pinax-blog
.. image:: https://img.shields.io/github/contributors/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/graphs/contributors
.. image:: https://img.shields.io/github/issues-pr/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/pulls
.. image:: https://img.shields.io/github/issues-pr-closed/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/pulls?q=is%3Apr+is%3Aclosed

\

.. image:: http://slack.pinaxproject.com/badge.svg
    :target: http://slack.pinaxproject.com/
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
    :target: https://opensource.org/licenses/MIT/

\

``pinax-blog`` is a blog app for Django.

Features
--------

* support for multiple channels (e.g. technical vs business)
* use of Creole (optional) and Markdown as markup format
* Atom and RSS feeds
* previewing of blog posts before publishing
* optional ability to announce new posts on twitter
* Traditional date based urls or simpler slug-only urls, via configuration
* Control over opengraph and twitter card meta data per post
* Review comments per post for multi-author workflows
* public but secret urls for unpublished blog posts for easier review

Supported Django and Python Versions
------------------------------------

+-----------------+-----+-----+-----+
| Django / Python | 3.6 | 3.7 | 3.8 |
+=================+=====+=====+=====+
|  2.2            |  *  |  *  |  *  |
+-----------------+-----+-----+-----+
|  3.0            |  *  |  *  |  *  |
+-----------------+-----+-----+-----+
"""

setup(
    author="Pinax Team",
    author_email="team@pinaxproject.com",
    description="Blogging app for the Django web framework",
    name="pinax-blog",
    long_description=LONG_DESCRIPTION,
    version=VERSION,
    url="http://github.com/pinax/pinax-blog/",
    license="MIT",
    packages=find_packages(),
    package_data={
        "pinax.blog": [
            "templates/pinax/blog/*ml",
            "static/js/admin_post_form.js"
        ]
    },
    classifiers=[
        "Development Status :: 5 - Production/Stable",
        "Environment :: Web Environment",
        "Framework :: Django",
        "Framework :: Django :: 2.2",
        "Framework :: Django :: 3.0",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Topic :: Software Development :: Libraries :: Python Modules",
    ],
    install_requires=[
        "django>=2.2",
        "django-appconf>=1.0.1",
        "markdown==2.6.11",
        "pillow>=3.0.0",
        "pinax-images>=3.0.1",
        "pygments>=2.0.2",
        "pytz>=2020.1",
    ],
    tests_require=[
        "django-bootstrap-form>=3.0.0",
        "django-test-plus>=1.0.22",
        "mock>=2.0.0",
        "pinax-templates>=1.0.0",
    ],
    test_suite="runtests.runtests",
    zip_safe=False
)
<commit_msg>Update version<commit_after>from setuptools import find_packages, setup

VERSION = "8.0.1"
LONG_DESCRIPTION = """
.. image:: http://pinaxproject.com/pinax-design/patches/pinax-blog.svg
    :target: https://pypi.python.org/pypi/pinax-blog/

==========
Pinax Blog
==========

.. image:: https://img.shields.io/pypi/v/pinax-blog.svg
    :target: https://pypi.python.org/pypi/pinax-blog/

\

.. image:: https://img.shields.io/circleci/project/github/pinax/pinax-blog.svg
    :target: https://circleci.com/gh/pinax/pinax-blog
.. image:: https://img.shields.io/codecov/c/github/pinax/pinax-blog.svg
    :target: https://codecov.io/gh/pinax/pinax-blog
.. image:: https://img.shields.io/github/contributors/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/graphs/contributors
.. image:: https://img.shields.io/github/issues-pr/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/pulls
.. image:: https://img.shields.io/github/issues-pr-closed/pinax/pinax-blog.svg
    :target: https://github.com/pinax/pinax-blog/pulls?q=is%3Apr+is%3Aclosed

\

.. image:: http://slack.pinaxproject.com/badge.svg
    :target: http://slack.pinaxproject.com/
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
    :target: https://opensource.org/licenses/MIT/

\

``pinax-blog`` is a blog app for Django.

Features
--------

* support for multiple channels (e.g. technical vs business)
* use of Creole (optional) and Markdown as markup format
* Atom and RSS feeds
* previewing of blog posts before publishing
* optional ability to announce new posts on twitter
* Traditional date based urls or simpler slug-only urls, via configuration
* Control over opengraph and twitter card meta data per post
* Review comments per post for multi-author workflows
* public but secret urls for unpublished blog posts for easier review

Supported Django and Python Versions
------------------------------------

+-----------------+-----+-----+-----+
| Django / Python | 3.6 | 3.7 | 3.8 |
+=================+=====+=====+=====+
|  2.2            |  *  |  *  |  *  |
+-----------------+-----+-----+-----+
|  3.0            |  *  |  *  |  *  |
+-----------------+-----+-----+-----+
"""

setup(
    author="Pinax Team",
    author_email="team@pinaxproject.com",
    description="Blogging app for the Django web framework",
    name="pinax-blog",
    long_description=LONG_DESCRIPTION,
    version=VERSION,
    url="http://github.com/pinax/pinax-blog/",
    license="MIT",
    packages=find_packages(),
    package_data={
        "pinax.blog": [
            "templates/pinax/blog/*ml",
            "static/js/admin_post_form.js"
        ]
    },
    classifiers=[
        "Development Status :: 5 - Production/Stable",
        "Environment :: Web Environment",
        "Framework :: Django",
        "Framework :: Django :: 2.2",
        "Framework :: Django :: 3.0",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Topic :: Software Development :: Libraries :: Python Modules",
    ],
    install_requires=[
        "django>=2.2",
        "django-appconf>=1.0.1",
        "markdown==2.6.11",
        "pillow>=3.0.0",
        "pinax-images>=3.0.1",
        "pygments>=2.0.2",
        "pytz>=2020.1",
    ],
    tests_require=[
        "django-bootstrap-form>=3.0.0",
        "django-test-plus>=1.0.22",
        "mock>=2.0.0",
        "pinax-templates>=1.0.0",
    ],
    test_suite="runtests.runtests",
    zip_safe=False
)
<|endoftext|>


<commit_before>from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'djangotest.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
)
<commit_msg>fix django test<commit_after>from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    # Examples:
    # url(r'^$', 'djangotest.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
]
<|endoftext|>


<commit_before>import sys
import time

import geopy.exc
from geopy.geocoders import GoogleV3
from joblib import Memory
import requests

from dataset import Database, Events
import settings

memory = Memory('cache/scrape', verbose=0)

class RateLimitError(RuntimeError):
    def __init__(self, reset_time):
        self.reset_time = reset_time

    @property
    def seconds_left(self):
        return self.reset_time - int(time.time())

    @property
    def finished(self):
        return self.seconds_left <= 0

    def wait(self):
        if self.finished:
            return

        seconds = self.seconds_left
        minutes = round(self.seconds_left / 60, 1)
        print('!', 'Waiting', minutes, 'minutes.')
        time.sleep(seconds)

class GitHub:
    def __init__(self):
        self.client_id = settings.CLIENT_ID
        self.client_secret = settings.CLIENT_SECRET

    def get(self, url, params=None):
        if params is None:
            params = {}

        params['client_id'] = self.client_id
        params['client_secret'] = self.client_secret

        response = requests.get(url, params=params)

        headers = response.headers
        if headers['X-RateLimit-Remaining'] == '0':
            reset_time = int(headers['X-RateLimit-Reset'])
            raise RateLimitError(reset_time)

        return response

    def get_all_users(self, since=0):
        next_url = 'https://api.github.com/users?since={}'.format(since)

        while next_url is not None:
            response = self.get(next_url)
            next_url = response.links['next']['url']
            for user in response.json():
                yield user

    def get_user(self, username):
        url = 'https://api.github.com/users/{}'.format(username)
        return self.get(url).json()

    def get_following_users(self, username):
        next_url = 'https://api.github.com/users/{}/following'.format(username)

        while next_url is not None:
            response = self.get(next_url)

            try:
                next_url = response.links['next']['url']
            except KeyError:
                next_url = None

            for user in response.json():
                yield user

class Geography:
    def __init__(self):
        self.api_key = settings.GOOGLE_API_KEY
        self.geolocator = GoogleV3(self.api_key)

        self.geocode = memory.cache(self.geocode)

    def geocode(self, text):
        try:
            result = self.geolocator.geocode(text)
        except geopy.exc.GeocoderQuotaExceeded:
            raise RateLimitError(int(time.time()) + (60 * 60))
        except geopy.exc.GeocoderTimedOut:
            raise RateLimitError(int(time.time()) + 10)
        else:
            time.sleep(0.1)  # to avoid rate limiting
            return result

    @staticmethod
    def get_country(location):
        for component in location.raw['address_components']:
            if 'country' in component['types']:
                return component['short_name'], component['long_name']

        raise ValueError()

class Genderize:
    def __init__(self):
        self.api_key = settings.GENDERIZE_API_KEY
        self.guess = memory.cache(self.guess)

    def guess(self, name):
        if name == "<script>alert('test')</script>":
            return '?', None

        params = {'name': name, 'apikey': self.api_key}
        response = requests.get('http://api.genderize.io', params=params)

        headers = response.headers
        if 'X-Rate-Limit-Remaining' not in headers:
            raise RateLimitError(int(time.time()) + (60 * 60))

        if headers['X-Rate-Limit-Remaining'] == '0':
            reset_time = int(headers['X-Rate-Limit-Reset'])
            raise RateLimitError(reset_time)

        data = response.json()

        if data['gender'] is None:
            return '?', None

        probability = float(data['probability'])
        code = data['gender'][0].upper()
        return code, probability

class Scraper:
    def __init__(self):
        self.github = GitHub()
        self.genderize = Genderize()
        self.geography = Geography()

        self.database = Database()
        self.events = Events()

    def print_status(self, user_id, *args):
        print('>', '#{}'.format(user_id), *args)
        sys.stdout.flush()

    def scrape_event(self, event):
        try:
            actor = event['actor']
            user_id = actor['id']
            user_login = actor['login']
        except KeyError:
            return

        if self.database.has_user(user_id):
            return

        github_user = self.github.get_user(user_login)
        if 'id' not in github_user or user_id != github_user['id']:  # no longer a user
            self.database.insert_user({'id': user_id, 'login': user_login, 'deleted': True})
            self.print_status(user_id, ':(')
            return

        fields = {
            'id': github_user['id'],
            'hireable': github_user['hireable']
        }

        for field in ['login', 'avatar_url', 'gravatar_id', 'name', 'company',
                      'blog', 'location', 'email', 'bio']:
            if github_user[field] is None:
                fields[field] = None
            else:
                fields[field] = github_user[field].strip()

        self.database.insert_user(fields)

        self.print_status(fields['id'], fields['login'], github_user['created_at'], '✓')

    def scrape(self, start_from):
        self.scrape_locations(4)
        self.scrape_genders(4)
        return

        started = False

        for name, event in self.events.iterate(with_names=True):
            if not started:
                if name.startswith(start_from):
                    started = True
                else:
                    continue

            while True:
                try:
                    self.scrape_event(event)
                    break
                except RateLimitError as e:
                    while not e.finished:
                        try:
                            self.scrape_locations(25)
                            self.scrape_genders(25)
                        except RateLimitError:
                            break

                    e.wait()

    def scrape_locations(self, n=100):
        users = self.database.get_users_without_location(limit=n)
        for user_id, location_str in users:
            location = self.geography.geocode(location_str)
            if location is None:
                self.print_status(user_id, location_str, '->', '?')
                self.database.update_user_location(user_id, None, None, '?')
                continue

            try:
                country_code, country_name = \
                    self.geography.get_country(location)
            except ValueError:
                self.print_status(user_id, location_str, '->', '?')
                self.database.update_user_location(user_id, None, None, '?')
                continue

            self.print_status(user_id, location_str, '->', country_code,
                              '({})'.format(country_name))

            self.database.update_user_location(user_id, location.latitude,
                                               location.longitude,
                                               country_code)

    def scrape_genders(self, n=100):
        users = self.database.get_users_without_gender(limit=n)
        for user_id, name in users:
            gender, probability = self.genderize.guess(name.split()[0])

            try:
                probability_str = '({}%)'.format(probability * 100)
            except TypeError:
                probability_str = '(N/A)'

            self.print_status(user_id, name, '->', gender, probability_str)

            self.database.update_user_gender(user_id, gender, probability)

if __name__ == '__main__':
    scraper = Scraper()
    scraper.scrape(sys.argv[1])
<commit_msg>Remove debug code<commit_after>import sys
import time

import geopy.exc
from geopy.geocoders import GoogleV3
from joblib import Memory
import requests

from dataset import Database, Events
import settings

memory = Memory('cache/scrape', verbose=0)

class RateLimitError(RuntimeError):
    def __init__(self, reset_time):
        self.reset_time = reset_time

    @property
    def seconds_left(self):
        return self.reset_time - int(time.time())

    @property
    def finished(self):
        return self.seconds_left <= 0

    def wait(self):
        if self.finished:
            return

        seconds = self.seconds_left
        minutes = round(self.seconds_left / 60, 1)
        print('!', 'Waiting', minutes, 'minutes.')
        time.sleep(seconds)

class GitHub:
    def __init__(self):
        self.client_id = settings.CLIENT_ID
        self.client_secret = settings.CLIENT_SECRET

    def get(self, url, params=None):
        if params is None:
            params = {}

        params['client_id'] = self.client_id
        params['client_secret'] = self.client_secret

        response = requests.get(url, params=params)

        headers = response.headers
        if headers['X-RateLimit-Remaining'] == '0':
            reset_time = int(headers['X-RateLimit-Reset'])
            raise RateLimitError(reset_time)

        return response

    def get_all_users(self, since=0):
        next_url = 'https://api.github.com/users?since={}'.format(since)

        while next_url is not None:
            response = self.get(next_url)
            next_url = response.links['next']['url']
            for user in response.json():
                yield user

    def get_user(self, username):
        url = 'https://api.github.com/users/{}'.format(username)
        return self.get(url).json()

    def get_following_users(self, username):
        next_url = 'https://api.github.com/users/{}/following'.format(username)

        while next_url is not None:
            response = self.get(next_url)

            try:
                next_url = response.links['next']['url']
            except KeyError:
                next_url = None

            for user in response.json():
                yield user

class Geography:
    def __init__(self):
        self.api_key = settings.GOOGLE_API_KEY
        self.geolocator = GoogleV3(self.api_key)

        self.geocode = memory.cache(self.geocode)

    def geocode(self, text):
        try:
            result = self.geolocator.geocode(text)
        except geopy.exc.GeocoderQuotaExceeded:
            raise RateLimitError(int(time.time()) + (60 * 60))
        except geopy.exc.GeocoderTimedOut:
            raise RateLimitError(int(time.time()) + 10)
        else:
            time.sleep(0.1)  # to avoid rate limiting
            return result

    @staticmethod
    def get_country(location):
        for component in location.raw['address_components']:
            if 'country' in component['types']:
                return component['short_name'], component['long_name']

        raise ValueError()

class Genderize:
    def __init__(self):
        self.api_key = settings.GENDERIZE_API_KEY
        self.guess = memory.cache(self.guess)

    def guess(self, name):
        if name == "<script>alert('test')</script>":
            return '?', None

        params = {'name': name, 'apikey': self.api_key}
        response = requests.get('http://api.genderize.io', params=params)

        headers = response.headers
        if 'X-Rate-Limit-Remaining' not in headers:
            raise RateLimitError(int(time.time()) + (60 * 60))

        if headers['X-Rate-Limit-Remaining'] == '0':
            reset_time = int(headers['X-Rate-Limit-Reset'])
            raise RateLimitError(reset_time)

        data = response.json()

        if data['gender'] is None:
            return '?', None

        probability = float(data['probability'])
        code = data['gender'][0].upper()
        return code, probability

class Scraper:
    def __init__(self):
        self.github = GitHub()
        self.genderize = Genderize()
        self.geography = Geography()

        self.database = Database()
        self.events = Events()

    def print_status(self, user_id, *args):
        print('>', '#{}'.format(user_id), *args)
        sys.stdout.flush()

    def scrape_event(self, event):
        try:
            actor = event['actor']
            user_id = actor['id']
            user_login = actor['login']
        except KeyError:
            return

        if self.database.has_user(user_id):
            return

        github_user = self.github.get_user(user_login)
        if 'id' not in github_user or user_id != github_user['id']:  # no longer a user
            self.database.insert_user({'id': user_id, 'login': user_login, 'deleted': True})
            self.print_status(user_id, ':(')
            return

        fields = {
            'id': github_user['id'],
            'hireable': github_user['hireable']
        }

        for field in ['login', 'avatar_url', 'gravatar_id', 'name', 'company',
                      'blog', 'location', 'email', 'bio']:
            if github_user[field] is None:
                fields[field] = None
            else:
                fields[field] = github_user[field].strip()

        self.database.insert_user(fields)

        self.print_status(fields['id'], fields['login'], github_user['created_at'], '✓')

    def scrape(self, start_from):
        started = False

        for name, event in self.events.iterate(with_names=True):
            if not started:
                if name.startswith(start_from):
                    started = True
                else:
                    continue

            while True:
                try:
                    self.scrape_event(event)
                    break
                except RateLimitError as e:
                    while not e.finished:
                        try:
                            self.scrape_locations(25)
                            self.scrape_genders(25)
                        except RateLimitError:
                            break

                    e.wait()

    def scrape_locations(self, n=100):
        users = self.database.get_users_without_location(limit=n)
        for user_id, location_str in users:
            location = self.geography.geocode(location_str)
            if location is None:
                self.print_status(user_id, location_str, '->', '?')
                self.database.update_user_location(user_id, None, None, '?')
                continue

            try:
                country_code, country_name = \
                    self.geography.get_country(location)
            except ValueError:
                self.print_status(user_id, location_str, '->', '?')
                self.database.update_user_location(user_id, None, None, '?')
                continue

            self.print_status(user_id, location_str, '->', country_code,
                              '({})'.format(country_name))

            self.database.update_user_location(user_id, location.latitude,
                                               location.longitude,
                                               country_code)

    def scrape_genders(self, n=100):
        users = self.database.get_users_without_gender(limit=n)
        for user_id, name in users:
            gender, probability = self.genderize.guess(name.split()[0])

            try:
                probability_str = '({}%)'.format(probability * 100)
            except TypeError:
                probability_str = '(N/A)'

            self.print_status(user_id, name, '->', gender, probability_str)

            self.database.update_user_gender(user_id, gender, probability)

if __name__ == '__main__':
    scraper = Scraper()
    scraper.scrape(sys.argv[1])
<|endoftext|>


<commit_before>#!/usr/bin/env python
#
# Copyright 2007 Neal Norwitz
# Portions Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Find warnings for C++ code.

TODO(nnorwitz): provide a mechanism to configure which warnings should
be generated and which should be suppressed.  Currently, all possible
warnings will always be displayed.  There is no way to suppress any.
There also needs to be a way to use annotations in the source code to
suppress warnings.
"""

__author__ = 'nnorwitz@google.com (Neal Norwitz)'

import os
import sys

from cpp import ast
from cpp import headers
from cpp import metrics
from cpp import symbols
from cpp import tokenize
from cpp import utils

class Module(object):
    """Data container represting a single source file."""

    def __init__(self, filename, ast_list):
        self.filename = filename
        self.ast_list = ast_list
        self.public_symbols = self._GetExportedSymbols()

    def _GetExportedSymbols(self):
        if not self.ast_list:
            return {}
        return dict([(n.name, n) for n in self.ast_list if n.IsExportable()])

    def IsAnyPublicSymbolUsed(self, ast_list):
        """Returns a bool whether any token in ast_list uses this module."""
        def _IsSymbolUsed(symbol):
            # TODO(nnorwitz): this doesn't handle namespaces properly.
            for node in ast_list:
                if node.Requires(symbol):
                    return True
            return False

        # Special case when we don't know the answer.  Assume it's ok.
        if self.ast_list is None:
            return True

        for symbol in self.public_symbols.itervalues():
            if _IsSymbolUsed(symbol):
                return True
        return False

def _IsHeaderFile(filename):
    base, ext = os.path.splitext(filename)
    return ext.lower() in ('.h', '.hpp', '.h++')

def _IsCppFile(filename):
    base, ext = os.path.splitext(filename)
    return ext.lower() in ('.c', '.cc', '.cpp', '.c++')

class WarningHunter(object):

    # Cache filename: ast_list
    _module_cache = {}

    def __init__(self, filename, source, ast_list):
        self.filename = filename
        self.source = source
        self.ast_list = ast_list
        self.symbol_table = symbols.SymbolTable()

        self.metrics = metrics.Metrics(source)
        self.warnings = []
        if filename not in self._module_cache:
            self._module_cache[filename] = Module(filename, ast_list)
        else:
            print 'Warning', filename, 'already in cache'

    def _AddWarning(self, msg, node, filename=None):
        source = self.metrics
        if filename is not None:
            source = metrics.Metrics(open(filename).read())
        else:
            filename = self.filename
        line_num = source.GetLineNumber(node.start)
        self.warnings.append((filename, line_num, msg))

    def ShowWarnings(self):
        self.warnings.sort()
        for filename, line_num, msg in self.warnings:
            print '%s:%d: %s' % (filename, line_num, msg)

    def FindWarnings(self):
        # print 'Searching for warnings in:', self.filename
        if _IsHeaderFile(self.filename):
            self._FindHeaderWarnings()
        elif _IsCppFile(self.filename):
            self._FindSourceWarnings()
        else:
            print 'Unknown filetype for: %s' % self.filename

    def _UpdateSymbolTable(self, module):
        for name, node in module.public_symbols.iteritems():
            self.symbol_table.AddSymbol(name, node.namespace, node, module)

    def _GetHeaderFile(self, filename):
        if filename in self._module_cache:
            return self._module_cache[filename]

        module = Module(filename, None)
        source, actual_filename = headers.ReadSource(filename)
        if source is None:
            print 'Unable to find', filename
        else:
            builder = ast.BuilderFromSource(source, filename)
            try:
                module = Module(filename, filter(None, builder.Generate()))
            except KeyboardInterrupt:
                sys.exit(1)
            except:
                print 'Exception while processing', filename
            else:
                self._UpdateSymbolTable(module)
        self._module_cache[filename] = module
        return module

    def _GetForwardDeclarations(self):
        # Map header-filename: (#include AST node, ast_list_for_file)
        included_files = {}
        # Find all the forward declared types.
        # TODO(nnorwitz): Need to handle structs too.
        forward_declared_classes = {}
        for node in self.ast_list:
            if isinstance(node, ast.Class) and node.IsDeclaration():
                forward_declared_classes[node.FullName()] = node
            if isinstance(node, ast.Include) and not node.system:
                module = self._GetHeaderFile(node.filename)
                included_files[node.filename] = node, module

        return forward_declared_classes, included_files

    def _GetClassesUsed(self):
        def _ProcessFunction(function):
            if function.return_type:
                index = 0
                if function.return_type[0].name in ('struct', 'class'):
                    index = 1
                classes_used[function.return_type[index].name] = True
            # TODO(nnorwitz): ignoring the body for now.
            for p in ast._SequenceToParameters(function.parameters):
                classes_used[p.type_name] = True

        def _ProcessTypedef(typedef):
            for token in typedef.alias:
                if (isinstance(token, tokenize.Token) and
                    token.token_type == tokenize.NAME):
                    classes_used[token.name] = True
                elif isinstance(token, ast.Struct):
                    pass                # TODO(nnorwitz): impl
                elif isinstance(token, ast.Union):
                    pass                # TODO(nnorwitz): impl

        # TODO(nnorwitz): this needs to be recursive.
        classes_used = {}
        for node in self.ast_list:
            if isinstance(node, ast.VariableDeclaration):
                classes_used[node.type_name] = True
            elif isinstance(node, ast.Function):
                _ProcessFunction(node)
            elif isinstance(node, ast.Typedef):
                _ProcessTypedef(node)
            elif isinstance(node, ast.Class) and node.body:
                for node in node.body:
                    if (isinstance(node, ast.Function) and
                        not (node.modifiers & ast.FUNCTION_DTOR)):
                        _ProcessFunction(node)
                    if isinstance(node, ast.VariableDeclaration):
                        classes_used[node.type_name] = True
        return classes_used

    def _FindUnusedWarnings(self):
        # NOTE(nnorwitz): this could be sped up by iterating over the
        # file's AST and finding which symbols are used.  Then iterate
        # over each header file and see if any of the symbols are used.
        #
        # This is how this method should be implemented:
        # Read all the #includes and store them in parsed form.
        # Keep a dict of all public identifiers from each #include
        # Iterate through the source AST/tokens.
        # For each initial token (ignore ->tokens), find the header
        # that referenced it and mark in that header.  If no header, bitch.

        # Finally, iterate over all the headers.  For each one that
        # has no markings of being used, bitch.

        forward_declarations, included_files = self._GetForwardDeclarations()
        classes_used = self._GetClassesUsed()

        # Find all the forward declarations that are not used.
        for cls in forward_declarations:
            if cls not in classes_used:
                node = forward_declarations[cls]
                self._AddWarning('%r not used' % cls, node)

        # Find all the header files that are not used.
        for node, module in included_files.values():
            if _IsCppFile(module.filename):
                msg = ('should not include C++ source files: %s' %
                       module.filename)
                self._AddWarning(msg, node)
            if node.filename == self.filename:
                self._AddWarning('%s includes itself' % node.filename, node)
            if not module.IsAnyPublicSymbolUsed(self.ast_list):
                msg = '%s does not need to be #included' % node.filename
                self._AddWarning(msg, node)

    def _FindHeaderWarnings(self):
        self._FindUnusedWarnings()
        # TODO(nnorwitz): other warnings to add:
        #   * when a symbol is used, check if it is used as a pointer
        #     and can be forward declared rather than #include'ing the
        #     header file.  This only applies to header files until we
        #     track all variable accesses/derefs.
        #   * too much non-template impl in header file

    def _FindPublicFunctionWarnings(self, node, name, primary_header,
                                    public_symbols, all_headers):
        # Not found in the primary header, search all other headers.
        for header_node, header in all_headers.itervalues():
            if name in header.public_symbols:
                # If the primary.filename == header.filename, it probably
                # indicates an error elsewhere.  It sucks to mask it,
                # but false positives are worse.
                if (primary_header and
                    primary_header.filename != header.filename):
                    msg = ('expected to find %s in %s, but found in %s' %
                           (name, primary_header.filename, header.filename))
                    self._AddWarning(msg, node)
                break
        else:
            where = 'in any directly included header'
            if primary_header:
                where = ('in expected header ' + primary_header.filename +
                         ' or any other directly #included header')
            self._AddWarning('%s not found %s' % (name, where), node)

    def _CheckPublicFunctions(self, primary_header, all_headers):
        # Verify all the public functions are also declared in a header file.
        public_symbols = ()
        declared_only_symbols = {}
        if primary_header:
            public_symbols = {}
            for name, symbol in primary_header.public_symbols.iteritems():
                if isinstance(symbol, ast.Function):
                    public_symbols[name] = symbol
            declared_only_symbols = dict.fromkeys(public_symbols, True)

        # TODO(nnorwitz): need to handle using statements.
        for node in self.ast_list:
            # Make sure we have a function that should be exported.
            if not isinstance(node, ast.Function):
                continue
            if isinstance(node, ast.Method):
                # Ensure that for Foo::Bar, Foo is *not* a namespace.
                # If Foo is a namespace, we have a function and not a method.
                names = [n.name for n in node.in_class]
                if names != self.symbol_table.GetNamespace(names):
                    continue
            if not (node.IsDefinition() and node.IsExportable()):
                continue

            # This function should be declared in a header file.
            name = node.name
            if name in public_symbols:
                declared_only_symbols[name] = False
            else:
                self._FindPublicFunctionWarnings(node, name, primary_header,
                                                 public_symbols, all_headers)

        for name, declared_only in declared_only_symbols.iteritems():
            if declared_only:
                # TODO(nnorwitz): shouldn't warn if function is templatized.
                node = public_symbols[name]
                msg = '%s declared but not defined' % name
                self._AddWarning(msg, node, primary_header.filename)

    def _GetPrimaryHeader(self, included_files):
        basename = os.path.splitext(self.filename)[0]
        primary_header = included_files.get(basename + '.h')
        if not primary_header:
            primary_header = included_files.get(basename)
        if primary_header:
            return primary_header[1]
        return None

    def _FindSourceWarnings(self):
        forward_declarations, included_files = self._GetForwardDeclarations()
        if forward_declarations:
            # TODO(nnorwitz): This really isn't a problem, but might
            # be something to warn against.  I expect this will either
            # be configurable or removed in the future.  But it's easy
            # to check for now.
            msg = 'forward declarations not expected in source file'
            self._AddWarning(msg, forward_declarations.itervalues().next())

        # A primary header is optional.  However, when looking up
        # defined methods in the source, always look in the
        # primary_header first.  Expect that is the most likely location.
        # Use of primary_header is primarily an optimization.
        primary_header = self._GetPrimaryHeader(included_files)
        if not primary_header:
            # TODO(nnorwitz): This shouldn't always be a warning.
            # For example, *main.cc shouldn't need a header.  But
            # almost all other source files should have a
            # corresponding header.
            msg = 'unable to find header file with matching name'
            self._AddWarning(msg, self.ast_list[0])

        self._CheckPublicFunctions(primary_header, included_files)

        # TODO(nnorwitz): other warnings to add:
        #   * unused forward decls for variables (globals)/classes
        #   * Functions that are too large/complex
        #   * Variables declared far from first use

def main(argv):
    for filename in argv[1:]:
        source = utils.ReadFile(filename)
        if source is None:
            continue

        print 'Processing', filename
        builder = ast.BuilderFromSource(source, filename)
        entire_ast = filter(None, builder.Generate())
        hunter = WarningHunter(filename, source, entire_ast)
        hunter.FindWarnings()
        hunter.ShowWarnings()

if __name__ == '__main__':
    main(sys.argv)
<commit_msg>Start adding a little support for using<commit_after>#!/usr/bin/env python
#
# Copyright 2007 Neal Norwitz
# Portions Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Find warnings for C++ code.

TODO(nnorwitz): provide a mechanism to configure which warnings should
be generated and which should be suppressed.  Currently, all possible
warnings will always be displayed.  There is no way to suppress any.
There also needs to be a way to use annotations in the source code to
suppress warnings.
"""

__author__ = 'nnorwitz@google.com (Neal Norwitz)'

import os
import sys

from cpp import ast
from cpp import headers
from cpp import metrics
from cpp import symbols
from cpp import tokenize
from cpp import utils

class Module(object):
    """Data container represting a single source file."""

    def __init__(self, filename, ast_list):
        self.filename = filename
        self.ast_list = ast_list
        self.public_symbols = self._GetExportedSymbols()

    def _GetExportedSymbols(self):
        if not self.ast_list:
            return {}
        return dict([(n.name, n) for n in self.ast_list if n.IsExportable()])

    def IsAnyPublicSymbolUsed(self, ast_list):
        """Returns a bool whether any token in ast_list uses this module."""
        def _IsSymbolUsed(symbol):
            # TODO(nnorwitz): this doesn't handle namespaces properly.
            for node in ast_list:
                if node.Requires(symbol):
                    return True
            return False

        # Special case when we don't know the answer.  Assume it's ok.
        if self.ast_list is None:
            return True

        for symbol in self.public_symbols.itervalues():
            if _IsSymbolUsed(symbol):
                return True
        return False

def _IsHeaderFile(filename):
    base, ext = os.path.splitext(filename)
    return ext.lower() in ('.h', '.hpp', '.h++')

def _IsCppFile(filename):
    base, ext = os.path.splitext(filename)
    return ext.lower() in ('.c', '.cc', '.cpp', '.c++')

class WarningHunter(object):

    # Cache filename: ast_list
    _module_cache = {}

    def __init__(self, filename, source, ast_list):
        self.filename = filename
        self.source = source
        self.ast_list = ast_list
        self.symbol_table = symbols.SymbolTable()

        self.metrics = metrics.Metrics(source)
        self.warnings = []
        if filename not in self._module_cache:
            self._module_cache[filename] = Module(filename, ast_list)
        else:
            print 'Warning', filename, 'already in cache'

    def _AddWarning(self, msg, node, filename=None):
        source = self.metrics
        if filename is not None:
            source = metrics.Metrics(open(filename).read())
        else:
            filename = self.filename
        line_num = source.GetLineNumber(node.start)
        self.warnings.append((filename, line_num, msg))

    def ShowWarnings(self):
        self.warnings.sort()
        for filename, line_num, msg in self.warnings:
            print '%s:%d: %s' % (filename, line_num, msg)

    def FindWarnings(self):
        # print 'Searching for warnings in:', self.filename
        if _IsHeaderFile(self.filename):
            self._FindHeaderWarnings()
        elif _IsCppFile(self.filename):
            self._FindSourceWarnings()
        else:
            print 'Unknown filetype for: %s' % self.filename

    def _UpdateSymbolTable(self, module):
        for name, node in module.public_symbols.iteritems():
            self.symbol_table.AddSymbol(name, node.namespace, node, module)

    def _GetHeaderFile(self, filename):
        if filename in self._module_cache:
            return self._module_cache[filename]

        module = Module(filename, None)
        source, actual_filename = headers.ReadSource(filename)
        if source is None:
            print 'Unable to find', filename
        else:
            builder = ast.BuilderFromSource(source, filename)
            try:
                module = Module(filename, filter(None, builder.Generate()))
            except KeyboardInterrupt:
                sys.exit(1)
            except:
                print 'Exception while processing', filename
            else:
                self._UpdateSymbolTable(module)
        self._module_cache[filename] = module
        return module

    def _GetForwardDeclarations(self):
        # Map header-filename: (#include AST node, ast_list_for_file)
        included_files = {}
        # Find all the forward declared types.
        # TODO(nnorwitz): Need to handle structs too.
        forward_declared_classes = {}
        for node in self.ast_list:
            if isinstance(node, ast.Class) and node.IsDeclaration():
                forward_declared_classes[node.FullName()] = node
            if isinstance(node, ast.Include) and not node.system:
                module = self._GetHeaderFile(node.filename)
                included_files[node.filename] = node, module

        return forward_declared_classes, included_files

    def _GetClassesUsed(self):
        def _ProcessFunction(function):
            if function.return_type:
                index = 0
                if function.return_type[0].name in ('struct', 'class'):
                    index = 1
                classes_used[function.return_type[index].name] = True
            # TODO(nnorwitz): ignoring the body for now.
            for p in ast._SequenceToParameters(function.parameters):
                classes_used[p.type_name] = True

        def _ProcessTypedef(typedef):
            for token in typedef.alias:
                if (isinstance(token, tokenize.Token) and
                    token.token_type == tokenize.NAME):
                    classes_used[token.name] = True
                elif isinstance(token, ast.Struct):
                    pass                # TODO(nnorwitz): impl
                elif isinstance(token, ast.Union):
                    pass                # TODO(nnorwitz): impl

        # TODO(nnorwitz): this needs to be recursive.
        classes_used = {}
        for node in self.ast_list:
            if isinstance(node, ast.VariableDeclaration):
                classes_used[node.type_name] = True
            elif isinstance(node, ast.Function):
                _ProcessFunction(node)
            elif isinstance(node, ast.Typedef):
                _ProcessTypedef(node)
            elif isinstance(node, ast.Class) and node.body:
                for node in node.body:
                    if (isinstance(node, ast.Function) and
                        not (node.modifiers & ast.FUNCTION_DTOR)):
                        _ProcessFunction(node)
                    if isinstance(node, ast.VariableDeclaration):
                        classes_used[node.type_name] = True
        return classes_used

    def _FindUnusedWarnings(self):
        # NOTE(nnorwitz): this could be sped up by iterating over the
        # file's AST and finding which symbols are used.  Then iterate
        # over each header file and see if any of the symbols are used.
        #
        # This is how this method should be implemented:
        # Read all the #includes and store them in parsed form.
        # Keep a dict of all public identifiers from each #include
        # Iterate through the source AST/tokens.
        # For each initial token (ignore ->tokens), find the header
        # that referenced it and mark in that header.  If no header, bitch.

        # Finally, iterate over all the headers.  For each one that
        # has no markings of being used, bitch.

        forward_declarations, included_files = self._GetForwardDeclarations()
        classes_used = self._GetClassesUsed()

        # Find all the forward declarations that are not used.
        for cls in forward_declarations:
            if cls not in classes_used:
                node = forward_declarations[cls]
                self._AddWarning('%r not used' % cls, node)

        # Find all the header files that are not used.
        for node, module in included_files.values():
            if _IsCppFile(module.filename):
                msg = ('should not include C++ source files: %s' %
                       module.filename)
                self._AddWarning(msg, node)
            if node.filename == self.filename:
                self._AddWarning('%s includes itself' % node.filename, node)
            if not module.IsAnyPublicSymbolUsed(self.ast_list):
                msg = '%s does not need to be #included' % node.filename
                self._AddWarning(msg, node)

    def _FindHeaderWarnings(self):
        self._FindUnusedWarnings()
        # TODO(nnorwitz): other warnings to add:
        #   * when a symbol is used, check if it is used as a pointer
        #     and can be forward declared rather than #include'ing the
        #     header file.  This only applies to header files until we
        #     track all variable accesses/derefs.
        #   * too much non-template impl in header file

    def _FindPublicFunctionWarnings(self, node, name, primary_header,
                                    public_symbols, all_headers):
        # Not found in the primary header, search all other headers.
        for header_node, header in all_headers.itervalues():
            if name in header.public_symbols:
                # If the primary.filename == header.filename, it probably
                # indicates an error elsewhere.  It sucks to mask it,
                # but false positives are worse.
                if (primary_header and
                    primary_header.filename != header.filename):
                    msg = ('expected to find %s in %s, but found in %s' %
                           (name, primary_header.filename, header.filename))
                    self._AddWarning(msg, node)
                break
        else:
            where = 'in any directly included header'
            if primary_header:
                where = ('in expected header ' + primary_header.filename +
                         ' or any other directly #included header')
            self._AddWarning('%s not found %s' % (name, where), node)

    def _CheckPublicFunctions(self, primary_header, all_headers):
        # Verify all the public functions are also declared in a header file.
        public_symbols = ()
        declared_only_symbols = {}
        if primary_header:
            public_symbols = {}
            for name, symbol in primary_header.public_symbols.iteritems():
                if isinstance(symbol, ast.Function):
                    public_symbols[name] = symbol
            declared_only_symbols = dict.fromkeys(public_symbols, True)

        using_values = []
        for node in self.ast_list:
            if isinstance(node, ast.Using):
                using_values.append(node)

            # Make sure we have a function that should be exported.
            if not isinstance(node, ast.Function):
                continue
            if isinstance(node, ast.Method):
                # Ensure that for Foo::Bar, Foo is *not* a namespace.
                # If Foo is a namespace, we have a function and not a method.
                names = [n.name for n in node.in_class]
                if names != self.symbol_table.GetNamespace(names):
                    continue
            if not (node.IsDefinition() and node.IsExportable()):
                continue

            # TODO(nnorwitz): need to handle using statements.

            # This function should be declared in a header file.
            name = node.name
            if name in public_symbols:
                declared_only_symbols[name] = False
            else:
                self._FindPublicFunctionWarnings(node, name, primary_header,
                                                 public_symbols, all_headers)

        for name, declared_only in declared_only_symbols.iteritems():
            if declared_only:
                # TODO(nnorwitz): shouldn't warn if function is templatized.
                node = public_symbols[name]
                msg = '%s declared but not defined' % name
                self._AddWarning(msg, node, primary_header.filename)

    def _GetPrimaryHeader(self, included_files):
        basename = os.path.splitext(self.filename)[0]
        primary_header = included_files.get(basename + '.h')
        if not primary_header:
            primary_header = included_files.get(basename)
        if primary_header:
            return primary_header[1]
        return None

    def _FindSourceWarnings(self):
        forward_declarations, included_files = self._GetForwardDeclarations()
        if forward_declarations:
            # TODO(nnorwitz): This really isn't a problem, but might
            # be something to warn against.  I expect this will either
            # be configurable or removed in the future.  But it's easy
            # to check for now.
            msg = 'forward declarations not expected in source file'
            self._AddWarning(msg, forward_declarations.itervalues().next())

        # A primary header is optional.  However, when looking up
        # defined methods in the source, always look in the
        # primary_header first.  Expect that is the most likely location.
        # Use of primary_header is primarily an optimization.
        primary_header = self._GetPrimaryHeader(included_files)
        if not primary_header:
            # TODO(nnorwitz): This shouldn't always be a warning.
            # For example, *main.cc shouldn't need a header.  But
            # almost all other source files should have a
            # corresponding header.
            msg = 'unable to find header file with matching name'
            self._AddWarning(msg, self.ast_list[0])

        self._CheckPublicFunctions(primary_header, included_files)

        # TODO(nnorwitz): other warnings to add:
        #   * unused forward decls for variables (globals)/classes
        #   * Functions that are too large/complex
        #   * Variables declared far from first use

def main(argv):
    for filename in argv[1:]:
        source = utils.ReadFile(filename)
        if source is None:
            continue

        print 'Processing', filename
        builder = ast.BuilderFromSource(source, filename)
        entire_ast = filter(None, builder.Generate())
        hunter = WarningHunter(filename, source, entire_ast)
        hunter.FindWarnings()
        hunter.ShowWarnings()

if __name__ == '__main__':
    main(sys.argv)
<|endoftext|>


<commit_before># Find sales prices of various (or all?) types of properties sold in a given
# period of time and prepare for plotting on a map with R
<commit_msg>Began writing the program<commit_after># Find sales prices of various (or all?) types of properties sold in a given
# period of time and prepare for plotting on a map with R

import requests, bs4, datetime

currentYear = datetime.datetime.now().year

while True:
    try:
        startYear = int(input("Starting year: "))
    except ValueError:
        # User did not enter a valid year
        print("Please enter a year between 1992 and " + str(currentYear))
        # Back to start
        continue
    if startYear < 1992 or startYear > currentYear:
        # There are no data before 1992 or for the future
        print("Please enter a year between 1992 and " + str(currentYear))
        # Back to start
        continue
    else:
        # Success
        break
<|endoftext|>


<commit_before>"""Group view methods"""
from flask import abort, Blueprint, jsonify
from flask import request
from flask_user import roles_required

from ..audit import auditable_event
from ..extensions import db, oauth
from ..models.group import Group
from ..models.role import ROLE
from ..models.user import current_user

group_api = Blueprint('group_api', __name__, url_prefix='/api/group')

@group_api.route('/')
@oauth.require_oauth()
def current_groups():
    """Returns simple JSON defining all current groups

    Returns a list of all known groups.  Groups are used to logically
    associate users, see `role` for access issues.

    ---
    tags:
      - Group
    operationId: current_groups
    produces:
      - application/json
    responses:
      200:
        description: Returns a list of all known groups.
        schema:
          id: groups
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, always a lower case string with no white space.
            description:
              type: string
              description: Plain text describing the group.
      401:
        description: if missing valid OAuth token

    """
    results = [g.as_json() for g in Group.query.all()]
    return jsonify(groups=results)

@group_api.route('/<string:group_name>')
@oauth.require_oauth()
def group_by_name(group_name):
    """Returns simple JSON for the requested group

    ---
    tags:
      - Group
    operationId: group_by_name
    parameters:
      - name: group_name
        in: path
        description: Group name
        required: true
        type: string
    produces:
      - application/json
    responses:
      200:
        description: Returns simple JSON for the named group
        schema:
          id: groups
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, always a lower case string with no white space.
            description:
              type: string
              description: Plain text describing the group.
      401:
        description: if missing valid OAuth token
      404:
        description: if named group doesn't exist

    """
    g = Group.query.filter_by(name=group_name).first()
    if not g:
        abort(404, "Group {n} not found".format(n=group_name))
    return jsonify(group=g.as_json())

@group_api.route('/', methods=('POST',))
@roles_required(ROLE.ADMIN)
def add_group():
    """Add new group

    POST simple JSON defining new group.

    ---
    tags:
      - Group
    operationId: add_group
    produces:
      - application/json
    parameters:
      - in: body
        name: body
        schema:
          id: group_definition
          description: Group details including name and description
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, limited to lower case letters and underscores.
            description:
              type: string
              description:
                Plain text detailing the groups use or members.
    responses:
      200:
        description: successful operation
        schema:
          id: response
          required:
            - message
          properties:
            message:
              type: string
              description: Result, typically "ok"
      400:
        description:
          if input isn't valid or if matching group name already exists
      401:
        description: if missing valid OAuth token

    """
    if not (request.json and 'name' in request.json and 'description' in
            request.json):
        abort(400,
              "Requires valid JSON including a group name and description")
    name = request.json['name']
    match = Group.query.filter_by(name=name).first()
    if match:
        abort(400,
              "{name} already exists, can't redefine".format(name=name))

    name = Group.validate_name(name)
    g = Group(name=name, description=request.json['description'])
    db.session.add(g)
    db.session.commit()
    auditable_event("{g} added".format(g=g), user_id=current_user().id)
    return jsonify(message="ok")
<commit_msg>Allow service users to POST to /api/group/<commit_after>"""Group view methods"""
from flask import abort, Blueprint, jsonify
from flask import request
from flask_user import roles_required

from ..audit import auditable_event
from ..extensions import db, oauth
from ..models.group import Group
from ..models.role import ROLE
from ..models.user import current_user

group_api = Blueprint('group_api', __name__, url_prefix='/api/group')

@group_api.route('/')
@oauth.require_oauth()
def current_groups():
    """Returns simple JSON defining all current groups

    Returns a list of all known groups.  Groups are used to logically
    associate users, see `role` for access issues.

    ---
    tags:
      - Group
    operationId: current_groups
    produces:
      - application/json
    responses:
      200:
        description: Returns a list of all known groups.
        schema:
          id: groups
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, always a lower case string with no white space.
            description:
              type: string
              description: Plain text describing the group.
      401:
        description: if missing valid OAuth token

    """
    results = [g.as_json() for g in Group.query.all()]
    return jsonify(groups=results)

@group_api.route('/<string:group_name>')
@oauth.require_oauth()
def group_by_name(group_name):
    """Returns simple JSON for the requested group

    ---
    tags:
      - Group
    operationId: group_by_name
    parameters:
      - name: group_name
        in: path
        description: Group name
        required: true
        type: string
    produces:
      - application/json
    responses:
      200:
        description: Returns simple JSON for the named group
        schema:
          id: groups
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, always a lower case string with no white space.
            description:
              type: string
              description: Plain text describing the group.
      401:
        description: if missing valid OAuth token
      404:
        description: if named group doesn't exist

    """
    g = Group.query.filter_by(name=group_name).first()
    if not g:
        abort(404, "Group {n} not found".format(n=group_name))
    return jsonify(group=g.as_json())

@group_api.route('/', methods=('POST',))
@roles_required([ROLE.ADMIN, ROLE.SERVICE])
def add_group():
    """Add new group

    POST simple JSON defining new group.

    ---
    tags:
      - Group
    operationId: add_group
    produces:
      - application/json
    parameters:
      - in: body
        name: body
        schema:
          id: group_definition
          description: Group details including name and description
          required:
            - name
            - description
          properties:
            name:
              type: string
              description:
                Group name, limited to lower case letters and underscores.
            description:
              type: string
              description:
                Plain text detailing the groups use or members.
    responses:
      200:
        description: successful operation
        schema:
          id: response
          required:
            - message
          properties:
            message:
              type: string
              description: Result, typically "ok"
      400:
        description:
          if input isn't valid or if matching group name already exists
      401:
        description: if missing valid OAuth token

    """
    if not (request.json and 'name' in request.json and 'description' in
            request.json):
        abort(400,
              "Requires valid JSON including a group name and description")
    name = request.json['name']
    match = Group.query.filter_by(name=name).first()
    if match:
        abort(400,
              "{name} already exists, can't redefine".format(name=name))

    name = Group.validate_name(name)
    g = Group(name=name, description=request.json['description'])
    db.session.add(g)
    db.session.commit()
    auditable_event("{g} added".format(g=g), user_id=current_user().id)
    return jsonify(message="ok")
<|endoftext|>


<commit_before>"""Support for Harmony Hub devices."""
import asyncio
import json
import logging

from aioharmony.const import ClientCallbackType
import aioharmony.exceptions as aioexc
from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient, SendCommandDevice
import voluptuous as vol

from homeassistant.components import remote
from homeassistant.components.remote import (
    ATTR_ACTIVITY,
    ATTR_DELAY_SECS,
    ATTR_DEVICE,
    ATTR_HOLD_SECS,
    ATTR_NUM_REPEATS,
    DEFAULT_DELAY_SECS,
    PLATFORM_SCHEMA,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity

from .const import (
    ACTIVITY_POWER_OFF,
    ATTR_ACTIVITY_LIST,
    ATTR_CURRENT_ACTIVITY,
    ATTR_DEVICES_LIST,
    ATTR_LAST_ACTIVITY,
    DOMAIN,
    HARMONY_OPTIONS_UPDATE,
    PREVIOUS_ACTIVE_ACTIVITY,
    SERVICE_CHANGE_CHANNEL,
    SERVICE_SYNC,
    UNIQUE_ID,
)
from .util import (
    find_best_name_for_remote,
    find_matching_config_entries_for_host,
    find_unique_id_for_remote,
    get_harmony_client_if_available,
    list_names_from_hublist,
)

_LOGGER = logging.getLogger(__name__)

# We want to fire remote commands right away
PARALLEL_UPDATES = 0

ATTR_CHANNEL = "channel"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Optional(ATTR_ACTIVITY): cv.string,
        vol.Required(CONF_NAME): cv.string,
        vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float),
        vol.Required(CONF_HOST): cv.string,
        # The client ignores port so lets not confuse the user by pretenting we do anything with this
    },
    extra=vol.ALLOW_EXTRA,
)

HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})

HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
        vol.Required(ATTR_CHANNEL): cv.positive_int,
    }
)

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Harmony platform."""

    if discovery_info:
        # Now handled by ssdp in the config flow
        return

    if find_matching_config_entries_for_host(hass, config[CONF_HOST]):
        return

    # We do the validation to verify we can connect
    # so we can raise PlatformNotReady to force
    # a retry so we can avoid a scenario where the config
    # entry cannot be created via import because hub
    # is not yet ready.
    harmony = await get_harmony_client_if_available(config[CONF_HOST])
    if not harmony:
        raise PlatformNotReady

    validated_config = config.copy()
    validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony)
    validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony)

    hass.async_create_task(
        hass.config_entries.flow.async_init(
            DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config
        )
    )

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
    """Set up the Harmony config entry."""

    device = hass.data[DOMAIN][entry.entry_id]

    _LOGGER.debug("Harmony Remote: %s", device)

    async_add_entities([device])

    platform = entity_platform.current_platform.get()

    platform.async_register_entity_service(
        SERVICE_SYNC, HARMONY_SYNC_SCHEMA, "sync",
    )
    platform.async_register_entity_service(
        SERVICE_CHANGE_CHANNEL, HARMONY_CHANGE_CHANNEL_SCHEMA, "change_channel"
    )

class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
    """Remote representation used to control a Harmony device."""

    def __init__(self, name, unique_id, host, activity, out_path, delay_secs):
        """Initialize HarmonyRemote class."""
        self._name = name
        self.host = host
        self._state = None
        self._current_activity = None
        self.default_activity = activity
        self._client = HarmonyClient(ip_address=host)
        self._config_path = out_path
        self.delay_secs = delay_secs
        self._available = False
        self._unique_id = unique_id
        self._last_activity = None

    @property
    def activity_names(self):
        """Names of all the remotes activities."""
        activities = [activity["label"] for activity in self._client.config["activity"]]

        # Remove both ways of representing PowerOff
        if None in activities:
            activities.remove(None)
        if ACTIVITY_POWER_OFF in activities:
            activities.remove(ACTIVITY_POWER_OFF)

        return activities

    async def _async_update_options(self, data):
        """Change options when the options flow does."""
        if ATTR_DELAY_SECS in data:
            self.delay_secs = data[ATTR_DELAY_SECS]

        if ATTR_ACTIVITY in data:
            self.default_activity = data[ATTR_ACTIVITY]

    def _update_callbacks(self):
        callbacks = {
            "config_updated": self.new_config,
            "connect": self.got_connected,
            "disconnect": self.got_disconnected,
            "new_activity_starting": self.new_activity,
            "new_activity": None,
        }
        self._client.callbacks = ClientCallbackType(**callbacks)

    async def async_added_to_hass(self):
        """Complete the initialization."""
        await super().async_added_to_hass()

        _LOGGER.debug("%s: Harmony Hub added", self._name)
        # Register the callbacks
        self._update_callbacks()

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{HARMONY_OPTIONS_UPDATE}-{self.unique_id}",
                self._async_update_options,
            )
        )

        # Store Harmony HUB config, this will also update our current
        # activity
        await self.new_config()

        # Restore the last activity so we know
        # how what to turn on if nothing
        # is specified
        last_state = await self.async_get_last_state()
        if not last_state:
            return
        if ATTR_LAST_ACTIVITY not in last_state.attributes:
            return
        if self.is_on:
            return

        self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY]

    async def shutdown(self):
        """Close connection on shutdown."""
        _LOGGER.debug("%s: Closing Harmony Hub", self._name)
        try:
            await self._client.close()
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Disconnect timed-out", self._name)

    @property
    def device_info(self):
        """Return device info."""
        model = "Harmony Hub"
        if "ethernetStatus" in self._client.hub_config.info:
            model = "Harmony Hub Pro 2400"
        return {
            "identifiers": {(DOMAIN, self.unique_id)},
            "manufacturer": "Logitech",
            "sw_version": self._client.hub_config.info.get(
                "hubSwVersion", self._client.fw_version
            ),
            "name": self.name,
            "model": model,
        }

    @property
    def unique_id(self):
        """Return the unique id."""
        return self._unique_id

    @property
    def name(self):
        """Return the Harmony device's name."""
        return self._name

    @property
    def should_poll(self):
        """Return the fact that we should not be polled."""
        return False

    @property
    def device_state_attributes(self):
        """Add platform specific attributes."""
        return {
            ATTR_CURRENT_ACTIVITY: self._current_activity,
            ATTR_ACTIVITY_LIST: list_names_from_hublist(
                self._client.hub_config.activities
            ),
            ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices),
            ATTR_LAST_ACTIVITY: self._last_activity,
        }

    @property
    def is_on(self):
        """Return False if PowerOff is the current activity, otherwise True."""
        return self._current_activity not in [None, "PowerOff"]

    @property
    def available(self):
        """Return True if connected to Hub, otherwise False."""
        return self._available

    async def connect(self):
        """Connect to the Harmony HUB."""
        _LOGGER.debug("%s: Connecting", self._name)
        try:
            if not await self._client.connect():
                _LOGGER.warning("%s: Unable to connect to HUB", self._name)
                await self._client.close()
                return False
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Connection timed-out", self._name)
            return False
        return True

    def new_activity(self, activity_info: tuple) -> None:
        """Call for updating the current activity."""
        activity_id, activity_name = activity_info
        _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name)
        self._current_activity = activity_name
        if activity_id != -1:
            # Save the activity so we can restore
            # to that activity if none is specified
            # when turning on
            self._last_activity = activity_name
        self._state = bool(activity_id != -1)
        self._available = True
        self.async_write_ha_state()

    async def new_config(self, _=None):
        """Call for updating the current activity."""
        _LOGGER.debug("%s: configuration has been updated", self._name)
        self.new_activity(self._client.current_activity)
        await self.hass.async_add_executor_job(self.write_config_file)

    async def got_connected(self, _=None):
        """Notification that we're connected to the HUB."""
        _LOGGER.debug("%s: connected to the HUB", self._name)
        if not self._available:
            # We were disconnected before.
            await self.new_config()

    async def got_disconnected(self, _=None):
        """Notification that we're disconnected from the HUB."""
        _LOGGER.debug("%s: disconnected from the HUB", self._name)
        self._available = False
        # We're going to wait for 10 seconds before announcing we're
        # unavailable, this to allow a reconnection to happen.
        await asyncio.sleep(10)

        if not self._available:
            # Still disconnected. Let the state engine know.
            self.async_write_ha_state()

    async def async_turn_on(self, **kwargs):
        """Start an activity from the Harmony device."""
        _LOGGER.debug("%s: Turn On", self.name)

        activity = kwargs.get(ATTR_ACTIVITY, self.default_activity)

        if not activity or activity == PREVIOUS_ACTIVE_ACTIVITY:
            if self._last_activity:
                activity = self._last_activity
            else:
                all_activities = list_names_from_hublist(
                    self._client.hub_config.activities
                )
                if all_activities:
                    activity = all_activities[0]

        if activity:
            activity_id = None
            if activity.isdigit() or activity == "-1":
                _LOGGER.debug("%s: Activity is numeric", self.name)
                if self._client.get_activity_name(int(activity)):
                    activity_id = activity

            if activity_id is None:
                _LOGGER.debug("%s: Find activity ID based on name", self.name)
                activity_id = self._client.get_activity_id(str(activity))

            if activity_id is None:
                _LOGGER.error("%s: Activity %s is invalid", self.name, activity)
                return

            try:
                await self._client.start_activity(activity_id)
            except aioexc.TimeOut:
                _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
        else:
            _LOGGER.error("%s: No activity specified with turn_on service", self.name)

    async def async_turn_off(self, **kwargs):
        """Start the PowerOff activity."""
        _LOGGER.debug("%s: Turn Off", self.name)
        try:
            await self._client.power_off()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Powering off timed-out", self.name)

    async def async_send_command(self, command, **kwargs):
        """Send a list of commands to one device."""
        _LOGGER.debug("%s: Send Command", self.name)
        device = kwargs.get(ATTR_DEVICE)
        if device is None:
            _LOGGER.error("%s: Missing required argument: device", self.name)
            return

        device_id = None
        if device.isdigit():
            _LOGGER.debug("%s: Device %s is numeric", self.name, device)
            if self._client.get_device_name(int(device)):
                device_id = device

        if device_id is None:
            _LOGGER.debug(
                "%s: Find device ID %s based on device name", self.name, device
            )
            device_id = self._client.get_device_id(str(device).strip())

        if device_id is None:
            _LOGGER.error("%s: Device %s is invalid", self.name, device)
            return

        num_repeats = kwargs[ATTR_NUM_REPEATS]
        delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs)
        hold_secs = kwargs[ATTR_HOLD_SECS]
        _LOGGER.debug(
            "Sending commands to device %s holding for %s seconds "
            "with a delay of %s seconds",
            device,
            hold_secs,
            delay_secs,
        )

        # Creating list of commands to send.
        snd_cmnd_list = []
        for _ in range(num_repeats):
            for single_command in command:
                send_command = SendCommandDevice(
                    device=device_id, command=single_command, delay=hold_secs
                )
                snd_cmnd_list.append(send_command)
                if delay_secs > 0:
                    snd_cmnd_list.append(float(delay_secs))

        _LOGGER.debug("%s: Sending commands", self.name)
        try:
            result_list = await self._client.send_commands(snd_cmnd_list)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Sending commands timed-out", self.name)
            return

        for result in result_list:
            _LOGGER.error(
                "Sending command %s to device %s failed with code %s: %s",
                result.command.command,
                result.command.device,
                result.code,
                result.msg,
            )

    async def change_channel(self, channel):
        """Change the channel using Harmony remote."""
        _LOGGER.debug("%s: Changing channel to %s", self.name, channel)
        try:
            await self._client.change_channel(channel)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel)

    async def sync(self):
        """Sync the Harmony device with the web service."""
        _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
        try:
            await self._client.sync()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name)
        else:
            await self.hass.async_add_executor_job(self.write_config_file)

    def write_config_file(self):
        """Write Harmony configuration file."""
        _LOGGER.debug(
            "%s: Writing hub configuration to file: %s", self.name, self._config_path
        )
        if self._client.config is None:
            _LOGGER.warning("%s: No configuration received from hub", self.name)
            return

        try:
            with open(self._config_path, "w+", encoding="utf-8") as file_out:
                json.dump(self._client.json_config, file_out, sort_keys=True, indent=4)
        except OSError as exc:
            _LOGGER.error(
                "%s: Unable to write HUB configuration to %s: %s",
                self.name,
                self._config_path,
                exc,
            )
<commit_msg>Prevent harmony from resetting state with multiple turn ons (#38183)<commit_after>"""Support for Harmony Hub devices."""
import asyncio
import json
import logging

from aioharmony.const import ClientCallbackType
import aioharmony.exceptions as aioexc
from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient, SendCommandDevice
import voluptuous as vol

from homeassistant.components import remote
from homeassistant.components.remote import (
    ATTR_ACTIVITY,
    ATTR_DELAY_SECS,
    ATTR_DEVICE,
    ATTR_HOLD_SECS,
    ATTR_NUM_REPEATS,
    DEFAULT_DELAY_SECS,
    PLATFORM_SCHEMA,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity

from .const import (
    ACTIVITY_POWER_OFF,
    ATTR_ACTIVITY_LIST,
    ATTR_CURRENT_ACTIVITY,
    ATTR_DEVICES_LIST,
    ATTR_LAST_ACTIVITY,
    DOMAIN,
    HARMONY_OPTIONS_UPDATE,
    PREVIOUS_ACTIVE_ACTIVITY,
    SERVICE_CHANGE_CHANNEL,
    SERVICE_SYNC,
    UNIQUE_ID,
)
from .util import (
    find_best_name_for_remote,
    find_matching_config_entries_for_host,
    find_unique_id_for_remote,
    get_harmony_client_if_available,
    list_names_from_hublist,
)

_LOGGER = logging.getLogger(__name__)

# We want to fire remote commands right away
PARALLEL_UPDATES = 0

ATTR_CHANNEL = "channel"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Optional(ATTR_ACTIVITY): cv.string,
        vol.Required(CONF_NAME): cv.string,
        vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float),
        vol.Required(CONF_HOST): cv.string,
        # The client ignores port so lets not confuse the user by pretenting we do anything with this
    },
    extra=vol.ALLOW_EXTRA,
)

HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})

HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
        vol.Required(ATTR_CHANNEL): cv.positive_int,
    }
)

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Harmony platform."""

    if discovery_info:
        # Now handled by ssdp in the config flow
        return

    if find_matching_config_entries_for_host(hass, config[CONF_HOST]):
        return

    # We do the validation to verify we can connect
    # so we can raise PlatformNotReady to force
    # a retry so we can avoid a scenario where the config
    # entry cannot be created via import because hub
    # is not yet ready.
    harmony = await get_harmony_client_if_available(config[CONF_HOST])
    if not harmony:
        raise PlatformNotReady

    validated_config = config.copy()
    validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony)
    validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony)

    hass.async_create_task(
        hass.config_entries.flow.async_init(
            DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config
        )
    )

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
    """Set up the Harmony config entry."""

    device = hass.data[DOMAIN][entry.entry_id]

    _LOGGER.debug("Harmony Remote: %s", device)

    async_add_entities([device])

    platform = entity_platform.current_platform.get()

    platform.async_register_entity_service(
        SERVICE_SYNC, HARMONY_SYNC_SCHEMA, "sync",
    )
    platform.async_register_entity_service(
        SERVICE_CHANGE_CHANNEL, HARMONY_CHANGE_CHANNEL_SCHEMA, "change_channel"
    )

class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
    """Remote representation used to control a Harmony device."""

    def __init__(self, name, unique_id, host, activity, out_path, delay_secs):
        """Initialize HarmonyRemote class."""
        self._name = name
        self.host = host
        self._state = None
        self._current_activity = ACTIVITY_POWER_OFF
        self.default_activity = activity
        self._client = HarmonyClient(ip_address=host)
        self._config_path = out_path
        self.delay_secs = delay_secs
        self._available = False
        self._unique_id = unique_id
        self._last_activity = None

    @property
    def activity_names(self):
        """Names of all the remotes activities."""
        activities = [activity["label"] for activity in self._client.config["activity"]]

        # Remove both ways of representing PowerOff
        if None in activities:
            activities.remove(None)
        if ACTIVITY_POWER_OFF in activities:
            activities.remove(ACTIVITY_POWER_OFF)

        return activities

    async def _async_update_options(self, data):
        """Change options when the options flow does."""
        if ATTR_DELAY_SECS in data:
            self.delay_secs = data[ATTR_DELAY_SECS]

        if ATTR_ACTIVITY in data:
            self.default_activity = data[ATTR_ACTIVITY]

    def _update_callbacks(self):
        callbacks = {
            "config_updated": self.new_config,
            "connect": self.got_connected,
            "disconnect": self.got_disconnected,
            "new_activity_starting": self.new_activity,
            "new_activity": None,
        }
        self._client.callbacks = ClientCallbackType(**callbacks)

    async def async_added_to_hass(self):
        """Complete the initialization."""
        await super().async_added_to_hass()

        _LOGGER.debug("%s: Harmony Hub added", self._name)
        # Register the callbacks
        self._update_callbacks()

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{HARMONY_OPTIONS_UPDATE}-{self.unique_id}",
                self._async_update_options,
            )
        )

        # Store Harmony HUB config, this will also update our current
        # activity
        await self.new_config()

        # Restore the last activity so we know
        # how what to turn on if nothing
        # is specified
        last_state = await self.async_get_last_state()
        if not last_state:
            return
        if ATTR_LAST_ACTIVITY not in last_state.attributes:
            return
        if self.is_on:
            return

        self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY]

    async def shutdown(self):
        """Close connection on shutdown."""
        _LOGGER.debug("%s: Closing Harmony Hub", self._name)
        try:
            await self._client.close()
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Disconnect timed-out", self._name)

    @property
    def device_info(self):
        """Return device info."""
        model = "Harmony Hub"
        if "ethernetStatus" in self._client.hub_config.info:
            model = "Harmony Hub Pro 2400"
        return {
            "identifiers": {(DOMAIN, self.unique_id)},
            "manufacturer": "Logitech",
            "sw_version": self._client.hub_config.info.get(
                "hubSwVersion", self._client.fw_version
            ),
            "name": self.name,
            "model": model,
        }

    @property
    def unique_id(self):
        """Return the unique id."""
        return self._unique_id

    @property
    def name(self):
        """Return the Harmony device's name."""
        return self._name

    @property
    def should_poll(self):
        """Return the fact that we should not be polled."""
        return False

    @property
    def device_state_attributes(self):
        """Add platform specific attributes."""
        return {
            ATTR_CURRENT_ACTIVITY: self._current_activity,
            ATTR_ACTIVITY_LIST: list_names_from_hublist(
                self._client.hub_config.activities
            ),
            ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices),
            ATTR_LAST_ACTIVITY: self._last_activity,
        }

    @property
    def is_on(self):
        """Return False if PowerOff is the current activity, otherwise True."""
        return self._current_activity not in [None, "PowerOff"]

    @property
    def available(self):
        """Return True if connected to Hub, otherwise False."""
        return self._available

    async def connect(self):
        """Connect to the Harmony HUB."""
        _LOGGER.debug("%s: Connecting", self._name)
        try:
            if not await self._client.connect():
                _LOGGER.warning("%s: Unable to connect to HUB", self._name)
                await self._client.close()
                return False
        except aioexc.TimeOut:
            _LOGGER.warning("%s: Connection timed-out", self._name)
            return False
        return True

    def new_activity(self, activity_info: tuple) -> None:
        """Call for updating the current activity."""
        activity_id, activity_name = activity_info
        _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name)
        self._current_activity = activity_name
        if activity_id != -1:
            # Save the activity so we can restore
            # to that activity if none is specified
            # when turning on
            self._last_activity = activity_name
        self._state = bool(activity_id != -1)
        self._available = True
        self.async_write_ha_state()

    async def new_config(self, _=None):
        """Call for updating the current activity."""
        _LOGGER.debug("%s: configuration has been updated", self._name)
        self.new_activity(self._client.current_activity)
        await self.hass.async_add_executor_job(self.write_config_file)

    async def got_connected(self, _=None):
        """Notification that we're connected to the HUB."""
        _LOGGER.debug("%s: connected to the HUB", self._name)
        if not self._available:
            # We were disconnected before.
            await self.new_config()

    async def got_disconnected(self, _=None):
        """Notification that we're disconnected from the HUB."""
        _LOGGER.debug("%s: disconnected from the HUB", self._name)
        self._available = False
        # We're going to wait for 10 seconds before announcing we're
        # unavailable, this to allow a reconnection to happen.
        await asyncio.sleep(10)

        if not self._available:
            # Still disconnected. Let the state engine know.
            self.async_write_ha_state()

    async def async_turn_on(self, **kwargs):
        """Start an activity from the Harmony device."""
        _LOGGER.debug("%s: Turn On", self.name)

        activity = kwargs.get(ATTR_ACTIVITY, self.default_activity)

        if not activity or activity == PREVIOUS_ACTIVE_ACTIVITY:
            if self._last_activity:
                activity = self._last_activity
            else:
                all_activities = list_names_from_hublist(
                    self._client.hub_config.activities
                )
                if all_activities:
                    activity = all_activities[0]

        if activity:
            activity_id = None
            activity_name = None

            if activity.isdigit() or activity == "-1":
                _LOGGER.debug("%s: Activity is numeric", self.name)
                activity_name = self._client.get_activity_name(int(activity))
                if activity_name:
                    activity_id = activity

            if activity_id is None:
                _LOGGER.debug("%s: Find activity ID based on name", self.name)
                activity_name = str(activity)
                activity_id = self._client.get_activity_id(activity_name)

            if activity_id is None:
                _LOGGER.error("%s: Activity %s is invalid", self.name, activity)
                return

            if self._current_activity == activity_name:
                # Automations or HomeKit may turn the device on multiple times
                # when the current activity is already active which will cause
                # harmony to loose state.  This behavior is unexpected as turning
                # the device on when its already on isn't expected to reset state.
                _LOGGER.debug(
                    "%s: Current activity is already %s", self.name, activity_name
                )
                return

            try:
                await self._client.start_activity(activity_id)
            except aioexc.TimeOut:
                _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
        else:
            _LOGGER.error("%s: No activity specified with turn_on service", self.name)

    async def async_turn_off(self, **kwargs):
        """Start the PowerOff activity."""
        _LOGGER.debug("%s: Turn Off", self.name)
        try:
            await self._client.power_off()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Powering off timed-out", self.name)

    async def async_send_command(self, command, **kwargs):
        """Send a list of commands to one device."""
        _LOGGER.debug("%s: Send Command", self.name)
        device = kwargs.get(ATTR_DEVICE)
        if device is None:
            _LOGGER.error("%s: Missing required argument: device", self.name)
            return

        device_id = None
        if device.isdigit():
            _LOGGER.debug("%s: Device %s is numeric", self.name, device)
            if self._client.get_device_name(int(device)):
                device_id = device

        if device_id is None:
            _LOGGER.debug(
                "%s: Find device ID %s based on device name", self.name, device
            )
            device_id = self._client.get_device_id(str(device).strip())

        if device_id is None:
            _LOGGER.error("%s: Device %s is invalid", self.name, device)
            return

        num_repeats = kwargs[ATTR_NUM_REPEATS]
        delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs)
        hold_secs = kwargs[ATTR_HOLD_SECS]
        _LOGGER.debug(
            "Sending commands to device %s holding for %s seconds "
            "with a delay of %s seconds",
            device,
            hold_secs,
            delay_secs,
        )

        # Creating list of commands to send.
        snd_cmnd_list = []
        for _ in range(num_repeats):
            for single_command in command:
                send_command = SendCommandDevice(
                    device=device_id, command=single_command, delay=hold_secs
                )
                snd_cmnd_list.append(send_command)
                if delay_secs > 0:
                    snd_cmnd_list.append(float(delay_secs))

        _LOGGER.debug("%s: Sending commands", self.name)
        try:
            result_list = await self._client.send_commands(snd_cmnd_list)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Sending commands timed-out", self.name)
            return

        for result in result_list:
            _LOGGER.error(
                "Sending command %s to device %s failed with code %s: %s",
                result.command.command,
                result.command.device,
                result.code,
                result.msg,
            )

    async def change_channel(self, channel):
        """Change the channel using Harmony remote."""
        _LOGGER.debug("%s: Changing channel to %s", self.name, channel)
        try:
            await self._client.change_channel(channel)
        except aioexc.TimeOut:
            _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel)

    async def sync(self):
        """Sync the Harmony device with the web service."""
        _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
        try:
            await self._client.sync()
        except aioexc.TimeOut:
            _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name)
        else:
            await self.hass.async_add_executor_job(self.write_config_file)

    def write_config_file(self):
        """Write Harmony configuration file."""
        _LOGGER.debug(
            "%s: Writing hub configuration to file: %s", self.name, self._config_path
        )
        if self._client.config is None:
            _LOGGER.warning("%s: No configuration received from hub", self.name)
            return

        try:
            with open(self._config_path, "w+", encoding="utf-8") as file_out:
                json.dump(self._client.json_config, file_out, sort_keys=True, indent=4)
        except OSError as exc:
            _LOGGER.error(
                "%s: Unable to write HUB configuration to %s: %s",
                self.name,
                self._config_path,
                exc,
            )
<|endoftext|>